Index: modules/openid/openid.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/openid/openid.inc,v
retrieving revision 1.26
diff -u -9 -p -r1.26 openid.inc
--- modules/openid/openid.inc	6 Mar 2010 06:39:00 -0000	1.26
+++ modules/openid/openid.inc	21 Mar 2010 20:47:47 -0000
@@ -55,18 +55,28 @@ define('OPENID_NS_1_0', 'http://openid.n
  */
 define('OPENID_NS_SREG', 'http://openid.net/extensions/sreg/1.1');
 
 /**
  * OpenID Attribute Exchange extension.
  */
 define('OPENID_NS_AX', 'http://openid.net/srv/ax/1.0');
 
 /**
+ * Extensible Resource Descriptor documents.
+ */
+define('OPENID_NS_XRD', 'xri://$xrd*($v*2.0)');
+
+/**
+ * OpenID IDP for Google hosted domains.
+ */
+define('OPENID_NS_GOOGLE', 'http://namespace.google.com/openid/xmlns');
+
+/**
  * Performs an HTTP 302 redirect (for the 1.x protocol).
  */
 function openid_redirect_http($url, $message) {
   $query = array();
   foreach ($message as $key => $val) {
     $query[] = $key . '=' . urlencode($val);
   }
 
   $sep = (strpos($url, '?') === FALSE) ? '?' : '&';
@@ -103,18 +113,61 @@ function openid_redirect_form($form, &$f
     '#prefix' => '<noscript>',
     '#suffix' => '</noscript>',
     '#value' => t('Send'),
   );
 
   return $form;
 }
 
 /**
+ * Parse an XRDS document.
+ *
+ * @param $raw_xml
+ *   A string containing the XRDS document.
+ * @return
+ *   An array of service entries.
+ */
+function _openid_xrds_parse($raw_xml) {
+  $services = array();
+  try {
+    $xml = @new SimpleXMLElement($raw_xml);
+    foreach ($xml->children(OPENID_NS_XRD)->XRD as $xrd) {
+      foreach ($xrd->children(OPENID_NS_XRD)->Service as $service_element) {
+        $service = array(
+          'priority' => $service_element->attributes()->priority ? (int)$service_element->attributes()->priority : PHP_INT_MAX,
+          'types' => array(),
+          'uri' => (string)$service_element->children(OPENID_NS_XRD)->URI,
+          'service' => $service_element,
+          'xrd' => $xrd,
+        );
+        foreach ($service_element->Type as $type) {
+          $service['types'][] = (string)$type;
+        }
+        if ($service_element->children(OPENID_NS_XRD)->Delegate) {
+          $service['identity'] = (string)$service_element->children(OPENID_NS_XRD)->Delegate;
+        }
+        if ($service_element->children(OPENID_NS_XRD)->LocalID) {
+          $service['identity'] = (string)$service_element->children(OPENID_NS_XRD)->LocalID;
+        }
+        else {
+          $service['identity'] = FALSE;
+        }
+        $services[] = $service;
+      }
+    }
+  }
+  catch (Exception $e) {
+    // Invalid XML.
+  }
+  return $services;
+}
+
+/**
  * Select a service element.
  *
  * The procedure is described in OpenID Authentication 2.0, section 7.3.2.
  *
  * A new entry is added to the returned array with the key 'version' and the
  * value 1 or 2 specifying the protocol version used by the service.
  *
  * @param $services
  *   An array of service arrays as returned by openid_discovery().
@@ -149,18 +202,24 @@ function _openid_select_service(array $s
       if (!empty($service['uri']) && in_array('http://specs.openid.net/auth/2.0/signon', $service['types'])) {
         $service['version'] = 2;
         if (!$selected_service || $service['priority'] < $selected_service['priority']) {
           $selected_service = $service;
         }
       }
     }
   }
 
+  if ($selected_service) {
+    // Unset SimpleXMLElement instances that cannot be saved in $_SESSION.
+    unset($selected_service['xrd']);
+    unset($selected_service['service']);
+  }
+
   return $selected_service;
 }
 
 /**
  * Determine if the given identifier is an XRI ID.
  */
 function _openid_is_xri($identifier) {
   // Strip the xri:// scheme from the identifier if present.
   if (stripos($identifier, 'xri://') !== FALSE) {
Index: modules/openid/openid.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/openid/openid.module,v
retrieving revision 1.75
diff -u -9 -p -r1.75 openid.module
--- modules/openid/openid.module	9 Mar 2010 12:07:37 -0000	1.75
+++ modules/openid/openid.module	21 Mar 2010 20:47:48 -0000
@@ -202,28 +202,24 @@ function openid_begin($claimed_id, $retu
   if (function_exists('bcadd')) {
     $assoc_handle = openid_association($service['uri']);
   }
 
   if (in_array('http://specs.openid.net/auth/2.0/server', $service['types'])) {
     // User entered an OP Identifier.
     $claimed_id = $identity = 'http://specs.openid.net/auth/2.0/identifier_select';
   }
   else {
-    // Look for OP-Local Identifier.
-    if (!empty($service['localid'])) {
-      $identity = $service['localid'];
-    }
-    elseif (!empty($service['delegate'])) {
-      $identity = $service['delegate'];
-    }
-    else {
-      $identity = $claimed_id;
+    // Use Claimed ID and/or OP-Local Identifier from service description, if
+    // available.
+    if (!empty($service['claimed_id'])) {
+      $claimed_id = $service['claimed_id'];
     }
+    $identity = !empty($service['identity']) ? $service['identity'] : $claimed_id;
   }
   $request = openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $service);
 
   if ($service['version'] == 2) {
     openid_redirect($service['uri'], $request);
   }
   else {
     openid_redirect_http($service['uri'], $request);
   }
@@ -252,24 +248,33 @@ function openid_complete($response = arr
     $claimed_id = $_SESSION['openid']['claimed_id'];
     unset($_SESSION['openid']['service']);
     unset($_SESSION['openid']['claimed_id']);
     if (isset($response['openid.mode'])) {
       if ($response['openid.mode'] == 'cancel') {
         $response['status'] = 'cancel';
       }
       else {
         if (openid_verify_assertion($service['uri'], $response)) {
+          // OpenID Authentication, section 7.3.2.3 and Appendix A.5:
+          // The CanonicalID specified in the XRDS document must be used as the
+          // account key. We rely on the XRI proxy resolver to verify that the
+          // provider is authorized to respond on behalf of the specified
+          // identifer (required per Extensible Resource Identifier (XRI)
+          // (XRI) Resolution Version 2.0, section 14.3):
+          if (!empty($service['claimed_id'])) {
+            $response['openid.claimed_id'] = $service['claimed_id'];
+          }
           // OpenID Authentication, section 11.2:
           // If the returned Claimed Identifier is different from the one sent
           // to the OpenID Provider, we need to do discovery on the returned
-          // identififer to make sure that the provider is authorized to respond
+          // identifier to make sure that the provider is authorized to respond
           // on behalf of this.
-          if ($service['version'] == 2 && $response['openid.claimed_id'] != openid_normalize($claimed_id)) {
+          elseif ($service['version'] == 2 && $response['openid.claimed_id'] != openid_normalize($claimed_id)) {
             $services = openid_discovery($response['openid.claimed_id']);
             $uris = array();
             foreach ($services as $discovered_service) {
               if (in_array('http://specs.openid.net/auth/2.0/server', $discovered_service['types']) || in_array('http://specs.openid.net/auth/2.0/signon', $discovered_service['types'])) {
                 $uris[] = $discovered_service['uri'];
               }
             }
             if (!in_array($service['uri'], $uris)) {
               return $response;
@@ -332,20 +337,26 @@ function openid_openid_discovery_method_
 
 /**
  * OpenID discovery method: perform an XRI discovery.
  *
  * @see http://openid.net/specs/openid-authentication-2_0.html#discovery
  * @see hook_openid_discovery_method_info()
  */
 function _openid_xri_discovery($claimed_id) {
   if (_openid_is_xri($claimed_id)) {
-    $xrds_url = 'http://xri.net/' . $claimed_id;
-    _openid_xrds_discovery($xrds_url);
+    // Resolve XRI using a proxy resolver (Extensible Resource Identifier (XRI)
+    // Resolution Version 2.0, section 11.2).
+    $xrds_url = variable_get('xri_proxy_resolver', 'http://xri.net/') . rawurlencode($claimed_id) . '?_xrd_r=application/xrds+xml';
+    $services = _openid_xrds_discovery($xrds_url);
+    foreach ($services as &$service) {
+      $service['claimed_id'] = openid_normalize((string)$service['xrd']->children(OPENID_NS_XRD)->CanonicalID);
+    }
+    return $services;
   }
 }
 
 /**
  * OpenID discovery method: perform a XRDS discovery.
  *
  * @see http://openid.net/specs/openid-authentication-2_0.html#discovery
  * @see hook_openid_discovery_method_info()
  */
@@ -356,55 +367,55 @@ function _openid_xrds_discovery($claimed
   $scheme = @parse_url($xrds_url, PHP_URL_SCHEME);
   if ($scheme == 'http' || $scheme == 'https') {
     // For regular URLs, try Yadis resolution first, then HTML-based discovery
     $headers = array('Accept' => 'application/xrds+xml');
     $result = drupal_http_request($xrds_url, array('headers' => $headers));
 
     if (!isset($result->error)) {
       if (isset($result->headers['Content-Type']) && preg_match("/application\/xrds\+xml/", $result->headers['Content-Type'])) {
         // Parse XML document to find URL
-        $services = xrds_parse($result->data);
+        $services = _openid_xrds_parse($result->data);
       }
       else {
         $xrds_url = NULL;
         if (isset($result->headers['X-XRDS-Location'])) {
           $xrds_url = $result->headers['X-XRDS-Location'];
         }
         else {
           // Look for meta http-equiv link in HTML head
           $xrds_url = _openid_meta_httpequiv('X-XRDS-Location', $result->data);
         }
         if (!empty($xrds_url)) {
           $headers = array('Accept' => 'application/xrds+xml');
           $xrds_result = drupal_http_request($xrds_url, array('headers' => $headers));
           if (!isset($xrds_result->error)) {
-            $services = xrds_parse($xrds_result->data);
+            $services = _openid_xrds_parse($xrds_result->data);
           }
         }
       }
 
       // Check for HTML delegation
       if (count($services) == 0) {
         // Look for 2.0 links
         $uri = _openid_link_href('openid2.provider', $result->data);
-        $delegate = _openid_link_href('openid2.local_id', $result->data);
+        $identity = _openid_link_href('openid2.local_id', $result->data);
         $type = 'http://specs.openid.net/auth/2.0/signon';
 
         // 1.x links
         if (empty($uri)) {
           $uri = _openid_link_href('openid.server', $result->data);
-          $delegate = _openid_link_href('openid.delegate', $result->data);
+          $identity = _openid_link_href('openid.delegate', $result->data);
           $type = 'http://openid.net/signon/1.1';
         }
         if (!empty($uri)) {
           $services[] = array(
             'uri' => $uri,
-            'delegate' => $delegate,
+            'identity' => $identity,
             'types' => array($type),
           );
         }
       }
     }
   }
   return $services;
 }
 
@@ -426,21 +437,21 @@ function _openid_google_user_discovery($
   $response = drupal_http_request('https://www.google.com/accounts/o8/.well-known/host-meta?hd=' . rawurlencode($url['host']));
   if (isset($response->error) || $response->code != 200) {
     return;
   }
 
   if (preg_match('/Link: <(.*)>/', $response->data, $matches)) {
     $xrds_url = $matches[1];
     $services = _openid_xrds_discovery($xrds_url);
 
-    foreach ($services as $service) {
-      if (in_array('http://www.iana.org/assignments/relation/describedby', $service['types']) && !empty($service['additional']['URITEMPLATE'])) {
-        $template = $service['additional']['URITEMPLATE'];
+    foreach ($services as $i => $service) {
+      if (in_array('http://www.iana.org/assignments/relation/describedby', $service['types']) && $service['service']->children(OPENID_NS_GOOGLE)->URITemplate) {
+        $template = (string)$service['service']->children(OPENID_NS_GOOGLE)->URITemplate;
         $xrds_url = str_replace('{%uri}', rawurlencode($claimed_id), $template);
         return _openid_xrds_discovery($xrds_url);
       }
     }
   }
 }
 
 /**
  * Implementation of hook_openid_normalization_method_info().
Index: modules/openid/openid.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/openid/openid.test,v
retrieving revision 1.15
diff -u -9 -p -r1.15 openid.test
--- modules/openid/openid.test	2 Mar 2010 08:59:54 -0000	1.15
+++ modules/openid/openid.test	21 Mar 2010 20:47:48 -0000
@@ -53,18 +53,21 @@ class OpenIDFunctionalTest extends Drupa
 
     // Identifier is the URL of an HTML page that is sent with an HTTP header
     // that contains the URL of an XRDS document.
     $this->addIdentity(url('openid-test/yadis/x-xrds-location', array('absolute' => TRUE)), 2);
 
     // Identifier is the URL of an HTML page containing a <meta http-equiv=...>
     // element that contains the URL of an XRDS document.
     $this->addIdentity(url('openid-test/yadis/http-equiv', array('absolute' => TRUE)), 2);
 
+    // Identifier is an XRI. Resolve using our own dummy proxy resolver.
+    variable_set('xri_proxy_resolver', url('openid-test/yadis/xrds/xri', array('absolute' => TRUE)) . '/');
+    $this->addIdentity('@example*résumé;%25', 2, 'http://example.com/user');
 
     // HTML-based discovery:
     // If the User-supplied Identifier is a URL of an HTML page, the page may
     // contain a <link rel=...> element containing the URL of the OpenID
     // Provider Endpoint. OpenID 1 and 2 describe slightly different formats.
 
     // OpenID Authentication 1.1, section 3.1:
     $this->addIdentity(url('openid-test/html/openid1', array('absolute' => TRUE)), 1);
 
Index: modules/openid/xrds.inc
===================================================================
RCS file: modules/openid/xrds.inc
diff -N modules/openid/xrds.inc
--- modules/openid/xrds.inc	2 Mar 2010 08:59:54 -0000	1.6
+++ /dev/null	1 Jan 1970 00:00:00 -0000
@@ -1,90 +0,0 @@
-<?php
-// $Id: xrds.inc,v 1.6 2010/03/02 08:59:54 dries Exp $
-
-// Global variables to track parsing state
-$xrds_open_elements = array();
-$xrds_services = array();
-$xrds_current_service = array();
-
-/**
- * Main entry point for parsing XRDS documents
- */
-function xrds_parse($xml) {
-  global $xrds_services;
-
-  $parser = xml_parser_create_ns();
-  xml_set_element_handler($parser, '_xrds_element_start', '_xrds_element_end');
-  xml_set_character_data_handler($parser, '_xrds_cdata');
-
-  xml_parse($parser, $xml);
-  xml_parser_free($parser);
-
-  return $xrds_services;
-}
-
-/**
- * Parser callback functions
- */
-function _xrds_element_start(&$parser, $name, $attributes) {
-  global $xrds_open_elements, $xrds_current_service;
-
-  $xrds_open_elements[] = _xrds_strip_namespace($name);
-
-  $path = strtoupper(implode('/', $xrds_open_elements));
-  if ($path == 'XRDS/XRD/SERVICE') {
-    foreach ($attributes as $attribute_name => $value) {
-      if (_xrds_strip_namespace($attribute_name) == 'PRIORITY') {
-        $xrds_current_service['priority'] = intval($value);
-      }
-    }
-  }
-}
-
-function _xrds_element_end(&$parser, $name) {
-  global $xrds_open_elements, $xrds_services, $xrds_current_service;
-
-  $name = _xrds_strip_namespace($name);
-  if ($name == 'SERVICE') {
-    if (!isset($xrds_current_service['priority'])) {
-      // If the priority attribute is absent, the default is infinity.
-      $xrds_current_service['priority'] = PHP_INT_MAX;
-    }
-    $xrds_services[] = $xrds_current_service;
-    $xrds_current_service = array();
-  }
-  array_pop($xrds_open_elements);
-}
-
-function _xrds_cdata(&$parser, $data) {
-  global $xrds_open_elements, $xrds_services, $xrds_current_service;
-  $path = strtoupper(implode('/', $xrds_open_elements));
-  switch ($path) {
-    case 'XRDS/XRD/SERVICE/TYPE':
-      $xrds_current_service['types'][] = $data;
-      break;
-    case 'XRDS/XRD/SERVICE/URI':
-      $xrds_current_service['uri'] = $data;
-      break;
-    case 'XRDS/XRD/SERVICE/DELEGATE':
-      $xrds_current_service['delegate'] = $data;
-      break;
-    case 'XRDS/XRD/SERVICE/LOCALID':
-      $xrds_current_service['localid'] = $data;
-      break;
-    default:
-      if (preg_match('@^XRDS/XRD/SERVICE/(.*)$@', $path, $matches)) {
-        $xrds_current_service['additional'][$matches[1]] = $data;
-      }
-      break;
-  }
-}
-
-function _xrds_strip_namespace($name) {
-  // Strip namespacing.
-  $pos = strrpos($name, ':');
-  if ($pos !== FALSE) {
-    $name = substr($name, $pos + 1, strlen($name));
-  }
-
-  return $name;
-}
Index: modules/openid/tests/openid_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/openid/tests/openid_test.module,v
retrieving revision 1.10
diff -u -9 -p -r1.10 openid_test.module
--- modules/openid/tests/openid_test.module	2 Mar 2010 08:55:42 -0000	1.10
+++ modules/openid/tests/openid_test.module	21 Mar 2010 20:47:48 -0000
@@ -63,22 +63,41 @@ function openid_test_menu() {
   );
   return $items;
 }
 
 /**
  * Menu callback; XRDS document that references the OP Endpoint URL.
  */
 function openid_test_yadis_xrds() {
   if ($_SERVER['HTTP_ACCEPT'] == 'application/xrds+xml') {
+    // Only respond to XRI requests for one specific XRI. The is used to verify
+    // that the XRI has been properly encoded. The "+" sign in the _xrd_r query
+    // parameter is decoded to a space by PHP.
+    if (arg(3) == 'xri') {
+      if (variable_get('clean_url', 0)) {
+        if (arg(4) != '@example*résumé;%25' || $_GET['_xrd_r'] != 'application/xrds xml') {
+          drupal_not_found();
+        }
+      }
+      else {
+        // Drupal cannot properly emulate an XRI proxy resolver using unclean
+        // URLs, so the arguments gets messed up.
+        if (arg(4) . '/' . arg(5) != '@example*résumé;%25?_xrd_r=application/xrds xml') {
+          drupal_not_found();
+        }
+      }
+    }
     drupal_add_http_header('Content-Type', 'application/xrds+xml');
     print '<?xml version="1.0" encoding="UTF-8"?>
       <xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">
         <XRD>
+          <ProviderID>xri://@</ProviderID>
+          <CanonicalID>http://example.com/user</CanonicalID>
           <Service>
             <Type>http://example.com/this-is-ignored</Type>
           </Service>
           <Service priority="10">
             <Type>http://specs.openid.net/auth/2.0/signon</Type>
             <Type>http://openid.net/srv/ax/1.0</Type>
             <URI>' . url('openid-test/endpoint', array('absolute' => TRUE)) . '</URI>
           </Service>
           <Service priority="15">
@@ -96,19 +115,19 @@ function openid_test_yadis_xrds() {
             <Type>http://specs.openid.net/auth/2.0/server</Type>
             <URI>http://example.com/this-has-too-low-priority</URI>
           </Service>
           <Service priority="20">
             <Type>http://specs.openid.net/auth/2.0/server</Type>
             <URI>' . url('openid-test/endpoint', array('absolute' => TRUE)) . '</URI>
           </Service>';
     }
     print '
-        <XRD>
+        </XRD>
       </xrds:XRDS>';
   }
   else {
     return t('This is a regular HTML page. If the client sends an Accept: application/xrds+xml header when requesting this URL, an XRDS document is returned.');
   }
 }
 
 /**
  * Menu callback; regular HTML page with an X-XRDS-Location HTTP header.
