Gentics Portal.Node PHP API
 All Classes Namespaces Functions Variables Pages
LightOpenID.php
1 <?php
2 /**
3  * This class provides a simple interface for OpenID (1.1 and 2.0) authentication.
4  * Supports Yadis discovery.
5  * The authentication process is stateless/dumb.
6  *
7  * Usage:
8  * Sign-on with OpenID is a two step process:
9  * Step one is authentication with the provider:
10  * <code>
11  * $openid = new LightOpenID;
12  * $openid->identity = 'ID supplied by user';
13  * header('Location: ' . $openid->authUrl());
14  * </code>
15  * The provider then sends various parameters via GET, one of them is openid_mode.
16  * Step two is verification:
17  * <code>
18  * if ($this->data['openid_mode']) {
19  * $openid = new LightOpenID;
20  * echo $openid->validate() ? 'Logged in.' : 'Failed';
21  * }
22  * </code>
23  *
24  * Optionally, you can set $returnUrl and $realm (or $trustRoot, which is an alias).
25  * The default values for those are:
26  * $openid->realm = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
27  * $openid->returnUrl = $openid->realm . $_SERVER['REQUEST_URI'];
28  * If you don't know their meaning, refer to any openid tutorial, or specification. Or just guess.
29  *
30  * AX and SREG extensions are supported.
31  * To use them, specify $openid->required and/or $openid->optional before calling $openid->authUrl().
32  * These are arrays, with values being AX schema paths (the 'path' part of the URL).
33  * For example:
34  * $openid->required = array('namePerson/friendly', 'contact/email');
35  * $openid->optional = array('namePerson/first');
36  * If the server supports only SREG or OpenID 1.1, these are automaticaly
37  * mapped to SREG names, so that user doesn't have to know anything about the server.
38  *
39  * To get the values, use $openid->getAttributes().
40  *
41  *
42  * The library requires PHP >= 5.1.2 with curl or http/https stream wrappers enabled.
43  * @author Mewp
44  * @copyright Copyright (c) 2010, Mewp
45  * @license http://www.opensource.org/licenses/mit-license.php MIT
46  */
48 {
49  public $returnUrl
50  , $required = array()
51  , $optional = array()
52  , $verify_peer = null
53  , $capath = null
54  , $cainfo = null;
55  private $identity, $claimed_id;
56  protected $server, $version, $trustRoot, $aliases, $identifier_select = false
57  , $ax = false, $sreg = false, $data;
58  static protected $ax_to_sreg = array(
59  'namePerson/friendly' => 'nickname',
60  'contact/email' => 'email',
61  'namePerson' => 'fullname',
62  'birthDate' => 'dob',
63  'person/gender' => 'gender',
64  'contact/postalCode/home' => 'postcode',
65  'contact/country/home' => 'country',
66  'pref/language' => 'language',
67  'pref/timezone' => 'timezone',
68  );
69 
70  function __construct()
71  {
72  $this->trustRoot = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
73  $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?');
74  $this->returnUrl = $this->trustRoot . $uri;
75 
76  $this->data = $_POST + $_GET; # OPs may send data as POST or GET.
77  }
78 
79  function __set($name, $value)
80  {
81  switch ($name) {
82  case 'identity':
83  if (strlen($value = trim((String) $value))) {
84  if (preg_match('#^xri:/*#i', $value, $m)) {
85  $value = substr($value, strlen($m[0]));
86  } elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) {
87  $value = "http://$value";
88  }
89  if (preg_match('#^https?://[^/]+$#i', $value, $m)) {
90  $value .= '/';
91  }
92  }
93  $this->$name = $this->claimed_id = $value;
94  break;
95  case 'trustRoot':
96  case 'realm':
97  $this->trustRoot = trim($value);
98  }
99  }
100 
101  function __get($name)
102  {
103  switch ($name) {
104  case 'identity':
105  # We return claimed_id instead of identity,
106  # because the developer should see the claimed identifier,
107  # i.e. what he set as identity, not the op-local identifier (which is what we verify)
108  return $this->claimed_id;
109  case 'trustRoot':
110  case 'realm':
111  return $this->trustRoot;
112  case 'mode':
113  return empty($this->data['openid_mode']) ? null : $this->data['openid_mode'];
114  }
115  }
116 
117  /**
118  * Checks if the server specified in the url exists.
119  *
120  * @param $url url to check
121  * @return true, if the server exists; false otherwise
122  */
123  function hostExists($url)
124  {
125  if (strpos($url, '/') === false) {
126  $server = $url;
127  } else {
128  $server = @parse_url($url, PHP_URL_HOST);
129  }
130 
131  if (!$server) {
132  return false;
133  }
134 
135  return !!gethostbynamel($server);
136  }
137 
138  protected function request_curl($url, $method='GET', $params=array())
139  {
140  $params = http_build_query($params, '', '&');
141  $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : ''));
142  curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
143  curl_setopt($curl, CURLOPT_HEADER, false);
144  curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
145  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
146  curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*'));
147 
148  if($this->verify_peer !== null) {
149  curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer);
150  if($this->capath) {
151  curl_setopt($curl, CURLOPT_CAPATH, $this->capath);
152  }
153 
154  if($this->cainfo) {
155  curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo);
156  }
157  }
158 
159  if ($method == 'POST') {
160  curl_setopt($curl, CURLOPT_POST, true);
161  curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
162  } elseif ($method == 'HEAD') {
163  curl_setopt($curl, CURLOPT_HEADER, true);
164  curl_setopt($curl, CURLOPT_NOBODY, true);
165  } else {
166  curl_setopt($curl, CURLOPT_HTTPGET, true);
167  }
168  $response = curl_exec($curl);
169 
170  if($method == 'HEAD') {
171  $headers = array();
172  foreach(explode("\n", $response) as $header) {
173  $pos = strpos($header,':');
174  $name = strtolower(trim(substr($header, 0, $pos)));
175  $headers[$name] = trim(substr($header, $pos+1));
176  }
177 
178  # Updating claimed_id in case of redirections.
179  $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
180  if($effective_url != $url) {
181  $this->identity = $this->claimed_id = $effective_url;
182  }
183 
184  return $headers;
185  }
186 
187  if (curl_errno($curl)) {
188  throw new ErrorException(curl_error($curl), curl_errno($curl));
189  }
190 
191  return $response;
192  }
193 
194  protected function request_streams($url, $method='GET', $params=array())
195  {
196  if(!$this->hostExists($url)) {
197  throw new ErrorException('Invalid request.');
198  }
199 
200  $params = http_build_query($params, '', '&');
201  switch($method) {
202  case 'GET':
203  $opts = array(
204  'http' => array(
205  'method' => 'GET',
206  'header' => 'Accept: application/xrds+xml, */*',
207  'ignore_errors' => true,
208  )
209  );
210  $url = $url . ($params ? '?' . $params : '');
211  break;
212  case 'POST':
213  $opts = array(
214  'http' => array(
215  'method' => 'POST',
216  'header' => 'Content-type: application/x-www-form-urlencoded',
217  'content' => $params,
218  'ignore_errors' => true,
219  )
220  );
221  break;
222  case 'HEAD':
223  # We want to send a HEAD request,
224  # but since get_headers doesn't accept $context parameter,
225  # we have to change the defaults.
226  $default = stream_context_get_options(stream_context_get_default());
227  stream_context_get_default(
228  array('http' => array(
229  'method' => 'HEAD',
230  'header' => 'Accept: application/xrds+xml, */*',
231  'ignore_errors' => true,
232  ))
233  );
234 
235  $url = $url . ($params ? '?' . $params : '');
236  $headers_tmp = get_headers ($url);
237  if(!$headers_tmp) {
238  return array();
239  }
240 
241  # Parsing headers.
242  $headers = array();
243  foreach($headers_tmp as $header) {
244  $pos = strpos($header,':');
245  $name = strtolower(trim(substr($header, 0, $pos)));
246  $headers[$name] = trim(substr($header, $pos+1));
247 
248  # Following possible redirections. The point is just to have
249  # claimed_id change with them, because get_headers() will
250  # follow redirections automatically.
251  # We ignore redirections with relative paths.
252  # If any known provider uses them, file a bug report.
253  if($name == 'location') {
254  if(strpos($headers[$name], 'http') === 0) {
255  $this->identity = $this->claimed_id = $headers[$name];
256  } elseif($headers[$name][0] == '/') {
257  $parsed_url = parse_url($this->claimed_id);
258  $this->identity =
259  $this->claimed_id = $parsed_url['scheme'] . '://'
260  . $parsed_url['host']
261  . $headers[$name];
262  }
263  }
264  }
265 
266  # And restore them.
267  stream_context_get_default($default);
268  return $headers;
269  }
270 
271  if($this->verify_peer) {
272  $opts += array('ssl' => array(
273  'verify_peer' => true,
274  'capath' => $this->capath,
275  'cafile' => $this->cainfo,
276  ));
277  }
278 
279  $context = stream_context_create ($opts);
280 
281  return file_get_contents($url, false, $context);
282  }
283 
284  protected function request($url, $method='GET', $params=array())
285  {
286  if(function_exists('curl_init') && !ini_get('safe_mode')) {
287  return $this->request_curl($url, $method, $params);
288  }
289  return $this->request_streams($url, $method, $params);
290  }
291 
292  protected function build_url($url, $parts)
293  {
294  if (isset($url['query'], $parts['query'])) {
295  $parts['query'] = $url['query'] . '&' . $parts['query'];
296  }
297 
298  $url = $parts + $url;
299  $url = $url['scheme'] . '://'
300  . (empty($url['username'])?''
301  :(empty($url['password'])? "{$url['username']}@"
302  :"{$url['username']}:{$url['password']}@"))
303  . $url['host']
304  . (empty($url['port'])?'':":{$url['port']}")
305  . (empty($url['path'])?'':$url['path'])
306  . (empty($url['query'])?'':"?{$url['query']}")
307  . (empty($url['fragment'])?'':"#{$url['fragment']}");
308  return $url;
309  }
310 
311  /**
312  * Helper function used to scan for <meta>/<link> tags and extract information
313  * from them
314  */
315  protected function htmlTag($content, $tag, $attrName, $attrValue, $valueName)
316  {
317  preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1);
318  preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2);
319 
320  $result = array_merge($matches1[1], $matches2[1]);
321  return empty($result)?false:$result[0];
322  }
323 
324  /**
325  * Performs Yadis and HTML discovery. Normally not used.
326  * @param $url Identity URL.
327  * @return String OP Endpoint (i.e. OpenID provider address).
328  * @throws ErrorException
329  */
330  function discover($url)
331  {
332  if (!$url) throw new ErrorException('No identity supplied.');
333  # Use xri.net proxy to resolve i-name identities
334  if (!preg_match('#^https?:#', $url)) {
335  $url = "https://xri.net/$url";
336  }
337 
338  # We save the original url in case of Yadis discovery failure.
339  # It can happen when we'll be lead to an XRDS document
340  # which does not have any OpenID2 services.
341  $originalUrl = $url;
342 
343  # A flag to disable yadis discovery in case of failure in headers.
344  $yadis = true;
345 
346  # We'll jump a maximum of 5 times, to avoid endless redirections.
347  for ($i = 0; $i < 5; $i ++) {
348  if ($yadis) {
349  $headers = $this->request($url, 'HEAD');
350 
351  $next = false;
352  if (isset($headers['x-xrds-location'])) {
353  $url = $this->build_url(parse_url($url), parse_url(trim($headers['x-xrds-location'])));
354  $next = true;
355  }
356 
357  if (isset($headers['content-type'])
358  && (strpos($headers['content-type'], 'application/xrds+xml') !== false
359  || strpos($headers['content-type'], 'text/xml') !== false)
360  ) {
361  # Apparently, some providers return XRDS documents as text/html.
362  # While it is against the spec, allowing this here shouldn't break
363  # compatibility with anything.
364  # ---
365  # Found an XRDS document, now let's find the server, and optionally delegate.
366  $content = $this->request($url, 'GET');
367 
368  preg_match_all('#<Service.*?>(.*?)</Service>#s', $content, $m);
369  foreach($m[1] as $content) {
370  $content = ' ' . $content; # The space is added, so that strpos doesn't return 0.
371 
372  # OpenID 2
373  $ns = preg_quote('http://specs.openid.net/auth/2.0/');
374  if(preg_match('#<Type>\s*'.$ns.'(server|signon)\s*</Type>#s', $content, $type)) {
375  if ($type[1] == 'server') $this->identifier_select = true;
376 
377  preg_match('#<URI.*?>(.*)</URI>#', $content, $server);
378  preg_match('#<(Local|Canonical)ID>(.*)</\1ID>#', $content, $delegate);
379  if (empty($server)) {
380  return false;
381  }
382  # Does the server advertise support for either AX or SREG?
383  $this->ax = (bool) strpos($content, '<Type>http://openid.net/srv/ax/1.0</Type>');
384  $this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>')
385  || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>');
386 
387  $server = $server[1];
388  if (isset($delegate[2])) $this->identity = trim($delegate[2]);
389  $this->version = 2;
390 
391  $this->server = $server;
392  return $server;
393  }
394 
395  # OpenID 1.1
396  $ns = preg_quote('http://openid.net/signon/1.1');
397  if (preg_match('#<Type>\s*'.$ns.'\s*</Type>#s', $content)) {
398 
399  preg_match('#<URI.*?>(.*)</URI>#', $content, $server);
400  preg_match('#<.*?Delegate>(.*)</.*?Delegate>#', $content, $delegate);
401  if (empty($server)) {
402  return false;
403  }
404  # AX can be used only with OpenID 2.0, so checking only SREG
405  $this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>')
406  || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>');
407 
408  $server = $server[1];
409  if (isset($delegate[1])) $this->identity = $delegate[1];
410  $this->version = 1;
411 
412  $this->server = $server;
413  return $server;
414  }
415  }
416 
417  $next = true;
418  $yadis = false;
419  $url = $originalUrl;
420  $content = null;
421  break;
422  }
423  if ($next) continue;
424 
425  # There are no relevant information in headers, so we search the body.
426  $content = $this->request($url, 'GET');
427  $location = $this->htmlTag($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content');
428  if ($location) {
429  $url = $this->build_url(parse_url($url), parse_url($location));
430  continue;
431  }
432  }
433 
434  if (!$content) $content = $this->request($url, 'GET');
435 
436  # At this point, the YADIS Discovery has failed, so we'll switch
437  # to openid2 HTML discovery, then fallback to openid 1.1 discovery.
438  $server = $this->htmlTag($content, 'link', 'rel', 'openid2.provider', 'href');
439  $delegate = $this->htmlTag($content, 'link', 'rel', 'openid2.local_id', 'href');
440  $this->version = 2;
441 
442  if (!$server) {
443  # The same with openid 1.1
444  $server = $this->htmlTag($content, 'link', 'rel', 'openid.server', 'href');
445  $delegate = $this->htmlTag($content, 'link', 'rel', 'openid.delegate', 'href');
446  $this->version = 1;
447  }
448 
449  if ($server) {
450  # We found an OpenID2 OP Endpoint
451  if ($delegate) {
452  # We have also found an OP-Local ID.
453  $this->identity = $delegate;
454  }
455  $this->server = $server;
456  return $server;
457  }
458  throw new ErrorException('No servers found!');
459  }
460  throw new ErrorException('Endless redirection!');
461  }
462 
463  protected function sregParams()
464  {
465  $params = array();
466  # We always use SREG 1.1, even if the server is advertising only support for 1.0.
467  # That's because it's fully backwards compatibile with 1.0, and some providers
468  # advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com
469  $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1';
470  if ($this->required) {
471  $params['openid.sreg.required'] = array();
472  foreach ($this->required as $required) {
473  if (!isset(self::$ax_to_sreg[$required])) continue;
474  $params['openid.sreg.required'][] = self::$ax_to_sreg[$required];
475  }
476  $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']);
477  }
478 
479  if ($this->optional) {
480  $params['openid.sreg.optional'] = array();
481  foreach ($this->optional as $optional) {
482  if (!isset(self::$ax_to_sreg[$optional])) continue;
483  $params['openid.sreg.optional'][] = self::$ax_to_sreg[$optional];
484  }
485  $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']);
486  }
487  return $params;
488  }
489 
490  protected function axParams()
491  {
492  $params = array();
493  if ($this->required || $this->optional) {
494  $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0';
495  $params['openid.ax.mode'] = 'fetch_request';
496  $this->aliases = array();
497  $counts = array();
498  $required = array();
499  $optional = array();
500  foreach (array('required','optional') as $type) {
501  foreach ($this->$type as $alias => $field) {
502  if (is_int($alias)) $alias = strtr($field, '/', '_');
503  $this->aliases[$alias] = 'http://axschema.org/' . $field;
504  if (empty($counts[$alias])) $counts[$alias] = 0;
505  $counts[$alias] += 1;
506  ${$type}[] = $alias;
507  }
508  }
509  foreach ($this->aliases as $alias => $ns) {
510  $params['openid.ax.type.' . $alias] = $ns;
511  }
512  foreach ($counts as $alias => $count) {
513  if ($count == 1) continue;
514  $params['openid.ax.count.' . $alias] = $count;
515  }
516 
517  # Don't send empty ax.requied and ax.if_available.
518  # Google and possibly other providers refuse to support ax when one of these is empty.
519  if($required) {
520  $params['openid.ax.required'] = implode(',', $required);
521  }
522  if($optional) {
523  $params['openid.ax.if_available'] = implode(',', $optional);
524  }
525  }
526  return $params;
527  }
528 
529  protected function authUrl_v1()
530  {
531  $returnUrl = $this->returnUrl;
532  # If we have an openid.delegate that is different from our claimed id,
533  # we need to somehow preserve the claimed id between requests.
534  # The simplest way is to just send it along with the return_to url.
535  if($this->identity != $this->claimed_id) {
536  $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->claimed_id;
537  }
538 
539  $params = array(
540  'openid.return_to' => $returnUrl,
541  'openid.mode' => 'checkid_setup',
542  'openid.identity' => $this->identity,
543  'openid.trust_root' => $this->trustRoot,
544  ) + $this->sregParams();
545 
546  return $this->build_url(parse_url($this->server)
547  , array('query' => http_build_query($params, '', '&')));
548  }
549 
550  protected function authUrl_v2($identifier_select)
551  {
552  $params = array(
553  'openid.ns' => 'http://specs.openid.net/auth/2.0',
554  'openid.mode' => 'checkid_setup',
555  'openid.return_to' => $this->returnUrl,
556  'openid.realm' => $this->trustRoot,
557  );
558  if ($this->ax) {
559  $params += $this->axParams();
560  }
561  if ($this->sreg) {
562  $params += $this->sregParams();
563  }
564  if (!$this->ax && !$this->sreg) {
565  # If OP doesn't advertise either SREG, nor AX, let's send them both
566  # in worst case we don't get anything in return.
567  $params += $this->axParams() + $this->sregParams();
568  }
569 
570  if ($identifier_select) {
571  $params['openid.identity'] = $params['openid.claimed_id']
572  = 'http://specs.openid.net/auth/2.0/identifier_select';
573  } else {
574  $params['openid.identity'] = $this->identity;
575  $params['openid.claimed_id'] = $this->claimed_id;
576  }
577 
578  return $this->build_url(parse_url($this->server)
579  , array('query' => http_build_query($params, '', '&')));
580  }
581 
582  /**
583  * Returns authentication url. Usually, you want to redirect your user to it.
584  * @return String The authentication url.
585  * @param String $select_identifier Whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1.
586  * @throws ErrorException
587  */
588  function authUrl($identifier_select = null)
589  {
590  if (!$this->server) $this->discover($this->identity);
591 
592  if ($this->version == 2) {
593  if ($identifier_select === null) {
594  return $this->authUrl_v2($this->identifier_select);
595  }
596  return $this->authUrl_v2($identifier_select);
597  }
598  return $this->authUrl_v1();
599  }
600 
601  /**
602  * Performs OpenID verification with the OP.
603  * @return Bool Whether the verification was successful.
604  * @throws ErrorException
605  */
606  function validate()
607  {
608  $this->claimed_id = isset($this->data['openid_claimed_id'])?$this->data['openid_claimed_id']:$this->data['openid_identity'];
609  $params = array(
610  'openid.assoc_handle' => $this->data['openid_assoc_handle'],
611  'openid.signed' => $this->data['openid_signed'],
612  'openid.sig' => $this->data['openid_sig'],
613  );
614 
615  if (isset($this->data['openid_ns'])) {
616  # We're dealing with an OpenID 2.0 server, so let's set an ns
617  # Even though we should know location of the endpoint,
618  # we still need to verify it by discovery, so $server is not set here
619  $params['openid.ns'] = 'http://specs.openid.net/auth/2.0';
620  } elseif (isset($this->data['openid_claimed_id'])
621  && $this->data['openid_claimed_id'] != $this->data['openid_identity']
622  ) {
623  # If it's an OpenID 1 provider, and we've got claimed_id,
624  # we have to append it to the returnUrl, like authUrl_v1 does.
625  $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?')
626  . 'openid.claimed_id=' . $this->claimed_id;
627  }
628 
629  if ($this->data['openid_return_to'] != $this->returnUrl) {
630  # The return_to url must match the url of current request.
631  # I'm assuing that noone will set the returnUrl to something that doesn't make sense.
632  return false;
633  }
634 
635  $server = $this->discover($this->claimed_id);
636 
637  foreach (explode(',', $this->data['openid_signed']) as $item) {
638  # Checking whether magic_quotes_gpc is turned on, because
639  # the function may fail if it is. For example, when fetching
640  # AX namePerson, it might containg an apostrophe, which will be escaped.
641  # In such case, validation would fail, since we'd send different data than OP
642  # wants to verify. stripslashes() should solve that problem, but we can't
643  # use it when magic_quotes is off.
644  $value = $this->data['openid_' . str_replace('.','_',$item)];
645  $params['openid.' . $item] = get_magic_quotes_gpc() ? stripslashes($value) : $value;
646 
647  }
648 
649  $params['openid.mode'] = 'check_authentication';
650 
651  $response = $this->request($server, 'POST', $params);
652 
653  return preg_match('/is_valid\s*:\s*true/i', $response);
654  }
655 
656  protected function getAxAttributes()
657  {
658  $alias = null;
659  if (isset($this->data['openid_ns_ax'])
660  && $this->data['openid_ns_ax'] != 'http://openid.net/srv/ax/1.0'
661  ) { # It's the most likely case, so we'll check it before
662  $alias = 'ax';
663  } else {
664  # 'ax' prefix is either undefined, or points to another extension,
665  # so we search for another prefix
666  foreach ($this->data as $key => $val) {
667  if (substr($key, 0, strlen('openid_ns_')) == 'openid_ns_'
668  && $val == 'http://openid.net/srv/ax/1.0'
669  ) {
670  $alias = substr($key, strlen('openid_ns_'));
671  break;
672  }
673  }
674  }
675  if (!$alias) {
676  # An alias for AX schema has not been found,
677  # so there is no AX data in the OP's response
678  return array();
679  }
680 
681  $attributes = array();
682  foreach ($this->data as $key => $value) {
683  $keyMatch = 'openid_' . $alias . '_value_';
684  if (substr($key, 0, strlen($keyMatch)) != $keyMatch) {
685  continue;
686  }
687  $key = substr($key, strlen($keyMatch));
688  if (!isset($this->data['openid_' . $alias . '_type_' . $key])) {
689  # OP is breaking the spec by returning a field without
690  # associated ns. This shouldn't happen, but it's better
691  # to check, than cause an E_NOTICE.
692  continue;
693  }
694  $key = substr($this->data['openid_' . $alias . '_type_' . $key],
695  strlen('http://axschema.org/'));
696  $attributes[$key] = $value;
697  }
698  return $attributes;
699  }
700 
701  protected function getSregAttributes()
702  {
703  $attributes = array();
704  $sreg_to_ax = array_flip(self::$ax_to_sreg);
705  foreach ($this->data as $key => $value) {
706  $keyMatch = 'openid_sreg_';
707  if (substr($key, 0, strlen($keyMatch)) != $keyMatch) {
708  continue;
709  }
710  $key = substr($key, strlen($keyMatch));
711  if (!isset($sreg_to_ax[$key])) {
712  # The field name isn't part of the SREG spec, so we ignore it.
713  continue;
714  }
715  $attributes[$sreg_to_ax[$key]] = $value;
716  }
717  return $attributes;
718  }
719 
720  /**
721  * Gets AX/SREG attributes provided by OP. should be used only after successful validaton.
722  * Note that it does not guarantee that any of the required/optional parameters will be present,
723  * or that there will be no other attributes besides those specified.
724  * In other words. OP may provide whatever information it wants to.
725  * * SREG names will be mapped to AX names.
726  * * @return Array Array of attributes with keys being the AX schema names, e.g. 'contact/email'
727  * @see http://www.axschema.org/types/
728  */
729  function getAttributes()
730  {
731  if (isset($this->data['openid_ns'])
732  && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0'
733  ) { # OpenID 2.0
734  # We search for both AX and SREG attributes, with AX taking precedence.
735  return $this->getAxAttributes() + $this->getSregAttributes();
736  }
737  return $this->getSregAttributes();
738  }
739 }