Files
adocWP/kb-markdown-importer/includes/Admin/SettingsPage.php
2026-05-27 14:17:22 +02:00

320 lines
20 KiB
PHP

<?php
declare(strict_types=1);
namespace KbMarkdownImporter\Admin;
use KbMarkdownImporter\GitLab\GitLabClient;
use KbMarkdownImporter\Import\ImportLogger;
use KbMarkdownImporter\Olm\ChangelogSync;
use KbMarkdownImporter\Plugin;
use KbMarkdownImporter\Settings;
final class SettingsPage
{
public static function registerSettings(): void
{
register_setting('kb_markdown_importer_settings', 'kb_markdown_importer_settings', [
'type' => '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'] = 'olm_changelog';
$settings['product_updates_olm_months'] = (string) max(1, min(24, (int) ($input['product_updates_olm_months'] ?? 4)));
$settings['product_updates_olm_ignore_numbers'] = self::sanitizeOlmNumberList((string) ($input['product_updates_olm_ignore_numbers'] ?? 'olm-10109,olm-10110'));
$settings['olm_base_url'] = esc_url_raw(ChangelogSync::normalizeBaseUrl((string) ($input['olm_base_url'] ?? '')));
$settings['olm_username'] = sanitize_text_field((string) ($input['olm_username'] ?? ''));
$settings['olm_password'] = trim((string) ($input['olm_password'] ?? '')) ?: (string) $old['olm_password'];
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();
?>
<div class="wrap">
<h1><?php esc_html_e('Markdown Knowledgebase Settings', 'kb-markdown-importer'); ?></h1>
<form method="post" action="options.php">
<?php settings_fields('kb_markdown_importer_settings'); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="gitlab_base_url">GitLab Base URL</label></th>
<td>
<input class="regular-text" id="gitlab_base_url" name="kb_markdown_importer_settings[gitlab_base_url]" type="url" value="<?php echo esc_attr($settings['gitlab_base_url']); ?>" placeholder="https://git.example.de">
<p class="description"><?php esc_html_e('Use the GitLab root URL, for example https://git.example.de. Do not include /api/v4.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="gitlab_token">GitLab API Token</label></th>
<td><input class="regular-text" id="gitlab_token" name="kb_markdown_importer_settings[gitlab_token]" type="password" value="" placeholder="<?php echo $settings['gitlab_token'] ? esc_attr__('Token is stored; leave blank to keep it', 'kb-markdown-importer') : ''; ?>"></td>
</tr>
<tr>
<th scope="row"><label for="gitlab_group">GitLab Group Path / ID</label></th>
<td><input class="regular-text" id="gitlab_group" name="kb_markdown_importer_settings[gitlab_group]" type="text" value="<?php echo esc_attr($settings['gitlab_group']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="branch_pattern">Branch Pattern</label></th>
<td><input class="regular-text" id="branch_pattern" name="kb_markdown_importer_settings[branch_pattern]" type="text" value="<?php echo esc_attr($settings['branch_pattern']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="docs_base_slug">Frontend Base Slug</label></th>
<td><input class="regular-text" id="docs_base_slug" name="kb_markdown_importer_settings[docs_base_slug]" type="text" value="<?php echo esc_attr($settings['docs_base_slug']); ?>"></td>
</tr>
<tr>
<th scope="row">Markdown Project Structure</th>
<td>
<p class="description"><?php esc_html_e('Each version branch must contain doku.md, stepbystep.md and an images/ folder. Optional files such as faq.md and doku.yml are supported.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row">Options</th>
<td>
<label><input type="checkbox" name="kb_markdown_importer_settings[image_lightbox]" value="1" <?php checked($settings['image_lightbox'], '1'); ?>> Link images with lightbox class</label><br>
<label><input type="checkbox" name="kb_markdown_importer_settings[public_docs]" value="1" <?php checked($settings['public_docs'], '1'); ?>> Show documentation publicly</label><br>
<label><input type="checkbox" name="kb_markdown_importer_settings[allow_svg]" value="1" <?php checked($settings['allow_svg'], '1'); ?>> Allow SVG image import</label>
</td>
</tr>
<tr>
<th scope="row"><label for="cron_interval">Automatic Sync</label></th>
<td>
<select id="cron_interval" name="kb_markdown_importer_settings[cron_interval]">
<option value="disabled" <?php selected($settings['cron_interval'], 'disabled'); ?>>Disabled</option>
<option value="hourly" <?php selected($settings['cron_interval'], 'hourly'); ?>>Hourly</option>
<option value="daily" <?php selected($settings['cron_interval'], 'daily'); ?>>Daily</option>
<option value="weekly" <?php selected($settings['cron_interval'], 'weekly'); ?>>Weekly</option>
</select>
</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_olm_months"><?php esc_html_e('OLM Zeitraum', 'kb-markdown-importer'); ?></label></th>
<td>
<input id="product_updates_olm_months" name="kb_markdown_importer_settings[product_updates_olm_months]" type="number" min="1" max="24" value="<?php echo esc_attr($settings['product_updates_olm_months']); ?>"> <?php esc_html_e('Monate zurück ab Monatsanfang', 'kb-markdown-importer'); ?>
<p class="description"><?php esc_html_e('Entspricht dem Python-Script: aktueller Monat plus die angegebene Anzahl vorheriger Monate.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="product_updates_olm_ignore_numbers"><?php esc_html_e('OLM Nummern ignorieren', 'kb-markdown-importer'); ?></label></th>
<td>
<input class="regular-text" id="product_updates_olm_ignore_numbers" name="kb_markdown_importer_settings[product_updates_olm_ignore_numbers]" type="text" value="<?php echo esc_attr($settings['product_updates_olm_ignore_numbers']); ?>" placeholder="olm-10109,olm-10110">
<p class="description"><?php esc_html_e('Kommagetrennte productNo-Liste, die nicht im Changelog erscheinen soll.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="olm_base_url"><?php esc_html_e('OLM Basis-URL', 'kb-markdown-importer'); ?></label></th>
<td>
<input class="regular-text" id="olm_base_url" name="kb_markdown_importer_settings[olm_base_url]" type="url" value="<?php echo esc_attr($settings['olm_base_url']); ?>" placeholder="https://olm.o-byte.com">
<p class="description"><?php esc_html_e('Wird für den OLM-Changelog-Sync nach dem Python-Script verwendet.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="olm_username"><?php esc_html_e('OLM Benutzername', 'kb-markdown-importer'); ?></label></th>
<td><input class="regular-text" id="olm_username" name="kb_markdown_importer_settings[olm_username]" type="text" value="<?php echo esc_attr($settings['olm_username']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="olm_password"><?php esc_html_e('OLM Passwort', 'kb-markdown-importer'); ?></label></th>
<td><input class="regular-text" id="olm_password" name="kb_markdown_importer_settings[olm_password]" type="password" value="" placeholder="<?php echo $settings['olm_password'] ? esc_attr__('Passwort ist gespeichert; leer lassen zum Beibehalten', 'kb-markdown-importer') : ''; ?>"></td>
</tr>
</table>
<h2><?php esc_html_e('Frontend Design', 'kb-markdown-importer'); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="design_theme">Design Theme</label></th>
<td>
<select id="design_theme" name="kb_markdown_importer_settings[design_theme]">
<option value="obyte" <?php selected($settings['design_theme'], 'obyte'); ?>>o-byte</option>
<option value="inherit" <?php selected($settings['design_theme'], 'inherit'); ?>>Theme inherit / neutral</option>
</select>
<p class="description"><?php esc_html_e('The bundled o-byte theme is loaded by default. A custom theme.css is loaded after it and can override everything.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="design_primary_color">Primary Color</label></th>
<td><input id="design_primary_color" name="kb_markdown_importer_settings[design_primary_color]" type="color" value="<?php echo esc_attr($settings['design_primary_color']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="design_accent_color">Accent Color</label></th>
<td><input id="design_accent_color" name="kb_markdown_importer_settings[design_accent_color]" type="color" value="<?php echo esc_attr($settings['design_accent_color']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="design_radius">Border Radius</label></th>
<td>
<input id="design_radius" name="kb_markdown_importer_settings[design_radius]" type="number" min="0" max="32" value="<?php echo esc_attr($settings['design_radius']); ?>"> px
</td>
</tr>
<tr>
<th scope="row"><label for="custom_theme_css_url">Custom theme.css</label></th>
<td>
<input class="regular-text" id="custom_theme_css_url" name="kb_markdown_importer_settings[custom_theme_css_url]" type="url" value="<?php echo esc_attr($settings['custom_theme_css_url']); ?>" placeholder="https://example.com/theme.css">
<button type="button" class="button" id="kb-markdown-upload-theme-css"><?php esc_html_e('Upload/select theme.css', 'kb-markdown-importer'); ?></button>
<p class="description"><?php esc_html_e('Upload a CSS file in the media library or paste a CSS URL. It is enqueued last, so it can override the bundled theme.', 'kb-markdown-importer'); ?></p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
<form method="post">
<?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(__('OLM Changelog synchronisieren', 'kb-markdown-importer'), 'secondary', 'kb_markdown_test_product_updates'); ?>
<p class="description"><?php esc_html_e('Nutzt die gespeicherten OLM-Einstellungen. 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('Gespeicherte Changelog-Vorschau', '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
}
private static function handleConnectionTest(): void
{
$client = new GitLabClient(Plugin::settings());
$result = $client->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
{
$response = (new ChangelogSync())->sync();
$data = (array) $response->get_data();
$ok = true === ($data['success'] ?? false);
$items = ChangelogSync::items();
$preview = wp_json_encode(array_slice($items, 0, 5), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
return [
'ok' => $ok,
'title' => $ok ? __('OLM Changelog synchronisiert.', 'kb-markdown-importer') : __('OLM Changelog konnte nicht synchronisiert werden.', 'kb-markdown-importer'),
'message' => $ok
? sprintf(
/* translators: %d: number of parsed changelog items. */
__('Gefundene Changelog-Einträge im Zeitraum: %d', 'kb-markdown-importer'),
count($items)
)
: (string) ($data['message'] ?? __('Unbekannter Fehler.', 'kb-markdown-importer')),
'body' => $preview ?: '',
];
}
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 sanitizeOlmNumberList(string $value): string
{
$items = array_filter(array_map(static function (string $item): string {
return strtolower(preg_replace('/[^a-zA-Z0-9_-]/', '', trim($item)) ?: '');
}, explode(',', $value)), static fn (string $item): bool => '' !== $item);
return implode(',', array_values(array_unique($items)));
}
}