//
// System.Web.Security.SqlMembershipProvider
//
// Authors:
//	Ben Maurer (bmaurer@users.sourceforge.net)
//	Lluis Sanchez Gual (lluis@novell.com)
//	Chris Toshok (toshok@ximian.com)
//
// (C) 2003 Ben Maurer
// Copyright (c) 2005,2006 Novell, Inc (http://www.novell.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

#if NET_2_0
using System.Collections;
using System.Collections.Specialized;
using System.Configuration;
using System.Configuration.Provider;
using System.Data;
using System.Data.Common;
using System.Text;
using System.Web.Configuration;
using System.Security.Cryptography;

namespace System.Web.Security {
	public class SqlMembershipProvider : MembershipProvider
	{
		bool enablePasswordReset;
		bool enablePasswordRetrieval;
		int maxInvalidPasswordAttempts;
		MembershipPasswordFormat passwordFormat;
		bool requiresQuestionAndAnswer;
		bool requiresUniqueEmail;
		int minRequiredNonAlphanumericCharacters;
		int minRequiredPasswordLength;
		int passwordAttemptWindow;
		string passwordStrengthRegularExpression;
		TimeSpan userIsOnlineTimeWindow;

		ConnectionStringSettings connectionString;
		DbProviderFactory factory;

		string applicationName;
		bool schemaIsOk = false;

		DbConnection CreateConnection ()
		{
			if (!schemaIsOk && !(schemaIsOk = AspNetDBSchemaChecker.CheckMembershipSchemaVersion (factory, connectionString.ConnectionString, "membership", "1")))
				throw new ProviderException ("Incorrect ASP.NET DB Schema Version.");

			DbConnection connection;

			if (connectionString == null)
				throw new ProviderException ("Connection string for the SQL Membership Provider has not been provided.");
			
			try {
				connection = factory.CreateConnection ();
				connection.ConnectionString = connectionString.ConnectionString;
				connection.Open ();
			} catch (Exception ex) {
				throw new ProviderException ("Unable to open SQL connection for the SQL Membership Provider.",
							     ex);
			}
			
			return connection;
		}

		DbParameter AddParameter (DbCommand command, string parameterName, object parameterValue)
		{
			return AddParameter (command, parameterName, ParameterDirection.Input, parameterValue);
		}

		DbParameter AddParameter (DbCommand command, string parameterName, ParameterDirection direction, object parameterValue)
		{
			DbParameter dbp = command.CreateParameter ();
			dbp.ParameterName = parameterName;
			dbp.Value = parameterValue;
			dbp.Direction = direction;
			command.Parameters.Add (dbp);
			return dbp;
		}

		DbParameter AddParameter (DbCommand command, string parameterName, ParameterDirection direction, DbType type, object parameterValue)
		{
			DbParameter dbp = command.CreateParameter ();
			dbp.ParameterName = parameterName;
			dbp.Value = parameterValue;
			dbp.Direction = direction;
			dbp.DbType = type;
			command.Parameters.Add (dbp);
			return dbp;
		}

		static int GetReturnValue (DbParameter returnValue)
		{
			object value = returnValue.Value;
			return value is int ? (int) value : -1;
		}

		void CheckParam (string pName, string p, int length)
		{
			if (p == null)
				throw new ArgumentNullException (pName);
			if (p.Length == 0 || p.Length > length || p.IndexOf (",") != -1)
				throw new ArgumentException (String.Format ("invalid format for {0}", pName));
		}

		public override bool ChangePassword (string username, string oldPwd, string newPwd)
		{
			if (username != null) username = username.Trim ();
			if (oldPwd != null) oldPwd = oldPwd.Trim ();
			if (newPwd != null) newPwd = newPwd.Trim ();

			CheckParam ("username", username, 256);
			CheckParam ("oldPwd", oldPwd, 128);
			CheckParam ("newPwd", newPwd, 128);

			if (!CheckPassword (newPwd))
				throw new ArgumentException (string.Format (
						"New Password invalid. New Password length minimum: {0}. Non-alphanumeric characters required: {1}.",
						MinRequiredPasswordLength,
						MinRequiredNonAlphanumericCharacters));

			using (DbConnection connection = CreateConnection ()) {
				PasswordInfo pi = ValidateUsingPassword (username, oldPwd);

				if (pi != null) {
					EmitValidatingPassword (username, newPwd, false);
					string db_password = EncodePassword (newPwd, pi.PasswordFormat, pi.PasswordSalt);

					DbCommand command = factory.CreateCommand ();
					command.Connection = connection;
					command.CommandText = @"aspnet_Membership_SetPassword";
					command.CommandType = CommandType.StoredProcedure;

					AddParameter (command, "@ApplicationName", ApplicationName);
					AddParameter (command, "@UserName", username);
					AddParameter (command, "@NewPassword", db_password);
					AddParameter (command, "@PasswordFormat", (int) pi.PasswordFormat);
					AddParameter (command, "@PasswordSalt", pi.PasswordSalt);
					AddParameter (command, "@CurrentTimeUtc", DateTime.UtcNow);
					DbParameter returnValue = AddParameter (command, "@ReturnVal", ParameterDirection.ReturnValue, DbType.Int32, null);

					command.ExecuteNonQuery ();

					if (GetReturnValue (returnValue) != 0)
						return false;

					return true;
				}
				return false;
			}
		}

