rpicms

A CMS for the Raspberry Pi
git clone git://archive.git.mtrnord.blog/RpicmsTeam/rpicms.git
Log | Files | Refs | README | LICENSE

openid.php (34725B)


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