summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchRequest.cs
blob: a69f8d9506791554403c80b052bac8e9130faef5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
//-----------------------------------------------------------------------
// <copyright file="FetchRequest.cs" company="Outercurve Foundation">
//     Copyright (c) Outercurve Foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

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;

	/// <summary>
	/// The Attribute Exchange Fetch message, request leg.
	/// </summary>
	[Serializable]
	public sealed class FetchRequest : ExtensionBase, IMessageWithEvents {
		/// <summary>
		/// The factory method that may be used in deserialization of this message.
		/// </summary>
		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;
		};

		/// <summary>
		/// Characters that may not appear in an attribute alias list.
		/// </summary>
		internal static readonly char[] IllegalAliasListCharacters = new[] { '.', '\n' };

		/// <summary>
		/// Characters that may not appear in an attribute Type URI alias.
		/// </summary>
		internal static readonly char[] IllegalAliasCharacters = new[] { '.', ',', ':' };

		/// <summary>
		/// The value for the 'mode' parameter.
		/// </summary>
		[MessagePart("mode", IsRequired = true)]
		private const string Mode = "fetch_request";

		/// <summary>
		/// The collection of requested attributes.
		/// </summary>
		private readonly KeyedCollection<string, AttributeRequest> attributes = new KeyedCollectionDelegate<string, AttributeRequest>(ar => ar.TypeUri);

		/// <summary>
		/// Initializes a new instance of the <see cref="FetchRequest"/> class.
		/// </summary>
		public FetchRequest()
			: base(new Version(1, 0), Constants.TypeUri, null) {
		}

		/// <summary>
		/// Gets a collection of the attributes whose values are 
		/// requested by the Relying Party.
		/// </summary>
		/// <value>A collection where the keys are the attribute type URIs, and the value
		/// is all the attribute request details.</value>
		public KeyedCollection<string, AttributeRequest> Attributes {
			get {
				return this.attributes;
			}
		}

		/// <summary>
		/// 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.
		/// </summary>
		[MessagePart("update_url", IsRequired = false)]
		public Uri UpdateUrl { get; set; }

		/// <summary>
		/// Gets or sets a list of aliases for optional attributes.
		/// </summary>
		/// <value>A comma-delimited list of aliases.</value>
		[MessagePart("if_available", IsRequired = false)]
		private string OptionalAliases { get; set; }

		/// <summary>
		/// Gets or sets a list of aliases for required attributes.
		/// </summary>
		/// <value>A comma-delimited list of aliases.</value>
		[MessagePart("required", IsRequired = false)]
		private string RequiredAliases { get; set; }

		/// <summary>
		/// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
		/// </summary>
		/// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
		/// <returns>
		/// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
		/// </returns>
		/// <exception cref="T:System.NullReferenceException">
		/// The <paramref name="obj"/> parameter is null.
		/// </exception>
		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;
		}

		/// <summary>
		/// Serves as a hash function for a particular type.
		/// </summary>
		/// <returns>
		/// A hash code for the current <see cref="T:System.Object"/>.
		/// </returns>
		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

		/// <summary>
		/// Called when the message is about to be transmitted,
		/// before it passes through the channel binding elements.
		/// </summary>
		void IMessageWithEvents.OnSending() {
			var fields = ((IMessage)this).ExtraData;
			fields.Clear();

			List<string> requiredAliases = new List<string>(), optionalAliases = new List<string>();
			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;
		}

		/// <summary>
		/// Called when the message has been received,
		/// after it passes through the channel binding elements.
		/// </summary>
		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<string>(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

		/// <summary>
		/// Checks the message state for conformity to the protocol specification
		/// and throws an exception if the message is invalid.
		/// </summary>
		/// <remarks>
		/// 	<para>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.</para>
		/// 	<para>Note that this property should <i>not</i> check signatures or perform any state checks
		/// outside this scope of this particular message.</para>
		/// </remarks>
		/// <exception cref="ProtocolException">Thrown if the message is invalid.</exception>
		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;
				}
			}
		}

		/// <summary>
		/// Splits a list of aliases by their commas.
		/// </summary>
		/// <param name="aliasList">The comma-delimited list of aliases.  May be null or empty.</param>
		/// <returns>The list of aliases.  Never null, but may be empty.</returns>
		private static IList<string> ParseAliasList(string aliasList) {
			if (string.IsNullOrEmpty(aliasList)) {
				return EmptyList<string>.Instance;
			}

			return aliasList.Split(',');
		}
	}
}