Every file under src/ exits outside WordPress (defined('ABSPATH') ||
exit; after the use block, so a direct request no longer prints a fatal
error with the server path); divi-convert-fixture.php gets the WP_CLI
guard the other integration scripts have. A unit test keeps both true;
PSR1 side-effects warning excluded for the guard.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
175 lines
5.9 KiB
PHP
175 lines
5.9 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Admin;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
/**
|
||
* Settings page "Einstellungen → LIT MCP" with tabs (spec §8.4). Settings API, no JS framework.
|
||
*/
|
||
final class SettingsPage
|
||
{
|
||
public const SLUG = 'lit-mcp';
|
||
|
||
/** Hook suffix WordPress gives this page (add_options_page → "settings_page_<slug>"). */
|
||
public const SCREEN_HOOK = 'settings_page_' . self::SLUG;
|
||
|
||
/** @var array<string, AdminTab> */
|
||
private array $tabs = [];
|
||
|
||
/**
|
||
* @param list<AdminTab> $tabs The first tab is the default.
|
||
* @param string $logoUrl Loosen-IT logo shown top right of the page ('' = none).
|
||
* @param string $assetsUrl URL of the plugin's assets/ directory ('' = no own CSS/JS).
|
||
* @param string $version Asset version for cache busting.
|
||
*/
|
||
public function __construct(
|
||
array $tabs,
|
||
private string $logoUrl = '',
|
||
private string $assetsUrl = '',
|
||
private string $version = ''
|
||
) {
|
||
foreach ($tabs as $tab) {
|
||
$this->tabs[$tab->slug()] = $tab;
|
||
}
|
||
}
|
||
|
||
public function register(): void
|
||
{
|
||
add_action('admin_menu', [$this, 'addMenu']);
|
||
add_action('admin_init', [$this, 'registerSettings']);
|
||
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
|
||
foreach ($this->tabs as $tab) {
|
||
$tab->registerActions();
|
||
}
|
||
}
|
||
|
||
public function addMenu(): void
|
||
{
|
||
add_options_page(
|
||
__('LIT MCP', 'wp-lit-mcp'),
|
||
__('LIT MCP', 'wp-lit-mcp'),
|
||
'manage_options',
|
||
self::SLUG,
|
||
[$this, 'render']
|
||
);
|
||
}
|
||
|
||
public function registerSettings(): void
|
||
{
|
||
foreach ($this->tabs as $tab) {
|
||
$tab->registerSettings();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Own CSS and a small script, only on this page. The script builds on WordPress' own
|
||
* clipboard.js and wp.a11y.speak (as the media modal's "Copy URL") and postbox.js.
|
||
*/
|
||
public function enqueueAssets(string $hook): void
|
||
{
|
||
if ($hook !== self::SCREEN_HOOK || $this->assetsUrl === '') {
|
||
return;
|
||
}
|
||
|
||
$base = rtrim($this->assetsUrl, '/');
|
||
wp_enqueue_style('lit-mcp-admin', $base . '/css/admin.css', [], $this->version);
|
||
$deps = ['clipboard', 'wp-a11y'];
|
||
if ($this->currentTab() === 'tools') {
|
||
// Collapsible tool groups use WordPress' own postbox toggles (remembered per user).
|
||
$deps[] = 'postbox';
|
||
}
|
||
wp_enqueue_script('lit-mcp-admin', $base . '/js/admin.js', $deps, $this->version, true);
|
||
wp_localize_script('lit-mcp-admin', 'litMcpAdmin', [
|
||
'copied' => __('Die URL wurde in die Zwischenablage kopiert.', 'wp-lit-mcp'),
|
||
]);
|
||
}
|
||
|
||
/** @return list<string> */
|
||
public function tabSlugs(): array
|
||
{
|
||
return array_keys($this->tabs);
|
||
}
|
||
|
||
public function currentTab(): string
|
||
{
|
||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab switch.
|
||
$requested = isset($_GET['tab']) ? sanitize_key(wp_unslash((string) $_GET['tab'])) : '';
|
||
|
||
return isset($this->tabs[$requested]) ? $requested : (string) array_key_first($this->tabs);
|
||
}
|
||
|
||
/** @param array<string, string> $args */
|
||
public static function url(string $tab = '', array $args = []): string
|
||
{
|
||
$query = array_merge(['page' => self::SLUG], $tab !== '' ? ['tab' => $tab] : [], $args);
|
||
|
||
return add_query_arg($query, admin_url('options-general.php'));
|
||
}
|
||
|
||
public function render(): void
|
||
{
|
||
if (! current_user_can('manage_options')) {
|
||
return;
|
||
}
|
||
|
||
$current = $this->currentTab();
|
||
echo '<div class="wrap lit-mcp">';
|
||
echo '<div class="lit-mcp-header"><h1>' . esc_html__('LIT MCP', 'wp-lit-mcp') . '</h1>';
|
||
$this->renderLogo();
|
||
// Admin notices of other plugins are moved right after this marker – above the tabs.
|
||
echo '</div><hr class="wp-header-end">';
|
||
$this->renderNotice();
|
||
printf('<nav class="nav-tab-wrapper wp-clearfix" aria-label="%s">', esc_attr__('Bereiche', 'wp-lit-mcp'));
|
||
foreach ($this->tabs as $slug => $tab) {
|
||
printf(
|
||
'<a href="%s" class="nav-tab%s"%s>%s</a>',
|
||
esc_url(self::url($slug)),
|
||
$slug === $current ? ' nav-tab-active' : '',
|
||
$slug === $current ? ' aria-current="page"' : '',
|
||
esc_html($tab->label())
|
||
);
|
||
}
|
||
echo '</nav>';
|
||
$this->tabs[$current]->render();
|
||
echo '</div>';
|
||
}
|
||
|
||
private function renderLogo(): void
|
||
{
|
||
if ($this->logoUrl === '') {
|
||
return;
|
||
}
|
||
|
||
printf(
|
||
'<a class="lit-mcp-logo-link" href="https://loosen-it.de" target="_blank" rel="noopener">'
|
||
. '<img src="%s" alt="Loosen-IT Logo" class="lit-mcp-logo">'
|
||
. '<span class="screen-reader-text"> %s</span></a>',
|
||
esc_url($this->logoUrl),
|
||
esc_html__('(öffnet in einem neuen Tab)', 'wp-lit-mcp')
|
||
);
|
||
}
|
||
|
||
private function renderNotice(): void
|
||
{
|
||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only.
|
||
$key = isset($_GET['lit_mcp_notice']) ? sanitize_key(wp_unslash((string) $_GET['lit_mcp_notice'])) : '';
|
||
$notice = match ($key) {
|
||
'revoked' => ['success', __('Verbindung widerrufen.', 'wp-lit-mcp')],
|
||
'revoke_failed' => ['error', __('Die Verbindung konnte nicht widerrufen werden.', 'wp-lit-mcp')],
|
||
'cleared' => ['success', __('Protokoll geleert.', 'wp-lit-mcp')],
|
||
default => null,
|
||
};
|
||
|
||
if ($notice !== null) {
|
||
printf(
|
||
'<div class="notice notice-%s is-dismissible"><p>%s</p></div>',
|
||
esc_attr($notice[0]),
|
||
esc_html($notice[1])
|
||
);
|
||
}
|
||
}
|
||
}
|