From 85551fe3e619df8344c20e1a6a73896b5312e88a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20=C3=85kre=20Solberg?= <andreas.solberg@uninett.no>
Date: Thu, 8 Nov 2007 09:48:30 +0000
Subject: [PATCH] Adding OpenID libraries, templates and server endpoint

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@68 44740490-163a-0410-bde0-09ae8108e29a
---
 lib/Auth/OpenID.php                        |  412 ++++++
 lib/Auth/OpenID/Association.php            |  308 +++++
 lib/Auth/OpenID/BigMath.php                |  444 +++++++
 lib/Auth/OpenID/Consumer.php               | 1186 ++++++++++++++++++
 lib/Auth/OpenID/CryptUtil.php              |  109 ++
 lib/Auth/OpenID/DatabaseConnection.php     |  131 ++
 lib/Auth/OpenID/DiffieHellman.php          |  181 +++
 lib/Auth/OpenID/Discover.php               |  258 ++++
 lib/Auth/OpenID/DumbStore.php              |  116 ++
 lib/Auth/OpenID/FileStore.php              |  674 ++++++++++
 lib/Auth/OpenID/HMACSHA1.php               |   72 ++
 lib/Auth/OpenID/Interface.php              |  188 +++
 lib/Auth/OpenID/KVForm.php                 |  112 ++
 lib/Auth/OpenID/MySQLStore.php             |   78 ++
 lib/Auth/OpenID/Parse.php                  |  308 +++++
 lib/Auth/OpenID/PostgreSQLStore.php        |  136 ++
 lib/Auth/OpenID/SQLStore.php               |  658 ++++++++++
 lib/Auth/OpenID/SQLiteStore.php            |   66 +
 lib/Auth/OpenID/Server.php                 | 1307 ++++++++++++++++++++
 lib/Auth/OpenID/ServerRequest.php          |   37 +
 lib/Auth/OpenID/TrustRoot.php              |  243 ++++
 lib/Auth/OpenID/URINorm.php                |  231 ++++
 lib/Services/Yadis/HTTPFetcher.php         |   92 ++
 lib/Services/Yadis/Manager.php             |  496 ++++++++
 lib/Services/Yadis/Misc.php                |   59 +
 lib/Services/Yadis/ParanoidHTTPFetcher.php |  177 +++
 lib/Services/Yadis/ParseHTML.php           |  258 ++++
 lib/Services/Yadis/PlainHTTPFetcher.php    |  245 ++++
 lib/Services/Yadis/XML.php                 |  365 ++++++
 lib/Services/Yadis/XRDS.php                |  425 +++++++
 lib/Services/Yadis/XRI.php                 |  233 ++++
 lib/Services/Yadis/XRIRes.php              |   68 +
 lib/Services/Yadis/Yadis.php               |  313 +++++
 templates/default/en/openid-about.php      |   60 +
 templates/default/en/openid-sites.php      |   81 ++
 templates/default/en/openid-trust.php      |   33 +
 www/openid/provider/server.php             |  676 ++++++++++
 37 files changed, 10836 insertions(+)
 create mode 100644 lib/Auth/OpenID.php
 create mode 100644 lib/Auth/OpenID/Association.php
 create mode 100644 lib/Auth/OpenID/BigMath.php
 create mode 100644 lib/Auth/OpenID/Consumer.php
 create mode 100644 lib/Auth/OpenID/CryptUtil.php
 create mode 100644 lib/Auth/OpenID/DatabaseConnection.php
 create mode 100644 lib/Auth/OpenID/DiffieHellman.php
 create mode 100644 lib/Auth/OpenID/Discover.php
 create mode 100644 lib/Auth/OpenID/DumbStore.php
 create mode 100644 lib/Auth/OpenID/FileStore.php
 create mode 100644 lib/Auth/OpenID/HMACSHA1.php
 create mode 100644 lib/Auth/OpenID/Interface.php
 create mode 100644 lib/Auth/OpenID/KVForm.php
 create mode 100644 lib/Auth/OpenID/MySQLStore.php
 create mode 100644 lib/Auth/OpenID/Parse.php
 create mode 100644 lib/Auth/OpenID/PostgreSQLStore.php
 create mode 100644 lib/Auth/OpenID/SQLStore.php
 create mode 100644 lib/Auth/OpenID/SQLiteStore.php
 create mode 100644 lib/Auth/OpenID/Server.php
 create mode 100644 lib/Auth/OpenID/ServerRequest.php
 create mode 100644 lib/Auth/OpenID/TrustRoot.php
 create mode 100644 lib/Auth/OpenID/URINorm.php
 create mode 100644 lib/Services/Yadis/HTTPFetcher.php
 create mode 100644 lib/Services/Yadis/Manager.php
 create mode 100644 lib/Services/Yadis/Misc.php
 create mode 100644 lib/Services/Yadis/ParanoidHTTPFetcher.php
 create mode 100644 lib/Services/Yadis/ParseHTML.php
 create mode 100644 lib/Services/Yadis/PlainHTTPFetcher.php
 create mode 100644 lib/Services/Yadis/XML.php
 create mode 100644 lib/Services/Yadis/XRDS.php
 create mode 100644 lib/Services/Yadis/XRI.php
 create mode 100644 lib/Services/Yadis/XRIRes.php
 create mode 100644 lib/Services/Yadis/Yadis.php
 create mode 100644 templates/default/en/openid-about.php
 create mode 100644 templates/default/en/openid-sites.php
 create mode 100644 templates/default/en/openid-trust.php
 create mode 100644 www/openid/provider/server.php

