feat: revoked+log tables, cron cleanup, Nginx rewrite fallback, HTTPS setting

This commit is contained in:
2026-05-08 22:17:32 +00:00
parent 9a99b6a0d9
commit d71b75cc16
+143 -90
View File
@@ -10,22 +10,25 @@ class Installer {
self::createTables(); self::createTables();
self::setDefaults(); self::setDefaults();
self::createWellKnownFiles(); self::createWellKnownFiles();
self::registerCron();
self::registerRewriteRules();
flush_rewrite_rules(); flush_rewrite_rules();
} }
public static function deactivate(): void { public static function deactivate(): void {
self::removeWellKnownFiles(); self::removeWellKnownFiles();
self::deregisterCron();
flush_rewrite_rules(); flush_rewrite_rules();
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// DB // DB tables
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private static function createTables(): void { public static function createTables(): void {
global $wpdb; global $wpdb;
$charset = $wpdb->get_charset_collate(); $charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}asktam_mcp_keys ( $keysSql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}asktam_mcp_keys (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(120) NOT NULL, name VARCHAR(120) NOT NULL,
api_key VARCHAR(64) NOT NULL, api_key VARCHAR(64) NOT NULL,
@@ -37,8 +40,32 @@ class Installer {
UNIQUE KEY api_key (api_key) UNIQUE KEY api_key (api_key)
) $charset;"; ) $charset;";
$revokedSql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}asktam_mcp_revoked (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
jti VARCHAR(32) NOT NULL,
expires_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY jti (jti),
KEY expires_at (expires_at)
) $charset;";
$logSql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}asktam_mcp_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
key_id BIGINT UNSIGNED NOT NULL,
tool VARCHAR(100) NOT NULL,
ip VARCHAR(45) NOT NULL DEFAULT '',
status TINYINT(1) NOT NULL DEFAULT 1,
duration_ms SMALLINT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY key_id (key_id),
KEY created_at (created_at)
) $charset;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php'; require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql); dbDelta($keysSql);
dbDelta($revokedSql);
dbDelta($logSql);
update_option('asktam_mcp_db_version', ASKTAM_MCP_VERSION); update_option('asktam_mcp_db_version', ASKTAM_MCP_VERSION);
} }
@@ -50,51 +77,112 @@ class Installer {
'log_requests' => true, 'log_requests' => true,
'allow_raw_sql' => false, 'allow_raw_sql' => false,
'wc_enabled' => class_exists('WooCommerce'), 'wc_enabled' => class_exists('WooCommerce'),
'require_https' => true,
]); ]);
} }
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// .well-known files — created dynamically using the site's own ABSPATH // Cron — prune expired revocations + old logs daily
// No hardcoded URLs — works on any WordPress installation
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
public static function createWellKnownFiles(): void { private static function registerCron(): void {
$dir = ABSPATH . '.well-known'; if (!wp_next_scheduled('asktam_mcp_daily_cleanup')) {
wp_schedule_event(time(), 'daily', 'asktam_mcp_daily_cleanup');
if (!file_exists($dir)) { }
wp_mkdir_p($dir); add_action('asktam_mcp_daily_cleanup', [self::class, 'runCleanup']);
} }
// Shared PHP bootstrap — no hardcoded URL private static function deregisterCron(): void {
$bootstrap = <<<'PHP' wp_clear_scheduled_hook('asktam_mcp_daily_cleanup');
<?php
// Dynamic bootstrap — works on any WordPress installation
if (!defined('ABSPATH')) {
// Find wp-load.php by walking up from this file's location
$dir = dirname(dirname(__FILE__)); // two levels up from .well-known/
$wpLoad = $dir . '/wp-load.php';
if (!file_exists($wpLoad)) {
http_response_code(500);
exit('WordPress not found');
} }
// Set minimal server vars WordPress needs
if (!isset($_SERVER['HTTP_HOST'])) {
$_SERVER['HTTP_HOST'] = parse_url(get_option('siteurl'), PHP_URL_HOST) ?? 'localhost';
}
if (!isset($_SERVER['REQUEST_URI'])) {
$_SERVER['REQUEST_URI'] = '/';
}
require $wpLoad;
}
PHP;
// oauth-authorization-server public static function runCleanup(): void {
$authServer = $bootstrap . <<<'PHP' TokenManager::pruneRevocations();
global $wpdb;
// Keep 90 days of logs
$wpdb->query("DELETE FROM {$wpdb->prefix}asktam_mcp_log WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)");
}
// -------------------------------------------------------------------------
// Nginx-compatible rewrite rules via WP rewrite API
// Works on both Apache (.htaccess) and Nginx (via WP's own rewrite)
// -------------------------------------------------------------------------
public static function registerRewriteRules(): void {
// These rules let WP handle /.well-known/ requests on Nginx
add_rewrite_rule(
'^\.well-known/oauth-authorization-server$',
'index.php?asktam_mcp_wellknown=oauth-authorization-server',
'top'
);
add_rewrite_rule(
'^\.well-known/oauth-protected-resource',
'index.php?asktam_mcp_wellknown=oauth-protected-resource',
'top'
);
// Register query var
add_filter('query_vars', function (array $vars): array {
$vars[] = 'asktam_mcp_wellknown';
return $vars;
});
// Handle the request
add_action('template_redirect', function (): void {
$which = get_query_var('asktam_mcp_wellknown');
if (!$which) return;
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Origin: *');
header('Cache-Control: no-cache'); header('Cache-Control: no-cache');
$base = rest_url('asktam-mcp/v1');
if ($which === 'oauth-authorization-server') {
echo wp_json_encode([
'issuer' => get_site_url(),
'authorization_endpoint' => $base . '/oauth/authorize',
'token_endpoint' => $base . '/oauth/token',
'token_endpoint_auth_methods_supported' => ['client_secret_post', 'client_secret_basic', 'none'],
'grant_types_supported' => ['authorization_code', 'client_credentials'],
'response_types_supported' => ['code'],
'code_challenge_methods_supported' => ['S256', 'plain'],
'scopes_supported' => ['read', 'write', 'admin', 'woocommerce'],
], JSON_UNESCAPED_SLASHES);
} elseif ($which === 'oauth-protected-resource') {
echo wp_json_encode([
'resource' => rest_url('asktam-mcp/v1/sse'),
'authorization_servers' => [get_site_url()],
'bearer_methods_supported' => ['header'],
'scopes_supported' => ['read', 'write', 'admin', 'woocommerce'],
], JSON_UNESCAPED_SLASHES);
}
exit;
});
}
// -------------------------------------------------------------------------
// .well-known physical files (Apache) — still created as fallback
// -------------------------------------------------------------------------
public static function createWellKnownFiles(): void {
$dir = ABSPATH . '.well-known';
if (!file_exists($dir)) wp_mkdir_p($dir);
$bootstrap = <<<'PHP'
<?php
if (!defined('ABSPATH')) {
$wpLoad = dirname(dirname(__FILE__)) . '/wp-load.php';
if (!file_exists($wpLoad)) { http_response_code(500); exit('WordPress not found'); }
if (!isset($_SERVER['HTTP_HOST'])) $_SERVER['HTTP_HOST'] = 'localhost';
if (!isset($_SERVER['REQUEST_URI'])) $_SERVER['REQUEST_URI'] = '/';
require $wpLoad;
}
PHP;
$authServer = $bootstrap . <<<'PHP'
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Cache-Control: no-cache');
$base = rest_url('asktam-mcp/v1'); $base = rest_url('asktam-mcp/v1');
echo json_encode([ echo json_encode([
'issuer' => get_site_url(), 'issuer' => get_site_url(),
@@ -108,13 +196,11 @@ echo json_encode([
], JSON_UNESCAPED_SLASHES); ], JSON_UNESCAPED_SLASHES);
PHP; PHP;
// oauth-protected-resource
$protectedResource = $bootstrap . <<<'PHP' $protectedResource = $bootstrap . <<<'PHP'
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Origin: *');
header('Cache-Control: no-cache'); header('Cache-Control: no-cache');
echo json_encode([ echo json_encode([
'resource' => rest_url('asktam-mcp/v1/sse'), 'resource' => rest_url('asktam-mcp/v1/sse'),
'authorization_servers' => [get_site_url()], 'authorization_servers' => [get_site_url()],
@@ -123,7 +209,6 @@ echo json_encode([
], JSON_UNESCAPED_SLASHES); ], JSON_UNESCAPED_SLASHES);
PHP; PHP;
// .htaccess inside .well-known — forces PHP execution on extensionless files
$htaccess = <<<'HTACCESS' $htaccess = <<<'HTACCESS'
<Files "oauth-authorization-server"> <Files "oauth-authorization-server">
ForceType application/x-httpd-php ForceType application/x-httpd-php
@@ -136,75 +221,43 @@ HTACCESS;
file_put_contents($dir . '/oauth-authorization-server', $authServer); file_put_contents($dir . '/oauth-authorization-server', $authServer);
file_put_contents($dir . '/oauth-protected-resource', $protectedResource); file_put_contents($dir . '/oauth-protected-resource', $protectedResource);
file_put_contents($dir . '/.htaccess', $htaccess); file_put_contents($dir . '/.htaccess', $htaccess);
// Also add rewrite rules in main .htaccess for the subpath variant
// /.well-known/oauth-protected-resource/{anything} → same file
self::addHtaccessRules(); self::addHtaccessRules();
} }
public static function removeWellKnownFiles(): void { public static function removeWellKnownFiles(): void {
$dir = ABSPATH . '.well-known'; $dir = ABSPATH . '.well-known';
$files = [ $files = ['oauth-authorization-server', 'oauth-protected-resource', '.htaccess'];
$dir . '/oauth-authorization-server', foreach ($files as $f) {
$dir . '/oauth-protected-resource', $path = $dir . '/' . $f;
$dir . '/.htaccess', if (file_exists($path)) @unlink($path);
];
foreach ($files as $file) {
if (file_exists($file)) {
@unlink($file);
}
}
// Remove dir if empty
if (file_exists($dir) && count(scandir($dir)) === 2) {
@rmdir($dir);
} }
if (file_exists($dir) && count(scandir($dir)) === 2) @rmdir($dir);
self::removeHtaccessRules(); self::removeHtaccessRules();
} }
// -------------------------------------------------------------------------
// Main .htaccess — add/remove rewrite rules for .well-known subpaths
// -------------------------------------------------------------------------
private static function addHtaccessRules(): void { private static function addHtaccessRules(): void {
$htaccessFile = ABSPATH . '.htaccess'; $file = ABSPATH . '.htaccess';
if (!file_exists($htaccessFile) || !is_writable($htaccessFile)) return; if (!file_exists($file) || !is_writable($file)) return;
$content = file_get_contents($file);
if (str_contains($content, '# BEGIN AskTam MCP')) return;
$content = file_get_contents($htaccessFile); $rules = "\n# BEGIN AskTam MCP\n<IfModule mod_rewrite.c>\nRewriteEngine On\n"
. "RewriteRule ^.well-known/oauth-authorization-server$ .well-known/oauth-authorization-server [L,T=application/x-httpd-php]\n"
. "RewriteRule ^.well-known/oauth-protected-resource.*$ .well-known/oauth-protected-resource [L,T=application/x-httpd-php]\n"
. "</IfModule>\n# END AskTam MCP\n\n";
$marker = '# BEGIN AskTam MCP'; $content = str_contains($content, '# BEGIN WordPress')
$marker2 = '# END AskTam MCP'; ? str_replace('# BEGIN WordPress', $rules . '# BEGIN WordPress', $content)
: $rules . $content;
// Don't add twice file_put_contents($file, $content);
if (str_contains($content, $marker)) return;
$rules = <<<'RULES'
# BEGIN AskTam MCP
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^.well-known/oauth-authorization-server$ .well-known/oauth-authorization-server [L,T=application/x-httpd-php]
RewriteRule ^.well-known/oauth-protected-resource.*$ .well-known/oauth-protected-resource [L,T=application/x-httpd-php]
</IfModule>
# END AskTam MCP
RULES;
// Insert before # BEGIN WordPress
$wpMarker = '# BEGIN WordPress';
if (str_contains($content, $wpMarker)) {
$content = str_replace($wpMarker, $rules . $wpMarker, $content);
} else {
$content = $rules . $content;
}
file_put_contents($htaccessFile, $content);
} }
private static function removeHtaccessRules(): void { private static function removeHtaccessRules(): void {
$htaccessFile = ABSPATH . '.htaccess'; $file = ABSPATH . '.htaccess';
if (!file_exists($htaccessFile)) return; if (!file_exists($file)) return;
$content = file_get_contents($file);
$content = file_get_contents($htaccessFile);
$content = preg_replace('/\n# BEGIN AskTam MCP.*?# END AskTam MCP\n/s', '', $content); $content = preg_replace('/\n# BEGIN AskTam MCP.*?# END AskTam MCP\n/s', '', $content);
file_put_contents($htaccessFile, $content); file_put_contents($file, $content);
} }
} }