cms/drupal/modules/openid/openid.inc
changeset 541 e756a8c72c3d
equal deleted inserted replaced
540:07239de796bb 541:e756a8c72c3d
       
     1 <?php
       
     2 
       
     3 /**
       
     4  * @file
       
     5  * OpenID utility functions.
       
     6  */
       
     7 
       
     8 /**
       
     9  * Diffie-Hellman Key Exchange Default Value.
       
    10  *
       
    11  * This is used to establish an association between the Relying Party and the
       
    12  * OpenID Provider.
       
    13  *
       
    14  * See RFC 2631: http://www.ietf.org/rfc/rfc2631.txt
       
    15  */
       
    16 define('OPENID_DH_DEFAULT_MOD', '155172898181473697471232257763715539915724801' .
       
    17        '966915404479707795314057629378541917580651227423698188993727816152646631' .
       
    18        '438561595825688188889951272158842675419950341258706556549803580104870537' .
       
    19        '681476726513255747040765857479291291572334510643245094715007229621094194' .
       
    20        '349783925984760375594985848253359305585439638443');
       
    21 
       
    22 /**
       
    23  * Diffie-Hellman generator; used for Diffie-Hellman key exchange computations.
       
    24  */
       
    25 define('OPENID_DH_DEFAULT_GEN', '2');
       
    26 
       
    27 /**
       
    28  * SHA-1 hash block size; used for Diffie-Hellman key exchange computations.
       
    29  */
       
    30 define('OPENID_SHA1_BLOCKSIZE', 64);
       
    31 
       
    32 /**
       
    33  * Random number generator; used for Diffie-Hellman key exchange computations.
       
    34  */
       
    35 define('OPENID_RAND_SOURCE', '/dev/urandom');
       
    36 
       
    37 /**
       
    38  * OpenID Authentication 2.0 namespace URL.
       
    39  */
       
    40 define('OPENID_NS_2_0', 'http://specs.openid.net/auth/2.0');
       
    41 
       
    42 /**
       
    43  * OpenID Authentication 1.1 namespace URL; used for backwards-compatibility.
       
    44  */
       
    45 define('OPENID_NS_1_1', 'http://openid.net/signon/1.1');
       
    46 
       
    47 /**
       
    48  * OpenID Authentication 1.0 namespace URL; used for backwards-compatibility.
       
    49  */
       
    50 define('OPENID_NS_1_0', 'http://openid.net/signon/1.0');
       
    51 
       
    52 /**
       
    53  * OpenID namespace used in Yadis documents.
       
    54  */
       
    55 define('OPENID_NS_OPENID', 'http://openid.net/xmlns/1.0');
       
    56 
       
    57 /**
       
    58  * OpenID Simple Registration extension.
       
    59  */
       
    60 define('OPENID_NS_SREG', 'http://openid.net/extensions/sreg/1.1');
       
    61 
       
    62 /**
       
    63  * OpenID Attribute Exchange extension.
       
    64  */
       
    65 define('OPENID_NS_AX', 'http://openid.net/srv/ax/1.0');
       
    66 
       
    67 /**
       
    68  * Extensible Resource Descriptor documents.
       
    69  */
       
    70 define('OPENID_NS_XRD', 'xri://$xrd*($v*2.0)');
       
    71 
       
    72 /**
       
    73  * Performs an HTTP 302 redirect (for the 1.x protocol).
       
    74  */
       
    75 function openid_redirect_http($url, $message) {
       
    76   $query = array();
       
    77   foreach ($message as $key => $val) {
       
    78     $query[] = $key . '=' . urlencode($val);
       
    79   }
       
    80 
       
    81   $sep = (strpos($url, '?') === FALSE) ? '?' : '&';
       
    82   header('Location: ' . $url . $sep . implode('&', $query), TRUE, 302);
       
    83 
       
    84   drupal_exit();
       
    85 }
       
    86 
       
    87 /**
       
    88  * Creates a js auto-submit redirect for (for the 2.x protocol)
       
    89  */
       
    90 function openid_redirect($url, $message) {
       
    91   global $language;
       
    92 
       
    93   $output = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n";
       
    94   $output .= '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="' . $language->language . '" lang="' . $language->language . '">' . "\n";
       
    95   $output .= "<head>\n";
       
    96   $output .= "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n";
       
    97   $output .= "<title>" . t('OpenID redirect') . "</title>\n";
       
    98   $output .= "</head>\n";
       
    99   $output .= "<body>\n";
       
   100   $elements = drupal_get_form('openid_redirect_form', $url, $message);
       
   101   $output .= drupal_render($elements);
       
   102   $output .= '<script type="text/javascript">document.getElementById("openid-redirect-form").submit();</script>' . "\n";
       
   103   $output .= "</body>\n";
       
   104   $output .= "</html>\n";
       
   105   print $output;
       
   106 
       
   107   drupal_exit();
       
   108 }
       
   109 
       
   110 function openid_redirect_form($form, &$form_state, $url, $message) {
       
   111   $form['#action'] = $url;
       
   112   $form['#method'] = "post";
       
   113   foreach ($message as $key => $value) {
       
   114     $form[$key] = array(
       
   115       '#type' => 'hidden',
       
   116       '#name' => $key,
       
   117       '#value' => $value,
       
   118     );
       
   119   }
       
   120   $form['actions'] = array('#type' => 'actions');
       
   121   $form['actions']['submit'] = array(
       
   122     '#type' => 'submit',
       
   123     '#prefix' => '<noscript><div>',
       
   124     '#suffix' => '</div></noscript>',
       
   125     '#value' => t('Send'),
       
   126   );
       
   127 
       
   128   return $form;
       
   129 }
       
   130 
       
   131 /**
       
   132  * Parse an XRDS document.
       
   133  *
       
   134  * @param $raw_xml
       
   135  *   A string containing the XRDS document.
       
   136  * @return
       
   137  *   An array of service entries.
       
   138  */
       
   139 function _openid_xrds_parse($raw_xml) {
       
   140   $services = array();
       
   141 
       
   142   // For PHP version >= 5.2.11, we can use this function to protect against
       
   143   // malicious doctype declarations and other unexpected entity loading.
       
   144   // However, we will not rely on it, and reject any XML with a DOCTYPE.
       
   145   $disable_entity_loader = function_exists('libxml_disable_entity_loader');
       
   146   if ($disable_entity_loader) {
       
   147     $load_entities = libxml_disable_entity_loader(TRUE);
       
   148   }
       
   149 
       
   150   // Load the XML into a DOM document.
       
   151   $dom = new DOMDocument();
       
   152   @$dom->loadXML($raw_xml);
       
   153 
       
   154   // Since DOCTYPE declarations from an untrusted source could be malicious, we
       
   155   // stop parsing here and treat the XML as invalid since XRDS documents do not
       
   156   // require, and are not expected to have, a DOCTYPE.
       
   157   if (isset($dom->doctype)) {
       
   158     return array();
       
   159   }
       
   160 
       
   161   // Also stop parsing if there is an unreasonably large number of tags.
       
   162   if ($dom->getElementsByTagName('*')->length > variable_get('openid_xrds_maximum_tag_count', 30000)) {
       
   163     return array();
       
   164   }
       
   165 
       
   166   // Parse the DOM document for the information we need.
       
   167   if ($xml = simplexml_import_dom($dom)) {
       
   168     foreach ($xml->children(OPENID_NS_XRD)->XRD as $xrd) {
       
   169       foreach ($xrd->children(OPENID_NS_XRD)->Service as $service_element) {
       
   170         $service = array(
       
   171           'priority' => $service_element->attributes()->priority ? (int)$service_element->attributes()->priority : PHP_INT_MAX,
       
   172           'types' => array(),
       
   173           'uri' => (string)$service_element->children(OPENID_NS_XRD)->URI,
       
   174           'service' => $service_element,
       
   175           'xrd' => $xrd,
       
   176         );
       
   177         foreach ($service_element->Type as $type) {
       
   178           $service['types'][] = (string)$type;
       
   179         }
       
   180         if ($service_element->children(OPENID_NS_XRD)->LocalID) {
       
   181           $service['identity'] = (string)$service_element->children(OPENID_NS_XRD)->LocalID;
       
   182         }
       
   183         elseif ($service_element->children(OPENID_NS_OPENID)->Delegate) {
       
   184           $service['identity'] = (string)$service_element->children(OPENID_NS_OPENID)->Delegate;
       
   185         }
       
   186         else {
       
   187           $service['identity'] = FALSE;
       
   188         }
       
   189         $services[] = $service;
       
   190       }
       
   191     }
       
   192   }
       
   193 
       
   194   // Return the LIBXML options to the previous state before returning.
       
   195   if ($disable_entity_loader) {
       
   196     libxml_disable_entity_loader($load_entities);
       
   197   }
       
   198 
       
   199   return $services;
       
   200 }
       
   201 
       
   202 /**
       
   203  * Select a service element.
       
   204  *
       
   205  * The procedure is described in OpenID Authentication 2.0, section 7.3.2.
       
   206  *
       
   207  * A new entry is added to the returned array with the key 'version' and the
       
   208  * value 1 or 2 specifying the protocol version used by the service.
       
   209  *
       
   210  * @param $services
       
   211  *   An array of service arrays as returned by openid_discovery().
       
   212  * @return
       
   213  *   The selected service array, or NULL if no valid services were found.
       
   214  */
       
   215 function _openid_select_service(array $services) {
       
   216   // Extensible Resource Identifier (XRI) Resolution Version 2.0, section 4.3.3:
       
   217   // Find the service with the highest priority (lowest integer value). If there
       
   218   // is a tie, select a random one, not just the first in the XML document.
       
   219   shuffle($services);
       
   220   $selected_service = NULL;
       
   221   $selected_type_priority = FALSE;
       
   222 
       
   223   // Search for an OP Identifier Element.
       
   224   foreach ($services as $service) {
       
   225     if (!empty($service['uri'])) {
       
   226       $type_priority = FALSE;
       
   227       if (in_array('http://specs.openid.net/auth/2.0/server', $service['types'])) {
       
   228         $service['version'] = 2;
       
   229         $type_priority = 1;
       
   230       }
       
   231       elseif (in_array('http://specs.openid.net/auth/2.0/signon', $service['types'])) {
       
   232         $service['version'] = 2;
       
   233         $type_priority = 2;
       
   234       }
       
   235       elseif (in_array(OPENID_NS_1_0, $service['types']) || in_array(OPENID_NS_1_1, $service['types'])) {
       
   236         $service['version'] = 1;
       
   237         $type_priority = 3;
       
   238       }
       
   239 
       
   240       if ($type_priority
       
   241           && (!$selected_service
       
   242               || $type_priority < $selected_type_priority
       
   243               || ($type_priority == $selected_type_priority && $service['priority'] < $selected_service['priority']))) {
       
   244         $selected_service = $service;
       
   245         $selected_type_priority = $type_priority;
       
   246       }
       
   247     }
       
   248   }
       
   249 
       
   250   if ($selected_service) {
       
   251     // Unset SimpleXMLElement instances that cannot be saved in $_SESSION.
       
   252     unset($selected_service['xrd']);
       
   253     unset($selected_service['service']);
       
   254   }
       
   255 
       
   256   return $selected_service;
       
   257 }
       
   258 
       
   259 /**
       
   260  * Determine if the given identifier is an XRI ID.
       
   261  */
       
   262 function _openid_is_xri($identifier) {
       
   263   // Strip the xri:// scheme from the identifier if present.
       
   264   if (stripos($identifier, 'xri://') === 0) {
       
   265     $identifier = substr($identifier, 6);
       
   266   }
       
   267 
       
   268   // Test whether the identifier starts with an XRI global context symbol or (.
       
   269   $firstchar = substr($identifier, 0, 1);
       
   270   if (strpos("=@+$!(", $firstchar) !== FALSE) {
       
   271     return TRUE;
       
   272   }
       
   273 
       
   274   return FALSE;
       
   275 }
       
   276 
       
   277 /**
       
   278  * Normalize the given identifier.
       
   279  *
       
   280  * The procedure is described in OpenID Authentication 2.0, section 7.2.
       
   281  */
       
   282 function openid_normalize($identifier) {
       
   283   $methods = module_invoke_all('openid_normalization_method_info');
       
   284   drupal_alter('openid_normalization_method_info', $methods);
       
   285 
       
   286   // Execute each method in turn, stopping after the first method accepted
       
   287   // the identifier.
       
   288   foreach ($methods as $method) {
       
   289     $result = $method($identifier);
       
   290     if ($result !== NULL) {
       
   291       $identifier = $result;
       
   292       break;
       
   293     }
       
   294   }
       
   295 
       
   296   return $identifier;
       
   297 }
       
   298 
       
   299 /**
       
   300  * OpenID normalization method: normalize XRI identifiers.
       
   301  */
       
   302 function _openid_xri_normalize($identifier) {
       
   303   if (_openid_is_xri($identifier)) {
       
   304     if (stristr($identifier, 'xri://') !== FALSE) {
       
   305       $identifier = substr($identifier, 6);
       
   306     }
       
   307     return $identifier;
       
   308   }
       
   309 }
       
   310 
       
   311 /**
       
   312  * OpenID normalization method: normalize URL identifiers.
       
   313  */
       
   314 function _openid_url_normalize($url) {
       
   315   $normalized_url = $url;
       
   316 
       
   317   if (stristr($url, '://') === FALSE) {
       
   318     $normalized_url = 'http://' . $url;
       
   319   }
       
   320 
       
   321   // Strip the fragment and fragment delimiter if present.
       
   322   $normalized_url = strtok($normalized_url, '#');
       
   323 
       
   324   if (substr_count($normalized_url, '/') < 3) {
       
   325     $normalized_url .= '/';
       
   326   }
       
   327 
       
   328   return $normalized_url;
       
   329 }
       
   330 
       
   331 /**
       
   332  * Create a serialized message packet as per spec: $key:$value\n .
       
   333  */
       
   334 function _openid_create_message($data) {
       
   335   $serialized = '';
       
   336 
       
   337   foreach ($data as $key => $value) {
       
   338     if ((strpos($key, ':') !== FALSE) || (strpos($key, "\n") !== FALSE) || (strpos($value, "\n") !== FALSE)) {
       
   339       return NULL;
       
   340     }
       
   341     $serialized .= "$key:$value\n";
       
   342   }
       
   343   return $serialized;
       
   344 }
       
   345 
       
   346 /**
       
   347  * Encode a message from _openid_create_message for HTTP Post
       
   348  */
       
   349 function _openid_encode_message($message) {
       
   350   $encoded_message = '';
       
   351 
       
   352   $items = explode("\n", $message);
       
   353   foreach ($items as $item) {
       
   354     $parts = explode(':', $item, 2);
       
   355 
       
   356     if (count($parts) == 2) {
       
   357       if ($encoded_message != '') {
       
   358         $encoded_message .= '&';
       
   359       }
       
   360       $encoded_message .= rawurlencode(trim($parts[0])) . '=' . rawurlencode(trim($parts[1]));
       
   361     }
       
   362   }
       
   363 
       
   364   return $encoded_message;
       
   365 }
       
   366 
       
   367 /**
       
   368  * Convert a direct communication message
       
   369  * into an associative array.
       
   370  */
       
   371 function _openid_parse_message($message) {
       
   372   $parsed_message = array();
       
   373 
       
   374   $items = explode("\n", $message);
       
   375   foreach ($items as $item) {
       
   376     $parts = explode(':', $item, 2);
       
   377 
       
   378     if (count($parts) == 2) {
       
   379       $parsed_message[$parts[0]] = $parts[1];
       
   380     }
       
   381   }
       
   382 
       
   383   return $parsed_message;
       
   384 }
       
   385 
       
   386 /**
       
   387  * Return a nonce value - formatted per OpenID spec.
       
   388  *
       
   389  * NOTE: This nonce is not cryptographically secure and only suitable for use
       
   390  * by the test framework.
       
   391  */
       
   392 function _openid_nonce() {
       
   393   // YYYY-MM-DDThh:mm:ssZ, plus some optional extra unique characters.
       
   394   return gmdate('Y-m-d\TH:i:s\Z') .
       
   395     chr(mt_rand(0, 25) + 65) .
       
   396     chr(mt_rand(0, 25) + 65) .
       
   397     chr(mt_rand(0, 25) + 65) .
       
   398     chr(mt_rand(0, 25) + 65);
       
   399 }
       
   400 
       
   401 /**
       
   402  * Pull the href attribute out of an html link element.
       
   403  */
       
   404 function _openid_link_href($rel, $html) {
       
   405   $rel = preg_quote($rel);
       
   406   preg_match('|<link\s+rel=["\'](.*)' . $rel . '(.*)["\'](.*)/?>|iUs', $html, $matches);
       
   407   if (isset($matches[3])) {
       
   408     preg_match('|href=["\']([^"]+)["\']|iU', $matches[3], $href);
       
   409     return trim($href[1]);
       
   410   }
       
   411   return FALSE;
       
   412 }
       
   413 
       
   414 /**
       
   415  * Pull the http-equiv attribute out of an html meta element
       
   416  */
       
   417 function _openid_meta_httpequiv($equiv, $html) {
       
   418   preg_match('|<meta\s+http-equiv=["\']' . $equiv . '["\'](.*)/?>|iUs', $html, $matches);
       
   419   if (isset($matches[1])) {
       
   420     preg_match('|content=["\']([^"]+)["\']|iUs', $matches[1], $content);
       
   421     if (isset($content[1])) {
       
   422       return $content[1];
       
   423     }
       
   424   }
       
   425   return FALSE;
       
   426 }
       
   427 
       
   428 /**
       
   429  * Sign certain keys in a message
       
   430  * @param $association - object loaded from openid_association or openid_server_association table
       
   431  *              - important fields are ->assoc_type and ->mac_key
       
   432  * @param $message_array - array of entire message about to be sent
       
   433  * @param $keys_to_sign - keys in the message to include in signature (without
       
   434  *  'openid.' appended)
       
   435  */
       
   436 function _openid_signature($association, $message_array, $keys_to_sign) {
       
   437   $signature = '';
       
   438   $sign_data = array();
       
   439 
       
   440   foreach ($keys_to_sign as $key) {
       
   441     if (isset($message_array['openid.' . $key])) {
       
   442       $sign_data[$key] = $message_array['openid.' . $key];
       
   443     }
       
   444   }
       
   445 
       
   446   $message = _openid_create_message($sign_data);
       
   447   $secret = base64_decode($association->mac_key);
       
   448   $signature = _openid_hmac($secret, $message);
       
   449 
       
   450   return base64_encode($signature);
       
   451 }
       
   452 
       
   453 function _openid_hmac($key, $text) {
       
   454   if (strlen($key) > OPENID_SHA1_BLOCKSIZE) {
       
   455     $key = sha1($key, TRUE);
       
   456   }
       
   457 
       
   458   $key = str_pad($key, OPENID_SHA1_BLOCKSIZE, chr(0x00));
       
   459   $ipad = str_repeat(chr(0x36), OPENID_SHA1_BLOCKSIZE);
       
   460   $opad = str_repeat(chr(0x5c), OPENID_SHA1_BLOCKSIZE);
       
   461   $hash1 = sha1(($key ^ $ipad) . $text, TRUE);
       
   462   $hmac = sha1(($key ^ $opad) . $hash1, TRUE);
       
   463 
       
   464   return $hmac;
       
   465 }
       
   466 
       
   467 function _openid_dh_base64_to_long($str) {
       
   468   $b64 = base64_decode($str);
       
   469 
       
   470   return _openid_dh_binary_to_long($b64);
       
   471 }
       
   472 
       
   473 function _openid_dh_long_to_base64($str) {
       
   474   return base64_encode(_openid_dh_long_to_binary($str));
       
   475 }
       
   476 
       
   477 function _openid_dh_binary_to_long($str) {
       
   478   $bytes = array_merge(unpack('C*', $str));
       
   479 
       
   480   $n = 0;
       
   481   foreach ($bytes as $byte) {
       
   482     $n = _openid_math_mul($n, pow(2, 8));
       
   483     $n = _openid_math_add($n, $byte);
       
   484   }
       
   485 
       
   486   return $n;
       
   487 }
       
   488 
       
   489 function _openid_dh_long_to_binary($long) {
       
   490   $cmp = _openid_math_cmp($long, 0);
       
   491   if ($cmp < 0) {
       
   492     return FALSE;
       
   493   }
       
   494 
       
   495   if ($cmp == 0) {
       
   496     return "\x00";
       
   497   }
       
   498 
       
   499   $bytes = array();
       
   500 
       
   501   while (_openid_math_cmp($long, 0) > 0) {
       
   502     array_unshift($bytes, _openid_math_mod($long, 256));
       
   503     $long = _openid_math_div($long, pow(2, 8));
       
   504   }
       
   505 
       
   506   if ($bytes && ($bytes[0] > 127)) {
       
   507     array_unshift($bytes, 0);
       
   508   }
       
   509 
       
   510   $string = '';
       
   511   foreach ($bytes as $byte) {
       
   512     $string .= pack('C', $byte);
       
   513   }
       
   514 
       
   515   return $string;
       
   516 }
       
   517 
       
   518 function _openid_dh_xorsecret($shared, $secret) {
       
   519   $dh_shared_str = _openid_dh_long_to_binary($shared);
       
   520   $sha1_dh_shared = sha1($dh_shared_str, TRUE);
       
   521   $xsecret = "";
       
   522   for ($i = 0; $i < strlen($secret); $i++) {
       
   523     $xsecret .= chr(ord($secret[$i]) ^ ord($sha1_dh_shared[$i]));
       
   524   }
       
   525 
       
   526   return $xsecret;
       
   527 }
       
   528 
       
   529 function _openid_dh_rand($stop) {
       
   530   $duplicate_cache = &drupal_static(__FUNCTION__, array());
       
   531 
       
   532   // Used as the key for the duplicate cache
       
   533   $rbytes = _openid_dh_long_to_binary($stop);
       
   534 
       
   535   if (isset($duplicate_cache[$rbytes])) {
       
   536     list($duplicate, $nbytes) = $duplicate_cache[$rbytes];
       
   537   }
       
   538   else {
       
   539     if ($rbytes[0] == "\x00") {
       
   540       $nbytes = strlen($rbytes) - 1;
       
   541     }
       
   542     else {
       
   543       $nbytes = strlen($rbytes);
       
   544     }
       
   545 
       
   546     $mxrand = _openid_math_pow(256, $nbytes);
       
   547 
       
   548     // If we get a number less than this, then it is in the
       
   549     // duplicated range.
       
   550     $duplicate = _openid_math_mod($mxrand, $stop);
       
   551 
       
   552     if (count($duplicate_cache) > 10) {
       
   553       $duplicate_cache = array();
       
   554     }
       
   555 
       
   556     $duplicate_cache[$rbytes] = array($duplicate, $nbytes);
       
   557   }
       
   558 
       
   559   do {
       
   560     $bytes = "\x00" . drupal_random_bytes($nbytes);
       
   561     $n = _openid_dh_binary_to_long($bytes);
       
   562     // Keep looping if this value is in the low duplicated range.
       
   563   } while (_openid_math_cmp($n, $duplicate) < 0);
       
   564 
       
   565   return _openid_math_mod($n, $stop);
       
   566 }
       
   567 
       
   568 function _openid_get_bytes($num_bytes) {
       
   569   return drupal_random_bytes($num_bytes);
       
   570 }
       
   571 
       
   572 function _openid_response($str = NULL) {
       
   573   $data = array();
       
   574 
       
   575   if (isset($_SERVER['REQUEST_METHOD'])) {
       
   576     $data = _openid_get_params($_SERVER['QUERY_STRING']);
       
   577 
       
   578     if ($_SERVER['REQUEST_METHOD'] == 'POST') {
       
   579       $str = file_get_contents('php://input');
       
   580 
       
   581       $post = array();
       
   582       if ($str !== FALSE) {
       
   583         $post = _openid_get_params($str);
       
   584       }
       
   585 
       
   586       $data = array_merge($data, $post);
       
   587     }
       
   588   }
       
   589 
       
   590   return $data;
       
   591 }
       
   592 
       
   593 function _openid_get_params($str) {
       
   594   $chunks = explode("&", $str);
       
   595 
       
   596   $data = array();
       
   597   foreach ($chunks as $chunk) {
       
   598     $parts = explode("=", $chunk, 2);
       
   599 
       
   600     if (count($parts) == 2) {
       
   601       list($k, $v) = $parts;
       
   602       $data[$k] = urldecode($v);
       
   603     }
       
   604   }
       
   605   return $data;
       
   606 }
       
   607 
       
   608 /**
       
   609  * Extract all the parameters belonging to an extension in a response message.
       
   610  *
       
   611  * OpenID 2.0 defines a simple extension mechanism, based on a namespace prefix.
       
   612  *
       
   613  * Each request or response can define a prefix using:
       
   614  * @code
       
   615  *   openid.ns.[prefix] = [extension_namespace]
       
   616  *   openid.[prefix].[key1] = [value1]
       
   617  *   openid.[prefix].[key2] = [value2]
       
   618  *   ...
       
   619  * @endcode
       
   620  *
       
   621  * This function extracts all the keys belonging to an extension namespace in a
       
   622  * response, optionally using a fallback prefix if none is provided in the response.
       
   623  *
       
   624  * Note that you cannot assume that a given extension namespace will use the same
       
   625  * prefix on the response and the request: each party may use a different prefix
       
   626  * to refer to the same namespace.
       
   627  *
       
   628  * @param $response
       
   629  *   The response array.
       
   630  * @param $extension_namespace
       
   631  *   The namespace of the extension.
       
   632  * @param $fallback_prefix
       
   633  *   An optional prefix that will be used in case no prefix is found for the
       
   634  *   target extension namespace.
       
   635  * @param $only_signed
       
   636  *   Return only keys that are included in the message signature in openid.sig.
       
   637  *   Unsigned fields may have been modified or added by other parties than the
       
   638  *   OpenID Provider.
       
   639  *
       
   640  * @return
       
   641  *   An associative array containing all the parameters in the response message
       
   642  *   that belong to the extension. The keys are stripped from their namespace
       
   643  *   prefix.
       
   644  *
       
   645  * @see http://openid.net/specs/openid-authentication-2_0.html#extensions
       
   646  */
       
   647 function openid_extract_namespace($response, $extension_namespace, $fallback_prefix = NULL, $only_signed = FALSE) {
       
   648   $signed_keys = explode(',', $response['openid.signed']);
       
   649 
       
   650   // Find the namespace prefix.
       
   651   $prefix = $fallback_prefix;
       
   652   foreach ($response as $key => $value) {
       
   653     if ($value == $extension_namespace && preg_match('/^openid\.ns\.([^.]+)$/', $key, $matches)) {
       
   654       $prefix = $matches[1];
       
   655       if ($only_signed && !in_array('ns.' . $matches[1], $signed_keys)) {
       
   656         // The namespace was defined but was not signed as required. In this
       
   657         // case we do not fall back to $fallback_prefix.
       
   658         $prefix = NULL;
       
   659       }
       
   660       break;
       
   661     }
       
   662   }
       
   663 
       
   664   // Now extract the namespace keys from the response.
       
   665   $output = array();
       
   666   if (!isset($prefix)) {
       
   667     return $output;
       
   668   }
       
   669   foreach ($response as $key => $value) {
       
   670     if (preg_match('/^openid\.' . $prefix . '\.(.+)$/', $key, $matches)) {
       
   671       $local_key = $matches[1];
       
   672       if (!$only_signed || in_array($prefix . '.' . $local_key, $signed_keys)) {
       
   673         $output[$local_key] = $value;
       
   674       }
       
   675     }
       
   676   }
       
   677 
       
   678   return $output;
       
   679 }
       
   680 
       
   681 /**
       
   682  * Extracts values from an OpenID AX Response.
       
   683  *
       
   684  * The values can be returned in two forms:
       
   685  *   - only openid.ax.value.<alias> (for single-valued answers)
       
   686  *   - both openid.ax.count.<alias> and openid.ax.value.<alias>.<count> (for both
       
   687  *     single and multiple-valued answers)
       
   688  *
       
   689  * @param $values
       
   690  *   An array as returned by openid_extract_namespace(..., OPENID_NS_AX).
       
   691  * @param $uris
       
   692  *   An array of identifier URIs.
       
   693  * @return
       
   694  *   An array of values.
       
   695  * @see http://openid.net/specs/openid-attribute-exchange-1_0.html#fetch_response
       
   696  */
       
   697 function openid_extract_ax_values($values, $uris) {
       
   698   $output = array();
       
   699   foreach ($values as $key => $value) {
       
   700     if (in_array($value, $uris) && preg_match('/^type\.([^.]+)$/', $key, $matches)) {
       
   701       $alias = $matches[1];
       
   702       if (isset($values['count.' . $alias])) {
       
   703         for ($i = 1; $i <= $values['count.' . $alias]; $i++) {
       
   704           $output[] = $values['value.' . $alias . '.' . $i];
       
   705         }
       
   706       }
       
   707       elseif (isset($values['value.' . $alias])) {
       
   708         $output[] = $values['value.' . $alias];
       
   709       }
       
   710       break;
       
   711     }
       
   712   }
       
   713   return $output;
       
   714 }
       
   715 
       
   716 /**
       
   717  * Determine the available math library GMP vs. BCMath, favouring GMP for performance.
       
   718  */
       
   719 function _openid_get_math_library() {
       
   720   // Not drupal_static(), because a function is not going to disappear and
       
   721   // change the output of this under any circumstances.
       
   722   static $library;
       
   723 
       
   724   if (empty($library)) {
       
   725     if (function_exists('gmp_add')) {
       
   726       $library =  'gmp';
       
   727     }
       
   728     elseif (function_exists('bcadd')) {
       
   729       $library = 'bcmath';
       
   730     }
       
   731   }
       
   732 
       
   733   return $library;
       
   734 }
       
   735 
       
   736 /**
       
   737  * Calls the add function from the available math library for OpenID.
       
   738  */
       
   739 function _openid_math_add($x, $y) {
       
   740   $library = _openid_get_math_library();
       
   741   switch ($library) {
       
   742     case 'gmp':
       
   743       return gmp_strval(gmp_add($x, $y));
       
   744     case 'bcmath':
       
   745       return bcadd($x, $y);
       
   746   }
       
   747 }
       
   748 
       
   749 /**
       
   750  * Calls the mul function from the available math library for OpenID.
       
   751  */
       
   752 function _openid_math_mul($x, $y) {
       
   753   $library = _openid_get_math_library();
       
   754   switch ($library) {
       
   755     case 'gmp':
       
   756       return gmp_mul($x, $y);
       
   757     case 'bcmath':
       
   758       return bcmul($x, $y);
       
   759   }
       
   760 }
       
   761 
       
   762 /**
       
   763  * Calls the div function from the available math library for OpenID.
       
   764  */
       
   765 function _openid_math_div($x, $y) {
       
   766   $library = _openid_get_math_library();
       
   767   switch ($library) {
       
   768     case 'gmp':
       
   769       return gmp_div($x, $y);
       
   770     case 'bcmath':
       
   771       return bcdiv($x, $y);
       
   772   }
       
   773 }
       
   774 
       
   775 /**
       
   776  * Calls the cmp function from the available math library for OpenID.
       
   777  */
       
   778 function _openid_math_cmp($x, $y) {
       
   779   $library = _openid_get_math_library();
       
   780   switch ($library) {
       
   781     case 'gmp':
       
   782       return gmp_cmp($x, $y);
       
   783     case 'bcmath':
       
   784       return bccomp($x, $y);
       
   785   }
       
   786 }
       
   787 
       
   788 /**
       
   789  * Calls the mod function from the available math library for OpenID.
       
   790  */
       
   791 function _openid_math_mod($x, $y) {
       
   792   $library = _openid_get_math_library();
       
   793   switch ($library) {
       
   794     case 'gmp':
       
   795       return gmp_mod($x, $y);
       
   796     case 'bcmath':
       
   797       return bcmod($x, $y);
       
   798   }
       
   799 }
       
   800 
       
   801 /**
       
   802  * Calls the pow function from the available math library for OpenID.
       
   803  */
       
   804 function _openid_math_pow($x, $y) {
       
   805   $library = _openid_get_math_library();
       
   806   switch ($library) {
       
   807     case 'gmp':
       
   808       return gmp_pow($x, $y);
       
   809     case 'bcmath':
       
   810       return bcpow($x, $y);
       
   811   }
       
   812 }
       
   813 
       
   814 /**
       
   815  * Calls the mul function from the available math library for OpenID.
       
   816  */
       
   817 function _openid_math_powmod($x, $y, $z) {
       
   818   $library = _openid_get_math_library();
       
   819   switch ($library) {
       
   820     case 'gmp':
       
   821       return gmp_powm($x, $y, $z);
       
   822     case 'bcmath':
       
   823       return bcpowmod($x, $y, $z);
       
   824   }
       
   825 }
       
   826 
       
   827 /**
       
   828  * Provides transition for accounts with possibly invalid OpenID identifiers in authmap.
       
   829  *
       
   830  * This function provides a less safe but more unobtrusive procedure for users
       
   831  * who cannot login with their OpenID identifiers. OpenID identifiers in the
       
   832  * authmap could be incomplete due to invalid OpenID implementation in previous
       
   833  * versions of Drupal (e.g. fragment part of the identifier could be missing).
       
   834  * For more information see http://drupal.org/node/1120290.
       
   835  *
       
   836  * @param string $identity
       
   837  *   The user's claimed OpenID identifier.
       
   838  *
       
   839  * @return
       
   840  *   A fully-loaded user object if the user is found or FALSE if not found.
       
   841  */
       
   842 function _openid_invalid_openid_transition($identity, $response) {
       
   843   $account = FALSE;
       
   844   $fallback_account = NULL;
       
   845   $fallback_identity = $identity;
       
   846 
       
   847   // Try to strip the fragment if it is present.
       
   848   if (strpos($fallback_identity, '#') !== FALSE) {
       
   849     $fallback_identity = preg_replace('/#.*/', '', $fallback_identity);
       
   850     $fallback_account = user_external_load($fallback_identity);
       
   851   }
       
   852 
       
   853   // Try to replace HTTPS with HTTP. OpenID providers often redirect
       
   854   // from http to https, but Drupal didn't follow the redirect.
       
   855   if (!$fallback_account && strpos($fallback_identity, 'https://') !== FALSE) {
       
   856     $fallback_identity = str_replace('https://', 'http://', $fallback_identity);
       
   857     $fallback_account = user_external_load($fallback_identity);
       
   858   }
       
   859 
       
   860   // Try to use original identifier.
       
   861   if (!$fallback_account && isset($_SESSION['openid']['user_login_values']['openid_identifier'])) {
       
   862     $fallback_identity = openid_normalize($_SESSION['openid']['user_login_values']['openid_identifier']);
       
   863     $fallback_account = user_external_load($fallback_identity);
       
   864   }
       
   865 
       
   866   if ($fallback_account) {
       
   867     // Try to extract e-mail address from Simple Registration (SREG) or
       
   868     // Attribute Exchanges (AX) keys.
       
   869     $email = '';
       
   870     $sreg_values = openid_extract_namespace($response, OPENID_NS_SREG, 'sreg', TRUE);
       
   871     $ax_values = openid_extract_namespace($response, OPENID_NS_AX, 'ax', TRUE);
       
   872     if (!empty($sreg_values['email']) && valid_email_address($sreg_values['email'])) {
       
   873       $email = $sreg_values['email'];
       
   874     }
       
   875     elseif ($ax_mail_values = openid_extract_ax_values($ax_values, array('http://axschema.org/contact/email', 'http://schema.openid.net/contact/email'))) {
       
   876       $email = current($ax_mail_values);
       
   877     }
       
   878 
       
   879     // If this e-mail address is the same as the e-mail address found in user
       
   880     // account, login the user and update the claimed identifier.
       
   881     if ($email && ($email == $fallback_account->mail || $email == $fallback_account->init)) {
       
   882       $query = db_insert('authmap')
       
   883         ->fields(array(
       
   884           'authname' => $identity,
       
   885           'uid' => $fallback_account->uid,
       
   886           'module' => 'openid',
       
   887         ))
       
   888         ->execute();
       
   889       drupal_set_message(t('New OpenID identifier %identity was added as a replacement for invalid identifier %invalid_identity. To finish the invalid OpenID transition process, please go to your <a href="@openid_url">OpenID identities page</a> and remove the old identifier %invalid_identity.', array('%invalid_identity' => $fallback_identity, '%identity' => $identity, '@openid_url' => 'user/' . $fallback_account->uid . '/openid')));
       
   890       // Set the account to the found one.
       
   891       $account = $fallback_account;
       
   892     }
       
   893     else {
       
   894       drupal_set_message(t('There is already an existing account associated with the OpenID identifier that you have provided. However, due to a bug in the previous version of the authentication system, we can\'t be sure that this account belongs to you. If you are new on this site, please continue registering the new user account. If you already have a registered account on this site associated with the provided OpenID identifier, please try to <a href="@url_password">reset the password</a> or contact the site administrator.', array('@url_password' => 'user/password')), 'warning');
       
   895     }
       
   896   }
       
   897 
       
   898   return $account;
       
   899 }