		public override bool ChangePasswordQuestionAndAnswer (string username, string password, string newPwdQuestion, string newPwdAnswer)
		{
			if (username != null) username = username.Trim ();
			if (newPwdQuestion != null) newPwdQuestion = newPwdQuestion.Trim ();
			if (newPwdAnswer != null) newPwdAnswer = newPwdAnswer.Trim ();

			CheckParam ("username", username, 256);
			if (RequiresQuestionAndAnswer)
				CheckParam ("newPwdQuestion", newPwdQuestion, 128);
			if (RequiresQuestionAndAnswer)
				CheckParam ("newPwdAnswer", newPwdAnswer, 128);

			using (DbConnection connection = CreateConnection ()) {
				PasswordInfo pi = ValidateUsingPassword (username, password);

				if (pi != null) {
					string db_passwordAnswer = EncodePassword (newPwdAnswer, pi.PasswordFormat, pi.PasswordSalt);

					DbCommand command = factory.CreateCommand ();
					command.Connection = connection;
					command.CommandType = CommandType.StoredProcedure;
					command.CommandText = @"aspnet_Membership_ChangePasswordQuestionAndAnswer";

					AddParameter (command, "@ApplicationName", ApplicationName);
					AddParameter (command, "@UserName", username);
					AddParameter (command, "@NewPasswordQuestion", newPwdQuestion);
					AddParameter (command, "@NewPasswordAnswer", db_passwordAnswer);
					DbParameter returnValue = AddParameter (command, "@ReturnVal", ParameterDirection.ReturnValue, DbType.Int32, null);

					command.ExecuteNonQuery ();

					if (GetReturnValue (returnValue) != 0)
						return false;

					return true;
				}
				return false;
			}
		}

		public override MembershipUser CreateUser (string username,
							   string password,
							   string email,
							   string pwdQuestion,
							   string pwdAnswer,
							   bool isApproved,
							   object providerUserKey,
							   out MembershipCreateStatus status)
		{
			if (username != null) username = username.Trim ();
			if (password != null) password = password.Trim ();
			if (email != null) email = email.Trim ();
			if (pwdQuestion != null) pwdQuestion = pwdQuestion.Trim ();
			if (pwdAnswer != null) pwdAnswer = pwdAnswer.Trim ();

			/* some initial validation */
			if (username == null || username.Length == 0 || username.Length > 256 || username.IndexOf (",") != -1) {
				status = MembershipCreateStatus.InvalidUserName;
				return null;
			}
			if (password == null || password.Length == 0 || password.Length > 128) {
				status = MembershipCreateStatus.InvalidPassword;
				return null;
			}

			if (!CheckPassword (password)) {
				status = MembershipCreateStatus.InvalidPassword;
				return null;
			}
			EmitValidatingPassword (username, password, true);

			if (RequiresUniqueEmail && (email == null || email.Length == 0)) {
				status = MembershipCreateStatus.InvalidEmail;
				return null;
			}
			if (RequiresQuestionAndAnswer &&
				(pwdQuestion == null ||
				 pwdQuestion.Length == 0 || pwdQuestion.Length > 256)) {
				status = MembershipCreateStatus.InvalidQuestion;
				return null;
			}
			if (RequiresQuestionAndAnswer &&
				(pwdAnswer == null ||
				 pwdAnswer.Length == 0 || pwdAnswer.Length > 128)) {
				status = MembershipCreateStatus.InvalidAnswer;
				return null;
			}
			if (providerUserKey != null && !(providerUserKey is Guid)) {
				status = MembershipCreateStatus.InvalidProviderUserKey;
				return null;
			}

			if (providerUserKey == null)
				providerUserKey = Guid.NewGuid();

			/* encode our password/answer using the
			 * "passwordFormat" configuration option */
			string passwordSalt = "";

			RandomNumberGenerator rng = RandomNumberGenerator.Create ();
			byte [] salt = new byte [SALT_BYTES];
			rng.GetBytes (salt);
			passwordSalt = Convert.ToBase64String (salt);

			password = EncodePassword (password, PasswordFormat, passwordSalt);
			if (RequiresQuestionAndAnswer)
				pwdAnswer = EncodePassword (pwdAnswer, PasswordFormat, passwordSalt);

			/* make sure the hashed/encrypted password and
			 * answer are still under 128 characters. */
			if (password.Length > 128) {
				status = MembershipCreateStatus.InvalidPassword;
				return null;
			}

			if (RequiresQuestionAndAnswer) {
				if (pwdAnswer.Length > 128) {
					status = MembershipCreateStatus.InvalidAnswer;
					return null;
				}
			}
			status = MembershipCreateStatus.Success;

			using (DbConnection connection = CreateConnection ()) {

				try {
					DbCommand command = factory.CreateCommand ();
					command.Connection = connection;
					command.CommandText = @"aspnet_Membership_CreateUser";
					command.CommandType = CommandType.StoredProcedure;

					DateTime Now = DateTime.UtcNow;

					AddParameter (command, "@ApplicationName", ApplicationName);
					AddParameter (command, "@UserName", username);
					AddParameter (command, "@Password", password);
					AddParameter (command, "@PasswordSalt", passwordSalt);
					AddParameter (command, "@Email", email);
					AddParameter (command, "@PasswordQuestion", pwdQuestion);
					AddParameter (command, "@PasswordAnswer", pwdAnswer);
					AddParameter (command, "@IsApproved", isApproved);
					AddParameter (command, "@CurrentTimeUtc", Now);
					AddParameter (command, "@CreateDate", Now);
					AddParameter (command, "@UniqueEmail", RequiresUniqueEmail);
					AddParameter (command, "@PasswordFormat", (int) PasswordFormat);
					AddParameter (command, "@UserId", ParameterDirection.InputOutput, providerUserKey);
					DbParameter returnValue = AddParameter (command, "@ReturnVal", ParameterDirection.ReturnValue, DbType.Int32, null);

					command.ExecuteNonQuery ();

					int st = GetReturnValue (returnValue);

					if (st == 0)
						return GetUser (username, false);
					else if (st == 6)
						status = MembershipCreateStatus.DuplicateUserName;
					else if (st == 7)
						status = MembershipCreateStatus.DuplicateEmail;
					else if (st == 10)
						status = MembershipCreateStatus.DuplicateProviderUserKey;
					else
						status = MembershipCreateStatus.ProviderError;

					return null;
				}
				catch (Exception) {
					status = MembershipCreateStatus.ProviderError;
					return null;
				}
			}
		}

