diff --git a/lib/QubitUserChallenge.class.php b/lib/QubitUserChallenge.class.php index 2a47311a44..56336382f1 100644 --- a/lib/QubitUserChallenge.class.php +++ b/lib/QubitUserChallenge.class.php @@ -109,6 +109,28 @@ public function shouldBypassChallenge(string $remoteIp, string $userAgent): bool return false; } + public static function matchesEndpointException(string $requestUri, array $prefixes): bool + { + $requestPath = parse_url($requestUri, PHP_URL_PATH); + if (false === $requestPath || null === $requestPath || '' === $requestPath) { + $requestPath = '/'; + } + + $patterns = array_map(function (string $path) { + $escaped = preg_quote($path, '#'); + + return '#^'.$escaped.'(/.*)?$#'; + }, $prefixes); + + foreach ($patterns as $pattern) { + if (preg_match($pattern, $requestPath)) { + return true; + } + } + + return false; + } + public function setCookie( $name, $value = '', diff --git a/lib/challenge/filter.php b/lib/challenge/filter.php index 5e288b57f9..7b24e4c949 100644 --- a/lib/challenge/filter.php +++ b/lib/challenge/filter.php @@ -33,29 +33,19 @@ return; } +// Load helper class. +require_once __DIR__.'/../../lib/QubitUserChallenge.class.php'; + // Check if the request URI matches any of the endpoint exceptions. $prefixes = $config['endpoint_exceptions'] ?? []; - -$patterns = array_map(function (string $path) { - // Escape any regex-special chars in the raw path. - $escaped = preg_quote($path, '#'); - - // Match exactly the path or any subpath. - return '#^'.$escaped.'(/.*)?$#'; -}, $prefixes); - $requestUri = $_SERVER['REQUEST_URI'] ?? '/'; -foreach ($patterns as $pattern) { - if (preg_match($pattern, $requestUri)) { - return; - } +if (QubitUserChallenge::matchesEndpointException($requestUri, $prefixes)) { + return; } -// Load helper classes. +// Load helper class. require_once __DIR__.'/../../lib/QubitGeoIpHelper.class.php'; -require_once __DIR__.'/../../lib/QubitUserChallenge.class.php'; - // Get Remote IP from request. $remoteIp = $_SERVER['REMOTE_ADDR'] ?? null; if (!$remoteIp) { diff --git a/test/phpunit/lib/QubitUserChallengeTest.php b/test/phpunit/lib/QubitUserChallengeTest.php index 652255f32a..4e53878588 100644 --- a/test/phpunit/lib/QubitUserChallengeTest.php +++ b/test/phpunit/lib/QubitUserChallengeTest.php @@ -141,4 +141,56 @@ public function testLogInfoFallsBackToErrorLogIfNoLogger() // Should not throw even if logger is not set $this->assertNull($method->invoke($userChallenge), 'logInfo should not throw if logger is not set.'); } + + /** + * @dataProvider endpointExceptionProvider + */ + public function testMatchesEndpointException(string $requestUri, array $prefixes, bool $expected) + { + $this->assertSame( + $expected, + QubitUserChallenge::matchesEndpointException($requestUri, $prefixes) + ); + } + + public function endpointExceptionProvider() + { + return [ + 'oai exact path with query string' => [ + 'requestUri' => '/;oai?verb=ListRecords&metadataPrefix=oai_dc&from=2026-03-14T03:03:00Z', + 'prefixes' => ['/;oai'], + 'expected' => true, + ], + 'api exact path with query string' => [ + 'requestUri' => '/api?foo=bar', + 'prefixes' => ['/api'], + 'expected' => true, + ], + 'api subpath' => [ + 'requestUri' => '/api/search?query=oai', + 'prefixes' => ['/api'], + 'expected' => true, + ], + 'qt sword plugin exact path with query string' => [ + 'requestUri' => '/qtSwordPlugin?x=1', + 'prefixes' => ['/qtSwordPlugin'], + 'expected' => true, + ], + 'qt sword plugin subpath' => [ + 'requestUri' => '/qtSwordPlugin/status', + 'prefixes' => ['/qtSwordPlugin'], + 'expected' => true, + ], + 'similar api path does not match' => [ + 'requestUri' => '/apix?foo=bar', + 'prefixes' => ['/api'], + 'expected' => false, + ], + 'similar plugin path does not match' => [ + 'requestUri' => '/qtSwordPlugins', + 'prefixes' => ['/qtSwordPlugin'], + 'expected' => false, + ], + ]; + } }