Files
WP-LIT-MCP/src/Admin/ConnectionUsage.php
T
janniclas.loosenandClaude Opus 5.5 b044cacc9d fix: admin UI review round 1 (S47) - Art labels, usage only on success, result fallback
- Art column: only lit/media-delete and lit/divi-delete say "löscht",
  lit/post-trash "Papierkorb", other destructive tools "überschreibt".
- ConnectionUsage records only responses with status < 400 (mcp-oauth answers
  auth failures with WP_REST_Response 401, not WP_Error).
- Log result column: translated "unbekannt" instead of the raw value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 17:02:39 +02:00

95 lines
3.3 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;
use LIT\MCP\Server\ServerRegistrar;
use Throwable;
use WP_REST_Request;
use WPMedia\MCP\OAuth\Auth\JWT;
use WPMedia\MCP\OAuth\Auth\SecretManager;
/**
* Records when an OAuth connection was last used, for "Zuletzt genutzt" in the tab "Zugriff".
*
* mcp-oauth authenticates with a JWT bound to an application password but never records
* its usage (WordPress only does that for Basic-Auth application passwords), so last_used
* stayed empty. After an authenticated request to our MCP route this calls WordPress' own
* WP_Application_Passwords::record_application_password_usage(), which writes at most once
* a day per password – the same data and throttle as in the user's profile.
*/
final class ConnectionUsage
{
/** @var callable(string): ?array<string, mixed> */
private $decode;
/** @var callable(int, string): mixed */
private $recordUsage;
/**
* @param (callable(string): ?array<string, mixed>)|null $decode JWT → claims, null if invalid.
* @param (callable(int, string): mixed)|null $recordUsage user id, application password uuid.
*/
public function __construct(?callable $decode = null, ?callable $recordUsage = null)
{
$this->decode = $decode ?? static function (string $token): ?array {
$claims = JWT::decode($token, SecretManager::get_secret());
return is_array($claims) ? $claims : null;
};
$this->recordUsage = $recordUsage ?? static function (int $userId, string $uuid): void {
\WP_Application_Passwords::record_application_password_usage($userId, $uuid);
};
}
public function register(): void
{
add_filter('rest_request_after_callbacks', [$this, 'record'], 10, 3);
}
/**
* @param array<string, mixed> $handler
* @return mixed The response, unchanged.
*/
public function record(mixed $response, array $handler, WP_REST_Request $request): mixed
{
$route = '/' . ServerRegistrar::NAMESPACE . '/' . ServerRegistrar::ROUTE;
if ($request->get_route() !== $route || ! self::succeeded($response)) {
return $response;
}
$header = $request->get_header('authorization');
$userId = get_current_user_id();
if (! is_string($header) || stripos($header, 'Bearer ') !== 0 || $userId <= 0) {
return $response;
}
try {
$claims = ($this->decode)(substr($header, 7));
$uuid = is_array($claims) ? (string) ($claims['app_pass_id'] ?? '') : '';
// Only the password of the user this request actually authenticated as.
if ($uuid !== '' && (int) ($claims['sub'] ?? 0) === $userId) {
($this->recordUsage)($userId, $uuid);
}
} catch (Throwable) {
// Bookkeeping only – never break the MCP response.
}
return $response;
}
/**
* Only successful answers count as use: mcp-oauth answers auth failures with a
* WP_REST_Response(401), not a WP_Error (OAuthHttpTransport::build_auth_error_response).
*/
private static function succeeded(mixed $response): bool
{
if (is_wp_error($response)) {
return false;
}
return ! $response instanceof \WP_HTTP_Response || $response->get_status() < 400;
}
}