		private bool CheckPassword (string password)
		{
			if (password.Length < MinRequiredPasswordLength)
				return false;

			if (MinRequiredNonAlphanumericCharacters > 0) {
				int nonAlphanumeric = 0;
				for (int i = 0; i < password.Length; i++) {
					if (!Char.IsLetterOrDigit (password [i]))
						nonAlphanumeric++;
				}
				return nonAlphanumeric >= MinRequiredNonAlphanumericCharacters;
			}
			return true;
		}

		public override bool DeleteUser (string username, bool deleteAllRelatedData)
		{
			CheckParam ("username", username, 256);

			DeleteUserTableMask deleteBitmask = DeleteUserTableMask.MembershipUsers;

			if (deleteAllRelatedData)
				deleteBitmask |=
					DeleteUserTableMask.Profiles |
					DeleteUserTableMask.UsersInRoles |
					DeleteUserTableMask.WebPartStateUser;

			using (DbConnection connection = CreateConnection ()) {
				DbCommand command = factory.CreateCommand ();
				command.Connection = connection;
				command.CommandText = @"aspnet_Users_DeleteUser";
				command.CommandType = CommandType.StoredProcedure;

				AddParameter (command, "@ApplicationName", ApplicationName);
				AddParameter (command, "@UserName", username);
				AddParameter (command, "@TablesToDeleteFrom", (int) deleteBitmask);
				AddParameter (command, "@NumTablesDeletedFrom", ParameterDirection.Output, 0);
				DbParameter returnValue = AddParameter (command, "@ReturnVal", ParameterDirection.ReturnValue, DbType.Int32, null);

				command.ExecuteNonQuery ();

				if (((int) command.Parameters ["@NumTablesDeletedFrom"].Value) == 0)
					return false;

				if (GetReturnValue (returnValue) == 0)
					return true;

				return false;
			}
		}

		public virtual string GeneratePassword ()
		{
			return Membership.GeneratePassword (MinRequiredPasswordLength, MinRequiredNonAlphanumericCharacters);
		}

