| Total Complexity | 121 | 
| Total Lines | 796 | 
| Duplicated Lines | 0 % | 
| Changes | 0 | ||
Complex classes like AbstractOpenId often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use AbstractOpenId, and based on these observations, apply Extract Interface, too.
| 1 | <?php | ||
| 36 | abstract class AbstractOpenId extends AbstractAuthClient | ||
| 37 | { | ||
| 38 | /** | ||
| 39 | * @var string authentication base URL, which should be used to compose actual authentication URL | ||
| 40 |      * by {@see buildAuthUrl()} method. | ||
| 41 | */ | ||
| 42 | private string $authUrl; | ||
| 43 | /** | ||
| 44 | * @var array list of attributes, which always should be returned from server. | ||
| 45 | * Attribute names should be always specified in AX format. | ||
| 46 | * For example: | ||
| 47 | * | ||
| 48 | * ```php | ||
| 49 | * ['namePerson/friendly', 'contact/email'] | ||
| 50 | * ``` | ||
| 51 | */ | ||
| 52 | private array $requiredAttributes = []; | ||
| 53 | /** | ||
| 54 | * @var array list of attributes, which could be returned from server. | ||
| 55 | * Attribute names should be always specified in AX format. | ||
| 56 | * For example: | ||
| 57 | * | ||
| 58 | * ```php | ||
| 59 | * ['namePerson/first', 'namePerson/last'] | ||
| 60 | * ``` | ||
| 61 | */ | ||
| 62 | private array $optionalAttributes = []; | ||
| 63 | /** | ||
| 64 | * @var bool whether to verify the peer's certificate. | ||
| 65 | */ | ||
| 66 | private bool $verifyPeer; | ||
| 67 | /** | ||
| 68 | * @var string directory that holds multiple CA certificates. | ||
| 69 | * This value will take effect only if [[verifyPeer]] is set. | ||
| 70 | */ | ||
| 71 | private string $capath; | ||
| 72 | /** | ||
| 73 | * @var string the name of a file holding one or more certificates to verify the peer with. | ||
| 74 | * This value will take effect only if [[verifyPeer]] is set. | ||
| 75 | */ | ||
| 76 | private string $cainfo; | ||
| 77 | /** | ||
| 78 | * @var array data, which should be used to retrieve the OpenID response. | ||
| 79 | * If not set combination of GET and POST will be used. | ||
| 80 | */ | ||
| 81 | private array $data; | ||
| 82 | /** | ||
| 83 | * @var array map of matches between AX and SREG attribute names in format: axAttributeName => sregAttributeName | ||
| 84 | */ | ||
| 85 | private array $axToSregMap = [ | ||
| 86 | 'namePerson/friendly' => 'nickname', | ||
| 87 | 'contact/email' => 'email', | ||
| 88 | 'namePerson' => 'fullname', | ||
| 89 | 'birthDate' => 'dob', | ||
| 90 | 'person/gender' => 'gender', | ||
| 91 | 'contact/postalCode/home' => 'postcode', | ||
| 92 | 'contact/country/home' => 'country', | ||
| 93 | 'pref/language' => 'language', | ||
| 94 | 'pref/timezone' => 'timezone', | ||
| 95 | ]; | ||
| 96 | |||
| 97 | /** | ||
| 98 | * @var string authentication return URL. | ||
| 99 | */ | ||
| 100 | private string $returnUrl; | ||
| 101 | /** | ||
| 102 | * @var string claimed identifier (identity) | ||
| 103 | */ | ||
| 104 | private string $claimedId; | ||
| 105 | /** | ||
| 106 | * @var string client trust root (realm), by default [[\yii\web\Request::hostInfo]] value will be used. | ||
| 107 | */ | ||
| 108 | private string $trustRoot; | ||
| 109 | |||
| 110 | /** | ||
| 111 | * Returns authentication URL. Usually, you want to redirect your user to it. | ||
| 112 | * | ||
| 113 | * @param ServerRequestInterface $incomingRequest | ||
| 114 | * @param bool $identifierSelect whether to request OP to select identity for an user in OpenID 2, does not affect OpenID 1. | ||
| 115 | * | ||
| 116 | * @param array $params | ||
| 117 | * | ||
| 118 | * @return string the authentication URL. | ||
| 119 | */ | ||
| 120 | public function buildAuthUrl(ServerRequestInterface $incomingRequest, array $params = []): string | ||
| 121 |     { | ||
| 122 | $authUrl = $this->authUrl; | ||
| 123 | $claimedId = $this->getClaimedId(); | ||
| 124 |         if (empty($claimedId)) { | ||
| 125 | $this->setClaimedId($authUrl); | ||
| 126 | } | ||
| 127 | $serverInfo = $this->discover($authUrl); | ||
| 128 |         if ($serverInfo['version'] === 2) { | ||
| 129 |             if (isset($params['identifierSelect'])) { | ||
| 130 | $serverInfo['identifier_select'] = $params['identifierSelect']; | ||
| 131 | } | ||
| 132 | |||
| 133 | return $this->buildAuthUrlV2($incomingRequest, $serverInfo); | ||
| 134 | } | ||
| 135 | |||
| 136 | return $this->buildAuthUrlV1($incomingRequest, $serverInfo); | ||
| 137 | } | ||
| 138 | |||
| 139 | /** | ||
| 140 | * @return string claimed identifier (identity). | ||
| 141 | */ | ||
| 142 | public function getClaimedId(): string | ||
| 143 |     { | ||
| 144 |         if ($this->claimedId === null) { | ||
| 145 |             if (isset($this->data['openid_claimed_id'])) { | ||
| 146 | $this->claimedId = $this->data['openid_claimed_id']; | ||
| 147 |             } elseif (isset($this->data['openid_identity'])) { | ||
| 148 | $this->claimedId = $this->data['openid_identity']; | ||
| 149 | } | ||
| 150 | } | ||
| 151 | |||
| 152 | return $this->claimedId; | ||
| 153 | } | ||
| 154 | |||
| 155 | /** | ||
| 156 | * @param string $claimedId claimed identifier (identity). | ||
| 157 | */ | ||
| 158 | public function setClaimedId(string $claimedId): void | ||
| 159 |     { | ||
| 160 | $this->claimedId = $claimedId; | ||
| 161 | } | ||
| 162 | |||
| 163 | /** | ||
| 164 | * Performs Yadis and HTML discovery. | ||
| 165 | * | ||
| 166 | * @param string $url Identity URL. | ||
| 167 | * | ||
| 168 | * @return array OpenID provider info, following keys will be available: | ||
| 169 | * | ||
| 170 | * - url: string, OP Endpoint (i.e. OpenID provider address). | ||
| 171 | * - version: int, OpenID protocol version used by provider. | ||
| 172 | * - identity: string, identity value. | ||
| 173 | * - identifier_select: bool, whether to request OP to select identity for an user in OpenID 2, does not affect OpenID 1. | ||
| 174 | * - ax: bool, whether AX attributes should be used. | ||
| 175 | * - sreg: bool, whether SREG attributes should be used. | ||
| 176 | */ | ||
| 177 | public function discover(string $url): array | ||
| 178 |     { | ||
| 179 |         if (empty($url)) { | ||
| 180 |             throw new \RuntimeException('No identity supplied.'); | ||
| 181 | } | ||
| 182 | $result = [ | ||
| 183 | 'url' => null, | ||
| 184 | 'version' => null, | ||
| 185 | 'identity' => $url, | ||
| 186 | 'identifier_select' => false, | ||
| 187 | 'ax' => false, | ||
| 188 | 'sreg' => false, | ||
| 189 | ]; | ||
| 190 | |||
| 191 | // Use xri.net proxy to resolve i-name identities | ||
| 192 |         if (!preg_match('#^https?:#', $url)) { | ||
| 193 | $url = 'https://xri.net/' . $url; | ||
| 194 | } | ||
| 195 | |||
| 196 | /* We save the original url in case of Yadis discovery failure. | ||
| 197 | It can happen when we'll be lead to an XRDS document | ||
| 198 | which does not have any OpenID2 services.*/ | ||
| 199 | $originalUrl = $url; | ||
| 200 | |||
| 201 | // A flag to disable yadis discovery in case of failure in headers. | ||
| 202 | $yadis = true; | ||
| 203 | |||
| 204 | // We'll jump a maximum of 5 times, to avoid endless redirections. | ||
| 205 |         for ($i = 0; $i < 5; $i++) { | ||
| 206 |             if ($yadis) { | ||
| 207 |                 $request = $this->createRequest('HEAD', $url); | ||
| 208 | $headers = []; | ||
| 209 | $response = $this->sendRequest($request); | ||
| 210 |                 foreach ($response->getHeaders() as $name => $values) { | ||
| 211 | $headers[strtolower($name)] = array_pop($values); | ||
| 212 | } | ||
| 213 | |||
| 214 | $next = false; | ||
| 215 |                 if (isset($headers['x-xrds-location'])) { | ||
| 216 | $url = $this->buildUrl($url, trim($headers['x-xrds-location'])); | ||
| 217 | $next = true; | ||
| 218 | } | ||
| 219 | |||
| 220 | if (isset($headers['content-type']) | ||
| 221 | && (strpos($headers['content-type'], 'application/xrds+xml') !== false | ||
| 222 | || strpos($headers['content-type'], 'text/xml') !== false) | ||
| 223 |                 ) { | ||
| 224 | /* Apparently, some providers return XRDS documents as text/html. | ||
| 225 | While it is against the spec, allowing this here shouldn't break | ||
| 226 | compatibility with anything. | ||
| 227 | --- | ||
| 228 | Found an XRDS document, now let's find the server, and optionally delegate.*/ | ||
| 229 | $content = $this->sendRequest( | ||
| 230 |                         $request->withMethod('GET')->withUri($request->getUri()->withPath($url)) | ||
| 231 | )->getBody()->getContents(); | ||
| 232 | |||
| 233 |                     preg_match_all('#<Service.*?>(.*?)</Service>#s', $content, $m); | ||
| 234 |                     foreach ($m[1] as $content) { | ||
| 235 | $content = ' ' . $content; // The space is added, so that strpos doesn't return 0. | ||
| 236 | |||
| 237 | // OpenID 2 | ||
| 238 |                         $ns = preg_quote('http://specs.openid.net/auth/2.0/', '#'); | ||
| 239 |                         if (preg_match('#<Type>\s*' . $ns . '(server|signon)\s*</Type>#s', $content, $type)) { | ||
| 240 |                             if ($type[1] === 'server') { | ||
| 241 | $result['identifier_select'] = true; | ||
| 242 | } | ||
| 243 | |||
| 244 |                             preg_match('#<URI.*?>(.*)</URI>#', $content, $server); | ||
| 245 |                             preg_match('#<(Local|Canonical)ID>(.*)</\1ID>#', $content, $delegate); | ||
| 246 |                             if (empty($server)) { | ||
| 247 |                                 throw new \RuntimeException('No servers found!'); | ||
| 248 | } | ||
| 249 | // Does the server advertise support for either AX or SREG? | ||
| 250 | $result['ax'] = (bool)strpos($content, '<Type>http://openid.net/srv/ax/1.0</Type>'); | ||
| 251 | $result['sreg'] = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>') | ||
| 252 | || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>'); | ||
| 253 | |||
| 254 | $server = $server[1]; | ||
| 255 |                             if (isset($delegate[2])) { | ||
| 256 | $result['identity'] = trim($delegate[2]); | ||
| 257 | } | ||
| 258 | |||
| 259 | $result['url'] = $server; | ||
| 260 | $result['version'] = 2; | ||
| 261 | |||
| 262 | return $result; | ||
| 263 | } | ||
| 264 | |||
| 265 | // OpenID 1.1 | ||
| 266 |                         $ns = preg_quote('http://openid.net/signon/1.1', '#'); | ||
| 267 |                         if (preg_match('#<Type>\s*' . $ns . '\s*</Type>#s', $content)) { | ||
| 268 |                             preg_match('#<URI.*?>(.*)</URI>#', $content, $server); | ||
| 269 |                             preg_match('#<.*?Delegate>(.*)</.*?Delegate>#', $content, $delegate); | ||
| 270 |                             if (empty($server)) { | ||
| 271 |                                 throw new \RuntimeException('No servers found!'); | ||
| 272 | } | ||
| 273 | // AX can be used only with OpenID 2.0, so checking only SREG | ||
| 274 | $result['sreg'] = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>') | ||
| 275 | || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>'); | ||
| 276 | |||
| 277 | $server = $server[1]; | ||
| 278 |                             if (isset($delegate[1])) { | ||
| 279 | $result['identity'] = $delegate[1]; | ||
| 280 | } | ||
| 281 | |||
| 282 | $result['url'] = $server; | ||
| 283 | $result['version'] = 1; | ||
| 284 | |||
| 285 | return $result; | ||
| 286 | } | ||
| 287 | } | ||
| 288 | |||
| 289 | $next = true; | ||
|  | |||
| 290 | $yadis = false; | ||
| 291 | $url = $originalUrl; | ||
| 292 | $content = null; | ||
| 293 | break; | ||
| 294 | } | ||
| 295 |                 if ($next) { | ||
| 296 | continue; | ||
| 297 | } | ||
| 298 | |||
| 299 | // There are no relevant information in headers, so we search the body. | ||
| 300 | $content = $this->sendRequest( | ||
| 301 |                     $request->withMethod('GET')->withUri($request->getUri()->withPath($url)) | ||
| 302 | )->getBody()->getContents(); | ||
| 303 | |||
| 304 | $location = $this->extractHtmlTagValue($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content'); | ||
| 305 |                 if ($location) { | ||
| 306 | $url = $this->buildUrl($url, $location); | ||
| 307 | continue; | ||
| 308 | } | ||
| 309 | } | ||
| 310 | |||
| 311 |             if (!isset($content)) { | ||
| 312 |                 $request = $this->createRequest('GET', $url); | ||
| 313 | $content = $this->sendRequest($request); | ||
| 314 | } | ||
| 315 | |||
| 316 | // At this point, the YADIS Discovery has failed, so we'll switch to openid2 HTML discovery, then fallback to openid 1.1 discovery. | ||
| 317 | $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid2.provider', 'href'); | ||
| 318 |             if (!$server) { | ||
| 319 | // The same with openid 1.1 | ||
| 320 | $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.server', 'href'); | ||
| 321 | $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.delegate', 'href'); | ||
| 322 | $version = 1; | ||
| 323 |             } else { | ||
| 324 | $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid2.local_id', 'href'); | ||
| 325 | $version = 2; | ||
| 326 | } | ||
| 327 | |||
| 328 |             if ($server) { | ||
| 329 | // We found an OpenID2 OP Endpoint | ||
| 330 |                 if ($delegate) { | ||
| 331 | // We have also found an OP-Local ID. | ||
| 332 | $result['identity'] = $delegate; | ||
| 333 | } | ||
| 334 | $result['url'] = $server; | ||
| 335 | $result['version'] = $version; | ||
| 336 | |||
| 337 | return $result; | ||
| 338 | } | ||
| 339 |             throw new \RuntimeException('No servers found!'); | ||
| 340 | } | ||
| 341 |         throw new \RuntimeException('Endless redirection!'); | ||
| 342 | } | ||
| 343 | |||
| 344 | /** | ||
| 345 | * Combines given URLs into single one. | ||
| 346 | * | ||
| 347 | * @param string $baseUrl base URL. | ||
| 348 | * @param array|string $additionalUrl additional URL string or information array. | ||
| 349 | * | ||
| 350 | * @return string composed URL. | ||
| 351 | */ | ||
| 352 | protected function buildUrl(string $baseUrl, $additionalUrl): string | ||
| 353 |     { | ||
| 354 | $baseUrl = parse_url($baseUrl); | ||
| 355 |         if (!is_array($additionalUrl)) { | ||
| 356 | $additionalUrl = parse_url($additionalUrl); | ||
| 357 | } | ||
| 358 | |||
| 359 |         if (isset($baseUrl['query'], $additionalUrl['query'])) { | ||
| 360 | $additionalUrl['query'] = $baseUrl['query'] . '&' . $additionalUrl['query']; | ||
| 361 | } | ||
| 362 | |||
| 363 | $urlInfo = array_merge($baseUrl, $additionalUrl); | ||
| 364 | return $urlInfo['scheme'] . '://' | ||
| 365 | . (empty($urlInfo['username']) ? '' | ||
| 366 |                 : (empty($urlInfo['password']) ? "{$urlInfo['username']}@" | ||
| 367 |                     : "{$urlInfo['username']}:{$urlInfo['password']}@")) | ||
| 368 | . $urlInfo['host'] | ||
| 369 |             . (empty($urlInfo['port']) ? '' : ":{$urlInfo['port']}") | ||
| 370 | . (empty($urlInfo['path']) ? '' : $urlInfo['path']) | ||
| 371 |             . (empty($urlInfo['query']) ? '' : "?{$urlInfo['query']}") | ||
| 372 |             . (empty($urlInfo['fragment']) ? '' : "#{$urlInfo['fragment']}"); | ||
| 373 | } | ||
| 374 | |||
| 375 | /** | ||
| 376 | * Scans content for <meta>/<link> tags and extract information from them. | ||
| 377 | * | ||
| 378 | * @param string $content HTML content to be be parsed. | ||
| 379 | * @param string $tag name of the source tag. | ||
| 380 | * @param string $matchAttributeName name of the source tag attribute, which should contain $matchAttributeValue | ||
| 381 | * @param string $matchAttributeValue required value of $matchAttributeName | ||
| 382 | * @param string $valueAttributeName name of the source tag attribute, which should contain searched value. | ||
| 383 | * | ||
| 384 | * @return bool|string searched value, "false" on failure. | ||
| 385 | */ | ||
| 386 | protected function extractHtmlTagValue( | ||
| 387 | string $content, | ||
| 388 | string $tag, | ||
| 389 | string $matchAttributeName, | ||
| 390 | string $matchAttributeValue, | ||
| 391 | string $valueAttributeName | ||
| 392 |     ) { | ||
| 393 | preg_match_all( | ||
| 394 |             "#<{$tag}[^>]*$matchAttributeName=['\"].*?$matchAttributeValue.*?['\"][^>]*$valueAttributeName=['\"](.+?)['\"][^>]*/?>#i", | ||
| 395 | $content, | ||
| 396 | $matches1 | ||
| 397 | ); | ||
| 398 | preg_match_all( | ||
| 399 |             "#<{$tag}[^>]*$valueAttributeName=['\"](.+?)['\"][^>]*$matchAttributeName=['\"].*?$matchAttributeValue.*?['\"][^>]*/?>#i", | ||
| 400 | $content, | ||
| 401 | $matches2 | ||
| 402 | ); | ||
| 403 | $result = array_merge($matches1[1], $matches2[1]); | ||
| 404 | |||
| 405 | return empty($result) ? false : $result[0]; | ||
| 406 | } | ||
| 407 | |||
| 408 | /** | ||
| 409 | * Builds authentication URL for the protocol version 2. | ||
| 410 | * | ||
| 411 | * @param ServerRequestInterface $incomingRequest | ||
| 412 | * @param array $serverInfo OpenID server info. | ||
| 413 | * | ||
| 414 | * @return string authentication URL. | ||
| 415 | */ | ||
| 416 | protected function buildAuthUrlV2(ServerRequestInterface $incomingRequest, array $serverInfo) | ||
| 417 |     { | ||
| 418 | $params = [ | ||
| 419 | 'openid.ns' => 'http://specs.openid.net/auth/2.0', | ||
| 420 | 'openid.mode' => 'checkid_setup', | ||
| 421 | 'openid.return_to' => $this->getReturnUrl($incomingRequest), | ||
| 422 | 'openid.realm' => $this->getTrustRoot(), | ||
| 423 | ]; | ||
| 424 |         if ($serverInfo['ax']) { | ||
| 425 | $params = array_merge($params, $this->buildAxParams()); | ||
| 426 | } | ||
| 427 |         if ($serverInfo['sreg']) { | ||
| 428 | $params = array_merge($params, $this->buildSregParams()); | ||
| 429 | } | ||
| 430 |         if (!$serverInfo['ax'] && !$serverInfo['sreg']) { | ||
| 431 | // If OP doesn't advertise either SREG, nor AX, let's send them both in worst case we don't get anything in return. | ||
| 432 | $params = array_merge($this->buildSregParams(), $this->buildAxParams(), $params); | ||
| 433 | } | ||
| 434 | |||
| 435 |         if ($serverInfo['identifier_select']) { | ||
| 436 | $url = 'http://specs.openid.net/auth/2.0/identifier_select'; | ||
| 437 | $params['openid.identity'] = $url; | ||
| 438 | $params['openid.claimed_id'] = $url; | ||
| 439 |         } else { | ||
| 440 | $params['openid.identity'] = $serverInfo['identity']; | ||
| 441 | $params['openid.claimed_id'] = $this->getClaimedId(); | ||
| 442 | } | ||
| 443 | |||
| 444 | return $this->buildUrl($serverInfo['url'], $params); | ||
| 445 | } | ||
| 446 | |||
| 447 | /** | ||
| 448 | * @param ServerRequestInterface $incomingRequest | ||
| 449 | * | ||
| 450 | * @return string authentication return URL. | ||
| 451 | */ | ||
| 452 | public function getReturnUrl(ServerRequestInterface $incomingRequest): string | ||
| 453 |     { | ||
| 454 |         if ($this->returnUrl === null) { | ||
| 455 | $this->returnUrl = $this->defaultReturnUrl($incomingRequest); | ||
| 456 | } | ||
| 457 | |||
| 458 | return $this->returnUrl; | ||
| 459 | } | ||
| 460 | |||
| 461 | /** | ||
| 462 | * @param string $returnUrl authentication return URL. | ||
| 463 | */ | ||
| 464 | public function setReturnUrl(string $returnUrl): void | ||
| 467 | } | ||
| 468 | |||
| 469 | /** | ||
| 470 |      * Generates default {@see returnUrl} value. | ||
| 471 | * | ||
| 472 | * @param ServerRequestInterface $incomingRequest | ||
| 473 | * | ||
| 474 | * @return string default authentication return URL. | ||
| 475 | */ | ||
| 476 | protected function defaultReturnUrl(ServerRequestInterface $incomingRequest): string | ||
| 477 |     { | ||
| 478 | $params = $incomingRequest->getQueryParams(); | ||
| 479 |         foreach ($params as $name => $value) { | ||
| 480 |             if (strncmp('openid', $name, 6) === 0) { | ||
| 481 | unset($params[$name]); | ||
| 482 | } | ||
| 483 | } | ||
| 484 | return (string)$incomingRequest->getUri()->withQuery(http_build_query($params, '', '&', PHP_QUERY_RFC3986)); | ||
| 485 | } | ||
| 486 | |||
| 487 | /** | ||
| 488 | * @return string client trust root (realm). | ||
| 489 | */ | ||
| 490 | public function getTrustRoot(): string | ||
| 491 |     { | ||
| 492 | return $this->trustRoot; | ||
| 493 | } | ||
| 494 | |||
| 495 | /** | ||
| 496 | * @param string $value client trust root (realm). | ||
| 497 | */ | ||
| 498 | public function setTrustRoot(string $value): void | ||
| 499 |     { | ||
| 500 | $this->trustRoot = $value; | ||
| 501 | } | ||
| 502 | |||
| 503 | /** | ||
| 504 | * Composes AX request parameters. | ||
| 505 | * | ||
| 506 | * @return array AX parameters. | ||
| 507 | */ | ||
| 508 | protected function buildAxParams(): array | ||
| 509 |     { | ||
| 510 | $params = []; | ||
| 511 |         if (!empty($this->requiredAttributes) || !empty($this->optionalAttributes)) { | ||
| 512 | $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; | ||
| 513 | $params['openid.ax.mode'] = 'fetch_request'; | ||
| 514 | $aliases = []; | ||
| 515 | $counts = []; | ||
| 516 | $requiredAttributes = []; | ||
| 517 | $optionalAttributes = []; | ||
| 518 |             foreach (['requiredAttributes', 'optionalAttributes'] as $type) { | ||
| 519 |                 foreach ($this->$type as $alias => $field) { | ||
| 520 |                     if (is_int($alias)) { | ||
| 521 | $alias = strtr($field, '/', '_'); | ||
| 522 | } | ||
| 523 | $aliases[$alias] = 'http://axschema.org/' . $field; | ||
| 524 |                     if (empty($counts[$alias])) { | ||
| 525 | $counts[$alias] = 0; | ||
| 526 | } | ||
| 527 | ++$counts[$alias]; | ||
| 528 |                     ${$type}[] = $alias; | ||
| 529 | } | ||
| 530 | } | ||
| 531 |             foreach ($aliases as $alias => $ns) { | ||
| 532 | $params['openid.ax.type.' . $alias] = $ns; | ||
| 533 | } | ||
| 534 |             foreach ($counts as $alias => $count) { | ||
| 535 |                 if ($count == 1) { | ||
| 536 | continue; | ||
| 537 | } | ||
| 538 | $params['openid.ax.count.' . $alias] = $count; | ||
| 539 | } | ||
| 540 | |||
| 541 | // Don't send empty ax.required and ax.if_available. | ||
| 542 | // Google and possibly other providers refuse to support ax when one of these is empty. | ||
| 543 |             if (!empty($requiredAttributes)) { | ||
| 544 |                 $params['openid.ax.required'] = implode(',', $requiredAttributes); | ||
| 545 | } | ||
| 546 |             if (!empty($optionalAttributes)) { | ||
| 547 |                 $params['openid.ax.if_available'] = implode(',', $optionalAttributes); | ||
| 548 | } | ||
| 549 | } | ||
| 550 | |||
| 551 | return $params; | ||
| 552 | } | ||
| 553 | |||
| 554 | /** | ||
| 555 | * Composes SREG request parameters. | ||
| 556 | * | ||
| 557 | * @return array SREG parameters. | ||
| 558 | */ | ||
| 559 | protected function buildSregParams(): array | ||
| 560 |     { | ||
| 561 | $params = []; | ||
| 562 | /* We always use SREG 1.1, even if the server is advertising only support for 1.0. | ||
| 563 | That's because it's fully backwards compatible with 1.0, and some providers | ||
| 564 | advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com */ | ||
| 565 | $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1'; | ||
| 566 |         if (!empty($this->requiredAttributes)) { | ||
| 567 | $params['openid.sreg.required'] = []; | ||
| 568 |             foreach ($this->requiredAttributes as $required) { | ||
| 569 |                 if (!isset($this->axToSregMap[$required])) { | ||
| 570 | continue; | ||
| 571 | } | ||
| 572 | $params['openid.sreg.required'][] = $this->axToSregMap[$required]; | ||
| 573 | } | ||
| 574 |             $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']); | ||
| 575 | } | ||
| 576 | |||
| 577 |         if (!empty($this->optionalAttributes)) { | ||
| 578 | $params['openid.sreg.optional'] = []; | ||
| 579 |             foreach ($this->optionalAttributes as $optional) { | ||
| 580 |                 if (!isset($this->axToSregMap[$optional])) { | ||
| 581 | continue; | ||
| 582 | } | ||
| 583 | $params['openid.sreg.optional'][] = $this->axToSregMap[$optional]; | ||
| 584 | } | ||
| 585 |             $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']); | ||
| 586 | } | ||
| 587 | |||
| 588 | return $params; | ||
| 589 | } | ||
| 590 | |||
| 591 | /** | ||
| 592 | * Builds authentication URL for the protocol version 1. | ||
| 593 | * | ||
| 594 | * @param ServerRequestInterface $incomingRequest | ||
| 595 | * @param array $serverInfo OpenID server info. | ||
| 596 | * | ||
| 597 | * @return string authentication URL. | ||
| 598 | */ | ||
| 599 | protected function buildAuthUrlV1(ServerRequestInterface $incomingRequest, array $serverInfo) | ||
| 600 |     { | ||
| 601 | $returnUrl = $this->getReturnUrl($incomingRequest); | ||
| 602 | /* If we have an openid.delegate that is different from our claimed id, | ||
| 603 | we need to somehow preserve the claimed id between requests. | ||
| 604 | The simplest way is to just send it along with the return_to url.*/ | ||
| 605 |         if ($serverInfo['identity'] !== $this->getClaimedId()) { | ||
| 606 | $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->getClaimedId(); | ||
| 607 | } | ||
| 608 | |||
| 609 | $params = array_merge( | ||
| 610 | [ | ||
| 611 | 'openid.return_to' => $returnUrl, | ||
| 612 | 'openid.mode' => 'checkid_setup', | ||
| 613 | 'openid.identity' => $serverInfo['identity'], | ||
| 614 | 'openid.trust_root' => $this->trustRoot, | ||
| 615 | ], | ||
| 616 | $this->buildSregParams() | ||
| 617 | ); | ||
| 618 | |||
| 619 | return $this->buildUrl($serverInfo['url'], ['query' => http_build_query($params, '', '&')]); | ||
| 620 | } | ||
| 621 | |||
| 622 | /** | ||
| 623 | * Performs OpenID verification with the OP. | ||
| 624 | * | ||
| 625 | * @param bool $validateRequiredAttributes whether to validate required attributes. | ||
| 626 | * | ||
| 627 | * @return bool whether the verification was successful. | ||
| 628 | */ | ||
| 629 | public function validate(bool $validateRequiredAttributes = true): bool | ||
| 630 |     { | ||
| 631 | $claimedId = $this->getClaimedId(); | ||
| 632 |         if (empty($claimedId)) { | ||
| 633 | return false; | ||
| 634 | } | ||
| 635 | $params = [ | ||
| 636 | 'openid.assoc_handle' => $this->data['openid_assoc_handle'], | ||
| 637 | 'openid.signed' => $this->data['openid_signed'], | ||
| 638 | 'openid.sig' => $this->data['openid_sig'], | ||
| 639 | ]; | ||
| 640 | |||
| 641 |         if (isset($this->data['openid_ns'])) { | ||
| 642 | /* We're dealing with an OpenID 2.0 server, so let's set an ns | ||
| 643 | Even though we should know location of the endpoint, | ||
| 644 | we still need to verify it by discovery, so $server is not set here*/ | ||
| 645 | $params['openid.ns'] = 'http://specs.openid.net/auth/2.0'; | ||
| 646 |         } elseif (isset($this->data['openid_claimed_id']) && $this->data['openid_claimed_id'] !== $this->data['openid_identity']) { | ||
| 647 | // If it's an OpenID 1 provider, and we've got claimed_id, | ||
| 648 | // we have to append it to the returnUrl, like authUrlV1 does. | ||
| 649 | $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $claimedId; | ||
| 650 | } | ||
| 651 | |||
| 652 |         if (!$this->compareUrl($this->data['openid_return_to'], $this->returnUrl)) { | ||
| 653 | // The return_to url must match the url of current request. | ||
| 654 | return false; | ||
| 655 | } | ||
| 656 | |||
| 657 | $serverInfo = $this->discover($claimedId); | ||
| 658 | |||
| 659 |         foreach (explode(',', $this->data['openid_signed']) as $item) { | ||
| 660 |             $value = $this->data['openid_' . str_replace('.', '_', $item)]; | ||
| 661 | $params['openid.' . $item] = $value; | ||
| 662 | } | ||
| 663 | |||
| 664 | $params['openid.mode'] = 'check_authentication'; | ||
| 665 |         $request = $this->createRequest('POST', $serverInfo['url']); | ||
| 666 | $request->getBody()->write((string)$params); | ||
| 667 | |||
| 668 | $response = $this->sendRequest($request); | ||
| 669 | |||
| 670 |         if (preg_match('/is_valid\s*:\s*true/i', $response)) { | ||
| 671 |             if ($validateRequiredAttributes) { | ||
| 672 | return $this->validateRequiredAttributes(); | ||
| 673 | } | ||
| 674 | |||
| 675 | return true; | ||
| 676 | } | ||
| 677 | |||
| 678 | return false; | ||
| 679 | } | ||
| 680 | |||
| 681 | /** | ||
| 682 | * Compares 2 URLs taking in account possible GET parameters order miss match and URL encoding inconsistencies. | ||
| 683 | * | ||
| 684 | * @param string $expectedUrl expected URL. | ||
| 685 | * @param string $actualUrl actual URL. | ||
| 686 | * | ||
| 687 | * @return bool whether URLs are equal. | ||
| 688 | */ | ||
| 689 | protected function compareUrl(string $expectedUrl, string $actualUrl): bool | ||
| 690 |     { | ||
| 691 | $expectedUrlInfo = parse_url($expectedUrl); | ||
| 692 | $actualUrlInfo = parse_url($actualUrl); | ||
| 693 |         foreach ($expectedUrlInfo as $name => $expectedValue) { | ||
| 694 |             if ($name === 'query') { | ||
| 695 | parse_str($expectedValue, $expectedUrlParams); | ||
| 696 | parse_str($actualUrlInfo[$name], $actualUrlParams); | ||
| 697 | $paramsDiff = array_diff_assoc($expectedUrlParams, $actualUrlParams); | ||
| 698 |                 if (!empty($paramsDiff)) { | ||
| 699 | return false; | ||
| 700 | } | ||
| 701 |             } elseif ($expectedValue !== $actualUrlInfo[$name]) { | ||
| 702 | return false; | ||
| 703 | } | ||
| 704 | } | ||
| 705 | return true; | ||
| 706 | } | ||
| 707 | |||
| 708 | /** | ||
| 709 | * Checks if all required attributes are present in the server response. | ||
| 710 | * | ||
| 711 | * @return bool whether all required attributes are present. | ||
| 712 | */ | ||
| 713 | protected function validateRequiredAttributes(): bool | ||
| 725 | } | ||
| 726 | |||
| 727 | /** | ||
| 728 | * Gets AX/SREG attributes provided by OP. Should be used only after successful validation. | ||
| 729 | * Note that it does not guarantee that any of the required/optional parameters will be present, | ||
| 730 | * or that there will be no other attributes besides those specified. | ||
| 731 | * In other words. OP may provide whatever information it wants to. | ||
| 732 | * SREG names will be mapped to AX names. | ||
| 733 | * | ||
| 734 | * @return array array of attributes with keys being the AX schema names, e.g. 'contact/email' | ||
| 735 | * | ||
| 736 | * @see http://www.axschema.org/types/ | ||
| 737 | */ | ||
| 738 | public function fetchAttributes(): array | ||
| 739 |     { | ||
| 740 |         if (isset($this->data['openid_ns']) && $this->data['openid_ns'] === 'http://specs.openid.net/auth/2.0') { | ||
| 741 | // OpenID 2.0 | ||
| 742 | // We search for both AX and SREG attributes, with AX taking precedence. | ||
| 743 | return array_merge($this->fetchSregAttributes(), $this->fetchAxAttributes()); | ||
| 744 | } | ||
| 745 | |||
| 746 | return $this->fetchSregAttributes(); | ||
| 747 | } | ||
| 748 | |||
| 749 | /** | ||
| 750 | * Gets SREG attributes provided by OP. SREG names will be mapped to AX names. | ||
| 751 | * | ||
| 752 | * @return array array of attributes with keys being the AX schema names, e.g. 'contact/email' | ||
| 753 | */ | ||
| 754 | protected function fetchSregAttributes() | ||
| 755 |     { | ||
| 756 | $attributes = []; | ||
| 757 | $sregToAx = array_flip($this->axToSregMap); | ||
| 758 |         foreach ($this->data as $key => $value) { | ||
| 759 | $keyMatch = 'openid_sreg_'; | ||
| 760 |             if (strncmp($key, $keyMatch, strlen($keyMatch))) { | ||
| 761 | continue; | ||
| 762 | } | ||
| 763 | $key = substr($key, strlen($keyMatch)); | ||
| 764 |             if (!isset($sregToAx[$key])) { | ||
| 765 | // The field name isn't part of the SREG spec, so we ignore it. | ||
| 766 | continue; | ||
| 767 | } | ||
| 768 | $attributes[$sregToAx[$key]] = $value; | ||
| 769 | } | ||
| 770 | |||
| 771 | return $attributes; | ||
| 772 | } | ||
| 773 | |||
| 774 | /** | ||
| 775 | * Gets AX attributes provided by OP. | ||
| 776 | * | ||
| 777 | * @return array array of attributes. | ||
| 778 | */ | ||
| 779 | protected function fetchAxAttributes(): array | ||
| 780 |     { | ||
| 781 | $alias = null; | ||
| 782 |         if (isset($this->data['openid_ns_ax']) && $this->data['openid_ns_ax'] !== 'http://openid.net/srv/ax/1.0') { | ||
| 783 | // It's the most likely case, so we'll check it before | ||
| 784 | $alias = 'ax'; | ||
| 785 |         } else { | ||
| 786 | // 'ax' prefix is either undefined, or points to another extension, so we search for another prefix | ||
| 787 |             foreach ($this->data as $key => $value) { | ||
| 788 |                 if ($value === 'http://openid.net/srv/ax/1.0' && strncmp($key, 'openid_ns_', 10) === 0) { | ||
| 789 |                     $alias = substr($key, strlen('openid_ns_')); | ||
| 790 | break; | ||
| 791 | } | ||
| 792 | } | ||
| 793 | } | ||
| 794 |         if (!$alias) { | ||
| 795 | // An alias for AX schema has not been found, so there is no AX data in the OP's response | ||
| 796 | return []; | ||
| 797 | } | ||
| 798 | |||
| 799 | $attributes = []; | ||
| 800 |         foreach ($this->data as $key => $value) { | ||
| 801 | $keyMatch = 'openid_' . $alias . '_value_'; | ||
| 802 |             if (strncmp($key, $keyMatch, strlen($keyMatch))) { | ||
| 803 | continue; | ||
| 804 | } | ||
| 805 | $key = substr($key, strlen($keyMatch)); | ||
| 806 |             if (!isset($this->data['openid_' . $alias . '_type_' . $key])) { | ||
| 807 | /* OP is breaking the spec by returning a field without | ||
| 808 | associated ns. This shouldn't happen, but it's better | ||
| 809 | to check, than cause an E_NOTICE.*/ | ||
| 810 | continue; | ||
| 811 | } | ||
| 812 |             $key = substr($this->data['openid_' . $alias . '_type_' . $key], strlen('http://axschema.org/')); | ||
| 813 | $attributes[$key] = $value; | ||
| 814 | } | ||
| 815 | |||
| 816 | return $attributes; | ||
| 817 | } | ||
| 818 | |||
| 819 | public function getName(): string | ||
| 822 | } | ||
| 823 | |||
| 824 | public function getTitle(): string | ||
| 825 |     { | ||
| 826 | return StringHelper::baseName(static::class); | ||
| 827 | } | ||
| 828 | |||
| 829 | protected function initUserAttributes(): array | ||
| 832 | } | ||
| 833 | } | ||
| 834 |