diff --git a/ProcessIndieAuth.module.php b/ProcessIndieAuth.module.php index 9295b58..e498eeb 100644 --- a/ProcessIndieAuth.module.php +++ b/ProcessIndieAuth.module.php @@ -926,9 +926,9 @@ public function authorizationEndpoint(): void $request['client_id'] = Server::canonizeUrl($request['client_id']); $request['redirect_uri'] = Server::canonizeUrl($request['redirect_uri']); - $client = $this->getClientInfo($request['client_id']); + $client = Server::getClientInfo($request['client_id']); - if (!Server::isRedirectUriAllowed($request['redirect_uri'], $request['client_id'], $client['redirect_uri'])) { + if (!Server::isRedirectUriAllowed($request['redirect_uri'], $request['client_id'], $client['redirect_uris'])) { $this->httpResponse('mismatched redirect_uri'); } @@ -2038,6 +2038,11 @@ private function httpResponse($response, int $http_status = 400, array $headers exit; } + /** + * DEPRECATED + * + * @see Client::getInfo() + */ private function getClientInfo(string $url): array { $info = array_fill_keys([ diff --git a/src/IndieAuth/Server.php b/src/IndieAuth/Server.php index 0cbafd1..d733c47 100644 --- a/src/IndieAuth/Server.php +++ b/src/IndieAuth/Server.php @@ -11,8 +11,81 @@ namespace IndieAuth; -final class Server +use IndieAuth\Libs\{ + Barnabywalters\Mf2 as Mf2Helper, + Mf2 +}; +use ProcessWire\WireHttp; + +final class Server { + /** + * Get client information + * First check if the client_id itself is the info (JSON) + * Fallback to checking for h-app or h-x-app + * + * @see https://github.com/indieweb/indieauth/issues/133 + */ + public static function getClientInfo(string $client_id): array + { + $headers = static::fetchHead($client_id, ['content-type']); + + $is_json = false; + if ($headers) { + if (static::hasContentType('application/json', $headers['content-type'])) { + $is_json = true; + } + } + + $content = static::fetchBody($client_id); + + if ($is_json) { + $json = json_decode($content, true); + if ($json) { + return [ + 'name' => $json['client_name'] ?? '', + 'url' => $json['client_uri'] ?? '', + 'logo' => $json['logo_uri'] ?? '', + 'redirect_uris' => $json['redirect_uris'] ?? [], + ]; + } + } + + $info = array_fill_keys([ + 'name', + 'url', + 'logo', + ], ''); + + $info['name'] = parse_url($client_id, PHP_URL_HOST); + $info['url'] = $client_id; + $info['redirect_uris'] = []; + + $mf2 = Mf2\parse($content, $client_id); + $info['redirect_uris'] = $mf2['rels']['redirect_uri'] ?? []; + + $apps = Mf2Helper\findMicroformatsByType($mf2, 'h-app'); + if (!$apps) { + $apps = Mf2Helper\findMicroformatsByType($mf2, 'h-x-app'); + } + + if (!$apps) { + return $info; + } + + $app = reset($apps); + + if (Mf2Helper\hasProp($app, 'name')) { + $info['name'] = Mf2Helper\getPlaintext($app, 'name'); + } + + if (Mf2Helper\hasProp($app, 'logo')) { + $info['logo'] = Mf2Helper\getPlaintext($app, 'logo'); + } + + return $info; + } + /** * @see https://indieauth.spec.indieweb.org/#client-identifier */ @@ -169,5 +242,66 @@ public static function base64UrlEncode(string $input): string { return rtrim(strtr(base64_encode($input), '+/', '-_'), '='); } + + /** + * Perform a HEAD request and return an array of headers + * If $requested_headers is provided, only return those headers. + * For example, `$requested_headers = ['link']` will return + * only the Link: headers + */ + private static function fetchHead( + string $url, + array $requested_headers = [] + ): ?array { + $http = new WireHttp(); + + $response = $http->status($url); + if (!$response) { + return null; + } + + $http_headers = $http->getResponseHeaderValues('', true); + + if ($requested_headers) { + $http_headers = array_intersect_key( + $http_headers, + array_fill_keys($requested_headers, []) + ); + + if (!$http_headers) { + return null; + } + } + + return $http_headers; + } + + /** + * Fetch the content of a URL + */ + private static function fetchBody(string $url): string + { + $http = new WireHttp(); + $content = $http->get($url); + + if (!$content) { + return ''; + } + + return $content; + } + + private static function hasContentType( + string $expected, + array $content_types + ) { + foreach ($content_types as $content_type) { + if (stripos($content_type, $expected) !== false) { + return true; + } + } + + return false; + } }