//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange {
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using DotNetOpenAuth.Logging;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.Messages;
///
/// The Attribute Exchange Fetch message, request leg.
///
[Serializable]
public sealed class FetchRequest : ExtensionBase, IMessageWithEvents {
///
/// The factory method that may be used in deserialization of this message.
///
internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
if (typeUri == Constants.TypeUri && isProviderRole) {
string mode;
if (data.TryGetValue("mode", out mode) && mode == Mode) {
return new FetchRequest();
}
}
return null;
};
///
/// Characters that may not appear in an attribute alias list.
///
internal static readonly char[] IllegalAliasListCharacters = new[] { '.', '\n' };
///
/// Characters that may not appear in an attribute Type URI alias.
///
internal static readonly char[] IllegalAliasCharacters = new[] { '.', ',', ':' };
///
/// The value for the 'mode' parameter.
///
[MessagePart("mode", IsRequired = true)]
private const string Mode = "fetch_request";
///
/// The collection of requested attributes.
///
private readonly KeyedCollection attributes = new KeyedCollectionDelegate(ar => ar.TypeUri);
///
/// Initializes a new instance of the class.
///
public FetchRequest()
: base(new Version(1, 0), Constants.TypeUri, null) {
}
///
/// Gets a collection of the attributes whose values are
/// requested by the Relying Party.
///
/// A collection where the keys are the attribute type URIs, and the value
/// is all the attribute request details.
public KeyedCollection Attributes {
get {
return this.attributes;
}
}
///
/// Gets or sets the URL that the OpenID Provider may re-post the fetch response
/// message to at some time after the initial response has been sent, using an
/// OpenID Authentication Positive Assertion to inform the relying party of updates
/// to the requested fields.
///
[MessagePart("update_url", IsRequired = false)]
public Uri UpdateUrl { get; set; }
///
/// Gets or sets a list of aliases for optional attributes.
///
/// A comma-delimited list of aliases.
[MessagePart("if_available", IsRequired = false)]
private string OptionalAliases { get; set; }
///
/// Gets or sets a list of aliases for required attributes.
///
/// A comma-delimited list of aliases.
[MessagePart("required", IsRequired = false)]
private string RequiredAliases { get; set; }
///
/// Determines whether the specified is equal to the current .
///
/// The to compare with the current .
///
/// true if the specified is equal to the current ; otherwise, false.
///
///
/// The parameter is null.
///
public override bool Equals(object obj) {
FetchRequest other = obj as FetchRequest;
if (other == null) {
return false;
}
if (this.Version != other.Version) {
return false;
}
if (this.UpdateUrl != other.UpdateUrl) {
return false;
}
if (!MessagingUtilities.AreEquivalentUnordered(this.Attributes.ToList(), other.Attributes.ToList())) {
return false;
}
return true;
}
///
/// Serves as a hash function for a particular type.
///
///
/// A hash code for the current .
///
public override int GetHashCode() {
unchecked {
int hashCode = this.Version.GetHashCode();
if (this.UpdateUrl != null) {
hashCode += this.UpdateUrl.GetHashCode();
}
foreach (AttributeRequest att in this.Attributes) {
hashCode += att.GetHashCode();
}
return hashCode;
}
}
#region IMessageWithEvents Members
///
/// Called when the message is about to be transmitted,
/// before it passes through the channel binding elements.
///
void IMessageWithEvents.OnSending() {
var fields = ((IMessage)this).ExtraData;
fields.Clear();
List requiredAliases = new List(), optionalAliases = new List();
AliasManager aliasManager = new AliasManager();
foreach (var att in this.attributes) {
string alias = aliasManager.GetAlias(att.TypeUri);
// define the alias<->typeUri mapping
fields.Add("type." + alias, att.TypeUri);
// set how many values the relying party wants max
fields.Add("count." + alias, att.Count.ToString(CultureInfo.InvariantCulture));
if (att.IsRequired) {
requiredAliases.Add(alias);
} else {
optionalAliases.Add(alias);
}
}
// Set optional/required lists
this.OptionalAliases = optionalAliases.Count > 0 ? string.Join(",", optionalAliases.ToArray()) : null;
this.RequiredAliases = requiredAliases.Count > 0 ? string.Join(",", requiredAliases.ToArray()) : null;
}
///
/// Called when the message has been received,
/// after it passes through the channel binding elements.
///
void IMessageWithEvents.OnReceiving() {
var extraData = ((IMessage)this).ExtraData;
var requiredAliases = ParseAliasList(this.RequiredAliases);
var optionalAliases = ParseAliasList(this.OptionalAliases);
// if an alias shows up in both lists, an exception will result implicitly.
var allAliases = new List(requiredAliases.Count + optionalAliases.Count);
allAliases.AddRange(requiredAliases);
allAliases.AddRange(optionalAliases);
if (allAliases.Count == 0) {
Logger.OpenId.Error("Attribute Exchange extension did not provide any aliases in the if_available or required lists.");
return;
}
AliasManager aliasManager = new AliasManager();
foreach (var alias in allAliases) {
string attributeTypeUri;
if (extraData.TryGetValue("type." + alias, out attributeTypeUri)) {
aliasManager.SetAlias(alias, attributeTypeUri);
AttributeRequest att = new AttributeRequest {
TypeUri = attributeTypeUri,
IsRequired = requiredAliases.Contains(alias),
};
string countString;
if (extraData.TryGetValue("count." + alias, out countString)) {
if (countString == "unlimited") {
att.Count = int.MaxValue;
} else {
int count;
if (int.TryParse(countString, out count) && count > 0) {
att.Count = count;
} else {
Logger.OpenId.Error("count." + alias + " could not be parsed into a positive integer.");
}
}
} else {
att.Count = 1;
}
this.Attributes.Add(att);
} else {
Logger.OpenId.Error("Type URI definition of alias " + alias + " is missing.");
}
}
}
#endregion
///
/// Checks the message state for conformity to the protocol specification
/// and throws an exception if the message is invalid.
///
///
/// Some messages have required fields, or combinations of fields that must relate to each other
/// in specialized ways. After deserializing a message, this method checks the state of the
/// message to see if it conforms to the protocol.
/// Note that this property should not check signatures or perform any state checks
/// outside this scope of this particular message.
///
/// Thrown if the message is invalid.
protected override void EnsureValidMessage() {
base.EnsureValidMessage();
if (this.UpdateUrl != null && !this.UpdateUrl.IsAbsoluteUri) {
this.UpdateUrl = null;
Logger.OpenId.ErrorFormat("The AX fetch request update_url parameter was not absolute ('{0}'). Ignoring value.", this.UpdateUrl);
}
if (this.OptionalAliases != null) {
if (this.OptionalAliases.IndexOfAny(IllegalAliasListCharacters) >= 0) {
Logger.OpenId.Error("Illegal characters found in Attribute Exchange if_available alias list. Ignoring value.");
this.OptionalAliases = null;
}
}
if (this.RequiredAliases != null) {
if (this.RequiredAliases.IndexOfAny(IllegalAliasListCharacters) >= 0) {
Logger.OpenId.Error("Illegal characters found in Attribute Exchange required alias list. Ignoring value.");
this.RequiredAliases = null;
}
}
}
///
/// Splits a list of aliases by their commas.
///
/// The comma-delimited list of aliases. May be null or empty.
/// The list of aliases. Never null, but may be empty.
private static IList ParseAliasList(string aliasList) {
if (string.IsNullOrEmpty(aliasList)) {
return EmptyList.Instance;
}
return aliasList.Split(',');
}
}
}