		public override MembershipUserCollection FindUsersByEmail (string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
		{
			CheckParam ("emailToMatch", emailToMatch, 256);

			if (pageIndex < 0)
				throw new ArgumentException ("pageIndex must be >= 0");
			if (pageSize < 0)
				throw new ArgumentException ("pageSize must be >= 0");
			if (pageIndex * pageSize + pageSize - 1 > Int32.MaxValue)
				throw new ArgumentException ("pageIndex and pageSize are too large");

			using (DbConnection connection = CreateConnection ()) {

				DbCommand command = factory.CreateCommand ();
				command.Connection = connection;
				command.CommandText = @"aspnet_Membership_FindUsersByEmail";
				command.CommandType = CommandType.StoredProcedure;

				AddParameter (command, "@PageIndex", pageIndex);
				AddParameter (command, "@PageSize", pageSize);
				AddParameter (command, "@EmailToMatch", emailToMatch);
				AddParameter (command, "@ApplicationName", ApplicationName);
				// return value
				AddParameter (command, "@ReturnValue", ParameterDirection.ReturnValue, null);

				MembershipUserCollection c = BuildMembershipUserCollection (command, pageIndex, pageSize, out totalRecords);

				return c;
			}
		}

		public override MembershipUserCollection FindUsersByName (string nameToMatch, int pageIndex, int pageSize, out int totalRecords)
		{
			CheckParam ("nameToMatch", nameToMatch, 256);

			if (pageIndex < 0)
				throw new ArgumentException ("pageIndex must be >= 0");
			if (pageSize < 0)
				throw new ArgumentException ("pageSize must be >= 0");
			if (pageIndex * pageSize + pageSize - 1 > Int32.MaxValue)
				throw new ArgumentException ("pageIndex and pageSize are too large");

			using (DbConnection connection = CreateConnection ()) {

				DbCommand command = factory.CreateCommand ();
				command.Connection = connection;
				command.CommandText = @"aspnet_Membership_FindUsersByName";
				command.CommandType = CommandType.StoredProcedure;

				AddParameter (command, "@PageIndex", pageIndex);
				AddParameter (command, "@PageSize", pageSize);
				AddParameter (command, "@UserNameToMatch", nameToMatch);
				AddParameter (command, "@ApplicationName", ApplicationName);
				// return value
				AddParameter (command, "@ReturnValue", ParameterDirection.ReturnValue, null);

				MembershipUserCollection c = BuildMembershipUserCollection (command, pageIndex, pageSize, out totalRecords);

				return c;
			}
		}

		public override MembershipUserCollection GetAllUsers (int pageIndex, int pageSize, out int totalRecords)
		{
			if (pageIndex < 0)
				throw new ArgumentException ("pageIndex must be >= 0");
			if (pageSize < 0)
				throw new ArgumentException ("pageSize must be >= 0");
			if (pageIndex * pageSize + pageSize - 1 > Int32.MaxValue)
				throw new ArgumentException ("pageIndex and pageSize are too large");

			using (DbConnection connection = CreateConnection ()) {
				DbCommand command = factory.CreateCommand ();
				command.Connection = connection;
				command.CommandText = @"aspnet_Membership_GetAllUsers";
				command.CommandType = CommandType.StoredProcedure;

				AddParameter (command, "@ApplicationName", ApplicationName);
				AddParameter (command, "@PageIndex", pageIndex);
				AddParameter (command, "@PageSize", pageSize);
				// return value
				AddParameter (command, "@ReturnValue", ParameterDirection.ReturnValue, null);

				MembershipUserCollection c = BuildMembershipUserCollection (command, pageIndex, pageSize, out totalRecords);

				return c;
			}
		}

		MembershipUserCollection BuildMembershipUserCollection (DbCommand command, int pageIndex, int pageSize, out int totalRecords)
		{
			DbDataReader reader = null;
			try {
				MembershipUserCollection users = new MembershipUserCollection ();
				reader = command.ExecuteReader ();
				while (reader.Read ())
					users.Add (GetUserFromReader (reader, null, null));

				totalRecords = Convert.ToInt32 (command.Parameters ["@ReturnValue"].Value);
				return users;
			} catch (Exception) {
				totalRecords = 0;
				return null; /* should we let the exception through? */
			}
			finally {
				if (reader != null)
					reader.Close ();
			}
		}


		public override int GetNumberOfUsersOnline ()
		{
			using (DbConnection connection = CreateConnection ()) {
				DateTime now = DateTime.UtcNow;

				DbCommand command = factory.CreateCommand ();
				command.Connection = connection;
				command.CommandText = @"aspnet_Membership_GetNumberOfUsersOnline";
				command.CommandType = CommandType.StoredProcedure;

				AddParameter (command, "@CurrentTimeUtc", now.ToString ());
				AddParameter (command, "@ApplicationName", ApplicationName);
				AddParameter (command, "@MinutesSinceLastInActive", userIsOnlineTimeWindow.Minutes);
				DbParameter returnValue = AddParameter (command, "@ReturnVal", ParameterDirection.ReturnValue, DbType.Int32, null);

				command.ExecuteScalar ();
				return GetReturnValue (returnValue);
			}
		}

		public override string GetPassword (string username, string answer)
		{
			if (!EnablePasswordRetrieval)
				throw new NotSupportedException ("this provider has not been configured to allow the retrieval of passwords");

			CheckParam ("username", username, 256);
			if (RequiresQuestionAndAnswer)
				CheckParam ("answer", answer, 128);

			PasswordInfo pi = GetPasswordInfo (username);
			if (pi == null)
				throw new ProviderException ("An error occurred while retrieving the password from the database");

			string user_answer = EncodePassword (answer, pi.PasswordFormat, pi.PasswordSalt);
			string password = null;

			using (DbConnection connection = CreateConnection ()) {
				DbCommand command = factory.CreateCommand ();
				command.Connection = connection;
				command.CommandText = @"aspnet_Membership_GetPassword";
				command.CommandType = CommandType.StoredProcedure;

				AddParameter (command, "@ApplicationName", ApplicationName);
				AddParameter (command, "@UserName", username);
				AddParameter (command, "@MaxInvalidPasswordAttempts", MaxInvalidPasswordAttempts);
				AddParameter (command, "@PasswordAttemptWindow", PasswordAttemptWindow);
				AddParameter (command, "@CurrentTimeUtc", DateTime.UtcNow);
				AddParameter (command, "@PasswordAnswer", user_answer);
				DbParameter retValue = AddParameter (command, "@ReturnVal", ParameterDirection.ReturnValue, DbType.Int32, null);

				DbDataReader reader = command.ExecuteReader ();

				int returnValue = GetReturnValue (retValue);
				if (returnValue == 3)
					throw new MembershipPasswordException ("Password Answer is invalid");
				if (returnValue == 99)
					throw new MembershipPasswordException ("The user account is currently locked out");

				if (reader.Read ()) {
					password = reader.GetString (0);
					reader.Close ();
				}

				if (pi.PasswordFormat == MembershipPasswordFormat.Clear)
					return password;
				else if (pi.PasswordFormat == MembershipPasswordFormat.Encrypted)
					return DecodePassword (password, pi.PasswordFormat);

				return password;
			}
		}

		MembershipUser GetUserFromReader (DbDataReader reader, string username, object userId)
		{
			int i = 0;
			if (username == null)
				i = 1;

			if (userId != null)
				username = reader.GetString (8);

			return new MembershipUser (this.Name, /* XXX is this right?  */
				(username == null ? reader.GetString (0) : username), /* name */
				(userId == null ? reader.GetGuid (8 + i) : userId), /* providerUserKey */
				reader.IsDBNull (0 + i) ? null : reader.GetString (0 + i), /* email */
				reader.IsDBNull (1 + i) ? null : reader.GetString (1 + i), /* passwordQuestion */
				reader.IsDBNull (2 + i) ? null : reader.GetString (2 + i), /* comment */
				reader.GetBoolean (3 + i), /* isApproved */
				reader.GetBoolean (9 + i), /* isLockedOut */
				reader.GetDateTime (4 + i).ToLocalTime (), /* creationDate */
				reader.GetDateTime (5 + i).ToLocalTime (), /* lastLoginDate */
				reader.GetDateTime (6 + i).ToLocalTime (), /* lastActivityDate */
				reader.GetDateTime (7 + i).ToLocalTime (), /* lastPasswordChangedDate */
				reader.GetDateTime (10 + i).ToLocalTime () /* lastLockoutDate */);
		}

		MembershipUser BuildMembershipUser (DbCommand query, string username, object userId)
		{
			try {
				using (DbConnection connection = CreateConnection ()) {
					query.Connection = connection;
					using (DbDataReader reader = query.ExecuteReader ()) {
						if (!reader.Read ())
							return null;

						return GetUserFromReader (reader, username, userId);
					}
				}
			} catch (Exception) {
				return null; /* should we let the exception through? */
			}
			finally {
				query.Connection = null;
			}
		}

		public override MembershipUser GetUser (string username, bool userIsOnline)
		{
			if (username == null)
				throw new ArgumentNullException ("username");

			if (username.Length == 0)
				return null;

			CheckParam ("username", username, 256);

			DbCommand command = factory.CreateCommand ();

			command.CommandText = @"aspnet_Membership_GetUserByName";
			command.CommandType = CommandType.StoredProcedure;

			AddParameter (command, "@UserName", username);
			AddParameter (command, "@ApplicationName", ApplicationName);
			AddParameter (command, "@CurrentTimeUtc", DateTime.Now);
			AddParameter (command, "@UpdateLastActivity", userIsOnline);

			MembershipUser u = BuildMembershipUser (command, username, null);

			return u;
		}

		public override MembershipUser GetUser (object providerUserKey, bool userIsOnline)
		{
			DbCommand command = factory.CreateCommand ();
			command.CommandText = @"aspnet_Membership_GetUserByUserId";
			command.CommandType = CommandType.StoredProcedure;

			AddParameter (command, "@UserId", providerUserKey);
			AddParameter (command, "@CurrentTimeUtc", DateTime.Now);
			AddParameter (command, "@UpdateLastActivity", userIsOnline);

			MembershipUser u = BuildMembershipUser (command, string.Empty, providerUserKey);
			return u;
		}

		public override string GetUserNameByEmail (string email)
		{
			CheckParam ("email", email, 256);

			using (DbConnection connection = CreateConnection ()) {

				DbCommand command = factory.CreateCommand ();
				command.Connection = connection;
				command.CommandText = @"aspnet_Membership_GetUserByEmail";
				command.CommandType = CommandType.StoredProcedure;

				AddParameter (command, "@ApplicationName", ApplicationName);
				AddParameter (command, "@Email", email);

				DbDataReader reader = command.ExecuteReader ();
				string rv = null;
				if (reader.Read ())
					rv = reader.GetString (0);
				reader.Close ();
				return rv;
			}
		}

		bool GetBoolConfigValue (NameValueCollection config, string name, bool def)
		{
			bool rv = def;
			string val = config [name];
			if (val != null) {
				try { rv = Boolean.Parse (val); }
				catch (Exception e) {
					throw new ProviderException (String.Format ("{0} must be true or false", name), e);
				}
			}
			return rv;
		}

		int GetIntConfigValue (NameValueCollection config, string name, int def)
		{
			int rv = def;
			string val = config [name];
			if (val != null) {
				try { rv = Int32.Parse (val); }
				catch (Exception e) {
					throw new ProviderException (String.Format ("{0} must be an integer", name), e);
				}
			}
			return rv;
		}

		int GetEnumConfigValue (NameValueCollection config, string name, Type enumType, int def)
		{
			int rv = def;
			string val = config [name];
			if (val != null) {
				try { rv = (int) Enum.Parse (enumType, val); }
				catch (Exception e) {
					throw new ProviderException (String.Format ("{0} must be one of the following values: {1}", name, String.Join (",", Enum.GetNames (enumType))), e);
				}
			}
			return rv;
		}

		string GetStringConfigValue (NameValueCollection config, string name, string def)
		{
			string rv = def;
			string val = config [name];
			if (val != null)
				rv = val;
			return rv;
		}

		void EmitValidatingPassword (string username, string password, bool isNewUser)
		{
			ValidatePasswordEventArgs args = new ValidatePasswordEventArgs (username, password, isNewUser);
			OnValidatingPassword (args);

			/* if we're canceled.. */
			if (args.Cancel) {
				if (args.FailureInformation == null)
					throw new ProviderException ("Password validation canceled");
				else
					throw args.FailureInformation;
			}
		}

		public override void Initialize (string name, NameValueCollection config)
		{
			if (config == null)
				throw new ArgumentNullException ("config");

			base.Initialize (name, config);

			applicationName = GetStringConfigValue (config, "applicationName", "/");
			enablePasswordReset = GetBoolConfigValue (config, "enablePasswordReset", true);
			enablePasswordRetrieval = GetBoolConfigValue (config, "enablePasswordRetrieval", false);
			requiresQuestionAndAnswer = GetBoolConfigValue (config, "requiresQuestionAndAnswer", true);
			requiresUniqueEmail = GetBoolConfigValue (config, "requiresUniqueEmail", false);
			passwordFormat = (MembershipPasswordFormat) GetEnumConfigValue (config, "passwordFormat", typeof (MembershipPasswordFormat),
											   (int) MembershipPasswordFormat.Hashed);
			maxInvalidPasswordAttempts = GetIntConfigValue (config, "maxInvalidPasswordAttempts", 5);
			minRequiredPasswordLength = GetIntConfigValue (config, "minRequiredPasswordLength", 7);
			minRequiredNonAlphanumericCharacters = GetIntConfigValue (config, "minRequiredNonalphanumericCharacters", 1);
			passwordAttemptWindow = GetIntConfigValue (config, "passwordAttemptWindow", 10);
			passwordStrengthRegularExpression = GetStringConfigValue (config, "passwordStrengthRegularExpression", "");

			MembershipSection section = (MembershipSection) WebConfigurationManager.GetSection ("system.web/membership");

			userIsOnlineTimeWindow = section.UserIsOnlineTimeWindow;

			/* we can't support password retrieval with hashed passwords */
			if (passwordFormat == MembershipPasswordFormat.Hashed && enablePasswordRetrieval)
				throw new ProviderException ("password retrieval cannot be used with hashed passwords");

			string connectionStringName = config ["connectionStringName"];

			if (applicationName.Length > 256)
				throw new ProviderException ("The ApplicationName attribute must be 256 characters long or less.");
			if (connectionStringName == null || connectionStringName.Length == 0)
				throw new ProviderException ("The ConnectionStringName attribute must be present and non-zero length.");

			connectionString = WebConfigurationManager.ConnectionStrings [connectionStringName];
			factory = connectionString == null || String.IsNullOrEmpty (connectionString.ProviderName) ?
				System.Data.SqlClient.SqlClientFactory.Instance :
				ProvidersHelper.GetDbProviderFactory (connectionString.ProviderName);
		}

		public override string ResetPassword (string username, string answer)
		{
			if (!EnablePasswordReset)
				throw new NotSupportedException ("this provider has not been configured to allow the resetting of passwords");

			CheckParam ("username", username, 256);

			if (RequiresQuestionAndAnswer)
				CheckParam ("answer", answer, 128);

			using (DbConnection connection = CreateConnection ()) {

				PasswordInfo pi = GetPasswordInfo (username);
				if (pi == null)
					throw new ProviderException (username + "is not found in the membership database");

				string newPassword = GeneratePassword ();
				EmitValidatingPassword (username, newPassword, false);

				string db_password = EncodePassword (newPassword, pi.PasswordFormat, pi.PasswordSalt);
				string db_answer = EncodePassword (answer, pi.PasswordFormat, pi.PasswordSalt);

				DbCommand command = factory.CreateCommand ();
				command.Connection = connection;
				command.CommandText = @"aspnet_Membership_ResetPassword";
				command.CommandType = CommandType.StoredProcedure;

				AddParameter (command, "@ApplicationName", ApplicationName);
				AddParameter (command, "@UserName", username);
				AddParameter (command, "@NewPassword", db_password);
				AddParameter (command, "@MaxInvalidPasswordAttempts", MaxInvalidPasswordAttempts);
				AddParameter (command, "@PasswordAttemptWindow", PasswordAttemptWindow);
				AddParameter (command, "@PasswordSalt", pi.PasswordSalt);
				AddParameter (command, "@CurrentTimeUtc", DateTime.UtcNow);
				AddParameter (command, "@PasswordFormat", (int) pi.PasswordFormat);
				AddParameter (command, "@PasswordAnswer", db_answer);
				DbParameter retValue = AddParameter (command, "@ReturnVal", ParameterDirection.ReturnValue, DbType.Int32, null);

				command.ExecuteNonQuery ();

				int returnValue = GetReturnValue (retValue);

				if (returnValue == 0)
					return newPassword;
				else if (returnValue == 3)
					throw new MembershipPasswordException ("Password Answer is invalid");
				else if (returnValue == 99)
					throw new MembershipPasswordException ("The user account is currently locked out");
				else
					throw new ProviderException ("Failed to reset password");
			}
		}

		public override void UpdateUser (MembershipUser user)
		{
			if (user == null)
				throw new ArgumentNullException ("user");

			if (user.UserName == null)
				throw new ArgumentNullException ("user.UserName");

			if (RequiresUniqueEmail && user.Email == null)
				throw new ArgumentNullException ("user.Email");

			CheckParam ("user.UserName", user.UserName, 256);

			if (user.Email.Length > 256 || (RequiresUniqueEmail && user.Email.Length == 0))
				throw new ArgumentException ("invalid format for user.Email");

			using (DbConnection connection = CreateConnection ()) {
				int returnValue = 0;

				DbCommand command = factory.CreateCommand ();
				command.Connection = connection;
				command.CommandText = @"aspnet_Membership_UpdateUser";
				command.CommandType = CommandType.StoredProcedure;

				AddParameter (command, "@ApplicationName", ApplicationName);
				AddParameter (command, "@UserName", user.UserName);
				AddParameter (command, "@Email", user.Email == null ? (object) DBNull.Value : (object) user.Email);
				AddParameter (command, "@Comment", user.Comment == null ? (object) DBNull.Value : (object) user.Comment);
				AddParameter (command, "@IsApproved", user.IsApproved);
				AddParameter (command, "@LastLoginDate", DateTime.UtcNow);
				AddParameter (command, "@LastActivityDate", DateTime.UtcNow);
				AddParameter (command, "@UniqueEmail", RequiresUniqueEmail);
				AddParameter (command, "@CurrentTimeUtc", DateTime.UtcNow);
				DbParameter retValue = AddParameter (command, "@ReturnVal", ParameterDirection.ReturnValue, DbType.Int32, null);

				command.ExecuteNonQuery ();

				returnValue = GetReturnValue (retValue);

				if (returnValue == 1)
					throw new ProviderException ("The UserName property of user was not found in the database.");
				if (returnValue == 7)
					throw new ProviderException ("The Email property of user was equal to an existing e-mail address in the database and RequiresUniqueEmail is set to true.");
				if (returnValue != 0)
					throw new ProviderException ("Failed to update user");
			}
		}

		public override bool ValidateUser (string username, string password)
		{
			if (username.Length == 0)
				return false;

			CheckParam ("username", username, 256);
			EmitValidatingPassword (username, password, false);

			PasswordInfo pi = ValidateUsingPassword (username, password);
			if (pi != null) {
				pi.LastLoginDate = DateTime.UtcNow;
				UpdateUserInfo (username, pi, true, true);
				return true;
			}
			return false;
		}


		public override bool UnlockUser (string username)
		{
			CheckParam ("username", username, 256);

			using (DbConnection connection = CreateConnection ()) {
				try {
					DbCommand command = factory.CreateCommand ();
					command.Connection = connection;
					command.CommandText = @"aspnet_Membership_UnlockUser"; ;
					command.CommandType = CommandType.StoredProcedure;

					AddParameter (command, "@ApplicationName", ApplicationName);
					AddParameter (command, "@UserName", username);
					DbParameter returnValue = AddParameter (command, "@ReturnVal", ParameterDirection.ReturnValue, DbType.Int32, null);

					command.ExecuteNonQuery ();
					if (GetReturnValue (returnValue) != 0)
						return false;
				}
				catch (Exception e) {
					throw new ProviderException ("Failed to unlock user", e);
				}
			}
			return true;
		}

		void UpdateUserInfo (string username, PasswordInfo pi, bool isPasswordCorrect, bool updateLoginActivity)
		{
			CheckParam ("username", username, 256);

			using (DbConnection connection = CreateConnection ()) {
				try {
					DbCommand command = factory.CreateCommand ();
					command.Connection = connection;
					command.CommandText = @"aspnet_Membership_UpdateUserInfo"; ;
					command.CommandType = CommandType.StoredProcedure;

					AddParameter (command, "@ApplicationName", ApplicationName);
					AddParameter (command, "@UserName", username);
					AddParameter (command, "@IsPasswordCorrect", isPasswordCorrect);
					AddParameter (command, "@UpdateLastLoginActivityDate", updateLoginActivity);
					AddParameter (command, "@MaxInvalidPasswordAttempts", MaxInvalidPasswordAttempts);
					AddParameter (command, "@PasswordAttemptWindow", PasswordAttemptWindow);
					AddParameter (command, "@CurrentTimeUtc", DateTime.UtcNow);
					AddParameter (command, "@LastLoginDate", pi.LastLoginDate);
					AddParameter (command, "@LastActivityDate", pi.LastActivityDate);
					DbParameter retValue = AddParameter (command, "@ReturnVal", ParameterDirection.ReturnValue, DbType.Int32, null);

					command.ExecuteNonQuery ();

					int returnValue = GetReturnValue (retValue);
					if (returnValue != 0)
						return;
				}
				catch (Exception e) {
					throw new ProviderException ("Failed to update Membership table", e);
				}

			}
		}

		PasswordInfo ValidateUsingPassword (string username, string password)
		{
			MembershipUser user = GetUser (username, true);
			if (user == null)
				return null;

			if (!user.IsApproved || user.IsLockedOut)
				return null;

			PasswordInfo pi = GetPasswordInfo (username);

			if (pi == null)
				return null;

			/* do the actual validation */
			string user_password = EncodePassword (password, pi.PasswordFormat, pi.PasswordSalt);

			if (user_password != pi.Password) {
				UpdateUserInfo (username, pi, false, false);
				return null;
			}

			return pi;
		}

		private PasswordInfo GetPasswordInfo (string username)
		{
			using (DbConnection connection = CreateConnection ()) {
				DbCommand command = factory.CreateCommand ();
				command.Connection = connection;
				command.CommandType = CommandType.StoredProcedure;
				command.CommandText = @"aspnet_Membership_GetPasswordWithFormat";

				AddParameter (command, "@ApplicationName", ApplicationName);
				AddParameter (command, "@UserName", username);
				AddParameter (command, "@UpdateLastLoginActivityDate", false);
				AddParameter (command, "@CurrentTimeUtc", DateTime.Now);
				// return value
				AddParameter (command, "@ReturnVal", ParameterDirection.ReturnValue, DbType.Int32, null);

				DbDataReader reader = command.ExecuteReader ();
				if (!reader.Read ())
					return null;

				PasswordInfo pi = new PasswordInfo (
					reader.GetString (0),
					(MembershipPasswordFormat) reader.GetInt32 (1),
					reader.GetString (2),
					reader.GetInt32 (3),
					reader.GetInt32 (4),
					reader.GetBoolean (5),
					reader.GetDateTime (6),
					reader.GetDateTime (7));

				return pi;
			}
		}

		private string EncodePassword (string password, MembershipPasswordFormat passwordFormat, string salt)
		{
			byte [] password_bytes;
			byte [] salt_bytes;

			switch (passwordFormat) {
				case MembershipPasswordFormat.Clear:
					return password;
				case MembershipPasswordFormat.Hashed:
					password_bytes = Encoding.Unicode.GetBytes (password);
					salt_bytes = Convert.FromBase64String (salt);

					byte [] hashBytes = new byte [salt_bytes.Length + password_bytes.Length];

					Buffer.BlockCopy (salt_bytes, 0, hashBytes, 0, salt_bytes.Length);
					Buffer.BlockCopy (password_bytes, 0, hashBytes, salt_bytes.Length, password_bytes.Length);

					MembershipSection section = (MembershipSection) WebConfigurationManager.GetSection ("system.web/membership");
					string alg_type = section.HashAlgorithmType;
					if (alg_type == "") {
						MachineKeySection keysection = (MachineKeySection) WebConfigurationManager.GetSection ("system.web/machineKey");
						alg_type = keysection.Validation.ToString ();
					}
					using (HashAlgorithm hash = HashAlgorithm.Create (alg_type)) {
						hash.TransformFinalBlock (hashBytes, 0, hashBytes.Length);
						return Convert.ToBase64String (hash.Hash);
					}
				case MembershipPasswordFormat.Encrypted:
					password_bytes = Encoding.Unicode.GetBytes (password);
					salt_bytes = Convert.FromBase64String (salt);

					byte [] buf = new byte [password_bytes.Length + salt_bytes.Length];

					Array.Copy (salt_bytes, 0, buf, 0, salt_bytes.Length);
					Array.Copy (password_bytes, 0, buf, salt_bytes.Length, password_bytes.Length);

					return Convert.ToBase64String (EncryptPassword (buf));
				default:
					/* not reached.. */
					return null;
			}
		}

		private string DecodePassword (string password, MembershipPasswordFormat passwordFormat)
		{
			switch (passwordFormat) {
				case MembershipPasswordFormat.Clear:
					return password;
				case MembershipPasswordFormat.Hashed:
					throw new ProviderException ("Hashed passwords cannot be decoded.");
				case MembershipPasswordFormat.Encrypted:
					return Encoding.Unicode.GetString (DecryptPassword (Convert.FromBase64String (password)));
				default:
					/* not reached.. */
					return null;
			}
		}

		public override string ApplicationName
		{
			get { return applicationName; }
			set { applicationName = value; }
		}

		public override bool EnablePasswordReset
		{
			get { return enablePasswordReset; }
		}

		public override bool EnablePasswordRetrieval
		{
			get { return enablePasswordRetrieval; }
		}

		public override MembershipPasswordFormat PasswordFormat
		{
			get { return passwordFormat; }
		}

		public override bool RequiresQuestionAndAnswer
		{
			get { return requiresQuestionAndAnswer; }
		}

		public override bool RequiresUniqueEmail
		{
			get { return requiresUniqueEmail; }
		}

		public override int MaxInvalidPasswordAttempts
		{
			get { return maxInvalidPasswordAttempts; }
		}

		public override int MinRequiredNonAlphanumericCharacters
		{
			get { return minRequiredNonAlphanumericCharacters; }
		}

		public override int MinRequiredPasswordLength
		{
			get { return minRequiredPasswordLength; }
		}

		public override int PasswordAttemptWindow
		{
			get { return passwordAttemptWindow; }
		}

		public override string PasswordStrengthRegularExpression
		{
			get { return passwordStrengthRegularExpression; }
		}

		[Flags]
		private enum DeleteUserTableMask
		{
			MembershipUsers = 1,
			UsersInRoles = 2,
			Profiles = 4,
			WebPartStateUser = 8
		}

		private sealed class PasswordInfo
		{
			private string _password;
			private MembershipPasswordFormat _passwordFormat;
			private string _passwordSalt;
			private int _failedPasswordAttemptCount;
			private int _failedPasswordAnswerAttemptCount;
			private bool _isApproved;
			private DateTime _lastLoginDate;
			private DateTime _lastActivityDate;

			internal PasswordInfo (
				string password,
				MembershipPasswordFormat passwordFormat,
				string passwordSalt,
				int failedPasswordAttemptCount,
				int failedPasswordAnswerAttemptCount,
				bool isApproved,
				DateTime lastLoginDate,
				DateTime lastActivityDate)
			{
				_password = password;
				_passwordFormat = passwordFormat;
				_passwordSalt = passwordSalt;
				_failedPasswordAttemptCount = failedPasswordAttemptCount;
				_failedPasswordAnswerAttemptCount = failedPasswordAnswerAttemptCount;
				_isApproved = isApproved;
				_lastLoginDate = lastLoginDate;
				_lastActivityDate = lastActivityDate;
			}

			public string Password
			{
				get { return _password; }
				set { _password = value; }
			}
			public MembershipPasswordFormat PasswordFormat
			{
				get { return _passwordFormat; }
				set { _passwordFormat = value; }
			}
			public string PasswordSalt
			{
				get { return _passwordSalt; }
				set { _passwordSalt = value; }
			}
			public int FailedPasswordAttemptCount
			{
				get { return _failedPasswordAttemptCount; }
				set { _failedPasswordAttemptCount = value; }
			}
			public int FailedPasswordAnswerAttemptCount
			{
				get { return _failedPasswordAnswerAttemptCount; }
				set { _failedPasswordAnswerAttemptCount = value; }
			}
			public bool IsApproved
			{
				get { return _isApproved; }
				set { _isApproved = value; }
			}
			public DateTime LastLoginDate
			{
				get { return _lastLoginDate; }
				set { _lastLoginDate = value; }
			}
			public DateTime LastActivityDate
			{
				get { return _lastActivityDate; }
				set { _lastActivityDate = value; }
			}
		}
	}
}
#endif