diff --git a/lib/Auth/OpenID.php b/lib/Auth/OpenID.php
new file mode 100644
index 000000000..86278b9f1
--- /dev/null
+++ b/lib/Auth/OpenID.php
@@ -0,0 +1,412 @@
+<?php
+
+/**
+ * This is the PHP OpenID library by JanRain, Inc.
+ *
+ * This module contains core utility functionality used by the
+ * library.  See Consumer.php and Server.php for the consumer and
+ * server implementations.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Require the fetcher code.
+ */
+require_once "Services/Yadis/PlainHTTPFetcher.php";
+require_once "Services/Yadis/ParanoidHTTPFetcher.php";
+require_once "Auth/OpenID/BigMath.php";
+
+/**
+ * Status code returned by the server when the only option is to show
+ * an error page, since we do not have enough information to redirect
+ * back to the consumer. The associated value is an error message that
+ * should be displayed on an HTML error page.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_LOCAL_ERROR', 'local_error');
+
+/**
+ * Status code returned when there is an error to return in key-value
+ * form to the consumer. The caller should return a 400 Bad Request
+ * response with content-type text/plain and the value as the body.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_REMOTE_ERROR', 'remote_error');
+
+/**
+ * Status code returned when there is a key-value form OK response to
+ * the consumer. The value associated with this code is the
+ * response. The caller should return a 200 OK response with
+ * content-type text/plain and the value as the body.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_REMOTE_OK', 'remote_ok');
+
+/**
+ * Status code returned when there is a redirect back to the
+ * consumer. The value is the URL to redirect back to. The caller
+ * should return a 302 Found redirect with a Location: header
+ * containing the URL.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_REDIRECT', 'redirect');
+
+/**
+ * Status code returned when the caller needs to authenticate the
+ * user. The associated value is a {@link Auth_OpenID_ServerRequest}
+ * object that can be used to complete the authentication. If the user
+ * has taken some authentication action, use the retry() method of the
+ * {@link Auth_OpenID_ServerRequest} object to complete the request.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_DO_AUTH', 'do_auth');
+
+/**
+ * Status code returned when there were no OpenID arguments
+ * passed. This code indicates that the caller should return a 200 OK
+ * response and display an HTML page that says that this is an OpenID
+ * server endpoint.
+ *
+ * @see Auth_OpenID_Server
+ */
+define('Auth_OpenID_DO_ABOUT', 'do_about');
+
+/**
+ * Defines for regexes and format checking.
+ */
+define('Auth_OpenID_letters',
+       "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
+
+define('Auth_OpenID_digits',
+       "0123456789");
+
+define('Auth_OpenID_punct',
+       "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~");
+
+if (Auth_OpenID_getMathLib() === null) {
+    define('Auth_OpenID_NO_MATH_SUPPORT', true);
+}
+
+/**
+ * The OpenID utility function class.
+ *
+ * @package OpenID
+ * @access private
+ */
+class Auth_OpenID {
+
+    /**
+     * These namespaces are automatically fixed in query arguments by
+     * Auth_OpenID::fixArgs.
+     */
+    function getOpenIDNamespaces()
+    {
+        return array('openid',
+                     'sreg');
+    }
+
+    /**
+     * Rename query arguments back to 'openid.' from 'openid_'
+     *
+     * @access private
+     * @param array $args An associative array of URL query arguments
+     */
+    function fixArgs($args)
+    {
+        foreach (array_keys($args) as $key) {
+            $fixed = $key;
+            if (preg_match('/^openid/', $key)) {
+                foreach (Auth_OpenID::getOpenIDNamespaces() as $ns) {
+                    if (preg_match('/'.$ns.'_/', $key)) {
+                        $fixed = preg_replace('/'.$ns.'_/', $ns.'.', $fixed);
+                    }
+                }
+
+                if ($fixed != $key) {
+                    $val = $args[$key];
+                    unset($args[$key]);
+                    $args[$fixed] = $val;
+                }
+            }
+        }
+
+        return $args;
+    }
+
+    /**
+     * Create dir_name as a directory if it does not exist. If it
+     * exists, make sure that it is, in fact, a directory.  Returns
+     * true if the operation succeeded; false if not.
+     *
+     * @access private
+     */
+    function ensureDir($dir_name)
+    {
+        if (is_dir($dir_name) || @mkdir($dir_name)) {
+            return true;
+        } else {
+            if (Auth_OpenID::ensureDir(dirname($dir_name))) {
+                return is_dir($dir_name) || @mkdir($dir_name);
+            } else {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Convenience function for getting array values.
+     *
+     * @access private
+     */
+    function arrayGet($arr, $key, $fallback = null)
+    {
+        if (is_array($arr)) {
+            if (array_key_exists($key, $arr)) {
+                return $arr[$key];
+            } else {
+                return $fallback;
+            }
+        } else {
+            trigger_error("Auth_OpenID::arrayGet expected " .
+                          "array as first parameter", E_USER_WARNING);
+            return false;
+        }
+    }
+
+    /**
+     * Implements the PHP 5 'http_build_query' functionality.
+     *
+     * @access private
+     * @param array $data Either an array key/value pairs or an array
+     * of arrays, each of which holding two values: a key and a value,
+     * sequentially.
+     * @return string $result The result of url-encoding the key/value
+     * pairs from $data into a URL query string
+     * (e.g. "username=bob&id=56").
+     */
+    function httpBuildQuery($data)
+    {
+        $pairs = array();
+        foreach ($data as $key => $value) {
+            if (is_array($value)) {
+                $pairs[] = urlencode($value[0])."=".urlencode($value[1]);
+            } else {
+                $pairs[] = urlencode($key)."=".urlencode($value);
+            }
+        }
+        return implode("&", $pairs);
+    }
+
+    /**
+     * "Appends" query arguments onto a URL.  The URL may or may not
+     * already have arguments (following a question mark).
+     *
+     * @param string $url A URL, which may or may not already have
+     * arguments.
+     * @param array $args Either an array key/value pairs or an array of
+     * arrays, each of which holding two values: a key and a value,
+     * sequentially.  If $args is an ordinary key/value array, the
+     * parameters will be added to the URL in sorted alphabetical order;
+     * if $args is an array of arrays, their order will be preserved.
+     * @return string $url The original URL with the new parameters added.
+     *
+     */
+    function appendArgs($url, $args)
+    {
+        if (count($args) == 0) {
+            return $url;
+        }
+
+        // Non-empty array; if it is an array of arrays, use
+        // multisort; otherwise use sort.
+        if (array_key_exists(0, $args) &&
+            is_array($args[0])) {
+            // Do nothing here.
+        } else {
+            $keys = array_keys($args);
+            sort($keys);
+            $new_args = array();
+            foreach ($keys as $key) {
+                $new_args[] = array($key, $args[$key]);
+            }
+            $args = $new_args;
+        }
+
+        $sep = '?';
+        if (strpos($url, '?') !== false) {
+            $sep = '&';
+        }
+
+        return $url . $sep . Auth_OpenID::httpBuildQuery($args);
+    }
+
+    /**
+     * Turn a string into an ASCII string.
+     *
+     * Replace non-ascii characters with a %-encoded, UTF-8
+     * encoding. This function will fail if the input is a string and
+     * there are non-7-bit-safe characters. It is assumed that the
+     * caller will have already translated the input into a Unicode
+     * character sequence, according to the encoding of the HTTP POST
+     * or GET.
+     *
+     * Do not escape anything that is already 7-bit safe, so we do the
+     * minimal transform on the identity URL
+     *
+     * @access private
+     */
+    function quoteMinimal($s)
+    {
+        $res = array();
+        for ($i = 0; $i < strlen($s); $i++) {
+            $c = $s[$i];
+            if ($c >= "\x80") {
+                for ($j = 0; $j < count(utf8_encode($c)); $j++) {
+                    array_push($res, sprintf("%02X", ord($c[$j])));
+                }
+            } else {
+                array_push($res, $c);
+            }
+        }
+    
+        return implode('', $res);
+    }
+
+    /**
+     * Implements python's urlunparse, which is not available in PHP.
+     * Given the specified components of a URL, this function rebuilds
+     * and returns the URL.
+     *
+     * @access private
+     * @param string $scheme The scheme (e.g. 'http').  Defaults to 'http'.
+     * @param string $host The host.  Required.
+     * @param string $port The port.
+     * @param string $path The path.
+     * @param string $query The query.
+     * @param string $fragment The fragment.
+     * @return string $url The URL resulting from assembling the
+     * specified components.
+     */
+    function urlunparse($scheme, $host, $port = null, $path = '/',
+                                    $query = '', $fragment = '')
+    {
+
+        if (!$scheme) {
+            $scheme = 'http';
+        }
+
+        if (!$host) {
+            return false;
+        }
+
+        if (!$path) {
+            $path = '/';
+        }
+
+        $result = $scheme . "://" . $host;
+
+        if ($port) {
+            $result .= ":" . $port;
+        }
+
+        $result .= $path;
+
+        if ($query) {
+            $result .= "?" . $query;
+        }
+
+        if ($fragment) {
+            $result .= "#" . $fragment;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Given a URL, this "normalizes" it by adding a trailing slash
+     * and / or a leading http:// scheme where necessary.  Returns
+     * null if the original URL is malformed and cannot be normalized.
+     *
+     * @access private
+     * @param string $url The URL to be normalized.
+     * @return mixed $new_url The URL after normalization, or null if
+     * $url was malformed.
+     */
+    function normalizeUrl($url)
+    {
+        if ($url === null) {
+            return null;
+        }
+
+        assert(is_string($url));
+
+        $old_url = $url;
+        $url = trim($url);
+
+        if (strpos($url, "://") === false) {
+            $url = "http://" . $url;
+        }
+
+        $parsed = @parse_url($url);
+
+        if ($parsed === false) {
+            return null;
+        }
+
+        $defaults = array(
+                          'scheme' => '',
+                          'host' => '',
+                          'path' => '',
+                          'query' => '',
+                          'fragment' => '',
+                          'port' => ''
+                          );
+
+        $parsed = array_merge($defaults, $parsed);
+
+        if (($parsed['scheme'] == '') ||
+            ($parsed['host'] == '')) {
+            if ($parsed['path'] == '' &&
+                $parsed['query'] == '' &&
+                $parsed['fragment'] == '') {
+                return null;
+            }
+
+            $url = 'http://' + $url;
+            $parsed = parse_url($url);
+
+            $parsed = array_merge($defaults, $parsed);
+        }
+
+        $tail = array_map(array('Auth_OpenID', 'quoteMinimal'),
+                          array($parsed['path'],
+                                $parsed['query'],
+                                $parsed['fragment']));
+        if ($tail[0] == '') {
+            $tail[0] = '/';
+        }
+
+        $url = Auth_OpenID::urlunparse($parsed['scheme'], $parsed['host'],
+                                       $parsed['port'], $tail[0], $tail[1],
+                                       $tail[2]);
+
+        assert(is_string($url));
+
+        return $url;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/Association.php b/lib/Auth/OpenID/Association.php
new file mode 100644
index 000000000..109a97080
--- /dev/null
+++ b/lib/Auth/OpenID/Association.php
@@ -0,0 +1,308 @@
+<?php
+
+/**
+ * This module contains code for dealing with associations between
+ * consumers and servers.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * @access private
+ */
+require_once 'Auth/OpenID/CryptUtil.php';
+
+/**
+ * @access private
+ */
+require_once 'Auth/OpenID/KVForm.php';
+
+/**
+ * This class represents an association between a server and a
+ * consumer.  In general, users of this library will never see
+ * instances of this object.  The only exception is if you implement a
+ * custom {@link Auth_OpenID_OpenIDStore}.
+ *
+ * If you do implement such a store, it will need to store the values
+ * of the handle, secret, issued, lifetime, and assoc_type instance
+ * variables.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Association {
+
+    /**
+     * This is a HMAC-SHA1 specific value.
+     *
+     * @access private
+     */
+    var $SIG_LENGTH = 20;
+
+    /**
+     * The ordering and name of keys as stored by serialize.
+     *
+     * @access private
+     */
+    var $assoc_keys = array(
+                            'version',
+                            'handle',
+                            'secret',
+                            'issued',
+                            'lifetime',
+                            'assoc_type'
+                            );
+
+    /**
+     * This is an alternate constructor (factory method) used by the
+     * OpenID consumer library to create associations.  OpenID store
+     * implementations shouldn't use this constructor.
+     *
+     * @access private
+     *
+     * @param integer $expires_in This is the amount of time this
+     * association is good for, measured in seconds since the
+     * association was issued.
+     *
+     * @param string $handle This is the handle the server gave this
+     * association.
+     *
+     * @param string secret This is the shared secret the server
+     * generated for this association.
+     *
+     * @param assoc_type This is the type of association this
+     * instance represents.  The only valid value of this field at
+     * this time is 'HMAC-SHA1', but new types may be defined in the
+     * future.
+     *
+     * @return association An {@link Auth_OpenID_Association}
+     * instance.
+     */
+    function fromExpiresIn($expires_in, $handle, $secret, $assoc_type)
+    {
+        $issued = time();
+        $lifetime = $expires_in;
+        return new Auth_OpenID_Association($handle, $secret,
+                                           $issued, $lifetime, $assoc_type);
+    }
+
+    /**
+     * This is the standard constructor for creating an association.
+     * The library should create all of the necessary associations, so
+     * this constructor is not part of the external API.
+     *
+     * @access private
+     *
+     * @param string $handle This is the handle the server gave this
+     * association.
+     *
+     * @param string $secret This is the shared secret the server
+     * generated for this association.
+     *
+     * @param integer $issued This is the time this association was
+     * issued, in seconds since 00:00 GMT, January 1, 1970.  (ie, a
+     * unix timestamp)
+     *
+     * @param integer $lifetime This is the amount of time this
+     * association is good for, measured in seconds since the
+     * association was issued.
+     *
+     * @param string $assoc_type This is the type of association this
+     * instance represents.  The only valid value of this field at
+     * this time is 'HMAC-SHA1', but new types may be defined in the
+     * future.
+     */
+    function Auth_OpenID_Association(
+        $handle, $secret, $issued, $lifetime, $assoc_type)
+    {
+        if ($assoc_type != 'HMAC-SHA1') {
+            $fmt = 'HMAC-SHA1 is the only supported association type (got %s)';
+            trigger_error(sprintf($fmt, $assoc_type), E_USER_ERROR);
+        }
+
+        $this->handle = $handle;
+        $this->secret = $secret;
+        $this->issued = $issued;
+        $this->lifetime = $lifetime;
+        $this->assoc_type = $assoc_type;
+    }
+
+    /**
+     * This returns the number of seconds this association is still
+     * valid for, or 0 if the association is no longer valid.
+     *
+     * @return integer $seconds The number of seconds this association
+     * is still valid for, or 0 if the association is no longer valid.
+     */
+    function getExpiresIn($now = null)
+    {
+        if ($now == null) {
+            $now = time();
+        }
+
+        return max(0, $this->issued + $this->lifetime - $now);
+    }
+
+    /**
+     * This checks to see if two {@link Auth_OpenID_Association}
+     * instances represent the same association.
+     *
+     * @return bool $result true if the two instances represent the
+     * same association, false otherwise.
+     */
+    function equal($other)
+    {
+        return ((gettype($this) == gettype($other))
+                && ($this->handle == $other->handle)
+                && ($this->secret == $other->secret)
+                && ($this->issued == $other->issued)
+                && ($this->lifetime == $other->lifetime)
+                && ($this->assoc_type == $other->assoc_type));
+    }
+
+    /**
+     * Convert an association to KV form.
+     *
+     * @return string $result String in KV form suitable for
+     * deserialization by deserialize.
+     */
+    function serialize()
+    {
+        $data = array(
+                     'version' => '2',
+                     'handle' => $this->handle,
+                     'secret' => base64_encode($this->secret),
+                     'issued' => strval(intval($this->issued)),
+                     'lifetime' => strval(intval($this->lifetime)),
+                     'assoc_type' => $this->assoc_type
+                     );
+
+        assert(array_keys($data) == $this->assoc_keys);
+
+        return Auth_OpenID_KVForm::fromArray($data, $strict = true);
+    }
+
+    /**
+     * Parse an association as stored by serialize().  This is the
+     * inverse of serialize.
+     *
+     * @param string $assoc_s Association as serialized by serialize()
+     * @return Auth_OpenID_Association $result instance of this class
+     */
+    function deserialize($class_name, $assoc_s)
+    {
+        $pairs = Auth_OpenID_KVForm::toArray($assoc_s, $strict = true);
+        $keys = array();
+        $values = array();
+        foreach ($pairs as $key => $value) {
+            if (is_array($value)) {
+                list($key, $value) = $value;
+            }
+            $keys[] = $key;
+            $values[] = $value;
+        }
+
+        $class_vars = get_class_vars($class_name);
+        $class_assoc_keys = $class_vars['assoc_keys'];
+
+        sort($keys);
+        sort($class_assoc_keys);
+
+        if ($keys != $class_assoc_keys) {
+            trigger_error('Unexpected key values: ' . strval($keys),
+                          E_USER_WARNING);
+            return null;
+        }
+
+        $version = $pairs['version'];
+        $handle = $pairs['handle'];
+        $secret = $pairs['secret'];
+        $issued = $pairs['issued'];
+        $lifetime = $pairs['lifetime'];
+        $assoc_type = $pairs['assoc_type'];
+
+        if ($version != '2') {
+            trigger_error('Unknown version: ' . $version, E_USER_WARNING);
+            return null;
+        }
+
+        $issued = intval($issued);
+        $lifetime = intval($lifetime);
+        $secret = base64_decode($secret);
+
+        return new $class_name(
+            $handle, $secret, $issued, $lifetime, $assoc_type);
+    }
+
+    /**
+     * Generate a signature for a sequence of (key, value) pairs
+     *
+     * @access private
+     * @param array $pairs The pairs to sign, in order.  This is an
+     * array of two-tuples.
+     * @return string $signature The binary signature of this sequence
+     * of pairs
+     */
+    function sign($pairs)
+    {
+        $kv = Auth_OpenID_KVForm::fromArray($pairs);
+        return Auth_OpenID_HMACSHA1($this->secret, $kv);
+    }
+
+    /**
+     * Generate a signature for some fields in a dictionary
+     *
+     * @access private
+     * @param array $fields The fields to sign, in order; this is an
+     * array of strings.
+     * @param array $data Dictionary of values to sign (an array of
+     * string => string pairs).
+     * @return string $signature The signature, base64 encoded
+     */
+    function signDict($fields, $data, $prefix = 'openid.')
+    {
+        $pairs = array();
+        foreach ($fields as $field) {
+            $pairs[] = array($field, $data[$prefix . $field]);
+        }
+
+        return base64_encode($this->sign($pairs));
+    }
+
+    /**
+     * Add a signature to an array of fields
+     *
+     * @access private
+     */
+    function addSignature($fields, &$data, $prefix = 'openid.')
+    {
+        $sig = $this->signDict($fields, $data, $prefix);
+        $signed = implode(",", $fields);
+        $data[$prefix . 'sig'] = $sig;
+        $data[$prefix . 'signed'] = $signed;
+    }
+
+    /**
+     * Confirm that the signature of these fields matches the
+     * signature contained in the data
+     *
+     * @access private
+     */
+    function checkSignature($data, $prefix = 'openid.')
+    {
+        $signed = $data[$prefix . 'signed'];
+        $fields = explode(",", $signed);
+        $expected_sig = $this->signDict($fields, $data, $prefix);
+        $request_sig = $data[$prefix . 'sig'];
+
+        return ($request_sig == $expected_sig);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/BigMath.php b/lib/Auth/OpenID/BigMath.php
new file mode 100644
index 000000000..3113104ba
--- /dev/null
+++ b/lib/Auth/OpenID/BigMath.php
@@ -0,0 +1,444 @@
+<?php
+
+/**
+ * BigMath: A math library wrapper that abstracts out the underlying
+ * long integer library.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Needed for random number generation
+ */
+require_once 'Auth/OpenID/CryptUtil.php';
+
+/**
+ * The superclass of all big-integer math implementations
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_MathLibrary {
+    /**
+     * Given a long integer, returns the number converted to a binary
+     * string.  This function accepts long integer values of arbitrary
+     * magnitude and uses the local large-number math library when
+     * available.
+     *
+     * @param integer $long The long number (can be a normal PHP
+     * integer or a number created by one of the available long number
+     * libraries)
+     * @return string $binary The binary version of $long
+     */
+    function longToBinary($long)
+    {
+        $cmp = $this->cmp($long, 0);
+        if ($cmp < 0) {
+            $msg = __FUNCTION__ . " takes only positive integers.";
+            trigger_error($msg, E_USER_ERROR);
+            return null;
+        }
+
+        if ($cmp == 0) {
+            return "\x00";
+        }
+
+        $bytes = array();
+
+        while ($this->cmp($long, 0) > 0) {
+            array_unshift($bytes, $this->mod($long, 256));
+            $long = $this->div($long, pow(2, 8));
+        }
+
+        if ($bytes && ($bytes[0] > 127)) {
+            array_unshift($bytes, 0);
+        }
+
+        $string = '';
+        foreach ($bytes as $byte) {
+            $string .= pack('C', $byte);
+        }
+
+        return $string;
+    }
+
+    /**
+     * Given a binary string, returns the binary string converted to a
+     * long number.
+     *
+     * @param string $binary The binary version of a long number,
+     * probably as a result of calling longToBinary
+     * @return integer $long The long number equivalent of the binary
+     * string $str
+     */
+    function binaryToLong($str)
+    {
+        if ($str === null) {
+            return null;
+        }
+
+        // Use array_merge to return a zero-indexed array instead of a
+        // one-indexed array.
+        $bytes = array_merge(unpack('C*', $str));
+
+        $n = $this->init(0);
+
+        if ($bytes && ($bytes[0] > 127)) {
+            trigger_error("bytesToNum works only for positive integers.",
+                          E_USER_WARNING);
+            return null;
+        }
+
+        foreach ($bytes as $byte) {
+            $n = $this->mul($n, pow(2, 8));
+            $n = $this->add($n, $byte);
+        }
+
+        return $n;
+    }
+
+    function base64ToLong($str)
+    {
+        $b64 = base64_decode($str);
+
+        if ($b64 === false) {
+            return false;
+        }
+
+        return $this->binaryToLong($b64);
+    }
+
+    function longToBase64($str)
+    {
+        return base64_encode($this->longToBinary($str));
+    }
+
+    /**
+     * Returns a random number in the specified range.  This function
+     * accepts $start, $stop, and $step values of arbitrary magnitude
+     * and will utilize the local large-number math library when
+     * available.
+     *
+     * @param integer $start The start of the range, or the minimum
+     * random number to return
+     * @param integer $stop The end of the range, or the maximum
+     * random number to return
+     * @param integer $step The step size, such that $result - ($step
+     * * N) = $start for some N
+     * @return integer $result The resulting randomly-generated number
+     */
+    function rand($stop)
+    {
+        static $duplicate_cache = array();
+
+        // Used as the key for the duplicate cache
+        $rbytes = $this->longToBinary($stop);
+
+        if (array_key_exists($rbytes, $duplicate_cache)) {
+            list($duplicate, $nbytes) = $duplicate_cache[$rbytes];
+        } else {
+            if ($rbytes[0] == "\x00") {
+                $nbytes = strlen($rbytes) - 1;
+            } else {
+                $nbytes = strlen($rbytes);
+            }
+
+            $mxrand = $this->pow(256, $nbytes);
+
+            // If we get a number less than this, then it is in the
+            // duplicated range.
+            $duplicate = $this->mod($mxrand, $stop);
+
+            if (count($duplicate_cache) > 10) {
+                $duplicate_cache = array();
+            }
+
+            $duplicate_cache[$rbytes] = array($duplicate, $nbytes);
+        }
+
+        do {
+            $bytes = "\x00" . Auth_OpenID_CryptUtil::getBytes($nbytes);
+            $n = $this->binaryToLong($bytes);
+            // Keep looping if this value is in the low duplicated range
+        } while ($this->cmp($n, $duplicate) < 0);
+
+        return $this->mod($n, $stop);
+    }
+}
+
+/**
+ * Exposes BCmath math library functionality.
+ *
+ * {@link Auth_OpenID_BcMathWrapper} wraps the functionality provided
+ * by the BCMath extension.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_BcMathWrapper extends Auth_OpenID_MathLibrary{
+    var $type = 'bcmath';
+
+    function add($x, $y)
+    {
+        return bcadd($x, $y);
+    }
+
+    function sub($x, $y)
+    {
+        return bcsub($x, $y);
+    }
+
+    function pow($base, $exponent)
+    {
+        return bcpow($base, $exponent);
+    }
+
+    function cmp($x, $y)
+    {
+        return bccomp($x, $y);
+    }
+
+    function init($number, $base = 10)
+    {
+        return $number;
+    }
+
+    function mod($base, $modulus)
+    {
+        return bcmod($base, $modulus);
+    }
+
+    function mul($x, $y)
+    {
+        return bcmul($x, $y);
+    }
+
+    function div($x, $y)
+    {
+        return bcdiv($x, $y);
+    }
+
+    /**
+     * Same as bcpowmod when bcpowmod is missing
+     *
+     * @access private
+     */
+    function _powmod($base, $exponent, $modulus)
+    {
+        $square = $this->mod($base, $modulus);
+        $result = 1;
+        while($this->cmp($exponent, 0) > 0) {
+            if ($this->mod($exponent, 2)) {
+                $result = $this->mod($this->mul($result, $square), $modulus);
+            }
+            $square = $this->mod($this->mul($square, $square), $modulus);
+            $exponent = $this->div($exponent, 2);
+        }
+        return $result;
+    }
+
+    function powmod($base, $exponent, $modulus)
+    {
+        if (function_exists('bcpowmod')) {
+            return bcpowmod($base, $exponent, $modulus);
+        } else {
+            return $this->_powmod($base, $exponent, $modulus);
+        }
+    }
+
+    function toString($num)
+    {
+        return $num;
+    }
+}
+
+/**
+ * Exposes GMP math library functionality.
+ *
+ * {@link Auth_OpenID_GmpMathWrapper} wraps the functionality provided
+ * by the GMP extension.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_GmpMathWrapper extends Auth_OpenID_MathLibrary{
+    var $type = 'gmp';
+
+    function add($x, $y)
+    {
+        return gmp_add($x, $y);
+    }
+
+    function sub($x, $y)
+    {
+        return gmp_sub($x, $y);
+    }
+
+    function pow($base, $exponent)
+    {
+        return gmp_pow($base, $exponent);
+    }
+
+    function cmp($x, $y)
+    {
+        return gmp_cmp($x, $y);
+    }
+
+    function init($number, $base = 10)
+    {
+        return gmp_init($number, $base);
+    }
+
+    function mod($base, $modulus)
+    {
+        return gmp_mod($base, $modulus);
+    }
+
+    function mul($x, $y)
+    {
+        return gmp_mul($x, $y);
+    }
+
+    function div($x, $y)
+    {
+        return gmp_div_q($x, $y);
+    }
+
+    function powmod($base, $exponent, $modulus)
+    {
+        return gmp_powm($base, $exponent, $modulus);
+    }
+
+    function toString($num)
+    {
+        return gmp_strval($num);
+    }
+}
+
+/**
+ * Define the supported extensions.  An extension array has keys
+ * 'modules', 'extension', and 'class'.  'modules' is an array of PHP
+ * module names which the loading code will attempt to load.  These
+ * values will be suffixed with a library file extension (e.g. ".so").
+ * 'extension' is the name of a PHP extension which will be tested
+ * before 'modules' are loaded.  'class' is the string name of a
+ * {@link Auth_OpenID_MathWrapper} subclass which should be
+ * instantiated if a given extension is present.
+ *
+ * You can define new math library implementations and add them to
+ * this array.
+ */
+global $_Auth_OpenID_math_extensions;
+$_Auth_OpenID_math_extensions = array(
+    array('modules' => array('gmp', 'php_gmp'),
+          'extension' => 'gmp',
+          'class' => 'Auth_OpenID_GmpMathWrapper'),
+    array('modules' => array('bcmath', 'php_bcmath'),
+          'extension' => 'bcmath',
+          'class' => 'Auth_OpenID_BcMathWrapper')
+    );
+
+/**
+ * Detect which (if any) math library is available
+ */
+function Auth_OpenID_detectMathLibrary($exts)
+{
+    $loaded = false;
+
+    foreach ($exts as $extension) {
+        // See if the extension specified is already loaded.
+        if ($extension['extension'] &&
+            extension_loaded($extension['extension'])) {
+            $loaded = true;
+        }
+
+        // Try to load dynamic modules.
+        if (!$loaded) {
+            foreach ($extension['modules'] as $module) {
+                if (@dl($module . "." . PHP_SHLIB_SUFFIX)) {
+                    $loaded = true;
+                    break;
+                }
+            }
+        }
+
+        // If the load succeeded, supply an instance of
+        // Auth_OpenID_MathWrapper which wraps the specified
+        // module's functionality.
+        if ($loaded) {
+            return $extension;
+        }
+    }
+
+    return false;
+}
+
+/**
+ * {@link Auth_OpenID_getMathLib} checks for the presence of long
+ * number extension modules and returns an instance of
+ * {@link Auth_OpenID_MathWrapper} which exposes the module's
+ * functionality.
+ *
+ * Checks for the existence of an extension module described by the
+ * local {@link Auth_OpenID_math_extensions} array and returns an
+ * instance of a wrapper for that extension module.  If no extension
+ * module is found, an instance of {@link Auth_OpenID_MathWrapper} is
+ * returned, which wraps the native PHP integer implementation.  The
+ * proper calling convention for this method is $lib =&
+ * Auth_OpenID_getMathLib().
+ *
+ * This function checks for the existence of specific long number
+ * implementations in the following order: GMP followed by BCmath.
+ *
+ * @return Auth_OpenID_MathWrapper $instance An instance of
+ * {@link Auth_OpenID_MathWrapper} or one of its subclasses
+ *
+ * @package OpenID
+ */
+function &Auth_OpenID_getMathLib()
+{
+    // The instance of Auth_OpenID_MathWrapper that we choose to
+    // supply will be stored here, so that subseqent calls to this
+    // method will return a reference to the same object.
+    static $lib = null;
+
+    if (isset($lib)) {
+        return $lib;
+    }
+
+    if (defined('Auth_OpenID_NO_MATH_SUPPORT')) {
+        $null = null;
+        return $null;
+    }
+
+    // If this method has not been called before, look at
+    // $Auth_OpenID_math_extensions and try to find an extension that
+    // works.
+    global $_Auth_OpenID_math_extensions;
+    $ext = Auth_OpenID_detectMathLibrary($_Auth_OpenID_math_extensions);
+    if ($ext === false) {
+        $tried = array();
+        foreach ($_Auth_OpenID_math_extensions as $extinfo) {
+            $tried[] = $extinfo['extension'];
+        }
+        $triedstr = implode(", ", $tried);
+
+        define('Auth_OpenID_NO_MATH_SUPPORT', true);
+        return null;
+    }
+
+    // Instantiate a new wrapper
+    $class = $ext['class'];
+    $lib = new $class();
+
+    return $lib;
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/Consumer.php b/lib/Auth/OpenID/Consumer.php
new file mode 100644
index 000000000..7ea75c75a
--- /dev/null
+++ b/lib/Auth/OpenID/Consumer.php
@@ -0,0 +1,1186 @@
+<?php
+
+/**
+ * This module documents the main interface with the OpenID consumer
+ * library.  The only part of the library which has to be used and
+ * isn't documented in full here is the store required to create an
+ * Auth_OpenID_Consumer instance.  More on the abstract store type and
+ * concrete implementations of it that are provided in the
+ * documentation for the Auth_OpenID_Consumer constructor.
+ *
+ * OVERVIEW
+ *
+ * The OpenID identity verification process most commonly uses the
+ * following steps, as visible to the user of this library:
+ *
+ *   1. The user enters their OpenID into a field on the consumer's
+ *      site, and hits a login button.
+ *   2. The consumer site discovers the user's OpenID server using the
+ *      YADIS protocol.
+ *   3. The consumer site sends the browser a redirect to the identity
+ *      server.  This is the authentication request as described in
+ *      the OpenID specification.
+ *   4. The identity server's site sends the browser a redirect back
+ *      to the consumer site.  This redirect contains the server's
+ *      response to the authentication request.
+ *
+ * The most important part of the flow to note is the consumer's site
+ * must handle two separate HTTP requests in order to perform the full
+ * identity check.
+ *
+ * LIBRARY DESIGN
+ * 
+ * This consumer library is designed with that flow in mind.  The goal
+ * is to make it as easy as possible to perform the above steps
+ * securely.
+ *
+ * At a high level, there are two important parts in the consumer
+ * library.  The first important part is this module, which contains
+ * the interface to actually use this library.  The second is the
+ * Auth_OpenID_Interface class, which describes the interface to use
+ * if you need to create a custom method for storing the state this
+ * library needs to maintain between requests.
+ *
+ * In general, the second part is less important for users of the
+ * library to know about, as several implementations are provided
+ * which cover a wide variety of situations in which consumers may use
+ * the library.
+ *
+ * This module contains a class, Auth_OpenID_Consumer, with methods
+ * corresponding to the actions necessary in each of steps 2, 3, and 4
+ * described in the overview.  Use of this library should be as easy
+ * as creating an Auth_OpenID_Consumer instance and calling the
+ * methods appropriate for the action the site wants to take.
+ *
+ * STORES AND DUMB MODE
+ *
+ * OpenID is a protocol that works best when the consumer site is able
+ * to store some state.  This is the normal mode of operation for the
+ * protocol, and is sometimes referred to as smart mode.  There is
+ * also a fallback mode, known as dumb mode, which is available when
+ * the consumer site is not able to store state.  This mode should be
+ * avoided when possible, as it leaves the implementation more
+ * vulnerable to replay attacks.
+ *
+ * The mode the library works in for normal operation is determined by
+ * the store that it is given.  The store is an abstraction that
+ * handles the data that the consumer needs to manage between http
+ * requests in order to operate efficiently and securely.
+ *
+ * Several store implementation are provided, and the interface is
+ * fully documented so that custom stores can be used as well.  See
+ * the documentation for the Auth_OpenID_Consumer class for more
+ * information on the interface for stores.  The implementations that
+ * are provided allow the consumer site to store the necessary data in
+ * several different ways, including several SQL databases and normal
+ * files on disk.
+ *
+ * There is an additional concrete store provided that puts the system
+ * in dumb mode.  This is not recommended, as it removes the library's
+ * ability to stop replay attacks reliably.  It still uses time-based
+ * checking to make replay attacks only possible within a small
+ * window, but they remain possible within that window.  This store
+ * should only be used if the consumer site has no way to retain data
+ * between requests at all.
+ *
+ * IMMEDIATE MODE
+ *
+ * In the flow described above, the user may need to confirm to the
+ * lidentity server that it's ok to authorize his or her identity.
+ * The server may draw pages asking for information from the user
+ * before it redirects the browser back to the consumer's site.  This
+ * is generally transparent to the consumer site, so it is typically
+ * ignored as an implementation detail.
+ *
+ * There can be times, however, where the consumer site wants to get a
+ * response immediately.  When this is the case, the consumer can put
+ * the library in immediate mode.  In immediate mode, there is an
+ * extra response possible from the server, which is essentially the
+ * server reporting that it doesn't have enough information to answer
+ * the question yet.  In addition to saying that, the identity server
+ * provides a URL to which the user can be sent to provide the needed
+ * information and let the server finish handling the original
+ * request.
+ *
+ * USING THIS LIBRARY
+ *
+ * Integrating this library into an application is usually a
+ * relatively straightforward process.  The process should basically
+ * follow this plan:
+ *
+ * Add an OpenID login field somewhere on your site.  When an OpenID
+ * is entered in that field and the form is submitted, it should make
+ * a request to the your site which includes that OpenID URL.
+ *
+ * First, the application should instantiate the Auth_OpenID_Consumer
+ * class using the store of choice (Auth_OpenID_FileStore or one of
+ * the SQL-based stores).  If the application has any sort of session
+ * framework that provides per-client state management, a dict-like
+ * object to access the session should be passed as the optional
+ * second parameter.  (The default behavior is to use PHP's standard
+ * session machinery.)
+ *
+ * Next, the application should call the Auth_OpenID_Consumer object's
+ * 'begin' method.  This method takes the OpenID URL.  The 'begin'
+ * method returns an Auth_OpenID_AuthRequest object.
+ *
+ * Next, the application should call the 'redirectURL' method of the
+ * Auth_OpenID_AuthRequest object.  The 'return_to' URL parameter is
+ * the URL that the OpenID server will send the user back to after
+ * attempting to verify his or her identity.  The 'trust_root' is the
+ * URL (or URL pattern) that identifies your web site to the user when
+ * he or she is authorizing it.  Send a redirect to the resulting URL
+ * to the user's browser.
+ *
+ * That's the first half of the authentication process.  The second
+ * half of the process is done after the user's ID server sends the
+ * user's browser a redirect back to your site to complete their
+ * login.
+ *
+ * When that happens, the user will contact your site at the URL given
+ * as the 'return_to' URL to the Auth_OpenID_AuthRequest::redirectURL
+ * call made above.  The request will have several query parameters
+ * added to the URL by the identity server as the information
+ * necessary to finish the request.
+ *
+ * Lastly, instantiate an Auth_OpenID_Consumer instance as above and
+ * call its 'complete' method, passing in all the received query
+ * arguments.
+ *
+ * There are multiple possible return types possible from that
+ * method. These indicate the whether or not the login was successful,
+ * and include any additional information appropriate for their type.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Require utility classes and functions for the consumer.
+ */
+require_once "Auth/OpenID.php";
+require_once "Auth/OpenID/HMACSHA1.php";
+require_once "Auth/OpenID/Association.php";
+require_once "Auth/OpenID/CryptUtil.php";
+require_once "Auth/OpenID/DiffieHellman.php";
+require_once "Auth/OpenID/KVForm.php";
+require_once "Auth/OpenID/Discover.php";
+require_once "Services/Yadis/Manager.php";
+require_once "Services/Yadis/XRI.php";
+
+/**
+ * This is the status code returned when the complete method returns
+ * successfully.
+ */
+define('Auth_OpenID_SUCCESS', 'success');
+
+/**
+ * Status to indicate cancellation of OpenID authentication.
+ */
+define('Auth_OpenID_CANCEL', 'cancel');
+
+/**
+ * This is the status code completeAuth returns when the value it
+ * received indicated an invalid login.
+ */
+define('Auth_OpenID_FAILURE', 'failure');
+
+/**
+ * This is the status code completeAuth returns when the
+ * {@link Auth_OpenID_Consumer} instance is in immediate mode, and the
+ * identity server sends back a URL to send the user to to complete his
+ * or her login.
+ */
+define('Auth_OpenID_SETUP_NEEDED', 'setup needed');
+
+/**
+ * This is the status code beginAuth returns when the page fetched
+ * from the entered OpenID URL doesn't contain the necessary link tags
+ * to function as an identity page.
+ */
+define('Auth_OpenID_PARSE_ERROR', 'parse error');
+
+/**
+ * This is the characters that the nonces are made from.
+ */
+define('Auth_OpenID_DEFAULT_NONCE_CHRS',"abcdefghijklmnopqrstuvwxyz" .
+       "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
+
+/**
+ * An OpenID consumer implementation that performs discovery and does
+ * session management.  See the Consumer.php file documentation for
+ * more information.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Consumer {
+
+    /**
+     * @access private
+     */
+    var $session_key_prefix = "_openid_consumer_";
+
+    /**
+     * @access private
+     */
+    var $_token_suffix = "last_token";
+
+    /**
+     * Initialize a Consumer instance.
+     *
+     * You should create a new instance of the Consumer object with
+     * every HTTP request that handles OpenID transactions.
+     *
+     * @param Auth_OpenID_OpenIDStore $store This must be an object
+     * that implements the interface in {@link
+     * Auth_OpenID_OpenIDStore}.  Several concrete implementations are
+     * provided, to cover most common use cases.  For stores backed by
+     * MySQL, PostgreSQL, or SQLite, see the {@link
+     * Auth_OpenID_SQLStore} class and its sublcasses.  For a
+     * filesystem-backed store, see the {@link Auth_OpenID_FileStore}
+     * module.  As a last resort, if it isn't possible for the server
+     * to store state at all, an instance of {@link
+     * Auth_OpenID_DumbStore} can be used.
+     *
+     * @param mixed session An object which implements the interface
+     * of the Services_Yadis_Session class.  Particularly, this object
+     * is expected to have these methods: get($key), set($key,
+     * $value), and del($key).  This defaults to a session object
+     * which wraps PHP's native session machinery.  You should only
+     * need to pass something here if you have your own sessioning
+     * implementation.
+     */
+    function Auth_OpenID_Consumer(&$store, $session = null)
+    {
+        if ($session === null) {
+            $session = new Services_Yadis_PHPSession();
+        }
+
+        $this->session =& $session;
+        $this->consumer =& new Auth_OpenID_GenericConsumer($store);
+        $this->_token_key = $this->session_key_prefix . $this->_token_suffix;
+    }
+
+    /**
+     * Start the OpenID authentication process. See steps 1-2 in the
+     * overview at the top of this file.
+     *
+     * @param User_url: Identity URL given by the user. This method
+     * performs a textual transformation of the URL to try and make
+     * sure it is normalized. For example, a user_url of example.com
+     * will be normalized to http://example.com/ normalizing and
+     * resolving any redirects the server might issue.
+     *
+     * @return Auth_OpenID_AuthRequest $auth_request An object
+     * containing the discovered information will be returned, with a
+     * method for building a redirect URL to the server, as described
+     * in step 3 of the overview. This object may also be used to add
+     * extension arguments to the request, using its 'addExtensionArg'
+     * method.
+     */
+    function begin($user_url)
+    {
+        $discoverMethod = '_Auth_OpenID_discoverServiceList';
+        $openid_url = $user_url;
+
+        if (Services_Yadis_identifierScheme($user_url) == 'XRI') {
+            $discoverMethod = '_Auth_OpenID_discoverXRIServiceList';
+        } else {
+            $openid_url = Auth_OpenID::normalizeUrl($user_url);
+        }
+
+        $disco =& new Services_Yadis_Discovery($this->session,
+                                               $openid_url,
+                                               $this->session_key_prefix);
+
+        // Set the 'stale' attribute of the manager.  If discovery
+        // fails in a fatal way, the stale flag will cause the manager
+        // to be cleaned up next time discovery is attempted.
+
+        $m = $disco->getManager();
+        $loader = new Services_Yadis_ManagerLoader();
+
+        if ($m) {
+            if ($m->stale) {
+                $disco->destroyManager();
+            } else {
+                $m->stale = true;
+                $disco->session->set($disco->session_key,
+                                     serialize($loader->toSession($m)));
+            }
+        }
+
+        $endpoint = $disco->getNextService($discoverMethod,
+                                           $this->consumer->fetcher);
+
+        // Reset the 'stale' attribute of the manager.
+        $m =& $disco->getManager();
+        if ($m) {
+            $m->stale = false;
+            $disco->session->set($disco->session_key,
+                                 serialize($loader->toSession($m)));
+        }
+
+        if ($endpoint === null) {
+            return null;
+        } else {
+            return $this->beginWithoutDiscovery($endpoint);
+        }
+    }
+
+    /**
+     * Start OpenID verification without doing OpenID server
+     * discovery. This method is used internally by Consumer.begin
+     * after discovery is performed, and exists to provide an
+     * interface for library users needing to perform their own
+     * discovery.
+     *
+     * @param Auth_OpenID_ServiceEndpoint $endpoint an OpenID service
+     * endpoint descriptor.
+     *
+     * @return Auth_OpenID_AuthRequest $auth_request An OpenID
+     * authentication request object.
+     */
+    function &beginWithoutDiscovery($endpoint)
+    {
+        $loader = new Auth_OpenID_ServiceEndpointLoader();
+        $auth_req = $this->consumer->begin($endpoint);
+        $this->session->set($this->_token_key,
+              $loader->toSession($auth_req->endpoint));
+        return $auth_req;
+    }
+
+    /**
+     * Called to interpret the server's response to an OpenID
+     * request. It is called in step 4 of the flow described in the
+     * consumer overview.
+     *
+     * @param array $query An array of the query parameters (key =>
+     * value pairs) for this HTTP request.
+     *
+     * @return Auth_OpenID_ConsumerResponse $response A instance of an
+     * Auth_OpenID_ConsumerResponse subclass. The type of response is
+     * indicated by the status attribute, which will be one of
+     * SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED.
+     */
+    function complete($query)
+    {
+        $query = Auth_OpenID::fixArgs($query);
+
+        $loader = new Auth_OpenID_ServiceEndpointLoader();
+        $endpoint_data = $this->session->get($this->_token_key);
+        $endpoint =
+            $loader->fromSession($endpoint_data);
+
+        if ($endpoint === null) {
+            $response = new Auth_OpenID_FailureResponse(null,
+                                                   'No session state found');
+        } else {
+            $response = $this->consumer->complete($query, $endpoint);
+            $this->session->del($this->_token_key);
+        }
+
+        if (in_array($response->status, array(Auth_OpenID_SUCCESS,
+                                              Auth_OpenID_CANCEL))) {
+            if ($response->identity_url !== null) {
+                $disco = new Services_Yadis_Discovery($this->session,
+                                                  $response->identity_url,
+                                                  $this->session_key_prefix);
+                $disco->cleanup();
+            }
+        }
+
+        return $response;
+    }
+}
+
+class Auth_OpenID_DiffieHellmanConsumerSession {
+    var $session_type = 'DH-SHA1';
+
+    function Auth_OpenID_DiffieHellmanConsumerSession($dh = null)
+    {
+        if ($dh === null) {
+            $dh = new Auth_OpenID_DiffieHellman();
+        }
+
+        $this->dh = $dh;
+    }
+
+    function getRequest()
+    {
+        $math =& Auth_OpenID_getMathLib();
+
+        $cpub = $math->longToBase64($this->dh->public);
+
+        $args = array('openid.dh_consumer_public' => $cpub);
+
+        if (!$this->dh->usingDefaultValues()) {
+            $args = array_merge($args, array(
+                'openid.dh_modulus' =>
+                     $math->longToBase64($this->dh->mod),
+                'openid.dh_gen' =>
+                $math->longToBase64($this->dh->gen)));
+        }
+
+        return $args;
+    }
+
+    function extractSecret($response)
+    {
+        if (!array_key_exists('dh_server_public', $response)) {
+            return null;
+        }
+
+        if (!array_key_exists('enc_mac_key', $response)) {
+            return null;
+        }
+
+        $math =& Auth_OpenID_getMathLib();
+        $spub = $math->base64ToLong($response['dh_server_public']);
+        $enc_mac_key = base64_decode($response['enc_mac_key']);
+
+        return $this->dh->xorSecret($spub, $enc_mac_key);
+    }
+}
+
+class Auth_OpenID_PlainTextConsumerSession {
+    var $session_type = null;
+
+    function getRequest()
+    {
+        return array();
+    }
+
+    function extractSecret($response)
+    {
+        if (!array_key_exists('mac_key', $response)) {
+            return null;
+        }
+
+        return base64_decode($response['mac_key']);
+    }
+}
+
+/**
+ * This class is the interface to the OpenID consumer logic.
+ * Instances of it maintain no per-request state, so they can be
+ * reused (or even used by multiple threads concurrently) as needed.
+ *
+ * @package OpenID
+ * @access private
+ */
+class Auth_OpenID_GenericConsumer {
+    /**
+     * This consumer's store object.
+     */
+    var $store;
+
+    /**
+     * @access private
+     */
+    var $_use_assocs;
+
+    /**
+     * This is the number of characters in the generated nonce for
+     * each transaction.
+     */
+    var $nonce_len = 8;
+
+    /**
+     * What characters are allowed in nonces
+     */
+    var $nonce_chrs = Auth_OpenID_DEFAULT_NONCE_CHRS;
+
+    /**
+     * This method initializes a new {@link Auth_OpenID_Consumer}
+     * instance to access the library.
+     *
+     * @param Auth_OpenID_OpenIDStore $store This must be an object
+     * that implements the interface in {@link Auth_OpenID_OpenIDStore}.
+     * Several concrete implementations are provided, to cover most common use
+     * cases.  For stores backed by MySQL, PostgreSQL, or SQLite, see
+     * the {@link Auth_OpenID_SQLStore} class and its sublcasses.  For a
+     * filesystem-backed store, see the {@link Auth_OpenID_FileStore} module.
+     * As a last resort, if it isn't possible for the server to store
+     * state at all, an instance of {@link Auth_OpenID_DumbStore} can be used.
+     *
+     * @param bool $immediate This is an optional boolean value.  It
+     * controls whether the library uses immediate mode, as explained
+     * in the module description.  The default value is False, which
+     * disables immediate mode.
+     */
+    function Auth_OpenID_GenericConsumer(&$store)
+    {
+        $this->store =& $store;
+        $this->_use_assocs =
+            !(defined('Auth_OpenID_NO_MATH_SUPPORT') ||
+              ($this->store && $this->store->isDumb()));
+
+        $this->fetcher = Services_Yadis_Yadis::getHTTPFetcher();
+    }
+
+    function begin($service_endpoint)
+    {
+        $nonce = $this->_createNonce();
+        $assoc = $this->_getAssociation($service_endpoint->server_url);
+        $r = new Auth_OpenID_AuthRequest($assoc, $service_endpoint);
+        $r->return_to_args['nonce'] = $nonce;
+        return $r;
+    }
+
+    function complete($query, $endpoint)
+    {
+        $mode = Auth_OpenID::arrayGet($query, 'openid.mode',
+                                      '<no mode specified>');
+
+        if ($mode == Auth_OpenID_CANCEL) {
+            return new Auth_OpenID_CancelResponse($endpoint);
+        } else if ($mode == 'error') {
+            $error = Auth_OpenID::arrayGet($query, 'openid.error');
+            return new Auth_OpenID_FailureResponse($endpoint, $error);
+        } else if ($mode == 'id_res') {
+            if ($endpoint->identity_url === null) {
+                return new Auth_OpenID_FailureResponse($identity_url,
+                                               "No session state found");
+            }
+
+            $response = $this->_doIdRes($query, $endpoint);
+
+            if ($response === null) {
+                return new Auth_OpenID_FailureResponse($endpoint,
+                                                       "HTTP request failed");
+            }
+            if ($response->status == Auth_OpenID_SUCCESS) {
+                return $this->_checkNonce($response,
+                                          Auth_OpenID::arrayGet($query,
+                                                                'nonce'));
+            } else {
+                return $response;
+            }
+        } else {
+            return new Auth_OpenID_FailureResponse($endpoint,
+                                           sprintf("Invalid openid.mode '%s'",
+                                                   $mode));
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function _doIdRes($query, $endpoint)
+    {
+        $user_setup_url = Auth_OpenID::arrayGet($query,
+                                                'openid.user_setup_url');
+
+        if ($user_setup_url !== null) {
+            return new Auth_OpenID_SetupNeededResponse($endpoint,
+                                                       $user_setup_url);
+        }
+
+        $return_to = Auth_OpenID::arrayGet($query, 'openid.return_to', null);
+        $server_id2 = Auth_OpenID::arrayGet($query, 'openid.identity', null);
+        $assoc_handle = Auth_OpenID::arrayGet($query,
+                                             'openid.assoc_handle', null);
+
+        if (($return_to === null) ||
+            ($server_id2 === null) ||
+            ($assoc_handle === null)) {
+            return new Auth_OpenID_FailureResponse($endpoint,
+                                                   "Missing required field");
+        }
+
+        if ($endpoint->getServerID() != $server_id2) {
+            return new Auth_OpenID_FailureResponse($endpoint,
+                                             "Server ID (delegate) mismatch");
+        }
+
+        $signed = Auth_OpenID::arrayGet($query, 'openid.signed');
+
+        $assoc = $this->store->getAssociation($endpoint->server_url,
+                                              $assoc_handle);
+
+        if ($assoc === null) {
+            // It's not an association we know about.  Dumb mode is
+            // our only possible path for recovery.
+            if ($this->_checkAuth($query, $endpoint->server_url)) {
+                return new Auth_OpenID_SuccessResponse($endpoint, $query,
+                                                       $signed);
+            } else {
+                return new Auth_OpenID_FailureResponse($endpoint,
+                                       "Server denied check_authentication");
+            }
+        }
+
+        if ($assoc->getExpiresIn() <= 0) {
+            $msg = sprintf("Association with %s expired",
+                           $endpoint->server_url);
+            return new Auth_OpenID_FailureResponse($endpoint, $msg);
+        }
+
+        // Check the signature
+        $sig = Auth_OpenID::arrayGet($query, 'openid.sig', null);
+        if (($sig === null) ||
+            ($signed === null)) {
+            return new Auth_OpenID_FailureResponse($endpoint,
+                                               "Missing argument signature");
+        }
+
+        $signed_list = explode(",", $signed);
+
+        //Fail if the identity field is present but not signed
+        if (($endpoint->identity_url !== null) &&
+            (!in_array('identity', $signed_list))) {
+            $msg = '"openid.identity" not signed';
+            return new Auth_OpenID_FailureResponse($endpoint, $msg);
+        }
+
+        $v_sig = $assoc->signDict($signed_list, $query);
+
+        if ($v_sig != $sig) {
+            return new Auth_OpenID_FailureResponse($endpoint,
+                                                   "Bad signature");
+        }
+
+        return Auth_OpenID_SuccessResponse::fromQuery($endpoint,
+                                                      $query, $signed);
+    }
+
+    /**
+     * @access private
+     */
+    function _checkAuth($query, $server_url)
+    {
+        $request = $this->_createCheckAuthRequest($query);
+        if ($request === null) {
+            return false;
+        }
+
+        $response = $this->_makeKVPost($request, $server_url);
+        if ($response == null) {
+            return false;
+        }
+
+        return $this->_processCheckAuthResponse($response, $server_url);
+    }
+
+    /**
+     * @access private
+     */
+    function _createCheckAuthRequest($query)
+    {
+        $signed = Auth_OpenID::arrayGet($query, 'openid.signed', null);
+        if ($signed === null) {
+            return null;
+        }
+
+        $whitelist = array('assoc_handle', 'sig',
+                           'signed', 'invalidate_handle');
+
+        $signed = array_merge(explode(",", $signed), $whitelist);
+
+        $check_args = array();
+
+        foreach ($query as $key => $value) {
+            if (in_array(substr($key, 7), $signed)) {
+                $check_args[$key] = $value;
+            }
+        }
+
+        $check_args['openid.mode'] = 'check_authentication';
+        return $check_args;
+    }
+
+    /**
+     * @access private
+     */
+    function _processCheckAuthResponse($response, $server_url)
+    {
+        $is_valid = Auth_OpenID::arrayGet($response, 'is_valid', 'false');
+
+        $invalidate_handle = Auth_OpenID::arrayGet($response,
+                                                   'invalidate_handle');
+
+        if ($invalidate_handle !== null) {
+            $this->store->removeAssociation($server_url,
+                                            $invalidate_handle);
+        }
+
+        if ($is_valid == 'true') {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @access private
+     */
+    function _makeKVPost($args, $server_url)
+    {
+        $mode = $args['openid.mode'];
+
+        $pairs = array();
+        foreach ($args as $k => $v) {
+            $v = urlencode($v);
+            $pairs[] = "$k=$v";
+        }
+
+        $body = implode("&", $pairs);
+
+        $resp = $this->fetcher->post($server_url, $body);
+
+        if ($resp === null) {
+            return null;
+        }
+
+        $response = Auth_OpenID_KVForm::toArray($resp->body);
+
+        if ($resp->status == 400) {
+            return null;
+        } else if ($resp->status != 200) {
+            return null;
+        }
+
+        return $response;
+    }
+
+    /**
+     * @access private
+     */
+    function _checkNonce($response, $nonce)
+    {
+        $parsed_url = parse_url($response->getReturnTo());
+        $query_str = @$parsed_url['query'];
+        $query = array();
+        parse_str($query_str, $query);
+
+        $found = false;
+
+        foreach ($query as $k => $v) {
+            if ($k == 'nonce') {
+                if ($v != $nonce) {
+                    return new Auth_OpenID_FailureResponse($response,
+                                                           "Nonce mismatch");
+                } else {
+                    $found = true;
+                    break;
+                }
+            }
+        }
+
+        if (!$found) {
+            return new Auth_OpenID_FailureResponse($response,
+                                 sprintf("Nonce missing from return_to: %s",
+                                         $response->getReturnTo()));
+        }
+
+        if (!$this->store->useNonce($nonce)) {
+            return new Auth_OpenID_FailureResponse($response,
+                                                   "Nonce missing from store");
+        }
+
+        return $response;
+    }
+
+    /**
+     * @access private
+     */
+    function _createNonce()
+    {
+        $nonce = Auth_OpenID_CryptUtil::randomString($this->nonce_len,
+                                                     $this->nonce_chrs);
+        $this->store->storeNonce($nonce);
+        return $nonce;
+    }
+
+    /**
+     * @access protected
+     */
+    function _createDiffieHellman()
+    {
+        return new Auth_OpenID_DiffieHellman();
+    }
+
+    /**
+     * @access private
+     */
+    function _getAssociation($server_url)
+    {
+        if (!$this->_use_assocs) {
+            return null;
+        }
+
+        $assoc = $this->store->getAssociation($server_url);
+
+        if (($assoc === null) ||
+            ($assoc->getExpiresIn() <= 0)) {
+
+            $parts = $this->_createAssociateRequest($server_url);
+
+            if ($parts === null) {
+                return null;
+            }
+
+            list($assoc_session, $args) = $parts;
+
+            $response = $this->_makeKVPost($args, $server_url);
+
+            if ($response === null) {
+                $assoc = null;
+            } else {
+                $assoc = $this->_parseAssociation($response, $assoc_session,
+                                                  $server_url);
+            }
+        }
+
+        return $assoc;
+    }
+
+    function _createAssociateRequest($server_url)
+    {
+        $parts = parse_url($server_url);
+
+        if ($parts === false) {
+            return null;
+        }
+
+        if (array_key_exists('scheme', $parts)) {
+            $proto = $parts['scheme'];
+        } else {
+            $proto = 'http';
+        }
+
+        if ($proto == 'https') {
+            $assoc_session = new Auth_OpenID_PlainTextConsumerSession();
+        } else {
+            $assoc_session = new Auth_OpenID_DiffieHellmanConsumerSession();
+        }
+
+        $args = array(
+            'openid.mode' => 'associate',
+            'openid.assoc_type' => 'HMAC-SHA1');
+
+        if ($assoc_session->session_type !== null) {
+            $args['openid.session_type'] = $assoc_session->session_type;
+        }
+
+        $args = array_merge($args, $assoc_session->getRequest());
+        return array($assoc_session, $args);
+    }
+
+    /**
+     * @access private
+     */
+    function _parseAssociation($results, $assoc_session, $server_url)
+    {
+        $required_keys = array('assoc_type', 'assoc_handle',
+                               'expires_in');
+
+        foreach ($required_keys as $key) {
+            if (!array_key_exists($key, $results)) {
+                return null;
+            }
+        }
+
+        $assoc_type = $results['assoc_type'];
+        $assoc_handle = $results['assoc_handle'];
+        $expires_in_str = $results['expires_in'];
+
+        if ($assoc_type != 'HMAC-SHA1') {
+            return null;
+        }
+
+        $expires_in = intval($expires_in_str);
+
+        if ($expires_in <= 0) {
+            return null;
+        }
+
+        $session_type = Auth_OpenID::arrayGet($results, 'session_type');
+        if ($session_type != $assoc_session->session_type) {
+            if ($session_type === null) {
+                $assoc_session = new Auth_OpenID_PlainTextConsumerSession();
+            } else {
+                return null;
+            }
+        }
+
+        $secret = $assoc_session->extractSecret($results);
+
+        if (!$secret) {
+            return null;
+        }
+
+        $assoc = Auth_OpenID_Association::fromExpiresIn(
+                         $expires_in, $assoc_handle, $secret, $assoc_type);
+        $this->store->storeAssociation($server_url, $assoc);
+
+        return $assoc;
+    }
+}
+
+/**
+ * This class represents an authentication request from a consumer to
+ * an OpenID server.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AuthRequest {
+
+    /**
+     * Initialize an authentication request with the specified token,
+     * association, and endpoint.
+     *
+     * Users of this library should not create instances of this
+     * class.  Instances of this class are created by the library when
+     * needed.
+     */
+    function Auth_OpenID_AuthRequest($assoc, $endpoint)
+    {
+        $this->assoc = $assoc;
+        $this->endpoint = $endpoint;
+        $this->extra_args = array();
+        $this->return_to_args = array();
+    }
+
+    /**
+     * Add an extension argument to this OpenID authentication
+     * request.
+     *
+     * Use caution when adding arguments, because they will be
+     * URL-escaped and appended to the redirect URL, which can easily
+     * get quite long.
+     *
+     * @param string $namespace The namespace for the extension. For
+     * example, the simple registration extension uses the namespace
+     * 'sreg'.
+     *
+     * @param string $key The key within the extension namespace. For
+     * example, the nickname field in the simple registration
+     * extension's key is 'nickname'.
+     *
+     * @param string $value The value to provide to the server for
+     * this argument.
+     */
+    function addExtensionArg($namespace, $key, $value)
+    {
+        $arg_name = implode('.', array('openid', $namespace, $key));
+        $this->extra_args[$arg_name] = $value;
+    }
+
+    /**
+     * Compute the appropriate redirection URL for this request based
+     * on a specified trust root and return-to.
+     *
+     * @param string $trust_root The trust root URI for your
+     * application.
+     *
+     * @param string$ $return_to The return-to URL to be used when the
+     * OpenID server redirects the user back to your site.
+     *
+     * @return string $redirect_url The resulting redirect URL that
+     * you should send to the user agent.
+     */
+    function redirectURL($trust_root, $return_to, $immediate=false)
+    {
+        if ($immediate) {
+            $mode = 'checkid_immediate';
+        } else {
+            $mode = 'checkid_setup';
+        }
+
+        $return_to = Auth_OpenID::appendArgs($return_to, $this->return_to_args);
+
+        $redir_args = array(
+            'openid.mode' => $mode,
+            'openid.identity' => $this->endpoint->getServerID(),
+            'openid.return_to' => $return_to,
+            'openid.trust_root' => $trust_root);
+
+        if ($this->assoc) {
+            $redir_args['openid.assoc_handle'] = $this->assoc->handle;
+        }
+
+        $redir_args = array_merge($redir_args, $this->extra_args);
+
+        return Auth_OpenID::appendArgs($this->endpoint->server_url,
+                                       $redir_args);
+    }
+}
+
+/**
+ * The base class for responses from the Auth_OpenID_Consumer.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_ConsumerResponse {
+    var $status = null;
+}
+
+/**
+ * A response with a status of Auth_OpenID_SUCCESS. Indicates that
+ * this request is a successful acknowledgement from the OpenID server
+ * that the supplied URL is, indeed controlled by the requesting
+ * agent.  This has three relevant attributes:
+ *
+ * identity_url - The identity URL that has been authenticated
+ *
+ * signed_args - The arguments in the server's response that were
+ * signed and verified.
+ *
+ * status - Auth_OpenID_SUCCESS.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SuccessResponse extends Auth_OpenID_ConsumerResponse {
+    var $status = Auth_OpenID_SUCCESS;
+
+    /**
+     * @access private
+     */
+    function Auth_OpenID_SuccessResponse($endpoint, $signed_args)
+    {
+        $this->endpoint = $endpoint;
+        $this->identity_url = $endpoint->identity_url;
+        $this->signed_args = $signed_args;
+    }
+
+    /**
+     * @access private
+     */
+    function fromQuery($endpoint, $query, $signed)
+    {
+        $signed_args = array();
+        foreach (explode(",", $signed) as $field_name) {
+            $field_name = 'openid.' . $field_name;
+            $signed_args[$field_name] = Auth_OpenID::arrayGet($query,
+                                                              $field_name, '');
+        }
+        return new Auth_OpenID_SuccessResponse($endpoint, $signed_args);
+    }
+
+    /**
+     * Extract signed extension data from the server's response.
+     *
+     * @param string $prefix The extension namespace from which to
+     * extract the extension data.
+     */
+    function extensionResponse($prefix)
+    {
+        $response = array();
+        $prefix = sprintf('openid.%s.', $prefix);
+        $prefix_len = strlen($prefix);
+        foreach ($this->signed_args as $k => $v) {
+            if (strpos($k, $prefix) === 0) {
+                $response_key = substr($k, $prefix_len);
+                $response[$response_key] = $v;
+            }
+        }
+
+        return $response;
+    }
+
+    /**
+     * Get the openid.return_to argument from this response.
+     *
+     * This is useful for verifying that this request was initiated by
+     * this consumer.
+     *
+     * @return string $return_to The return_to URL supplied to the
+     * server on the initial request, or null if the response did not
+     * contain an 'openid.return_to' argument.
+    */
+    function getReturnTo()
+    {
+        return Auth_OpenID::arrayGet($this->signed_args, 'openid.return_to');
+    }
+}
+
+/**
+ * A response with a status of Auth_OpenID_FAILURE. Indicates that the
+ * OpenID protocol has failed. This could be locally or remotely
+ * triggered.  This has three relevant attributes:
+ *
+ * identity_url - The identity URL for which authentication was
+ * attempted, if it can be determined.  Otherwise, null.
+ *
+ * message - A message indicating why the request failed, if one is
+ * supplied.  Otherwise, null.
+ *
+ * status - Auth_OpenID_FAILURE.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_FailureResponse extends Auth_OpenID_ConsumerResponse {
+    var $status = Auth_OpenID_FAILURE;
+
+    function Auth_OpenID_FailureResponse($endpoint, $message = null)
+    {
+        $this->endpoint = $endpoint;
+        if ($endpoint !== null) {
+            $this->identity_url = $endpoint->identity_url;
+        } else {
+            $this->identity_url = null;
+        }
+        $this->message = $message;
+    }
+}
+
+/**
+ * A response with a status of Auth_OpenID_CANCEL. Indicates that the
+ * user cancelled the OpenID authentication request.  This has two
+ * relevant attributes:
+ *
+ * identity_url - The identity URL for which authentication was
+ * attempted, if it can be determined.  Otherwise, null.
+ *
+ * status - Auth_OpenID_SUCCESS.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_CancelResponse extends Auth_OpenID_ConsumerResponse {
+    var $status = Auth_OpenID_CANCEL;
+
+    function Auth_OpenID_CancelResponse($endpoint)
+    {
+        $this->endpoint = $endpoint;
+        $this->identity_url = $endpoint->identity_url;
+    }
+}
+
+/**
+ * A response with a status of Auth_OpenID_SETUP_NEEDED. Indicates
+ * that the request was in immediate mode, and the server is unable to
+ * authenticate the user without further interaction.
+ *
+ * identity_url - The identity URL for which authentication was
+ * attempted.
+ *
+ * setup_url - A URL that can be used to send the user to the server
+ * to set up for authentication. The user should be redirected in to
+ * the setup_url, either in the current window or in a new browser
+ * window.
+ *
+ * status - Auth_OpenID_SETUP_NEEDED.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SetupNeededResponse extends Auth_OpenID_ConsumerResponse {
+    var $status = Auth_OpenID_SETUP_NEEDED;
+
+    function Auth_OpenID_SetupNeededResponse($endpoint,
+                                             $setup_url = null)
+    {
+        $this->endpoint = $endpoint;
+        $this->identity_url = $endpoint->identity_url;
+        $this->setup_url = $setup_url;
+    }
+}
+
+?>
diff --git a/lib/Auth/OpenID/CryptUtil.php b/lib/Auth/OpenID/CryptUtil.php
new file mode 100644
index 000000000..8d7e06983
--- /dev/null
+++ b/lib/Auth/OpenID/CryptUtil.php
@@ -0,0 +1,109 @@
+<?php
+
+/**
+ * CryptUtil: A suite of wrapper utility functions for the OpenID
+ * library.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+if (!defined('Auth_OpenID_RAND_SOURCE')) {
+    /**
+     * The filename for a source of random bytes. Define this yourself
+     * if you have a different source of randomness.
+     */
+    define('Auth_OpenID_RAND_SOURCE', '/dev/urandom');
+}
+
+class Auth_OpenID_CryptUtil {
+    /**
+     * Get the specified number of random bytes.
+     *
+     * Attempts to use a cryptographically secure (not predictable)
+     * source of randomness if available. If there is no high-entropy
+     * randomness source available, it will fail. As a last resort,
+     * for non-critical systems, define
+     * <code>Auth_OpenID_RAND_SOURCE</code> as <code>null</code>, and
+     * the code will fall back on a pseudo-random number generator.
+     *
+     * @param int $num_bytes The length of the return value
+     * @return string $bytes random bytes
+     */
+    function getBytes($num_bytes)
+    {
+        static $f = null;
+        $bytes = '';
+        if ($f === null) {
+            if (Auth_OpenID_RAND_SOURCE === null) {
+                $f = false;
+            } else {
+                $f = @fopen(Auth_OpenID_RAND_SOURCE, "r");
+                if ($f === false) {
+                    $msg = 'Define Auth_OpenID_RAND_SOURCE as null to ' .
+                        ' continue with an insecure random number generator.';
+                    trigger_error($msg, E_USER_ERROR);
+                }
+            }
+        }
+        if ($f === false) {
+            // pseudorandom used
+            $bytes = '';
+            for ($i = 0; $i < $num_bytes; $i += 4) {
+                $bytes .= pack('L', mt_rand());
+            }
+            $bytes = substr($bytes, 0, $num_bytes);
+        } else {
+            $bytes = fread($f, $num_bytes);
+        }
+        return $bytes;
+    }
+
+    /**
+     * Produce a string of length random bytes, chosen from chrs.  If
+     * $chrs is null, the resulting string may contain any characters.
+     *
+     * @param integer $length The length of the resulting
+     * randomly-generated string
+     * @param string $chrs A string of characters from which to choose
+     * to build the new string
+     * @return string $result A string of randomly-chosen characters
+     * from $chrs
+     */
+    function randomString($length, $population = null)
+    {
+        if ($population === null) {
+            return Auth_OpenID_CryptUtil::getBytes($length);
+        }
+
+        $popsize = strlen($population);
+
+        if ($popsize > 256) {
+            $msg = 'More than 256 characters supplied to ' . __FUNCTION__;
+            trigger_error($msg, E_USER_ERROR);
+        }
+
+        $duplicate = 256 % $popsize;
+
+        $str = "";
+        for ($i = 0; $i < $length; $i++) {
+            do {
+                $n = ord(Auth_OpenID_CryptUtil::getBytes(1));
+            } while ($n < $duplicate);
+
+            $n %= $popsize;
+            $str .= $population[$n];
+        }
+
+        return $str;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/DatabaseConnection.php b/lib/Auth/OpenID/DatabaseConnection.php
new file mode 100644
index 000000000..3f4515fa5
--- /dev/null
+++ b/lib/Auth/OpenID/DatabaseConnection.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * The Auth_OpenID_DatabaseConnection class, which is used to emulate
+ * a PEAR database connection.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * An empty base class intended to emulate PEAR connection
+ * functionality in applications that supply their own database
+ * abstraction mechanisms.  See {@link Auth_OpenID_SQLStore} for more
+ * information.  You should subclass this class if you need to create
+ * an SQL store that needs to access its database using an
+ * application's database abstraction layer instead of a PEAR database
+ * connection.  Any subclass of Auth_OpenID_DatabaseConnection MUST
+ * adhere to the interface specified here.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_DatabaseConnection {
+    /**
+     * Sets auto-commit mode on this database connection.
+     *
+     * @param bool $mode True if auto-commit is to be used; false if
+     * not.
+     */
+    function autoCommit($mode)
+    {
+    }
+
+    /**
+     * Run an SQL query with the specified parameters, if any.
+     *
+     * @param string $sql An SQL string with placeholders.  The
+     * placeholders are assumed to be specific to the database engine
+     * for this connection.
+     *
+     * @param array $params An array of parameters to insert into the
+     * SQL string using this connection's escaping mechanism.
+     *
+     * @return mixed $result The result of calling this connection's
+     * internal query function.  The type of result depends on the
+     * underlying database engine.  This method is usually used when
+     * the result of a query is not important, like a DDL query.
+     */
+    function query($sql, $params = array())
+    {
+    }
+
+    /**
+     * Starts a transaction on this connection, if supported.
+     */
+    function begin()
+    {
+    }
+
+    /**
+     * Commits a transaction on this connection, if supported.
+     */
+    function commit()
+    {
+    }
+
+    /**
+     * Performs a rollback on this connection, if supported.
+     */
+    function rollback()
+    {
+    }
+
+    /**
+     * Run an SQL query and return the first column of the first row
+     * of the result set, if any.
+     *
+     * @param string $sql An SQL string with placeholders.  The
+     * placeholders are assumed to be specific to the database engine
+     * for this connection.
+     *
+     * @param array $params An array of parameters to insert into the
+     * SQL string using this connection's escaping mechanism.
+     *
+     * @return mixed $result The value of the first column of the
+     * first row of the result set.  False if no such result was
+     * found.
+     */
+    function getOne($sql, $params = array())
+    {
+    }
+
+    /**
+     * Run an SQL query and return the first row of the result set, if
+     * any.
+     *
+     * @param string $sql An SQL string with placeholders.  The
+     * placeholders are assumed to be specific to the database engine
+     * for this connection.
+     *
+     * @param array $params An array of parameters to insert into the
+     * SQL string using this connection's escaping mechanism.
+     *
+     * @return array $result The first row of the result set, if any,
+     * keyed on column name.  False if no such result was found.
+     */
+    function getRow($sql, $params = array())
+    {
+    }
+
+    /**
+     * Run an SQL query with the specified parameters, if any.
+     *
+     * @param string $sql An SQL string with placeholders.  The
+     * placeholders are assumed to be specific to the database engine
+     * for this connection.
+     *
+     * @param array $params An array of parameters to insert into the
+     * SQL string using this connection's escaping mechanism.
+     *
+     * @return array $result An array of arrays representing the
+     * result of the query; each array is keyed on column name.
+     */
+    function getAll($sql, $params = array())
+    {
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/DiffieHellman.php b/lib/Auth/OpenID/DiffieHellman.php
new file mode 100644
index 000000000..2b845b78b
--- /dev/null
+++ b/lib/Auth/OpenID/DiffieHellman.php
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * The OpenID library's Diffie-Hellman implementation.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+require_once 'Auth/OpenID/BigMath.php';
+require_once 'Auth/OpenID/HMACSHA1.php';
+
+function Auth_OpenID_getDefaultMod()
+{
+    return '155172898181473697471232257763715539915724801'.
+        '966915404479707795314057629378541917580651227423'.
+        '698188993727816152646631438561595825688188889951'.
+        '272158842675419950341258706556549803580104870537'.
+        '681476726513255747040765857479291291572334510643'.
+        '245094715007229621094194349783925984760375594985'.
+        '848253359305585439638443';
+}
+
+function Auth_OpenID_getDefaultGen()
+{
+    return '2';
+}
+
+/**
+ * The Diffie-Hellman key exchange class.  This class relies on
+ * {@link Auth_OpenID_MathLibrary} to perform large number operations.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_DiffieHellman {
+
+    var $mod;
+    var $gen;
+    var $private;
+    var $lib = null;
+
+    function Auth_OpenID_DiffieHellman($mod = null, $gen = null,
+                                       $private = null, $lib = null)
+    {
+        if ($lib === null) {
+            $this->lib =& Auth_OpenID_getMathLib();
+        } else {
+            $this->lib =& $lib;
+        }
+
+        if ($mod === null) {
+            $this->mod = $this->lib->init(Auth_OpenID_getDefaultMod());
+        } else {
+            $this->mod = $mod;
+        }
+
+        if ($gen === null) {
+            $this->gen = $this->lib->init(Auth_OpenID_getDefaultGen());
+        } else {
+            $this->gen = $gen;
+        }
+
+        if ($private === null) {
+            $r = $this->lib->rand($this->mod);
+            $this->private = $this->lib->add($r, 1);
+        } else {
+            $this->private = $private;
+        }
+
+        $this->public = $this->lib->powmod($this->gen, $this->private,
+                                           $this->mod);
+    }
+
+    function getSharedSecret($composite)
+    {
+        return $this->lib->powmod($composite, $this->private, $this->mod);
+    }
+
+    function getPublicKey()
+    {
+        return $this->public;
+    }
+
+    /**
+     * Generate the arguments for an OpenID Diffie-Hellman association
+     * request
+     */
+    function getAssocArgs()
+    {
+        $cpub = $this->lib->longToBase64($this->getPublicKey());
+        $args = array(
+                      'openid.dh_consumer_public' => $cpub,
+                      'openid.session_type' => 'DH-SHA1'
+                      );
+
+        if ($this->lib->cmp($this->mod, Auth_OpenID_getDefaultMod()) ||
+            $this->lib->cmp($this->gen, Auth_OpenID_getDefaultGen())) {
+            $args['openid.dh_modulus'] = $this->lib->longToBase64($this->mod);
+            $args['openid.dh_gen'] = $this->lib->longToBase64($this->gen);
+        }
+
+        return $args;
+    }
+
+    function usingDefaultValues()
+    {
+        return ($this->mod == Auth_OpenID_getDefaultMod() &&
+                $this->gen == Auth_OpenID_getDefaultGen());
+    }
+
+    /**
+     * Perform the server side of the OpenID Diffie-Hellman association
+     */
+    function serverAssociate($consumer_args, $assoc_secret)
+    {
+        $lib =& Auth_OpenID_getMathLib();
+
+        if (isset($consumer_args['openid.dh_modulus'])) {
+            $mod = $lib->base64ToLong($consumer_args['openid.dh_modulus']);
+        } else {
+            $mod = null;
+        }
+
+        if (isset($consumer_args['openid.dh_gen'])) {
+            $gen = $lib->base64ToLong($consumer_args['openid.dh_gen']);
+        } else {
+            $gen = null;
+        }
+        
+        $cpub64 = @$consumer_args['openid.dh_consumer_public'];
+        if (!isset($cpub64)) {
+            return false;
+        }
+
+        $dh = new Auth_OpenID_DiffieHellman($mod, $gen);
+        $cpub = $lib->base64ToLong($cpub64);
+        $mac_key = $dh->xorSecret($cpub, $assoc_secret);
+        $enc_mac_key = base64_encode($mac_key);
+        $spub64 = $lib->longToBase64($dh->getPublicKey());
+
+        $server_args = array(
+                             'session_type' => 'DH-SHA1',
+                             'dh_server_public' => $spub64,
+                             'enc_mac_key' => $enc_mac_key
+                             );
+
+        return $server_args;
+    }
+
+    function consumerFinish($reply)
+    {
+        $spub = $this->lib->base64ToLong($reply['dh_server_public']);
+        if ($this->lib->cmp($spub, 0) <= 0) {
+            return false;
+        }
+        $enc_mac_key = base64_decode($reply['enc_mac_key']);
+        return $this->xorSecret($spub, $enc_mac_key);
+    }
+
+    function xorSecret($composite, $secret)
+    {
+        $dh_shared = $this->getSharedSecret($composite);
+        $dh_shared_str = $this->lib->longToBinary($dh_shared);
+        $sha1_dh_shared = Auth_OpenID_SHA1($dh_shared_str);
+
+        $xsecret = "";
+        for ($i = 0; $i < strlen($secret); $i++) {
+            $xsecret .= chr(ord($secret[$i]) ^ ord($sha1_dh_shared[$i]));
+        }
+
+        return $xsecret;
+    }
+}
diff --git a/lib/Auth/OpenID/Discover.php b/lib/Auth/OpenID/Discover.php
new file mode 100644
index 000000000..d87d47d16
--- /dev/null
+++ b/lib/Auth/OpenID/Discover.php
@@ -0,0 +1,258 @@
+<?php
+
+/**
+ * The OpenID and Yadis discovery implementation for OpenID 1.2.
+ */
+
+require_once "Auth/OpenID.php";
+require_once "Auth/OpenID/Parse.php";
+require_once "Services/Yadis/XRIRes.php";
+require_once "Services/Yadis/Yadis.php";
+
+define('_OPENID_1_0_NS', 'http://openid.net/xmlns/1.0');
+define('_OPENID_1_2_TYPE', 'http://openid.net/signon/1.2');
+define('_OPENID_1_1_TYPE', 'http://openid.net/signon/1.1');
+define('_OPENID_1_0_TYPE', 'http://openid.net/signon/1.0');
+
+/**
+ * Object representing an OpenID service endpoint.
+ */
+class Auth_OpenID_ServiceEndpoint {
+    function Auth_OpenID_ServiceEndpoint()
+    {
+        $this->identity_url = null;
+        $this->server_url = null;
+        $this->type_uris = array();
+        $this->delegate = null;
+        $this->canonicalID = null;
+        $this->used_yadis = false; // whether this came from an XRDS
+    }
+
+    function usesExtension($extension_uri)
+    {
+        return in_array($extension_uri, $this->type_uris);
+    }
+
+    function parseService($yadis_url, $uri, $type_uris, $service_element)
+    {
+        // Set the state of this object based on the contents of the
+        // service element.
+        $this->type_uris = $type_uris;
+        $this->identity_url = $yadis_url;
+        $this->server_url = $uri;
+        $this->delegate = Auth_OpenID_ServiceEndpoint::findDelegate(
+                                                         $service_element);
+        $this->used_yadis = true;
+    }
+
+    function findDelegate($service)
+    {
+        // Extract a openid:Delegate value from a Yadis Service
+        // element.  If no delegate is found, returns null.
+
+        // Try to register new namespace.
+        $service->parser->registerNamespace('openid',
+                                            'http://openid.net/xmlns/1.0');
+
+        // XXX: should this die if there is more than one delegate
+        // element?
+        $delegates = $service->getElements("openid:Delegate");
+
+        if ($delegates) {
+            return $service->parser->content($delegates[0]);
+        } else {
+            return null;
+        }
+    }
+
+    function getServerID()
+    {
+        // Return the identifier that should be sent as the
+        // openid.identity_url parameter to the server.
+        if ($this->delegate === null) {
+            if ($this->canonicalID) {
+                return $this->canonicalID;
+            } else {
+                return $this->identity_url;
+            }
+        } else {
+            return $this->delegate;
+        }
+    }
+
+    function fromHTML($uri, $html)
+    {
+        // Parse the given document as HTML looking for an OpenID <link
+        // rel=...>
+        $urls = Auth_OpenID_legacy_discover($html);
+        if ($urls === false) {
+            return null;
+        }
+
+        list($delegate_url, $server_url) = $urls;
+
+        $service = new Auth_OpenID_ServiceEndpoint();
+        $service->identity_url = $uri;
+        $service->delegate = $delegate_url;
+        $service->server_url = $server_url;
+        $service->type_uris = array(_OPENID_1_0_TYPE);
+        return $service;
+    }
+}
+
+function filter_MatchesAnyOpenIDType(&$service)
+{
+    $uris = $service->getTypes();
+
+    foreach ($uris as $uri) {
+        if (in_array($uri,
+                     array(_OPENID_1_0_TYPE,
+                           _OPENID_1_1_TYPE,
+                           _OPENID_1_2_TYPE))) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+function Auth_OpenID_makeOpenIDEndpoints($uri, $endpoints)
+{
+    $s = array();
+
+    if (!$endpoints) {
+        return $s;
+    }
+
+    foreach ($endpoints as $service) {
+        $type_uris = $service->getTypes();
+        $uris = $service->getURIs();
+
+        // If any Type URIs match and there is an endpoint URI
+        // specified, then this is an OpenID endpoint
+        if ($type_uris &&
+            $uris) {
+
+            foreach ($uris as $service_uri) {
+                $openid_endpoint = new Auth_OpenID_ServiceEndpoint();
+                $openid_endpoint->parseService($uri,
+                                               $service_uri,
+                                               $type_uris,
+                                               $service);
+
+                $s[] = $openid_endpoint;
+            }
+        }
+    }
+
+    return $s;
+}
+
+function Auth_OpenID_discoverWithYadis($uri, &$fetcher)
+{
+    // Discover OpenID services for a URI. Tries Yadis and falls back
+    // on old-style <link rel='...'> discovery if Yadis fails.
+
+    // Might raise a yadis.discover.DiscoveryFailure if no document
+    // came back for that URI at all.  I don't think falling back to
+    // OpenID 1.0 discovery on the same URL will help, so don't bother
+    // to catch it.
+    $openid_services = array();
+
+    $http_response = null;
+    $response = Services_Yadis_Yadis::discover($uri, $http_response,
+                                                $fetcher);
+
+    if ($response) {
+        $identity_url = $response->uri;
+        $openid_services =
+            $response->xrds->services(array('filter_MatchesAnyOpenIDType'));
+    }
+
+    if (!$openid_services) {
+        return @Auth_OpenID_discoverWithoutYadis($uri,
+                                                 $fetcher);
+    }
+
+    if (!$openid_services) {
+        $body = $response->body;
+
+        // Try to parse the response as HTML to get OpenID 1.0/1.1
+        // <link rel="...">
+        $service = Auth_OpenID_ServiceEndpoint::fromHTML($identity_url,
+                                                         $body);
+
+        if ($service !== null) {
+            $openid_services = array($service);
+        }
+    } else {
+        $openid_services = Auth_OpenID_makeOpenIDEndpoints($response->uri,
+                                                           $openid_services);
+    }
+
+    return array($identity_url, $openid_services, $http_response);
+}
+
+function _Auth_OpenID_discoverServiceList($uri, &$fetcher)
+{
+    list($url, $services, $resp) = Auth_OpenID_discoverWithYadis($uri,
+                                                                 $fetcher);
+
+    return $services;
+}
+
+function _Auth_OpenID_discoverXRIServiceList($uri, &$fetcher)
+{
+    list($url, $services, $resp) = _Auth_OpenID_discoverXRI($uri,
+                                                            $fetcher);
+    return $services;
+}
+
+function Auth_OpenID_discoverWithoutYadis($uri, &$fetcher)
+{
+    $http_resp = @$fetcher->get($uri);
+
+    if ($http_resp->status != 200) {
+        return array(null, array(), $http_resp);
+    }
+
+    $identity_url = $http_resp->final_url;
+
+    // Try to parse the response as HTML to get OpenID 1.0/1.1 <link
+    // rel="...">
+    $endpoint =& new Auth_OpenID_ServiceEndpoint();
+    $service = $endpoint->fromHTML($identity_url, $http_resp->body);
+    if ($service === null) {
+        $openid_services = array();
+    } else {
+        $openid_services = array($service);
+    }
+
+    return array($identity_url, $openid_services, $http_resp);
+}
+
+function _Auth_OpenID_discoverXRI($iname, &$fetcher)
+{
+    $services = new Services_Yadis_ProxyResolver($fetcher);
+    list($canonicalID, $service_list) = $services->query($iname,
+                                                  array(_OPENID_1_0_TYPE,
+                                                        _OPENID_1_1_TYPE,
+                                                        _OPENID_1_2_TYPE),
+                                     array('filter_MatchesAnyOpenIDType'));
+
+    $endpoints = Auth_OpenID_makeOpenIDEndpoints($iname, $service_list);
+
+    for ($i = 0; $i < count($endpoints); $i++) {
+        $endpoints[$i]->canonicalID = $canonicalID;
+    }
+
+    // FIXME: returned xri should probably be in some normal form
+    return array($iname, $endpoints, null);
+}
+
+function Auth_OpenID_discover($uri, &$fetcher)
+{
+    return @Auth_OpenID_discoverWithYadis($uri, $fetcher);
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/DumbStore.php b/lib/Auth/OpenID/DumbStore.php
new file mode 100644
index 000000000..d4d8a8b42
--- /dev/null
+++ b/lib/Auth/OpenID/DumbStore.php
@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * This file supplies a dumb store backend for OpenID servers and
+ * consumers.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Import the interface for creating a new store class.
+ */
+require_once 'Auth/OpenID/Interface.php';
+require_once 'Auth/OpenID/HMACSHA1.php';
+
+/**
+ * This is a store for use in the worst case, when you have no way of
+ * saving state on the consumer site. Using this store makes the
+ * consumer vulnerable to replay attacks, as it's unable to use
+ * nonces. Avoid using this store if it is at all possible.
+ *
+ * Most of the methods of this class are implementation details.
+ * Users of this class need to worry only about the constructor.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_DumbStore extends Auth_OpenID_OpenIDStore {
+
+    /**
+     * Creates a new {@link Auth_OpenID_DumbStore} instance. For the security
+     * of the tokens generated by the library, this class attempts to
+     * at least have a secure implementation of getAuthKey.
+     *
+     * When you create an instance of this class, pass in a secret
+     * phrase. The phrase is hashed with sha1 to make it the correct
+     * length and form for an auth key. That allows you to use a long
+     * string as the secret phrase, which means you can make it very
+     * difficult to guess.
+     *
+     * Each {@link Auth_OpenID_DumbStore} instance that is created for use by
+     * your consumer site needs to use the same $secret_phrase.
+     *
+     * @param string secret_phrase The phrase used to create the auth
+     * key returned by getAuthKey
+     */
+    function Auth_OpenID_DumbStore($secret_phrase)
+    {
+        $this->auth_key = Auth_OpenID_SHA1($secret_phrase);
+    }
+
+    /**
+     * This implementation does nothing.
+     */
+    function storeAssociation($server_url, $association)
+    {
+    }
+
+    /**
+     * This implementation always returns null.
+     */
+    function getAssociation($server_url, $handle = null)
+    {
+        return null;
+    }
+
+    /**
+     * This implementation always returns false.
+     */
+    function removeAssociation($server_url, $handle)
+    {
+        return false;
+    }
+
+    /**
+     * This implementation does nothing.
+     */
+    function storeNonce($nonce)
+    {
+    }
+
+    /**
+     * In a system truly limited to dumb mode, nonces must all be
+     * accepted. This therefore always returns true, which makes
+     * replay attacks feasible.
+     */
+    function useNonce($nonce)
+    {
+        return true;
+    }
+
+    /**
+     * This method returns the auth key generated by the constructor.
+     */
+    function getAuthKey()
+    {
+        return $this->auth_key;
+    }
+
+    /**
+     * This store is a dumb mode store, so this method is overridden
+     * to return true.
+     */
+    function isDumb()
+    {
+        return true;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/FileStore.php b/lib/Auth/OpenID/FileStore.php
new file mode 100644
index 000000000..6ce88568e
--- /dev/null
+++ b/lib/Auth/OpenID/FileStore.php
@@ -0,0 +1,674 @@
+<?php
+
+/**
+ * This file supplies a Memcached store backend for OpenID servers and
+ * consumers.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ *
+ */
+
+/**
+ * Require base class for creating a new interface.
+ */
+require_once 'Auth/OpenID.php';
+require_once 'Auth/OpenID/Interface.php';
+require_once 'Auth/OpenID/HMACSHA1.php';
+
+/**
+ * This is a filesystem-based store for OpenID associations and
+ * nonces.  This store should be safe for use in concurrent systems on
+ * both windows and unix (excluding NFS filesystems).  There are a
+ * couple race conditions in the system, but those failure cases have
+ * been set up in such a way that the worst-case behavior is someone
+ * having to try to log in a second time.
+ *
+ * Most of the methods of this class are implementation details.
+ * People wishing to just use this store need only pay attention to
+ * the constructor.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore {
+
+    /**
+     * Initializes a new {@link Auth_OpenID_FileStore}.  This
+     * initializes the nonce and association directories, which are
+     * subdirectories of the directory passed in.
+     *
+     * @param string $directory This is the directory to put the store
+     * directories in.
+     */
+    function Auth_OpenID_FileStore($directory)
+    {
+        if (!Auth_OpenID::ensureDir($directory)) {
+            trigger_error('Not a directory and failed to create: '
+                          . $directory, E_USER_ERROR);
+        }
+        $directory = realpath($directory);
+
+        $this->directory = $directory;
+        $this->active = true;
+
+        $this->nonce_dir = $directory . DIRECTORY_SEPARATOR . 'nonces';
+
+        $this->association_dir = $directory . DIRECTORY_SEPARATOR .
+            'associations';
+
+        // Temp dir must be on the same filesystem as the assciations
+        // $directory and the $directory containing the auth key file.
+        $this->temp_dir = $directory . DIRECTORY_SEPARATOR . 'temp';
+
+        $this->auth_key_name = $directory . DIRECTORY_SEPARATOR . 'auth_key';
+
+        $this->max_nonce_age = 6 * 60 * 60; // Six hours, in seconds
+
+        if (!$this->_setup()) {
+            trigger_error('Failed to initialize OpenID file store in ' .
+                          $directory, E_USER_ERROR);
+        }
+    }
+
+    function destroy()
+    {
+        Auth_OpenID_FileStore::_rmtree($this->directory);
+        $this->active = false;
+    }
+
+    /**
+     * Make sure that the directories in which we store our data
+     * exist.
+     *
+     * @access private
+     */
+    function _setup()
+    {
+        return (Auth_OpenID::ensureDir(dirname($this->auth_key_name)) &&
+                Auth_OpenID::ensureDir($this->nonce_dir) &&
+                Auth_OpenID::ensureDir($this->association_dir) &&
+                Auth_OpenID::ensureDir($this->temp_dir));
+    }
+
+    /**
+     * Create a temporary file on the same filesystem as
+     * $this->auth_key_name and $this->association_dir.
+     *
+     * The temporary directory should not be cleaned if there are any
+     * processes using the store. If there is no active process using
+     * the store, it is safe to remove all of the files in the
+     * temporary directory.
+     *
+     * @return array ($fd, $filename)
+     * @access private
+     */
+    function _mktemp()
+    {
+        $name = Auth_OpenID_FileStore::_mkstemp($dir = $this->temp_dir);
+        $file_obj = @fopen($name, 'wb');
+        if ($file_obj !== false) {
+            return array($file_obj, $name);
+        } else {
+            Auth_OpenID_FileStore::_removeIfPresent($name);
+        }
+    }
+
+    /**
+     * Read the auth key from the auth key file. Will return None if
+     * there is currently no key.
+     *
+     * @return mixed
+     */
+    function readAuthKey()
+    {
+        if (!$this->active) {
+            trigger_error("FileStore no longer active", E_USER_ERROR);
+            return null;
+        }
+
+        $auth_key_file = @fopen($this->auth_key_name, 'rb');
+        if ($auth_key_file === false) {
+            return null;
+        }
+
+        $key = fread($auth_key_file, filesize($this->auth_key_name));
+        fclose($auth_key_file);
+
+        return $key;
+    }
+
+    /**
+     * Generate a new random auth key and safely store it in the
+     * location specified by $this->auth_key_name.
+     *
+     * @return string $key
+     */
+    function createAuthKey()
+    {
+        if (!$this->active) {
+            trigger_error("FileStore no longer active", E_USER_ERROR);
+            return null;
+        }
+
+        $auth_key = Auth_OpenID_CryptUtil::randomString($this->AUTH_KEY_LEN);
+
+        list($file_obj, $tmp) = $this->_mktemp();
+
+        fwrite($file_obj, $auth_key);
+        fflush($file_obj);
+        fclose($file_obj);
+
+        if (function_exists('link')) {
+            // Posix filesystem
+            $saved = link($tmp, $this->auth_key_name);
+            Auth_OpenID_FileStore::_removeIfPresent($tmp);
+        } else {
+            // Windows filesystem
+            $saved = rename($tmp, $this->auth_key_name);
+        }
+
+        if (!$saved) {
+            // The link failed, either because we lack the permission,
+            // or because the file already exists; try to read the key
+            // in case the file already existed.
+            $auth_key = $this->readAuthKey();
+        }
+
+        return $auth_key;
+    }
+
+    /**
+     * Retrieve the auth key from the file specified by
+     * $this->auth_key_name, creating it if it does not exist.
+     *
+     * @return string $key
+     */
+    function getAuthKey()
+    {
+        if (!$this->active) {
+            trigger_error("FileStore no longer active", E_USER_ERROR);
+            return null;
+        }
+
+        $auth_key = $this->readAuthKey();
+        if ($auth_key === null) {
+            $auth_key = $this->createAuthKey();
+
+            if (strlen($auth_key) != $this->AUTH_KEY_LEN) {
+                $fmt = 'Got an invalid auth key from %s. Expected '.
+                    '%d-byte string. Got: %s';
+                $msg = sprintf($fmt, $this->auth_key_name, $this->AUTH_KEY_LEN,
+                               $auth_key);
+                trigger_error($msg, E_USER_WARNING);
+                return null;
+            }
+        }
+        return $auth_key;
+    }
+
+    /**
+     * Create a unique filename for a given server url and
+     * handle. This implementation does not assume anything about the
+     * format of the handle. The filename that is returned will
+     * contain the domain name from the server URL for ease of human
+     * inspection of the data directory.
+     *
+     * @return string $filename
+     */
+    function getAssociationFilename($server_url, $handle)
+    {
+        if (!$this->active) {
+            trigger_error("FileStore no longer active", E_USER_ERROR);
+            return null;
+        }
+
+        if (strpos($server_url, '://') === false) {
+            trigger_error(sprintf("Bad server URL: %s", $server_url),
+                          E_USER_WARNING);
+            return null;
+        }
+
+        list($proto, $rest) = explode('://', $server_url, 2);
+        $parts = explode('/', $rest);
+        $domain = Auth_OpenID_FileStore::_filenameEscape($parts[0]);
+        $url_hash = Auth_OpenID_FileStore::_safe64($server_url);
+        if ($handle) {
+            $handle_hash = Auth_OpenID_FileStore::_safe64($handle);
+        } else {
+            $handle_hash = '';
+        }
+
+        $filename = sprintf('%s-%s-%s-%s', $proto, $domain, $url_hash,
+                            $handle_hash);
+
+        return $this->association_dir. DIRECTORY_SEPARATOR . $filename;
+    }
+
+    /**
+     * Store an association in the association directory.
+     */
+    function storeAssociation($server_url, $association)
+    {
+        if (!$this->active) {
+            trigger_error("FileStore no longer active", E_USER_ERROR);
+            return false;
+        }
+
+        $association_s = $association->serialize();
+        $filename = $this->getAssociationFilename($server_url,
+                                                  $association->handle);
+        list($tmp_file, $tmp) = $this->_mktemp();
+
+        if (!$tmp_file) {
+            trigger_error("_mktemp didn't return a valid file descriptor",
+                          E_USER_WARNING);
+            return false;
+        }
+
+        fwrite($tmp_file, $association_s);
+
+        fflush($tmp_file);
+
+        fclose($tmp_file);
+
+        if (@rename($tmp, $filename)) {
+            return true;
+        } else {
+            // In case we are running on Windows, try unlinking the
+            // file in case it exists.
+            @unlink($filename);
+
+            // Now the target should not exist. Try renaming again,
+            // giving up if it fails.
+            if (@rename($tmp, $filename)) {
+                return true;
+            }
+        }
+
+        // If there was an error, don't leave the temporary file
+        // around.
+        Auth_OpenID_FileStore::_removeIfPresent($tmp);
+        return false;
+    }
+
+    /**
+     * Retrieve an association. If no handle is specified, return the
+     * association with the most recent issue time.
+     *
+     * @return mixed $association
+     */
+    function getAssociation($server_url, $handle = null)
+    {
+        if (!$this->active) {
+            trigger_error("FileStore no longer active", E_USER_ERROR);
+            return null;
+        }
+
+        if ($handle === null) {
+            $handle = '';
+        }
+
+        // The filename with the empty handle is a prefix of all other
+        // associations for the given server URL.
+        $filename = $this->getAssociationFilename($server_url, $handle);
+
+        if ($handle) {
+            return $this->_getAssociation($filename);
+        } else {
+            $association_files =
+                Auth_OpenID_FileStore::_listdir($this->association_dir);
+            $matching_files = array();
+
+            // strip off the path to do the comparison
+            $name = basename($filename);
+            foreach ($association_files as $association_file) {
+                if (strpos($association_file, $name) === 0) {
+                    $matching_files[] = $association_file;
+                }
+            }
+
+            $matching_associations = array();
+            // read the matching files and sort by time issued
+            foreach ($matching_files as $name) {
+                $full_name = $this->association_dir . DIRECTORY_SEPARATOR .
+                    $name;
+                $association = $this->_getAssociation($full_name);
+                if ($association !== null) {
+                    $matching_associations[] = array($association->issued,
+                                                     $association);
+                }
+            }
+
+            $issued = array();
+            $assocs = array();
+            foreach ($matching_associations as $key => $assoc) {
+                $issued[$key] = $assoc[0];
+                $assocs[$key] = $assoc[1];
+            }
+
+            array_multisort($issued, SORT_DESC, $assocs, SORT_DESC,
+                            $matching_associations);
+
+            // return the most recently issued one.
+            if ($matching_associations) {
+                list($issued, $assoc) = $matching_associations[0];
+                return $assoc;
+            } else {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function _getAssociation($filename)
+    {
+        if (!$this->active) {
+            trigger_error("FileStore no longer active", E_USER_ERROR);
+            return null;
+        }
+
+        $assoc_file = @fopen($filename, 'rb');
+
+        if ($assoc_file === false) {
+            return null;
+        }
+
+        $assoc_s = fread($assoc_file, filesize($filename));
+        fclose($assoc_file);
+
+        if (!$assoc_s) {
+            return null;
+        }
+
+        $association =
+            Auth_OpenID_Association::deserialize('Auth_OpenID_Association',
+                                                $assoc_s);
+
+        if (!$association) {
+            Auth_OpenID_FileStore::_removeIfPresent($filename);
+            return null;
+        }
+
+        if ($association->getExpiresIn() == 0) {
+            Auth_OpenID_FileStore::_removeIfPresent($filename);
+            return null;
+        } else {
+            return $association;
+        }
+    }
+
+    /**
+     * Remove an association if it exists. Do nothing if it does not.
+     *
+     * @return bool $success
+     */
+    function removeAssociation($server_url, $handle)
+    {
+        if (!$this->active) {
+            trigger_error("FileStore no longer active", E_USER_ERROR);
+            return null;
+        }
+
+        $assoc = $this->getAssociation($server_url, $handle);
+        if ($assoc === null) {
+            return false;
+        } else {
+            $filename = $this->getAssociationFilename($server_url, $handle);
+            return Auth_OpenID_FileStore::_removeIfPresent($filename);
+        }
+    }
+
+    /**
+     * Mark this nonce as present.
+     */
+    function storeNonce($nonce)
+    {
+        if (!$this->active) {
+            trigger_error("FileStore no longer active", E_USER_ERROR);
+            return null;
+        }
+
+        $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce;
+        $nonce_file = fopen($filename, 'w');
+        if ($nonce_file === false) {
+            return false;
+        }
+        fclose($nonce_file);
+        return true;
+    }
+
+    /**
+     * Return whether this nonce is present. As a side effect, mark it
+     * as no longer present.
+     *
+     * @return bool $present
+     */
+    function useNonce($nonce)
+    {
+        if (!$this->active) {
+            trigger_error("FileStore no longer active", E_USER_ERROR);
+            return null;
+        }
+
+        $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce;
+        $st = @stat($filename);
+
+        if ($st === false) {
+            return false;
+        }
+
+        // Either it is too old or we are using it. Either way, we
+        // must remove the file.
+        if (!unlink($filename)) {
+            return false;
+        }
+
+        $now = time();
+        $nonce_age = $now - $st[9];
+
+        // We can us it if the age of the file is less than the
+        // expiration time.
+        return $nonce_age <= $this->max_nonce_age;
+    }
+
+    /**
+     * Remove expired entries from the database. This is potentially
+     * expensive, so only run when it is acceptable to take time.
+     */
+    function clean()
+    {
+        if (!$this->active) {
+            trigger_error("FileStore no longer active", E_USER_ERROR);
+            return null;
+        }
+
+        $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir);
+        $now = time();
+
+        // Check all nonces for expiry
+        foreach ($nonces as $nonce) {
+            $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce;
+            $st = @stat($filename);
+
+            if ($st !== false) {
+                // Remove the nonce if it has expired
+                $nonce_age = $now - $st[9];
+                if ($nonce_age > $this->max_nonce_age) {
+                    Auth_OpenID_FileStore::_removeIfPresent($filename);
+                }
+            }
+        }
+
+        $association_filenames =
+            Auth_OpenID_FileStore::_listdir($this->association_dir);
+
+        foreach ($association_filenames as $association_filename) {
+            $association_file = fopen($association_filename, 'rb');
+
+            if ($association_file !== false) {
+                $assoc_s = fread($association_file,
+                                 filesize($association_filename));
+                fclose($association_file);
+
+                // Remove expired or corrupted associations
+                $association =
+                  Auth_OpenID_Association::deserialize(
+                         'Auth_OpenID_Association', $assoc_s);
+
+                if ($association === null) {
+                    Auth_OpenID_FileStore::_removeIfPresent(
+                                                 $association_filename);
+                } else {
+                    if ($association->getExpiresIn() == 0) {
+                        Auth_OpenID_FileStore::_removeIfPresent(
+                                                 $association_filename);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function _rmtree($dir)
+    {
+        if ($dir[strlen($dir) - 1] != DIRECTORY_SEPARATOR) {
+            $dir .= DIRECTORY_SEPARATOR;
+        }
+
+        if ($handle = opendir($dir)) {
+            while ($item = readdir($handle)) {
+                if (!in_array($item, array('.', '..'))) {
+                    if (is_dir($dir . $item)) {
+
+                        if (!Auth_OpenID_FileStore::_rmtree($dir . $item)) {
+                            return false;
+                        }
+                    } else if (is_file($dir . $item)) {
+                        if (!unlink($dir . $item)) {
+                            return false;
+                        }
+                    }
+                }
+            }
+
+            closedir($handle);
+
+            if (!@rmdir($dir)) {
+                return false;
+            }
+
+            return true;
+        } else {
+            // Couldn't open directory.
+            return false;
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function _mkstemp($dir)
+    {
+        foreach (range(0, 4) as $i) {
+            $name = tempnam($dir, "php_openid_filestore_");
+
+            if ($name !== false) {
+                return $name;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @access private
+     */
+    function _mkdtemp($dir)
+    {
+        foreach (range(0, 4) as $i) {
+            $name = $dir . strval(DIRECTORY_SEPARATOR) . strval(getmypid()) .
+                "-" . strval(rand(1, time()));
+            if (!mkdir($name, 0700)) {
+                return false;
+            } else {
+                return $name;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @access private
+     */
+    function _listdir($dir)
+    {
+        $handle = opendir($dir);
+        $files = array();
+        while (false !== ($filename = readdir($handle))) {
+            $files[] = $filename;
+        }
+        return $files;
+    }
+
+    /**
+     * @access private
+     */
+    function _isFilenameSafe($char)
+    {
+        $_Auth_OpenID_filename_allowed = Auth_OpenID_letters .
+            Auth_OpenID_digits . ".";
+        return (strpos($_Auth_OpenID_filename_allowed, $char) !== false);
+    }
+
+    /**
+     * @access private
+     */
+    function _safe64($str)
+    {
+        $h64 = base64_encode(Auth_OpenID_SHA1($str));
+        $h64 = str_replace('+', '_', $h64);
+        $h64 = str_replace('/', '.', $h64);
+        $h64 = str_replace('=', '', $h64);
+        return $h64;
+    }
+
+    /**
+     * @access private
+     */
+    function _filenameEscape($str)
+    {
+        $filename = "";
+        for ($i = 0; $i < strlen($str); $i++) {
+            $c = $str[$i];
+            if (Auth_OpenID_FileStore::_isFilenameSafe($c)) {
+                $filename .= $c;
+            } else {
+                $filename .= sprintf("_%02X", ord($c));
+            }
+        }
+        return $filename;
+    }
+
+    /**
+     * Attempt to remove a file, returning whether the file existed at
+     * the time of the call.
+     *
+     * @access private
+     * @return bool $result True if the file was present, false if not.
+     */
+    function _removeIfPresent($filename)
+    {
+        return @unlink($filename);
+    }
+}
+
+?>
diff --git a/lib/Auth/OpenID/HMACSHA1.php b/lib/Auth/OpenID/HMACSHA1.php
new file mode 100644
index 000000000..6daadb5f8
--- /dev/null
+++ b/lib/Auth/OpenID/HMACSHA1.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * This is the HMACSHA1 implementation for the OpenID library.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * SHA1_BLOCKSIZE is this module's SHA1 blocksize used by the fallback
+ * implementation.
+ */
+define('Auth_OpenID_SHA1_BLOCKSIZE', 64);
+
+if (!function_exists('sha1')) {
+    /**
+     * Return a raw SHA1 hash of the given string
+     *
+     * XXX: include the SHA1 code from Dan Libby's OpenID library
+     */
+    function Auth_OpenID_SHA1($text)
+    {
+        trigger_error('No SHA1 function found', E_USER_ERROR);
+    }
+} else {
+    /**
+     * @ignore
+     */
+    function Auth_OpenID_SHA1($text)
+        {
+            $hex = sha1($text);
+            $raw = '';
+            for ($i = 0; $i < 40; $i += 2) {
+                $hexcode = substr($hex, $i, 2);
+                $charcode = (int)base_convert($hexcode, 16, 10);
+                $raw .= chr($charcode);
+            }
+            return $raw;
+        }
+}
+
+/**
+ * Compute an HMAC/SHA1 hash.
+ *
+ * @access private
+ * @param string $key The HMAC key
+ * @param string $text The message text to hash
+ * @return string $mac The MAC
+ */
+function Auth_OpenID_HMACSHA1($key, $text)
+{
+    if (strlen($key) > Auth_OpenID_SHA1_BLOCKSIZE) {
+        $key = Auth_OpenID_SHA1($key, true);
+    }
+
+    $key = str_pad($key, Auth_OpenID_SHA1_BLOCKSIZE, chr(0x00));
+    $ipad = str_repeat(chr(0x36), Auth_OpenID_SHA1_BLOCKSIZE);
+    $opad = str_repeat(chr(0x5c), Auth_OpenID_SHA1_BLOCKSIZE);
+    $hash1 = Auth_OpenID_SHA1(($key ^ $ipad) . $text, true);
+    $hmac = Auth_OpenID_SHA1(($key ^ $opad) . $hash1, true);
+    return $hmac;
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/Interface.php b/lib/Auth/OpenID/Interface.php
new file mode 100644
index 000000000..ce9fa1feb
--- /dev/null
+++ b/lib/Auth/OpenID/Interface.php
@@ -0,0 +1,188 @@
+<?php
+
+/**
+ * This file specifies the interface for PHP OpenID store implementations.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * This is the interface for the store objects the OpenID library
+ * uses. It is a single class that provides all of the persistence
+ * mechanisms that the OpenID library needs, for both servers and
+ * consumers.  If you want to create an SQL-driven store, please see
+ * then {@link Auth_OpenID_SQLStore} class.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ */
+class Auth_OpenID_OpenIDStore {
+    /**
+     * @var integer The length of the auth key that should be returned
+     * by the getAuthKey method.
+     */
+    var $AUTH_KEY_LEN = 20;
+
+    /**
+     * This method puts an Association object into storage,
+     * retrievable by server URL and handle.
+     *
+     * @param string $server_url The URL of the identity server that
+     * this association is with. Because of the way the server portion
+     * of the library uses this interface, don't assume there are any
+     * limitations on the character set of the input string. In
+     * particular, expect to see unescaped non-url-safe characters in
+     * the server_url field.
+     *
+     * @param Association $association The Association to store.
+     */
+    function storeAssociation($server_url, $association)
+    {
+        trigger_error("Auth_OpenID_OpenIDStore::storeAssociation ".
+                      "not implemented", E_USER_ERROR);
+    }
+
+    /**
+     * This method returns an Association object from storage that
+     * matches the server URL and, if specified, handle. It returns
+     * null if no such association is found or if the matching
+     * association is expired.
+     *
+     * If no handle is specified, the store may return any association
+     * which matches the server URL. If multiple associations are
+     * valid, the recommended return value for this method is the one
+     * that will remain valid for the longest duration.
+     *
+     * This method is allowed (and encouraged) to garbage collect
+     * expired associations when found. This method must not return
+     * expired associations.
+     *
+     * @param string $server_url The URL of the identity server to get
+     * the association for. Because of the way the server portion of
+     * the library uses this interface, don't assume there are any
+     * limitations on the character set of the input string.  In
+     * particular, expect to see unescaped non-url-safe characters in
+     * the server_url field.
+     *
+     * @param mixed $handle This optional parameter is the handle of
+     * the specific association to get. If no specific handle is
+     * provided, any valid association matching the server URL is
+     * returned.
+     *
+     * @return Association The Association for the given identity
+     * server.
+     */
+    function getAssociation($server_url, $handle = null)
+    {
+        trigger_error("Auth_OpenID_OpenIDStore::getAssociation ".
+                      "not implemented", E_USER_ERROR);
+    }
+
+    /**
+     * This method removes the matching association if it's found, and
+     * returns whether the association was removed or not.
+     *
+     * @param string $server_url The URL of the identity server the
+     * association to remove belongs to. Because of the way the server
+     * portion of the library uses this interface, don't assume there
+     * are any limitations on the character set of the input
+     * string. In particular, expect to see unescaped non-url-safe
+     * characters in the server_url field.
+     *
+     * @param string $handle This is the handle of the association to
+     * remove. If there isn't an association found that matches both
+     * the given URL and handle, then there was no matching handle
+     * found.
+     *
+     * @return mixed Returns whether or not the given association existed.
+     */
+    function removeAssociation($server_url, $handle)
+    {
+        trigger_error("Auth_OpenID_OpenIDStore::removeAssociation ".
+                      "not implemented", E_USER_ERROR);
+    }
+
+    /**
+     * Stores a nonce. This is used by the consumer to prevent replay
+     * attacks.
+     *
+     * @param string $nonce The nonce to store.
+     *
+     * @return null
+     */
+    function storeNonce($nonce)
+    {
+        trigger_error("Auth_OpenID_OpenIDStore::storeNonce ".
+                      "not implemented", E_USER_ERROR);
+    }
+
+    /**
+     * This method is called when the library is attempting to use a
+     * nonce. If the nonce is in the store, this method removes it and
+     * returns a value which evaluates as true. Otherwise it returns a
+     * value which evaluates as false.
+     *
+     * This method is allowed and encouraged to treat nonces older
+     * than some period (a very conservative window would be 6 hours,
+     * for example) as no longer existing, and return False and remove
+     * them.
+     *
+     * @param string $nonce The nonce to use.
+     *
+     * @return bool Whether or not the nonce was valid.
+     */
+    function useNonce($nonce)
+    {
+        trigger_error("Auth_OpenID_OpenIDStore::useNonce ".
+                      "not implemented", E_USER_ERROR);
+    }
+
+    /**
+     * This method returns a key used to sign the tokens, to ensure
+     * that they haven't been tampered with in transit. It should
+     * return the same key every time it is called. The key returned
+     * should be {@link AUTH_KEY_LEN} bytes long.
+     *
+     * @return string The key. It should be {@link AUTH_KEY_LEN} bytes in
+     * length, and use the full range of byte values. That is, it
+     * should be treated as a lump of binary data stored in a string.
+     */
+    function getAuthKey()
+    {
+        trigger_error("Auth_OpenID_OpenIDStore::getAuthKey ".
+                      "not implemented", E_USER_ERROR);
+    }
+
+    /**
+     * This method must return true if the store is a dumb-mode-style
+     * store. Unlike all other methods in this class, this one
+     * provides a default implementation, which returns false.
+     *
+     * In general, any custom subclass of {@link Auth_OpenID_OpenIDStore}
+     * won't override this method, as custom subclasses are only likely to
+     * be created when the store is fully functional.
+     *
+     * @return bool true if the store works fully, false if the
+     * consumer will have to use dumb mode to use this store.
+     */
+    function isDumb()
+    {
+        return false;
+    }
+
+    /**
+     * Removes all entries from the store; implementation is optional.
+     */
+    function reset()
+    {
+    }
+
+}
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/KVForm.php b/lib/Auth/OpenID/KVForm.php
new file mode 100644
index 000000000..6075c44f0
--- /dev/null
+++ b/lib/Auth/OpenID/KVForm.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * OpenID protocol key-value/comma-newline format parsing and
+ * serialization
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Container for key-value/comma-newline OpenID format and parsing
+ */
+class Auth_OpenID_KVForm {
+    /**
+     * Convert an OpenID colon/newline separated string into an
+     * associative array
+     *
+     * @static
+     * @access private
+     */
+    function toArray($kvs, $strict=false)
+    {
+        $lines = explode("\n", $kvs);
+
+        $last = array_pop($lines);
+        if ($last !== '') {
+            array_push($lines, $last);
+            if ($strict) {
+                return false;
+            }
+        }
+
+        $values = array();
+
+        for ($lineno = 0; $lineno < count($lines); $lineno++) {
+            $line = $lines[$lineno];
+            $kv = explode(':', $line, 2);
+            if (count($kv) != 2) {
+                if ($strict) {
+                    return false;
+                }
+                continue;
+            }
+
+            $key = $kv[0];
+            $tkey = trim($key);
+            if ($tkey != $key) {
+                if ($strict) {
+                    return false;
+                }
+            }
+
+            $value = $kv[1];
+            $tval = trim($value);
+            if ($tval != $value) {
+                if ($strict) {
+                    return false;
+                }
+            }
+
+            $values[$tkey] = $tval;
+        }
+
+        return $values;
+    }
+
+    /**
+     * Convert an array into an OpenID colon/newline separated string
+     *
+     * @static
+     * @access private
+     */
+    function fromArray($values)
+    {
+        if ($values === null) {
+            return null;
+        }
+
+        ksort($values);
+
+        $serialized = '';
+        foreach ($values as $key => $value) {
+            if (is_array($value)) {
+                list($key, $value) = array($value[0], $value[1]);
+            }
+
+            if (strpos($key, ':') !== false) {
+                return null;
+            }
+
+            if (strpos($key, "\n") !== false) {
+                return null;
+            }
+
+            if (strpos($value, "\n") !== false) {
+                return null;
+            }
+            $serialized .= "$key:$value\n";
+        }
+        return $serialized;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/MySQLStore.php b/lib/Auth/OpenID/MySQLStore.php
new file mode 100644
index 000000000..f89afc6fe
--- /dev/null
+++ b/lib/Auth/OpenID/MySQLStore.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * A MySQL store.
+ *
+ * @package OpenID
+ */
+
+/**
+ * Require the base class file.
+ */
+require_once "Auth/OpenID/SQLStore.php";
+
+/**
+ * An SQL store that uses MySQL as its backend.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_MySQLStore extends Auth_OpenID_SQLStore {
+    /**
+     * @access private
+     */
+    function setSQL()
+    {
+        $this->sql['nonce_table'] =
+            "CREATE TABLE %s (nonce CHAR(8) UNIQUE PRIMARY KEY, ".
+            "expires INTEGER) TYPE=InnoDB";
+
+        $this->sql['assoc_table'] =
+            "CREATE TABLE %s (server_url BLOB, handle VARCHAR(255), ".
+            "secret BLOB, issued INTEGER, lifetime INTEGER, ".
+            "assoc_type VARCHAR(64), PRIMARY KEY (server_url(255), handle)) ".
+            "TYPE=InnoDB";
+
+        $this->sql['settings_table'] =
+            "CREATE TABLE %s (setting VARCHAR(128) UNIQUE PRIMARY KEY, ".
+            "value BLOB) TYPE=InnoDB";
+
+        $this->sql['create_auth'] =
+            "INSERT INTO %s VALUES ('auth_key', !)";
+
+        $this->sql['get_auth'] =
+            "SELECT value FROM %s WHERE setting = 'auth_key'";
+
+        $this->sql['set_assoc'] =
+            "REPLACE INTO %s VALUES (?, ?, !, ?, ?, ?)";
+
+        $this->sql['get_assocs'] =
+            "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+            "WHERE server_url = ?";
+
+        $this->sql['get_assoc'] =
+            "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+            "WHERE server_url = ? AND handle = ?";
+
+        $this->sql['remove_assoc'] =
+            "DELETE FROM %s WHERE server_url = ? AND handle = ?";
+
+        $this->sql['add_nonce'] =
+            "REPLACE INTO %s (nonce, expires) VALUES (?, ?)";
+
+        $this->sql['get_nonce'] =
+            "SELECT * FROM %s WHERE nonce = ?";
+
+        $this->sql['remove_nonce'] =
+            "DELETE FROM %s WHERE nonce = ?";
+    }
+
+    /**
+     * @access private
+     */
+    function blobEncode($blob)
+    {
+        return "0x" . bin2hex($blob);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/Parse.php b/lib/Auth/OpenID/Parse.php
new file mode 100644
index 000000000..891ca5e71
--- /dev/null
+++ b/lib/Auth/OpenID/Parse.php
@@ -0,0 +1,308 @@
+<?php
+
+/**
+ * This module implements a VERY limited parser that finds <link> tags
+ * in the head of HTML or XHTML documents and parses out their
+ * attributes according to the OpenID spec. It is a liberal parser,
+ * but it requires these things from the data in order to work:
+ *
+ * - There must be an open <html> tag
+ *
+ * - There must be an open <head> tag inside of the <html> tag
+ *
+ * - Only <link>s that are found inside of the <head> tag are parsed
+ *   (this is by design)
+ *
+ * - The parser follows the OpenID specification in resolving the
+ *   attributes of the link tags. This means that the attributes DO
+ *   NOT get resolved as they would by an XML or HTML parser. In
+ *   particular, only certain entities get replaced, and href
+ *   attributes do not get resolved relative to a base URL.
+ *
+ * From http://openid.net/specs.bml:
+ *
+ * - The openid.server URL MUST be an absolute URL. OpenID consumers
+ *   MUST NOT attempt to resolve relative URLs.
+ *
+ * - The openid.server URL MUST NOT include entities other than &amp;,
+ *   &lt;, &gt;, and &quot;.
+ *
+ * The parser ignores SGML comments and <![CDATA[blocks]]>. Both kinds
+ * of quoting are allowed for attributes.
+ *
+ * The parser deals with invalid markup in these ways:
+ *
+ * - Tag names are not case-sensitive
+ *
+ * - The <html> tag is accepted even when it is not at the top level
+ *
+ * - The <head> tag is accepted even when it is not a direct child of
+ *   the <html> tag, but a <html> tag must be an ancestor of the
+ *   <head> tag
+ *
+ * - <link> tags are accepted even when they are not direct children
+ *   of the <head> tag, but a <head> tag must be an ancestor of the
+ *   <link> tag
+ *
+ * - If there is no closing tag for an open <html> or <head> tag, the
+ *   remainder of the document is viewed as being inside of the
+ *   tag. If there is no closing tag for a <link> tag, the link tag is
+ *   treated as a short tag. Exceptions to this rule are that <html>
+ *   closes <html> and <body> or <head> closes <head>
+ *
+ * - Attributes of the <link> tag are not required to be quoted.
+ *
+ * - In the case of duplicated attribute names, the attribute coming
+ *   last in the tag will be the value returned.
+ *
+ * - Any text that does not parse as an attribute within a link tag
+ *   will be ignored. (e.g. <link pumpkin rel='openid.server' /> will
+ *   ignore pumpkin)
+ *
+ * - If there are more than one <html> or <head> tag, the parser only
+ *   looks inside of the first one.
+ *
+ * - The contents of <script> tags are ignored entirely, except
+ *   unclosed <script> tags. Unclosed <script> tags are ignored.
+ *
+ * - Any other invalid markup is ignored, including unclosed SGML
+ *   comments and unclosed <![CDATA[blocks.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @access private
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Require Auth_OpenID::arrayGet().
+ */
+require_once "Auth/OpenID.php";
+
+class Auth_OpenID_Parse {
+
+    /**
+     * Specify some flags for use with regex matching.
+     */
+    var $_re_flags = "si";
+
+    /**
+     * Stuff to remove before we start looking for tags
+     */
+    var $_removed_re =
+           "<!--.*?-->|<!\[CDATA\[.*?\]\]>|<script\b(?!:)[^>]*>.*?<\/script>";
+
+    /**
+     * Starts with the tag name at a word boundary, where the tag name
+     * is not a namespace
+     */
+    var $_tag_expr = "<%s\b(?!:)([^>]*?)(?:\/>|>(.*?)(?:<\/?%s\s*>|\Z))";
+
+    var $_attr_find = '\b(\w+)=("[^"]*"|\'[^\']*\'|[^\'"\s\/<>]+)';
+
+    function Auth_OpenID_Parse()
+    {
+        $this->_link_find = sprintf("/<link\b(?!:)([^>]*)(?!<)>/%s",
+                                    $this->_re_flags);
+
+        $this->_entity_replacements = array(
+                                            'amp' => '&',
+                                            'lt' => '<',
+                                            'gt' => '>',
+                                            'quot' => '"'
+                                            );
+
+        $this->_attr_find = sprintf("/%s/%s",
+                                    $this->_attr_find,
+                                    $this->_re_flags);
+
+        $this->_removed_re = sprintf("/%s/%s",
+                                     $this->_removed_re,
+                                     $this->_re_flags);
+
+        $this->_ent_replace =
+            sprintf("&(%s);", implode("|",
+                                      $this->_entity_replacements));
+    }
+
+    /**
+     * Returns a regular expression that will match a given tag in an
+     * SGML string.
+     */
+    function tagMatcher($tag_name, $close_tags = null)
+    {
+        if ($close_tags) {
+            $options = implode("|", array_merge(array($tag_name), $close_tags));
+            $closer = sprintf("(?:%s)", $options);
+        } else {
+            $closer = $tag_name;
+        }
+
+        $expr = sprintf($this->_tag_expr, $tag_name, $closer);
+        return sprintf("/%s/%s", $expr, $this->_re_flags);
+    }
+
+    function htmlFind()
+    {
+        return $this->tagMatcher('html');
+    }
+
+    function headFind()
+    {
+        return $this->tagMatcher('head', array('body'));
+    }
+
+    function replaceEntities($str)
+    {
+        foreach ($this->_entity_replacements as $old => $new) {
+            $str = preg_replace(sprintf("/&%s;/", $old), $new, $str);
+        }
+        return $str;
+    }
+
+    function removeQuotes($str)
+    {
+        $matches = array();
+        $double = '/^"(.*)"$/';
+        $single = "/^\'(.*)\'$/";
+
+        if (preg_match($double, $str, $matches)) {
+            return $matches[1];
+        } else if (preg_match($single, $str, $matches)) {
+            return $matches[1];
+        } else {
+            return $str;
+        }
+    }
+
+    /**
+     * Find all link tags in a string representing a HTML document and
+     * return a list of their attributes.
+     *
+     * @param string $html The text to parse
+     * @return array $list An array of arrays of attributes, one for each
+     * link tag
+     */
+    function parseLinkAttrs($html)
+    {
+        $stripped = preg_replace($this->_removed_re,
+                                 "",
+                                 $html);
+
+        // Try to find the <HTML> tag.
+        $html_re = $this->htmlFind();
+        $html_matches = array();
+        if (!preg_match($html_re, $stripped, $html_matches)) {
+            return array();
+        }
+
+        // Try to find the <HEAD> tag.
+        $head_re = $this->headFind();
+        $head_matches = array();
+        if (!preg_match($head_re, $html_matches[0], $head_matches)) {
+            return array();
+        }
+
+        $link_data = array();
+        $link_matches = array();
+
+        if (!preg_match_all($this->_link_find, $head_matches[0],
+                            $link_matches)) {
+            return array();
+        }
+
+        foreach ($link_matches[0] as $link) {
+            $attr_matches = array();
+            preg_match_all($this->_attr_find, $link, $attr_matches);
+            $link_attrs = array();
+            foreach ($attr_matches[0] as $index => $full_match) {
+                $name = $attr_matches[1][$index];
+                $value = $this->replaceEntities(
+                              $this->removeQuotes($attr_matches[2][$index]));
+
+                $link_attrs[strtolower($name)] = $value;
+            }
+            $link_data[] = $link_attrs;
+        }
+
+        return $link_data;
+    }
+
+    function relMatches($rel_attr, $target_rel)
+    {
+        // Does this target_rel appear in the rel_str?
+        // XXX: TESTME
+        $rels = preg_split("/\s+/", trim($rel_attr));
+        foreach ($rels as $rel) {
+            $rel = strtolower($rel);
+            if ($rel == $target_rel) {
+                return 1;
+            }
+        }
+
+        return 0;
+    }
+
+    function linkHasRel($link_attrs, $target_rel)
+    {
+        // Does this link have target_rel as a relationship?
+        // XXX: TESTME
+        $rel_attr = Auth_OpeniD::arrayGet($link_attrs, 'rel', null);
+        return ($rel_attr && $this->relMatches($rel_attr,
+                                               $target_rel));
+    }
+
+    function findLinksRel($link_attrs_list, $target_rel)
+    {
+        // Filter the list of link attributes on whether it has
+        // target_rel as a relationship.
+        // XXX: TESTME
+        $result = array();
+        foreach ($link_attrs_list as $attr) {
+            if ($this->linkHasRel($attr, $target_rel)) {
+                $result[] = $attr;
+            }
+        }
+
+        return $result;
+    }
+
+    function findFirstHref($link_attrs_list, $target_rel)
+    {
+        // Return the value of the href attribute for the first link
+        // tag in the list that has target_rel as a relationship.
+        // XXX: TESTME
+        $matches = $this->findLinksRel($link_attrs_list,
+                                       $target_rel);
+        if (!$matches) {
+            return null;
+        }
+        $first = $matches[0];
+        return Auth_OpenID::arrayGet($first, 'href', null);
+    }
+}
+
+function Auth_OpenID_legacy_discover($html_text)
+{
+    $p = new Auth_OpenID_Parse();
+
+    $link_attrs = $p->parseLinkAttrs($html_text);
+
+    $server_url = $p->findFirstHref($link_attrs,
+                                    'openid.server');
+
+    if ($server_url === null) {
+        return false;
+    } else {
+        $delegate_url = $p->findFirstHref($link_attrs,
+                                          'openid.delegate');
+        return array($delegate_url, $server_url);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/PostgreSQLStore.php b/lib/Auth/OpenID/PostgreSQLStore.php
new file mode 100644
index 000000000..a41528078
--- /dev/null
+++ b/lib/Auth/OpenID/PostgreSQLStore.php
@@ -0,0 +1,136 @@
+<?php
+
+/**
+ * A PostgreSQL store.
+ *
+ * @package OpenID
+ */
+
+/**
+ * Require the base class file.
+ */
+require_once "Auth/OpenID/SQLStore.php";
+
+/**
+ * An SQL store that uses PostgreSQL as its backend.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_PostgreSQLStore extends Auth_OpenID_SQLStore {
+    /**
+     * @access private
+     */
+    function setSQL()
+    {
+        $this->sql['nonce_table'] =
+            "CREATE TABLE %s (nonce CHAR(8) UNIQUE PRIMARY KEY, ".
+            "expires INTEGER)";
+
+        $this->sql['assoc_table'] =
+            "CREATE TABLE %s (server_url VARCHAR(2047), handle VARCHAR(255), ".
+            "secret BYTEA, issued INTEGER, lifetime INTEGER, ".
+            "assoc_type VARCHAR(64), PRIMARY KEY (server_url, handle), ".
+            "CONSTRAINT secret_length_constraint CHECK ".
+            "(LENGTH(secret) <= 128))";
+
+        $this->sql['settings_table'] =
+            "CREATE TABLE %s (setting VARCHAR(128) UNIQUE PRIMARY KEY, ".
+            "value BYTEA, ".
+            "CONSTRAINT value_length_constraint CHECK (LENGTH(value) <= 20))";
+
+        $this->sql['create_auth'] =
+            "INSERT INTO %s VALUES ('auth_key', '!')";
+
+        $this->sql['get_auth'] =
+            "SELECT value FROM %s WHERE setting = 'auth_key'";
+
+        $this->sql['set_assoc'] =
+            array(
+                  'insert_assoc' => "INSERT INTO %s (server_url, handle, ".
+                  "secret, issued, lifetime, assoc_type) VALUES ".
+                  "(?, ?, '!', ?, ?, ?)",
+                  'update_assoc' => "UPDATE %s SET secret = '!', issued = ?, ".
+                  "lifetime = ?, assoc_type = ? WHERE server_url = ? AND ".
+                  "handle = ?"
+                  );
+
+        $this->sql['get_assocs'] =
+            "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+            "WHERE server_url = ?";
+
+        $this->sql['get_assoc'] =
+            "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+            "WHERE server_url = ? AND handle = ?";
+
+        $this->sql['remove_assoc'] =
+            "DELETE FROM %s WHERE server_url = ? AND handle = ?";
+
+        $this->sql['add_nonce'] =
+            array(
+                  'insert_nonce' => "INSERT INTO %s (nonce, expires) VALUES ".
+                  "(?, ?)",
+                  'update_nonce' => "UPDATE %s SET expires = ? WHERE nonce = ?"
+                  );
+
+        $this->sql['get_nonce'] =
+            "SELECT * FROM %s WHERE nonce = ?";
+
+        $this->sql['remove_nonce'] =
+            "DELETE FROM %s WHERE nonce = ?";
+    }
+
+    /**
+     * @access private
+     */
+    function _set_assoc($server_url, $handle, $secret, $issued, $lifetime,
+                        $assoc_type)
+    {
+        $result = $this->_get_assoc($server_url, $handle);
+        if ($result) {
+            // Update the table since this associations already exists.
+            $this->connection->query($this->sql['set_assoc']['update_assoc'],
+                                     array($secret, $issued, $lifetime,
+                                           $assoc_type, $server_url, $handle));
+        } else {
+            // Insert a new record because this association wasn't
+            // found.
+            $this->connection->query($this->sql['set_assoc']['insert_assoc'],
+                                     array($server_url, $handle, $secret,
+                                           $issued, $lifetime, $assoc_type));
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function _add_nonce($nonce, $expires)
+    {
+        if ($this->_get_nonce($nonce)) {
+            return $this->resultToBool($this->connection->query(
+                                      $this->sql['add_nonce']['update_nonce'],
+                                      array($expires, $nonce)));
+        } else {
+            return $this->resultToBool($this->connection->query(
+                                      $this->sql['add_nonce']['insert_nonce'],
+                                      array($nonce, $expires)));
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function blobEncode($blob)
+    {
+        return $this->_octify($blob);
+    }
+
+    /**
+     * @access private
+     */
+    function blobDecode($blob)
+    {
+        return $this->_unoctify($blob);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/SQLStore.php b/lib/Auth/OpenID/SQLStore.php
new file mode 100644
index 000000000..c7bd5401f
--- /dev/null
+++ b/lib/Auth/OpenID/SQLStore.php
@@ -0,0 +1,658 @@
+<?php
+
+/**
+ * SQL-backed OpenID stores.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Require the PEAR DB module because we'll need it for the SQL-based
+ * stores implemented here.  We silence any errors from the inclusion
+ * because it might not be present, and a user of the SQL stores may
+ * supply an Auth_OpenID_DatabaseConnection instance that implements
+ * its own storage.
+ */
+global $__Auth_OpenID_PEAR_AVAILABLE;
+$__Auth_OpenID_PEAR_AVAILABLE = @include_once 'DB.php';
+
+/**
+ * @access private
+ */
+require_once 'Auth/OpenID/Interface.php';
+
+/**
+ * This is the parent class for the SQL stores, which contains the
+ * logic common to all of the SQL stores.
+ *
+ * The table names used are determined by the class variables
+ * settings_table_name, associations_table_name, and
+ * nonces_table_name.  To change the name of the tables used, pass new
+ * table names into the constructor.
+ *
+ * To create the tables with the proper schema, see the createTables
+ * method.
+ *
+ * This class shouldn't be used directly.  Use one of its subclasses
+ * instead, as those contain the code necessary to use a specific
+ * database.  If you're an OpenID integrator and you'd like to create
+ * an SQL-driven store that wraps an application's database
+ * abstraction, be sure to create a subclass of
+ * {@link Auth_OpenID_DatabaseConnection} that calls the application's
+ * database abstraction calls.  Then, pass an instance of your new
+ * database connection class to your SQLStore subclass constructor.
+ *
+ * All methods other than the constructor and createTables should be
+ * considered implementation details.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore {
+
+    /**
+     * This creates a new SQLStore instance.  It requires an
+     * established database connection be given to it, and it allows
+     * overriding the default table names.
+     *
+     * @param connection $connection This must be an established
+     * connection to a database of the correct type for the SQLStore
+     * subclass you're using.  This must either be an PEAR DB
+     * connection handle or an instance of a subclass of
+     * Auth_OpenID_DatabaseConnection.
+     *
+     * @param string $settings_table This is an optional parameter to
+     * specify the name of the table used for this store's settings.
+     * The default value is 'oid_settings'.
+     *
+     * @param associations_table: This is an optional parameter to
+     * specify the name of the table used for storing associations.
+     * The default value is 'oid_associations'.
+     *
+     * @param nonces_table: This is an optional parameter to specify
+     * the name of the table used for storing nonces.  The default
+     * value is 'oid_nonces'.
+     */
+    function Auth_OpenID_SQLStore($connection, $settings_table = null,
+                                  $associations_table = null,
+                                  $nonces_table = null)
+    {
+        global $__Auth_OpenID_PEAR_AVAILABLE;
+
+        $this->settings_table_name = "oid_settings";
+        $this->associations_table_name = "oid_associations";
+        $this->nonces_table_name = "oid_nonces";
+
+        // Check the connection object type to be sure it's a PEAR
+        // database connection.
+        if (!(is_object($connection) &&
+              (is_subclass_of($connection, 'db_common') ||
+               is_subclass_of($connection,
+                              'auth_openid_databaseconnection')))) {
+            trigger_error("Auth_OpenID_SQLStore expected PEAR connection " .
+                          "object (got ".get_class($connection).")",
+                          E_USER_ERROR);
+            return;
+        }
+
+        $this->connection = $connection;
+
+        // Be sure to set the fetch mode so the results are keyed on
+        // column name instead of column index.  This is a PEAR
+        // constant, so only try to use it if PEAR is present.  Note
+        // that Auth_Openid_Databaseconnection instances need not
+        // implement ::setFetchMode for this reason.
+        if ($__Auth_OpenID_PEAR_AVAILABLE) {
+            $this->connection->setFetchMode(DB_FETCHMODE_ASSOC);
+        }
+
+        if ($settings_table) {
+            $this->settings_table_name = $settings_table;
+        }
+
+        if ($associations_table) {
+            $this->associations_table_name = $associations_table;
+        }
+
+        if ($nonces_table) {
+            $this->nonces_table_name = $nonces_table;
+        }
+
+        $this->max_nonce_age = 6 * 60 * 60;
+
+        // Be sure to run the database queries with auto-commit mode
+        // turned OFF, because we want every function to run in a
+        // transaction, implicitly.  As a rule, methods named with a
+        // leading underscore will NOT control transaction behavior.
+        // Callers of these methods will worry about transactions.
+        $this->connection->autoCommit(false);
+
+        // Create an empty SQL strings array.
+        $this->sql = array();
+
+        // Call this method (which should be overridden by subclasses)
+        // to populate the $this->sql array with SQL strings.
+        $this->setSQL();
+
+        // Verify that all required SQL statements have been set, and
+        // raise an error if any expected SQL strings were either
+        // absent or empty.
+        list($missing, $empty) = $this->_verifySQL();
+
+        if ($missing) {
+            trigger_error("Expected keys in SQL query list: " .
+                          implode(", ", $missing),
+                          E_USER_ERROR);
+            return;
+        }
+
+        if ($empty) {
+            trigger_error("SQL list keys have no SQL strings: " .
+                          implode(", ", $empty),
+                          E_USER_ERROR);
+            return;
+        }
+
+        // Add table names to queries.
+        $this->_fixSQL();
+    }
+
+    function tableExists($table_name)
+    {
+        return !$this->isError(
+                      $this->connection->query("SELECT * FROM %s LIMIT 0",
+                                               $table_name));
+    }
+
+    /**
+     * Returns true if $value constitutes a database error; returns
+     * false otherwise.
+     */
+    function isError($value)
+    {
+        return PEAR::isError($value);
+    }
+
+    /**
+     * Converts a query result to a boolean.  If the result is a
+     * database error according to $this->isError(), this returns
+     * false; otherwise, this returns true.
+     */
+    function resultToBool($obj)
+    {
+        if ($this->isError($obj)) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * This method should be overridden by subclasses.  This method is
+     * called by the constructor to set values in $this->sql, which is
+     * an array keyed on sql name.
+     */
+    function setSQL()
+    {
+    }
+
+    /**
+     * Resets the store by removing all records from the store's
+     * tables.
+     */
+    function reset()
+    {
+        $this->connection->query(sprintf("DELETE FROM %s",
+                                         $this->associations_table_name));
+
+        $this->connection->query(sprintf("DELETE FROM %s",
+                                         $this->nonces_table_name));
+
+        $this->connection->query(sprintf("DELETE FROM %s",
+                                         $this->settings_table_name));
+    }
+
+    /**
+     * @access private
+     */
+    function _verifySQL()
+    {
+        $missing = array();
+        $empty = array();
+
+        $required_sql_keys = array(
+                                   'nonce_table',
+                                   'assoc_table',
+                                   'settings_table',
+                                   'get_auth',
+                                   'create_auth',
+                                   'set_assoc',
+                                   'get_assoc',
+                                   'get_assocs',
+                                   'remove_assoc',
+                                   'add_nonce',
+                                   'get_nonce',
+                                   'remove_nonce'
+                                   );
+
+        foreach ($required_sql_keys as $key) {
+            if (!array_key_exists($key, $this->sql)) {
+                $missing[] = $key;
+            } else if (!$this->sql[$key]) {
+                $empty[] = $key;
+            }
+        }
+
+        return array($missing, $empty);
+    }
+
+    /**
+     * @access private
+     */
+    function _fixSQL()
+    {
+        $replacements = array(
+                              array(
+                                    'value' => $this->nonces_table_name,
+                                    'keys' => array('nonce_table',
+                                                    'add_nonce',
+                                                    'get_nonce',
+                                                    'remove_nonce')
+                                    ),
+                              array(
+                                    'value' => $this->associations_table_name,
+                                    'keys' => array('assoc_table',
+                                                    'set_assoc',
+                                                    'get_assoc',
+                                                    'get_assocs',
+                                                    'remove_assoc')
+                                    ),
+                              array(
+                                    'value' => $this->settings_table_name,
+                                    'keys' => array('settings_table',
+                                                    'get_auth',
+                                                    'create_auth')
+                                    )
+                              );
+
+        foreach ($replacements as $item) {
+            $value = $item['value'];
+            $keys = $item['keys'];
+
+            foreach ($keys as $k) {
+                if (is_array($this->sql[$k])) {
+                    foreach ($this->sql[$k] as $part_key => $part_value) {
+                        $this->sql[$k][$part_key] = sprintf($part_value,
+                                                            $value);
+                    }
+                } else {
+                    $this->sql[$k] = sprintf($this->sql[$k], $value);
+                }
+            }
+        }
+    }
+
+    function blobDecode($blob)
+    {
+        return $blob;
+    }
+
+    function blobEncode($str)
+    {
+        return $str;
+    }
+
+    function createTables()
+    {
+        $this->connection->autoCommit(true);
+        $n = $this->create_nonce_table();
+        $a = $this->create_assoc_table();
+        $s = $this->create_settings_table();
+        $this->connection->autoCommit(false);
+
+        if ($n && $a && $s) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    function create_nonce_table()
+    {
+        if (!$this->tableExists($this->nonces_table_name)) {
+            $r = $this->connection->query($this->sql['nonce_table']);
+            return $this->resultToBool($r);
+        }
+        return true;
+    }
+
+    function create_assoc_table()
+    {
+        if (!$this->tableExists($this->associations_table_name)) {
+            $r = $this->connection->query($this->sql['assoc_table']);
+            return $this->resultToBool($r);
+        }
+        return true;
+    }
+
+    function create_settings_table()
+    {
+        if (!$this->tableExists($this->settings_table_name)) {
+            $r = $this->connection->query($this->sql['settings_table']);
+            return $this->resultToBool($r);
+        }
+        return true;
+    }
+
+    /**
+     * @access private
+     */
+    function _get_auth()
+    {
+        return $this->connection->getOne($this->sql['get_auth']);
+    }
+
+    /**
+     * @access private
+     */
+    function _create_auth($str)
+    {
+        return $this->connection->query($this->sql['create_auth'],
+                                        array($str));
+    }
+
+    function getAuthKey()
+    {
+        $value = $this->_get_auth();
+        if (!$value) {
+            $auth_key =
+                Auth_OpenID_CryptUtil::randomString($this->AUTH_KEY_LEN);
+
+            $auth_key_s = $this->blobEncode($auth_key);
+            $this->_create_auth($auth_key_s);
+        } else {
+            $auth_key_s = $value;
+            $auth_key = $this->blobDecode($auth_key_s);
+        }
+
+        $this->connection->commit();
+
+        if (strlen($auth_key) != $this->AUTH_KEY_LEN) {
+            $fmt = "Expected %d-byte string for auth key. Got key of length %d";
+            trigger_error(sprintf($fmt, $this->AUTH_KEY_LEN, strlen($auth_key)),
+                          E_USER_WARNING);
+            return null;
+        }
+
+        return $auth_key;
+    }
+
+    /**
+     * @access private
+     */
+    function _set_assoc($server_url, $handle, $secret, $issued,
+                        $lifetime, $assoc_type)
+    {
+        return $this->connection->query($this->sql['set_assoc'],
+                                        array(
+                                              $server_url,
+                                              $handle,
+                                              $secret,
+                                              $issued,
+                                              $lifetime,
+                                              $assoc_type));
+    }
+
+    function storeAssociation($server_url, $association)
+    {
+        if ($this->resultToBool($this->_set_assoc(
+                                            $server_url,
+                                            $association->handle,
+                                            $this->blobEncode(
+                                                  $association->secret),
+                                            $association->issued,
+                                            $association->lifetime,
+                                            $association->assoc_type
+                                            ))) {
+            $this->connection->commit();
+        } else {
+            $this->connection->rollback();
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function _get_assoc($server_url, $handle)
+    {
+        $result = $this->connection->getRow($this->sql['get_assoc'],
+                                            array($server_url, $handle));
+        if ($this->isError($result)) {
+            return null;
+        } else {
+            return $result;
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function _get_assocs($server_url)
+    {
+        $result = $this->connection->getAll($this->sql['get_assocs'],
+                                            array($server_url));
+
+        if ($this->isError($result)) {
+            return array();
+        } else {
+            return $result;
+        }
+    }
+
+    function removeAssociation($server_url, $handle)
+    {
+        if ($this->_get_assoc($server_url, $handle) == null) {
+            return false;
+        }
+
+        if ($this->resultToBool($this->connection->query(
+                              $this->sql['remove_assoc'],
+                              array($server_url, $handle)))) {
+            $this->connection->commit();
+        } else {
+            $this->connection->rollback();
+        }
+
+        return true;
+    }
+
+    function getAssociation($server_url, $handle = null)
+    {
+        if ($handle !== null) {
+            $assoc = $this->_get_assoc($server_url, $handle);
+
+            $assocs = array();
+            if ($assoc) {
+                $assocs[] = $assoc;
+            }
+        } else {
+            $assocs = $this->_get_assocs($server_url);
+        }
+
+        if (!$assocs || (count($assocs) == 0)) {
+            return null;
+        } else {
+            $associations = array();
+
+            foreach ($assocs as $assoc_row) {
+                $assoc = new Auth_OpenID_Association($assoc_row['handle'],
+                                                     $assoc_row['secret'],
+                                                     $assoc_row['issued'],
+                                                     $assoc_row['lifetime'],
+                                                     $assoc_row['assoc_type']);
+
+                $assoc->secret = $this->blobDecode($assoc->secret);
+
+                if ($assoc->getExpiresIn() == 0) {
+                    $this->removeAssociation($server_url, $assoc->handle);
+                } else {
+                    $associations[] = array($assoc->issued, $assoc);
+                }
+            }
+
+            if ($associations) {
+                $issued = array();
+                $assocs = array();
+                foreach ($associations as $key => $assoc) {
+                    $issued[$key] = $assoc[0];
+                    $assocs[$key] = $assoc[1];
+                }
+
+                array_multisort($issued, SORT_DESC, $assocs, SORT_DESC,
+                                $associations);
+
+                // return the most recently issued one.
+                list($issued, $assoc) = $associations[0];
+                return $assoc;
+            } else {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function _add_nonce($nonce, $expires)
+    {
+        $sql = $this->sql['add_nonce'];
+        $result = $this->connection->query($sql, array($nonce, $expires));
+        return $this->resultToBool($result);
+    }
+
+    /**
+     * @access private
+     */
+    function storeNonce($nonce)
+    {
+        if ($this->_add_nonce($nonce, time())) {
+            $this->connection->commit();
+        } else {
+            $this->connection->rollback();
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function _get_nonce($nonce)
+    {
+        $result = $this->connection->getRow($this->sql['get_nonce'],
+                                            array($nonce));
+
+        if ($this->isError($result)) {
+            return null;
+        } else {
+            return $result;
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function _remove_nonce($nonce)
+    {
+        $this->connection->query($this->sql['remove_nonce'],
+                                 array($nonce));
+    }
+
+    function useNonce($nonce)
+    {
+        $row = $this->_get_nonce($nonce);
+
+        if ($row !== null) {
+            $nonce = $row['nonce'];
+            $timestamp = $row['expires'];
+            $nonce_age = time() - $timestamp;
+
+            if ($nonce_age > $this->max_nonce_age) {
+                $present = 0;
+            } else {
+                $present = 1;
+            }
+
+            $this->_remove_nonce($nonce);
+        } else {
+            $present = 0;
+        }
+
+        $this->connection->commit();
+
+        return $present;
+    }
+
+    /**
+     * "Octifies" a binary string by returning a string with escaped
+     * octal bytes.  This is used for preparing binary data for
+     * PostgreSQL BYTEA fields.
+     *
+     * @access private
+     */
+    function _octify($str)
+    {
+        $result = "";
+        for ($i = 0; $i < strlen($str); $i++) {
+            $ch = substr($str, $i, 1);
+            if ($ch == "\\") {
+                $result .= "\\\\\\\\";
+            } else if (ord($ch) == 0) {
+                $result .= "\\\\000";
+            } else {
+                $result .= "\\" . strval(decoct(ord($ch)));
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * "Unoctifies" octal-escaped data from PostgreSQL and returns the
+     * resulting ASCII (possibly binary) string.
+     *
+     * @access private
+     */
+    function _unoctify($str)
+    {
+        $result = "";
+        $i = 0;
+        while ($i < strlen($str)) {
+            $char = $str[$i];
+            if ($char == "\\") {
+                // Look to see if the next char is a backslash and
+                // append it.
+                if ($str[$i + 1] != "\\") {
+                    $octal_digits = substr($str, $i + 1, 3);
+                    $dec = octdec($octal_digits);
+                    $char = chr($dec);
+                    $i += 4;
+                } else {
+                    $char = "\\";
+                    $i += 2;
+                }
+            } else {
+                $i += 1;
+            }
+
+            $result .= $char;
+        }
+
+        return $result;
+    }
+}
+
+?>
diff --git a/lib/Auth/OpenID/SQLiteStore.php b/lib/Auth/OpenID/SQLiteStore.php
new file mode 100644
index 000000000..6351df3ca
--- /dev/null
+++ b/lib/Auth/OpenID/SQLiteStore.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * An SQLite store.
+ *
+ * @package OpenID
+ */
+
+/**
+ * Require the base class file.
+ */
+require_once "Auth/OpenID/SQLStore.php";
+
+/**
+ * An SQL store that uses SQLite as its backend.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SQLiteStore extends Auth_OpenID_SQLStore {
+    function setSQL()
+    {
+        $this->sql['nonce_table'] =
+            "CREATE TABLE %s (nonce CHAR(8) UNIQUE PRIMARY KEY, ".
+            "expires INTEGER)";
+
+        $this->sql['assoc_table'] =
+            "CREATE TABLE %s (server_url VARCHAR(2047), handle VARCHAR(255), ".
+            "secret BLOB(128), issued INTEGER, lifetime INTEGER, ".
+            "assoc_type VARCHAR(64), PRIMARY KEY (server_url, handle))";
+
+        $this->sql['settings_table'] =
+            "CREATE TABLE %s (setting VARCHAR(128) UNIQUE PRIMARY KEY, ".
+            "value BLOB(20))";
+
+        $this->sql['create_auth'] =
+            "INSERT INTO %s VALUES ('auth_key', ?)";
+
+        $this->sql['get_auth'] =
+            "SELECT value FROM %s WHERE setting = 'auth_key'";
+
+        $this->sql['set_assoc'] =
+            "INSERT OR REPLACE INTO %s VALUES (?, ?, ?, ?, ?, ?)";
+
+        $this->sql['get_assocs'] =
+            "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+            "WHERE server_url = ?";
+
+        $this->sql['get_assoc'] =
+            "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ".
+            "WHERE server_url = ? AND handle = ?";
+
+        $this->sql['remove_assoc'] =
+            "DELETE FROM %s WHERE server_url = ? AND handle = ?";
+
+        $this->sql['add_nonce'] =
+            "INSERT OR REPLACE INTO %s (nonce, expires) VALUES (?, ?)";
+
+        $this->sql['get_nonce'] =
+            "SELECT * FROM %s WHERE nonce = ?";
+
+        $this->sql['remove_nonce'] =
+            "DELETE FROM %s WHERE nonce = ?";
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/Server.php b/lib/Auth/OpenID/Server.php
new file mode 100644
index 000000000..b82bb4acb
--- /dev/null
+++ b/lib/Auth/OpenID/Server.php
@@ -0,0 +1,1307 @@
+<?php
+
+/**
+ * OpenID server protocol and logic.
+ * 
+ * Overview
+ *
+ * An OpenID server must perform three tasks:
+ *
+ *  1. Examine the incoming request to determine its nature and validity.
+ *  2. Make a decision about how to respond to this request.
+ *  3. Format the response according to the protocol.
+ * 
+ * The first and last of these tasks may performed by the
+ * 'decodeRequest' and 'encodeResponse' methods of the
+ * Auth_OpenID_Server object.  Who gets to do the intermediate task --
+ * deciding how to respond to the request -- will depend on what type
+ * of request it is.
+ *
+ * If it's a request to authenticate a user (a 'checkid_setup' or
+ * 'checkid_immediate' request), you need to decide if you will assert
+ * that this user may claim the identity in question.  Exactly how you
+ * do that is a matter of application policy, but it generally
+ * involves making sure the user has an account with your system and
+ * is logged in, checking to see if that identity is hers to claim,
+ * and verifying with the user that she does consent to releasing that
+ * information to the party making the request.
+ *
+ * Examine the properties of the Auth_OpenID_CheckIDRequest object,
+ * and if and when you've come to a decision, form a response by
+ * calling Auth_OpenID_CheckIDRequest::answer.
+ *
+ * Other types of requests relate to establishing associations between
+ * client and server and verifing the authenticity of previous
+ * communications.  Auth_OpenID_Server contains all the logic and data
+ * necessary to respond to such requests; just pass it to
+ * Auth_OpenID_Server::handleRequest.
+ *
+ * OpenID Extensions
+ * 
+ * Do you want to provide other information for your users in addition
+ * to authentication?  Version 1.2 of the OpenID protocol allows
+ * consumers to add extensions to their requests.  For example, with
+ * sites using the Simple Registration
+ * Extension
+ * (http://www.openidenabled.com/openid/simple-registration-extension/),
+ * a user can agree to have their nickname and e-mail address sent to
+ * a site when they sign up.
+ *
+ * Since extensions do not change the way OpenID authentication works,
+ * code to handle extension requests may be completely separate from
+ * the Auth_OpenID_Request class here.  But you'll likely want data
+ * sent back by your extension to be signed.  Auth_OpenID_Response
+ * provides methods with which you can add data to it which can be
+ * signed with the other data in the OpenID signature.
+ *
+ * For example:
+ *
+ *   //  when request is a checkid_* request
+ *   response = request.answer(True)
+ *   // this will a signed 'openid.sreg.timezone' parameter to the response
+ *   response.addField('sreg', 'timezone', 'America/Los_Angeles')
+ *
+ * Stores
+ *
+ * The OpenID server needs to maintain state between requests in order
+ * to function.  Its mechanism for doing this is called a store.  The
+ * store interface is defined in Interface.php.  Additionally, several
+ * concrete store implementations are provided, so that most sites
+ * won't need to implement a custom store.  For a store backed by flat
+ * files on disk, see Auth_OpenID_FileStore.  For stores based on
+ * MySQL, SQLite, or PostgreSQL, see the Auth_OpenID_SQLStore
+ * subclasses.
+ *
+ * Upgrading
+ *
+ * The keys by which a server looks up associations in its store have
+ * changed in version 1.2 of this library.  If your store has entries
+ * created from version 1.0 code, you should empty it.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Required imports
+ */
+require_once "Auth/OpenID.php";
+require_once "Auth/OpenID/Association.php";
+require_once "Auth/OpenID/CryptUtil.php";
+require_once "Auth/OpenID/BigMath.php";
+require_once "Auth/OpenID/DiffieHellman.php";
+require_once "Auth/OpenID/KVForm.php";
+require_once "Auth/OpenID/TrustRoot.php";
+require_once "Auth/OpenID/ServerRequest.php";
+
+define('AUTH_OPENID_HTTP_OK', 200);
+define('AUTH_OPENID_HTTP_REDIRECT', 302);
+define('AUTH_OPENID_HTTP_ERROR', 400);
+
+global $_Auth_OpenID_Request_Modes,
+    $_Auth_OpenID_OpenID_Prefix,
+    $_Auth_OpenID_Encode_Kvform,
+    $_Auth_OpenID_Encode_Url;
+
+/**
+ * @access private
+ */
+$_Auth_OpenID_Request_Modes = array('checkid_setup',
+                                    'checkid_immediate');
+
+/**
+ * @access private
+ */
+$_Auth_OpenID_OpenID_Prefix = "openid.";
+
+/**
+ * @access private
+ */
+$_Auth_OpenID_Encode_Kvform = array('kfvorm');
+
+/**
+ * @access private
+ */
+$_Auth_OpenID_Encode_Url = array('URL/redirect');
+
+/**
+ * @access private
+ */
+function _isError($obj, $cls = 'Auth_OpenID_ServerError')
+{
+    return is_a($obj, $cls);
+}
+
+/**
+ * An error class which gets instantiated and returned whenever an
+ * OpenID protocol error occurs.  Be prepared to use this in place of
+ * an ordinary server response.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_ServerError {
+    /**
+     * @access private
+     */
+    function Auth_OpenID_ServerError($query = null, $message = null)
+    {
+        $this->message = $message;
+        $this->query = $query;
+    }
+
+    /**
+     * Returns the return_to URL for the request which caused this
+     * error.
+     */
+    function hasReturnTo()
+    {
+        global $_Auth_OpenID_OpenID_Prefix;
+        if ($this->query) {
+            return array_key_exists($_Auth_OpenID_OpenID_Prefix .
+                                    'return_to', $this->query);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Encodes this error's response as a URL suitable for
+     * redirection.  If the response has no return_to, another
+     * Auth_OpenID_ServerError is returned.
+     */
+    function encodeToURL()
+    {
+        global $_Auth_OpenID_OpenID_Prefix;
+        $return_to = Auth_OpenID::arrayGet($this->query,
+                                           $_Auth_OpenID_OpenID_Prefix .
+                                           'return_to');
+        if (!$return_to) {
+            return new Auth_OpenID_ServerError(null, "no return_to URL");
+        }
+
+        return Auth_OpenID::appendArgs($return_to,
+                            array('openid.mode' => 'error',
+                                  'openid.error' => $this->toString()));
+    }
+
+    /**
+     * Encodes the response to key-value form.  This is a
+     * machine-readable format used to respond to messages which came
+     * directly from the consumer and not through the user-agent.  See
+     * the OpenID specification.
+     */
+    function encodeToKVForm()
+    {
+        return Auth_OpenID_KVForm::fromArray(
+                                      array('mode' => 'error',
+                                            'error' => $this->toString()));
+    }
+
+    /**
+     * Returns one of $_Auth_OpenID_Encode_Url,
+     * $_Auth_OpenID_Encode_Kvform, or null, depending on the type of
+     * encoding expected for this error's payload.
+     */
+    function whichEncoding()
+    {
+        global $_Auth_OpenID_Encode_Url,
+            $_Auth_OpenID_Encode_Kvform,
+            $_Auth_OpenID_Request_Modes;
+
+        if ($this->hasReturnTo()) {
+            return $_Auth_OpenID_Encode_Url;
+        }
+
+        $mode = Auth_OpenID::arrayGet($this->query, 'openid.mode');
+
+        if ($mode) {
+            if (!in_array($mode, $_Auth_OpenID_Request_Modes)) {
+                return $_Auth_OpenID_Encode_Kvform;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns this error message.
+     */
+    function toString()
+    {
+        if ($this->message) {
+            return $this->message;
+        } else {
+            return get_class($this) . " error";
+        }
+    }
+}
+
+/**
+ * An error indicating that the return_to URL is malformed.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_MalformedReturnURL extends Auth_OpenID_ServerError {
+    function Auth_OpenID_MalformedReturnURL($query, $return_to)
+    {
+        $this->return_to = $return_to;
+        parent::Auth_OpenID_ServerError($query, "malformed return_to URL");
+    }
+}
+
+/**
+ * This error is returned when the trust_root value is malformed.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_MalformedTrustRoot extends Auth_OpenID_ServerError {
+    function toString()
+    {
+        return "Malformed trust root";
+    }
+}
+
+/**
+ * The base class for all server request classes.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_Request {
+    var $mode = null;
+}
+
+/**
+ * A request to verify the validity of a previous response.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request {
+    var $mode = "check_authentication";
+    var $invalidate_handle = null;
+
+    function Auth_OpenID_CheckAuthRequest($assoc_handle, $sig, $signed,
+                                          $invalidate_handle = null)
+    {
+        $this->assoc_handle = $assoc_handle;
+        $this->sig = $sig;
+        $this->signed = $signed;
+        if ($invalidate_handle !== null) {
+            $this->invalidate_handle = $invalidate_handle;
+        }
+    }
+
+    function fromQuery($query)
+    {
+        global $_Auth_OpenID_OpenID_Prefix;
+
+        $required_keys = array('assoc_handle', 'sig', 'signed');
+
+        foreach ($required_keys as $k) {
+            if (!array_key_exists($_Auth_OpenID_OpenID_Prefix . $k,
+                                  $query)) {
+                return new Auth_OpenID_ServerError($query,
+                    sprintf("%s request missing required parameter %s from \
+                            query", "check_authentication", $k));
+            }
+        }
+
+        $assoc_handle = $query[$_Auth_OpenID_OpenID_Prefix . 'assoc_handle'];
+        $sig = $query[$_Auth_OpenID_OpenID_Prefix . 'sig'];
+        $signed_list = $query[$_Auth_OpenID_OpenID_Prefix . 'signed'];
+
+        $signed_list = explode(",", $signed_list);
+        $signed_pairs = array();
+
+        foreach ($signed_list as $field) {
+            if ($field == 'mode') {
+                // XXX KLUDGE HAX WEB PROTOCoL BR0KENNN
+                //
+                // openid.mode is currently check_authentication
+                // because that's the mode of this request.  But the
+                // signature was made on something with a different
+                // openid.mode.
+                $value = "id_res";
+            } else {
+                if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field,
+                                     $query)) {
+                    $value = $query[$_Auth_OpenID_OpenID_Prefix . $field];
+                } else {
+                    return new Auth_OpenID_ServerError($query,
+                          sprintf("Couldn't find signed field %r in query %s",
+                                  $field, var_export($query, true)));
+                }
+            }
+            $signed_pairs[] = array($field, $value);
+        }
+
+        $result = new Auth_OpenID_CheckAuthRequest($assoc_handle, $sig,
+                                                   $signed_pairs);
+        $result->invalidate_handle = Auth_OpenID::arrayGet($query,
+                    $_Auth_OpenID_OpenID_Prefix . 'invalidate_handle');
+        return $result;
+    }
+
+    function answer(&$signatory)
+    {
+        $is_valid = $signatory->verify($this->assoc_handle, $this->sig,
+                                       $this->signed);
+
+        // Now invalidate that assoc_handle so it this checkAuth
+        // message cannot be replayed.
+        $signatory->invalidate($this->assoc_handle, true);
+        $response = new Auth_OpenID_ServerResponse($this);
+        $response->fields['is_valid'] = $is_valid ? "true" : "false";
+
+        if ($this->invalidate_handle) {
+            $assoc = $signatory->getAssociation($this->invalidate_handle,
+                                                false);
+            if (!$assoc) {
+                $response->fields['invalidate_handle'] =
+                    $this->invalidate_handle;
+            }
+        }
+        return $response;
+    }
+}
+
+class Auth_OpenID_PlainTextServerSession {
+    /**
+     * An object that knows how to handle association requests with no
+     * session type.
+     */
+    var $session_type = 'plaintext';
+
+    function fromQuery($unused_request)
+    {
+        return new Auth_OpenID_PlainTextServerSession();
+    }
+
+    function answer($secret)
+    {
+        return array('mac_key' => base64_encode($secret));
+    }
+}
+
+class Auth_OpenID_DiffieHellmanServerSession {
+    /**
+     * An object that knows how to handle association requests with
+     * the Diffie-Hellman session type.
+     */
+
+    var $session_type = 'DH-SHA1';
+
+    function Auth_OpenID_DiffieHellmanServerSession($dh, $consumer_pubkey)
+    {
+        $this->dh = $dh;
+        $this->consumer_pubkey = $consumer_pubkey;
+    }
+
+    function fromQuery($query)
+    {
+        $dh_modulus = Auth_OpenID::arrayGet($query, 'openid.dh_modulus');
+        $dh_gen = Auth_OpenID::arrayGet($query, 'openid.dh_gen');
+
+        if ((($dh_modulus === null) && ($dh_gen !== null)) ||
+            (($dh_gen === null) && ($dh_modulus !== null))) {
+
+            if ($dh_modulus === null) {
+                $missing = 'modulus';
+            } else {
+                $missing = 'generator';
+            }
+
+            return new Auth_OpenID_ServerError(
+                                'If non-default modulus or generator is '.
+                                'supplied, both must be supplied.  Missing '.
+                                $missing);
+        }
+
+        $lib =& Auth_OpenID_getMathLib();
+
+        if ($dh_modulus || $dh_gen) {
+            $dh_modulus = $lib->base64ToLong($dh_modulus);
+            $dh_gen = $lib->base64ToLong($dh_gen);
+            if ($lib->cmp($dh_modulus, 0) == 0 ||
+                $lib->cmp($dh_gen, 0) == 0) {
+                return new Auth_OpenID_ServerError(
+                  $query, "Failed to parse dh_mod or dh_gen");
+            }
+            $dh = new Auth_OpenID_DiffieHellman($dh_modulus, $dh_gen);
+        } else {
+            $dh = new Auth_OpenID_DiffieHellman();
+        }
+
+        $consumer_pubkey = Auth_OpenID::arrayGet($query,
+                                                 'openid.dh_consumer_public');
+        if ($consumer_pubkey === null) {
+            return new Auth_OpenID_ServerError(
+                                  'Public key for DH-SHA1 session '.
+                                  'not found in query');
+        }
+
+        $consumer_pubkey =
+            $lib->base64ToLong($consumer_pubkey);
+
+        if ($consumer_pubkey === false) {
+            return new Auth_OpenID_ServerError($query,
+                                       "dh_consumer_public is not base64");
+        }
+
+        return new Auth_OpenID_DiffieHellmanServerSession($dh,
+                                                          $consumer_pubkey);
+    }
+
+    function answer($secret)
+    {
+        $lib =& Auth_OpenID_getMathLib();
+        $mac_key = $this->dh->xorSecret($this->consumer_pubkey, $secret);
+        return array(
+           'dh_server_public' =>
+                $lib->longToBase64($this->dh->public),
+           'enc_mac_key' => base64_encode($mac_key));
+    }
+}
+
+/**
+ * A request to associate with the server.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request {
+    var $mode = "associate";
+    var $assoc_type = 'HMAC-SHA1';
+
+    function Auth_OpenID_AssociateRequest(&$session)
+    {
+        $this->session =& $session;
+    }
+
+    function fromQuery($query)
+    {
+        global $_Auth_OpenID_OpenID_Prefix;
+
+        $session_classes = array(
+                 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanServerSession',
+                 null => 'Auth_OpenID_PlainTextServerSession');
+
+        $session_type = null;
+
+        if (array_key_exists($_Auth_OpenID_OpenID_Prefix . 'session_type',
+                             $query)) {
+            $session_type = $query[$_Auth_OpenID_OpenID_Prefix .
+                                   'session_type'];
+        }
+
+        if (!array_key_exists($session_type, $session_classes)) {
+            return new Auth_OpenID_ServerError($query,
+                                    "Unknown session type $session_type");
+        }
+
+        $session_cls = $session_classes[$session_type];
+        $session = call_user_func_array(array($session_cls, 'fromQuery'),
+                                        array($query));
+
+        if (($session === null) || (_isError($session))) {
+            return new Auth_OpenID_ServerError($query,
+                                     "Error parsing $session_type session");
+        }
+
+        return new Auth_OpenID_AssociateRequest($session);
+    }
+
+    function answer($assoc)
+    {
+        $ml =& Auth_OpenID_getMathLib();
+        $response = new Auth_OpenID_ServerResponse($this);
+
+        $response->fields = array('expires_in' => $assoc->getExpiresIn(),
+                                  'assoc_type' => 'HMAC-SHA1',
+                                  'assoc_handle' => $assoc->handle);
+
+        $r = $this->session->answer($assoc->secret);
+        foreach ($r as $k => $v) {
+            $response->fields[$k] = $v;
+        }
+
+        if ($this->session->session_type != 'plaintext') {
+            $response->fields['session_type'] = $this->session->session_type;
+        }
+
+        return $response;
+    }
+}
+
+/**
+ * A request to confirm the identity of a user.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request {
+    var $mode = "checkid_setup"; // or "checkid_immediate"
+    var $immediate = false;
+    var $trust_root = null;
+
+    function make($query, $identity, $return_to, $trust_root = null,
+                  $immediate = false, $assoc_handle = null)
+    {
+        if (!Auth_OpenID_TrustRoot::_parse($return_to)) {
+            return new Auth_OpenID_MalformedReturnURL($query, $return_to);
+        }
+
+        $r = new Auth_OpenID_CheckIDRequest($identity, $return_to,
+                                            $trust_root, $immediate,
+                                            $assoc_handle);
+
+        if (!$r->trustRootValid()) {
+            return new Auth_OpenID_UntrustedReturnURL($return_to,
+                                                      $trust_root);
+        } else {
+            return $r;
+        }
+    }
+
+    function Auth_OpenID_CheckIDRequest($identity, $return_to,
+                                        $trust_root = null, $immediate = false,
+                                        $assoc_handle = null)
+    {
+        $this->identity = $identity;
+        $this->return_to = $return_to;
+        $this->trust_root = $trust_root;
+        $this->assoc_handle = $assoc_handle;
+
+        if ($immediate) {
+            $this->immediate = true;
+            $this->mode = "checkid_immediate";
+        } else {
+            $this->immediate = false;
+            $this->mode = "checkid_setup";
+        }
+    }
+
+    function fromQuery($query)
+    {
+        global $_Auth_OpenID_OpenID_Prefix;
+
+        $mode = $query[$_Auth_OpenID_OpenID_Prefix . 'mode'];
+        $immediate = null;
+
+        if ($mode == "checkid_immediate") {
+            $immediate = true;
+            $mode = "checkid_immediate";
+        } else {
+            $immediate = false;
+            $mode = "checkid_setup";
+        }
+
+        $required = array('identity',
+                          'return_to');
+
+        $optional = array('trust_root',
+                          'assoc_handle');
+
+        $values = array();
+
+        foreach ($required as $field) {
+            if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field,
+                                 $query)) {
+                $value = $query[$_Auth_OpenID_OpenID_Prefix . $field];
+            } else {
+                return new Auth_OpenID_ServerError($query,
+                               sprintf("Missing required field %s from request",
+                                       $field));
+            }
+            $values[$field] = $value;
+        }
+
+        foreach ($optional as $field) {
+            $value = null;
+            if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field,
+                                 $query)) {
+                $value = $query[$_Auth_OpenID_OpenID_Prefix. $field];
+            }
+            if ($value) {
+                $values[$field] = $value;
+            }
+        }
+
+        if (!Auth_OpenID_TrustRoot::_parse($values['return_to'])) {
+            return new Auth_OpenID_MalformedReturnURL($query,
+                                                      $values['return_to']);
+        }
+
+        $obj = Auth_OpenID_CheckIDRequest::make($query,
+                                   $values['identity'],
+                                   $values['return_to'],
+                                   Auth_OpenID::arrayGet($values,
+                                                         'trust_root', null),
+                                   $immediate);
+
+        if (is_a($obj, 'Auth_OpenID_ServerError')) {
+            return $obj;
+        }
+
+        if (Auth_OpenID::arrayGet($values, 'assoc_handle')) {
+            $obj->assoc_handle = $values['assoc_handle'];
+        }
+
+        return $obj;
+    }
+
+    function trustRootValid()
+    {
+        if (!$this->trust_root) {
+            return true;
+        }
+
+        $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root);
+        if ($tr === false) {
+            return new Auth_OpenID_MalformedTrustRoot(null, $this->trust_root);
+        }
+
+        return Auth_OpenID_TrustRoot::match($this->trust_root,
+                                            $this->return_to);
+    }
+
+    function answer($allow, $server_url = null)
+    {
+        if ($allow || $this->immediate) {
+            $mode = 'id_res';
+        } else {
+            $mode = 'cancel';
+        }
+
+        $response = new Auth_OpenID_CheckIDResponse($this, $mode);
+
+        if ($allow) {
+            $response->fields['identity'] = $this->identity;
+            $response->fields['return_to'] = $this->return_to;
+            if (!$this->trustRootValid()) {
+                return new Auth_OpenID_UntrustedReturnURL($this->return_to,
+                                                          $this->trust_root);
+            }
+        } else {
+            $response->signed = array();
+            if ($this->immediate) {
+                if (!$server_url) {
+                    return new Auth_OpenID_ServerError(null,
+                                 'setup_url is required for $allow=false \
+                                  in immediate mode.');
+                }
+
+                $setup_request =& new Auth_OpenID_CheckIDRequest(
+                                                $this->identity,
+                                                $this->return_to,
+                                                $this->trust_root,
+                                                false,
+                                                $this->assoc_handle);
+
+                $setup_url = $setup_request->encodeToURL($server_url);
+
+                $response->fields['user_setup_url'] = $setup_url;
+            }
+        }
+
+        return $response;
+    }
+
+    function encodeToURL($server_url)
+    {
+        global $_Auth_OpenID_OpenID_Prefix;
+
+        // Imported from the alternate reality where these classes are
+        // used in both the client and server code, so Requests are
+        // Encodable too.  That's right, code imported from alternate
+        // realities all for the love of you, id_res/user_setup_url.
+
+        $q = array('mode' => $this->mode,
+                   'identity' => $this->identity,
+                   'return_to' => $this->return_to);
+
+        if ($this->trust_root) {
+            $q['trust_root'] = $this->trust_root;
+        }
+
+        if ($this->assoc_handle) {
+            $q['assoc_handle'] = $this->assoc_handle;
+        }
+
+        $_q = array();
+
+        foreach ($q as $k => $v) {
+            $_q[$_Auth_OpenID_OpenID_Prefix . $k] = $v;
+        }
+
+        return Auth_OpenID::appendArgs($server_url, $_q);
+    }
+
+    function getCancelURL()
+    {
+        global $_Auth_OpenID_OpenID_Prefix;
+
+        if ($this->immediate) {
+            return new Auth_OpenID_ServerError(null,
+                                               "Cancel is not an appropriate \
+                                               response to immediate mode \
+                                               requests.");
+        }
+
+        return Auth_OpenID::appendArgs($this->return_to,
+                              array($_Auth_OpenID_OpenID_Prefix . 'mode' =>
+                                    'cancel'));
+    }
+}
+
+/**
+ * This class encapsulates the response to an OpenID server request.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_ServerResponse {
+
+    function Auth_OpenID_ServerResponse($request)
+    {
+        $this->request = $request;
+        $this->fields = array();
+    }
+
+    function whichEncoding()
+    {
+        global $_Auth_OpenID_Encode_Kvform,
+            $_Auth_OpenID_Request_Modes,
+            $_Auth_OpenID_Encode_Url;
+
+        if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) {
+            return $_Auth_OpenID_Encode_Url;
+        } else {
+            return $_Auth_OpenID_Encode_Kvform;
+        }
+    }
+
+    function encodeToURL()
+    {
+        global $_Auth_OpenID_OpenID_Prefix;
+
+        $fields = array();
+
+        foreach ($this->fields as $k => $v) {
+            $fields[$_Auth_OpenID_OpenID_Prefix . $k] = $v;
+        }
+
+        return Auth_OpenID::appendArgs($this->request->return_to, $fields);
+    }
+
+    function encodeToKVForm()
+    {
+        return Auth_OpenID_KVForm::fromArray($this->fields);
+    }
+}
+
+/**
+ * A response to a checkid request.
+ *
+ * @access private
+ * @package OpenID
+ */
+class Auth_OpenID_CheckIDResponse extends Auth_OpenID_ServerResponse {
+
+    function Auth_OpenID_CheckIDResponse(&$request, $mode = 'id_res')
+    {
+        parent::Auth_OpenID_ServerResponse($request);
+        $this->fields['mode'] = $mode;
+        $this->signed = array();
+
+        if ($mode == 'id_res') {
+            array_push($this->signed, 'mode', 'identity', 'return_to');
+        }
+    }
+
+    function addField($namespace, $key, $value, $signed = true)
+    {
+        if ($namespace) {
+            $key = sprintf('%s.%s', $namespace, $key);
+        }
+        $this->fields[$key] = $value;
+        if ($signed && !in_array($key, $this->signed)) {
+            $this->signed[] = $key;
+        }
+    }
+
+    function addFields($namespace, $fields, $signed = true)
+    {
+        foreach ($fields as $k => $v) {
+            $this->addField($namespace, $k, $v, $signed);
+        }
+    }
+
+    function update($namespace, $other)
+    {
+        $namespaced_fields = array();
+
+        foreach ($other->fields as $k => $v) {
+            $name = sprintf('%s.%s', $namespace, $k);
+
+            $namespaced_fields[$name] = $v;
+        }
+
+        $this->fields = array_merge($this->fields, $namespaced_fields);
+        $this->signed = array_merge($this->signed, $other->signed);
+    }
+}
+
+/**
+ * A web-capable response object which you can use to generate a
+ * user-agent response.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_WebResponse {
+    var $code = AUTH_OPENID_HTTP_OK;
+    var $body = "";
+
+    function Auth_OpenID_WebResponse($code = null, $headers = null,
+                                     $body = null)
+    {
+        if ($code) {
+            $this->code = $code;
+        }
+
+        if ($headers !== null) {
+            $this->headers = $headers;
+        } else {
+            $this->headers = array();
+        }
+
+        if ($body !== null) {
+            $this->body = $body;
+        }
+    }
+}
+
+/**
+ * Responsible for the signature of query data and the verification of
+ * OpenID signature values.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Signatory {
+
+    // = 14 * 24 * 60 * 60; # 14 days, in seconds
+    var $SECRET_LIFETIME = 1209600;
+
+    // keys have a bogus server URL in them because the filestore
+    // really does expect that key to be a URL.  This seems a little
+    // silly for the server store, since I expect there to be only one
+    // server URL.
+    var $normal_key = 'http://localhost/|normal';
+    var $dumb_key = 'http://localhost/|dumb';
+
+    /**
+     * Create a new signatory using a given store.
+     */
+    function Auth_OpenID_Signatory(&$store)
+    {
+        // assert store is not None
+        $this->store =& $store;
+    }
+
+    /**
+     * Verify, using a given association handle, a signature with
+     * signed key-value pairs from an HTTP request.
+     */
+    function verify($assoc_handle, $sig, $signed_pairs)
+    {
+        $assoc = $this->getAssociation($assoc_handle, true);
+        if (!$assoc) {
+            // oidutil.log("failed to get assoc with handle %r to verify sig %r"
+            //             % (assoc_handle, sig))
+            return false;
+        }
+
+        $expected_sig = base64_encode($assoc->sign($signed_pairs));
+
+        return $sig == $expected_sig;
+    }
+
+    /**
+     * Given a response, sign the fields in the response's 'signed'
+     * list, and insert the signature into the response.
+     */
+    function sign($response)
+    {
+        $signed_response = $response;
+        $assoc_handle = $response->request->assoc_handle;
+
+        if ($assoc_handle) {
+            // normal mode
+            $assoc = $this->getAssociation($assoc_handle, false);
+            if (!$assoc) {
+                // fall back to dumb mode
+                $signed_response->fields['invalidate_handle'] = $assoc_handle;
+                $assoc = $this->createAssociation(true);
+            }
+        } else {
+            // dumb mode.
+            $assoc = $this->createAssociation(true);
+        }
+
+        $signed_response->fields['assoc_handle'] = $assoc->handle;
+        $assoc->addSignature($signed_response->signed,
+                             $signed_response->fields, '');
+        return $signed_response;
+    }
+
+    /**
+     * Make a new association.
+     */
+    function createAssociation($dumb = true, $assoc_type = 'HMAC-SHA1')
+    {
+        $secret = Auth_OpenID_CryptUtil::getBytes(20);
+        $uniq = base64_encode(Auth_OpenID_CryptUtil::getBytes(4));
+        $handle = sprintf('{%s}{%x}{%s}', $assoc_type, intval(time()), $uniq);
+
+        $assoc = Auth_OpenID_Association::fromExpiresIn(
+                      $this->SECRET_LIFETIME, $handle, $secret, $assoc_type);
+
+        if ($dumb) {
+            $key = $this->dumb_key;
+        } else {
+            $key = $this->normal_key;
+        }
+
+        $this->store->storeAssociation($key, $assoc);
+        return $assoc;
+    }
+
+    /**
+     * Given an association handle, get the association from the
+     * store, or return a ServerError or null if something goes wrong.
+     */
+    function getAssociation($assoc_handle, $dumb)
+    {
+        if ($assoc_handle === null) {
+            return new Auth_OpenID_ServerError(null,
+                                     "assoc_handle must not be null");
+        }
+
+        if ($dumb) {
+            $key = $this->dumb_key;
+        } else {
+            $key = $this->normal_key;
+        }
+
+        $assoc = $this->store->getAssociation($key, $assoc_handle);
+
+        if (($assoc !== null) && ($assoc->getExpiresIn() <= 0)) {
+            $this->store->removeAssociation($key, $assoc_handle);
+            $assoc = null;
+        }
+
+        return $assoc;
+    }
+
+    /**
+     * Invalidate a given association handle.
+     */
+    function invalidate($assoc_handle, $dumb)
+    {
+        if ($dumb) {
+            $key = $this->dumb_key;
+        } else {
+            $key = $this->normal_key;
+        }
+        $this->store->removeAssociation($key, $assoc_handle);
+    }
+}
+
+/**
+ * Encode an Auth_OpenID_Response to an Auth_OpenID_WebResponse.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Encoder {
+
+    var $responseFactory = 'Auth_OpenID_WebResponse';
+
+    /**
+     * Encode an Auth_OpenID_Response and return an
+     * Auth_OpenID_WebResponse.
+     */
+    function encode(&$response)
+    {
+        global $_Auth_OpenID_Encode_Kvform,
+            $_Auth_OpenID_Encode_Url;
+
+        $cls = $this->responseFactory;
+
+        $encode_as = $response->whichEncoding();
+        if ($encode_as == $_Auth_OpenID_Encode_Kvform) {
+            $wr = new $cls(null, null, $response->encodeToKVForm());
+            if (is_a($response, 'Auth_OpenID_ServerError')) {
+                $wr->code = AUTH_OPENID_HTTP_ERROR;
+            }
+        } else if ($encode_as == $_Auth_OpenID_Encode_Url) {
+            $location = $response->encodeToURL();
+            $wr = new $cls(AUTH_OPENID_HTTP_REDIRECT,
+                           array('location' => $location));
+        } else {
+            return new Auth_OpenID_EncodingError($response);
+        }
+        return $wr;
+    }
+}
+
+/**
+ * Returns true if the given response needs a signature.
+ *
+ * @access private
+ */
+function needsSigning($response)
+{
+    return (in_array($response->request->mode, array('checkid_setup',
+                                                     'checkid_immediate')) &&
+            $response->signed);
+}
+
+/**
+ * An encoder which also takes care of signing fields when required.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_SigningEncoder extends Auth_OpenID_Encoder {
+
+    function Auth_OpenID_SigningEncoder(&$signatory)
+    {
+        $this->signatory =& $signatory;
+    }
+
+    /**
+     * Sign an Auth_OpenID_Response and return an
+     * Auth_OpenID_WebResponse.
+     */
+    function encode(&$response)
+    {
+        // the isinstance is a bit of a kludge... it means there isn't
+        // really an adapter to make the interfaces quite match.
+        if (!is_a($response, 'Auth_OpenID_ServerError') &&
+            needsSigning($response)) {
+
+            if (!$this->signatory) {
+                return new Auth_OpenID_ServerError(null,
+                                       "Must have a store to sign request");
+            }
+            if (array_key_exists('sig', $response->fields)) {
+                return new Auth_OpenID_AlreadySigned($response);
+            }
+            $response = $this->signatory->sign($response);
+        }
+        return parent::encode($response);
+    }
+}
+
+/**
+ * Decode an incoming Auth_OpenID_WebResponse into an
+ * Auth_OpenID_Request.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Decoder {
+
+    function Auth_OpenID_Decoder()
+    {
+        global $_Auth_OpenID_OpenID_Prefix;
+        $this->prefix = $_Auth_OpenID_OpenID_Prefix;
+
+        $this->handlers = array(
+            'checkid_setup' => 'Auth_OpenID_CheckIDRequest',
+            'checkid_immediate' => 'Auth_OpenID_CheckIDRequest',
+            'check_authentication' => 'Auth_OpenID_CheckAuthRequest',
+            'associate' => 'Auth_OpenID_AssociateRequest'
+            );
+    }
+
+    /**
+     * Given an HTTP query in an array (key-value pairs), decode it
+     * into an Auth_OpenID_Request object.
+     */
+    function decode($query)
+    {
+        if (!$query) {
+            return null;
+        }
+
+        $myquery = array();
+
+        foreach ($query as $k => $v) {
+            if (strpos($k, $this->prefix) === 0) {
+                $myquery[$k] = $v;
+            }
+        }
+
+        if (!$myquery) {
+            return null;
+        }
+
+        $mode = Auth_OpenID::arrayGet($myquery, $this->prefix . 'mode');
+        if (!$mode) {
+            return new Auth_OpenID_ServerError($query,
+                           sprintf("No %s mode found in query", $this->prefix));
+        }
+
+        $handlerCls = Auth_OpenID::arrayGet($this->handlers, $mode,
+                                            $this->defaultDecoder($query));
+
+        if (!is_a($handlerCls, 'Auth_OpenID_ServerError')) {
+            return call_user_func_array(array($handlerCls, 'fromQuery'),
+                                        array($query));
+        } else {
+            return $handlerCls;
+        }
+    }
+
+    function defaultDecoder($query)
+    {
+        $mode = $query[$this->prefix . 'mode'];
+        return new Auth_OpenID_ServerError($query,
+                       sprintf("No decoder for mode %s", $mode));
+    }
+}
+
+/**
+ * An error that indicates an encoding problem occurred.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_EncodingError {
+    function Auth_OpenID_EncodingError(&$response)
+    {
+        $this->response =& $response;
+    }
+}
+
+/**
+ * An error that indicates that a response was already signed.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_AlreadySigned extends Auth_OpenID_EncodingError {
+    // This response is already signed.
+}
+
+/**
+ * An error that indicates that the given return_to is not under the
+ * given trust_root.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_UntrustedReturnURL extends Auth_OpenID_ServerError {
+    function Auth_OpenID_UntrustedReturnURL($return_to, $trust_root)
+    {
+        global $_Auth_OpenID_OpenID_Prefix;
+
+        $query = array(
+               $_Auth_OpenID_OpenID_Prefix . 'return_to' => $return_to,
+               $_Auth_OpenID_OpenID_Prefix . 'trust_root' => $trust_root);
+
+        parent::Auth_OpenID_ServerError($query);
+    }
+
+    function toString()
+    {
+        global $_Auth_OpenID_OpenID_Prefix;
+
+        $return_to = $this->query[$_Auth_OpenID_OpenID_Prefix . 'return_to'];
+        $trust_root = $this->query[$_Auth_OpenID_OpenID_Prefix . 'trust_root'];
+
+        return sprintf("return_to %s not under trust_root %s",
+                       $return_to, $trust_root);
+    }
+}
+
+/**
+ * An object that implements the OpenID protocol for a single URL.
+ *
+ * Use this object by calling getOpenIDResponse when you get any
+ * request for the server URL.
+ *
+ * @package OpenID
+ */
+class Auth_OpenID_Server {
+    function Auth_OpenID_Server(&$store)
+    {
+        $this->store =& $store;
+        $this->signatory =& new Auth_OpenID_Signatory($this->store);
+        $this->encoder =& new Auth_OpenID_SigningEncoder($this->signatory);
+        $this->decoder =& new Auth_OpenID_Decoder();
+    }
+
+    /**
+     * Handle a request.  Given an Auth_OpenID_Request object, call
+     * the appropriate Auth_OpenID_Server method to process the
+     * request and generate a response.
+     *
+     * @param Auth_OpenID_Request $request An Auth_OpenID_Request
+     * returned by Auth_OpenID_Server::decodeRequest.
+     *
+     * @return Auth_OpenID_Response $response A response object
+     * capable of generating a user-agent reply.
+     */
+    function handleRequest($request)
+    {
+        if (method_exists($this, "openid_" . $request->mode)) {
+            $handler = array($this, "openid_" . $request->mode);
+            return call_user_func($handler, $request);
+        }
+        return null;
+    }
+
+    /**
+     * The callback for 'check_authentication' messages.
+     *
+     * @access private
+     */
+    function openid_check_authentication(&$request)
+    {
+        return $request->answer($this->signatory);
+    }
+
+    /**
+     * The callback for 'associate' messages.
+     *
+     * @access private
+     */
+    function openid_associate(&$request)
+    {
+        $assoc = $this->signatory->createAssociation(false);
+        return $request->answer($assoc);
+    }
+
+    /**
+     * Encodes as response in the appropriate format suitable for
+     * sending to the user agent.
+     */
+    function encodeResponse(&$response)
+    {
+        return $this->encoder->encode($response);
+    }
+
+    /**
+     * Decodes a query args array into the appropriate
+     * Auth_OpenID_Request object.
+     */
+    function decodeRequest(&$query)
+    {
+        return $this->decoder->decode($query);
+    }
+}
+
+?>
diff --git a/lib/Auth/OpenID/ServerRequest.php b/lib/Auth/OpenID/ServerRequest.php
new file mode 100644
index 000000000..00728941a
--- /dev/null
+++ b/lib/Auth/OpenID/ServerRequest.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * OpenID Server Request
+ *
+ * @see Auth_OpenID_Server
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Imports
+ */
+require_once "Auth/OpenID.php";
+
+/**
+ * Object that holds the state of a request to the OpenID server
+ *
+ * With accessor functions to get at the internal request data.
+ *
+ * @see Auth_OpenID_Server
+ * @package OpenID
+ */
+class Auth_OpenID_ServerRequest {
+    function Auth_OpenID_ServerRequest()
+    {
+        $this->mode = null;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/TrustRoot.php b/lib/Auth/OpenID/TrustRoot.php
new file mode 100644
index 000000000..88eff295d
--- /dev/null
+++ b/lib/Auth/OpenID/TrustRoot.php
@@ -0,0 +1,243 @@
+<?php
+/**
+ * Functions for dealing with OpenID trust roots
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * A regular expression that matches a domain ending in a top-level domains.
+ * Used in checking trust roots for sanity.
+ *
+ * @access private
+ */
+define('Auth_OpenID___TLDs',
+       '/\.(com|edu|gov|int|mil|net|org|biz|info|name|museum|coop|aero|ac|' .
+       'ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|az|ba|bb|bd|be|bf|bg|' .
+       'bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|' .
+       'cm|cn|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|' .
+       'fi|fj|fk|fm|fo|fr|ga|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|' .
+       'gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|' .
+       'ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|' .
+       'ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|' .
+       'nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|' .
+       'ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|' .
+       'so|sr|st|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tm|tn|to|tp|tr|tt|tv|tw|tz|' .
+       'ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)$/');
+
+/**
+ * A wrapper for trust-root related functions
+ */
+class Auth_OpenID_TrustRoot {
+    /**
+     * Parse a URL into its trust_root parts.
+     *
+     * @static
+     *
+     * @access private
+     *
+     * @param string $trust_root The url to parse
+     *
+     * @return mixed $parsed Either an associative array of trust root
+     * parts or false if parsing failed.
+     */
+    function _parse($trust_root)
+    {
+        $parts = @parse_url($trust_root);
+        if ($parts === false) {
+            return false;
+        }
+        $required_parts = array('scheme', 'host');
+        $forbidden_parts = array('user', 'pass', 'fragment');
+        $keys = array_keys($parts);
+        if (array_intersect($keys, $required_parts) != $required_parts) {
+            return false;
+        }
+
+        if (array_intersect($keys, $forbidden_parts) != array()) {
+            return false;
+        }
+
+        // Return false if the original trust root value has more than
+        // one port specification.
+        if (preg_match("/:\/\/[^:]+(:\d+){2,}(\/|$)/", $trust_root)) {
+            return false;
+        }
+
+        $scheme = strtolower($parts['scheme']);
+        $allowed_schemes = array('http', 'https');
+        if (!in_array($scheme, $allowed_schemes)) {
+            return false;
+        }
+        $parts['scheme'] = $scheme;
+
+        $host = strtolower($parts['host']);
+        $hostparts = explode('*', $host);
+        switch (count($hostparts)) {
+        case 1:
+            $parts['wildcard'] = false;
+            break;
+        case 2:
+            if ($hostparts[0] ||
+                ($hostparts[1] && substr($hostparts[1], 0, 1) != '.')) {
+                return false;
+            }
+            $host = $hostparts[1];
+            $parts['wildcard'] = true;
+            break;
+        default:
+            return false;
+        }
+        if (strpos($host, ':') !== false) {
+            return false;
+        }
+
+        $parts['host'] = $host;
+
+        if (isset($parts['path'])) {
+            $path = strtolower($parts['path']);
+            if (substr($path, -1) != '/') {
+                $path .= '/';
+            }
+        } else {
+            $path = '/';
+        }
+        $parts['path'] = $path;
+        if (!isset($parts['port'])) {
+            $parts['port'] = false;
+        }
+        return $parts;
+    }
+
+    /**
+     * Is this trust root sane?
+     *
+     * A trust root is sane if it is syntactically valid and it has a
+     * reasonable domain name. Specifically, the domain name must be
+     * more than one level below a standard TLD or more than two
+     * levels below a two-letter tld.
+     *
+     * For example, '*.com' is not a sane trust root, but '*.foo.com'
+     * is.  '*.co.uk' is not sane, but '*.bbc.co.uk' is.
+     *
+     * This check is not always correct, but it attempts to err on the
+     * side of marking sane trust roots insane instead of marking
+     * insane trust roots sane. For example, 'kink.fm' is marked as
+     * insane even though it "should" (for some meaning of should) be
+     * marked sane.
+     *
+     * This function should be used when creating OpenID servers to
+     * alert the users of the server when a consumer attempts to get
+     * the user to accept a suspicious trust root.
+     *
+     * @static
+     * @param string $trust_root The trust root to check
+     * @return bool $sanity Whether the trust root looks OK
+     */
+    function isSane($trust_root)
+    {
+        $parts = Auth_OpenID_TrustRoot::_parse($trust_root);
+        if ($parts === false) {
+            return false;
+        }
+
+        // Localhost is a special case
+        if ($parts['host'] == 'localhost') {
+            return true;
+        }
+
+        // Get the top-level domain of the host. If it is not a valid TLD,
+        // it's not sane.
+        preg_match(Auth_OpenID___TLDs, $parts['host'], $matches);
+        if (!$matches) {
+            return false;
+        }
+        $tld = $matches[1];
+
+        // Require at least two levels of specificity for non-country
+        // tlds and three levels for country tlds.
+        $elements = explode('.', $parts['host']);
+        $n = count($elements);
+        if ($parts['wildcard']) {
+            $n -= 1;
+        }
+        if (strlen($tld) == 2) {
+            $n -= 1;
+        }
+        if ($n <= 1) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Does this URL match the given trust root?
+     *
+     * Return whether the URL falls under the given trust root. This
+     * does not check whether the trust root is sane. If the URL or
+     * trust root do not parse, this function will return false.
+     *
+     * @param string $trust_root The trust root to match against
+     *
+     * @param string $url The URL to check
+     *
+     * @return bool $matches Whether the URL matches against the
+     * trust root
+     */
+    function match($trust_root, $url)
+    {
+        $trust_root_parsed = Auth_OpenID_TrustRoot::_parse($trust_root);
+        $url_parsed = Auth_OpenID_TrustRoot::_parse($url);
+        if (!$trust_root_parsed || !$url_parsed) {
+            return false;
+        }
+
+        // Check hosts matching
+        if ($url_parsed['wildcard']) {
+            return false;
+        }
+        if ($trust_root_parsed['wildcard']) {
+            $host_tail = $trust_root_parsed['host'];
+            $host = $url_parsed['host'];
+            if ($host_tail &&
+                substr($host, -(strlen($host_tail))) != $host_tail &&
+                substr($host_tail, 1) != $host) {
+                return false;
+            }
+        } else {
+            if ($trust_root_parsed['host'] != $url_parsed['host']) {
+                return false;
+            }
+        }
+
+        // Check path and query matching
+        $base_path = $trust_root_parsed['path'];
+        $path = $url_parsed['path'];
+        if (!isset($trust_root_parsed['query'])) {
+            if (substr($path, 0, strlen($base_path)) != $base_path) {
+                return false;
+            }
+        } else {
+            $base_query = $trust_root_parsed['query'];
+            $query = @$url_parsed['query'];
+            $qplus = substr($query, 0, strlen($base_query) + 1);
+            $bqplus = $base_query . '&';
+            if ($base_path != $path ||
+                ($base_query != $query && $qplus != $bqplus)) {
+                return false;
+            }
+        }
+
+        // The port and scheme need to match exactly
+        return ($trust_root_parsed['scheme'] == $url_parsed['scheme'] &&
+                $url_parsed['port'] === $trust_root_parsed['port']);
+    }
+}
+?>
\ No newline at end of file
diff --git a/lib/Auth/OpenID/URINorm.php b/lib/Auth/OpenID/URINorm.php
new file mode 100644
index 000000000..d1c653ebe
--- /dev/null
+++ b/lib/Auth/OpenID/URINorm.php
@@ -0,0 +1,231 @@
+<?php
+
+/**
+ * URI normalization routines.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+require_once 'Services/Yadis/Misc.php';
+
+// from appendix B of rfc 3986 (http://www.ietf.org/rfc/rfc3986.txt)
+function Auth_OpenID_getURIPattern()
+{
+    return '&^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?&';
+}
+
+function Auth_OpenID_getAuthorityPattern()
+{
+    return '/^([^@]*@)?([^:]*)(:.*)?/';
+}
+
+function Auth_OpenID_getEncodedPattern()
+{
+    return '/%([0-9A-Fa-f]{2})/';
+}
+
+function Auth_OpenID_getUnreserved()
+{
+    $_unreserved = array();
+    for ($i = 0; $i < 256; $i++) {
+        $_unreserved[$i] = false;
+    }
+
+    for ($i = ord('A'); $i <= ord('Z'); $i++) {
+        $_unreserved[$i] = true;
+    }
+
+    for ($i = ord('0'); $i <= ord('9'); $i++) {
+        $_unreserved[$i] = true;
+    }
+
+    for ($i = ord('a'); $i <= ord('z'); $i++) {
+        $_unreserved[$i] = true;
+    }
+
+    $_unreserved[ord('-')] = true;
+    $_unreserved[ord('.')] = true;
+    $_unreserved[ord('_')] = true;
+    $_unreserved[ord('~')] = true;
+
+    return $_unreserved;
+}
+
+function Auth_OpenID_getEscapeRE()
+{
+    $parts = array();
+    foreach (array_merge(Services_Yadis_getUCSChars(),
+                         Services_Yadis_getIPrivateChars()) as $pair) {
+        list($m, $n) = $pair;
+        $parts[] = sprintf("%s-%s", chr($m), chr($n));
+    }
+
+    return sprintf('[%s]', implode('', $parts));
+}
+
+function Auth_OpenID_pct_encoded_replace_unreserved($mo)
+{
+    $_unreserved = Auth_OpenID_getUnreserved();
+
+    $i = intval($mo[1], 16);
+    if ($_unreserved[$i]) {
+        return chr($i);
+    } else {
+        return strtoupper($mo[0]);
+    }
+
+    return $mo[0];
+}
+
+function Auth_OpenID_pct_encoded_replace($mo)
+{
+    return chr(intval($mo[1], 16));
+}
+
+function Auth_OpenID_remove_dot_segments($path)
+{
+    $result_segments = array();
+    
+    while ($path) {
+        if (Services_Yadis_startswith($path, '../')) {
+            $path = substr($path, 3);
+        } else if (Services_Yadis_startswith($path, './')) {
+            $path = substr($path, 2);
+        } else if (Services_Yadis_startswith($path, '/./')) {
+            $path = substr($path, 2);
+        } else if ($path == '/.') {
+            $path = '/';
+        } else if (Services_Yadis_startswith($path, '/../')) {
+            $path = substr($path, 3);
+            if ($result_segments) {
+                array_pop($result_segments);
+            }
+        } else if ($path == '/..') {
+            $path = '/';
+            if ($result_segments) {
+                array_pop($result_segments);
+            }
+        } else if (($path == '..') ||
+                   ($path == '.')) {
+            $path = '';
+        } else {
+            $i = 0;
+            if ($path[0] == '/') {
+                $i = 1;
+            }
+            $i = strpos($path, '/', $i);
+            if ($i === false) {
+                $i = strlen($path);
+            }
+            $result_segments[] = substr($path, 0, $i);
+            $path = substr($path, $i);
+        }
+    }
+
+    return implode('', $result_segments);
+}
+
+function Auth_OpenID_urinorm($uri)
+{
+    $uri_matches = array();
+    preg_match(Auth_OpenID_getURIPattern(), $uri, $uri_matches);
+
+    if (count($uri_matches) < 9) {
+        for ($i = count($uri_matches); $i <= 9; $i++) {
+            $uri_matches[] = '';
+        }
+    }
+
+    $scheme = $uri_matches[2];
+    if ($scheme) {
+        $scheme = strtolower($scheme);
+    }
+
+    $scheme = $uri_matches[2];
+    if ($scheme === '') {
+        // No scheme specified
+        return null;
+    }
+
+    $scheme = strtolower($scheme);
+    if (!in_array($scheme, array('http', 'https'))) {
+        // Not an absolute HTTP or HTTPS URI
+        return null;
+    }
+
+    $authority = $uri_matches[4];
+    if ($authority === '') {
+        // Not an absolute URI
+        return null;
+    }
+
+    $authority_matches = array();
+    preg_match(Auth_OpenID_getAuthorityPattern(),
+               $authority, $authority_matches);
+    if (count($authority_matches) === 0) {
+        // URI does not have a valid authority
+        return null;
+    }
+
+    if (count($authority_matches) < 4) {
+        for ($i = count($authority_matches); $i <= 4; $i++) {
+            $authority_matches[] = '';
+        }
+    }
+
+    list($_whole, $userinfo, $host, $port) = $authority_matches;
+
+    if ($userinfo === null) {
+        $userinfo = '';
+    }
+
+    if (strpos($host, '%') !== -1) {
+        $host = strtolower($host);
+        $host = preg_replace_callback(
+                  Auth_OpenID_getEncodedPattern(),
+                  'Auth_OpenID_pct_encoded_replace', $host);
+        // NO IDNA.
+        // $host = unicode($host, 'utf-8').encode('idna');
+    } else {
+        $host = strtolower($host);
+    }
+
+    if ($port) {
+        if (($port == ':') ||
+            ($scheme == 'http' && $port == ':80') ||
+            ($scheme == 'https' && $port == ':443')) {
+            $port = '';
+        }
+    } else {
+        $port = '';
+    }
+
+    $authority = $userinfo . $host . $port;
+
+    $path = $uri_matches[5];
+    $path = preg_replace_callback(
+               Auth_OpenID_getEncodedPattern(),
+               'Auth_OpenID_pct_encoded_replace_unreserved', $path);
+
+    $path = Auth_OpenID_remove_dot_segments($path);
+    if (!$path) {
+        $path = '/';
+    }
+
+    $query = $uri_matches[6];
+    if ($query === null) {
+        $query = '';
+    }
+
+    $fragment = $uri_matches[8];
+    if ($fragment === null) {
+        $fragment = '';
+    }
+
+    return $scheme . '://' . $authority . $path . $query . $fragment;
+}
+
+?>
diff --git a/lib/Services/Yadis/HTTPFetcher.php b/lib/Services/Yadis/HTTPFetcher.php
new file mode 100644
index 000000000..97940a4d5
--- /dev/null
+++ b/lib/Services/Yadis/HTTPFetcher.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * This module contains the HTTP fetcher interface
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package Yadis
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+class Services_Yadis_HTTPResponse {
+    function Services_Yadis_HTTPResponse($final_url = null, $status = null,
+                                         $headers = null, $body = null)
+    {
+        $this->final_url = $final_url;
+        $this->status = $status;
+        $this->headers = $headers;
+        $this->body = $body;
+    }
+}
+
+/**
+ * This class is the interface for HTTP fetchers the Yadis library
+ * uses.  This interface is only important if you need to write a new
+ * fetcher for some reason.
+ *
+ * @access private
+ * @package Yadis
+ */
+class Services_Yadis_HTTPFetcher {
+
+    var $timeout = 20; // timeout in seconds.
+
+    /**
+     * Return whether a URL should be allowed. Override this method to
+     * conform to your local policy.
+     *
+     * By default, will attempt to fetch any http or https URL.
+     */
+    function allowedURL($url)
+    {
+        return $this->URLHasAllowedScheme($url);
+    }
+
+    /**
+     * Is this an http or https URL?
+     *
+     * @access private
+     */
+    function URLHasAllowedScheme($url)
+    {
+        return (bool)preg_match('/^https?:\/\//i', $url);
+    }
+
+    /**
+     * @access private
+     */
+    function _findRedirect($headers)
+    {
+        foreach ($headers as $line) {
+            if (strpos($line, "Location: ") === 0) {
+                $parts = explode(" ", $line, 2);
+                return $parts[1];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Fetches the specified URL using optional extra headers and
+     * returns the server's response.
+     *
+     * @param string $url The URL to be fetched.
+     * @param array $extra_headers An array of header strings
+     * (e.g. "Accept: text/html").
+     * @return mixed $result An array of ($code, $url, $headers,
+     * $body) if the URL could be fetched; null if the URL does not
+     * pass the URLHasAllowedScheme check or if the server's response
+     * is malformed.
+     */
+    function get($url, $headers)
+    {
+        trigger_error("not implemented", E_USER_ERROR);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Services/Yadis/Manager.php b/lib/Services/Yadis/Manager.php
new file mode 100644
index 000000000..524eea2ae
--- /dev/null
+++ b/lib/Services/Yadis/Manager.php
@@ -0,0 +1,496 @@
+<?php
+
+/**
+ * Yadis service manager to be used during yadis-driven authentication
+ * attempts.
+ *
+ * @package Yadis
+ */
+
+/**
+ * The base session class used by the Services_Yadis_Manager.  This
+ * class wraps the default PHP session machinery and should be
+ * subclassed if your application doesn't use PHP sessioning.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_PHPSession {
+    /**
+     * Set a session key/value pair.
+     *
+     * @param string $name The name of the session key to add.
+     * @param string $value The value to add to the session.
+     */
+    function set($name, $value)
+    {
+        $_SESSION[$name] = $value;
+    }
+
+    /**
+     * Get a key's value from the session.
+     *
+     * @param string $name The name of the key to retrieve.
+     * @param string $default The optional value to return if the key
+     * is not found in the session.
+     * @return string $result The key's value in the session or
+     * $default if it isn't found.
+     */
+    function get($name, $default=null)
+    {
+        if (array_key_exists($name, $_SESSION)) {
+            return $_SESSION[$name];
+        } else {
+            return $default;
+        }
+    }
+
+    /**
+     * Remove a key/value pair from the session.
+     *
+     * @param string $name The name of the key to remove.
+     */
+    function del($name)
+    {
+        unset($_SESSION[$name]);
+    }
+
+    /**
+     * Return the contents of the session in array form.
+     */
+    function contents()
+    {
+        return $_SESSION;
+    }
+}
+
+/**
+ * A session helper class designed to translate between arrays and
+ * objects.  Note that the class used must have a constructor that
+ * takes no parameters.  This is not a general solution, but it works
+ * for dumb objects that just need to have attributes set.  The idea
+ * is that you'll subclass this and override $this->check($data) ->
+ * bool to implement your own session data validation.
+ */
+class Services_Yadis_SessionLoader {
+    /**
+     * Override this.
+     */
+    function check($data)
+    {
+        return true;
+    }
+
+    /**
+     * Given a session data value (an array), this creates an object
+     * (returned by $this->newObject()) whose attributes and values
+     * are those in $data.  Returns null if $data lacks keys found in
+     * $this->requiredKeys().  Returns null if $this->check($data)
+     * evaluates to false.  Returns null if $this->newObject()
+     * evaluates to false.
+     */
+    function fromSession($data)
+    {
+        if (!$data) {
+            return null;
+        }
+
+        $required = $this->requiredKeys();
+
+        foreach ($required as $k) {
+            if (!array_key_exists($k, $data)) {
+                return null;
+            }
+        }
+
+        if (!$this->check($data)) {
+            return null;
+        }
+
+        $data = array_merge($data, $this->prepareForLoad($data));
+        $obj = $this->newObject($data);
+
+        if (!$obj) {
+            return null;
+        }
+
+        foreach ($required as $k) {
+            $obj->$k = $data[$k];
+        }
+
+        return $obj;
+    }
+
+    /**
+     * Prepares the data array by making any necessary changes.
+     * Returns an array whose keys and values will be used to update
+     * the original data array before calling $this->newObject($data).
+     */
+    function prepareForLoad($data)
+    {
+        return array();
+    }
+
+    /**
+     * Returns a new instance of this loader's class, using the
+     * session data to construct it if necessary.  The object need
+     * only be created; $this->fromSession() will take care of setting
+     * the object's attributes.
+     */
+    function newObject($data)
+    {
+        return null;
+    }
+
+    /**
+     * Returns an array of keys and values built from the attributes
+     * of $obj.  If $this->prepareForSave($obj) returns an array, its keys
+     * and values are used to update the $data array of attributes
+     * from $obj.
+     */
+    function toSession($obj)
+    {
+        $data = array();
+        foreach ($obj as $k => $v) {
+            $data[$k] = $v;
+        }
+
+        $extra = $this->prepareForSave($obj);
+
+        if ($extra && is_array($extra)) {
+            foreach ($extra as $k => $v) {
+                $data[$k] = $v;
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Override this.
+     */
+    function prepareForSave($obj)
+    {
+        return array();
+    }
+}
+
+class Auth_OpenID_ServiceEndpointLoader extends Services_Yadis_SessionLoader {
+    function newObject($data)
+    {
+        return new Auth_OpenID_ServiceEndpoint();
+    }
+
+    function requiredKeys()
+    {
+        $obj = new Auth_OpenID_ServiceEndpoint();
+        $data = array();
+        foreach ($obj as $k => $v) {
+            $data[] = $k;
+        }
+        return $data;
+    }
+
+    function check($data)
+    {
+        return is_array($data['type_uris']);
+    }
+}
+
+class Services_Yadis_ManagerLoader extends Services_Yadis_SessionLoader {
+    function requiredKeys()
+    {
+        return array('starting_url',
+                     'yadis_url',
+                     'services',
+                     'session_key',
+                     '_current',
+                     'stale');
+    }
+
+    function newObject($data)
+    {
+        return new Services_Yadis_Manager($data['starting_url'],
+                                          $data['yadis_url'],
+                                          $data['services'],
+                                          $data['session_key']);
+    }
+
+    function check($data)
+    {
+        return is_array($data['services']);
+    }
+
+    function prepareForLoad($data)
+    {
+        $loader = new Auth_OpenID_ServiceEndpointLoader();
+        $services = array();
+        foreach ($data['services'] as $s) {
+            $services[] = $loader->fromSession($s);
+        }
+        return array('services' => $services);
+    }
+
+    function prepareForSave($obj)
+    {
+        $loader = new Auth_OpenID_ServiceEndpointLoader();
+        $services = array();
+        foreach ($obj->services as $s) {
+            $services[] = $loader->toSession($s);
+        }
+        return array('services' => $services);
+    }
+}
+
+/**
+ * The Yadis service manager which stores state in a session and
+ * iterates over <Service> elements in a Yadis XRDS document and lets
+ * a caller attempt to use each one.  This is used by the Yadis
+ * library internally.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_Manager {
+
+    /**
+     * Intialize a new yadis service manager.
+     *
+     * @access private
+     */
+    function Services_Yadis_Manager($starting_url, $yadis_url,
+                                    $services, $session_key)
+    {
+        // The URL that was used to initiate the Yadis protocol
+        $this->starting_url = $starting_url;
+
+        // The URL after following redirects (the identifier)
+        $this->yadis_url = $yadis_url;
+
+        // List of service elements
+        $this->services = $services;
+
+        $this->session_key = $session_key;
+
+        // Reference to the current service object
+        $this->_current = null;
+
+        // Stale flag for cleanup if PHP lib has trouble.
+        $this->stale = false;
+    }
+
+    /**
+     * @access private
+     */
+    function length()
+    {
+        // How many untried services remain?
+        return count($this->services);
+    }
+
+    /**
+     * Return the next service
+     *
+     * $this->current() will continue to return that service until the
+     * next call to this method.
+     */
+    function nextService()
+    {
+
+        if ($this->services) {
+            $this->_current = array_shift($this->services);
+        } else {
+            $this->_current = null;
+        }
+
+        return $this->_current;
+    }
+
+    /**
+     * @access private
+     */
+    function current()
+    {
+        // Return the current service.
+        // Returns None if there are no services left.
+        return $this->_current;
+    }
+
+    /**
+     * @access private
+     */
+    function forURL($url)
+    {
+        return in_array($url, array($this->starting_url, $this->yadis_url));
+    }
+
+    /**
+     * @access private
+     */
+    function started()
+    {
+        // Has the first service been returned?
+        return $this->_current !== null;
+    }
+}
+
+/**
+ * State management for discovery.
+ *
+ * High-level usage pattern is to call .getNextService(discover) in
+ * order to find the next available service for this user for this
+ * session. Once a request completes, call .finish() to clean up the
+ * session state.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_Discovery {
+
+    /**
+     * @access private
+     */
+    var $DEFAULT_SUFFIX = 'auth';
+
+    /**
+     * @access private
+     */
+    var $PREFIX = '_yadis_services_';
+
+    /**
+     * Initialize a discovery object.
+     *
+     * @param Services_Yadis_PHPSession $session An object which
+     * implements the Services_Yadis_PHPSession API.
+     * @param string $url The URL on which to attempt discovery.
+     * @param string $session_key_suffix The optional session key
+     * suffix override.
+     */
+    function Services_Yadis_Discovery(&$session, $url,
+                                      $session_key_suffix = null)
+    {
+        /// Initialize a discovery object
+        $this->session =& $session;
+        $this->url = $url;
+        if ($session_key_suffix === null) {
+            $session_key_suffix = $this->DEFAULT_SUFFIX;
+        }
+
+        $this->session_key_suffix = $session_key_suffix;
+        $this->session_key = $this->PREFIX . $this->session_key_suffix;
+    }
+
+    /**
+     * Return the next authentication service for the pair of
+     * user_input and session. This function handles fallback.
+     */
+    function getNextService($discover_cb, &$fetcher)
+    {
+        $manager = $this->getManager();
+        if (!$manager || (!$manager->services)) {
+            $this->destroyManager();
+            $http_response = array();
+
+            $services = call_user_func($discover_cb, $this->url,
+                                       $fetcher);
+
+            $manager = $this->createManager($services, $this->url);
+        }
+
+        if ($manager) {
+            $loader = new Services_Yadis_ManagerLoader();
+            $service = $manager->nextService();
+            $this->session->set($this->session_key,
+                                serialize($loader->toSession($manager)));
+        } else {
+            $service = null;
+        }
+
+        return $service;
+    }
+
+    /**
+     * Clean up Yadis-related services in the session and return the
+     * most-recently-attempted service from the manager, if one
+     * exists.
+     */
+    function cleanup()
+    {
+        $manager = $this->getManager();
+        if ($manager) {
+            $service = $manager->current();
+            $this->destroyManager();
+        } else {
+            $service = null;
+        }
+
+        return $service;
+    }
+
+    /**
+     * @access private
+     */
+    function getSessionKey()
+    {
+        // Get the session key for this starting URL and suffix
+        return $this->PREFIX . $this->session_key_suffix;
+    }
+
+    /**
+     * @access private
+     */
+    function &getManager()
+    {
+        // Extract the YadisServiceManager for this object's URL and
+        // suffix from the session.
+
+        $manager_str = $this->session->get($this->getSessionKey());
+        $manager = null;
+
+        if ($manager_str !== null) {
+            $loader = new Services_Yadis_ManagerLoader();
+            $manager = $loader->fromSession(unserialize($manager_str));
+        }
+
+        if ($manager && $manager->forURL($this->url)) {
+            return $manager;
+        } else {
+            $unused = null;
+            return $unused;
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function &createManager($services, $yadis_url = null)
+    {
+        $key = $this->getSessionKey();
+        if ($this->getManager()) {
+            return $this->getManager();
+        }
+
+        if ($services) {
+            $loader = new Services_Yadis_ManagerLoader();
+            $manager = new Services_Yadis_Manager($this->url, $yadis_url,
+                                              $services, $key);
+            $this->session->set($this->session_key,
+                                serialize($loader->toSession($manager)));
+            return $manager;
+        } else {
+            // Oh, PHP.
+            $unused = null;
+            return $unused;
+        }
+    }
+
+    /**
+     * @access private
+     */
+    function destroyManager()
+    {
+        if ($this->getManager() !== null) {
+            $key = $this->getSessionKey();
+            $this->session->del($key);
+        }
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Services/Yadis/Misc.php b/lib/Services/Yadis/Misc.php
new file mode 100644
index 000000000..794b62ead
--- /dev/null
+++ b/lib/Services/Yadis/Misc.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * Miscellaneous utility values and functions for OpenID and Yadis.
+ *
+ * @package OpenID
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+function Services_Yadis_getUCSChars()
+{
+    return array(
+                 array(0xA0, 0xD7FF),
+                 array(0xF900, 0xFDCF),
+                 array(0xFDF0, 0xFFEF),
+                 array(0x10000, 0x1FFFD),
+                 array(0x20000, 0x2FFFD),
+                 array(0x30000, 0x3FFFD),
+                 array(0x40000, 0x4FFFD),
+                 array(0x50000, 0x5FFFD),
+                 array(0x60000, 0x6FFFD),
+                 array(0x70000, 0x7FFFD),
+                 array(0x80000, 0x8FFFD),
+                 array(0x90000, 0x9FFFD),
+                 array(0xA0000, 0xAFFFD),
+                 array(0xB0000, 0xBFFFD),
+                 array(0xC0000, 0xCFFFD),
+                 array(0xD0000, 0xDFFFD),
+                 array(0xE1000, 0xEFFFD)
+                 );
+}
+
+function Services_Yadis_getIPrivateChars()
+{
+    return array(
+                 array(0xE000, 0xF8FF),
+                 array(0xF0000, 0xFFFFD),
+                 array(0x100000, 0x10FFFD)
+                 );
+}
+
+function Services_Yadis_pct_escape_unicode($char_match)
+{
+    $c = $char_match[0];
+    $result = "";
+    for ($i = 0; $i < strlen($c); $i++) {
+        $result .= "%".sprintf("%X", ord($c[$i]));
+    }
+    return $result;
+}
+
+function Services_Yadis_startswith($s, $stuff)
+{
+    return strpos($s, $stuff) === 0;
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Services/Yadis/ParanoidHTTPFetcher.php b/lib/Services/Yadis/ParanoidHTTPFetcher.php
new file mode 100644
index 000000000..e14799f83
--- /dev/null
+++ b/lib/Services/Yadis/ParanoidHTTPFetcher.php
@@ -0,0 +1,177 @@
+<?php
+
+/**
+ * This module contains the CURL-based HTTP fetcher implementation.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package Yadis
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Interface import
+ */
+require_once "Services/Yadis/HTTPFetcher.php";
+
+/**
+ * A paranoid {@link Services_Yadis_HTTPFetcher} class which uses CURL
+ * for fetching.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_ParanoidHTTPFetcher extends Services_Yadis_HTTPFetcher {
+    function Services_Yadis_ParanoidHTTPFetcher()
+    {
+        $this->reset();
+    }
+
+    function reset()
+    {
+        $this->headers = array();
+        $this->data = "";
+    }
+
+    /**
+     * @access private
+     */
+    function _writeHeader($ch, $header)
+    {
+        array_push($this->headers, rtrim($header));
+        return strlen($header);
+    }
+
+    /**
+     * @access private
+     */
+    function _writeData($ch, $data)
+    {
+        $this->data .= $data;
+        return strlen($data);
+    }
+
+    function get($url, $extra_headers = null)
+    {
+        $stop = time() + $this->timeout;
+        $off = $this->timeout;
+
+        $redir = true;
+
+        while ($redir && ($off > 0)) {
+            $this->reset();
+
+            $c = curl_init();
+            if (defined('CURLOPT_NOSIGNAL')) {
+                curl_setopt($c, CURLOPT_NOSIGNAL, true);
+            }
+
+            if (!$this->allowedURL($url)) {
+                trigger_error(sprintf("Fetching URL not allowed: %s", $url),
+                              E_USER_WARNING);
+                return null;
+            }
+
+            curl_setopt($c, CURLOPT_WRITEFUNCTION,
+                        array(&$this, "_writeData"));
+            curl_setopt($c, CURLOPT_HEADERFUNCTION,
+                        array(&$this, "_writeHeader"));
+
+            if ($extra_headers) {
+                curl_setopt($c, CURLOPT_HTTPHEADER, $extra_headers);
+            }
+
+            curl_setopt($c, CURLOPT_TIMEOUT, $off);
+            curl_setopt($c, CURLOPT_URL, $url);
+
+            curl_exec($c);
+
+            $code = curl_getinfo($c, CURLINFO_HTTP_CODE);
+            $body = $this->data;
+            $headers = $this->headers;
+
+            if (!$code) {
+                return null;
+            }
+
+            if (in_array($code, array(301, 302, 303, 307))) {
+                $url = $this->_findRedirect($headers);
+                $redir = true;
+            } else {
+                $redir = false;
+                curl_close($c);
+
+                $new_headers = array();
+
+                foreach ($headers as $header) {
+                    if (preg_match("/:/", $header)) {
+                        list($name, $value) = explode(": ", $header, 2);
+                        $new_headers[$name] = $value;
+                    }
+                }
+
+                return new Services_Yadis_HTTPResponse($url, $code,
+                                                    $new_headers, $body);
+            }
+
+            $off = $stop - time();
+        }
+
+        trigger_error(sprintf("Timed out fetching: %s", $url),
+                      E_USER_WARNING);
+
+        return null;
+    }
+
+    function post($url, $body)
+    {
+        $this->reset();
+
+        if (!$this->allowedURL($url)) {
+            trigger_error(sprintf("Fetching URL not allowed: %s", $url),
+                          E_USER_WARNING);
+            return null;
+        }
+
+        $c = curl_init();
+
+        curl_setopt($c, CURLOPT_NOSIGNAL, true);
+        curl_setopt($c, CURLOPT_POST, true);
+        curl_setopt($c, CURLOPT_POSTFIELDS, $body);
+        curl_setopt($c, CURLOPT_TIMEOUT, $this->timeout);
+        curl_setopt($c, CURLOPT_URL, $url);
+        curl_setopt($c, CURLOPT_WRITEFUNCTION,
+                    array(&$this, "_writeData"));
+
+        curl_exec($c);
+
+        $code = curl_getinfo($c, CURLINFO_HTTP_CODE);
+
+        if (!$code) {
+            trigger_error("No HTTP code returned", E_USER_WARNING);
+            return null;
+        }
+
+        $body = $this->data;
+
+        curl_close($c);
+
+        $new_headers = array();
+
+        foreach ($this->headers as $header) {
+            if (preg_match("/:/", $header)) {
+                list($name, $value) = explode(": ", $header, 2);
+                $new_headers[$name] = $value;
+            }
+
+        }
+
+        return new Services_Yadis_HTTPResponse($url, $code,
+                                               $new_headers, $body);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Services/Yadis/ParseHTML.php b/lib/Services/Yadis/ParseHTML.php
new file mode 100644
index 000000000..ca8c3644d
--- /dev/null
+++ b/lib/Services/Yadis/ParseHTML.php
@@ -0,0 +1,258 @@
+<?php
+
+/**
+ * This is the HTML pseudo-parser for the Yadis library.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package Yadis
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * This class is responsible for scanning an HTML string to find META
+ * tags and their attributes.  This is used by the Yadis discovery
+ * process.  This class must be instantiated to be used.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_ParseHTML {
+
+    /**
+     * @access private
+     */
+    var $_re_flags = "si";
+
+    /**
+     * @access private
+     */
+    var $_removed_re =
+           "<!--.*?-->|<!\[CDATA\[.*?\]\]>|<script\b(?!:)[^>]*>.*?<\/script>";
+
+    /**
+     * @access private
+     */
+    var $_tag_expr = "<%s%s(?:\s.*?)?%s>";
+
+    /**
+     * @access private
+     */
+    var $_attr_find = '\b([-\w]+)=(".*?"|\'.*?\'|.+?)[\s>]';
+
+    function Services_Yadis_ParseHTML()
+    {
+        $this->_attr_find = sprintf("/%s/%s",
+                                    $this->_attr_find,
+                                    $this->_re_flags);
+
+        $this->_removed_re = sprintf("/%s/%s",
+                                     $this->_removed_re,
+                                     $this->_re_flags);
+
+        $this->_entity_replacements = array(
+                                            'amp' => '&',
+                                            'lt' => '<',
+                                            'gt' => '>',
+                                            'quot' => '"'
+                                            );
+
+        $this->_ent_replace =
+            sprintf("&(%s);", implode("|",
+                                      $this->_entity_replacements));
+    }
+
+    /**
+     * Replace HTML entities (amp, lt, gt, and quot) as well as
+     * numeric entities (e.g. #x9f;) with their actual values and
+     * return the new string.
+     *
+     * @access private
+     * @param string $str The string in which to look for entities
+     * @return string $new_str The new string entities decoded
+     */
+    function replaceEntities($str)
+    {
+        foreach ($this->_entity_replacements as $old => $new) {
+            $str = preg_replace(sprintf("/&%s;/", $old), $new, $str);
+        }
+
+        // Replace numeric entities because html_entity_decode doesn't
+        // do it for us.
+        $str = preg_replace('~&#x([0-9a-f]+);~ei', 'chr(hexdec("\\1"))', $str);
+        $str = preg_replace('~&#([0-9]+);~e', 'chr(\\1)', $str);
+
+        return $str;
+    }
+
+    /**
+     * Strip single and double quotes off of a string, if they are
+     * present.
+     *
+     * @access private
+     * @param string $str The original string
+     * @return string $new_str The new string with leading and
+     * trailing quotes removed
+     */
+    function removeQuotes($str)
+    {
+        $matches = array();
+        $double = '/^"(.*)"$/';
+        $single = "/^\'(.*)\'$/";
+
+        if (preg_match($double, $str, $matches)) {
+            return $matches[1];
+        } else if (preg_match($single, $str, $matches)) {
+            return $matches[1];
+        } else {
+            return $str;
+        }
+    }
+
+    /**
+     * Create a regular expression that will match an opening 
+     * or closing tag from a set of names.
+     *
+     * @access private
+     * @param mixed $tag_names Tag names to match
+     * @param mixed $close false/0 = no, true/1 = yes, other = maybe
+     * @param mixed $self_close false/0 = no, true/1 = yes, other = maybe
+     * @return string $regex A regular expression string to be used
+     * in, say, preg_match.
+     */
+    function tagPattern($tag_names, $close, $self_close)
+    {
+        if (is_array($tag_names)) {
+            $tag_names = '(?:'.implode('|',$tag_names).')';
+        }
+        if ($close) {
+            $close = '\/' . (($close == 1)? '' : '?');
+        } else {
+            $close = '';
+        }
+        if ($self_close) {
+            $self_close = '(?:\/\s*)' . (($self_close == 1)? '' : '?');
+        } else {
+            $self_close = '';
+        }
+        $expr = sprintf($this->_tag_expr, $close, $tag_names, $self_close);
+
+        return sprintf("/%s/%s", $expr, $this->_re_flags);
+    }
+
+    /**
+     * Given an HTML document string, this finds all the META tags in
+     * the document, provided they are found in the
+     * <HTML><HEAD>...</HEAD> section of the document.  The <HTML> tag
+     * may be missing.
+     *
+     * @access private
+     * @param string $html_string An HTMl document string
+     * @return array $tag_list Array of tags; each tag is an array of
+     * attribute -> value.
+     */
+    function getMetaTags($html_string)
+    {
+        $html_string = preg_replace($this->_removed_re,
+                                    "",
+                                    $html_string);
+
+        $key_tags = array($this->tagPattern('html', false, false),
+                          $this->tagPattern('head', false, false),
+                          $this->tagPattern('head', true, false),
+                          $this->tagPattern('html', true, false),
+                          $this->tagPattern(array(
+                          'body', 'frameset', 'frame', 'p', 'div',
+                          'table','span','a'), 'maybe', 'maybe'));
+        $key_tags_pos = array();
+        foreach ($key_tags as $pat) {
+            $matches = array();
+            preg_match($pat, $html_string, $matches, PREG_OFFSET_CAPTURE);
+            if($matches) {
+                $key_tags_pos[] = $matches[0][1];
+            } else {
+                $key_tags_pos[] = null;
+            }
+        }
+        // no opening head tag
+        if (is_null($key_tags_pos[1])) {
+            return array();
+        }
+        // the effective </head> is the min of the following
+        if (is_null($key_tags_pos[2])) {
+            $key_tags_pos[2] = strlen($html_string);
+        }
+        foreach (array($key_tags_pos[3], $key_tags_pos[4]) as $pos) {
+            if (!is_null($pos) && $pos < $key_tags_pos[2]) {
+                $key_tags_pos[2] = $pos;
+            }
+        }
+        // closing head tag comes before opening head tag
+        if ($key_tags_pos[1] > $key_tags_pos[2]) {
+            return array();
+        }
+        // if there is an opening html tag, make sure the opening head tag
+        // comes after it
+        if (!is_null($key_tags_pos[0]) && $key_tags_pos[1] < $key_tags_pos[0]) {
+            return array();
+        }
+        $html_string = substr($html_string, $key_tags_pos[1], ($key_tags_pos[2]-$key_tags_pos[1]));
+
+        $link_data = array();
+        $link_matches = array();
+        
+        if (!preg_match_all($this->tagPattern('meta', false, 'maybe'),
+                            $html_string, $link_matches)) {
+            return array();
+        }
+
+        foreach ($link_matches[0] as $link) {
+            $attr_matches = array();
+            preg_match_all($this->_attr_find, $link, $attr_matches);
+            $link_attrs = array();
+            foreach ($attr_matches[0] as $index => $full_match) {
+                $name = $attr_matches[1][$index];
+                $value = $this->replaceEntities(
+                              $this->removeQuotes($attr_matches[2][$index]));
+
+                $link_attrs[strtolower($name)] = $value;
+            }
+            $link_data[] = $link_attrs;
+        }
+
+        return $link_data;
+    }
+
+    /**
+     * Looks for a META tag with an "http-equiv" attribute whose value
+     * is one of ("x-xrds-location", "x-yadis-location"), ignoring
+     * case.  If such a META tag is found, its "content" attribute
+     * value is returned.
+     *
+     * @param string $html_string An HTML document in string format
+     * @return mixed $content The "content" attribute value of the
+     * META tag, if found, or null if no such tag was found.
+     */
+    function getHTTPEquiv($html_string)
+    {
+        $meta_tags = $this->getMetaTags($html_string);
+
+        if ($meta_tags) {
+            foreach ($meta_tags as $tag) {
+                if (array_key_exists('http-equiv', $tag) &&
+                    (in_array(strtolower($tag['http-equiv']),
+                              array('x-xrds-location', 'x-yadis-location'))) &&
+                    array_key_exists('content', $tag)) {
+                    return $tag['content'];
+                }
+            }
+        }
+
+        return null;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Services/Yadis/PlainHTTPFetcher.php b/lib/Services/Yadis/PlainHTTPFetcher.php
new file mode 100644
index 000000000..6b4b143a6
--- /dev/null
+++ b/lib/Services/Yadis/PlainHTTPFetcher.php
@@ -0,0 +1,245 @@
+<?php
+
+/**
+ * This module contains the plain non-curl HTTP fetcher
+ * implementation.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package Yadis
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Interface import
+ */
+require_once "Services/Yadis/HTTPFetcher.php";
+
+/**
+ * This class implements a plain, hand-built socket-based fetcher
+ * which will be used in the event that CURL is unavailable.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_PlainHTTPFetcher extends Services_Yadis_HTTPFetcher {
+    function get($url, $extra_headers = null)
+    {
+        if (!$this->allowedURL($url)) {
+            trigger_error("Bad URL scheme in url: " . $url,
+                          E_USER_WARNING);
+            return null;
+        }
+
+        $redir = true;
+
+        $stop = time() + $this->timeout;
+        $off = $this->timeout;
+
+        while ($redir && ($off > 0)) {
+
+            $parts = parse_url($url);
+
+            $specify_port = true;
+
+            // Set a default port.
+            if (!array_key_exists('port', $parts)) {
+                $specify_port = false;
+                if ($parts['scheme'] == 'http') {
+                    $parts['port'] = 80;
+                } elseif ($parts['scheme'] == 'https') {
+                    $parts['port'] = 443;
+                } else {
+                    trigger_error("fetcher post method doesn't support " .
+                                  " scheme '" . $parts['scheme'] .
+                                  "', no default port available",
+                                  E_USER_WARNING);
+                    return null;
+                }
+            }
+
+            $host = $parts['host'];
+
+            if ($parts['scheme'] == 'https') {
+                $host = 'ssl://' . $host;
+            }
+
+            $user_agent = "PHP Yadis Library Fetcher";
+
+            $headers = array(
+                             "GET ".$parts['path'].
+                             (array_key_exists('query', $parts) ?
+                              "?".$parts['query'] : "").
+                                 " HTTP/1.0",
+                             "User-Agent: $user_agent",
+                             "Host: ".$parts['host'].
+                                ($specify_port ? ":".$parts['port'] : ""),
+                             "Port: ".$parts['port']);
+
+            $errno = 0;
+            $errstr = '';
+
+            if ($extra_headers) {
+                foreach ($extra_headers as $h) {
+                    $headers[] = $h;
+                }
+            }
+
+            @$sock = fsockopen($host, $parts['port'], $errno, $errstr,
+                               $this->timeout);
+            if ($sock === false) {
+                return false;
+            }
+
+            stream_set_timeout($sock, $this->timeout);
+
+            fputs($sock, implode("\r\n", $headers) . "\r\n\r\n");
+
+            $data = "";
+            while (!feof($sock)) {
+                $data .= fgets($sock, 1024);
+            }
+
+            fclose($sock);
+
+            // Split response into header and body sections
+            list($headers, $body) = explode("\r\n\r\n", $data, 2);
+            $headers = explode("\r\n", $headers);
+
+            $http_code = explode(" ", $headers[0]);
+            $code = $http_code[1];
+
+            if (in_array($code, array('301', '302'))) {
+                $url = $this->_findRedirect($headers);
+                $redir = true;
+            } else {
+                $redir = false;
+            }
+
+            $off = $stop - time();
+        }
+
+        $new_headers = array();
+
+        foreach ($headers as $header) {
+            if (preg_match("/:/", $header)) {
+                list($name, $value) = explode(": ", $header, 2);
+                $new_headers[$name] = $value;
+            }
+
+        }
+
+        return new Services_Yadis_HTTPResponse($url, $code, $new_headers, $body);
+    }
+
+    function post($url, $body, $extra_headers = null)
+    {
+        if (!$this->allowedURL($url)) {
+            trigger_error("Bad URL scheme in url: " . $url,
+                          E_USER_WARNING);
+            return null;
+        }
+
+        $parts = parse_url($url);
+
+        $headers = array();
+
+        $post_path = $parts['path'];
+        if (isset($parts['query'])) {
+            $post_path .= '?' . $parts['query'];
+        }
+
+        $headers[] = "POST ".$post_path." HTTP/1.0";
+        $headers[] = "Host: " . $parts['host'];
+        $headers[] = "Content-type: application/x-www-form-urlencoded";
+        $headers[] = "Content-length: " . strval(strlen($body));
+
+        if ($extra_headers &&
+            is_array($extra_headers)) {
+            $headers = array_merge($headers, $extra_headers);
+        }
+
+        // Join all headers together.
+        $all_headers = implode("\r\n", $headers);
+
+        // Add headers, two newlines, and request body.
+        $request = $all_headers . "\r\n\r\n" . $body;
+
+        // Set a default port.
+        if (!array_key_exists('port', $parts)) {
+            if ($parts['scheme'] == 'http') {
+                $parts['port'] = 80;
+            } elseif ($parts['scheme'] == 'https') {
+                $parts['port'] = 443;
+            } else {
+                trigger_error("fetcher post method doesn't support scheme '" .
+                              $parts['scheme'] .
+                              "', no default port available",
+                              E_USER_WARNING);
+                return null;
+            }
+        }
+
+        if ($parts['scheme'] == 'https') {
+            $parts['host'] = sprintf("ssl://%s", $parts['host']);
+        }
+
+        // Connect to the remote server.
+        $errno = 0;
+        $errstr = '';
+
+        $sock = fsockopen($parts['host'], $parts['port'], $errno, $errstr,
+                          $this->timeout);
+
+        if ($sock === false) {
+            trigger_error("Could not connect to " . $parts['host'] .
+                          " port " . $parts['port'],
+                          E_USER_WARNING);
+            return null;
+        }
+
+        stream_set_timeout($sock, $this->timeout);
+
+        // Write the POST request.
+        fputs($sock, $request);
+
+        // Get the response from the server.
+        $response = "";
+        while (!feof($sock)) {
+            if ($data = fgets($sock, 128)) {
+                $response .= $data;
+            } else {
+                break;
+            }
+        }
+
+        // Split the request into headers and body.
+        list($headers, $response_body) = explode("\r\n\r\n", $response, 2);
+
+        $headers = explode("\r\n", $headers);
+
+        // Expect the first line of the headers data to be something
+        // like HTTP/1.1 200 OK.  Split the line on spaces and take
+        // the second token, which should be the return code.
+        $http_code = explode(" ", $headers[0]);
+        $code = $http_code[1];
+
+        $new_headers = array();
+
+        foreach ($headers as $header) {
+            if (preg_match("/:/", $header)) {
+                list($name, $value) = explode(": ", $header, 2);
+                $new_headers[$name] = $value;
+            }
+
+        }
+
+        return new Services_Yadis_HTTPResponse($url, $code,
+                                               $headers, $response_body);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Services/Yadis/XML.php b/lib/Services/Yadis/XML.php
new file mode 100644
index 000000000..8e1d23763
--- /dev/null
+++ b/lib/Services/Yadis/XML.php
@@ -0,0 +1,365 @@
+<?php
+
+/**
+ * XML-parsing classes to wrap the domxml and DOM extensions for PHP 4
+ * and 5, respectively.
+ *
+ * @package Yadis
+ */
+
+/**
+ * The base class for wrappers for available PHP XML-parsing
+ * extensions.  To work with this Yadis library, subclasses of this
+ * class MUST implement the API as defined in the remarks for this
+ * class.  Subclasses of Services_Yadis_XMLParser are used to wrap
+ * particular PHP XML extensions such as 'domxml'.  These are used
+ * internally by the library depending on the availability of
+ * supported PHP XML extensions.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_XMLParser {
+    /**
+     * Initialize an instance of Services_Yadis_XMLParser with some
+     * XML and namespaces.  This SHOULD NOT be overridden by
+     * subclasses.
+     *
+     * @param string $xml_string A string of XML to be parsed.
+     * @param array $namespace_map An array of ($ns_name => $ns_uri)
+     * to be registered with the XML parser.  May be empty.
+     * @return boolean $result True if the initialization and
+     * namespace registration(s) succeeded; false otherwise.
+     */
+    function init($xml_string, $namespace_map)
+    {
+        if (!$this->setXML($xml_string)) {
+            return false;
+        }
+
+        foreach ($namespace_map as $prefix => $uri) {
+            if (!$this->registerNamespace($prefix, $uri)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Register a namespace with the XML parser.  This should be
+     * overridden by subclasses.
+     *
+     * @param string $prefix The namespace prefix to appear in XML tag
+     * names.
+     *
+     * @param string $uri The namespace URI to be used to identify the
+     * namespace in the XML.
+     *
+     * @return boolean $result True if the registration succeeded;
+     * false otherwise.
+     */
+    function registerNamespace($prefix, $uri)
+    {
+        // Not implemented.
+    }
+
+    /**
+     * Set this parser object's XML payload.  This should be
+     * overridden by subclasses.
+     *
+     * @param string $xml_string The XML string to pass to this
+     * object's XML parser.
+     *
+     * @return boolean $result True if the initialization succeeded;
+     * false otherwise.
+     */
+    function setXML($xml_string)
+    {
+        // Not implemented.
+    }
+
+    /**
+     * Evaluate an XPath expression and return the resulting node
+     * list.  This should be overridden by subclasses.
+     *
+     * @param string $xpath The XPath expression to be evaluated.
+     *
+     * @param mixed $node A node object resulting from a previous
+     * evalXPath call.  This node, if specified, provides the context
+     * for the evaluation of this xpath expression.
+     *
+     * @return array $node_list An array of matching opaque node
+     * objects to be used with other methods of this parser class.
+     */
+    function evalXPath($xpath, $node = null)
+    {
+        // Not implemented.
+    }
+
+    /**
+     * Return the textual content of a specified node.
+     *
+     * @param mixed $node A node object from a previous call to
+     * $this->evalXPath().
+     *
+     * @return string $content The content of this node.
+     */
+    function content($node)
+    {
+        // Not implemented.
+    }
+
+    /**
+     * Return the attributes of a specified node.
+     *
+     * @param mixed $node A node object from a previous call to
+     * $this->evalXPath().
+     *
+     * @return array $attrs An array mapping attribute names to
+     * values.
+     */
+    function attributes($node)
+    {
+        // Not implemented.
+    }
+}
+
+/**
+ * This concrete implementation of Services_Yadis_XMLParser implements
+ * the appropriate API for the 'domxml' extension which is typically
+ * packaged with PHP 4.  This class will be used whenever the 'domxml'
+ * extension is detected.  See the Services_Yadis_XMLParser class for
+ * details on this class's methods.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_domxml extends Services_Yadis_XMLParser {
+    function Services_Yadis_domxml()
+    {
+        $this->xml = null;
+        $this->doc = null;
+        $this->xpath = null;
+        $this->errors = array();
+    }
+
+    function setXML($xml_string)
+    {
+        $this->xml = $xml_string;
+        $this->doc = @domxml_open_mem($xml_string, DOMXML_LOAD_PARSING,
+                                      $this->errors);
+
+        if (!$this->doc) {
+            return false;
+        }
+
+        $this->xpath = $this->doc->xpath_new_context();
+
+        return true;
+    }
+
+    function registerNamespace($prefix, $uri)
+    {
+        return xpath_register_ns($this->xpath, $prefix, $uri);
+    }
+
+    function &evalXPath($xpath, $node = null)
+    {
+        if ($node) {
+            $result = @$this->xpath->xpath_eval($xpath, $node);
+        } else {
+            $result = @$this->xpath->xpath_eval($xpath);
+        }
+
+        if (!$result->nodeset) {
+            $n = array();
+            return $n;
+        }
+
+        return $result->nodeset;
+    }
+
+    function content($node)
+    {
+        if ($node) {
+            return $node->get_content();
+        }
+    }
+
+    function attributes($node)
+    {
+        if ($node) {
+            $arr = $node->attributes();
+            $result = array();
+
+            if ($arr) {
+                foreach ($arr as $attrnode) {
+                    $result[$attrnode->name] = $attrnode->value;
+                }
+            }
+
+            return $result;
+        }
+    }
+}
+
+/**
+ * This concrete implementation of Services_Yadis_XMLParser implements
+ * the appropriate API for the 'dom' extension which is typically
+ * packaged with PHP 5.  This class will be used whenever the 'dom'
+ * extension is detected.  See the Services_Yadis_XMLParser class for
+ * details on this class's methods.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_dom extends Services_Yadis_XMLParser {
+    function Services_Yadis_dom()
+    {
+        $this->xml = null;
+        $this->doc = null;
+        $this->xpath = null;
+        $this->errors = array();
+    }
+
+    function setXML($xml_string)
+    {
+        $this->xml = $xml_string;
+        $this->doc = new DOMDocument;
+
+        if (!$this->doc) {
+            return false;
+        }
+
+        if (!@$this->doc->loadXML($xml_string)) {
+            return false;
+        }
+
+        $this->xpath = new DOMXPath($this->doc);
+
+        if ($this->xpath) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    function registerNamespace($prefix, $uri)
+    {
+        return $this->xpath->registerNamespace($prefix, $uri);
+    }
+
+    function &evalXPath($xpath, $node = null)
+    {
+        if ($node) {
+            $result = @$this->xpath->query($xpath, $node);
+        } else {
+            $result = @$this->xpath->query($xpath);
+        }
+
+        $n = array();
+
+        for ($i = 0; $i < $result->length; $i++) {
+            $n[] = $result->item($i);
+        }
+
+        return $n;
+    }
+
+    function content($node)
+    {
+        if ($node) {
+            return $node->textContent;
+        }
+    }
+
+    function attributes($node)
+    {
+        if ($node) {
+            $arr = $node->attributes;
+            $result = array();
+
+            if ($arr) {
+                for ($i = 0; $i < $arr->length; $i++) {
+                    $node = $arr->item($i);
+                    $result[$node->nodeName] = $node->nodeValue;
+                }
+            }
+
+            return $result;
+        }
+    }
+}
+
+global $__Services_Yadis_defaultParser;
+$__Services_Yadis_defaultParser = null;
+
+/**
+ * Set a default parser to override the extension-driven selection of
+ * available parser classes.  This is helpful in a test environment or
+ * one in which multiple parsers can be used but one is more
+ * desirable.
+ *
+ * @param Services_Yadis_XMLParser $parser An instance of a
+ * Services_Yadis_XMLParser subclass.
+ */
+function Services_Yadis_setDefaultParser(&$parser)
+{
+    global $__Services_Yadis_defaultParser;
+    $__Services_Yadis_defaultParser =& $parser;
+}
+
+function Services_Yadis_getSupportedExtensions()
+{
+    return array(
+                 'dom' => array('classname' => 'Services_Yadis_dom',
+                                'libname' => array('dom.so', 'dom.dll')),
+                 'domxml' => array('classname' => 'Services_Yadis_domxml',
+                                   'libname' => array('domxml.so', 'php_domxml.dll')),
+                 );
+}
+
+/**
+ * Returns an instance of a Services_Yadis_XMLParser subclass based on
+ * the availability of PHP extensions for XML parsing.  If
+ * Services_Yadis_setDefaultParser has been called, the parser used in
+ * that call will be returned instead.
+ */
+function &Services_Yadis_getXMLParser()
+{
+    global $__Services_Yadis_defaultParser;
+
+    if (isset($__Services_Yadis_defaultParser)) {
+        return $__Services_Yadis_defaultParser;
+    }
+
+    $p = null;
+    $classname = null;
+
+    $extensions = Services_Yadis_getSupportedExtensions();
+
+    // Return a wrapper for the resident implementation, if any.
+    foreach ($extensions as $name => $params) {
+        if (!extension_loaded($name)) {
+            foreach ($params['libname'] as $libname) {
+                if (@dl($libname)) {
+                    $classname = $params['classname'];
+                }
+            }
+        } else {
+            $classname = $params['classname'];
+        }
+        if (isset($classname)) {
+            $p = new $classname();
+            return $p;
+        }
+    }
+
+    if (!isset($p)) {
+        trigger_error('No XML parser was found', E_USER_ERROR);
+    } else {
+        Services_Yadis_setDefaultParser($p);
+    }
+
+    return $p;
+}
+
+?>
diff --git a/lib/Services/Yadis/XRDS.php b/lib/Services/Yadis/XRDS.php
new file mode 100644
index 000000000..bc82a205f
--- /dev/null
+++ b/lib/Services/Yadis/XRDS.php
@@ -0,0 +1,425 @@
+<?php
+
+/**
+ * This module contains the XRDS parsing code.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package Yadis
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Require the XPath implementation.
+ */
+require_once 'Services/Yadis/XML.php';
+
+/**
+ * This match mode means a given service must match ALL filters passed
+ * to the Services_Yadis_XRDS::services() call.
+ */
+define('SERVICES_YADIS_MATCH_ALL', 101);
+
+/**
+ * This match mode means a given service must match ANY filters (at
+ * least one) passed to the Services_Yadis_XRDS::services() call.
+ */
+define('SERVICES_YADIS_MATCH_ANY', 102);
+
+/**
+ * The priority value used for service elements with no priority
+ * specified.
+ */
+define('SERVICES_YADIS_MAX_PRIORITY', pow(2, 30));
+
+function Services_Yadis_getNSMap()
+{
+    return array('xrds' => 'xri://$xrds',
+                 'xrd' => 'xri://$xrd*($v*2.0)');
+}
+
+/**
+ * @access private
+ */
+function Services_Yadis_array_scramble($arr)
+{
+    $result = array();
+
+    while (count($arr)) {
+        $index = array_rand($arr, 1);
+        $result[] = $arr[$index];
+        unset($arr[$index]);
+    }
+
+    return $result;
+}
+
+/**
+ * This class represents a <Service> element in an XRDS document.
+ * Objects of this type are returned by
+ * Services_Yadis_XRDS::services() and
+ * Services_Yadis_Yadis::services().  Each object corresponds directly
+ * to a <Service> element in the XRDS and supplies a
+ * getElements($name) method which you should use to inspect the
+ * element's contents.  See {@link Services_Yadis_Yadis} for more
+ * information on the role this class plays in Yadis discovery.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_Service {
+
+    /**
+     * Creates an empty service object.
+     */
+    function Services_Yadis_Service()
+    {
+        $this->element = null;
+        $this->parser = null;
+    }
+
+    /**
+     * Return the URIs in the "Type" elements, if any, of this Service
+     * element.
+     *
+     * @return array $type_uris An array of Type URI strings.
+     */
+    function getTypes()
+    {
+        $t = array();
+        foreach ($this->getElements('xrd:Type') as $elem) {
+            $c = $this->parser->content($elem);
+            if ($c) {
+                $t[] = $c;
+            }
+        }
+        return $t;
+    }
+
+    /**
+     * Return the URIs in the "URI" elements, if any, of this Service
+     * element.  The URIs are returned sorted in priority order.
+     *
+     * @return array $uris An array of URI strings.
+     */
+    function getURIs()
+    {
+        $uris = array();
+        $last = array();
+
+        foreach ($this->getElements('xrd:URI') as $elem) {
+            $uri_string = $this->parser->content($elem);
+            $attrs = $this->parser->attributes($elem);
+            if ($attrs &&
+                array_key_exists('priority', $attrs)) {
+                $priority = intval($attrs['priority']);
+                if (!array_key_exists($priority, $uris)) {
+                    $uris[$priority] = array();
+                }
+
+                $uris[$priority][] = $uri_string;
+            } else {
+                $last[] = $uri_string;
+            }
+        }
+
+        $keys = array_keys($uris);
+        sort($keys);
+
+        // Rebuild array of URIs.
+        $result = array();
+        foreach ($keys as $k) {
+            $new_uris = Services_Yadis_array_scramble($uris[$k]);
+            $result = array_merge($result, $new_uris);
+        }
+
+        $result = array_merge($result,
+                              Services_Yadis_array_scramble($last));
+
+        return $result;
+    }
+
+    /**
+     * Returns the "priority" attribute value of this <Service>
+     * element, if the attribute is present.  Returns null if not.
+     *
+     * @return mixed $result Null or integer, depending on whether
+     * this Service element has a 'priority' attribute.
+     */
+    function getPriority()
+    {
+        $attributes = $this->parser->attributes($this->element);
+
+        if (array_key_exists('priority', $attributes)) {
+            return intval($attributes['priority']);
+        }
+
+        return null;
+    }
+
+    /**
+     * Used to get XML elements from this object's <Service> element.
+     *
+     * This is what you should use to get all custom information out
+     * of this element. This is used by service filter functions to
+     * determine whether a service element contains specific tags,
+     * etc.  NOTE: this only considers elements which are direct
+     * children of the <Service> element for this object.
+     *
+     * @param string $name The name of the element to look for
+     * @return array $list An array of elements with the specified
+     * name which are direct children of the <Service> element.  The
+     * nodes returned by this function can be passed to $this->parser
+     * methods (see {@link Services_Yadis_XMLParser}).
+     */
+    function getElements($name)
+    {
+        return $this->parser->evalXPath($name, $this->element);
+    }
+}
+
+/**
+ * This class performs parsing of XRDS documents.
+ *
+ * You should not instantiate this class directly; rather, call
+ * parseXRDS statically:
+ *
+ * <pre>  $xrds = Services_Yadis_XRDS::parseXRDS($xml_string);</pre>
+ *
+ * If the XRDS can be parsed and is valid, an instance of
+ * Services_Yadis_XRDS will be returned.  Otherwise, null will be
+ * returned.  This class is used by the Services_Yadis_Yadis::discover
+ * method.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_XRDS {
+
+    /**
+     * Instantiate a Services_Yadis_XRDS object.  Requires an XPath
+     * instance which has been used to parse a valid XRDS document.
+     */
+    function Services_Yadis_XRDS(&$xmlParser, &$xrdNodes)
+    {
+        $this->parser =& $xmlParser;
+        $this->xrdNode = $xrdNodes[count($xrdNodes) - 1];
+        $this->allXrdNodes =& $xrdNodes;
+        $this->serviceList = array();
+        $this->_parse();
+    }
+
+    /**
+     * Parse an XML string (XRDS document) and return either a
+     * Services_Yadis_XRDS object or null, depending on whether the
+     * XRDS XML is valid.
+     *
+     * @param string $xml_string An XRDS XML string.
+     * @return mixed $xrds An instance of Services_Yadis_XRDS or null,
+     * depending on the validity of $xml_string
+     */
+    function &parseXRDS($xml_string, $extra_ns_map = null)
+    {
+        $_null = null;
+
+        if (!$xml_string) {
+            return $_null;
+        }
+
+        $parser = Services_Yadis_getXMLParser();
+
+        $ns_map = Services_Yadis_getNSMap();
+
+        if ($extra_ns_map && is_array($extra_ns_map)) {
+            $ns_map = array_merge($ns_map, $extra_ns_map);
+        }
+
+        if (!($parser && $parser->init($xml_string, $ns_map))) {
+            return $_null;
+        }
+
+        // Try to get root element.
+        $root = $parser->evalXPath('/xrds:XRDS[1]');
+        if (!$root) {
+            return $_null;
+        }
+
+        if (is_array($root)) {
+            $root = $root[0];
+        }
+
+        $attrs = $parser->attributes($root);
+
+        if (array_key_exists('xmlns:xrd', $attrs) &&
+            $attrs['xmlns:xrd'] != 'xri://$xrd*($v*2.0)') {
+            return $_null;
+        } else if (array_key_exists('xmlns', $attrs) &&
+                   preg_match('/xri/', $attrs['xmlns']) &&
+                   $attrs['xmlns'] != 'xri://$xrd*($v*2.0)') {
+            return $_null;
+        }
+
+        // Get the last XRD node.
+        $xrd_nodes = $parser->evalXPath('/xrds:XRDS[1]/xrd:XRD');
+
+        if (!$xrd_nodes) {
+            return $_null;
+        }
+
+        $xrds = new Services_Yadis_XRDS($parser, $xrd_nodes);
+        return $xrds;
+    }
+
+    /**
+     * @access private
+     */
+    function _addService($priority, $service)
+    {
+        $priority = intval($priority);
+
+        if (!array_key_exists($priority, $this->serviceList)) {
+            $this->serviceList[$priority] = array();
+        }
+
+        $this->serviceList[$priority][] = $service;
+    }
+
+    /**
+     * Creates the service list using nodes from the XRDS XML
+     * document.
+     *
+     * @access private
+     */
+    function _parse()
+    {
+        $this->serviceList = array();
+
+        $services = $this->parser->evalXPath('xrd:Service', $this->xrdNode);
+
+        foreach ($services as $node) {
+            $s =& new Services_Yadis_Service();
+            $s->element = $node;
+            $s->parser =& $this->parser;
+
+            $priority = $s->getPriority();
+
+            if ($priority === null) {
+                $priority = SERVICES_YADIS_MAX_PRIORITY;
+            }
+
+            $this->_addService($priority, $s);
+        }
+    }
+
+    /**
+     * Returns a list of service objects which correspond to <Service>
+     * elements in the XRDS XML document for this object.
+     *
+     * Optionally, an array of filter callbacks may be given to limit
+     * the list of returned service objects.  Furthermore, the default
+     * mode is to return all service objects which match ANY of the
+     * specified filters, but $filter_mode may be
+     * SERVICES_YADIS_MATCH_ALL if you want to be sure that the
+     * returned services match all the given filters.  See {@link
+     * Services_Yadis_Yadis} for detailed usage information on filter
+     * functions.
+     *
+     * @param mixed $filters An array of callbacks to filter the
+     * returned services, or null if all services are to be returned.
+     * @param integer $filter_mode SERVICES_YADIS_MATCH_ALL or
+     * SERVICES_YADIS_MATCH_ANY, depending on whether the returned
+     * services should match ALL or ANY of the specified filters,
+     * respectively.
+     * @return mixed $services An array of {@link
+     * Services_Yadis_Service} objects if $filter_mode is a valid
+     * mode; null if $filter_mode is an invalid mode (i.e., not
+     * SERVICES_YADIS_MATCH_ANY or SERVICES_YADIS_MATCH_ALL).
+     */
+    function services($filters = null,
+                      $filter_mode = SERVICES_YADIS_MATCH_ANY)
+    {
+
+        $pri_keys = array_keys($this->serviceList);
+        sort($pri_keys, SORT_NUMERIC);
+
+        // If no filters are specified, return the entire service
+        // list, ordered by priority.
+        if (!$filters ||
+            (!is_array($filters))) {
+
+            $result = array();
+            foreach ($pri_keys as $pri) {
+                $result = array_merge($result, $this->serviceList[$pri]);
+            }
+
+            return $result;
+        }
+
+        // If a bad filter mode is specified, return null.
+        if (!in_array($filter_mode, array(SERVICES_YADIS_MATCH_ANY,
+                                          SERVICES_YADIS_MATCH_ALL))) {
+            return null;
+        }
+
+        // Otherwise, use the callbacks in the filter list to
+        // determine which services are returned.
+        $filtered = array();
+
+        foreach ($pri_keys as $priority_value) {
+            $service_obj_list = $this->serviceList[$priority_value];
+
+            foreach ($service_obj_list as $service) {
+
+                $matches = 0;
+
+                foreach ($filters as $filter) {
+                    if (call_user_func_array($filter, array($service))) {
+                        $matches++;
+
+                        if ($filter_mode == SERVICES_YADIS_MATCH_ANY) {
+                            $pri = $service->getPriority();
+                            if ($pri === null) {
+                                $pri = SERVICES_YADIS_MAX_PRIORITY;
+                            }
+
+                            if (!array_key_exists($pri, $filtered)) {
+                                $filtered[$pri] = array();
+                            }
+
+                            $filtered[$pri][] = $service;
+                            break;
+                        }
+                    }
+                }
+
+                if (($filter_mode == SERVICES_YADIS_MATCH_ALL) &&
+                    ($matches == count($filters))) {
+
+                    $pri = $service->getPriority();
+                    if ($pri === null) {
+                        $pri = SERVICES_YADIS_MAX_PRIORITY;
+                    }
+
+                    if (!array_key_exists($pri, $filtered)) {
+                        $filtered[$pri] = array();
+                    }
+                    $filtered[$pri][] = $service;
+                }
+            }
+        }
+
+        $pri_keys = array_keys($filtered);
+        sort($pri_keys, SORT_NUMERIC);
+
+        $result = array();
+        foreach ($pri_keys as $pri) {
+            $result = array_merge($result, $filtered[$pri]);
+        }
+
+        return $result;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Services/Yadis/XRI.php b/lib/Services/Yadis/XRI.php
new file mode 100644
index 000000000..91d385e48
--- /dev/null
+++ b/lib/Services/Yadis/XRI.php
@@ -0,0 +1,233 @@
+<?php
+
+/**
+ * Routines for XRI resolution.
+ *
+ * @package Yadis
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+require_once 'Services/Yadis/Misc.php';
+require_once 'Services/Yadis/Yadis.php';
+require_once 'Auth/OpenID.php';
+
+function Services_Yadis_getDefaultProxy()
+{
+    return 'http://proxy.xri.net/';
+}
+
+function Services_Yadis_getXRIAuthorities()
+{
+    return array('!', '=', '@', '+', '$', '(');
+}
+
+function Services_Yadis_getEscapeRE()
+{
+    $parts = array();
+    foreach (array_merge(Services_Yadis_getUCSChars(),
+                         Services_Yadis_getIPrivateChars()) as $pair) {
+        list($m, $n) = $pair;
+        $parts[] = sprintf("%s-%s", chr($m), chr($n));
+    }
+
+    return sprintf('/[%s]/', implode('', $parts));
+}
+
+function Services_Yadis_getXrefRE()
+{
+    return '/\((.*?)\)/';
+}
+
+function Services_Yadis_identifierScheme($identifier)
+{
+    if (Services_Yadis_startswith($identifier, 'xri://') ||
+        (in_array($identifier[0], Services_Yadis_getXRIAuthorities()))) {
+        return "XRI";
+    } else {
+        return "URI";
+    }
+}
+
+function Services_Yadis_toIRINormal($xri)
+{
+    if (!Services_Yadis_startswith($xri, 'xri://')) {
+        $xri = 'xri://' . $xri;
+    }
+
+    return Services_Yadis_escapeForIRI($xri);
+}
+
+function _escape_xref($xref_match)
+{
+    $xref = $xref_match[0];
+    $xref = str_replace('/', '%2F', $xref);
+    $xref = str_replace('?', '%3F', $xref);
+    $xref = str_replace('#', '%23', $xref);
+    return $xref;
+}
+
+function Services_Yadis_escapeForIRI($xri)
+{
+    $xri = str_replace('%', '%25', $xri);
+    $xri = preg_replace_callback(Services_Yadis_getXrefRE(),
+                                 '_escape_xref', $xri);
+    return $xri;
+}
+
+function Services_Yadis_toURINormal($xri)
+{
+    return Services_Yadis_iriToURI(Services_Yadis_toIRINormal($xri));
+}
+
+function Services_Yadis_iriToURI($iri)
+{
+    if (1) {
+        return $iri;
+    } else {
+        // According to RFC 3987, section 3.1, "Mapping of IRIs to URIs"
+        return preg_replace_callback(Services_Yadis_getEscapeRE(),
+                                     'Services_Yadis_pct_escape_unicode', $iri);
+    }
+}
+
+
+function Services_Yadis_XRIAppendArgs($url, $args)
+{
+    // Append some arguments to an HTTP query.  Yes, this is just like
+    // OpenID's appendArgs, but with special seasoning for XRI
+    // queries.
+
+    if (count($args) == 0) {
+        return $url;
+    }
+
+    // Non-empty array; if it is an array of arrays, use multisort;
+    // otherwise use sort.
+    if (array_key_exists(0, $args) &&
+        is_array($args[0])) {
+        // Do nothing here.
+    } else {
+        $keys = array_keys($args);
+        sort($keys);
+        $new_args = array();
+        foreach ($keys as $key) {
+            $new_args[] = array($key, $args[$key]);
+        }
+        $args = $new_args;
+    }
+
+    // According to XRI Resolution section "QXRI query parameters":
+    //
+    // "If the original QXRI had a null query component (only a
+    //  leading question mark), or a query component consisting of
+    //  only question marks, one additional leading question mark MUST
+    //  be added when adding any XRI resolution parameters."
+    if (strpos(rtrim($url, '?'), '?') !== false) {
+        $sep = '&';
+    } else {
+        $sep = '?';
+    }
+
+    return $url . $sep . Auth_OpenID::httpBuildQuery($args);
+}
+
+function Services_Yadis_providerIsAuthoritative($providerID, $canonicalID)
+{
+    $lastbang = strrpos($canonicalID, '!');
+    $p = substr($canonicalID, 0, $lastbang);
+    return $p == $providerID;
+}
+
+function Services_Yadis_rootAuthority($xri)
+{
+    // Return the root authority for an XRI.
+
+    $root = null;
+
+    if (Services_Yadis_startswith($xri, 'xri://')) {
+        $xri = substr($xri, 6);
+    }
+
+    $authority = explode('/', $xri, 2);
+    $authority = $authority[0];
+    if ($authority[0] == '(') {
+        // Cross-reference.
+        // XXX: This is incorrect if someone nests cross-references so
+        //   there is another close-paren in there.  Hopefully nobody
+        //   does that before we have a real xriparse function.
+        //   Hopefully nobody does that *ever*.
+        $root = substr($authority, 0, strpos($authority, ')') + 1);
+    } else if (in_array($authority[0], Services_Yadis_getXRIAuthorities())) {
+        // Other XRI reference.
+        $root = $authority[0];
+    } else {
+        // IRI reference.
+        $_segments = explode("!", $authority);
+        $segments = array();
+        foreach ($_segments as $s) {
+            $segments = array_merge($segments, explode("*", $s));
+        }
+        $root = $segments[0];
+    }
+
+    return Services_Yadis_XRI($root);
+}
+
+function Services_Yadis_XRI($xri)
+{
+    if (!Services_Yadis_startswith($xri, 'xri://')) {
+        $xri = 'xri://' . $xri;
+    }
+    return $xri;
+}
+
+function Services_Yadis_getCanonicalID($iname, $xrds)
+{
+    // Returns FALSE or a canonical ID value.
+
+    // Now nodes are in reverse order.
+    $xrd_list = array_reverse($xrds->allXrdNodes);
+    $parser =& $xrds->parser;
+    $node = $xrd_list[0];
+
+    $canonicalID_nodes = $parser->evalXPath('xrd:CanonicalID', $node);
+
+    if (!$canonicalID_nodes) {
+        return false;
+    }
+
+    $canonicalID = $canonicalID_nodes[count($canonicalID_nodes) - 1];
+    $canonicalID = Services_Yadis_XRI($parser->content($canonicalID));
+
+    $childID = $canonicalID;
+
+    for ($i = 1; $i < count($xrd_list); $i++) {
+        $xrd = $xrd_list[$i];
+
+        $parent_sought = substr($childID, 0, strrpos($childID, '!'));
+        $parent_list = array();
+
+        foreach ($parser->evalXPath('xrd:CanonicalID', $xrd) as $c) {
+            $parent_list[] = Services_Yadis_XRI($parser->content($c));
+        }
+
+        if (!in_array($parent_sought, $parent_list)) {
+            // raise XRDSFraud.
+            return false;
+        }
+
+        $childID = $parent_sought;
+    }
+
+    $root = Services_Yadis_rootAuthority($iname);
+    if (!Services_Yadis_providerIsAuthoritative($root, $childID)) {
+        // raise XRDSFraud.
+        return false;
+    }
+
+    return $canonicalID;
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Services/Yadis/XRIRes.php b/lib/Services/Yadis/XRIRes.php
new file mode 100644
index 000000000..b87cf0440
--- /dev/null
+++ b/lib/Services/Yadis/XRIRes.php
@@ -0,0 +1,68 @@
+<?php
+
+require_once 'Services/Yadis/XRDS.php';
+require_once 'Services/Yadis/XRI.php';
+
+class Services_Yadis_ProxyResolver {
+    function Services_Yadis_ProxyResolver(&$fetcher, $proxy_url = null)
+    {
+        $this->fetcher =& $fetcher;
+        $this->proxy_url = $proxy_url;
+        if (!$this->proxy_url) {
+            $this->proxy_url = Services_Yadis_getDefaultProxy();
+        }
+    }
+
+    function queryURL($xri, $service_type = null)
+    {
+        // trim off the xri:// prefix
+        $qxri = substr(Services_Yadis_toURINormal($xri), 6);
+        $hxri = $this->proxy_url . $qxri;
+        $args = array(
+                      '_xrd_r' => 'application/xrds+xml'
+                      );
+
+        if ($service_type) {
+            $args['_xrd_t'] = $service_type;
+        } else {
+            // Don't perform service endpoint selection.
+            $args['_xrd_r'] .= ';sep=false';
+        }
+
+        $query = Services_Yadis_XRIAppendArgs($hxri, $args);
+        return $query;
+    }
+
+    function query($xri, $service_types, $filters = array())
+    {
+        $services = array();
+        $canonicalID = null;
+        foreach ($service_types as $service_type) {
+            $url = $this->queryURL($xri, $service_type);
+            $response = $this->fetcher->get($url);
+            if ($response->status != 200) {
+                continue;
+            }
+            $xrds = Services_Yadis_XRDS::parseXRDS($response->body);
+            if (!$xrds) {
+                continue;
+            }
+            $canonicalID = Services_Yadis_getCanonicalID($xri,
+                                                         $xrds);
+
+            if ($canonicalID === false) {
+                return null;
+            }
+
+            $some_services = $xrds->services($filters);
+            $services = array_merge($services, $some_services);
+            // TODO:
+            //  * If we do get hits for multiple service_types, we're
+            //    almost certainly going to have duplicated service
+            //    entries and broken priority ordering.
+        }
+        return array($canonicalID, $services);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Services/Yadis/Yadis.php b/lib/Services/Yadis/Yadis.php
new file mode 100644
index 000000000..338bb3a24
--- /dev/null
+++ b/lib/Services/Yadis/Yadis.php
@@ -0,0 +1,313 @@
+<?php
+
+/**
+ * The core PHP Yadis implementation.
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: See the COPYING file included in this distribution.
+ *
+ * @package Yadis
+ * @author JanRain, Inc. <openid@janrain.com>
+ * @copyright 2005 Janrain, Inc.
+ * @license http://www.gnu.org/copyleft/lesser.html LGPL
+ */
+
+/**
+ * Need both fetcher types so we can use the right one based on the
+ * presence or absence of CURL.
+ */
+require_once "Services/Yadis/PlainHTTPFetcher.php";
+require_once "Services/Yadis/ParanoidHTTPFetcher.php";
+
+/**
+ * Need this for parsing HTML (looking for META tags).
+ */
+require_once "Services/Yadis/ParseHTML.php";
+
+/**
+ * Need this to parse the XRDS document during Yadis discovery.
+ */
+require_once "Services/Yadis/XRDS.php";
+
+/**
+ * This is the core of the PHP Yadis library.  This is the only class
+ * a user needs to use to perform Yadis discovery.  This class
+ * performs the discovery AND stores the result of the discovery.
+ *
+ * First, require this library into your program source:
+ *
+ * <pre>  require_once "Services/Yadis/Yadis.php";</pre>
+ *
+ * To perform Yadis discovery, first call the "discover" method
+ * statically with a URI parameter:
+ *
+ * <pre>  $http_response = array();
+ *  $fetcher = Services_Yadis_Yadis::getHTTPFetcher();
+ *  $yadis_object = Services_Yadis_Yadis::discover($uri,
+ *                                    $http_response, $fetcher);</pre>
+ *
+ * If the discovery succeeds, $yadis_object will be an instance of
+ * {@link Services_Yadis_Yadis}.  If not, it will be null.  The XRDS
+ * document found during discovery should have service descriptions,
+ * which can be accessed by calling
+ *
+ * <pre>  $service_list = $yadis_object->services();</pre>
+ *
+ * which returns an array of objects which describe each service.
+ * These objects are instances of Services_Yadis_Service.  Each object
+ * describes exactly one whole Service element, complete with all of
+ * its Types and URIs (no expansion is performed).  The common use
+ * case for using the service objects returned by services() is to
+ * write one or more filter functions and pass those to services():
+ *
+ * <pre>  $service_list = $yadis_object->services(
+ *                               array("filterByURI",
+ *                                     "filterByExtension"));</pre>
+ *
+ * The filter functions (whose names appear in the array passed to
+ * services()) take the following form:
+ *
+ * <pre>  function myFilter(&$service) {
+ *       // Query $service object here.  Return true if the service
+ *       // matches your query; false if not.
+ *  }</pre>
+ *
+ * This is an example of a filter which uses a regular expression to
+ * match the content of URI tags (note that the Services_Yadis_Service
+ * class provides a getURIs() method which you should use instead of
+ * this contrived example):
+ *
+ * <pre>
+ *  function URIMatcher(&$service) {
+ *      foreach ($service->getElements('xrd:URI') as $uri) {
+ *          if (preg_match("/some_pattern/",
+ *                         $service->parser->content($uri))) {
+ *              return true;
+ *          }
+ *      }
+ *      return false;
+ *  }</pre>
+ *
+ * The filter functions you pass will be called for each service
+ * object to determine which ones match the criteria your filters
+ * specify.  The default behavior is that if a given service object
+ * matches ANY of the filters specified in the services() call, it
+ * will be returned.  You can specify that a given service object will
+ * be returned ONLY if it matches ALL specified filters by changing
+ * the match mode of services():
+ *
+ * <pre>  $yadis_object->services(array("filter1", "filter2"),
+ *                          SERVICES_YADIS_MATCH_ALL);</pre>
+ *
+ * See {@link SERVICES_YADIS_MATCH_ALL} and {@link
+ * SERVICES_YADIS_MATCH_ANY}.
+ *
+ * Services described in an XRDS should have a library which you'll
+ * probably be using.  Those libraries are responsible for defining
+ * filters that can be used with the "services()" call.  If you need
+ * to write your own filter, see the documentation for {@link
+ * Services_Yadis_Service}.
+ *
+ * @package Yadis
+ */
+class Services_Yadis_Yadis {
+
+    /**
+     * Returns an HTTP fetcher object.  If the CURL extension is
+     * present, an instance of {@link Services_Yadis_ParanoidHTTPFetcher}
+     * is returned.  If not, an instance of
+     * {@link Services_Yadis_PlainHTTPFetcher} is returned.
+     */
+    function getHTTPFetcher($timeout = 20)
+    {
+        if (Services_Yadis_Yadis::curlPresent()) {
+            $fetcher = new Services_Yadis_ParanoidHTTPFetcher($timeout);
+        } else {
+            $fetcher = new Services_Yadis_PlainHTTPFetcher($timeout);
+        }
+        return $fetcher;
+    }
+
+    function curlPresent()
+    {
+        return function_exists('curl_init');
+    }
+
+    /**
+     * @access private
+     */
+    function _getHeader($header_list, $names)
+    {
+        foreach ($header_list as $name => $value) {
+            foreach ($names as $n) {
+                if (strtolower($name) == strtolower($n)) {
+                    return $value;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * @access private
+     */
+    function _getContentType($content_type_header)
+    {
+        if ($content_type_header) {
+            $parts = explode(";", $content_type_header);
+            return strtolower($parts[0]);
+        }
+    }
+
+    /**
+     * This should be called statically and will build a Yadis
+     * instance if the discovery process succeeds.  This implements
+     * Yadis discovery as specified in the Yadis specification.
+     *
+     * @param string $uri The URI on which to perform Yadis discovery.
+     *
+     * @param array $http_response An array reference where the HTTP
+     * response object will be stored (see {@link
+     * Services_Yadis_HTTPResponse}.
+     *
+     * @param Services_Yadis_HTTPFetcher $fetcher An instance of a
+     * Services_Yadis_HTTPFetcher subclass.
+     *
+     * @param array $extra_ns_map An array which maps namespace names
+     * to namespace URIs to be used when parsing the Yadis XRDS
+     * document.
+     *
+     * @param integer $timeout An optional fetcher timeout, in seconds.
+     *
+     * @return mixed $obj Either null or an instance of
+     * Services_Yadis_Yadis, depending on whether the discovery
+     * succeeded.
+     */
+    function discover($uri, &$http_response, &$fetcher,
+                      $extra_ns_map = null, $timeout = 20)
+    {
+        if (!$uri) {
+            return null;
+        }
+
+        $request_uri = $uri;
+        $headers = array("Accept: application/xrds+xml");
+
+        if (!$fetcher) {
+            $fetcher = Services_Yadis_Yadis::getHTTPFetcher($timeout);
+        }
+
+        $response = $fetcher->get($uri, $headers);
+        $http_response = $response;
+
+        if (!$response) {
+            return null;
+        }
+
+        if ($response->status != 200) {
+          return null;
+        }
+
+        $xrds_uri = $response->final_url;
+        $uri = $response->final_url;
+        $body = $response->body;
+
+        $xrds_header_uri = Services_Yadis_Yadis::_getHeader(
+                                                    $response->headers,
+                                                    array('x-xrds-location',
+                                                          'x-yadis-location'));
+
+        $content_type = Services_Yadis_Yadis::_getHeader($response->headers,
+                                                         array('content-type'));
+
+        if ($xrds_header_uri) {
+            $xrds_uri = $xrds_header_uri;
+            $response = $fetcher->get($xrds_uri);
+            $http_response = $response;
+            if (!$response) {
+                return null;
+            } else {
+                $body = $response->body;
+                $headers = $response->headers;
+                $content_type = Services_Yadis_Yadis::_getHeader($headers,
+                                                       array('content-type'));
+            }
+        }
+
+        if (Services_Yadis_Yadis::_getContentType($content_type) !=
+            'application/xrds+xml') {
+            // Treat the body as HTML and look for a META tag.
+            $parser = new Services_Yadis_ParseHTML();
+            $new_uri = $parser->getHTTPEquiv($body);
+            $xrds_uri = null;
+            if ($new_uri) {
+                $response = $fetcher->get($new_uri);
+                if ($response->status != 200) {
+                  return null;
+                }
+                $http_response = $response;
+                $body = $response->body;
+                $xrds_uri = $new_uri;
+                $content_type = Services_Yadis_Yadis::_getHeader(
+                                                         $response->headers,
+                                                         array('content-type'));
+            }
+        }
+
+        $xrds = Services_Yadis_XRDS::parseXRDS($body, $extra_ns_map);
+
+        if ($xrds !== null) {
+            $y = new Services_Yadis_Yadis();
+
+            $y->request_uri = $request_uri;
+            $y->xrds = $xrds;
+            $y->uri = $uri;
+            $y->xrds_uri = $xrds_uri;
+            $y->body = $body;
+            $y->content_type = $content_type;
+
+            return $y;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Instantiates an empty Services_Yadis_Yadis object.  This
+     * constructor should not be used by any user of the library.
+     * This constructor results in a completely useless object which
+     * must be populated with valid discovery information.  Instead of
+     * using this constructor, call
+     * Services_Yadis_Yadis::discover($uri).
+     */
+    function Services_Yadis_Yadis()
+    {
+        $this->request_uri = null;
+        $this->uri = null;
+        $this->xrds = null;
+        $this->xrds_uri = null;
+        $this->body = null;
+        $this->content_type = null;
+    }
+
+    /**
+     * Returns the list of service objects as described by the XRDS
+     * document, if this yadis object represents a successful Yadis
+     * discovery.
+     *
+     * @return array $services An array of {@link Services_Yadis_Service}
+     * objects
+     */
+    function services()
+    {
+        if ($this->xrds) {
+            return $this->xrds->services();
+        }
+
+        return null;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/templates/default/en/openid-about.php b/templates/default/en/openid-about.php
new file mode 100644
index 000000000..f72fc2b88
--- /dev/null
+++ b/templates/default/en/openid-about.php
@@ -0,0 +1,60 @@
+<?php $this->includeAtTemplateBase('includes/header.php'); ?>
+
+	<div id="header">
+		<h1>simpleSAMLphp OpenID</h1>
+		<div id="poweredby"><img src="/<?php echo $data['baseurlpath']; ?>resources/icons/bino.png" alt="Bino" /></div>
+	</div>
+	
+	<div id="content">
+
+		<?php if (isset($data['header'])) { echo '<h2>' . $data['header'] . '</h2>'; } ?>
+		
+		<p>[ <a href="/<?php echo $data['baseurlpath']; ?>/openid/provider/server.php/sites">List of trusted sites</a> |
+		About simpleSAMLphp OpenID ]</p>
+
+		
+		<p>Welcome to the simpleSAMLphp OpenID provider.</p>
+
+
+		<p>
+		  To use this server, you will have to set up a URL to use as an identifier.
+		  Insert the following markup into the <code>&lt;head&gt;</code> of the HTML
+		  document at that URL:
+		</p>
+<pre>&lt;link rel="openid.server" href="<?php echo $data['openidserver']; ?>" /&gt;
+&lt;link rel="openid.delegation" href="<?php echo $data['openiddelegation']; ?>" /&gt;
+		
+		</pre>
+		
+		
+		<p><?php
+			
+			if (isset($data['userid'])) {
+				echo 'You are now logged in as ' . $data['userid']; 
+			} else {
+				echo '<a href="' . $data['initssourl'] . '">Login</a>';
+			}
+		
+		?>
+		
+		<p>
+		  Then configure this server so that you can log in with that URL. Once you
+		  have configured the server, and marked up your identity URL, you can verify
+		  that it is working by using the <a href="http://www.openidenabled.com/"
+		  >openidenabled.com</a>
+		  <a href="http://www.openidenabled.com/resources/openid-test/checkup">OpenID Checkup tool</a>:
+		  <form method="post"
+				action="http://www.openidenabled.com/resources/openid-test/checkup/start">
+			<label for="checkup">OpenID URL:
+			</label><input id="checkup" type="text" name="openid_url" />
+			<input type="submit" value="Check" />
+		  </form>
+		</p>
+
+		
+		<h2>About simpleSAMLphp</h2>
+		<p>Hey! This simpleSAMLphp thing is pretty cool, where can I read more about it?
+		You can find more information about simpleSAMLphp at <a href="http://rnd.feide.no">the Feide RnD blog</a> over at <a href="http://uninett.no">UNINETT</a>.</p>
+		
+<?php $this->includeAtTemplateBase('includes/footer.php'); ?>
+
diff --git a/templates/default/en/openid-sites.php b/templates/default/en/openid-sites.php
new file mode 100644
index 000000000..024d9e7a8
--- /dev/null
+++ b/templates/default/en/openid-sites.php
@@ -0,0 +1,81 @@
+<?php $this->includeAtTemplateBase('includes/header.php'); ?>
+
+	<div id="header">
+		<h1>simpleSAMLphp OpenID</h1>
+		<div id="poweredby"><img src="/<?php echo $data['baseurlpath']; ?>resources/icons/bino.png" alt="Bino" /></div>
+	</div>
+	
+	<div id="content">
+
+		<?php if (isset($data['header'])) { echo '<h2>' . $data['header'] . '</h2>'; } ?>
+		
+		
+		<p>[ List of trusted sites |
+		<a href="/<?php echo $data['baseurlpath']; ?>/openid/provider/server.php/about">About simpleSAMLphp OpenID</a> ]</p>
+
+		
+		<p>These decisions have been remembered for this session. All decisions will be forgotten when the session ends.</p>
+		
+		
+		<?php if (isset($data['sites'])) { ?>
+		
+			<div class="form">
+				<form method="post" action="<?php echo '/' . $data['baseurlpath'] . 'openid/provider/server.php/sites'; ?>">
+					<table>
+						<tbody>
+							<?php 
+							
+							    $trusted_sites = array();
+								$untrusted_sites = array();
+								foreach ($data['sites'] as $site => $trusted) {
+									if ($trusted) {
+										$trusted_sites[] = $site;
+									} else {
+										$untrusted_sites[] = $site;
+									}
+								}
+								
+								$i = 0;
+								foreach (array('Trusted Sites' => $trusted_sites,
+											   'Untrusted Sites' => $untrusted_sites) as
+										 $name => $sites) {
+									if ($sites) {
+										echo '<tr><th colspan="2">'. $name . '</th></tr>';
+										foreach ($sites as $site) {
+											$siteid = 'site' . $i;
+											echo '<tr>
+													<td><input type="checkbox" name="' . $siteid . '" value="' . 
+														htmlspecialchars($site, ENT_QUOTES) . '" id="' . $siteid . '" /></td>
+													<td><label for="' . $siteid . '"><code>' . htmlspecialchars($site, ENT_QUOTES) . '</code></label></td>
+												</tr>';
+											$i += 1;
+										}
+									}
+								}
+							
+							
+							?>
+						</tbody>
+					</table>
+					<input type="submit" name="remove" value="Remove Selected" />
+					<input type="submit" name="refresh" value="Refresh List" />
+					<input type="submit" name="forget" value="Forget All" />
+				</form>
+			</div>
+
+		<?php } else { ?>
+		
+			<p>No sites are remembered for this session. When you authenticate with a site,
+				you can choose to add it to this list by choosing <q>Remember this decision</q>.
+			</p>
+
+		<?php } ?>
+
+
+		<h2>About simpleSAMLphp</h2>
+
+			<p>Hey! This simpleSAMLphp thing is pretty cool, where can I read more about it?
+		You can find more information about simpleSAMLphp at <a href="http://rnd.feide.no">the Feide RnD blog</a> over at <a href="http://uninett.no">UNINETT</a>.</p>
+		
+<?php $this->includeAtTemplateBase('includes/footer.php'); ?>
+
diff --git a/templates/default/en/openid-trust.php b/templates/default/en/openid-trust.php
new file mode 100644
index 000000000..2e1266f8d
--- /dev/null
+++ b/templates/default/en/openid-trust.php
@@ -0,0 +1,33 @@
+<?php $this->includeAtTemplateBase('includes/header.php'); ?>
+
+	<div id="header">
+		<h1>simpleSAMLphp OpenID</h1>
+		<div id="poweredby"><img src="/<?php echo $data['baseurlpath']; ?>resources/icons/bino.png" alt="Bino" /></div>
+	</div>
+	
+	<div id="content">
+
+		<?php if (isset($data['header'])) { echo '<h2>' . $data['header'] . '</h2>'; } ?>
+		
+		
+		<p>[ <a href="/<?php echo $data['baseurlpath']; ?>/openid/provider/server.php/sites">List of trusted sites</a> |
+		<a href="/<?php echo $data['baseurlpath']; ?>/openid/provider/server.php/about">About simpleSAMLphp OpenID</a> ]</p>
+		
+		<div class="form">
+		  <p>Do you wish to confirm your identity URL (<code><?php echo $data['openidurl']; ?></code>) 
+		  	with <code><?php echo $data['siteurl']; ?></code>?</p>
+		  <form method="post" action="<?php echo $data['trusturl']; ?>">
+			<input type="checkbox" name="remember" value="on" id="remember"><label
+				for="remember">Remember this decision</label>
+			<br />
+			<input type="submit" name="trust" value="Confirm" />
+			<input type="submit" value="Do not confirm" />
+		  </form>
+		</div>
+
+		
+		<h2>About simpleSAMLphp</h2>
+		<p>Hey! This simpleSAMLphp thing is pretty cool, where can I read more about it?
+		You can find more information about simpleSAMLphp at <a href="http://rnd.feide.no">the Feide RnD blog</a> over at <a href="http://uninett.no">UNINETT</a>.</p>
+		
+<?php $this->includeAtTemplateBase('includes/footer.php'); ?>
\ No newline at end of file
diff --git a/www/openid/provider/server.php b/www/openid/provider/server.php
new file mode 100644
index 000000000..3508a681a
--- /dev/null
+++ b/www/openid/provider/server.php
@@ -0,0 +1,676 @@
+<?php
+
+
+
+
+	
+require_once('../../_include.php');
+
+// Include simpleSAMLphp libraries
+require_once('SimpleSAML/Utilities.php');
+require_once('SimpleSAML/Session.php');
+require_once('SimpleSAML/Logger.php');
+require_once('SimpleSAML/XML/MetaDataStore.php');
+require_once('SimpleSAML/XML/AttributeFilter.php');
+require_once('SimpleSAML/XHTML/Template.php');
+
+// Include openid libs
+require_once 'lib/session.php';
+require_once 'lib/actions.php';
+
+require_once "Auth/OpenID.php";
+require_once "Auth/OpenID/Server.php";
+require_once "Auth/OpenID/HMACSHA1.php";
+require_once "Auth/OpenID/FileStore.php";
+
+session_start();
+
+
+
+
+
+
+
+/*
+ * CONFIGURATION
+ */
+
+
+
+/**
+ * Initialize an OpenID store
+ *
+ * @return object $store an instance of OpenID store (see the
+ * documentation for how to create one)
+ */
+function getOpenIDStore()
+{
+    
+	$config = SimpleSAML_Configuration::getInstance();
+    return new Auth_OpenID_FileStore($config->getValue('openid.filestore'));
+}
+
+/**
+ * Trusted sites is an array of trust roots.
+ *
+ * Sites in this list will not have to be approved by the user in
+ * order to be used. It is OK to leave this value as-is.
+ *
+ * In a more robust server, this should be a per-user setting.
+ */
+$trusted_sites = array(
+);
+
+
+
+
+
+
+/*
+ * ACTIONS
+ */
+
+
+
+/**
+ * Handle a standard OpenID server request
+ */
+function action_default()
+{
+    $server =& getServer();
+    $method = $_SERVER['REQUEST_METHOD'];
+    $request = null;
+    if ($method == 'GET') {
+        $request = $_GET;
+    } else {
+        $request = $_POST;
+    }
+
+    $request = Auth_OpenID::fixArgs($request);
+    $request = $server->decodeRequest($request);
+
+    if (!$request) {
+
+		$config = SimpleSAML_Configuration::getInstance();
+		$metadata = new SimpleSAML_XML_MetaDataStore($config);
+		
+		$t = new SimpleSAML_XHTML_Template($config, 'openid-about.php');
+		$t->data['openidserver'] = $metadata->getGenerated('server', 'openid-provider');
+		
+		
+		$session = SimpleSAML_Session::getInstance(true);
+		
+		$useridfield = $config->getValue('openid.userid_attributename');
+		$delegationprefix = $config->getValue('openid.delegation_prefix');
+		
+		$username = 'your_username';
+		if (isset($session) && $session->isValid() ) {
+			$attributes = $session->getAttributes();
+			$username = $attributes[$useridfield][0];
+			$t->data['userid'] = $username;
+		}
+		$idpmeta = $metadata->getMetaDataCurrent('openid-provider');
+		
+		$relaystate = SimpleSAML_Utilities::selfURLNoQuery() . '?RelayState=' . urlencode($_GET['RelayState']) .
+			'&RequestID=' . urlencode($requestid);
+		$authurl = SimpleSAML_Utilities::addURLparameter('/' . $config->getValue('baseurlpath') . $idpmeta['auth'], 
+			'RelayState=' . urlencode($relaystate));
+		
+		$t->data['initssourl'] 			= $authurl;
+		$t->data['openiddelegation'] 	= $delegationprefix . $username;
+		
+		
+		
+		$t->show();    
+		exit(0);
+    }
+
+    setRequestInfo($request);
+
+    if (in_array($request->mode,
+                 array('checkid_immediate', 'checkid_setup'))) {
+
+        if (isTrusted($request->identity, $request->trust_root)) {
+            $response =& $request->answer(true);
+            $sreg = getSreg($request->identity);
+            if (is_array($sreg)) {
+                foreach ($sreg as $k => $v) {
+                    $response->addField('sreg', $k, $v);
+                }
+            }
+        } else if ($request->immediate) {
+            $response =& $request->answer(false, getServerURL());
+        } else {
+            if (!getLoggedInUser()) {
+            	// TODO Login
+                //return login_render();
+                check_authenticated_user();
+            }
+            
+			$config = SimpleSAML_Configuration::getInstance();
+			$t = new SimpleSAML_XHTML_Template($config, 'openid-trust.php');
+			
+			$t->data['openidurl'] = getLoggedInUser();
+			$t->data['siteurl'] = htmlspecialchars($request->trust_root);;
+			$t->data['trusturl'] = buildURL('trust', true);
+			
+			$t->show();    
+			exit(0);
+            
+            //return trust_render($request);
+        }
+    } else {
+        $response =& $server->handleRequest($request);
+    }
+
+    $webresponse =& $server->encodeResponse($response);
+
+    foreach ($webresponse->headers as $k => $v) {
+        header("$k: $v");
+    }
+
+    header(header_connection_close);
+    print $webresponse->body;
+    exit(0);
+}
+
+/**
+ * Log out the currently logged in user
+ */
+function action_logout()
+{
+    setLoggedInUser(null);
+    setRequestInfo(null);
+    return authCancel(null);
+}
+
+/**
+ * Check the input values for a login request
+ */
+function _login_checkInput($input)
+{
+    $openid_url = false;
+    $errors = array();
+
+    if (!isset($input['openid_url'])) {
+        $errors[] = 'Enter an OpenID URL to continue';
+    }
+    if (!isset($input['password'])) {
+        $errors[] = 'Enter a password to continue';
+    }
+    if (count($errors) == 0) {
+        $openid_url = $input['openid_url'];
+        $openid_url = Auth_OpenID::normalizeUrl($openid_url);
+        $password = $input['password'];
+        if (!checkLogin($openid_url, $password)) {
+            $errors[] = 'The entered password does not match the ' .
+                'entered identity URL.';
+        }
+    }
+    return array($errors, $openid_url);
+}
+
+
+
+
+function check_authenticated_user() {
+
+	//session_start();
+	
+	$config = SimpleSAML_Configuration::getInstance();
+	$metadata = new SimpleSAML_XML_MetaDataStore($config);
+	$session = SimpleSAML_Session::getInstance(true);
+	
+	$logger = new SimpleSAML_Logger();
+	
+	$idpentityid = $metadata->getMetaDataCurrentEntityID('openid-provider');
+	$idpmeta = $metadata->getMetaDataCurrent('openid-provider');
+
+
+	/* Check if valid local session exists.. */
+	if (!isset($session) || !$session->isValid() ) {
+	
+		
+		
+		$relaystate = SimpleSAML_Utilities::selfURLNoQuery() . '/login';
+		$authurl = SimpleSAML_Utilities::addURLparameter('/' . $config->getValue('baseurlpath') . $idpmeta['auth'], 
+			'RelayState=' . urlencode($relaystate));
+		
+	
+		header('Location: ' . $authurl);
+		exit(0);
+	}
+	
+	$attributes = $session->getAttributes();
+	$info = getRequestInfo();
+	
+	
+	$useridfield = $config->getValue('openid.userid_attributename');
+	$delegationprefix = $config->getValue('openid.delegation_prefix');
+	
+	$username = $attributes[$useridfield][0];
+
+		
+	$openid_url = $delegationprefix . $username;
+
+	error_log('set logged in user to be [' .$delegationprefix. '][' . $username . ']' );
+	setLoggedInUser($openid_url);
+
+}
+
+
+/**
+ * Log in a user and potentially continue the requested identity approval
+ */
+function action_login()
+{
+
+	error_log('action login');
+	
+	//session_start();
+	
+	check_authenticated_user();
+	
+	$info = getRequestInfo();
+	
+	return doAuth($info);
+
+}
+
+
+
+
+
+/**
+ * Ask the user whether he wants to trust this site
+ */
+function action_trust()
+{
+    $info = getRequestInfo();
+    $trusted = isset($_POST['trust']);
+    if ($info && isset($_POST['remember'])) {
+        $sites = getSessionSites();
+        $sites[$info->trust_root] = $trusted;
+        setSessionSites($sites);
+    }
+    return doAuth($info, $trusted, true);
+}
+
+function action_sites()
+{
+    $sites = getSessionSites();
+    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+        if (isset($_POST['forget'])) {
+            $sites = null;
+            setSessionSites($sites);
+        } elseif (isset($_POST['remove'])) {
+            foreach ($_POST as $k => $v) {
+                if (preg_match('/^site[0-9]+$/', $k) && isset($sites[$v])) {
+                    unset($sites[$v]);
+                }
+            }
+            setSessionSites($sites);
+        }
+    }
+    
+	$config = SimpleSAML_Configuration::getInstance();
+	$t = new SimpleSAML_XHTML_Template($config, 'openid-sites.php');
+	
+	$t->data['openidurl'] = getLoggedInUser();
+	$t->data['sites'] = $sites;
+	
+	$t->show();    
+	exit(0);
+    
+    
+    // TODO Render sites
+    //return sites_render($sites);
+}
+
+
+
+/**
+ * Return an HTTP redirect response
+ */
+function redirect_render($redir_url)
+{
+	/*
+    $headers = array(http_found,
+                     header_content_text,
+                     header_connection_close,
+                     'Location: ' . $redir_url,
+                     );
+      */               
+	header('Location: ' . $redir_url);
+                     
+//    $body = sprintf(redirect_message, $redir_url);
+ //   return array($headers, $body);
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/*
+ * SESSION
+ */
+
+
+
+/**
+ * Get the URL of the current script
+ */
+function getServerURL()
+{
+    $path = $_SERVER['SCRIPT_NAME'];
+    $host = $_SERVER['HTTP_HOST'];
+    $port = $_SERVER['SERVER_PORT'];
+    $s = $_SERVER['HTTPS'] ? 's' : '';
+    if (($s && $port == "443") || (!$s && $port == "80")) {
+        $p = '';
+    } else {
+        $p = ':' . $port;
+    }
+    
+    return "http$s://$host$p$path";
+}
+
+/**
+ * Build a URL to a server action
+ */
+function buildURL($action=null, $escaped=true)
+{
+    $url = getServerURL();
+    if ($action) {
+        $url .= '/' . $action;
+    }
+    return $escaped ? htmlspecialchars($url, ENT_QUOTES) : $url;
+}
+
+/**
+ * Extract the current action from the request
+ */
+function getAction()
+{
+    $path_info = @$_SERVER['PATH_INFO'];
+    $action = ($path_info) ? substr($path_info, 1) : '';
+    $function_name = 'action_' . $action;
+    return $function_name;
+}
+
+/**
+ * Write the response to the request
+ */
+function writeResponse($resp)
+{
+    list ($headers, $body) = $resp;
+    array_walk($headers, 'header');
+    header(header_connection_close);
+    print $body;
+}
+
+/**
+ * Instantiate a new OpenID server object
+ */
+function getServer()
+{
+    static $server = null;
+    if (!isset($server)) {
+        $server =& new Auth_OpenID_Server(getOpenIDStore());
+    }
+    return $server;
+}
+
+/**
+ * Return whether the trust root is currently trusted
+ */
+function isTrusted($identity_url, $trust_root)
+{
+    // from config.php
+    global $trusted_sites;
+
+    if ($identity_url != getLoggedInUser()) {
+        return false;
+    }
+
+    if (in_array($trust_root, $trusted_sites)) {
+        return true;
+    }
+
+    $sites = getSessionSites();
+    return isset($sites[$trust_root]) && $sites[$trust_root];
+}
+
+/**
+ * Return a hashed form of the user's password
+ */
+function hashPassword($password)
+{
+    return bin2hex(Auth_OpenID_SHA1($password));
+}
+
+/**
+ * Check the user's login information
+ */
+function checkLogin($openid_url, $password)
+{
+    // from config.php
+    global $openid_users;
+    $hash = hashPassword($password);
+
+    return isset($openid_users[$openid_url])
+        && $hash == $openid_users[$openid_url];
+}
+
+/**
+ * Get the openid_url out of the cookie
+ *
+ * @return mixed $openid_url The URL that was stored in the cookie or
+ * false if there is none present or if the cookie is bad.
+ */
+function getLoggedInUser()
+{
+    return isset($_SESSION['openid_url'])
+        ? $_SESSION['openid_url']
+        : false;
+}
+
+/**
+ * Set the openid_url in the cookie
+ *
+ * @param mixed $identity_url The URL to set. If set to null, the
+ * value will be unset.
+ */
+function setLoggedInUser($identity_url=null)
+{
+    if (!isset($identity_url)) {
+        unset($_SESSION['openid_url']);
+    } else {
+        $_SESSION['openid_url'] = $identity_url;
+    }
+}
+
+function setSessionSites($sites=null)
+{
+    if (!isset($sites)) {
+        unset($_SESSION['session_sites']);
+    } else {
+        $_SESSION['session_sites'] = serialize($sites);
+    }
+}
+
+function getSessionSites()
+{
+    return isset($_SESSION['session_sites'])
+        ? unserialize($_SESSION['session_sites'])
+        : false;
+}
+
+function getRequestInfo()
+{
+    return isset($_SESSION['request'])
+        ? unserialize($_SESSION['request'])
+        : false;
+}
+
+function setRequestInfo($info=null)
+{
+    if (!isset($info)) {
+        unset($_SESSION['request']);
+    } else {
+        $_SESSION['request'] = serialize($info);
+    }
+}
+
+
+function getSreg($identity)
+{
+    // from config.php
+    global $openid_sreg;
+
+    if (!is_array($openid_sreg)) {
+        return null;
+    }
+
+    return $openid_sreg[$identity];
+
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/*
+ * OpenID Transactions
+ */ 
+
+
+
+function authCancel($info)
+{
+    if ($info) {
+        setRequestInfo();
+        $url = $info->getCancelURL();
+    } else {
+        $url = getServerURL();
+    }
+    redirect_render($url);
+}
+
+function doAuth($info, $trusted=null, $fail_cancels=false)
+{
+    if (!$info) {
+        // There is no authentication information, so bail
+        authCancel(null);
+    }
+
+    $req_url = $info->identity;
+    $user = getLoggedInUser();
+    setRequestInfo($info);
+
+    if ($req_url != $user) {
+		error_log('simpleSAMLphp doauth():' . 'Your identity ' . $user . 
+			' does not match the requested identity from the OpenID consumer, which was: ' . $req_url);
+		$config = SimpleSAML_Configuration::getInstance();
+		$t = new SimpleSAML_XHTML_Template($config, 'error.php');
+
+		$t->data['header'] = 'OpenID identity mismatch';
+		$t->data['message'] = 'Your identity ' . $user . ' does not match the requested identity from the 
+			OpenID consumer, which was: ' . $req_url;
+		$t->data['e'] = new Exception('OpenID Error');
+		
+		$t->show();    
+		exit(0);
+		
+    }
+
+    $sites = getSessionSites();
+    $trust_root = $info->trust_root;
+    $fail_cancels = $fail_cancels || isset($sites[$trust_root]);
+    $trusted = isset($trusted) ? $trusted : isTrusted($req_url, $trust_root);
+    
+    if ($trusted) {
+        setRequestInfo();
+        $server =& getServer();
+        $response =& $info->answer(true);
+        $webresponse =& $server->encodeResponse($response);
+
+        $new_headers = array();
+
+        foreach ($webresponse->headers as $k => $v) {
+            $new_headers[] = $k.": ".$v;
+        }
+
+
+		array_walk($new_headers, 'header');
+		header(header_connection_close);
+		print $webresponse->body;
+        
+        
+    } elseif ($fail_cancels) {
+        authCancel($info);
+    } else {
+
+		$config = SimpleSAML_Configuration::getInstance();
+		$t = new SimpleSAML_XHTML_Template($config, 'openid-trust.php');
+		
+		$t->data['openidurl'] = getLoggedInUser();
+		$t->data['siteurl'] = htmlspecialchars($request->trust_root);;
+		$t->data['trusturl'] = buildURL('trust', true);
+		
+		$t->show();    
+		exit(0);
+
+
+    }
+}
+
+
+
+
+/*
+ * Handle actions
+ */
+
+
+//init();
+$action = getAction();
+if (!function_exists($action)) {
+	$action = 'action_default';
+}
+$action();
+
+
+
+?>
\ No newline at end of file
-- 
GitLab