Index: modules/openid/openid.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/openid/openid.inc,v
retrieving revision 1.22
diff -u -9 -p -r1.22 openid.inc
--- modules/openid/openid.inc	24 Nov 2009 05:20:48 -0000	1.22
+++ modules/openid/openid.inc	17 Jan 2010 12:59:03 -0000
@@ -93,18 +93,68 @@ function openid_redirect_form($form, &$f
     '#prefix' => '<noscript>',
     '#suffix' => '</noscript>',
     '#value' => t('Send'),
   );
 
   return $form;
 }
 
 /**
+ * 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().
+ * @return
+ *   The selected service array, or NULL if no valid services were found.
+ */
+function _openid_select_service(array $services) {
+  // Extensible Resource Identifier (XRI) Resolution Version 2.0, section 4.3.3:
+  // Find the service with the highest priority (lowest integer value). If there
+  // is a tie, select a random one, not just the first in the XML document.
+  $selected_service = NULL;
+  shuffle($services);
+
+  // Search for an OP Identifier Element.
+  foreach ($services as $service) {
+    if (!empty($service['uri'])) {
+      if (in_array('http://specs.openid.net/auth/2.0/server', $service['types'])) {
+        $service['version'] = 2;
+      }
+      elseif (in_array(OPENID_NS_1_0, $service['types']) || in_array(OPENID_NS_1_1, $service['types'])) {
+        $service['version'] = 1;
+      }
+      if (isset($service['version']) && (!$selected_service || $service['priority'] < $selected_service['priority'])) {
+        $selected_service = $service;
+      }
+    }
+  }
+
+  if (!$selected_service) {
+    // Search for Claimed Identifier Element.
+    foreach ($services as $service) {
+      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;
+        }
+      }
+    }
+  }
+
+  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) {
     $identifier = substr($identifier, 6);
   }
 
 
@@ -112,19 +162,21 @@ function _openid_is_xri($identifier) {
   $firstchar = substr($identifier, 0, 1);
   if (strpos("=@+$!(", $firstchar) !== FALSE) {
     return TRUE;
   }
 
   return FALSE;
 }
 
 /**
- * Normalize the given identifier as per spec.
+ * Normalize the given identifier.
+ *
+ * The procedure is described in OpenID Authentication 2.0, section 7.2.
  */
 function _openid_normalize($identifier) {
   if (_openid_is_xri($identifier)) {
     return _openid_normalize_xri($identifier);
   }
   else {
     return _openid_normalize_url($identifier);
   }
 }
