summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew Arnott <andrewarnott@gmail.com>2009-06-12 22:36:06 -0700
committerAndrew Arnott <andrewarnott@gmail.com>2009-06-12 22:36:06 -0700
commitfc714cae7f69d24af4ab12bca1dcbb351708e413 (patch)
tree5e8fe95847b7ab69794a6ebf3382920c60af21ce
parent7111f5c52f07e191424f0e4b0ade85e0db11c116 (diff)
downloadDotNetOpenAuth-fc714cae7f69d24af4ab12bca1dcbb351708e413.zip
DotNetOpenAuth-fc714cae7f69d24af4ab12bca1dcbb351708e413.tar.gz
DotNetOpenAuth-fc714cae7f69d24af4ab12bca1dcbb351708e413.tar.bz2
Progress on moving OpenIdAjaxTextBox-like behavior into the common base class.
-rw-r--r--src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj1
-rw-r--r--src/DotNetOpenAuth.Test/Messaging/EnumerableCacheTests.cs129
-rw-r--r--src/DotNetOpenAuth/DotNetOpenAuth.csproj4
-rw-r--r--src/DotNetOpenAuth/Messaging/EnumerableCache.cs244
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.ReturnTo.html11
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs227
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js320
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&lt;T&gt;"/> 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&lt;T&gt;"/> types and returns a caching <see cref="IEnumerator&lt;T&gt;"/>
+ /// from its <see cref="IEnumerable&lt;T&gt;.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&lt;T&gt;"/> 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&lt;T&gt;"/> 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&lt;T&gt;.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