action = new AdminPostAction($terminate); } public function slug(): string { return 'zugriff'; } public function label(): string { return __('Zugriff', 'wp-lit-mcp'); } public function registerSettings(): void { register_setting(self::GROUP, Access::OPTION, [ 'type' => 'array', 'sanitize_callback' => [self::class, 'sanitizeRoles'], 'default' => ['administrator'], ]); } public function registerActions(): void { add_action('admin_post_' . self::REVOKE_ACTION, [$this, 'handleRevoke']); } /** * Keeps only existing role slugs. Runs twice on the very first save (update_option * falls through to add_option, which sanitizes again), so each warning is added * only once. The settings page itself needs manage_options, not lit_mcp_use, so * removing the current user's own role is allowed – but warned about. * * @return list */ public static function sanitizeRoles(mixed $input): array { $existing = array_keys((array) wp_roles()->roles); $roles = array_values(array_unique(array_intersect( array_filter(is_array($input) ? $input : [], 'is_string'), $existing ))); if ($roles === []) { self::warnOnce( 'lit_mcp_no_roles', __('Keine Rolle freigegeben – niemand kann LIT MCP nutzen.', 'wp-lit-mcp') ); } elseif (self::currentUserLosesAccess($roles)) { self::warnOnce( 'lit_mcp_own_role', __( 'Keine Ihrer Rollen ist mehr freigegeben – Sie selbst können LIT MCP nicht mehr nutzen.', 'wp-lit-mcp' ) ); } return $roles; } public function render(): void { $allowed = $this->access->allowedRoles(); $legend = __('Rollen mit Zugriff', 'wp-lit-mcp'); echo '
'; settings_fields(self::GROUP); // Makes an all-unchecked submission arrive as [''] instead of dropping the field. printf('', esc_attr(Access::OPTION)); printf( ''; submit_button(); echo '
'; $this->renderConnections(); } public function handleRevoke(): void { $this->action->guard(); // phpcs:disable WordPress.Security.NonceVerification.Missing -- verified right below. $uuid = isset($_POST['uuid']) && is_string($_POST['uuid']) ? sanitize_text_field(wp_unslash($_POST['uuid'])) : ''; $userId = isset($_POST['user_id']) ? absint($_POST['user_id']) : 0; // phpcs:enable WordPress.Security.NonceVerification.Missing check_admin_referer(self::REVOKE_ACTION . '_' . $uuid); $revoked = $this->connections->revoke($userId, $uuid); $this->action->redirect($this->slug(), $revoked ? 'revoked' : 'revoke_failed'); } private static function warnOnce(string $code, string $message): void { foreach ((array) get_settings_errors(Access::OPTION) as $error) { if (is_array($error) && ($error['code'] ?? '') === $code) { return; } } add_settings_error(Access::OPTION, $code, $message, 'warning'); } /** @param list $roles */ private static function currentUserLosesAccess(array $roles): bool { // Multisite super admins pass every capability check regardless of role. if (is_multisite() && is_super_admin()) { return false; } $user = wp_get_current_user(); return array_intersect((array) $user->roles, $roles) === []; } private function renderConnections(): void { echo '

' . esc_html__('Verbindungen', 'wp-lit-mcp') . '

'; $connections = $this->connections->all(); printf( '' . '' . '' . '' . '', esc_html__('Benutzer', 'wp-lit-mcp'), esc_html__('Client', 'wp-lit-mcp'), esc_html__('Verbunden am', 'wp-lit-mcp'), esc_html__('Zuletzt genutzt', 'wp-lit-mcp'), esc_html__('Aktionen', 'wp-lit-mcp') ); if ($connections === []) { printf( '', esc_html__('Noch keine Verbindung.', 'wp-lit-mcp'), esc_html__('So verbindest du Claude:', 'wp-lit-mcp'), esc_url(SettingsPage::url('verbindung')), esc_html__('Tab „Verbindung“', 'wp-lit-mcp') ); } foreach ($connections as $connection) { $this->renderConnectionRow($connection); } echo '
%s%s%s%s%s
%s %s %s
'; if ($connections !== []) { echo '

' . esc_html__( '„Zuletzt genutzt“ wird wie bei Anwendungspasswörtern höchstens einmal am Tag aktualisiert.', 'wp-lit-mcp' ) . '

'; } } /** * @param array{user_id: int, user_login: string, uuid: string, name: string, * app_id: string, created: int, last_used: ?int} $connection */ private function renderConnectionRow(array $connection): void { $dateFormat = (string) get_option('date_format', 'd.m.Y'); $timeFormat = (string) get_option('time_format', 'H:i'); echo ''; echo '' . esc_html($connection['user_login']) . ''; printf( '%s
%s', esc_html($connection['name']), esc_html($connection['app_id']) ); printf( '%s', esc_html((string) wp_date($dateFormat . ' ' . $timeFormat, $connection['created'])) ); $lastUsed = $connection['last_used'] === null ? '–' : (string) wp_date($dateFormat, $connection['last_used']); echo '' . esc_html($lastUsed) . ''; echo ''; printf('
', esc_url(admin_url('admin-post.php'))); printf('', esc_attr(self::REVOKE_ACTION)); printf('', esc_attr((string) $connection['user_id'])); printf('', esc_attr($connection['uuid'])); wp_nonce_field(self::REVOKE_ACTION . '_' . $connection['uuid']); $confirm = esc_js(sprintf( /* translators: %s: client name */ __('Verbindung „%s“ widerrufen? Claude muss sich danach neu anmelden.', 'wp-lit-mcp'), $connection['name'] )); submit_button( __('Widerrufen', 'wp-lit-mcp'), 'delete small', 'submit', false, ['onclick' => "return window.confirm('" . $confirm . "');"] ); echo '
'; } }