#727650: make discovery methods pluggeable and implement Google Discovery Protocol.

From: Damien Tournoud <damien@tournoud.net>


---
 openid/openid.api.php |   22 +++++++++++
 openid/openid.inc     |    6 +++
 openid/openid.module  |   98 ++++++++++++++++++++++++++++++++++++++++++++++++-
 openid/xrds.inc       |    5 +++
 4 files changed, 129 insertions(+), 2 deletions(-)

diff --git modules/openid/openid.api.php modules/openid/openid.api.php
index ef405aa..e67ff81 100644
--- modules/openid/openid.api.php
+++ modules/openid/openid.api.php
@@ -47,5 +47,27 @@ function hook_openid_response($response, $account) {
 }
 
 /**
+ * Allow modules to declare OpenID discovery methods.
+ *
+ * @return
+ *   An array with a set of function callbacks, that will be called in turn
+ *   when discovering an OpenID identifier.
+ */
+function hook_openid_discovery_method_info() {
+  return array(
+    '_my_discovery_method',
+  );
+}
+
+/**
+ * Allow modules to alter discovery methods.
+ */
+function hook_openid_discovery_method_info_alter(&$methods) {
+  // Remove Google discovery scheme.
+  unset($methods['google_idp']);
+  unset($methods['google_user']);
+}
+
+/**
  * @} End of "addtogroup hooks".
  */
diff --git modules/openid/openid.inc modules/openid/openid.inc
index 0996970..5ed8704 100644
--- modules/openid/openid.inc
+++ modules/openid/openid.inc
@@ -190,6 +190,12 @@ function _openid_normalize_xri($xri) {
 }
 
 function _openid_normalize_url($url) {
+  $scheme = @parse_url($url, PHP_URL_SCHEME);
+  if ($scheme != 'http' && $scheme != 'https' && valid_email_address($url)) {
+    // The passed input looks like an email address, give up.
+    return $url;
+  }
+
   $normalized_url = $url;
 
   if (stristr($url, '://') === FALSE) {
diff --git modules/openid/openid.module modules/openid/openid.module
index d6f51ec..3fad202 100644
--- modules/openid/openid.module
+++ modules/openid/openid.module
@@ -230,6 +230,20 @@ function openid_begin($claimed_id, $return_to = '', $form_values = array()) {
 }
 
 /**
+ * Implementation of hook_openid_discovery_method_info().
+ *
+ * Define standard discovery methods.
+ */
+function openid_openid_discovery_method_info() {
+  return array(
+    'google_idp' => '_openid_google_idp_discovery',
+    'xri' => '_openid_xri_discovery',
+    'xrds' => '_openid_xrds_discovery',
+    'google_user' => '_openid_google_user_discovery',
+  );
+}
+
+/**
  * Completes OpenID authentication by validating returned data from the OpenID
  * Provider.
  *
@@ -298,12 +312,31 @@ function openid_discovery($claimed_id) {
   module_load_include('inc', 'openid');
   module_load_include('inc', 'openid', 'xrds');
 
-  $services = array();
+  $methods = module_invoke_all('openid_discovery_method_info');
+  drupal_alter('openid_discovery_method_info', $methods);
 
-  $xrds_url = $claimed_id;
+  // Execute each method in turn.
+  foreach ($methods as $method) {
+    $discovered_services = $method($claimed_id);
+    if (!empty($discovered_services)) {
+      return $discovered_services;
+    }
+  }
+
+  return array();
+}
+
+function _openid_xri_discovery($claimed_id) {
   if (_openid_is_xri($claimed_id)) {
     $xrds_url = 'http://xri.net/' . $claimed_id;
+    _openid_xrds_discovery($xrds_url);
   }
+}
+
+function _openid_xrds_discovery($claimed_id) {
+  $services = array();
+
+  $xrds_url = $claimed_id;
   $scheme = @parse_url($xrds_url, PHP_URL_SCHEME);
   if ($scheme == 'http' || $scheme == 'https') {
     // For regular URLs, try Yadis resolution first, then HTML-based discovery
@@ -360,6 +393,67 @@ function openid_discovery($claimed_id) {
 }
 
 /**
+ * Perform a IDP discovery using Google Discovery protocol.
+ *
+ * This transforms a Google identifier (user@domain) into an XRDS URL first,
+ * then fetches the XRDS document to discover services.
+ *
+ * @see http://sites.google.com/site/oauthgoog/fedlogininterp/openiddiscovery#TOC-IdP-Discovery
+ */
+function _openid_google_idp_discovery($claimed_id) {
+  if (!valid_email_address($claimed_id)) {
+    return;
+  }
+
+  // If the identifier is a valid email address, try to discover the domain
+  // with Google Federated Login.
+  list($name, $domain) = explode('@', $claimed_id, 2);
+  $response = drupal_http_request('https://www.google.com/accounts/o8/.well-known/host-meta?hd=' . rawurlencode($domain));
+  if (isset($response->error) || $response->code != 200) {
+    return;
+  }
+
+  if (preg_match('/Link: <(.*)>/', $response->data, $matches)) {
+    $xrds_url = $matches[1];
+    return _openid_xrds_discovery($xrds_url);
+  }
+}
+
+/**
+ * OpenID discovery method: Perform an user discovery using Google Discovery protocol.
+ *
+ * This transforms a OpenID identifier into an OpenID endpoint.
+ *
+ * @see http://sites.google.com/site/oauthgoog/fedlogininterp/openiddiscovery#TOC-User-Discovery
+ */
+function _openid_google_user_discovery($claimed_id) {
+  $xrds_url = $claimed_id;
+  $url = @parse_url($xrds_url);
+  if (empty($url['scheme']) || ($url['scheme'] != 'http' && $scheme['scheme'] != 'https') || empty($url['host'])) {
+    return;
+  }
+
+  $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'];
+        $xrds_url = str_replace('{%uri}', rawurlencode($claimed_id), $template);
+        return _openid_xrds_discovery($xrds_url);
+      }
+    }
+  }
+}
+
+
+/**
  * Attempt to create a shared secret with the OpenID Provider.
  *
  * @param $op_endpoint URL of the OpenID Provider endpoint.
diff --git modules/openid/xrds.inc modules/openid/xrds.inc
index 2926596..bd7d811 100644
--- modules/openid/xrds.inc
+++ modules/openid/xrds.inc
@@ -71,6 +71,11 @@ function _xrds_cdata(&$parser, $data) {
     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;
   }
 }
 