Index: modules/openid/openid.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/openid/openid.module,v
retrieving revision 1.70
diff -u -9 -p -r1.70 openid.module
--- modules/openid/openid.module	11 Jan 2010 16:25:16 -0000	1.70
+++ modules/openid/openid.module	17 Jan 2010 12:59:04 -0000
@@ -176,62 +176,62 @@ function openid_login_validate($form, &$
  * @param $claimed_id The OpenID to authenticate
  * @param $return_to The endpoint to return to from the OpenID Provider
  */
 function openid_begin($claimed_id, $return_to = '', $form_values = array()) {
   module_load_include('inc', 'openid');
 
   $claimed_id = _openid_normalize($claimed_id);
 
   $services = openid_discovery($claimed_id);
-  if (count($services) == 0) {
+  $service = _openid_select_service($services);
+
+  if (!$service) {
     form_set_error('openid_identifier', t('Sorry, that is not a valid OpenID. Ensure you have spelled your ID correctly.'));
     return;
   }
 
   // Store discovered information in the users' session so we don't have to rediscover.
-  $_SESSION['openid']['service'] = $services[0];
+  $_SESSION['openid']['service'] = $service;
   // Store the claimed id
   $_SESSION['openid']['claimed_id'] = $claimed_id;
   // Store the login form values so we can pass them to
   // user_exteral_login later.
   $_SESSION['openid']['user_login_values'] = $form_values;
 
-  $op_endpoint = $services[0]['uri'];
   // If bcmath is present, then create an association
   $assoc_handle = '';
   if (function_exists('bcadd')) {
-    $assoc_handle = openid_association($op_endpoint);
+    $assoc_handle = openid_association($service['uri']);
   }
 
-  // Now that there is an association created, move on
-  // to request authentication from the IdP
-  // First check for LocalID. If not found, check for Delegate. Fall
-  // back to $claimed_id if neither is found.
-  if (!empty($services[0]['localid'])) {
-    $identity = $services[0]['localid'];
-  }
-  elseif (!empty($services[0]['delegate'])) {
-    $identity = $services[0]['delegate'];
+  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 {
-    $identity = $claimed_id;
-  }
-
-  if (isset($services[0]['types']) && is_array($services[0]['types']) && in_array(OPENID_NS_2_0 . '/server', $services[0]['types'])) {
-    $claimed_id = $identity = 'http://specs.openid.net/auth/2.0/identifier_select';
+    // Look for OP-Local Identifier.
+    if (!empty($service['localid'])) {
+      $identity = $service['localid'];
+    }
+    elseif (!empty($service['delegate'])) {
+      $identity = $service['delegate'];
+    }
+    else {
+      $identity = $claimed_id;
+    }
   }
-  $authn_request = openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $services[0]['version']);
+  $request = openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $service['version']);
 
-  if ($services[0]['version'] == 2) {
-    openid_redirect($op_endpoint, $authn_request);
+  if ($service['version'] == 2) {
+    openid_redirect($service['uri'], $request);
   }
   else {
-    openid_redirect_http($op_endpoint, $authn_request);
+    openid_redirect_http($service['uri'], $request);
   }
 }
 
 /**
  * Completes OpenID authentication by validating returned data from the OpenID
  * Provider.
  *
  * @param $response Array of returned values from the OpenID Provider.
  *
@@ -252,23 +252,32 @@ 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)) {
-          // If the returned claimed_id is different from the session claimed_id,
-          // then we need to do discovery and make sure the op_endpoint matches.
-          if ($service['version'] == 2 && $response['openid.claimed_id'] != $claimed_id) {
-            $disco = openid_discovery($response['openid.claimed_id']);
-            if ($disco[0]['uri'] != $service['uri']) {
+          // 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
+          // on behalf of this.
+          if ($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;
             }
           }
           else {
             $response['openid.claimed_id'] = $claimed_id;
           }
           $response['status'] = 'success';
         }
       }
@@ -323,28 +332,32 @@ function openid_discovery($claimed_id) {
           }
         }
       }
 
       // 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);
-        $version = 2;
+        $type = 'http://specs.openid.net/auth/2.0/signon';
 
-        // 1.0 links
+        // 1.x links
         if (empty($uri)) {
           $uri = _openid_link_href('openid.server', $result->data);
           $delegate = _openid_link_href('openid.delegate', $result->data);
-          $version = 1;
+          $type = 'http://openid.net/signon/1.1';
         }
         if (!empty($uri)) {
-          $services[] = array('uri' => $uri, 'delegate' => $delegate, 'version' => $version);
+          $services[] = array(
+            'uri' => $uri,
+            'delegate' => $delegate,
+            'types' => array($type),
+          );
         }
       }
     }
   }
   return $services;
 }
 
 /**
  * Attempt to create a shared secret with the OpenID Provider.
Index: modules/openid/openid.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/openid/openid.test,v
retrieving revision 1.9
diff -u -9 -p -r1.9 openid.test
--- modules/openid/openid.test	9 Jan 2010 23:03:21 -0000	1.9
+++ modules/openid/openid.test	17 Jan 2010 12:59:04 -0000
@@ -39,18 +39,24 @@ class OpenIDFunctionalTest extends Drupa
 
     // Yadis discovery (see Yadis Specification 1.0, section 6.2.5):
     // If the User-supplied Identifier is a URL, it may be a direct or indirect
     // reference to an XRDS document (a Yadis Resource Descriptor) that contains
     // the URL of the OpenID Provider Endpoint.
 
     // Identifier is the URL of an XRDS document.
     $this->addIdentity(url('openid-test/yadis/xrds', array('absolute' => TRUE)), 2);
 
+    // Identifier is the URL of an XRDS document containing an OP Identifier
+    // Element. The Relying Party sends the special value
+    // "http://specs.openid.net/auth/2.0/identifier_select" as Claimed
+    // Identifier. The OpenID Provider responds with the actual identifier.
+    $this->addIdentity(url('openid-test/yadis/xrds/server', array('absolute' => TRUE)), 2, url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE)));
+
     // 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);
 
 
@@ -120,32 +126,42 @@ class OpenIDFunctionalTest extends Drupa
     $this->clickLink(t('Delete'));
     $this->drupalPost(NULL, array(), t('Confirm'));
 
     $this->assertText(t('OpenID deleted.'), t('Identity deleted'));
     $this->assertNoText($identity, t('Identity no longer appears in list.'));
   }
 
   /**
    * Add OpenID identity to user's profile.
+   *
+   * @param $identity
+   *   The User-supplied Identifier.
+   * @param $version
+   *   The protocol version used by the service.
+   * @param $claimed_id
+   *   The expected Claimed Identifier returned by the OpenID Provider.
    */
-  function addIdentity($identity, $version = 2) {
+  function addIdentity($identity, $version = 2, $claimed_id = NULL) {
     $this->drupalGet('user/' . $this->web_user->uid . '/openid');
     $edit = array('openid_identifier' => $identity);
     $this->drupalPost(NULL, $edit, t('Add an OpenID'));
 
     // OpenID 1 used a HTTP redirect, OpenID 2 uses a HTML form that is submitted automatically using JavaScript.
     if ($version == 2) {
       // Manually submit form because SimpleTest is not able to execute JavaScript.
       $this->assertRaw('<script type="text/javascript">document.getElementById("openid-redirect-form").submit();</script>', t('JavaScript form submission found.'));
       $this->drupalPost(NULL, array(), t('Send'));
     }
 
-    $this->assertRaw(t('Successfully added %identity', array('%identity' => $identity)), t('Identity %identity was added.', array('%identity' => $identity)));
+    if (!$claimed_id) {
+      $claimed_id = $identity;
+    }
+    $this->assertRaw(t('Successfully added %identity', array('%identity' => $claimed_id)), t('Identity %identity was added.', array('%identity' => $identity)));
   }
 
   /**
    * Test OpenID auto-registration with e-mail verification disabled.
    */
   function testRegisterUserWithoutEmailVerification() {
     variable_set('user_email_verification', FALSE);
 
     // Load the front page to get the user login block.
Index: modules/openid/xrds.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/openid/xrds.inc,v
retrieving revision 1.3
diff -u -9 -p -r1.3 xrds.inc
--- modules/openid/xrds.inc	14 Apr 2008 17:48:38 -0000	1.3
+++ modules/openid/xrds.inc	17 Jan 2010 12:59:04 -0000
@@ -19,40 +19,43 @@ function xrds_parse($xml) {
   xml_parse($parser, $xml);
   xml_parser_free($parser);
 
   return $xrds_services;
 }
 
 /**
  * Parser callback functions
  */
-function _xrds_element_start(&$parser, $name, $attribs) {
-  global $xrds_open_elements;
+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 (in_array(OPENID_NS_2_0 . '/signon', $xrds_current_service['types']) ||
-        in_array(OPENID_NS_2_0 . '/server', $xrds_current_service['types'])) {
-      $xrds_current_service['version'] = 2;
-    }
-    elseif (in_array(OPENID_NS_1_1, $xrds_current_service['types']) ||
-            in_array(OPENID_NS_1_0, $xrds_current_service['types'])) {
-      $xrds_current_service['version'] = 1;
-    }
-    if (!empty($xrds_current_service['version'])) {
-      $xrds_services[] = $xrds_current_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) {
@@ -73,10 +76,10 @@ function _xrds_cdata(&$parser, $data) {
 
 function _xrds_strip_namespace($name) {
   // Strip namespacing.
   $pos = strrpos($name, ':');
   if ($pos !== FALSE) {
     $name = substr($name, $pos + 1, strlen($name));
   }
 
   return $name;
-}
\ No newline at end of file
+}
Index: modules/openid/tests/openid_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/openid/tests/openid_test.module,v
retrieving revision 1.8
diff -u -9 -p -r1.8 openid_test.module
--- modules/openid/tests/openid_test.module	4 Dec 2009 16:49:47 -0000	1.8
+++ modules/openid/tests/openid_test.module	17 Jan 2010 12:59:04 -0000
@@ -68,21 +68,45 @@ function openid_test_menu() {
  * Menu callback; XRDS document that references the OP Endpoint URL.
  */
 function openid_test_yadis_xrds() {
   if ($_SERVER['HTTP_ACCEPT'] == 'application/xrds+xml') {
     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>
           <Service>
+            <Type>http://example.com/this-is-ignored</Type>
+          </Service>
+          <Service priority="10">
             <Type>http://specs.openid.net/auth/2.0/signon</Type>
             <URI>' . url('openid-test/endpoint', array('absolute' => TRUE)) . '</URI>
           </Service>
+          <Service priority="15">
+            <Type>http://specs.openid.net/auth/2.0/signon</Type>
+            <URI>http://example.com/this-has-too-low-priority</URI>
+          </Service>
+          <Service>
+            <Type>http://specs.openid.net/auth/2.0/signon</Type>
+            <URI>http://example.com/this-has-too-low-priority</URI>
+          </Service>
+          ';
+    if (arg(3) == 'server') {
+      print '
+          <Service>
+            <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>
       </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.');
   }
 }
 
 /**
@@ -196,34 +220,44 @@ function _openid_test_endpoint_associate
  * OpenID endpoint; handle "authenticate" requests.
  *
  * All requests result in a successful response. The request is a GET or POST
  * made by the user's browser based on an HTML form or HTTP redirect generated
  * by the Relying Party. The user is redirected back to the Relying Party using
  * a URL containing a signed message in the query string confirming the user's
  * identity.
  */
 function _openid_test_endpoint_authenticate() {
-  global $base_url;
-
   module_load_include('inc', 'openid');
 
   // Generate unique identifier for this authentication.
   $nonce = _openid_nonce();
 
+  if (!isset($_REQUEST['openid_claimed_id'])) {
+    // openid.claimed_id is not used in OpenID 1.x.
+    $claimed_id = '';
+  }
+  elseif ($_REQUEST['openid_claimed_id'] == 'http://specs.openid.net/auth/2.0/identifier_select') {
+    // The Relying Party did not specify a Claimed Identifier, so the OpenID
+    // Provider decides on one.
+    $claimed_id = url('openid-test/yadis/xrds/dummy-user', array('absolute' => TRUE));
+  }
+  else {
+    $claimed_id = $_REQUEST['openid_claimed_id'];
+  }
+
   // Generate response containing the user's identity. The openid.sreg.xxx
   // entries contain profile data stored by the OpenID Provider (see OpenID
   // Simple Registration Extension 1.0).
   $response = variable_get('openid_test_response', array()) + array(
     'openid.ns' => OPENID_NS_2_0,
     'openid.mode' => 'id_res',
-    'openid.op_endpoint' => $base_url . url('openid/provider'),
-    // openid.claimed_id is not sent by OpenID 1 clients.
-    'openid.claimed_id' => isset($_REQUEST['openid_claimed_id']) ? $_REQUEST['openid_claimed_id'] : '',
+    'openid.op_endpoint' => url('openid-test/endpoint', array('absolute' => TRUE)),
+    'openid.claimed_id' => $claimed_id,
     'openid.identity' => $_REQUEST['openid_identity'],
     'openid.return_to' => $_REQUEST['openid_return_to'],
     'openid.response_nonce' => $nonce,
     'openid.assoc_handle' => 'openid-test',
     'openid.signed' => 'op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle',
   );
 
   // Sign the message using the MAC key that was exchanged during association.
   $association = new stdClass;
