Files
WP-LIT-MCP/src/Admin/SettingsPage.php
T
janniclas.loosenandClaude Opus 5.5 b24eb6c3e9 fix(security): no direct HTTP access to plugin PHP files (audit L2, cheap part)
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
2026-09-26 19:32:12 +02:00

175 lines
5.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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])
);
}
}
}