PluginProbe
Loginizer / 1.9.9
Loginizer v1.9.9
2.1.0 2.0.9 2.0.8 1.9.8 1.9.9 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 trunk 1.0 1.0.1 1.0.2 1.1.0 1.1.1 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 All 74 releases
loginizer / lib / hybridauth / Thirdparty / OpenID / LightOpenID.php

LightOpenID.php in Loginizer 1.9.9, at lib/hybridauth/Thirdparty/OpenID/LightOpenID.php

1,257 lines 43.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*!
3 * This file is part of the LightOpenID PHP Library (https://github.com/iignatov/LightOpenID)
4 *
5 * LightOpenID is an open source software available under the MIT License.
6 *
7 * Updated: 52f9910 on 4 Mar 2016.
8 */
9
10 namespace Hybridauth\Thirdparty\OpenID;
11
12 use Hybridauth\Exception\Exception;
13 use Hybridauth\Exception\ExceptionInterface;
14
15 /**
16 * Class ErrorException
17 *
18 * @package Hybridauth\Thirdparty\OpenID
19 */
20 class ErrorException extends Exception implements ExceptionInterface
21 {
22 }
23
24 /**
25 * This class provides a simple interface for OpenID 1.1/2.0 authentication.
26 *
27 * It requires PHP >= 5.1.2 with cURL or HTTP/HTTPS stream wrappers enabled.
28 *
29 * @version v1.3.1 (2016-03-04)
30 * @link https://code.google.com/p/lightopenid/ Project URL
31 * @link https://github.com/iignatov/LightOpenID GitHub Repo
32 * @author Mewp <mewp151 at gmail dot com>
33 * @copyright Copyright (c) 2013 Mewp
34 * @license http://opensource.org/licenses/mit-license.php MIT License
35 */
36 class LightOpenID
37 {
38 public $returnUrl
39 ;
40 public $required = array()
41 ;
42 public $optional = array()
43 ;
44 public $verify_peer = null
45 ;
46 public $capath = null
47 ;
48 public $cainfo = null
49 ;
50 public $cnmatch = null
51 ;
52 public $data
53 ;
54 public $oauth = array()
55 ;
56 public $curl_time_out = 30 // in seconds
57 ;
58 public $curl_connect_time_out = 30; // in seconds
59 private $identity;
60 private $claimed_id;
61 protected $server;
62 protected $version;
63 protected $trustRoot;
64 protected $aliases;
65 protected $identifier_select = false
66 ;
67 protected $ax = false;
68 protected $sreg = false;
69 protected $setup_url = null;
70 protected $headers = array()
71 ;
72 protected $proxy = null;
73 protected $user_agent = 'LightOpenID'
74 ;
75 protected $xrds_override_pattern = null;
76 protected $xrds_override_replacement = null;
77 protected static $ax_to_sreg = array(
78 'namePerson/friendly' => 'nickname',
79 'contact/email' => 'email',
80 'namePerson' => 'fullname',
81 'birthDate' => 'dob',
82 'person/gender' => 'gender',
83 'contact/postalCode/home' => 'postcode',
84 'contact/country/home' => 'country',
85 'pref/language' => 'language',
86 'pref/timezone' => 'timezone',
87 );
88
89 /**
90 * LightOpenID constructor.
91 *
92 * @param $host
93 * @param null $proxy
94 *
95 * @throws ErrorException
96 */
97 public function __construct($host, $proxy = null)
98 {
99 $this->set_realm($host);
100 $this->set_proxy($proxy);
101
102 $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?');
103 $this->returnUrl = $this->trustRoot . $uri;
104
105 $this->data = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET;
106
107 if (!function_exists('curl_init') && !in_array('https', stream_get_wrappers())) {
108 throw new ErrorException('You must have either https wrappers or curl enabled.');
109 }
110 }
111
112 /**
113 * @param $name
114 *
115 * @return bool
116 */
117 public function __isset($name)
118 {
119 return in_array($name, array('identity', 'trustRoot', 'realm', 'xrdsOverride', 'mode'));
120 }
121
122 /**
123 * @param $name
124 * @param $value
125 */
126 public function __set($name, $value)
127 {
128 switch ($name) {
129 case 'identity':
130 if (strlen($value = trim((String) $value))) {
131 if (preg_match('#^xri:/*#i', $value, $m)) {
132 $value = substr($value, strlen($m[0]));
133 } elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) {
134 $value = "http://$value";
135 }
136 if (preg_match('#^https?://[^/]+$#i', $value, $m)) {
137 $value .= '/';
138 }
139 }
140 $this->$name = $this->claimed_id = $value;
141 break;
142 case 'trustRoot':
143 case 'realm':
144 $this->trustRoot = trim($value);
145 break;
146 case 'xrdsOverride':
147 if (is_array($value)) {
148 list($pattern, $replacement) = $value;
149 $this->xrds_override_pattern = $pattern;
150 $this->xrds_override_replacement = $replacement;
151 } else {
152 trigger_error('Invalid value specified for "xrdsOverride".', E_USER_ERROR);
153 }
154 break;
155 }
156 }
157
158 /**
159 * @param $name
160 *
161 * @return |null
162 */
163 public function __get($name)
164 {
165 switch ($name) {
166 case 'identity':
167 # We return claimed_id instead of identity,
168 # because the developer should see the claimed identifier,
169 # i.e. what he set as identity, not the op-local identifier (which is what we verify)
170 return $this->claimed_id;
171 case 'trustRoot':
172 case 'realm':
173 return $this->trustRoot;
174 case 'mode':
175 return empty($this->data['openid_mode']) ? null : $this->data['openid_mode'];
176 }
177 }
178
179 /**
180 * @param $proxy
181 *
182 * @throws ErrorException
183 */
184 public function set_proxy($proxy)
185 {
186 if (!empty($proxy)) {
187 // When the proxy is a string - try to parse it.
188 if (!is_array($proxy)) {
189 $proxy = parse_url($proxy);
190 }
191
192 // Check if $proxy is valid after the parsing.
193 if ($proxy && !empty($proxy['host'])) {
194 // Make sure that a valid port number is specified.
195 if (array_key_exists('port', $proxy)) {
196 if (!is_int($proxy['port'])) {
197 $proxy['port'] = is_numeric($proxy['port']) ? intval($proxy['port']) : 0;
198 }
199
200 if ($proxy['port'] <= 0) {
201 throw new ErrorException('The specified proxy port number is invalid.');
202 }
203 }
204
205 $this->proxy = $proxy;
206 }
207 }
208 }
209
210 /**
211 * Checks if the server specified in the url exists.
212 *
213 * @param $url string url to check
214 * @return true, if the server exists; false otherwise
215 */
216 public function hostExists($url)
217 {
218 if (strpos($url, '/') === false) {
219 $server = $url;
220 } else {
221 $server = @parse_url($url, PHP_URL_HOST);
222 }
223
224 if (!$server) {
225 return false;
226 }
227
228 return !!gethostbynamel($server);
229 }
230
231 /**
232 * @param $uri
233 */
234 protected function set_realm($uri)
235 {
236 $realm = '';
237
238 # Set a protocol, if not specified.
239 $realm .= (($offset = strpos($uri, '://')) === false) ? $this->get_realm_protocol() : '';
240
241 # Set the offset properly.
242 $offset = (($offset !== false) ? $offset + 3 : 0);
243
244 # Get only the root, without the path.
245 $realm .= (($end = strpos($uri, '/', $offset)) === false) ? $uri : substr($uri, 0, $end);
246
247 $this->trustRoot = $realm;
248 }
249
250 /**
251 * @return string
252 */
253 protected function get_realm_protocol()
254 {
255 if (!empty($_SERVER['HTTPS'])) {
256 $use_secure_protocol = ($_SERVER['HTTPS'] !== 'off');
257 } elseif (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
258 $use_secure_protocol = ($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https');
259 } elseif (isset($_SERVER['HTTP__WSSC'])) {
260 $use_secure_protocol = ($_SERVER['HTTP__WSSC'] == 'https');
261 } else {
262 $use_secure_protocol = false;
263 }
264
265 return $use_secure_protocol ? 'https://' : 'http://';
266 }
267
268 /**
269 * @param $url
270 * @param string $method
271 * @param array $params
272 * @param $update_claimed_id
273 *
274 * @return array|bool|string
275 * @throws ErrorException
276 */
277 protected function request_curl($url, $method='GET', $params=array(), $update_claimed_id=false)
278 {
279 $params = http_build_query($params, '', '&');
280 $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : ''));
281 curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
282 curl_setopt($curl, CURLOPT_HEADER, false);
283 curl_setopt($curl, CURLOPT_USERAGENT, $this->user_agent);
284 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
285 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
286
287 if ($method == 'POST') {
288 curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded'));
289 } else {
290 curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*'));
291 }
292
293 curl_setopt($curl, CURLOPT_TIMEOUT, $this->curl_time_out); // defaults to infinite
294 curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $this->curl_connect_time_out); // defaults to 300s
295
296 if (!empty($this->proxy)) {
297 curl_setopt($curl, CURLOPT_PROXY, $this->proxy['host']);
298
299 if (!empty($this->proxy['port'])) {
300 curl_setopt($curl, CURLOPT_PROXYPORT, $this->proxy['port']);
301 }
302
303 if (!empty($this->proxy['user'])) {
304 curl_setopt($curl, CURLOPT_PROXYUSERPWD, $this->proxy['user'] . ':' . $this->proxy['pass']);
305 }
306 }
307
308 if ($this->verify_peer !== null) {
309 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer);
310 if ($this->capath) {
311 curl_setopt($curl, CURLOPT_CAPATH, $this->capath);
312 }
313
314 if ($this->cainfo) {
315 curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo);
316 }
317 }
318
319 if ($method == 'POST') {
320 curl_setopt($curl, CURLOPT_POST, true);
321 curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
322 } elseif ($method == 'HEAD') {
323 curl_setopt($curl, CURLOPT_HEADER, true);
324 curl_setopt($curl, CURLOPT_NOBODY, true);
325 } else {
326 curl_setopt($curl, CURLOPT_HEADER, true);
327 curl_setopt($curl, CURLOPT_HTTPGET, true);
328 }
329 $response = curl_exec($curl);
330
331 if ($method == 'HEAD' && curl_getinfo($curl, CURLINFO_HTTP_CODE) == 405) {
332 curl_setopt($curl, CURLOPT_HTTPGET, true);
333 $response = curl_exec($curl);
334 $response = substr($response, 0, strpos($response, "\r\n\r\n"));
335 }
336
337 if ($method == 'HEAD' || $method == 'GET') {
338 $header_response = $response;
339
340 # If it's a GET request, we want to only parse the header part.
341 if ($method == 'GET') {
342 $header_response = substr($response, 0, strpos($response, "\r\n\r\n"));
343 }
344
345 $headers = array();
346 foreach (explode("\n", $header_response) as $header) {
347 $pos = strpos($header, ':');
348 if ($pos !== false) {
349 $name = strtolower(trim(substr($header, 0, $pos)));
350 $headers[$name] = trim(substr($header, $pos+1));
351 }
352 }
353
354 if ($update_claimed_id) {
355 # Update the claimed_id value in case of redirections.
356 $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
357 # Ignore the fragment (some cURL versions don't handle it well).
358 if (strtok($effective_url, '#') != strtok($url, '#')) {
359 $this->identity = $this->claimed_id = $effective_url;
360 }
361 }
362
363 if ($method == 'HEAD') {
364 return $headers;
365 } else {
366 $this->headers = $headers;
367 }
368 }
369
370 if (curl_errno($curl)) {
371 throw new ErrorException(curl_error($curl), curl_errno($curl));
372 }
373
374 return $response;
375 }
376
377 /**
378 * @param $array
379 * @param $update_claimed_id
380 *
381 * @return array
382 */
383 protected function parse_header_array($array, $update_claimed_id)
384 {
385 $headers = array();
386 foreach ($array as $header) {
387 $pos = strpos($header, ':');
388 if ($pos !== false) {
389 $name = strtolower(trim(substr($header, 0, $pos)));
390 $headers[$name] = trim(substr($header, $pos+1));
391
392 # Following possible redirections. The point is just to have
393 # claimed_id change with them, because the redirections
394 # are followed automatically.
395 # We ignore redirections with relative paths.
396 # If any known provider uses them, file a bug report.
397 if ($name == 'location' && $update_claimed_id) {
398 if (strpos($headers[$name], 'http') === 0) {
399 $this->identity = $this->claimed_id = $headers[$name];
400 } elseif ($headers[$name][0] == '/') {
401 $parsed_url = parse_url($this->claimed_id);
402 $this->identity =
403 $this->claimed_id = $parsed_url['scheme'] . '://'
404 . $parsed_url['host']
405 . $headers[$name];
406 }
407 }
408 }
409 }
410 return $headers;
411 }
412
413 /**
414 * @param $url
415 * @param string $method
416 * @param array $params
417 * @param $update_claimed_id
418 *
419 * @return array|false|string
420 * @throws ErrorException
421 */
422 protected function request_streams($url, $method='GET', $params=array(), $update_claimed_id=false)
423 {
424 if (!$this->hostExists($url)) {
425 throw new ErrorException("Could not connect to $url.", 404);
426 }
427
428 if (empty($this->cnmatch)) {
429 $this->cnmatch = parse_url($url, PHP_URL_HOST);
430 }
431
432 $params = http_build_query($params, '', '&');
433 switch ($method) {
434 case 'GET':
435 $opts = array(
436 'http' => array(
437 'method' => 'GET',
438 'header' => 'Accept: application/xrds+xml, */*',
439 'user_agent' => $this->user_agent,
440 'ignore_errors' => true,
441 ),
442 'ssl' => array(
443 'CN_match' => $this->cnmatch
444 )
445 );
446 $url = $url . ($params ? '?' . $params : '');
447 if (!empty($this->proxy)) {
448 $opts['http']['proxy'] = $this->proxy_url();
449 }
450 break;
451 case 'POST':
452 $opts = array(
453 'http' => array(
454 'method' => 'POST',
455 'header' => 'Content-type: application/x-www-form-urlencoded',
456 'user_agent' => $this->user_agent,
457 'content' => $params,
458 'ignore_errors' => true,
459 ),
460 'ssl' => array(
461 'CN_match' => $this->cnmatch
462 )
463 );
464 if (!empty($this->proxy)) {
465 $opts['http']['proxy'] = $this->proxy_url();
466 }
467 break;
468 case 'HEAD':
469 // We want to send a HEAD request, but since get_headers() doesn't
470 // accept $context parameter, we have to change the defaults.
471 $default = stream_context_get_options(stream_context_get_default());
472
473 // PHP does not reset all options. Instead, it just sets the options
474 // available in the passed array, therefore set the defaults manually.
475 $default += array(
476 'http' => array(),
477 'ssl' => array()
478 );
479 $default['http'] += array(
480 'method' => 'GET',
481 'header' => '',
482 'user_agent' => '',
483 'ignore_errors' => false
484 );
485 $default['ssl'] += array(
486 'CN_match' => ''
487 );
488
489 $opts = array(
490 'http' => array(
491 'method' => 'HEAD',
492 'header' => 'Accept: application/xrds+xml, */*',
493 'user_agent' => $this->user_agent,
494 'ignore_errors' => true,
495 ),
496 'ssl' => array(
497 'CN_match' => $this->cnmatch
498 )
499 );
500
501 // Enable validation of the SSL certificates.
502 if ($this->verify_peer) {
503 $default['ssl'] += array(
504 'verify_peer' => false,
505 'capath' => '',
506 'cafile' => ''
507 );
508 $opts['ssl'] += array(
509 'verify_peer' => true,
510 'capath' => $this->capath,
511 'cafile' => $this->cainfo
512 );
513 }
514
515 // Change the stream context options.
516 stream_context_get_default($opts);
517
518 $headers = get_headers($url . ($params ? '?' . $params : ''));
519
520 // Restore the stream context options.
521 stream_context_get_default($default);
522
523 if (!empty($headers)) {
524 if (intval(substr($headers[0], strlen('HTTP/1.1 '))) == 405) {
525 // The server doesn't support HEAD - emulate it with a GET.
526 $args = func_get_args();
527 $args[1] = 'GET';
528 call_user_func_array(array($this, 'request_streams'), $args);
529 $headers = $this->headers;
530 } else {
531 $headers = $this->parse_header_array($headers, $update_claimed_id);
532 }
533 } else {
534 $headers = array();
535 }
536
537 return $headers;
538 }
539
540 if ($this->verify_peer) {
541 $opts['ssl'] += array(
542 'verify_peer' => true,
543 'capath' => $this->capath,
544 'cafile' => $this->cainfo
545 );
546 }
547
548 $context = stream_context_create($opts);
549 $data = file_get_contents($url, false, $context);
550 # This is a hack for providers who don't support HEAD requests.
551 # It just creates the headers array for the last request in $this->headers.
552 if (isset($http_response_header)) {
553 $this->headers = $this->parse_header_array($http_response_header, $update_claimed_id);
554 }
555
556 return $data;
557 }
558
559 /**
560 * @param $url
561 * @param string $method
562 * @param array $params
563 * @param bool $update_claimed_id
564 *
565 * @return array|bool|false|string
566 * @throws ErrorException
567 */
568 protected function request($url, $method='GET', $params=array(), $update_claimed_id=false)
569 {
570 $use_curl = false;
571
572 if (function_exists('curl_init')) {
573 if (!$use_curl) {
574 # When allow_url_fopen is disabled, PHP streams will not work.
575 $use_curl = !ini_get('allow_url_fopen');
576 }
577
578 if (!$use_curl) {
579 # When there is no HTTPS wrapper, PHP streams cannott be used.
580 $use_curl = !in_array('https', stream_get_wrappers());
581 }
582
583 if (!$use_curl) {
584 # With open_basedir or safe_mode set, cURL can't follow redirects.
585 $use_curl = !(ini_get('safe_mode') || ini_get('open_basedir'));
586 }
587 }
588
589 return
590 $use_curl
591 ? $this->request_curl($url, $method, $params, $update_claimed_id)
592 : $this->request_streams($url, $method, $params, $update_claimed_id);
593 }
594
595 /**
596 * @return string
597 */
598 protected function proxy_url()
599 {
600 $result = '';
601
602 if (!empty($this->proxy)) {
603 $result = $this->proxy['host'];
604
605 if (!empty($this->proxy['port'])) {
606 $result = $result . ':' . $this->proxy['port'];
607 }
608
609 if (!empty($this->proxy['user'])) {
610 $result = $this->proxy['user'] . ':' . $this->proxy['pass'] . '@' . $result;
611 }
612
613 $result = 'http://' . $result;
614 }
615
616 return $result;
617 }
618
619 /**
620 * @param $url
621 * @param $parts
622 *
623 * @return string
624 */
625 protected function build_url($url, $parts)
626 {
627 if (isset($url['query'], $parts['query'])) {
628 $parts['query'] = $url['query'] . '&' . $parts['query'];
629 }
630
631 $url = $parts + $url;
632 $url = $url['scheme'] . '://'
633 . (empty($url['username'])?''
634 :(empty($url['password'])? "{$url['username']}@"
635 :"{$url['username']}:{$url['password']}@"))
636 . $url['host']
637 . (empty($url['port'])?'':":{$url['port']}")
638 . (empty($url['path'])?'':$url['path'])
639 . (empty($url['query'])?'':"?{$url['query']}")
640 . (empty($url['fragment'])?'':"#{$url['fragment']}");
641 return $url;
642 }
643
644 /**
645 * Helper function used to scan for <meta>/<link> tags and extract information
646 * from them
647 *
648 * @param $content
649 * @param $tag
650 * @param $attrName
651 * @param $attrValue
652 * @param $valueName
653 *
654 * @return bool
655 */
656 protected function htmlTag($content, $tag, $attrName, $attrValue, $valueName)
657 {
658 preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1);
659 preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2);
660
661 $result = array_merge($matches1[1], $matches2[1]);
662 return empty($result)?false:$result[0];
663 }
664
665 /**
666 * Performs Yadis and HTML discovery. Normally not used.
667 * @param $url Identity URL.
668 * @return String OP Endpoint (i.e. OpenID provider address).
669 * @throws ErrorException
670 */
671 public function discover($url)
672 {
673 if (!$url) {
674 throw new ErrorException('No identity supplied.');
675 }
676 # Use xri.net proxy to resolve i-name identities
677 if (!preg_match('#^https?:#', $url)) {
678 $url = "https://xri.net/$url";
679 }
680
681 # We save the original url in case of Yadis discovery failure.
682 # It can happen when we'll be lead to an XRDS document
683 # which does not have any OpenID2 services.
684 $originalUrl = $url;
685
686 # A flag to disable yadis discovery in case of failure in headers.
687 $yadis = true;
688
689 # Allows optional regex replacement of the URL, e.g. to use Google Apps
690 # as an OpenID provider without setting up XRDS on the domain hosting.
691 if (!is_null($this->xrds_override_pattern) && !is_null($this->xrds_override_replacement)) {
692 $url = preg_replace($this->xrds_override_pattern, $this->xrds_override_replacement, $url);
693 }
694
695 # We'll jump a maximum of 5 times, to avoid endless redirections.
696 for ($i = 0; $i < 5; $i ++) {
697 if ($yadis) {
698 $headers = $this->request($url, 'HEAD', array(), true);
699
700 $next = false;
701 if (isset($headers['x-xrds-location'])) {
702 $url = $this->build_url(parse_url($url), parse_url(trim($headers['x-xrds-location'])));
703 $next = true;
704 }
705
706 if (isset($headers['content-type']) && $this->is_allowed_type($headers['content-type'])) {
707 # Found an XRDS document, now let's find the server, and optionally delegate.
708 $content = $this->request($url, 'GET');
709
710 preg_match_all('#<Service.*?>(.*?)</Service>#s', $content, $m);
711 foreach ($m[1] as $content) {
712 $content = ' ' . $content; # The space is added, so that strpos doesn't return 0.
713
714 # OpenID 2
715 $ns = preg_quote('http://specs.openid.net/auth/2.0/', '#');
716 if (preg_match('#<Type>\s*'.$ns.'(server|signon)\s*</Type>#s', $content, $type)) {
717 if ($type[1] == 'server') {
718 $this->identifier_select = true;
719 }
720
721 preg_match('#<URI.*?>(.*)</URI>#', $content, $server);
722 preg_match('#<(Local|Canonical)ID>(.*)</\1ID>#', $content, $delegate);
723 if (empty($server)) {
724 return false;
725 }
726 # Does the server advertise support for either AX or SREG?
727 $this->ax = (bool) strpos($content, '<Type>http://openid.net/srv/ax/1.0</Type>');
728 $this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>')
729 || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>');
730
731 $server = $server[1];
732 if (isset($delegate[2])) {
733 $this->identity = trim($delegate[2]);
734 }
735 $this->version = 2;
736
737 $this->server = $server;
738 return $server;
739 }
740
741 # OpenID 1.1
742 $ns = preg_quote('http://openid.net/signon/1.1', '#');
743 if (preg_match('#<Type>\s*'.$ns.'\s*</Type>#s', $content)) {
744 preg_match('#<URI.*?>(.*)</URI>#', $content, $server);
745 preg_match('#<.*?Delegate>(.*)</.*?Delegate>#', $content, $delegate);
746 if (empty($server)) {
747 return false;
748 }
749 # AX can be used only with OpenID 2.0, so checking only SREG
750 $this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>')
751 || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>');
752
753 $server = $server[1];
754 if (isset($delegate[1])) {
755 $this->identity = $delegate[1];
756 }
757 $this->version = 1;
758
759 $this->server = $server;
760 return $server;
761 }
762 }
763
764 $next = true;
765 $yadis = false;
766 $url = $originalUrl;
767 $content = null;
768 break;
769 }
770 if ($next) {
771 continue;
772 }
773
774 # There are no relevant information in headers, so we search the body.
775 $content = $this->request($url, 'GET', array(), true);
776
777 if (isset($this->headers['x-xrds-location'])) {
778 $url = $this->build_url(parse_url($url), parse_url(trim($this->headers['x-xrds-location'])));
779 continue;
780 }
781
782 $location = $this->htmlTag($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content');
783 if ($location) {
784 $url = $this->build_url(parse_url($url), parse_url($location));
785 continue;
786 }
787 }
788
789 if (!$content) {
790 $content = $this->request($url, 'GET');
791 }
792
793 # At this point, the YADIS Discovery has failed, so we'll switch
794 # to openid2 HTML discovery, then fallback to openid 1.1 discovery.
795 $server = $this->htmlTag($content, 'link', 'rel', 'openid2.provider', 'href');
796 $delegate = $this->htmlTag($content, 'link', 'rel', 'openid2.local_id', 'href');
797 $this->version = 2;
798
799 if (!$server) {
800 # The same with openid 1.1
801 $server = $this->htmlTag($content, 'link', 'rel', 'openid.server', 'href');
802 $delegate = $this->htmlTag($content, 'link', 'rel', 'openid.delegate', 'href');
803 $this->version = 1;
804 }
805
806 if ($server) {
807 # We found an OpenID2 OP Endpoint
808 if ($delegate) {
809 # We have also found an OP-Local ID.
810 $this->identity = $delegate;
811 }
812 $this->server = $server;
813 return $server;
814 }
815
816 throw new ErrorException("No OpenID Server found at $url", 404);
817 }
818 throw new ErrorException('Endless redirection!', 500);
819 }
820
821 /**
822 * @param $content_type
823 *
824 * @return bool
825 */
826 protected function is_allowed_type($content_type)
827 {
828 # Apparently, some providers return XRDS documents as text/html.
829 # While it is against the spec, allowing this here shouldn't break
830 # compatibility with anything.
831 $allowed_types = array('application/xrds+xml', 'text/xml');
832
833 # Only allow text/html content type for the Yahoo logins, since
834 # it might cause an endless redirection for the other providers.
835 if ($this->get_provider_name($this->claimed_id) == 'yahoo') {
836 $allowed_types[] = 'text/html';
837 }
838
839 foreach ($allowed_types as $type) {
840 if (strpos($content_type, $type) !== false) {
841 return true;
842 }
843 }
844
845 return false;
846 }
847
848 /**
849 * @param $provider_url
850 *
851 * @return string
852 */
853 protected function get_provider_name($provider_url)
854 {
855 $result = '';
856
857 if (!empty($provider_url)) {
858 $tokens = array_reverse(
859 explode('.', parse_url($provider_url, PHP_URL_HOST))
860 );
861 $result = strtolower(
862 (count($tokens) > 1 && strlen($tokens[1]) > 3)
863 ? $tokens[1]
864 : (count($tokens) > 2 ? $tokens[2] : '')
865 );
866 }
867
868 return $result;
869 }
870
871 /**
872 * @return array
873 */
874 protected function sregParams()
875 {
876 $params = array();
877 # We always use SREG 1.1, even if the server is advertising only support for 1.0.
878 # That's because it's fully backwards compatible with 1.0, and some providers
879 # advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com
880 $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1';
881 if ($this->required) {
882 $params['openid.sreg.required'] = array();
883 foreach ($this->required as $required) {
884 if (!isset(self::$ax_to_sreg[$required])) {
885 continue;
886 }
887 $params['openid.sreg.required'][] = self::$ax_to_sreg[$required];
888 }
889 $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']);
890 }
891
892 if ($this->optional) {
893 $params['openid.sreg.optional'] = array();
894 foreach ($this->optional as $optional) {
895 if (!isset(self::$ax_to_sreg[$optional])) {
896 continue;
897 }
898 $params['openid.sreg.optional'][] = self::$ax_to_sreg[$optional];
899 }
900 $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']);
901 }
902 return $params;
903 }
904
905 /**
906 * @return array
907 */
908 protected function axParams()
909 {
910 $params = array();
911 if ($this->required || $this->optional) {
912 $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0';
913 $params['openid.ax.mode'] = 'fetch_request';
914 $this->aliases = array();
915 $counts = array();
916 $required = array();
917 $optional = array();
918 foreach (array('required','optional') as $type) {
919 foreach ($this->$type as $alias => $field) {
920 if (is_int($alias)) {
921 $alias = strtr($field, '/', '_');
922 }
923 $this->aliases[$alias] = 'http://axschema.org/' . $field;
924 if (empty($counts[$alias])) {
925 $counts[$alias] = 0;
926 }
927 $counts[$alias] += 1;
928 ${$type}[] = $alias;
929 }
930 }
931 foreach ($this->aliases as $alias => $ns) {
932 $params['openid.ax.type.' . $alias] = $ns;
933 }
934 foreach ($counts as $alias => $count) {
935 if ($count == 1) {
936 continue;
937 }
938 $params['openid.ax.count.' . $alias] = $count;
939 }
940
941 # Don't send empty ax.required and ax.if_available.
942 # Google and possibly other providers refuse to support ax when one of these is empty.
943 if ($required) {
944 $params['openid.ax.required'] = implode(',', $required);
945 }
946 if ($optional) {
947 $params['openid.ax.if_available'] = implode(',', $optional);
948 }
949 }
950 return $params;
951 }
952
953 /**
954 * @param $immediate
955 *
956 * @return string
957 */
958 protected function authUrl_v1($immediate)
959 {
960 $returnUrl = $this->returnUrl;
961 # If we have an openid.delegate that is different from our claimed id,
962 # we need to somehow preserve the claimed id between requests.
963 # The simplest way is to just send it along with the return_to url.
964 if ($this->identity != $this->claimed_id) {
965 $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->claimed_id;
966 }
967
968 $params = array(
969 'openid.return_to' => $returnUrl,
970 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup',
971 'openid.identity' => $this->identity,
972 'openid.trust_root' => $this->trustRoot,
973 ) + $this->sregParams();
974
975 return $this->build_url(parse_url($this->server), array('query' => http_build_query($params, '', '&')));
976 }
977
978 /**
979 * @param $immediate
980 *
981 * @return string
982 */
983 protected function authUrl_v2($immediate)
984 {
985 $params = array(
986 'openid.ns' => 'http://specs.openid.net/auth/2.0',
987 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup',
988 'openid.return_to' => $this->returnUrl,
989 'openid.realm' => $this->trustRoot,
990 );
991
992 if ($this->ax) {
993 $params += $this->axParams();
994 }
995
996 if ($this->sreg) {
997 $params += $this->sregParams();
998 }
999
1000 if (!$this->ax && !$this->sreg) {
1001 # If OP doesn't advertise either SREG, nor AX, let's send them both
1002 # in worst case we don't get anything in return.
1003 $params += $this->axParams() + $this->sregParams();
1004 }
1005
1006 if (!empty($this->oauth) && is_array($this->oauth)) {
1007 $params['openid.ns.oauth'] = 'http://specs.openid.net/extensions/oauth/1.0';
1008 $params['openid.oauth.consumer'] = str_replace(array('http://', 'https://'), '', $this->trustRoot);
1009 $params['openid.oauth.scope'] = implode(' ', $this->oauth);
1010 }
1011
1012 if ($this->identifier_select) {
1013 $params['openid.identity'] = $params['openid.claimed_id']
1014 = 'http://specs.openid.net/auth/2.0/identifier_select';
1015 } else {
1016 $params['openid.identity'] = $this->identity;
1017 $params['openid.claimed_id'] = $this->claimed_id;
1018 }
1019
1020 return $this->build_url(parse_url($this->server), array('query' => http_build_query($params, '', '&')));
1021 }
1022
1023 /**
1024 * Returns authentication url. Usually, you want to redirect your user to it.
1025 * @param bool $immediate
1026 * @return String The authentication url.
1027 * @throws ErrorException
1028 */
1029 public function authUrl($immediate = false)
1030 {
1031 if ($this->setup_url && !$immediate) {
1032 return $this->setup_url;
1033 }
1034 if (!$this->server) {
1035 $this->discover($this->identity);
1036 }
1037
1038 if ($this->version == 2) {
1039 return $this->authUrl_v2($immediate);
1040 }
1041 return $this->authUrl_v1($immediate);
1042 }
1043
1044 /**
1045 * Performs OpenID verification with the OP.
1046 * @return Bool Whether the verification was successful.
1047 * @throws ErrorException
1048 */
1049 public function validate()
1050 {
1051 # If the request was using immediate mode, a failure may be reported
1052 # by presenting user_setup_url (for 1.1) or reporting
1053 # mode 'setup_needed' (for 2.0). Also catching all modes other than
1054 # id_res, in order to avoid throwing errors.
1055 if (isset($this->data['openid_user_setup_url'])) {
1056 $this->setup_url = $this->data['openid_user_setup_url'];
1057 return false;
1058 }
1059 if ($this->mode != 'id_res') {
1060 return false;
1061 }
1062
1063 $this->claimed_id = isset($this->data['openid_claimed_id'])?$this->data['openid_claimed_id']:$this->data['openid_identity'];
1064 $params = array(
1065 'openid.assoc_handle' => $this->data['openid_assoc_handle'],
1066 'openid.signed' => $this->data['openid_signed'],
1067 'openid.sig' => $this->data['openid_sig'],
1068 );
1069
1070 if (isset($this->data['openid_ns'])) {
1071 # We're dealing with an OpenID 2.0 server, so let's set an ns
1072 # Even though we should know location of the endpoint,
1073 # we still need to verify it by discovery, so $server is not set here
1074 $params['openid.ns'] = 'http://specs.openid.net/auth/2.0';
1075 } elseif (isset($this->data['openid_claimed_id'])
1076 && $this->data['openid_claimed_id'] != $this->data['openid_identity']
1077 ) {
1078 # If it's an OpenID 1 provider, and we've got claimed_id,
1079 # we have to append it to the returnUrl, like authUrl_v1 does.
1080 $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?')
1081 . 'openid.claimed_id=' . $this->claimed_id;
1082 }
1083
1084 if ($this->data['openid_return_to'] != $this->returnUrl) {
1085 # The return_to url must match the url of current request.
1086 # I'm assuming that no one will set the returnUrl to something that doesn't make sense.
1087 return false;
1088 }
1089
1090 $server = $this->discover($this->claimed_id);
1091
1092 foreach (explode(',', $this->data['openid_signed']) as $item) {
1093 $value = $this->data['openid_' . str_replace('.', '_', $item)];
1094 $params['openid.' . $item] = $value;
1095 }
1096
1097 $params['openid.mode'] = 'check_authentication';
1098
1099 $response = $this->request($server, 'POST', $params);
1100
1101 return preg_match('/is_valid\s*:\s*true/i', $response);
1102 }
1103
1104 /**
1105 * @return array
1106 */
1107 protected function getAxAttributes()
1108 {
1109 $result = array();
1110
1111 if ($alias = $this->getNamespaceAlias('http://openid.net/srv/ax/1.0', 'ax')) {
1112 $prefix = 'openid_' . $alias;
1113 $length = strlen('http://axschema.org/');
1114
1115 foreach (explode(',', $this->data['openid_signed']) as $key) {
1116 $keyMatch = $alias . '.type.';
1117
1118 if (strncmp($key, $keyMatch, strlen($keyMatch)) !== 0) {
1119 continue;
1120 }
1121
1122 $key = substr($key, strlen($keyMatch));
1123 $idv = $prefix . '_value_' . $key;
1124 $idc = $prefix . '_count_' . $key;
1125 $key = substr($this->getItem($prefix . '_type_' . $key), $length);
1126
1127 if (!empty($key)) {
1128 if (($count = intval($this->getItem($idc))) > 0) {
1129 $value = array();
1130
1131 for ($i = 1; $i <= $count; $i++) {
1132 $value[] = $this->getItem($idv . '_' . $i);
1133 }
1134
1135 $value = ($count == 1) ? reset($value) : $value;
1136 } else {
1137 $value = $this->getItem($idv);
1138 }
1139
1140 if (!is_null($value)) {
1141 $result[$key] = $value;
1142 }
1143 }
1144 }
1145 } else {
1146 // No alias for the AX schema has been found,
1147 // so there is no AX data in the OP's response.
1148 }
1149
1150 return $result;
1151 }
1152
1153 /**
1154 * @return array
1155 */
1156 protected function getSregAttributes()
1157 {
1158 $attributes = array();
1159 $sreg_to_ax = array_flip(self::$ax_to_sreg);
1160 if ($alias = $this->getNamespaceAlias('http://openid.net/extensions/sreg/1.1', 'sreg')) {
1161 foreach (explode(',', $this->data['openid_signed']) as $key) {
1162 $keyMatch = $alias . '.';
1163 if (strncmp($key, $keyMatch, strlen($keyMatch)) !== 0) {
1164 continue;
1165 }
1166 $key = substr($key, strlen($keyMatch));
1167 if (!isset($sreg_to_ax[$key])) {
1168 # The field name isn't part of the SREG spec, so we ignore it.
1169 continue;
1170 }
1171 $attributes[$sreg_to_ax[$key]] = $this->data['openid_' . $alias . '_' . $key];
1172 }
1173 }
1174 return $attributes;
1175 }
1176
1177 /**
1178 * Gets AX/SREG attributes provided by OP. should be used only after successful validation.
1179 * Note that it does not guarantee that any of the required/optional parameters will be present,
1180 * or that there will be no other attributes besides those specified.
1181 * In other words. OP may provide whatever information it wants to.
1182 * * SREG names will be mapped to AX names.
1183 * *
1184 * @return array Array of attributes with keys being the AX schema names, e.g. 'contact/email' @see http://www.axschema.org/types/
1185 */
1186 public function getAttributes()
1187 {
1188 if (isset($this->data['openid_ns'])
1189 && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0'
1190 ) { # OpenID 2.0
1191 # We search for both AX and SREG attributes, with AX taking precedence.
1192 return $this->getAxAttributes() + $this->getSregAttributes();
1193 }
1194 return $this->getSregAttributes();
1195 }
1196
1197 /**
1198 * Gets an OAuth request token if the OpenID+OAuth hybrid protocol has been used.
1199 *
1200 * In order to use the OpenID+OAuth hybrid protocol, you need to add at least one
1201 * scope to the $openid->oauth array before you get the call to getAuthUrl(), e.g.:
1202 * $openid->oauth[] = 'https://www.googleapis.com/auth/plus.me';
1203 *
1204 * Furthermore the registered consumer name must fit the OpenID realm.
1205 * To register an OpenID consumer at Google use: https://www.google.com/accounts/ManageDomains
1206 *
1207 * @return string|bool OAuth request token on success, FALSE if no token was provided.
1208 */
1209 public function getOAuthRequestToken()
1210 {
1211 $alias = $this->getNamespaceAlias('http://specs.openid.net/extensions/oauth/1.0');
1212
1213 return !empty($alias) ? $this->data['openid_' . $alias . '_request_token'] : false;
1214 }
1215
1216 /**
1217 * Gets the alias for the specified namespace, if it's present.
1218 *
1219 * @param string $namespace The namespace for which an alias is needed.
1220 * @param string $hint Common alias of this namespace, used for optimization.
1221 * @return string|null The namespace alias if found, otherwise - NULL.
1222 */
1223 private function getNamespaceAlias($namespace, $hint = null)
1224 {
1225 $result = null;
1226
1227 if (empty($hint) || $this->getItem('openid_ns_' . $hint) != $namespace) {
1228 // The common alias is either undefined or points to
1229 // some other extension - search for another alias..
1230 $prefix = 'openid_ns_';
1231 $length = strlen($prefix);
1232
1233 foreach ($this->data as $key => $val) {
1234 if (strncmp($key, $prefix, $length) === 0 && $val === $namespace) {
1235 $result = trim(substr($key, $length));
1236 break;
1237 }
1238 }
1239 } else {
1240 $result = $hint;
1241 }
1242
1243 return $result;
1244 }
1245
1246 /**
1247 * Gets an item from the $data array by the specified id.
1248 *
1249 * @param string $id The id of the desired item.
1250 * @return string|null The item if found, otherwise - NULL.
1251 */
1252 private function getItem($id)
1253 {
1254 return isset($this->data[$id]) ? $this->data[$id] : null;
1255 }
1256 }
1257