Style anpassungen für die Dokumentationsseite und die Produktseite. Hinzufügen eines Feeds für Produktaktualisierungen. Aktualisierung der Router- und Suchcontroller-Logik, um die neuen Seiten zu unterstützen. Anpassung der Admin-Einstellungen für die Dokumentationsseite.
This commit is contained in:
@@ -38,6 +38,22 @@ final class SettingsPage
|
||||
$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']) {
|
||||
@@ -57,6 +73,11 @@ final class SettingsPage
|
||||
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();
|
||||
?>
|
||||
<div class="wrap">
|
||||
@@ -113,6 +134,116 @@ final class SettingsPage
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h2><?php esc_html_e('Dokumentations-Startseite', 'kb-markdown-importer'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row"><label for="docs_home_intro_title"><?php esc_html_e('Anleitungs-Titel', 'kb-markdown-importer'); ?></label></th>
|
||||
<td><input class="regular-text" id="docs_home_intro_title" name="kb_markdown_importer_settings[docs_home_intro_title]" type="text" value="<?php echo esc_attr($settings['docs_home_intro_title']); ?>"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="docs_home_intro_content"><?php esc_html_e('Anleitungstext', 'kb-markdown-importer'); ?></label></th>
|
||||
<td>
|
||||
<?php
|
||||
wp_editor((string) $settings['docs_home_intro_content'], 'docs_home_intro_content', [
|
||||
'textarea_name' => 'kb_markdown_importer_settings[docs_home_intro_content]',
|
||||
'textarea_rows' => 7,
|
||||
'media_buttons' => false,
|
||||
'teeny' => true,
|
||||
]);
|
||||
?>
|
||||
<p class="description"><?php esc_html_e('Dieser Text erscheint oben auf der Dokumentations-Startseite.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_source"><?php esc_html_e('Update-Quelle', 'kb-markdown-importer'); ?></label></th>
|
||||
<td>
|
||||
<select id="product_updates_source" name="kb_markdown_importer_settings[product_updates_source]">
|
||||
<option value="rss" <?php selected($settings['product_updates_source'], 'rss'); ?>><?php esc_html_e('RSS/XML', 'kb-markdown-importer'); ?></option>
|
||||
<option value="rest" <?php selected($settings['product_updates_source'], 'rest'); ?>><?php esc_html_e('REST/JSON', 'kb-markdown-importer'); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_feed_url"><?php esc_html_e('RSS/XML-Feed URL', 'kb-markdown-importer'); ?></label></th>
|
||||
<td>
|
||||
<input class="regular-text" id="product_updates_feed_url" name="kb_markdown_importer_settings[product_updates_feed_url]" type="url" value="<?php echo esc_attr($settings['product_updates_feed_url']); ?>" placeholder="https://example.com/updates.xml">
|
||||
<p class="description"><?php esc_html_e('RSS- oder XML-Feed mit den neuesten Produktupdates. Wird nur genutzt, wenn RSS/XML als Quelle ausgewählt ist.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_rest_url"><?php esc_html_e('REST/JSON URL', 'kb-markdown-importer'); ?></label></th>
|
||||
<td>
|
||||
<input class="regular-text" id="product_updates_rest_url" name="kb_markdown_importer_settings[product_updates_rest_url]" type="url" value="<?php echo esc_attr($settings['product_updates_rest_url']); ?>" placeholder="https://example.com/api/product-versions">
|
||||
<p class="description"><?php esc_html_e('REST-Endpunkt mit JSON-Antwort. Wird nur genutzt, wenn REST/JSON als Quelle ausgewählt ist.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_feed_limit"><?php esc_html_e('Anzahl Updates', 'kb-markdown-importer'); ?></label></th>
|
||||
<td><input id="product_updates_feed_limit" name="kb_markdown_importer_settings[product_updates_feed_limit]" type="number" min="1" max="20" value="<?php echo esc_attr($settings['product_updates_feed_limit']); ?>"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_feed_item_path"><?php esc_html_e('Eintrag-Pfad', 'kb-markdown-importer'); ?></label></th>
|
||||
<td>
|
||||
<input class="regular-text" id="product_updates_feed_item_path" name="kb_markdown_importer_settings[product_updates_feed_item_path]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_item_path']); ?>" placeholder="channel/item">
|
||||
<p class="description"><?php esc_html_e('Pfad zum wiederholten Feed-Eintrag, zum Beispiel channel/item.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="product_updates_rest_list_path"><?php esc_html_e('REST Listenpfad', 'kb-markdown-importer'); ?></label></th>
|
||||
<td>
|
||||
<input class="regular-text" id="product_updates_rest_list_path" name="kb_markdown_importer_settings[product_updates_rest_list_path]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_list_path']); ?>" placeholder="content,data,items">
|
||||
<p class="description"><?php esc_html_e('Pfad zur Liste in der JSON-Antwort. Mehrere Alternativen mit Komma trennen. Leer lassen, wenn die Antwort direkt ein Array ist.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e('XML-Felder', 'kb-markdown-importer'); ?></th>
|
||||
<td>
|
||||
<fieldset class="kb-feed-fields">
|
||||
<p>
|
||||
<label for="product_updates_feed_product_field"><?php esc_html_e('Produktname', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_feed_product_field" name="kb_markdown_importer_settings[product_updates_feed_product_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_product_field']); ?>" placeholder="title">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_feed_version_field"><?php esc_html_e('Version', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_feed_version_field" name="kb_markdown_importer_settings[product_updates_feed_version_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_version_field']); ?>" placeholder="category">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_feed_date_field"><?php esc_html_e('Datum', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_feed_date_field" name="kb_markdown_importer_settings[product_updates_feed_date_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_date_field']); ?>" placeholder="pubDate">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_feed_changelog_field"><?php esc_html_e('Changelog', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_feed_changelog_field" name="kb_markdown_importer_settings[product_updates_feed_changelog_field]" type="text" value="<?php echo esc_attr($settings['product_updates_feed_changelog_field']); ?>" placeholder="description">
|
||||
</p>
|
||||
</fieldset>
|
||||
<p class="description"><?php esc_html_e('Feldpfade relativ zum Eintrag, zum Beispiel product/name, version oder changelog. Namespaces wie dc:date werden unterstützt.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e('REST-Felder', 'kb-markdown-importer'); ?></th>
|
||||
<td>
|
||||
<fieldset class="kb-rest-fields">
|
||||
<p>
|
||||
<label for="product_updates_rest_product_field"><?php esc_html_e('Produktname', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_rest_product_field" name="kb_markdown_importer_settings[product_updates_rest_product_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_product_field']); ?>" placeholder="product.name,productName,name">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_rest_version_field"><?php esc_html_e('Version', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_rest_version_field" name="kb_markdown_importer_settings[product_updates_rest_version_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_version_field']); ?>" placeholder="version,versionName,name">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_rest_date_field"><?php esc_html_e('Datum', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_rest_date_field" name="kb_markdown_importer_settings[product_updates_rest_date_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_date_field']); ?>" placeholder="releaseDate,date,updatedAt,createdAt">
|
||||
</p>
|
||||
<p>
|
||||
<label for="product_updates_rest_changelog_field"><?php esc_html_e('Changelog', 'kb-markdown-importer'); ?></label><br>
|
||||
<input class="regular-text" id="product_updates_rest_changelog_field" name="kb_markdown_importer_settings[product_updates_rest_changelog_field]" type="text" value="<?php echo esc_attr($settings['product_updates_rest_changelog_field']); ?>" placeholder="changelog,changeLog,description,changes">
|
||||
</p>
|
||||
</fieldset>
|
||||
<p class="description"><?php esc_html_e('JSON-Feldpfade relativ zu einem Eintrag. Verschachtelte Felder mit Punkt oder Slash angeben, Alternativen mit Komma trennen.', 'kb-markdown-importer'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h2><?php esc_html_e('Frontend Design', 'kb-markdown-importer'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
@@ -154,6 +285,21 @@ final class SettingsPage
|
||||
<?php wp_nonce_field('kb_markdown_test_connection'); ?>
|
||||
<?php submit_button(__('Test GitLab Connection', 'kb-markdown-importer'), 'secondary', 'kb_markdown_test_connection'); ?>
|
||||
</form>
|
||||
<form method="post">
|
||||
<?php wp_nonce_field('kb_markdown_test_product_updates'); ?>
|
||||
<?php submit_button(__('Produktupdate-Quelle testen', 'kb-markdown-importer'), 'secondary', 'kb_markdown_test_product_updates'); ?>
|
||||
<p class="description"><?php esc_html_e('Der Test nutzt die gespeicherten Einstellungen der ausgewählten Update-Quelle. Bitte Änderungen vorher speichern.', 'kb-markdown-importer'); ?></p>
|
||||
</form>
|
||||
<?php if (is_array($updatesTest)) : ?>
|
||||
<div class="notice notice-<?php echo $updatesTest['ok'] ? 'success' : 'error'; ?>">
|
||||
<p><strong><?php echo esc_html($updatesTest['title']); ?></strong></p>
|
||||
<p><?php echo esc_html($updatesTest['message']); ?></p>
|
||||
</div>
|
||||
<?php if ('' !== $updatesTest['body']) : ?>
|
||||
<h2><?php esc_html_e('Antwort der Produktupdate-Quelle', 'kb-markdown-importer'); ?></h2>
|
||||
<textarea class="large-text code" rows="16" readonly><?php echo esc_textarea($updatesTest['body']); ?></textarea>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
@@ -176,6 +322,78 @@ final class SettingsPage
|
||||
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();
|
||||
@@ -206,4 +424,20 @@ final class SettingsPage
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ final class BreadcrumbBuilder
|
||||
{
|
||||
$base = trim((string) Plugin::settings()['docs_base_slug'], '/');
|
||||
$items = [
|
||||
sprintf('<a href="%s">%s</a>', esc_url(home_url('/' . $base . '/')), esc_html__('Docs', 'kb-markdown-importer')),
|
||||
sprintf('<a href="%s">%s</a>', esc_url(home_url('/' . $base . '/')), esc_html__('Dokumentation', 'kb-markdown-importer')),
|
||||
];
|
||||
$path = $base;
|
||||
|
||||
|
||||
266
kb-markdown-importer/includes/Frontend/ProductUpdatesFeed.php
Normal file
266
kb-markdown-importer/includes/Frontend/ProductUpdatesFeed.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KbMarkdownImporter\Frontend;
|
||||
|
||||
use KbMarkdownImporter\Plugin;
|
||||
|
||||
final class ProductUpdatesFeed
|
||||
{
|
||||
public static function items(array $settings = []): array
|
||||
{
|
||||
$settings = $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 [];
|
||||
}
|
||||
|
||||
$cacheKey = 'kb_product_updates_' . md5($source . $url . wp_json_encode([
|
||||
$settings['product_updates_feed_item_path'] ?? '',
|
||||
$settings['product_updates_feed_product_field'] ?? '',
|
||||
$settings['product_updates_feed_version_field'] ?? '',
|
||||
$settings['product_updates_feed_date_field'] ?? '',
|
||||
$settings['product_updates_feed_changelog_field'] ?? '',
|
||||
$settings['product_updates_rest_list_path'] ?? '',
|
||||
$settings['product_updates_rest_product_field'] ?? '',
|
||||
$settings['product_updates_rest_version_field'] ?? '',
|
||||
$settings['product_updates_rest_date_field'] ?? '',
|
||||
$settings['product_updates_rest_changelog_field'] ?? '',
|
||||
]));
|
||||
$cached = get_transient($cacheKey);
|
||||
|
||||
if (is_array($cached)) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$response = wp_remote_get($url, [
|
||||
'timeout' => 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);
|
||||
}
|
||||
}
|
||||
@@ -194,9 +194,13 @@ final class Router
|
||||
|
||||
private function captureIndex(): string
|
||||
{
|
||||
$settings = Plugin::settings();
|
||||
|
||||
return (new TemplateLoader())->capture('documentation-index', [
|
||||
'products' => self::productsWithVersions(),
|
||||
'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'),
|
||||
'settings' => $settings,
|
||||
'updates' => ProductUpdatesFeed::items($settings),
|
||||
'base_slug' => trim((string) $settings['docs_base_slug'], '/'),
|
||||
'url_builder' => UrlBuilder::class,
|
||||
]);
|
||||
}
|
||||
@@ -245,6 +249,7 @@ final class Router
|
||||
return (new TemplateLoader())->capture('version', [
|
||||
'product' => $product,
|
||||
'version' => $version,
|
||||
'versions' => $this->versionsForProduct($productSlug),
|
||||
'pages' => $this->pagesForVersion($productSlug, $versionSlug),
|
||||
'base_slug' => trim((string) Plugin::settings()['docs_base_slug'], '/'),
|
||||
'url_builder' => UrlBuilder::class,
|
||||
@@ -332,7 +337,7 @@ final class Router
|
||||
{
|
||||
status_header(404);
|
||||
(new TemplateLoader())->render('search', [
|
||||
'title' => __('Documentation page not found.', 'kb-markdown-importer'),
|
||||
'title' => __('Dokumentationsseite nicht gefunden.', 'kb-markdown-importer'),
|
||||
'results' => [],
|
||||
'query' => '',
|
||||
]);
|
||||
@@ -342,7 +347,7 @@ final class Router
|
||||
{
|
||||
status_header(404);
|
||||
return (new TemplateLoader())->capture('search', [
|
||||
'title' => __('Documentation page not found.', 'kb-markdown-importer'),
|
||||
'title' => __('Dokumentationsseite nicht gefunden.', 'kb-markdown-importer'),
|
||||
'results' => [],
|
||||
'query' => '',
|
||||
]);
|
||||
|
||||
@@ -17,7 +17,7 @@ final class SearchController
|
||||
$results = $query ? self::search($query, sanitize_text_field(wp_unslash((string) ($_GET['product'] ?? ''))), sanitize_text_field(wp_unslash((string) ($_GET['version'] ?? '')))) : [];
|
||||
|
||||
return (new TemplateLoader())->capture('search', [
|
||||
'title' => __('Search Documentation', 'kb-markdown-importer'),
|
||||
'title' => __('Dokumentation durchsuchen', 'kb-markdown-importer'),
|
||||
'query' => $query,
|
||||
'results' => $results,
|
||||
]);
|
||||
|
||||
@@ -11,7 +11,7 @@ final class TemplateLoader
|
||||
|
||||
if (! is_readable($path)) {
|
||||
status_header(500);
|
||||
echo esc_html__('Knowledgebase template missing.', 'kb-markdown-importer');
|
||||
echo esc_html__('Dokumentationsvorlage fehlt.', 'kb-markdown-importer');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,22 @@ final class Settings
|
||||
'design_accent_color' => '#F59C00',
|
||||
'design_radius' => '14',
|
||||
'custom_theme_css_url' => '',
|
||||
'docs_home_intro_title' => 'So nutzt du die Dokumentation',
|
||||
'docs_home_intro_content' => '<p>Wähle links ein Produkt aus und öffne anschließend die passende Version. Innerhalb einer Version findest du die zugehörigen Seiten der Dokumentation in der Navigation.</p>',
|
||||
'product_updates_source' => 'rss',
|
||||
'product_updates_feed_url' => '',
|
||||
'product_updates_feed_limit' => '5',
|
||||
'product_updates_feed_item_path' => 'channel/item',
|
||||
'product_updates_feed_product_field' => 'title',
|
||||
'product_updates_feed_version_field' => 'category',
|
||||
'product_updates_feed_date_field' => 'pubDate',
|
||||
'product_updates_feed_changelog_field' => 'description',
|
||||
'product_updates_rest_url' => '',
|
||||
'product_updates_rest_list_path' => 'content,data,items',
|
||||
'product_updates_rest_product_field' => 'product.name,productName,name',
|
||||
'product_updates_rest_version_field' => 'version,versionName,name',
|
||||
'product_updates_rest_date_field' => 'releaseDate,date,updatedAt,createdAt',
|
||||
'product_updates_rest_changelog_field' => 'changelog,changeLog,description,changes',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user