Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>tags/v21.0.0beta3
@@ -61,10 +61,6 @@ | |||
RewriteCond %{HTTP_USER_AGENT} DavClnt | |||
RewriteRule ^$ /remote.php/webdav/ [L,R=302] | |||
RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}] | |||
RewriteRule ^\.well-known/host-meta /public.php?service=host-meta [QSA,L] | |||
RewriteRule ^\.well-known/host-meta\.json /public.php?service=host-meta-json [QSA,L] | |||
RewriteRule ^\.well-known/webfinger /public.php?service=webfinger [QSA,L] | |||
RewriteRule ^\.well-known/nodeinfo /public.php?service=nodeinfo [QSA,L] | |||
RewriteRule ^\.well-known/carddav /remote.php/dav/ [R=301,L] | |||
RewriteRule ^\.well-known/caldav /remote.php/dav/ [R=301,L] | |||
RewriteRule ^remote/(.*) remote.php [QSA,L] |
@@ -256,10 +256,10 @@ window.addEventListener('DOMContentLoaded', function(){ | |||
// run setup checks then gather error messages | |||
$.when( | |||
OC.SetupChecks.checkWebDAV(), | |||
OC.SetupChecks.checkWellKnownUrl('/.well-known/webfinger', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true && !!OC.appConfig.core.public_webfinger, [200, 404]), | |||
OC.SetupChecks.checkWellKnownUrl('/.well-known/nodeinfo', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true && !!OC.appConfig.core.public_nodeinfo, [200, 404]), | |||
OC.SetupChecks.checkWellKnownUrl('/.well-known/caldav', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true), | |||
OC.SetupChecks.checkWellKnownUrl('/.well-known/carddav', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true), | |||
OC.SetupChecks.checkWellKnownUrl('GET', '/.well-known/webfinger', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true, [200, 404], true), | |||
OC.SetupChecks.checkWellKnownUrl('GET', '/.well-known/nodeinfo', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true, [200, 404], true), | |||
OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/caldav', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true), | |||
OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/carddav', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true), | |||
OC.SetupChecks.checkProviderUrl(OC.getRootPath() + '/ocm-provider/', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true), | |||
OC.SetupChecks.checkProviderUrl(OC.getRootPath() + '/ocs-provider/', OC.theme.docPlaceholderUrl, $('#postsetupchecks').data('check-wellknown') === true), | |||
OC.SetupChecks.checkSetup(), |
@@ -163,8 +163,6 @@ class Application extends App implements IBootstrap { | |||
$groupManager->listen('\OC\Group', 'postRemoveUser', [$this, 'removeUserFromGroup']); | |||
$groupManager->listen('\OC\Group', 'postAddUser', [$this, 'addUserToGroup']); | |||
}); | |||
Util::connectHook('\OCP\Config', 'js', $this, 'extendJsConfig'); | |||
} | |||
public function addUserToGroup(IGroup $group, IUser $user): void { | |||
@@ -209,23 +207,4 @@ class Application extends App implements IBootstrap { | |||
$hooks = $this->getContainer()->query(Hooks::class); | |||
$hooks->onChangeEmail($parameters['user'], $parameters['old_value']); | |||
} | |||
/** | |||
* @param array $settings | |||
*/ | |||
public function extendJsConfig(array $settings) { | |||
$appConfig = json_decode($settings['array']['oc_appconfig'], true); | |||
$publicWebFinger = \OC::$server->getConfig()->getAppValue('core', 'public_webfinger', ''); | |||
if (!empty($publicWebFinger)) { | |||
$appConfig['core']['public_webfinger'] = $publicWebFinger; | |||
} | |||
$publicNodeInfo = \OC::$server->getConfig()->getAppValue('core', 'public_nodeinfo', ''); | |||
if (!empty($publicNodeInfo)) { | |||
$appConfig['core']['public_nodeinfo'] = $publicNodeInfo; | |||
} | |||
$settings['array']['oc_appconfig'] = json_encode($appConfig); | |||
} | |||
} |
@@ -0,0 +1,67 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OC\Core\Controller; | |||
use OC\Http\WellKnown\RequestManager; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OCP\AppFramework\Http\Response; | |||
use OCP\IRequest; | |||
class WellKnownController extends Controller { | |||
/** @var RequestManager */ | |||
private $requestManager; | |||
public function __construct(IRequest $request, | |||
RequestManager $wellKnownManager) { | |||
parent::__construct('core', $request); | |||
$this->requestManager = $wellKnownManager; | |||
} | |||
/** | |||
* @PublicPage | |||
* @NoCSRFRequired | |||
* | |||
* @return Response | |||
*/ | |||
public function handle(string $service): Response { | |||
$response = $this->requestManager->process( | |||
$service, | |||
$this->request | |||
); | |||
if ($response === null) { | |||
$httpResponse = new JSONResponse(["message" => "$service not supported"], Http::STATUS_NOT_FOUND); | |||
} else { | |||
$httpResponse = $response->toHttpResponse(); | |||
} | |||
// We add a custom header so that setup checks can detect if their requests are answered by this controller | |||
return $httpResponse->addHeader('X-NEXTCLOUD-WELL-KNOWN', '1'); | |||
} | |||
} |
@@ -56,7 +56,7 @@ | |||
* @param {int|int[]} expectedStatus the expected HTTP status to be returned by the URL, 207 by default | |||
* @return $.Deferred object resolved with an array of error messages | |||
*/ | |||
checkWellKnownUrl: function(url, placeholderUrl, runCheck, expectedStatus) { | |||
checkWellKnownUrl: function(verb, url, placeholderUrl, runCheck, expectedStatus, checkCustomHeader) { | |||
if (expectedStatus === undefined) { | |||
expectedStatus = [207]; | |||
} | |||
@@ -73,7 +73,8 @@ | |||
} | |||
var afterCall = function(xhr) { | |||
var messages = []; | |||
if (expectedStatus.indexOf(xhr.status) === -1) { | |||
var customWellKnown = xhr.getResponseHeader('X-NEXTCLOUD-WELL-KNOWN') | |||
if (expectedStatus.indexOf(xhr.status) === -1 || (checkCustomHeader && !customWellKnown)) { | |||
var docUrl = placeholderUrl.replace('PLACEHOLDER', 'admin-setup-well-known-URL'); | |||
messages.push({ | |||
msg: t('core', 'Your web server is not properly set up to resolve "{url}". Further information can be found in the <a target="_blank" rel="noreferrer noopener" href="{docLink}">documentation</a>.', { docLink: docUrl, url: url }), | |||
@@ -84,7 +85,7 @@ | |||
}; | |||
$.ajax({ | |||
type: 'PROPFIND', | |||
type: verb, | |||
url: url, | |||
complete: afterCall, | |||
allowAuthErrors: true |
@@ -62,7 +62,7 @@ describe('OC.SetupChecks tests', function() { | |||
describe('checkWellKnownUrl', function() { | |||
it('should fail with another response status code than the expected one', function(done) { | |||
var async = OC.SetupChecks.checkWellKnownUrl('/.well-known/caldav', 'http://example.org/PLACEHOLDER', true, 207); | |||
var async = OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/caldav', 'http://example.org/PLACEHOLDER', true, 207); | |||
suite.server.requests[0].respond(200); | |||
@@ -76,7 +76,7 @@ describe('OC.SetupChecks tests', function() { | |||
}); | |||
it('should return no error with the expected response status code', function(done) { | |||
var async = OC.SetupChecks.checkWellKnownUrl('/.well-known/caldav', 'http://example.org/PLACEHOLDER', true, 207); | |||
var async = OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/caldav', 'http://example.org/PLACEHOLDER', true, 207); | |||
suite.server.requests[0].respond(207); | |||
@@ -87,7 +87,7 @@ describe('OC.SetupChecks tests', function() { | |||
}); | |||
it('should return no error with the default expected response status code', function(done) { | |||
var async = OC.SetupChecks.checkWellKnownUrl('/.well-known/caldav', 'http://example.org/PLACEHOLDER', true); | |||
var async = OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/caldav', 'http://example.org/PLACEHOLDER', true); | |||
suite.server.requests[0].respond(207); | |||
@@ -98,7 +98,7 @@ describe('OC.SetupChecks tests', function() { | |||
}); | |||
it('should return no error when no check should be run', function(done) { | |||
var async = OC.SetupChecks.checkWellKnownUrl('/.well-known/caldav', 'http://example.org/PLACEHOLDER', false); | |||
var async = OC.SetupChecks.checkWellKnownUrl('PROPFIND', '/.well-known/caldav', 'http://example.org/PLACEHOLDER', false); | |||
async.done(function( data, s, x ){ | |||
expect(data).toEqual([]); |
@@ -89,6 +89,9 @@ $application->registerRoutes($this, [ | |||
// Logins for passwordless auth | |||
['name' => 'WebAuthn#startAuthentication', 'url' => 'login/webauthn/start', 'verb' => 'POST'], | |||
['name' => 'WebAuthn#finishAuthentication', 'url' => 'login/webauthn/finish', 'verb' => 'POST'], | |||
// Well known requests https://tools.ietf.org/html/rfc5785 | |||
['name' => 'WellKnown#handle', 'url' => '.well-known/{service}'], | |||
], | |||
'ocs' => [ | |||
['root' => '/cloud', 'name' => 'OCS#getCapabilities', 'url' => '/capabilities', 'verb' => 'GET'], |
@@ -359,6 +359,11 @@ return array( | |||
'OCP\\Http\\Client\\IClientService' => $baseDir . '/lib/public/Http/Client/IClientService.php', | |||
'OCP\\Http\\Client\\IResponse' => $baseDir . '/lib/public/Http/Client/IResponse.php', | |||
'OCP\\Http\\Client\\LocalServerException' => $baseDir . '/lib/public/Http/Client/LocalServerException.php', | |||
'OCP\\Http\\WellKnown\\GenericResponse' => $baseDir . '/lib/public/Http/WellKnown/GenericResponse.php', | |||
'OCP\\Http\\WellKnown\\IHandler' => $baseDir . '/lib/public/Http/WellKnown/IHandler.php', | |||
'OCP\\Http\\WellKnown\\IRequestContext' => $baseDir . '/lib/public/Http/WellKnown/IRequestContext.php', | |||
'OCP\\Http\\WellKnown\\IResponse' => $baseDir . '/lib/public/Http/WellKnown/IResponse.php', | |||
'OCP\\Http\\WellKnown\\JrdResponse' => $baseDir . '/lib/public/Http/WellKnown/JrdResponse.php', | |||
'OCP\\IAddressBook' => $baseDir . '/lib/public/IAddressBook.php', | |||
'OCP\\IAppConfig' => $baseDir . '/lib/public/IAppConfig.php', | |||
'OCP\\IAvatar' => $baseDir . '/lib/public/IAvatar.php', | |||
@@ -892,6 +897,7 @@ return array( | |||
'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php', | |||
'OC\\Core\\Controller\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.php', | |||
'OC\\Core\\Controller\\WebAuthnController' => $baseDir . '/core/Controller/WebAuthnController.php', | |||
'OC\\Core\\Controller\\WellKnownController' => $baseDir . '/core/Controller/WellKnownController.php', | |||
'OC\\Core\\Controller\\WhatsNewController' => $baseDir . '/core/Controller/WhatsNewController.php', | |||
'OC\\Core\\Controller\\WipeController' => $baseDir . '/core/Controller/WipeController.php', | |||
'OC\\Core\\Data\\LoginFlowV2Credentials' => $baseDir . '/core/Data/LoginFlowV2Credentials.php', | |||
@@ -1136,6 +1142,7 @@ return array( | |||
'OC\\Http\\Client\\ClientService' => $baseDir . '/lib/private/Http/Client/ClientService.php', | |||
'OC\\Http\\Client\\Response' => $baseDir . '/lib/private/Http/Client/Response.php', | |||
'OC\\Http\\CookieHelper' => $baseDir . '/lib/private/Http/CookieHelper.php', | |||
'OC\\Http\\WellKnown\\RequestManager' => $baseDir . '/lib/private/Http/WellKnown/RequestManager.php', | |||
'OC\\InitialStateService' => $baseDir . '/lib/private/InitialStateService.php', | |||
'OC\\Installer' => $baseDir . '/lib/private/Installer.php', | |||
'OC\\IntegrityCheck\\Checker' => $baseDir . '/lib/private/IntegrityCheck/Checker.php', |
@@ -388,6 +388,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c | |||
'OCP\\Http\\Client\\IClientService' => __DIR__ . '/../../..' . '/lib/public/Http/Client/IClientService.php', | |||
'OCP\\Http\\Client\\IResponse' => __DIR__ . '/../../..' . '/lib/public/Http/Client/IResponse.php', | |||
'OCP\\Http\\Client\\LocalServerException' => __DIR__ . '/../../..' . '/lib/public/Http/Client/LocalServerException.php', | |||
'OCP\\Http\\WellKnown\\GenericResponse' => __DIR__ . '/../../..' . '/lib/public/Http/WellKnown/GenericResponse.php', | |||
'OCP\\Http\\WellKnown\\IHandler' => __DIR__ . '/../../..' . '/lib/public/Http/WellKnown/IHandler.php', | |||
'OCP\\Http\\WellKnown\\IRequestContext' => __DIR__ . '/../../..' . '/lib/public/Http/WellKnown/IRequestContext.php', | |||
'OCP\\Http\\WellKnown\\IResponse' => __DIR__ . '/../../..' . '/lib/public/Http/WellKnown/IResponse.php', | |||
'OCP\\Http\\WellKnown\\JrdResponse' => __DIR__ . '/../../..' . '/lib/public/Http/WellKnown/JrdResponse.php', | |||
'OCP\\IAddressBook' => __DIR__ . '/../../..' . '/lib/public/IAddressBook.php', | |||
'OCP\\IAppConfig' => __DIR__ . '/../../..' . '/lib/public/IAppConfig.php', | |||
'OCP\\IAvatar' => __DIR__ . '/../../..' . '/lib/public/IAvatar.php', | |||
@@ -921,6 +926,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c | |||
'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php', | |||
'OC\\Core\\Controller\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.php', | |||
'OC\\Core\\Controller\\WebAuthnController' => __DIR__ . '/../../..' . '/core/Controller/WebAuthnController.php', | |||
'OC\\Core\\Controller\\WellKnownController' => __DIR__ . '/../../..' . '/core/Controller/WellKnownController.php', | |||
'OC\\Core\\Controller\\WhatsNewController' => __DIR__ . '/../../..' . '/core/Controller/WhatsNewController.php', | |||
'OC\\Core\\Controller\\WipeController' => __DIR__ . '/../../..' . '/core/Controller/WipeController.php', | |||
'OC\\Core\\Data\\LoginFlowV2Credentials' => __DIR__ . '/../../..' . '/core/Data/LoginFlowV2Credentials.php', | |||
@@ -1165,6 +1171,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c | |||
'OC\\Http\\Client\\ClientService' => __DIR__ . '/../../..' . '/lib/private/Http/Client/ClientService.php', | |||
'OC\\Http\\Client\\Response' => __DIR__ . '/../../..' . '/lib/private/Http/Client/Response.php', | |||
'OC\\Http\\CookieHelper' => __DIR__ . '/../../..' . '/lib/private/Http/CookieHelper.php', | |||
'OC\\Http\\WellKnown\\RequestManager' => __DIR__ . '/../../..' . '/lib/private/Http/WellKnown/RequestManager.php', | |||
'OC\\InitialStateService' => __DIR__ . '/../../..' . '/lib/private/InitialStateService.php', | |||
'OC\\Installer' => __DIR__ . '/../../..' . '/lib/private/Installer.php', | |||
'OC\\IntegrityCheck\\Checker' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Checker.php', |
@@ -72,6 +72,9 @@ class RegistrationContext { | |||
/** @var array[] */ | |||
private $initialStates = []; | |||
/** @var array[] */ | |||
private $wellKnownHandlers = []; | |||
/** @var ILogger */ | |||
private $logger; | |||
@@ -174,6 +177,13 @@ class RegistrationContext { | |||
$class | |||
); | |||
} | |||
public function registerWellKnownHandler(string $class): void { | |||
$this->context->registerWellKnown( | |||
$this->appId, | |||
$class | |||
); | |||
} | |||
}; | |||
} | |||
@@ -260,6 +270,13 @@ class RegistrationContext { | |||
]; | |||
} | |||
public function registerWellKnown(string $appId, string $class): void { | |||
$this->wellKnownHandlers[] = [ | |||
'appId' => $appId, | |||
'class' => $class, | |||
]; | |||
} | |||
/** | |||
* @param App[] $apps | |||
*/ | |||
@@ -437,4 +454,11 @@ class RegistrationContext { | |||
public function getInitialStates(): array { | |||
return $this->initialStates; | |||
} | |||
/** | |||
* @return array[] | |||
*/ | |||
public function getWellKnownHandlers(): array { | |||
return $this->wellKnownHandlers; | |||
} | |||
} |
@@ -0,0 +1,124 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OC\Http\WellKnown; | |||
use OC\AppFramework\Bootstrap\Coordinator; | |||
use OCP\AppFramework\QueryException; | |||
use OCP\Http\WellKnown\IHandler; | |||
use OCP\Http\WellKnown\IRequestContext; | |||
use OCP\Http\WellKnown\IResponse; | |||
use OCP\Http\WellKnown\JrdResponse; | |||
use OCP\IRequest; | |||
use OCP\IServerContainer; | |||
use Psr\Log\LoggerInterface; | |||
use RuntimeException; | |||
use function array_reduce; | |||
class RequestManager { | |||
/** @var Coordinator */ | |||
private $coordinator; | |||
/** @var IServerContainer */ | |||
private $container; | |||
/** @var LoggerInterface */ | |||
private $logger; | |||
public function __construct(Coordinator $coordinator, | |||
IServerContainer $container, | |||
LoggerInterface $logger) { | |||
$this->coordinator = $coordinator; | |||
$this->container = $container; | |||
$this->logger = $logger; | |||
} | |||
public function process(string $service, IRequest $request): ?IResponse { | |||
$handlers = $this->loadHandlers(); | |||
$context = new class($request) implements IRequestContext { | |||
/** @var IRequest */ | |||
private $request; | |||
public function __construct(IRequest $request) { | |||
$this->request = $request; | |||
} | |||
public function getHttpRequest(): IRequest { | |||
return $this->request; | |||
} | |||
}; | |||
$subject = $request->getParam('resource'); | |||
$initialResponse = new JrdResponse($subject ?? ''); | |||
$finalResponse = array_reduce($handlers, function (?IResponse $previousResponse, IHandler $handler) use ($context, $service) { | |||
return $handler->handle($service, $context, $previousResponse); | |||
}, $initialResponse); | |||
if ($finalResponse instanceof JrdResponse && $finalResponse->isEmpty()) { | |||
return null; | |||
} | |||
return $finalResponse; | |||
} | |||
/** | |||
* @return IHandler[] | |||
*/ | |||
private function loadHandlers(): array { | |||
$context = $this->coordinator->getRegistrationContext(); | |||
if ($context === null) { | |||
throw new RuntimeException("Well known handlers requested before the apps had been fully registered"); | |||
} | |||
$registrations = $context->getWellKnownHandlers(); | |||
$this->logger->debug(count($registrations) . " well known handlers registered"); | |||
return array_filter( | |||
array_map(function (array $registration) { | |||
$class = $registration['class']; | |||
try { | |||
$handler = $this->container->get($class); | |||
if (!($handler) instanceof IHandler) { | |||
$this->logger->error("Well known handler $class is invalid"); | |||
return null; | |||
} | |||
return $handler; | |||
} catch (QueryException $e) { | |||
$this->logger->error("Could not load well known handler $class", [ | |||
'exception' => $e, | |||
]); | |||
return null; | |||
} | |||
}, $registrations) | |||
); | |||
} | |||
} |
@@ -180,4 +180,18 @@ interface IRegistrationContext { | |||
* @since 21.0.0 | |||
*/ | |||
public function registerInitialStateProvider(string $class): void; | |||
/** | |||
* Register a well known protocol handler | |||
* | |||
* It is allowed to register more than one handler per app. | |||
* | |||
* @param string $class | |||
* @psalm-param class-string<\OCP\Http\WellKnown\IHandler> $class | |||
* | |||
* @return void | |||
* | |||
* @since 21.0.0 | |||
*/ | |||
public function registerWellKnownHandler(string $class): void; | |||
} |
@@ -0,0 +1,51 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\Http\WellKnown; | |||
use OCP\AppFramework\Http\Response; | |||
/** | |||
* @since 21.0.0 | |||
*/ | |||
final class GenericResponse implements IResponse { | |||
/** @var Response */ | |||
private $response; | |||
/** | |||
* @since 21.0.0 | |||
*/ | |||
public function __construct(Response $response) { | |||
$this->response = $response; | |||
} | |||
/** | |||
* @since 21.0.0 | |||
*/ | |||
public function toHttpResponse(): Response { | |||
return $this->response; | |||
} | |||
} |
@@ -0,0 +1,48 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\Http\WellKnown; | |||
/** | |||
* Interface for an app handler that reacts to requests to Nextcloud's well | |||
* known URLs, e.g. to a WebFinger | |||
* | |||
* @ref https://tools.ietf.org/html/rfc5785 | |||
* | |||
* @since 21.0.0 | |||
*/ | |||
interface IHandler { | |||
/** | |||
* @param string $service the name of the well known service, e.g. 'webfinger' | |||
* @param IRequestContext $context | |||
* @param IResponse|null $previousResponse the response of the previous handler, if any | |||
* | |||
* @return IResponse|null a response object if the request could be handled, null otherwise | |||
* | |||
* @since 21.0.0 | |||
*/ | |||
public function handle(string $service, IRequestContext $context, ?IResponse $previousResponse): ?IResponse; | |||
} |
@@ -0,0 +1,46 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\Http\WellKnown; | |||
use OCP\IRequest; | |||
/** | |||
* The context object for \OCP\Http\IWellKnownHandler::handle | |||
* | |||
* Objects of this type will transport any optional information, e.g. the request | |||
* object through which the app well known handler can obtain URL parameters | |||
* | |||
* @since 21.0.0 | |||
*/ | |||
interface IRequestContext { | |||
/** | |||
* @return IRequest | |||
* | |||
* @since 21.0.0 | |||
*/ | |||
public function getHttpRequest(): IRequest; | |||
} |
@@ -0,0 +1,39 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\Http\WellKnown; | |||
use OCP\AppFramework\Http\Response; | |||
/** | |||
* @since 21.0.0 | |||
*/ | |||
interface IResponse { | |||
/** | |||
* @since 21.0.0 | |||
*/ | |||
public function toHttpResponse(): Response; | |||
} |
@@ -0,0 +1,170 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace OCP\Http\WellKnown; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OCP\AppFramework\Http\Response; | |||
use function array_filter; | |||
/** | |||
* A JSON Document Format (JDF) response to a well-known request | |||
* | |||
* @ref https://tools.ietf.org/html/rfc6415#appendix-A | |||
* @ref https://tools.ietf.org/html/rfc7033#section-4.4 | |||
* | |||
* @since 21.0.0 | |||
*/ | |||
final class JrdResponse implements IResponse { | |||
/** @var string */ | |||
private $subject; | |||
/** @var string|null */ | |||
private $expires; | |||
/** @var string[] */ | |||
private $aliases = []; | |||
/** @var (string|null)[] */ | |||
private $properties = []; | |||
/** @var mixed[] */ | |||
private $links; | |||
/** | |||
* @param string $subject https://tools.ietf.org/html/rfc7033#section-4.4.1 | |||
* | |||
* @since 21.0.0 | |||
*/ | |||
public function __construct(string $subject) { | |||
$this->subject = $subject; | |||
} | |||
/** | |||
* @param string $expires | |||
* | |||
* @return $this | |||
* | |||
* @since 21.0.0 | |||
*/ | |||
public function setExpires(string $expires): self { | |||
$this->expires = $expires; | |||
return $this; | |||
} | |||
/** | |||
* Add an alias | |||
* | |||
* @ref https://tools.ietf.org/html/rfc7033#section-4.4.2 | |||
* | |||
* @param string $alias | |||
* | |||
* @return $this | |||
* | |||
* @since 21.0.0 | |||
*/ | |||
public function addAlias(string $alias): self { | |||
$this->aliases[] = $alias; | |||
return $this; | |||
} | |||
/** | |||
* Add a property | |||
* | |||
* @ref https://tools.ietf.org/html/rfc7033#section-4.4.3 | |||
* | |||
* @param string $property | |||
* @param string|null $value | |||
* | |||
* @return $this | |||
* | |||
* @since 21.0.0 | |||
*/ | |||
public function addProperty(string $property, ?string $value): self { | |||
$this->properties[$property] = $value; | |||
return $this; | |||
} | |||
/** | |||
* Add a link | |||
* | |||
* @ref https://tools.ietf.org/html/rfc7033#section-8.4 | |||
* | |||
* @param string $rel https://tools.ietf.org/html/rfc7033#section-4.4.4.1 | |||
* @param string|null $type https://tools.ietf.org/html/rfc7033#section-4.4.4.2 | |||
* @param string|null $href https://tools.ietf.org/html/rfc7033#section-4.4.4.3 | |||
* @param string[]|null $titles https://tools.ietf.org/html/rfc7033#section-4.4.4.4 | |||
* @param string|null $properties https://tools.ietf.org/html/rfc7033#section-4.4.4.5 | |||
* | |||
* @psalm-param array<string,(string|null)>|null $properties https://tools.ietf.org/html/rfc7033#section-4.4.4.5 | |||
* | |||
* @return JrdResponse | |||
* @since 21.0.0 | |||
*/ | |||
public function addLink(string $rel, | |||
?string $type, | |||
?string $href, | |||
?array $titles = [], | |||
?array $properties = []): self { | |||
$this->links[] = array_filter([ | |||
'rel' => $rel, | |||
'type' => $type, | |||
'href' => $href, | |||
'titles' => $titles, | |||
'properties' => $properties, | |||
]); | |||
return $this; | |||
} | |||
/** | |||
* @since 21.0.0 | |||
*/ | |||
public function toHttpResponse(): Response { | |||
return new JSONResponse(array_filter([ | |||
'subject' => $this->subject, | |||
'expires' => $this->expires, | |||
'aliases' => $this->aliases, | |||
'properties' => $this->properties, | |||
'links' => $this->links, | |||
])); | |||
} | |||
/** | |||
* Does this response have any data attached to it? | |||
* | |||
* @since 21.0.0 | |||
*/ | |||
public function isEmpty(): bool { | |||
return $this->expires === null | |||
&& empty($this->aliases) | |||
&& empty($this->properties) | |||
&& empty($this->links); | |||
} | |||
} |
@@ -0,0 +1,86 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace Tests\Core\Controller; | |||
use OC\Core\Controller\WellKnownController; | |||
use OC\Http\WellKnown\RequestManager; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OCP\Http\WellKnown\IResponse; | |||
use OCP\IRequest; | |||
use PHPUnit\Framework\MockObject\MockObject; | |||
use Test\TestCase; | |||
class WellKnownControllerTest extends TestCase { | |||
/** @var IRequest|MockObject */ | |||
private $request; | |||
/** @var RequestManager|MockObject */ | |||
private $manager; | |||
/** @var WellKnownController */ | |||
private $controller; | |||
protected function setUp(): void { | |||
parent::setUp(); | |||
$this->request = $this->createMock(IRequest::class); | |||
$this->manager = $this->createMock(RequestManager::class); | |||
$this->controller = new WellKnownController( | |||
$this->request, | |||
$this->manager, | |||
); | |||
} | |||
public function testHandleNotProcessed(): void { | |||
$httpResponse = $this->controller->handle("nodeinfo"); | |||
self::assertInstanceOf(JSONResponse::class, $httpResponse); | |||
self::assertArrayHasKey('X-NEXTCLOUD-WELL-KNOWN', $httpResponse->getHeaders()); | |||
} | |||
public function testHandle(): void { | |||
$response = $this->createMock(IResponse::class); | |||
$jsonResponse = $this->createMock(JSONResponse::class); | |||
$response->expects(self::once()) | |||
->method('toHttpResponse') | |||
->willReturn($jsonResponse); | |||
$this->manager->expects(self::once()) | |||
->method('process') | |||
->with( | |||
"nodeinfo", | |||
$this->request | |||
)->willReturn($response); | |||
$jsonResponse->expects(self::once()) | |||
->method('addHeader') | |||
->willReturnSelf(); | |||
$httpResponse = $this->controller->handle("nodeinfo"); | |||
self::assertInstanceOf(JSONResponse::class, $httpResponse); | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace Tests\Http\WellKnown; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OCP\Http\WellKnown\GenericResponse; | |||
use Test\TestCase; | |||
class GenericResponseTest extends TestCase { | |||
public function testToHttpResponse(): void { | |||
$httpResponse = $this->createMock(JSONResponse::class); | |||
$response = new GenericResponse($httpResponse); | |||
self::assertSame($httpResponse, $response->toHttpResponse()); | |||
} | |||
} |
@@ -0,0 +1,108 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace Test\Http\WellKnown; | |||
use OCP\AppFramework\Http\JSONResponse; | |||
use OCP\Http\WellKnown\JrdResponse; | |||
use Test\TestCase; | |||
class JrdResponseTest extends TestCase { | |||
public function testEmptyToHttpResponse(): void { | |||
$response = new JrdResponse("subject"); | |||
$httpResponse = $response->toHttpResponse(); | |||
self::assertTrue($response->isEmpty()); | |||
self::assertInstanceOf(JSONResponse::class, $httpResponse); | |||
/** @var JSONResponse $httpResponse */ | |||
self::assertEquals( | |||
[ | |||
'subject' => 'subject', | |||
], | |||
$httpResponse->getData() | |||
); | |||
} | |||
public function testComplexToHttpResponse(): void { | |||
$response = new JrdResponse("subject"); | |||
$response->addAlias('alias'); | |||
$response->addAlias('blias'); | |||
$response->addProperty('propa', 'a'); | |||
$response->addProperty('propb', null); | |||
$response->setExpires('tomorrow'); | |||
$response->addLink('rel', null, null); | |||
$response->addLink('rel', 'type', null); | |||
$response->addLink('rel', 'type', 'href', ['title' => 'titlevalue']); | |||
$response->addLink('rel', 'type', 'href', ['title' => 'titlevalue'], ['propx' => 'valx']); | |||
$httpResponse = $response->toHttpResponse(); | |||
self::assertFalse($response->isEmpty()); | |||
self::assertInstanceOf(JSONResponse::class, $httpResponse); | |||
/** @var JSONResponse $httpResponse */ | |||
self::assertEquals( | |||
[ | |||
'subject' => 'subject', | |||
'aliases' => [ | |||
'alias', | |||
'blias', | |||
], | |||
'properties' => [ | |||
'propa' => 'a', | |||
'propb' => null, | |||
], | |||
'expires' => 'tomorrow', | |||
'links' => [ | |||
[ | |||
'rel' => 'rel', | |||
], | |||
[ | |||
'rel' => 'rel', | |||
'type' => 'type', | |||
], | |||
[ | |||
'rel' => 'rel', | |||
'type' => 'type', | |||
'href' => 'href', | |||
'titles' => [ | |||
'title' => 'titlevalue', | |||
], | |||
], | |||
[ | |||
'rel' => 'rel', | |||
'type' => 'type', | |||
'href' => 'href', | |||
'titles' => [ | |||
'title' => 'titlevalue', | |||
], | |||
'properties' => [ | |||
'propx' => 'valx', | |||
], | |||
], | |||
] | |||
], | |||
$httpResponse->getData() | |||
); | |||
} | |||
} |
@@ -0,0 +1,176 @@ | |||
<?php | |||
declare(strict_types=1); | |||
/* | |||
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This program is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License as | |||
* published by the Free Software Foundation, either version 3 of the | |||
* License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License | |||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
*/ | |||
namespace Test\Http\WellKnown; | |||
use OC\AppFramework\Bootstrap\Coordinator; | |||
use OC\AppFramework\Bootstrap\RegistrationContext; | |||
use OC\Http\WellKnown\RequestManager; | |||
use OCP\AppFramework\QueryException; | |||
use OCP\Http\WellKnown\IHandler; | |||
use OCP\Http\WellKnown\IRequestContext; | |||
use OCP\Http\WellKnown\IResponse; | |||
use OCP\Http\WellKnown\JrdResponse; | |||
use OCP\IRequest; | |||
use OCP\IServerContainer; | |||
use PHPUnit\Framework\MockObject\MockObject; | |||
use Psr\Log\LoggerInterface; | |||
use RuntimeException; | |||
use Test\TestCase; | |||
use function get_class; | |||
class RequestManagerTest extends TestCase { | |||
/** @var Coordinator|MockObject */ | |||
private $coordinator; | |||
/** @var IServerContainer|MockObject */ | |||
private $container; | |||
/** @var MockObject|LoggerInterface */ | |||
private $logger; | |||
/** @var RequestManager */ | |||
private $manager; | |||
protected function setUp(): void { | |||
parent::setUp(); | |||
$this->coordinator = $this->createMock(Coordinator::class); | |||
$this->container = $this->createMock(IServerContainer::class); | |||
$this->logger = $this->createMock(LoggerInterface::class); | |||
$this->manager = new RequestManager( | |||
$this->coordinator, | |||
$this->container, | |||
$this->logger, | |||
); | |||
} | |||
public function testProcessAppsNotRegistered(): void { | |||
$request = $this->createMock(IRequest::class); | |||
$this->expectException(RuntimeException::class); | |||
$this->manager->process("webfinger", $request); | |||
} | |||
public function testProcessNoHandlersRegistered(): void { | |||
$request = $this->createMock(IRequest::class); | |||
$registrationContext = $this->createMock(RegistrationContext::class); | |||
$this->coordinator->expects(self::once()) | |||
->method('getRegistrationContext') | |||
->willReturn($registrationContext); | |||
$registrationContext->expects(self::once()) | |||
->method('getWellKnownHandlers') | |||
->willReturn([]); | |||
$response = $this->manager->process("webfinger", $request); | |||
self::assertNull($response); | |||
} | |||
public function testProcessHandlerNotLoadable(): void { | |||
$request = $this->createMock(IRequest::class); | |||
$registrationContext = $this->createMock(RegistrationContext::class); | |||
$this->coordinator->expects(self::once()) | |||
->method('getRegistrationContext') | |||
->willReturn($registrationContext); | |||
$handler = new class { | |||
}; | |||
$registrationContext->expects(self::once()) | |||
->method('getWellKnownHandlers') | |||
->willReturn([ | |||
[ | |||
'class' => get_class($handler), | |||
], | |||
]); | |||
$this->container->expects(self::once()) | |||
->method('get') | |||
->with(get_class($handler)) | |||
->willThrowException(new QueryException("")); | |||
$this->logger->expects(self::once()) | |||
->method('error'); | |||
$response = $this->manager->process("webfinger", $request); | |||
self::assertNull($response); | |||
} | |||
public function testProcessHandlerOfWrongType(): void { | |||
$request = $this->createMock(IRequest::class); | |||
$registrationContext = $this->createMock(RegistrationContext::class); | |||
$this->coordinator->expects(self::once()) | |||
->method('getRegistrationContext') | |||
->willReturn($registrationContext); | |||
$handler = new class { | |||
}; | |||
$registrationContext->expects(self::once()) | |||
->method('getWellKnownHandlers') | |||
->willReturn([ | |||
[ | |||
'class' => get_class($handler), | |||
], | |||
]); | |||
$this->container->expects(self::once()) | |||
->method('get') | |||
->with(get_class($handler)) | |||
->willReturn($handler); | |||
$this->logger->expects(self::once()) | |||
->method('error'); | |||
$response = $this->manager->process("webfinger", $request); | |||
self::assertNull($response); | |||
} | |||
public function testProcess(): void { | |||
$request = $this->createMock(IRequest::class); | |||
$registrationContext = $this->createMock(RegistrationContext::class); | |||
$this->coordinator->expects(self::once()) | |||
->method('getRegistrationContext') | |||
->willReturn($registrationContext); | |||
$handler = new class implements IHandler { | |||
public function handle(string $service, IRequestContext $context, ?IResponse $previousResponse): ?IResponse { | |||
return (new JrdResponse($service))->addAlias('alias'); | |||
} | |||
}; | |||
$registrationContext->expects(self::once()) | |||
->method('getWellKnownHandlers') | |||
->willReturn([ | |||
[ | |||
'class' => get_class($handler), | |||
], | |||
]); | |||
$this->container->expects(self::once()) | |||
->method('get') | |||
->with(get_class($handler)) | |||
->willReturn($handler); | |||
$response = $this->manager->process("webfinger", $request); | |||
self::assertNotNull($response); | |||
self::assertInstanceOf(JrdResponse::class, $response); | |||
} | |||
} |