<?php
namespace Modules\RemoteData\Mcp\Config; class Htaccess { public const GENERATED_MARKER = '# AUTO-GENERATED by RemoteData/Mcp/Config — do not edit inside the markers.'; private const BLOCK_BEGIN_PREFIX = '# >>> MCP .well-known BEGIN '; private const BLOCK_BEGIN_SUFFIX = ' >>>'; private const BLOCK_END_PREFIX = '# <<< MCP .well-known END '; private const BLOCK_END_SUFFIX = ' <<<'; public const ERROR_WRITE = 'writeError'; private const METADATA_ACTIONS = [ 'oauth-protected-resource' => 'oauthProtectedResource', 'oauth-authorization-server' => 'oauthAuthorizationServer', ]; private const OAUTH2_ENDPOINTS = ['authorize', 'token', 'register']; private string $resourcePath; private ?int $depthOverride; private ?string $lastErrorKey = null; private ?string $lastFile = null; public function __construct(string $resourcePath, ?int $depthOverride = null) { $this->resourcePath = self::normalizeResourcePath($resourcePath); $this->depthOverride = $depthOverride; } public static function defaultsFromAppUrl(): array { $urlParts = parse_url(APP_URL); $baseUrl = $urlParts['scheme'] . '://' . $urlParts['host']; if (!empty($urlParts['port'])) { $baseUrl .= ':' . $urlParts['port']; } return [ 'baseUrl' => $baseUrl, 'resourcePath' => self::normalizeResourcePath($urlParts['path'] ?? ''), ]; } public static function normalizeResourcePath(string $path): string { $path = trim($path); if ($path === '') { return ''; } return '/' . trim($path, '/'); } public function getResourcePath(): string { return $this->resourcePath; } public function getLastErrorKey(): ?string { return $this->lastErrorKey; } public function getLastFile(): ?string { return $this->lastFile; } public function resolveFilePath(): string { $depthBelowDocroot = $this->depthOverride ?? $this->detectDepthBelowDocroot(); return APP_REAL_PATH . str_repeat('../', $depthBelowDocroot) . '.htaccess'; } private function detectDepthBelowDocroot(): int { return count(array_filter(explode('/', trim($this->resourcePath, '/')), 'strlen')); } public function buildBlock(): string { $lines = [ $this->blockBegin(), self::GENERATED_MARKER, '# @see https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization', '', '# OAuth 2.0 / MCP .well-known discovery metadata', ]; foreach (self::METADATA_ACTIONS as $wellKnown => $action) { $pattern = '^\.well-known/' . $wellKnown . preg_quote($this->resourcePath) . '$'; $target = $this->resourcePath . '/disp.php?module=RemoteData/Mcp&action=' . $action; $lines[] = 'RewriteRule ' . $pattern . ' ' . $target . ' [L,QSA]'; } $lines[] = ''; $lines[] = '# OAuth 2.0 server endpoints (APP_URL/authorize, /token, /register)'; $relativePath = ltrim($this->resourcePath, '/'); $patternPrefix = $relativePath === '' ? '' : preg_quote($relativePath) . '/'; foreach (self::OAUTH2_ENDPOINTS as $endpoint) { $pattern = '^' . $patternPrefix . $endpoint . '$'; $target = $this->resourcePath . '/disp.php?module=Authenticate/OAuth2&action=' . $endpoint; $lines[] = 'RewriteRule ' . $pattern . ' ' . $target . ' [L,QSA]'; } $lines[] = $this->blockEnd(); return implode("\n", $lines); } public function save(): bool { $this->lastErrorKey = null; $file = $this->resolveFilePath(); $this->lastFile = $file; $block = $this->buildBlock(); if (!is_file($file)) { $content = 'RewriteEngine On' . "\n\n" . $block . "\n"; if (\Engine\File::create($file, $content) === false) { $this->lastErrorKey = self::ERROR_WRITE; return false; } return true; } $existing = \Engine\File::getContents($file); if ($existing === false) { $this->lastErrorKey = self::ERROR_WRITE; return false; } if (\Engine\File::create($file, $this->spliceBlock($existing, $block)) === false) { $this->lastErrorKey = self::ERROR_WRITE; return false; } return true; } private function spliceBlock(string $existing, string $block): string { $begin = $this->blockBegin(); $end = $this->blockEnd(); $startPos = strpos($existing, $begin); if ($startPos !== false) { $endPos = strpos($existing, $end, $startPos); if ($endPos !== false) { $endPos += strlen($end); return substr($existing, 0, $startPos) . $block . substr($existing, $endPos); } } return rtrim($existing, "\n") . "\n\n" . $block . "\n"; } private function blockBegin(): string { return self::BLOCK_BEGIN_PREFIX . $this->resourcePath . self::BLOCK_BEGIN_SUFFIX; } private function blockEnd(): string { return self::BLOCK_END_PREFIX . $this->resourcePath . self::BLOCK_END_SUFFIX; } }