//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Mail;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Serialization;
using DotNetOpenAuth.Logging;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.Messages;
using Validation;
///
/// A struct storing Simple Registration field values describing an
/// authenticating user.
///
[Serializable]
public sealed class ClaimsResponse : ExtensionBase, IClientScriptExtensionResponse, 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.TypeUris.Standard || Array.IndexOf(Constants.AdditionalTypeUris, typeUri) >= 0) && !isProviderRole) {
return new ClaimsResponse(typeUri);
}
return null;
};
///
/// The allowed format for birthdates.
///
private static readonly Regex birthDateValidator = new Regex(@"^\d\d\d\d-\d\d-\d\d$");
///
/// Storage for the raw string birthdate value.
///
private string birthDateRaw;
///
/// Backing field for the property.
///
private DateTime? birthDate;
///
/// Backing field for the property.
///
private CultureInfo culture;
///
/// Initializes a new instance of the class
/// using the most common, and spec prescribed type URI.
///
public ClaimsResponse()
: this(Constants.TypeUris.Standard) {
}
///
/// Initializes a new instance of the class.
///
///
/// The type URI that must be used to identify this extension in the response message.
/// This value should be the same one the relying party used to send the extension request.
/// Commonly used type URIs supported by relying parties are defined in the
/// class.
///
public ClaimsResponse(string typeUriToUse = Constants.TypeUris.Standard)
: base(new Version(1, 0), typeUriToUse, Constants.AdditionalTypeUris) {
Requires.NotNullOrEmpty(typeUriToUse, "typeUriToUse");
}
///
/// Gets or sets the nickname the user goes by.
///
[MessagePart(Constants.nickname)]
public string Nickname { get; set; }
///
/// Gets or sets the user's email address.
///
[MessagePart(Constants.email)]
public string Email { get; set; }
///
/// Gets or sets the full name of a user as a single string.
///
[MessagePart(Constants.fullname)]
public string FullName { get; set; }
///
/// Gets or sets the user's birthdate.
///
public DateTime? BirthDate {
get {
return this.birthDate;
}
set {
this.birthDate = value;
// Don't use property accessor for peer property to avoid infinite loop between the two proeprty accessors.
if (value.HasValue) {
this.birthDateRaw = value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
} else {
this.birthDateRaw = null;
}
}
}
///
/// Gets or sets the raw birth date string given by the extension.
///
/// A string in the format yyyy-MM-dd.
[MessagePart(Constants.dob)]
public string BirthDateRaw {
get {
return this.birthDateRaw;
}
set {
ErrorUtilities.VerifyArgument(value == null || birthDateValidator.IsMatch(value), OpenIdStrings.SregInvalidBirthdate);
if (value != null) {
// Update the BirthDate property, if possible.
// Don't use property accessor for peer property to avoid infinite loop between the two proeprty accessors.
// Some valid sreg dob values like "2000-00-00" will not work as a DateTime struct,
// in which case we null it out, but don't show any error.
DateTime newBirthDate;
if (DateTime.TryParse(value, out newBirthDate)) {
this.birthDate = newBirthDate;
} else {
Logger.OpenId.WarnFormat("Simple Registration birthdate '{0}' could not be parsed into a DateTime and may not include month and/or day information. Setting BirthDate property to null.", value);
this.birthDate = null;
}
} else {
this.birthDate = null;
}
this.birthDateRaw = value;
}
}
///
/// Gets or sets the gender of the user.
///
[MessagePart(Constants.gender, Encoder = typeof(GenderEncoder))]
public Gender? Gender { get; set; }
///
/// Gets or sets the zip code / postal code of the user.
///
[MessagePart(Constants.postcode)]
public string PostalCode { get; set; }
///
/// Gets or sets the country of the user.
///
[MessagePart(Constants.country)]
public string Country { get; set; }
///
/// Gets or sets the primary/preferred language of the user.
///
[MessagePart(Constants.language)]
public string Language { get; set; }
///
/// Gets or sets the user's timezone.
///
[MessagePart(Constants.timezone)]
public string TimeZone { get; set; }
///
/// Gets a combination of the user's full name and email address.
///
public MailAddress MailAddress {
get {
if (string.IsNullOrEmpty(this.Email)) {
return null;
} else if (string.IsNullOrEmpty(this.FullName)) {
return new MailAddress(this.Email);
} else {
return new MailAddress(this.Email, this.FullName);
}
}
}
///
/// Gets or sets a combination of the language and country of the user.
///
[XmlIgnore]
public CultureInfo Culture {
get {
if (this.culture == null && !string.IsNullOrEmpty(this.Language)) {
string cultureString = string.Empty;
cultureString = this.Language;
if (!string.IsNullOrEmpty(this.Country)) {
cultureString += "-" + this.Country;
}
// language-country may not always form a recongized valid culture.
// For instance, a Google OpenID Provider can return a random combination
// of language and country based on user settings.
try {
this.culture = CultureInfo.GetCultureInfo(cultureString);
} catch (ArgumentException) { // CultureNotFoundException derives from this, and .NET 3.5 throws the base type
// Fallback to just reporting a culture based on language.
this.culture = CultureInfo.GetCultureInfo(this.Language);
}
}
return this.culture;
}
set {
this.culture = value;
this.Language = (value != null) ? value.TwoLetterISOLanguageName : null;
int indexOfHyphen = (value != null) ? value.Name.IndexOf('-') : -1;
this.Country = indexOfHyphen > 0 ? value.Name.Substring(indexOfHyphen + 1) : null;
}
}
///
/// Gets a value indicating whether this extension is signed by the Provider.
///
///
/// true if this instance is signed by the Provider; otherwise, false.
///
public bool IsSignedByProvider {
get { return this.IsSignedByRemoteParty; }
}
///
/// Tests equality of two objects.
///
/// One instance to compare.
/// Another instance to compare.
/// The result of the operator.
public static bool operator ==(ClaimsResponse one, ClaimsResponse other) {
return one.EqualsNullSafe(other);
}
///
/// Tests inequality of two objects.
///
/// One instance to compare.
/// Another instance to compare.
/// The result of the operator.
public static bool operator !=(ClaimsResponse one, ClaimsResponse other) {
return !(one == other);
}
///
/// Tests equality of two objects.
///
/// 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) {
ClaimsResponse other = obj as ClaimsResponse;
if (other == null) {
return false;
}
return
this.BirthDateRaw.EqualsNullSafe(other.BirthDateRaw) &&
this.Country.EqualsNullSafe(other.Country) &&
this.Language.EqualsNullSafe(other.Language) &&
this.Email.EqualsNullSafe(other.Email) &&
this.FullName.EqualsNullSafe(other.FullName) &&
this.Gender.Equals(other.Gender) &&
this.Nickname.EqualsNullSafe(other.Nickname) &&
this.PostalCode.EqualsNullSafe(other.PostalCode) &&
this.TimeZone.EqualsNullSafe(other.TimeZone);
}
///
/// Serves as a hash function for a particular type.
///
///
/// A hash code for the current .
///
public override int GetHashCode() {
return (this.Nickname != null) ? this.Nickname.GetHashCode() : base.GetHashCode();
}
#region IClientScriptExtension Members
///
/// Reads the extension information on an authentication response from the provider.
///
/// The incoming OpenID response carrying the extension.
///
/// A Javascript snippet that when executed on the user agent returns an object with
/// the information deserialized from the extension response.
///
///
/// This method is called before the signature on the assertion response has been
/// verified. Therefore all information in these fields should be assumed unreliable
/// and potentially falsified.
///
string IClientScriptExtensionResponse.InitializeJavaScriptData(IProtocolMessageWithExtensions response) {
var sreg = new Dictionary(15);
// Although we could probably whip up a trip with MessageDictionary
// to avoid explicitly setting each field, doing so would likely
// open ourselves up to security exploits from the OP as it would
// make possible sending arbitrary javascript in arbitrary field names.
sreg[Constants.nickname] = this.Nickname;
sreg[Constants.email] = this.Email;
sreg[Constants.fullname] = this.FullName;
sreg[Constants.dob] = this.BirthDateRaw;
sreg[Constants.gender] = this.Gender.HasValue ? this.Gender.Value.ToString() : null;
sreg[Constants.postcode] = this.PostalCode;
sreg[Constants.country] = this.Country;
sreg[Constants.language] = this.Language;
sreg[Constants.timezone] = this.TimeZone;
return MessagingUtilities.CreateJsonObject(sreg, false);
}
#endregion
#region IMessageWithEvents Members
///
/// Called when the message is about to be transmitted,
/// before it passes through the channel binding elements.
///
void IMessageWithEvents.OnSending() {
// Null out empty values so we don't send out a lot of empty parameters.
this.Country = EmptyToNull(this.Country);
this.Email = EmptyToNull(this.Email);
this.FullName = EmptyToNull(this.FullName);
this.Language = EmptyToNull(this.Language);
this.Nickname = EmptyToNull(this.Nickname);
this.PostalCode = EmptyToNull(this.PostalCode);
this.TimeZone = EmptyToNull(this.TimeZone);
}
///
/// Called when the message has been received,
/// after it passes through the channel binding elements.
///
void IMessageWithEvents.OnReceiving() {
}
#endregion
///
/// Translates an empty string value to null, or passes through non-empty values.
///
/// The value to consider changing to null.
/// Either null or a non-empty string.
private static string EmptyToNull(string value) {
return string.IsNullOrEmpty(value) ? null : value;
}
}
}