diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2009-06-12 22:36:06 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2009-06-12 22:36:06 -0700 |
commit | fc714cae7f69d24af4ab12bca1dcbb351708e413 (patch) | |
tree | 5e8fe95847b7ab69794a6ebf3382920c60af21ce | |
parent | 7111f5c52f07e191424f0e4b0ade85e0db11c116 (diff) | |
download | DotNetOpenAuth-fc714cae7f69d24af4ab12bca1dcbb351708e413.zip DotNetOpenAuth-fc714cae7f69d24af4ab12bca1dcbb351708e413.tar.gz DotNetOpenAuth-fc714cae7f69d24af4ab12bca1dcbb351708e413.tar.bz2 |
Progress on moving OpenIdAjaxTextBox-like behavior into the common base class.
7 files changed, 682 insertions, 254 deletions
diff --git a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj index 3957cfa..1a7a406 100644 --- a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj +++ b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj @@ -128,6 +128,7 @@ <Compile Include="Hosting\HttpHost.cs" /> <Compile Include="Hosting\TestingWorkerRequest.cs" /> <Compile Include="Messaging\CollectionAssert.cs" /> + <Compile Include="Messaging\EnumerableCacheTests.cs" /> <Compile Include="Messaging\ErrorUtilitiesTests.cs" /> <Compile Include="Messaging\MessageSerializerTests.cs" /> <Compile Include="Messaging\Reflection\MessageDescriptionTests.cs" /> diff --git a/src/DotNetOpenAuth.Test/Messaging/EnumerableCacheTests.cs b/src/DotNetOpenAuth.Test/Messaging/EnumerableCacheTests.cs new file mode 100644 index 0000000..7355388 --- /dev/null +++ b/src/DotNetOpenAuth.Test/Messaging/EnumerableCacheTests.cs @@ -0,0 +1,129 @@ +//----------------------------------------------------------------------- +// <copyright file="EnumerableCacheTests.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// This code is released under the Microsoft Public License (Ms-PL). +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Test.Messaging { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// <summary> + /// Tests for cached enumeration. + /// </summary> + [TestClass] + public class EnumerableCacheTests { + /// <summary> + /// The number of times the generator method's implementation is started. + /// </summary> + private int generatorInvocations; + + /// <summary> + /// The number of times the end of the generator method's implementation is reached. + /// </summary> + private int generatorCompleted; + + /// <summary> + /// Gets or sets the test context. + /// </summary> + public TestContext TestContext { get; set; } + + /// <summary> + /// Sets up a test. + /// </summary> + [TestInitialize] + public void Setup() { + this.generatorInvocations = 0; + this.generatorCompleted = 0; + } + + [TestMethod] + public void EnumerableCache() { + // Baseline + var generator = this.NumberGenerator(); + var list1 = generator.ToList(); + var list2 = generator.ToList(); + Assert.AreEqual(2, this.generatorInvocations); + CollectionAssert.AreEqual(list1, list2); + + // Cache behavior + this.generatorInvocations = 0; + this.generatorCompleted = 0; + generator = this.NumberGenerator().CacheGeneratedResults(); + var list3 = generator.ToList(); + var list4 = generator.ToList(); + Assert.AreEqual(1, this.generatorInvocations); + Assert.AreEqual(1, this.generatorCompleted); + CollectionAssert.AreEqual(list1, list3); + CollectionAssert.AreEqual(list1, list4); + } + + [TestMethod] + public void GeneratesOnlyRequiredElements() { + var generator = this.NumberGenerator().CacheGeneratedResults(); + Assert.AreEqual(0, this.generatorInvocations); + generator.Take(2).ToList(); + Assert.AreEqual(1, this.generatorInvocations); + Assert.AreEqual(0, this.generatorCompleted, "Only taking part of the list should not have completed the generator."); + } + + [TestMethod] + public void PassThruDoubleCache() { + var cache1 = this.NumberGenerator().CacheGeneratedResults(); + var cache2 = cache1.CacheGeneratedResults(); + Assert.AreSame(cache1, cache2, "Two caches were set up rather than just sharing the first one."); + } + + [TestMethod] + public void PassThruList() { + var list = this.NumberGenerator().ToList(); + var cache = list.CacheGeneratedResults(); + Assert.AreSame(list, cache); + } + + [TestMethod] + public void PassThruArray() { + var array = this.NumberGenerator().ToArray(); + var cache = array.CacheGeneratedResults(); + Assert.AreSame(array, cache); + } + + [TestMethod] + public void PassThruCollection() { + var collection = new Collection<int>(); + var cache = collection.CacheGeneratedResults(); + Assert.AreSame(collection, cache); + } + + /// <summary> + /// Tests calling IEnumerator.Current before first call to MoveNext. + /// </summary> + [TestMethod, ExpectedException(typeof(InvalidOperationException))] + public void EnumerableCacheCurrentThrowsBefore() { + var foo = this.NumberGenerator().CacheGeneratedResults().GetEnumerator().Current; + } + + /// <summary> + /// Tests calling IEnumerator.Current after MoveNext returns false. + /// </summary> + [TestMethod, ExpectedException(typeof(InvalidOperationException))] + public void EnumerableCacheCurrentThrowsAfter() { + var enumerator = this.NumberGenerator().CacheGeneratedResults().GetEnumerator(); + while (enumerator.MoveNext()) { + } + var foo = enumerator.Current; + } + + private IEnumerable<int> NumberGenerator() { + this.generatorInvocations++; + for (int i = 10; i < 15; i++) { + yield return i; + } + this.generatorCompleted++; + } + } +} diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 6209b2a..34dc7b4 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -211,6 +211,7 @@ <Compile Include="Messaging\CachedDirectWebResponse.cs" /> <Compile Include="Messaging\ChannelContract.cs" /> <Compile Include="Messaging\DirectWebRequestOptions.cs" /> + <Compile Include="Messaging\EnumerableCache.cs" /> <Compile Include="Messaging\HostErrorException.cs" /> <Compile Include="Messaging\IHttpDirectResponse.cs" /> <Compile Include="Messaging\IExtensionMessage.cs" /> @@ -563,6 +564,9 @@ <ItemGroup> <EmbeddedResource Include="OpenId\RelyingParty\OpenIdRelyingPartyControlBase.js" /> </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="OpenId\RelyingParty\OpenIdRelyingPartyControlBase.ReturnTo.html" /> + </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="..\..\tools\DotNetOpenAuth.Versioning.targets" /> </Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/Messaging/EnumerableCache.cs b/src/DotNetOpenAuth/Messaging/EnumerableCache.cs new file mode 100644 index 0000000..d343410 --- /dev/null +++ b/src/DotNetOpenAuth/Messaging/EnumerableCache.cs @@ -0,0 +1,244 @@ +//----------------------------------------------------------------------- +// <copyright file="EnumerableCache.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// This code is released under the Microsoft Public License (Ms-PL). +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System; + using System.Collections; + using System.Collections.Generic; + + /// <summary> + /// Extension methods for <see cref="IEnumerable<T>"/> types. + /// </summary> + public static class EnumerableCacheExtensions { + /// <summary> + /// Caches the results of enumerating over a given object so that subsequence enumerations + /// don't require interacting with the object a second time. + /// </summary> + /// <typeparam name="T">The type of element found in the enumeration.</typeparam> + /// <param name="sequence">The enumerable object.</param> + /// <returns> + /// Either a new enumerable object that caches enumerated results, or the original, <paramref name="sequence"/> + /// object if no caching is necessary to avoid additional CPU work. + /// </returns> + /// <remarks> + /// <para>This is designed for use on the results of generator methods (the ones with <c>yield return</c> in them) + /// so that only those elements in the sequence that are needed are ever generated, while not requiring + /// regeneration of elements that are enumerated over multiple times.</para> + /// <para>This can be a huge performance gain if enumerating multiple times over an expensive generator method.</para> + /// <para>Some enumerable types such as collections, lists, and already-cached generators do not require + /// any (additional) caching, and this method will simply return those objects rather than caching them + /// to avoid double-caching.</para> + /// </remarks> + public static IEnumerable<T> CacheGeneratedResults<T>(this IEnumerable<T> sequence) { + // Don't create a cache for types that don't need it. + if (sequence is IList<T> || + sequence is ICollection<T> || + sequence is Array || + sequence is EnumerableCache<T>) { + return sequence; + } + + return new EnumerableCache<T>(sequence); + } + + /// <summary> + /// A wrapper for <see cref="IEnumerable<T>"/> types and returns a caching <see cref="IEnumerator<T>"/> + /// from its <see cref="IEnumerable<T>.GetEnumerator"/> method. + /// </summary> + /// <typeparam name="T">The type of element in the sequence.</typeparam> + private class EnumerableCache<T> : IEnumerable<T> { + /// <summary> + /// The results from enumeration of the live object that have been collected thus far. + /// </summary> + private List<T> cache; + + /// <summary> + /// The original generator method or other enumerable object whose contents should only be enumerated once. + /// </summary> + private IEnumerable<T> generator; + + /// <summary> + /// The enumerator we're using over the generator method's results. + /// </summary> + private IEnumerator<T> generatorEnumerator; + + /// <summary> + /// The sync object our caching enumerators use when adding a new live generator method result to the cache. + /// </summary> + /// <remarks> + /// Although individual enumerators are not thread-safe, this <see cref="IEnumerable<T>"/> should be + /// thread safe so that multiple enumerators can be created from it and used from different threads. + /// </remarks> + private object generatorLock = new object(); + + /// <summary> + /// Initializes a new instance of the EnumerableCache class. + /// </summary> + /// <param name="generator">The generator.</param> + internal EnumerableCache(IEnumerable<T> generator) { + if (generator == null) { + throw new ArgumentNullException("generator"); + } + + this.generator = generator; + } + + #region IEnumerable<T> Members + + /// <summary> + /// Returns an enumerator that iterates through the collection. + /// </summary> + /// <returns> + /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection. + /// </returns> + public IEnumerator<T> GetEnumerator() { + if (this.generatorEnumerator == null) { + this.cache = new List<T>(); + this.generatorEnumerator = this.generator.GetEnumerator(); + } + + return new EnumeratorCache(this); + } + + #endregion + + #region IEnumerable Members + + /// <summary> + /// Returns an enumerator that iterates through a collection. + /// </summary> + /// <returns> + /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection. + /// </returns> + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { + return this.GetEnumerator(); + } + + #endregion + + /// <summary> + /// An enumerator that uses cached enumeration results whenever they are available, + /// and caches whatever results it has to pull from the original <see cref="IEnumerable<T>"/> object. + /// </summary> + private class EnumeratorCache : IEnumerator<T> { + /// <summary> + /// The parent enumeration wrapper class that stores the cached results. + /// </summary> + private EnumerableCache<T> parent; + + /// <summary> + /// The position of this enumerator in the cached list. + /// </summary> + private int cachePosition = -1; + + /// <summary> + /// Initializes a new instance of the <see cref="EnumerableCache<T>.EnumeratorCache"/> class. + /// </summary> + /// <param name="parent">The parent cached enumerable whose GetEnumerator method is calling this constructor.</param> + internal EnumeratorCache(EnumerableCache<T> parent) { + if (parent == null) { + throw new ArgumentNullException("parent"); + } + + this.parent = parent; + } + + #region IEnumerator<T> Members + + /// <summary> + /// Gets the element in the collection at the current position of the enumerator. + /// </summary> + /// <returns> + /// The element in the collection at the current position of the enumerator. + /// </returns> + public T Current { + get { + if (this.cachePosition < 0 || this.cachePosition >= this.parent.cache.Count) { + throw new InvalidOperationException(); + } + + return this.parent.cache[this.cachePosition]; + } + } + + #endregion + + #region IEnumerator Properties + + /// <summary> + /// Gets the element in the collection at the current position of the enumerator. + /// </summary> + /// <returns> + /// The element in the collection at the current position of the enumerator. + /// </returns> + object System.Collections.IEnumerator.Current { + get { return this.Current; } + } + + #endregion + + #region IDisposable Members + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + #region IEnumerator Methods + + /// <summary> + /// Advances the enumerator to the next element of the collection. + /// </summary> + /// <returns> + /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection. + /// </returns> + /// <exception cref="T:System.InvalidOperationException"> + /// The collection was modified after the enumerator was created. + /// </exception> + public bool MoveNext() { + this.cachePosition++; + if (this.cachePosition >= this.parent.cache.Count) { + lock (this.parent.generatorLock) { + if (this.parent.generatorEnumerator.MoveNext()) { + this.parent.cache.Add(this.parent.generatorEnumerator.Current); + } else { + return false; + } + } + } + + return true; + } + + /// <summary> + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// </summary> + /// <exception cref="T:System.InvalidOperationException"> + /// The collection was modified after the enumerator was created. + /// </exception> + public void Reset() { + this.cachePosition = -1; + } + + #endregion + + /// <summary> + /// Releases unmanaged and - optionally - managed resources + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) { + // Nothing to do here. + } + } + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.ReturnTo.html b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.ReturnTo.html new file mode 100644 index 0000000..22ac9a2 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.ReturnTo.html @@ -0,0 +1,11 @@ +<html> + <head> + <script type="text/javascript" language="javascript"> + //<![CDATA[ + alert("Hey, it worked: " + location); + //]]> + </script> + </head> + <body> + </body> +</html>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs index dff6fc9..9ff78bf 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -5,6 +5,7 @@ //----------------------------------------------------------------------- [assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.EmbeddedJavascriptResource, "text/javascript")] +[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.ReturnToStaticPageResource, "text/html")] namespace DotNetOpenAuth.OpenId.RelyingParty { using System; @@ -23,6 +24,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId.Extensions.UI; + using System.Web; /// <summary> /// A common base class for OpenID Relying Party controls. @@ -34,6 +36,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { internal const string EmbeddedJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyControlBase.js"; /// <summary> + /// The manifest resource name of the static HTML file that serves as the return_to URL for popup windows and iframes. + /// </summary> + internal const string ReturnToStaticPageResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyControlBase.ReturnTo.html"; + + /// <summary> /// The name of the javascript function that will initiate a synchronous callback. /// </summary> protected const string CallbackJsFunction = "window.dnoa_internal.callback"; @@ -70,11 +77,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const bool UsePersistentCookieDefault = false; /// <summary> - /// The default value for the <see cref="ReturnToUrl"/> property. - /// </summary> - private const string ReturnToUrlDefault = ""; - - /// <summary> /// The default value for the <see cref="RealmUrl"/> property. /// </summary> private const string RealmUrlDefault = "~/"; @@ -109,11 +111,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const string RealmUrlViewStateKey = "RealmUrl"; /// <summary> - /// The viewstate key to use for the <see cref="ReturnToUrl"/> property. - /// </summary> - private const string ReturnToUrlViewStateKey = "ReturnToUrl"; - - /// <summary> /// The key under which the value for the <see cref="Identifier"/> property will be stored. /// </summary> private const string IdentifierViewStateKey = "Identifier"; @@ -263,39 +260,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Gets or sets the OpenID ReturnTo of the relying party web site. - /// </summary> - [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Bindable property must be simple type")] - [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")] - [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")] - [Bindable(true), DefaultValue(ReturnToUrlDefault), Category(BehaviorCategory)] - [Description("The OpenID ReturnTo of the relying party web site.")] - [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] - public string ReturnToUrl { - get { - return (string)(this.ViewState[ReturnToUrlViewStateKey] ?? ReturnToUrlDefault); - } - - set { - if (this.Page != null && !this.DesignMode) { - // Validate new value by trying to construct a Uri based on it. - new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.Page.ResolveUrl(value)); // throws an exception on failure. - } else { - // We can't fully test it, but it should start with either ~/ or a protocol. - if (Regex.IsMatch(value, @"^https?://")) { - new Uri(value); // make sure it's fully-qualified, but ignore wildcards - } else if (value.StartsWith("~/", StringComparison.Ordinal)) { - // this is valid too - } else { - throw new UriFormatException(); - } - } - - this.ViewState[ReturnToUrlViewStateKey] = value; - } - } - - /// <summary> /// Gets or sets a value indicating whether to send a persistent cookie upon successful /// login so the user does not have to log in upon returning to this site. /// </summary> @@ -346,7 +310,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// provided in the text box. /// </summary> public void LogOn() { - IAuthenticationRequest request = this.CreateRequest(); + IAuthenticationRequest request = this.CreateRequests().FirstOrDefault(); if (this.IsPopupAppropriate(request)) { this.ScriptPopupWindow(request); } else { @@ -393,9 +357,9 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { discoveryResultBuilder.Append("{"); try { this.Identifier = userSuppliedIdentifier; - List<IAuthenticationRequest> requests = new List<IAuthenticationRequest>(new [] { this.CreateRequest() }); - if (requests.Count > 0) { - discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(requests[0].ClaimedIdentifier)); + IEnumerable<IAuthenticationRequest> requests = this.CreateRequests().CacheGeneratedResults(); + if (requests.Any()) { + discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(requests.First().ClaimedIdentifier)); discoveryResultBuilder.Append("requests: ["); foreach (IAuthenticationRequest request in requests) { this.OnLoggingIn(request); @@ -426,57 +390,82 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion /// <summary> - /// Constructs the authentication request and returns it. - /// </summary> - /// <returns>The instantiated authentication request, or <c>null</c> if a failure occurred.</returns> - [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")] - protected virtual IAuthenticationRequest CreateRequest() { - Contract.Requires(this.Identifier != null, OpenIdStrings.OpenIdTextBoxEmpty); - ErrorUtilities.VerifyOperation(!string.IsNullOrEmpty(this.Identifier), OpenIdStrings.OpenIdTextBoxEmpty); - IAuthenticationRequest request; - - try { - // Approximate the returnTo (either based on the customize property or the page URL) - // so we can use it to help with Realm resolution. - var requestContext = this.RelyingParty.Channel.GetRequestFromContext(); - Uri returnToApproximation = this.ReturnToUrl != null ? new Uri(requestContext.UrlBeforeRewriting, this.ReturnToUrl) : this.Page.Request.Url; - - // Resolve the trust root, and swap out the scheme and port if necessary to match the - // return_to URL, since this match is required by OpenId, and the consumer app - // may be using HTTP at some times and HTTPS at others. - UriBuilder realm = OpenIdUtilities.GetResolvedRealm(this.Page, this.RealmUrl, this.RelyingParty.Channel.GetRequestFromContext()); - realm.Scheme = returnToApproximation.Scheme; - realm.Port = returnToApproximation.Port; - - // Initiate openid request - // We use TryParse here to avoid throwing an exception which - // might slip through our validator control if it is disabled. - Realm typedRealm = new Realm(realm); - if (string.IsNullOrEmpty(this.ReturnToUrl)) { - request = this.RelyingParty.CreateRequest(this.Identifier, typedRealm); - } else { - // Since the user actually gave us a return_to value, - // the "approximation" is exactly what we want. - request = this.RelyingParty.CreateRequest(this.Identifier, typedRealm, returnToApproximation); - } - - if (this.IsPopupAppropriate(request)) { - // Inform the OP that it will appear in a popup window. - request.AddExtension(new UIRequest()); + /// Creates the authentication requests for a given user-supplied Identifier. + /// </summary> + /// <returns>A sequence of authentication requests, any one of which may be + /// used to determine the user's control of the <see cref="IAuthenticationRequest.ClaimedIdentifier"/>.</returns> + private IEnumerable<IAuthenticationRequest> CreateRequests() { + Contract.Requires(this.Identifier != null, OpenIdStrings.NoIdentifierSet); + ErrorUtilities.VerifyOperation(this.Identifier != null, OpenIdStrings.NoIdentifierSet); + + // The return_to page will be a static resource that has simple javascript to pass the openid response + // to the parent frame or window. + Uri returnTo = new Uri( + this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, + this.Page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), ReturnToStaticPageResource)); + + // Resolve the trust root, and swap out the scheme and port if necessary to match the + // return_to URL, since this match is required by OpenId, and the consumer app + // may be using HTTP at some times and HTTPS at others. + UriBuilder realm = OpenIdUtilities.GetResolvedRealm(this.Page, this.RealmUrl, this.RelyingParty.Channel.GetRequestFromContext()); + realm.Scheme = returnTo.Scheme; + realm.Port = returnTo.Port; + + // Initiate openid request + // We use TryParse here to avoid throwing an exception which + // might slip through our validator control if it is disabled. + Realm typedRealm = new Realm(realm); + var requests = this.RelyingParty.CreateRequests(this.Identifier, typedRealm, returnTo); + + // Some OPs may be listed multiple times (one with HTTPS and the other with HTTP, for example). + // Since we're gathering OPs to try one after the other, just take the first choice of each OP + // and don't try it multiple times. + requests = requests.Distinct(DuplicateRequestedHostsComparer.Instance); + + // Configure each generated request. + int reqIndex = 0; + foreach (var req in requests) { + req.AddCallbackArguments("index", (reqIndex++).ToString(CultureInfo.InvariantCulture)); + + if (this.IsPopupAppropriate(req)) { + // Inform the OP that we'll be using a popup window. + req.AddExtension(new UIRequest()); + + // Provide a hint for the client javascript about whether the OP supports the UI extension. + // This is so the window can be made the correct size for the extension. + // If the OP doesn't advertise support for the extension, the javascript will use + // a bigger popup window. + req.AddCallbackArguments("dotnetopenid.popupUISupported", "1"); } // Add state that needs to survive across the redirect. if (!this.Stateless) { - request.AddCallbackArguments(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString(CultureInfo.InvariantCulture)); + req.AddCallbackArguments(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString(CultureInfo.InvariantCulture)); } - this.OnLoggingIn(request); - } catch (ProtocolException ex) { - this.OnFailed(new FailedAuthenticationResponse(ex)); - return null; - } + // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter + if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)["dotnetopenid.userSuppliedIdentifier"])) { + req.AddCallbackArguments("dotnetopenid.userSuppliedIdentifier", this.Identifier); + } + + // Our javascript needs to let the user know which endpoint responded. So we force it here. + // This gives us the info even for 1.0 OPs and 2.0 setup_required responses. + req.AddCallbackArguments("dotnetopenid.op_endpoint", req.Provider.Uri.AbsoluteUri); + req.AddCallbackArguments("dotnetopenid.claimed_id", (string)req.ClaimedIdentifier ?? string.Empty); + req.AddCallbackArguments("dotnetopenid.phase", "2"); + ((AuthenticationRequest)req).AssociationPreference = AssociationPreference.IfAlreadyEstablished; + + this.OnLoggingIn(req); - return request; + // We append a # at the end so that if the OP happens to support it, + // the OpenID response "query string" is appended after the hash rather than before, resulting in the + // browser being super-speedy in closing the popup window since it doesn't try to pull a newer version + // of the static resource down from the server merely because of a changed URL. + // http://www.nabble.com/Re:-Defining-how-OpenID-should-behave-with-fragments-in-the-return_to-url-p22694227.html + // TODO: + + yield return req; + } } /// <summary> @@ -700,5 +689,61 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.Page.ClientScript.RegisterStartupScript(this.GetType(), "loginPopupClose", startupScript.ToString(), true); } + + /// <summary> + /// An authentication request comparer that judges equality solely on the OP endpoint hostname. + /// </summary> + private class DuplicateRequestedHostsComparer : IEqualityComparer<IAuthenticationRequest> { + /// <summary> + /// The singleton instance of this comparer. + /// </summary> + internal static IEqualityComparer<IAuthenticationRequest> Instance = new DuplicateRequestedHostsComparer(); + + /// <summary> + /// Initializes a new instance of the <see cref="DuplicateRequestedHostsComparer"/> class. + /// </summary> + private DuplicateRequestedHostsComparer() { + } + + #region IEqualityComparer<IAuthenticationRequest> Members + + /// <summary> + /// Determines whether the specified objects are equal. + /// </summary> + /// <param name="x">The first object of type <paramref name="T"/> to compare.</param> + /// <param name="y">The second object of type <paramref name="T"/> to compare.</param> + /// <returns> + /// true if the specified objects are equal; otherwise, false. + /// </returns> + public bool Equals(IAuthenticationRequest x, IAuthenticationRequest y) { + if (x == null && y == null) { + return true; + } + + if (x == null || y == null) { + return false; + } + + // We'll distinguish based on the host name only, which + // admittedly is only a heuristic, but if we remove one that really wasn't a duplicate, well, + // this multiple OP attempt thing was just a convenience feature anyway. + return string.Equals(x.Provider.Uri.Host, y.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Returns a hash code for the specified object. + /// </summary> + /// <param name="obj">The <see cref="T:System.Object"/> for which a hash code is to be returned.</param> + /// <returns>A hash code for the specified object.</returns> + /// <exception cref="T:System.ArgumentNullException"> + /// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null. + /// </exception> + public int GetHashCode(IAuthenticationRequest obj) { + return obj.Provider.Uri.Host.GetHashCode(); + } + + #endregion + } + } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js index ade88e6..0da5cd9 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js @@ -8,16 +8,6 @@ //window.openid_visible_iframe = true; // causes the hidden iframe to show up //window.openid_trace = true; // causes lots of messages -window.dnoa_internal = new Object(); -window.dnoa_internal.discoveryCompletedCallbacks = new Array(); // user supplied identifiers with functions to call back when discovery completes -window.dnoa_internal.discoveryResults = new Array(); // user supplied identifiers and discovery results - -// The possible authentication results -window.dnoa_internal.authSuccess = new Object(); -window.dnoa_internal.authRefused = new Object(); -window.dnoa_internal.timedOut = new Object(); - - trace = function(msg) { if (window.openid_trace) { if (!window.openid_tracediv) { @@ -31,181 +21,185 @@ trace = function(msg) { } }; -/// <summary>Initiates asynchronous discovery on an identifier.</summary> -/// <param name="identifier">The identifier on which to perform discovery.<param> -/// <param name="discoveryCompletedCallback">The function(identifier, discoveryResult) to invoke when discovery is completed.</param> -window.dnoa_internal.discover = function(identifier, discoveryCompletedCallback) { - trace('starting discovery on ' + identifier); - window.dnoa_internal.callback(identifier, window.dnoa_internal.discoverSuccess, window.dnoa_internal.discoverFailure); - - // Save the discovery completed callback for lookup when discovery is done. - window.dnoa_internal.discoveryCompletedCallbacks[identifier] = discoveryCompletedCallback; -}; - -window.dnoa_internal.discoverSuccess = function(discoveryResult, userSuppliedIdentifier) { - trace('Discovery completed for: ' + userSuppliedIdentifier); +window.dnoa_internal = { + discoveryResults: new Array(), // user supplied identifiers and discovery results + // The possible authentication results + authSuccess: new Object(), + authRefused: new Object(), + timedOut: new Object(), - // Deserialize the JSON object and store the result if it was a successful discovery. - discoveryResult = eval('(' + discoveryResult + ')'); + /// <summary>Initiates asynchronous discovery on an identifier.</summary> + /// <param name="identifier">The identifier on which to perform discovery.<param> + /// <param name="discoveryCompletedCallback">The function(identifier, discoveryResult) to invoke when discovery is completed.</param> + discover: function(identifier, discoveryCompletedCallback) { + successCallback = function(discoveryResult, userSuppliedIdentifier) { + trace('Discovery completed for: ' + userSuppliedIdentifier); - // Add behavior for later use. - window.dnoa_internal.discoveryResults[userSuppliedIdentifier] = discoveryResult = new window.dnoa_internal.DiscoveryResult(discoveryResult, userSuppliedIdentifier); + // Deserialize the JSON object and store the result if it was a successful discovery. + discoveryResult = eval('(' + discoveryResult + ')'); - var callback = window.dnoa_internal.discoveryCompletedCallbacks[userSuppliedIdentifier]; - if (callback) { - trace('Calling back discovery completed handler.'); - callback(userSuppliedIdentifier, discoveryResult); - window.dnoa_internal.discoveryCompletedCallbacks[userSuppliedIdentifier] = null; - } else { - trace('No handler registered to receive discovery completed event.'); - } -}; + // Add behavior for later use. + window.dnoa_internal.discoveryResults[userSuppliedIdentifier] = discoveryResult = new window.dnoa_internal.DiscoveryResult(discoveryResult, userSuppliedIdentifier); -window.dnoa_internal.discoverFailure = function(message, userSuppliedIdentifier) { - trace('Discovery failed for: ' + identifier); - - var callback = window.dnoa_internal.discoveryCompletedCallbacks[userSuppliedIdentifier]; - if (callback) { - callback(userSuppliedIdentifier); - window.dnoa_internal.discoveryCompletedCallbacks[userSuppliedIdentifier] = null; - } -}; - -window.dnoa_internal.trySetup = function(userSuppliedIdentifier) { - window.dnoa_internal.discover(userSuppliedIdentifier, function(identifier, result) { - trace('discovery completed... now proceeding to trySetup.'); - result[0].trySetup(); - }); -}; + if (discoveryCompletedCallback) { + discoveryCompletedCallback(userSuppliedIdentifier, discoveryResult); + } + }; -window.dnoa_internal.DiscoveryResult = function(discoveryInfo, userSuppliedIdentifier) { - this.userSuppliedIdentifier = userSuppliedIdentifier; - // The claimed identifier may be null if the user provided an OP Identifier. - this.claimedIdentifier = discoveryInfo.claimedIdentifier; - trace('Discovered claimed identifier: ' + (this.claimedIdentifier ? this.claimedIdentifier : "(directed identity)")); + failureCallback = function(message, userSuppliedIdentifier) { + trace('Discovery failed for: ' + identifier); - this.length = discoveryInfo.requests.length; - for (var i = 0; i < discoveryInfo.requests.length; i++) { - this[i] = new window.dnoa_internal.TrackingRequest(discoveryInfo.requests[i], userSuppliedIdentifier); - } -}; - -window.dnoa_internal.TrackingRequest = function(requestInfo, userSuppliedIdentifier) { - this.immediate = requestInfo.immediate ? new Uri(requestInfo.immediate) : null; - this.setup = requestInfo.setup ? new Uri(requestInfo.setup) : null; - this.endpoint = new Uri(requestInfo.endpoint); - this.userSuppliedIdentifier = userSuppliedIdentifier; - var self = this; // closure so that delegates have the right instance - this.trySetup = function(callback) { - //self.abort(); // ensure no concurrent attempts - window.dnoa_internal.authenticationCompleted = callback; - var width = 800; - var height = 600; - if (self.setup.getQueryArgValue("openid.return_to").indexOf("dotnetopenid.popupUISupported") >= 0) { - width = 450; - height = 500; + if (discoveryCompletedCallback) { + discoveryCompletedCallback(userSuppliedIdentifier); + } + }; + + trace('starting discovery on ' + identifier); + window.dnoa_internal.callback(identifier, successCallback, failureCallback); + }, + + /// <summary>Instantiates an object that stores discovery results of some identifier.</summary> + DiscoveryResult: function(discoveryInfo, userSuppliedIdentifier) { + this.userSuppliedIdentifier = userSuppliedIdentifier; + // The claimed identifier may be null if the user provided an OP Identifier. + this.claimedIdentifier = discoveryInfo.claimedIdentifier; + trace('Discovered claimed identifier: ' + (this.claimedIdentifier ? this.claimedIdentifier : "(directed identity)")); + + this.length = discoveryInfo.requests.length; + for (var i = 0; i < discoveryInfo.requests.length; i++) { + this[i] = new window.dnoa_internal.TrackingRequest(discoveryInfo.requests[i], userSuppliedIdentifier); } - - var left = (screen.width - width) / 2; - var top = (screen.height - height) / 2; - self.popup = window.open(self.setup, 'opLogin', 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,left=' + left + ',top=' + top + ',width=' + width + ',height=' + height); - - // If the OP supports the UI extension it MAY close its own window - // for a negative assertion. We must be able to recover from that scenario. - var localSelf = self; - self.popupCloseChecker = window.setInterval(function() { - if (localSelf.popup && localSelf.popup.closed) { - // So the user canceled and the window closed. - // It turns out we hae nothing special to do. - // If we were graying out the entire page while the child window was up, - // we would probably revert that here. - trace('User or OP canceled by closing the window.'); - window.clearInterval(localSelf.popupCloseChecker); - localSelf.popup = null; + }, + + /// <summary>Instantiates an object that facilitates initiating and tracking an authentication request.</summary> + TrackingRequest: function(requestInfo, userSuppliedIdentifier) { + this.immediate = requestInfo.immediate ? new window.dnoa_internal.Uri(requestInfo.immediate) : null; + this.setup = requestInfo.setup ? new window.dnoa_internal.Uri(requestInfo.setup) : null; + this.endpoint = new window.dnoa_internal.Uri(requestInfo.endpoint); + this.userSuppliedIdentifier = userSuppliedIdentifier; + var self = this; // closure so that delegates have the right instance + this.trySetup = function(callback) { + //self.abort(); // ensure no concurrent attempts + window.dnoa_internal.authenticationCompleted = callback; + var width = 800; + var height = 600; + if (self.setup.getQueryArgValue("openid.return_to").indexOf("dotnetopenid.popupUISupported") >= 0) { + width = 450; + height = 500; } - }, 250); - }; -}; -/*************************************** - * Uri class - ***************************************/ + var left = (screen.width - width) / 2; + var top = (screen.height - height) / 2; + self.popup = window.open(self.setup, 'opLogin', 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,left=' + left + ',top=' + top + ',width=' + width + ',height=' + height); + + // If the OP supports the UI extension it MAY close its own window + // for a negative assertion. We must be able to recover from that scenario. + var localSelf = self; + self.popupCloseChecker = window.setInterval(function() { + if (localSelf.popup && localSelf.popup.closed) { + // So the user canceled and the window closed. + // It turns out we hae nothing special to do. + // If we were graying out the entire page while the child window was up, + // we would probably revert that here. + trace('User or OP canceled by closing the window.'); + window.clearInterval(localSelf.popupCloseChecker); + localSelf.popup = null; + } + }, 250); + }; + }, + + /// <summary>Performs discovery and immediately begins checkid_setup to authenticate the user using a given identifier.</summary> + trySetup: function(userSuppliedIdentifier) { + onDiscoveryCompleted = function(identifier, result) { + if (result) { + trace('discovery completed... now proceeding to trySetup.'); + result[0].trySetup(); + } else { + trace('discovery completed with no results.'); + } + }; -function Uri(url) { - this.originalUri = url; + window.dnoa_internal.discover(userSuppliedIdentifier, onDiscoveryCompleted); + }, - this.toString = function() { - return this.originalUri; - }; + /// <summary>Instantiates an object that provides string manipulation services for URIs.</summary> + Uri: function(url) { + this.originalUri = url; - this.getAuthority = function() { - var authority = this.getScheme() + "://" + this.getHost(); - return authority; - } - - this.getHost = function() { - var hostStartIdx = this.originalUri.indexOf("://") + 3; - var hostEndIndex = this.originalUri.indexOf("/", hostStartIdx); - if (hostEndIndex < 0) hostEndIndex = this.originalUri.length; - var host = this.originalUri.substr(hostStartIdx, hostEndIndex - hostStartIdx); - return host; - } + this.toString = function() { + return this.originalUri; + }; - this.getScheme = function() { - var schemeStartIdx = this.indexOf("://"); - return this.originalUri.substr(this.originalUri, schemeStartIdx); - } + this.getAuthority = function() { + var authority = this.getScheme() + "://" + this.getHost(); + return authority; + } - this.trimFragment = function() { - var hashmark = this.originalUri.indexOf('#'); - if (hashmark >= 0) { - return new Uri(this.originalUri.substr(0, hashmark)); + this.getHost = function() { + var hostStartIdx = this.originalUri.indexOf("://") + 3; + var hostEndIndex = this.originalUri.indexOf("/", hostStartIdx); + if (hostEndIndex < 0) hostEndIndex = this.originalUri.length; + var host = this.originalUri.substr(hostStartIdx, hostEndIndex - hostStartIdx); + return host; } - return this; - }; - - this.appendQueryVariable = function(name, value) { - var pair = encodeURI(name) + "=" + encodeURI(value); - if (this.originalUri.indexOf('?') >= 0) { - this.originalUri = this.originalUri + "&" + pair; - } else { - this.originalUri = this.originalUri + "?" + pair; + + this.getScheme = function() { + var schemeStartIdx = this.indexOf("://"); + return this.originalUri.substr(this.originalUri, schemeStartIdx); } - }; - function KeyValuePair(key, value) { - this.key = key; - this.value = value; - }; + this.trimFragment = function() { + var hashmark = this.originalUri.indexOf('#'); + if (hashmark >= 0) { + return new window.dnoa_internal.Uri(this.originalUri.substr(0, hashmark)); + } + return this; + }; + + this.appendQueryVariable = function(name, value) { + var pair = encodeURI(name) + "=" + encodeURI(value); + if (this.originalUri.indexOf('?') >= 0) { + this.originalUri = this.originalUri + "&" + pair; + } else { + this.originalUri = this.originalUri + "?" + pair; + } + }; - this.Pairs = new Array(); + function KeyValuePair(key, value) { + this.key = key; + this.value = value; + }; - var queryBeginsAt = this.originalUri.indexOf('?'); - if (queryBeginsAt >= 0) { - this.queryString = url.substr(queryBeginsAt + 1); - var queryStringPairs = this.queryString.split('&'); + this.Pairs = new Array(); - for (var i = 0; i < queryStringPairs.length; i++) { - var pair = queryStringPairs[i].split('='); - this.Pairs.push(new KeyValuePair(unescape(pair[0]), unescape(pair[1]))) - } - }; + var queryBeginsAt = this.originalUri.indexOf('?'); + if (queryBeginsAt >= 0) { + this.queryString = url.substr(queryBeginsAt + 1); + var queryStringPairs = this.queryString.split('&'); - this.getQueryArgValue = function(key) { - for (var i = 0; i < this.Pairs.length; i++) { - if (this.Pairs[i].key == key) { - return this.Pairs[i].value; + for (var i = 0; i < queryStringPairs.length; i++) { + var pair = queryStringPairs[i].split('='); + this.Pairs.push(new KeyValuePair(unescape(pair[0]), unescape(pair[1]))) } - } - }; + }; - this.containsQueryArg = function(key) { - return this.getQueryArgValue(key); - }; + this.getQueryArgValue = function(key) { + for (var i = 0; i < this.Pairs.length; i++) { + if (this.Pairs[i].key == key) { + return this.Pairs[i].value; + } + } + }; - this.indexOf = function(args) { - return this.originalUri.indexOf(args); - }; + this.containsQueryArg = function(key) { + return this.getQueryArgValue(key); + }; - return this; -}; + this.indexOf = function(args) { + return this.originalUri.indexOf(args); + }; + + return this; + } +};
\ No newline at end of file |