using System;
using System.Data;
using System.Diagnostics;
using System.Collections;
using System.Security.Principal;
using System.Threading;
using System.Text;
using System.Text.RegularExpressions;
using System.Security.Cryptography;

namespace Orciid.Core
{
	/// <summary>
	/// User management class
	/// </summary>
	/// <remarks>
	/// The User class represents user accounts and their properties. It implements all IIdentity members,
	/// so that User objects can be used within the .NET managed security context. Every running thread must
	/// have a user attached to it via <see cref="User.Activate(string)"/> to perform restricted functions.
	/// </remarks>
	public class User:
		ModifiableObject, IIdentity, IComparable, IPrivilegeAssignee, IHasProperties
	{
		/// <summary>
		/// Internal identifier
		/// </summary>
		/// <remarks>
		/// The primary key of the user record in the database, which is used as the internal identifier for
		/// the user. The <see cref="AnonymousUser"/> always has an identifier of <c>-1</c>.
		/// </remarks>
		protected int id = 0;
		/// <summary>
		/// Login name
		/// </summary>
		/// <remarks>
		/// The user's login name. If the login process should be performed against an external user/password
		/// database (e.g. an LDAP directory), this login name must match the login name in the external
		/// database.
		/// </remarks>
		private string login;
		/// <summary>
		/// Password
		/// </summary>
		/// <remarks>
		/// MD5-encoded password if internal authentication is used.
		/// </remarks>
		private string password;
		/// <summary>
		/// Last name
		/// </summary>
		/// <remarks>
		/// The user's last name. This value is only used for display purposes.
		/// </remarks>
		protected string name;
		/// <summary>
		/// First name
		/// </summary>
		/// <remarks>
		/// The user's first name. This value is only used for display purposes.
		/// </remarks>
		protected string firstname;
		/// <summary>
		/// Email address
		/// </summary>
		/// <remarks>
		/// The user's email address. This must be a valid, fully qualified email address.
		/// </remarks>
		private string email;
		/// <summary>
		/// Administrator flag
		/// </summary>
		/// <remarks>
		/// If this is <c>true</c>, the user is automatically granted all privileges on all items
		/// </remarks>
		private bool administrator;
		/// <summary>
		/// Authenticated flag
		/// </summary>
		/// <remarks>
		/// After a successful authentication, this is set to <c>true</c>
		/// </remarks>
		private bool authenticated = false;
		/// <summary>
		/// Authentication type
		/// </summary>
		/// <remarks>
		/// The authentication type used to authenticate the user.
		/// </remarks>
		private string authenticationtype = "none";

		private DateTime lastauthenticated = DateTime.MinValue;

		private DateTime prevlastauthenticated = DateTime.MinValue;

		private SortedList folders = null;

		private string ipaddress = null;

		private Hashtable attributes = null;

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Creates a new user.
		/// </remarks>
		public User():
			base()
		{
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Does not initialize members or check for privileges. This constructor is used
		/// to create user objects from the database.
		/// </remarks>
		/// <param name="init">dummy parameter, should be <c>false</c></param>
		protected User(bool init):
			base(init)
		{
		}

		/// <summary>
		/// Check creation privilege
		/// </summary>
		/// <remarks>
		/// This method checks if a specified user has sufficient privileges to create this object.
		/// This method is called from the constructor, so the object already exists and therefore CanCreate
		/// does not need to be static.
		/// </remarks>
		/// <param name="user">The user to check privileges for</param>
		/// <returns><c>true</c> if the User has sufficient privileges to create this object, 
		/// <c>false</c> otherwise.</returns>
		public override bool CanCreate(User user)
		{
			return User.HasPrivilege(Privilege.ManageUsers, user);
		}

		/// <summary>
		/// Check modification privilege
		/// </summary>
		/// <remarks>
		/// This method checks if a specified user has sufficient privileges to modify this object.
		/// </remarks>
		/// <param name="user">The user to check privileges for</param>
		/// <returns><c>true</c> if the User has sufficient privileges to modify this object, 
		/// <c>false</c> otherwise.</returns>
		public override bool CanModify(User user)
		{
			return User.HasPrivilege(Privilege.ManageUsers, user);
		}

		/// <summary>
		/// Type of authentication used
		/// </summary>
		/// <remarks>
		/// AuthenticationType is a member of the IIdentity interface. It returns the type
		/// of authentication used for this user.
		/// </remarks>
		/// <value>
		/// The type of authentication used.
		/// </value>
		public string AuthenticationType 
		{
			get
			{
				return authenticationtype;
			}
		}

		/// <summary>
		/// Authentication status
		/// </summary>
		/// <remarks>
		/// IsAuthenticated is a member of the IIdentity interface. It returns true if
		/// the user has been authenticated, i.e. the <see cref="GetByLogin(string, string)"/> method has
		/// been called to retrieve the user.
		/// </remarks>
		/// <value>
		/// The authentication status for this user.
		/// </value>
		public virtual bool IsAuthenticated
		{
			get
			{
				return authenticated;
			}
		}
		
		/// <summary>
		/// Login name
		/// </summary>
		/// <remarks>
		/// Name is member of the IIdentity interface. It returns the login name of the user.
		/// </remarks>
		/// <value>
		/// The login name for this user.
		/// </value>
		public string Name
		{
			get
			{
				return login;
			}
		}

		/// <summary>
		/// Last Authentication Date
		/// </summary>
		/// <remarks>
		/// Depending on the database, accuracy may only be to the day.
		/// </remarks>
		/// <value>
		/// The date this user has successfully authenticated last.
		/// </value>
        /// <seealso cref="PrevLastAuthenticated"/>
		public DateTime LastAuthenticated
		{
			get
			{
				return lastauthenticated;
			}
		}

        /// <summary>
        /// Last Authentication Date before this session
        /// </summary>
        /// <remarks>
        /// Depending on the database, accuracy may only be to the day.
        /// </remarks>
        /// <value>
        /// The date this user has successfully authenticated last before the current session.
        /// </value>
        /// <seealso cref="LastAuthenticated"/>
		public DateTime PrevLastAuthenticated
		{
			get
			{
				return prevlastauthenticated;
			}
		}

		/// <summary>
		/// Client IP address
		/// </summary>
		/// <remarks>
		/// This value represents the client's IPv4 address in standard dotted notation.
		/// If an invalid address is passed to this property, an exception is thrown.
		/// </remarks>
		/// <value>
		/// The IP address of the client the user is currently using, or <c>null</c> if
		/// no IP address is available.
		/// </value>
		public string IPAddress
		{
			get
			{
				return ipaddress;
			}
			set
			{
				// had to remove IP address validity check due to lack of IPv6 support
				// if (value == null || Orciid.Core.IPAddress.IsValid(value))
					ipaddress = value;
				// else
				//	throw new CoreException("Not a valid IP Address");
			}
		}

		/// <summary>
		/// Get attribute value
		/// </summary>
		/// <remarks>
		/// Attributes are certain properties for the user that are retrieved upon login,
		/// e.g. from an external authentication source.  Attributes are not stored within
		/// the system and are not available unless a user is currently logged in.  Not all
		/// authentication sources must or can provide attributes.  If an attribute has multiple
		/// values, this method only returns the first value.  Use <see cref="GetMultiValueAttribute"/>
		/// to retrieve multiple values.
		/// </remarks>
		/// <param name="name">The name of the attribute</param>
		/// <returns>The attribute value, or null if the attribute does not exist.</returns>
		public string GetAttribute(string name)
		{
			if (attributes == null)
				return null;
			object a = attributes[name];
			if (a == null)
				return null;
			if (a is ArrayList)
				return (string)((ArrayList)a)[0];
			if (a is string)
				return (string)a;
			throw new Exception("Internal error");
		}

		/// <summary>
		/// Get attribute values
		/// </summary>
		/// <remarks>
		/// Attributes are certain properties for the user that are retrieved upon login,
		/// e.g. from an external authentication source.  Attributes are not stored within
		/// the system and are not available unless a user is currently logged in.  Not all
		/// authentication sources must or can provide attributes.
		/// </remarks>
		/// <param name="name">The name of the attribute</param>
		/// <returns>An array of attribute values, or null if the attribute does not exist.</returns>
		public string[] GetMultiValueAttribute(string name)
		{
			if (attributes == null)
				return null;
			object a = attributes[name];
			if (a == null)
				return null;
			if (a is ArrayList)
				return (string[])((ArrayList)a).ToArray(typeof(string));
			if (a is string)
				return new string[] { (string)a };
			throw new Exception("Internal error");
		}

		/// <summary>
		/// Attach user to the current thread
		/// </summary>
		/// <remarks>
		/// This method sets this user object as the CurrentPrincipal for the current thread. This
		/// is only possible if the user has been authenticated, otherwise this will raise an
		/// exception.
		/// </remarks>
		/// <param name="ip">The current IP address of the client machine the user is using,
		/// or <c>null</c> if unavailable.  This parameter is required for 
		/// <see cref="IPBasedUserGroup"/>.</param>
		/// <exception cref="CoreException">Thrown if this user object is not authenticated.</exception>
		public void Activate(string ip)
		{
			Activate(true, ip);
		}

		internal void Activate(bool enforcepriv, string ip)
		{
			if (!enforcepriv || IsAuthenticated)
			{
				this.IPAddress = ip;
				Thread.CurrentPrincipal = GetPrincipal();
			}
			else
				throw new CoreException("This user object has not been authenticated.");
		}
	
		/// <summary>
		/// Create User object from DataTable
		/// </summary>
		/// <remarks>
		/// This static method is called when a User object is created directly from a database row.
		/// </remarks>
		/// <param name="conn">A DBConnection object used to perform data transformation.</param>
		/// <param name="table">A DataTable resulting from a query of the Users table.</param>
		/// <param name="rowid">The row index of the user record to be read from the table.</param>
		/// <returns>A User object or <c>null</c> if the row does not exist.</returns>
		private static User CreateFromTable(DBConnection conn, DataTable table, int rowid)
		{
			User user = null;
			if (rowid >= 0 && rowid < table.Rows.Count) 
			{
				DataRow row = table.Rows[rowid];
				user = new User(false);
				user.login = conn.DataToString(row["Login"]);
				user.password = conn.DataToString(row["Password"]);
				user.id = conn.DataToInt(row["ID"], 0);
				user.name = conn.DataToString(row["Name"]);
				user.firstname = conn.DataToString(row["FirstName"]);
				user.email = conn.DataToString(row["Email"]);
				user.administrator = conn.DataToBool(row["Administrator"], false);
				user.lastauthenticated = conn.DataToDateTime(row["LastAuthenticated"], DateTime.Now);
			}
			return user;
		}

		/// <summary>
		/// Create MD5 password hash
		/// </summary>
		/// <remarks>
		/// If the internal password mechanism is used, passwords are stored in the database in an MD5 hashed format.
		/// This method converts a clear text password into a 32 character MD5 hash 
		/// (hex representation of 16 byte value). Since all strings are unicode, every character in the string
		/// is converted into bytes, which discards some information.
		/// </remarks>
		/// <param name="password">The password to hash</param>
		/// <returns>The MD5 hash value for the specified password</returns>
		public static string MD5Password(string password)
		{
			if (password == null)
				return null;
			byte[] pwd = new Byte[password.Length];
			for (int i = 0; i < password.Length; i++)
				pwd[i] = Convert.ToByte(password[i]);
			byte[] hash = new MD5CryptoServiceProvider().ComputeHash(pwd);
			StringBuilder result = new StringBuilder(hash.Length * 2);
			for (int i = 0; i < hash.Length; i++)
				result.Append(String.Format("{0,2:X}", hash[i]).Replace(" ", "0"));
			return result.ToString();
		}

		/// <summary>
		/// Retrieve user by login
		/// </summary>
		/// <remarks>
		/// This method returns a User object identified by its login name.
		/// </remarks>
		/// <param name="username">The login name of the user account.</param>
		/// <returns>A User object for the specified user or <c>null</c> if the user was not found.</returns>
		public static User GetByLogin(string username)
		{
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ID,Login,Password,Name,FirstName,Email,Administrator,LastAuthenticated 
					FROM Users WHERE Login={login}");
				query.AddParam("login", username);
				DataTable table = conn.SelectQuery(query);
				return CreateFromTable(conn, table, 0);
			}
		}

		/// <summary>
		/// Retrieve user by login and password
		/// </summary>
		/// <remarks>
		/// This method returns a User object identified by its login name and password.
		/// This method will automatically try to verify the password and authenticate the user.
		/// If no user exists, any external authentication source is checked, if successful,
		/// a new user is created automatically if so specified in the configuration.
		/// </remarks>
		/// <param name="username">The login name of the user account.</param>
		/// <param name="password">The password of the user account.</param>
		/// <returns>A User object for the specified user or <c>null</c> if the user was not found.</returns>
		public static User GetByLogin(string username, string password)
		{
			// Don't allow blank passwords
			if (password == null || password.Length == 0)
				return null;
			User user = GetByLogin(username);
			if (user != null) 
			{
				if (user.password != null)
				{
					if (MD5Password(password) == user.password)
					{
						TransactionLog.Instance.Add("User authenticated internally", 
							String.Format("{0} [{1}]", user.login, user.ID));
						user.authenticated = true;
						user.authenticationtype = "internal";							
					}
				}
				if (!user.authenticated)
					foreach (System.Type method in UserAuthentication.methods)
					{
						UserAuthentication m = (UserAuthentication)Activator.CreateInstance(method);
						if (m.Authenticate(username, password))
						{
							TransactionLog.Instance.Add("User authenticated using " + 
								m.GetAuthenticationType(), 
								String.Format("{0} [{1}]", user.login, user.ID));
							user.authenticated = true;
							user.authenticationtype = m.GetAuthenticationType();
							user.attributes = m.GetAttributes();
							break;
						}
					}
			}
			else if (Configuration.Instance.GetBool("authentication.autocreateuser"))
			{
				foreach (System.Type method in UserAuthentication.methods)
				{
					UserAuthentication m = (UserAuthentication)Activator.CreateInstance(method);
					if (m.Authenticate(username, password))
					{
						user = new User(false);
						// directly access properties here to avoid security check
						user.login = username;
						user.firstname = m.GetFirstName();
						user.name = m.GetLastName();
						user.email = m.GetEmail();
						user.CommitChanges();
						user.ResetModified();
						string pwd = m.GetPassword();
						if (pwd != null)
							user.SetPassword(false, pwd);
						user.authenticated = true;
						user.authenticationtype = m.GetAuthenticationType();
						user.attributes = m.GetAttributes();
						TransactionLog.Instance.Add("User autocreated using " + 
							m.GetAuthenticationType(), 
							String.Format("{0} [{1}]", user.login, user.ID));
						break;
					}
				}
			}
			if (user != null && user.authenticated)
			{
				UpdateUserLastAuthenticated(user);
			}
			return user;
		}

		private static void UpdateUserLastAuthenticated(User user)
		{
			using (DBConnection conn = DBConnector.GetConnection())
			{
				user.prevlastauthenticated = user.lastauthenticated;
				user.lastauthenticated = DateTime.Now;
				Query query = new Query(conn,
					@"UPDATE Users SET LastAuthenticated={lastauthenticated} WHERE ID={id}");
				query.AddParam("lastauthenticated", user.lastauthenticated);
				query.AddParam("id", user.id);
				conn.ExecQuery(query);
			}
		}

		/// <summary>
		/// Retrieve user by single sign-on reference
		/// </summary>
		/// <remarks>
		/// This method returns a User object identified by a single sign-on reference.
		/// </remarks>
		/// <param name="siteid">The identifier of the single sign-on site</param>
		/// <param name="parameters">Any parameters passed by the single sign-on site</param>
		/// <returns>A User object for the specified user or <c>null</c> if the user was not found.</returns>
		public static User GetBySingleSignOn(string siteid, Hashtable parameters)
		{
			try
			{
				SingleSignOn sso = new SingleSignOn(siteid, parameters);
				if (!sso.Execute())
					throw new CoreException("Could not initialize single sign-on");
					
				User user = GetByLogin(sso.Login);
				if (user != null)
				{
					user.authenticated = true;
					user.authenticationtype = "singlesignon [" + siteid + "]";
					user.attributes = sso.GetAttributes();
					TransactionLog.Instance.Add("User authenticated using " + 
						user.authenticationtype, 
						String.Format("{0} [{1}]", user.login, user.ID));
				}
				else if (Configuration.Instance.GetBool("authentication.autocreateuser"))
				{
					user = new User(false);
					// directly access properties here to avoid security check
					user.login = sso.Login;
					Hashtable attributes = sso.GetAttributes();
					user.firstname = attributes.ContainsKey("firstname") ? 
						(string)attributes ["firstname"] : null;
					user.name = attributes.ContainsKey("lastname") ? 
						(string)attributes ["lastname"] : "Unknown";
					user.email = attributes.ContainsKey("email") ? 
						(string)attributes ["email"] : null;
					user.CommitChanges();
					user.ResetModified();
					user.authenticated = true;
					user.authenticationtype = "singlesignon [" + siteid + "]";
					user.attributes = attributes;
					TransactionLog.Instance.Add("User autocreated using " + 
						user.authenticationtype, 
						String.Format("{0} [{1}]", user.login, user.ID));
				}
				if (user != null && user.authenticated)
					UpdateUserLastAuthenticated(user);
				return user;
			}
			catch (Exception ex)
			{
				StringBuilder b = new StringBuilder();
				b.Append("Site: ");
				b.Append(siteid);
				b.Append("\n\n");
				foreach (string k in parameters.Keys)
				{
					b.Append(k);
					b.Append(": ");
					b.Append(parameters[k]);
					b.Append('\n');
				}
				TransactionLog.Instance.AddException("Single sign-on failed", b.ToString(), ex);
				return null;
			}	
		}

		/// <summary>
		/// Return User object identified by internal identifier
		/// </summary>
		/// <remarks>
		/// This method returns a User object identified by its internal identifier. 
		/// </remarks>
		/// <param name="id">The internal identifier of the user.</param>
		/// <returns>A User object for the specified user or <c>null</c> if the user was not found.</returns>
		public static User GetByID(int id)
		{
			if (id == AnonymousUser.User.id)
				return AnonymousUser.User;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ID,Login,Password,Name,FirstName,Email,Administrator,LastAuthenticated
					FROM Users WHERE ID={id}");
				query.AddParam("id", id);
				DataTable table = conn.SelectQuery(query);
				if (table.Rows.Count > 0)
					return CreateFromTable(conn, table, 0);
			}
			return null;
		}
		
		/// <summary>
		/// Return User objects identified by internal identifiers
		/// </summary>
		/// <remarks>
		/// This method returns an array of User objects identified by their internal identifiers. 
		/// Only users with UserAdministration privileges can use this method.
		/// </remarks>
		/// <param name="id">The internal identifiers of the user.</param>
		/// <returns>An array of User objects for the specified users. The order of user objects in the
		/// returned array matches the order of the identifiers. Unavailable users are represented by <c>null</c>
		/// elements.</returns>
		public static User[] GetByID(params int[] id)
		{
			if (id == null || id.Length == 0)
				return new User[0];
			User[] users = new User[id.Length];
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ID,Login,Password,Name,FirstName,Email,Administrator,LastAuthenticated 
					FROM Users WHERE ID IN {ids}");
				query.AddParam("ids", id);
				DataTable table = conn.SelectQuery(query);
				for (int row = 0; row < table.Rows.Count; row++)
				{
					User u = CreateFromTable(conn, table, row);
					for (int i = 0; i < id.Length; i++)
						if (u.id == id[i])
						{
							users[i] = u;
							break;
						}
				}
			}
			for (int i = 0; i < id.Length; i++)
				if (id[i] == AnonymousUser.User.id)
					users[i] = AnonymousUser.User;
			return users;
		}

		/// <summary>
		/// Return different starting characters for login names
		/// </summary>
		/// <remarks>
		/// This method is a utility function for user directories. It returns all the different
		/// starting patterns for all login names in the system.
		/// </remarks>
		/// <param name="length">The length of the starting patterns. The typical value is
		/// <c>1</c> to just return the different first characters of all login names.</param>
		/// <returns>An array of strings with the different patterns.</returns>
		public static string[] GetLoginPatterns(int length)
		{
			if (length < 1)
				return null;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT DISTINCT UPPER(LEFT(Login,{length})) AS P FROM Users ORDER BY P");
				query.AddParam("length", length);
				DataTable table = conn.SelectQuery(query);
				string[] result = new string[table.Rows.Count];
				for (int i = 0; i < table.Rows.Count; i++)
					result[i] = conn.DataToString(table.Rows[i]["P"]);
				return result;
			}
		}

		/// <summary>
		/// Retrieve users by login name pattern
		/// </summary>
		/// <remarks>
		/// This method returns all user accounts whose login name matches a given starting
		/// pattern. This is mainly useful in user directories.
		/// </remarks>
		/// <param name="beginswith">The starting pattern to match the login names of the 
		/// user accounts to retrieve</param>
		/// <returns>An array of users matching the given pattern. The array may be empty, but
		/// will not be <c>null</c>.</returns>
		public static User[] GetByLoginPattern(string beginswith)
		{
			if (beginswith == null || beginswith.Length == 0)
				return new User[0];
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ID,Login,Password,Name,FirstName,Email,Administrator,LastAuthenticated
					FROM Users WHERE Login LIKE '{beginswith:lq}%' ORDER BY Login");
				query.AddParam("beginswith", beginswith);
				DataTable table = conn.SelectQuery(query);
				User[] users = new User[table.Rows.Count];
				for (int row = 0; row < table.Rows.Count; row++)
					users[row] = CreateFromTable(conn, table, row);
				return users;
			}
		}

		/// <summary>
		/// Return different starting characters for surname (User.Name)
		/// </summary>
		/// <remarks>
		/// This method is a utility function for user directories. It returns all the different
		/// starting patterns for all surnames in the system.
		/// </remarks>
		/// <param name="length">The length of the starting patterns. The typical value is
		/// <c>1</c> to just return the different first characters of all surnames.</param>
		/// <returns>An array of strings with the different patterns.</returns>
		public static string[] GetNamePatterns(int length)
		{
			if (length < 1)
				return null;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT DISTINCT UPPER(LEFT(Name,{length})) AS P FROM Users ORDER BY P");
				query.AddParam("length", length);
				DataTable table = conn.SelectQuery(query);
				string[] result = new string[table.Rows.Count];
				for (int i = 0; i < table.Rows.Count; i++)
					result[i] = conn.DataToString(table.Rows[i]["P"]);
				return result;
			}
		}
		
		/// <summary>
		/// Retrieve users by surname pattern
		/// </summary>
		/// <remarks>
		/// This method returns all user accounts whose surname matches a given starting
		/// pattern. This is mainly useful in user directories.
		/// </remarks>
		/// <param name="beginswith">The starting pattern to match the surnames of the
		/// user accounts to retrieve</param>
		/// <returns>An array of users matching the given pattern. The array may be empty, but
		/// will not be <c>null</c>.</returns>
		public static User[] GetByNamePattern(string beginswith)
		{
			if (beginswith == null || beginswith.Length == 0)
				return new User[0];
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ID,Login,Password,Name,FirstName,Email,Administrator,LastAuthenticated
					FROM Users WHERE Name LIKE '{beginswith:lq}%' ORDER BY Name, FirstName");
				query.AddParam("beginswith", beginswith);
				DataTable table = conn.SelectQuery(query);
				User[] users = new User[table.Rows.Count];
				for (int row = 0; row < table.Rows.Count; row++)
					users[row] = CreateFromTable(conn, table, row);
				return users;
			}
		}

		private static void GetBySlideshowsAddRows(Hashtable userids, 
			DataTable table, DBConnection conn)
		{
			foreach (DataRow row in table.Rows)
			{
				int userid = conn.DataToInt(row["UserID"], 0);
				int folderid = conn.DataToInt(row["FolderID"], 0);
				if (userids.ContainsKey(userid))
				{
					ArrayList l = (ArrayList)userids[userid];
					if (!l.Contains(folderid))
						l.Add(folderid);
				}
				else
				{
					ArrayList l = new ArrayList();
					l.Add(folderid);
					userids.Add(userid, l);
				}
			}
		}

		/// <summary>
		/// Retrieve users with accessible slideshows
		/// </summary>
		/// <remarks>
		/// This method returns a list of users that have at least one 
		/// slideshow accessible to the current user.  The user identifiers are the
		/// keys of the hashtable, the values are integer arrays with the folder identifiers.
		/// </remarks>
		/// <returns>A <c>Hashtable</c> of user identifiers as keys and
		/// <c>int[]</c> containing folder identifiers as values.</returns>
		public static Hashtable GetBySlideshows()
		{
			User user = User.CurrentUser();
			if (user == null)
				return null;
			// the values of this hashtable will temporarily be arraylists of ints
			Hashtable userids = new Hashtable();
			using (DBConnection conn = DBConnector.GetConnection())
			{
				UserGroup[] groups = UserGroup.FindUser(user);
				int[] uids;
				if (groups == null || groups.Length == 0)
					uids = new int[] { 0 };
				else
				{
					uids = new int[groups.Length];
					for (int i = 0; i < groups.Length; i++)
						uids[i] = groups[i].ID;
				}
				// get all visible slideshows
				Query query = new Query(conn,
					@"SELECT DISTINCT UserID,FolderID 
					FROM Slideshows 
					WHERE {adminflag}=1 OR ((UserID={userid} OR ArchiveFlag=0) AND 
					ID IN (
					SELECT ObjectID FROM AccessControl 
					WHERE (ObjectType='S' AND GroupID IN {groupids} AND (GrantPriv & {view} = {view})) AND ObjectID NOT IN 
					(SELECT ObjectID FROM AccessControl 
					WHERE (ObjectType='S' AND (UserID={userid} OR GroupID IN {groupids}) AND (DenyPriv & {view} = {view}))) 
					UNION 
					SELECT ObjectID FROM AccessControl 
					WHERE (ObjectType='S' AND UserID={userid} AND (GrantPriv & {view} = {view})) AND ObjectID NOT IN 
					(SELECT ObjectID FROM AccessControl 
					WHERE (ObjectType='S' AND UserID={userid} AND (DenyPriv & {view} = {view})))))");
				query.AddParam("adminflag", user.Administrator ? 1 : 0);
				query.AddParam("userid", user.id);
				query.AddParam("groupids", uids);
				query.AddParam("view", Privilege.ViewSlideshow);
				DataTable table = conn.SelectQuery(query);
				GetBySlideshowsAddRows(userids, table, conn);
				if (!user.Administrator)
				{
					// get slideshows that are invisible, but that user is allowed to modify
					query = new Query(conn,
						@"SELECT DISTINCT UserID,FolderID 
						FROM Slideshows 
						WHERE Slideshows.ID IN (
						SELECT ObjectID FROM AccessControl 
						WHERE (ObjectType='S' AND GroupID IN {groupids} AND (GrantPriv & {modify} = {modify})) AND ObjectID NOT IN 
						(SELECT ObjectID FROM AccessControl 
						WHERE (ObjectType='S' AND (UserID={userid} OR GroupID IN {groupids}) AND ((DenyPriv & {modify} = {modify})))) 
						UNION 
						SELECT ObjectID FROM AccessControl 
						WHERE (ObjectType='S' AND UserID={userid} AND (GrantPriv & {modify} = {modify})) AND ObjectID NOT IN 
						(SELECT ObjectID FROM AccessControl 
						WHERE (ObjectType='S' AND UserID={userid} AND ((DenyPriv & {modify} = {modify})))))");
					query.AddParam("userid", user.id);
					query.AddParam("groupids", uids);
					query.AddParam("modify", Privilege.ModifySlideshow);
					table = conn.SelectQuery(query);
					GetBySlideshowsAddRows(userids, table, conn);

					// get slideshows that are invisible, but that user is allowed to modify
					query = new Query(conn,
						@"SELECT DISTINCT UserID,FolderID 
						FROM Slideshows 
						WHERE Slideshows.ID IN (
						SELECT ObjectID FROM AccessControl 
						WHERE (ObjectType='S' AND GroupID IN {groupids} AND (GrantPriv & {copy} = {copy})) AND ObjectID NOT IN 
						(SELECT ObjectID FROM AccessControl 
						WHERE (ObjectType='S' AND (UserID={userid} OR GroupID IN {groupids}) AND ((DenyPriv & {copy} = {copy})))) 
						UNION 
						SELECT ObjectID FROM AccessControl 
						WHERE (ObjectType='S' AND UserID={userid} AND (GrantPriv & {copy} = {copy})) AND ObjectID NOT IN 
						(SELECT ObjectID FROM AccessControl 
						WHERE (ObjectType='S' AND UserID={userid} AND ((DenyPriv & {copy} = {copy})))))");
					query.AddParam("userid", user.id);
					query.AddParam("groupids", uids);
					query.AddParam("copy", Privilege.CopySlideshow);
					table = conn.SelectQuery(query);
					GetBySlideshowsAddRows(userids, table, conn);
				}
			}
			// convert values from arraylists of ints to int arrays
			Hashtable temp = new Hashtable(userids.Count);
			foreach (int i in userids.Keys)
				temp.Add(i, (int[])((ArrayList)userids[i]).ToArray(typeof(int)));
			return temp;
		}

		/// <summary>
		/// Set internal password for user
		/// </summary>
		/// <remarks>
		/// This method sets the internal password for an existing user.
		/// </remarks>
		/// <param name="pwd">The clear text password to be used for internal authentication, or <c>null</c>
		/// to remove internal password</param>
		/// <param name="enforce">Specifies if privileges should be enforced when
		/// setting the password. <c>false</c> bypasses all privilege checks.</param>
		private void SetPassword(bool enforce, string pwd)
		{
			if (id == 0)
				throw new CoreException("User must be written to database first.");
			if (enforce)
			{
				User current = CurrentUser();
				if (administrator && (current == null || !current.administrator))
					throw new CoreException("Cannot set password for administrator account.");
				if (current.id != this.id)
					RequirePrivilege(Privilege.ResetPassword);
			}
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"UPDATE Users SET Password={password} WHERE ID={id}");
				query.AddParam("password", pwd == null || pwd.Length == 0 ? null : MD5Password(pwd));
				query.AddParam("id", id);
				conn.ExecQuery(query);
			}
		}

		/// <summary>
		/// Set internal password for user
		/// </summary>
		/// <remarks>
		/// This method sets the internal password for an existing user.
		/// </remarks>
		/// <param name="pwd">The clear text password to be used for internal authentication, or <c>null</c>
		/// to remove internal password</param>
		public void SetPassword(string pwd)
		{
			SetPassword(true, pwd);
		}

		/// <summary>
		/// Search for users
		/// </summary>
		/// <remarks>
		/// This method returns a list of users matching a specified keyword. The search looks for the
		/// keyword in the login, full name, and email fields. Only users
		/// with UserAdministration privileges can use this method.
		/// </remarks>
		/// <param name="criteria">A keyword that will be matched against the login, full name and email fields.</param>
		/// <returns>An array of User objects matching the criteria or <c>null</c> of no records matched.
		/// The records are sorted by logins.</returns>
		public static User[] Search(string criteria)
		{
			if (criteria == null || criteria.Length == 0)
				return null;
			User[] users = null;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ID,Login,Password,Name,FirstName,Email,Administrator,LastAuthenticated
					FROM Users 
					WHERE Login LIKE '%{login:lq}%' OR Name LIKE '%{login:lq}%' OR Email LIKE '%{login:lq}%'");
				query.AddParam("login", criteria);
				DataTable table = conn.SelectQuery(query);
				if (table.Rows.Count > 0) 
				{
					users = new User[table.Rows.Count];
					for (int i = 0; i < table.Rows.Count; i++)
						users[i] = CreateFromTable(conn, table, i);
				}
			}
			return users;
		}

		/// <summary>
		/// Delete user from database
		/// </summary>
		/// <remarks>
		/// This method removes a user record from the database. The user object itself is invalidated. Only users
		/// with UserAdministration privileges can use this method. The current user cannot delete his/her own
		/// account.
		/// </remarks>
		/// <exception cref="CoreException">Thrown if the user could not be deleted.</exception>
		public void Delete()
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			User current = User.CurrentUser();
			if (id > 0 && (current == null || current.id != id))
			{
				UserGroup[] groups = UserGroup.FindUser(this);
				if (groups != null)
					foreach (UserGroup group in groups)
						if (group != null)
							group.DeletingUser(this);
				foreach (Slideshow show in Slideshow.GetByUser(id))
					show.DeleteWithUser();
				Properties.ClearProperties(this);
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						"DELETE FROM Users WHERE ID={id}");
					query.AddParam("id", id);
					conn.ExecQuery(query);

					query = new Query(conn,
						"DELETE FROM Folders WHERE UserID={id}");
					query.AddParam("id", id);
					conn.ExecQuery(query);

					query = new Query(conn,
						"DELETE FROM AccessControl WHERE UserID={id}");
					query.AddParam("id", id);
					conn.ExecQuery(query);

					query = new Query(conn,
						"DELETE FROM SavedSearches WHERE UserID={id}");
					query.AddParam("id", id);
					conn.ExecQuery(query);

					query = new Query(conn,
						"DELETE FROM FavoriteImages WHERE UserID={id}");
					query.AddParam("id", id);
					conn.ExecQuery(query);

					query = new Query(conn,
						"DELETE FROM ImageAnnotations WHERE UserID={id}");
					query.AddParam("id", id);
					conn.ExecQuery(query);

					query = new Query(conn,
						"DELETE FROM TrackUsageAgreements WHERE UserID={id}");
					query.AddParam("id", id);
					conn.ExecQuery(query);
				}
				foreach (int cid in Collection.GetCollectionIDs())
				{
					Collection coll = Collection.GetByID(false, cid);
					if (coll != null)
						coll.DeleteUserImages(this);
				}

				id = 0;
				ResetModified();
			}
			else
				throw new CoreException("Could not delete user.");
		}

		/// <summary>
		/// Writes a modified user object to the database
		/// </summary>
		/// <remarks> 
		/// After creating or modifying a user object, this method must be called. It writes
		/// the object to the database and returns it to an unmodifiable state.
		/// </remarks>
		/// <exception cref="CoreException">Thrown if the object cannot be written
		/// to the database.</exception>
		protected override void CommitChanges()
		{
			int result;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				if (id == 0) 
				{
					Query query = new Query(conn,
						@"INSERT INTO Users (Name,Login,Email,Administrator,FirstName) 
						VALUES ({name},{login},{email},{administrator},{firstname})");
					query.AddParam("name", name);
					query.AddParam("login", login);
					query.AddParam("email", email);
					query.AddParam("administrator", administrator);
					query.AddParam("firstname", firstname);
					result = conn.ExecQuery(query);
					if (result == 1)
						id = conn.LastIdentity("Users");
					else
						throw new CoreException("Could not write new User object to database");
				}
				else
				{
					Query query = new Query(conn,
						@"UPDATE Users SET Name={name},Login={login},Email={email},
						Administrator={administrator},FirstName={firstname} WHERE ID={id}");
					query.AddParam("name", name);
					query.AddParam("login", login);
					query.AddParam("email", email);
					query.AddParam("administrator", administrator);
					query.AddParam("firstname", firstname);
					query.AddParam("id", id);
					result = conn.ExecQuery(query);
				}
			}
			if (result != 1) 
				throw new CoreException("Could not write modified User object to database");
		}

		/// <summary>
		/// Last name
		/// </summary>
		/// <remarks>
		/// Gets or sets the user's last name. The last name is only used for display purposes.
		/// The last name cannot be empty or <c>null</c>.
		/// </remarks>
		/// <exception cref="CoreException">Thrown if the last name is set to empty or <c>null</c>.</exception>
		/// <value>
		/// The user's last name.
		/// </value>
		public string LastName
		{
			get
			{
				return name;
			}
			set
			{
				if (value != null && value.Length > 0)
				{
					MarkModified();
					name = (value.Length > 100 ? value.Substring(0, 100) : value);
				}
				else
					throw new CoreException("User.LastName cannot be empty or null");
			}
		}

		/// <summary>
		/// First name
		/// </summary>
		/// <remarks>
		/// Gets or sets the user's first name. The first name is only used for display purposes.
		/// The first name cannot be empty, it is set to <c>null</c> instead.
		/// </remarks>
		/// <value>
		/// The user's first name.
		/// </value>
		public string FirstName
		{
			get
			{
				return firstname;
			}
			set
			{
				MarkModified();
				if (value != null && value.Length == 0)
					firstname = null;
				else
					firstname = (value.Length > 100 ? value.Substring(0, 100) : value);
			}
		}

		/// <summary>
		/// Full name
		/// </summary>
		/// <remarks>
		/// Gets the user's full name. The full name is only used for display purposes.
		/// </remarks>
		/// <value>
		/// The user's full name in the format <c>LastName, FirstName</c>
		/// </value>
		public string FullName
		{
			get
			{
				return GenerateFullName(name, firstname);
			}
		}

		internal static string GenerateFullName(string last, string first)
		{
			if (last != null)
				return last + (first != null ? ", " + first : "");
			else
				return (first != null ? first : "n/a");
		}

		/// <summary>
		/// Login name
		/// </summary>
		/// <remarks>
		/// Gets or sets the user's login name. The login name is used to authenticate the user internally
		/// or against external user directories. It cannot be empty or <c>null</c>.
		/// </remarks>
		/// <exception cref="CoreException">Thrown if the login name is empty or <c>null</c>.</exception>
		/// <value>
		/// The user's login name.
		/// </value>
		public string Login
		{
			get
			{
				return login;
			}
			set
			{
				if (value != null && value.Length > 0)
				{
					MarkModified();
					login = (value.Length > 50 ? value.Substring(0, 50) : value);
				}
				else
					throw new CoreException("User.Login cannot be empty or null");
			}
		}
		
		/// <summary>
		/// Email address
		/// </summary>
		/// <remarks>
		/// The user's email address. This email address must be fully qualified.
		/// </remarks>
		/// <value>
		/// The user's email address.
		/// </value>
		public string Email
		{
			get
			{
				return email;
			}
			set
			{
				MarkModified();
				if (value == null || value.Length == 0)
					email = null;
				else
					email = (value.Length > 100 ? value.Substring(0, 100) : value);
			}
		}

		/// <summary>
		/// Internal identifier
		/// </summary>
		/// <remarks>
		/// The primary key of the user's database record is used as the internal identifier. This property
		/// is <c>0</c> for newly created or deleted user objects.  The <see cref="AnonymousUser"/> always 
		/// has an identifier of <c>-1</c>.
		/// </remarks>
		/// <value>
		/// The user's internal identifier.
		/// </value>
		public int ID
		{
			get
			{
				return id;
			}
		}

		/// <summary>
		/// Internal identifier
		/// </summary>
		/// <remarks>
		/// The internal identifier is the primary key used in the database.
		/// </remarks>
		/// <returns>
		/// The internal identifier of this user.
		/// </returns>
		public override int GetID()
		{
			return id;
		}


		/// <summary>
		/// Administrator flag
		/// </summary>
		/// <remarks>
		/// A user with a <c>true</c> administrator flag is automatically granted all privileges on all
		/// items in the system.
		/// </remarks>
		/// <value>
		/// The user's administrator flag.
		/// </value>
		public bool Administrator
		{
			get
			{
				return administrator;
			}
			set
			{
				MarkModified();
				User user = CurrentUser();
				if (user != null && user.administrator)
					administrator = value;
				else
					throw new CoreException("Only users with administrator flag set can modify administrator flags.");
			}
		}

		/// <summary>
		/// Create a principal
		/// </summary>
		/// <remarks>
		/// This method returns an IPrincipal for use within the .NET security context
		/// </remarks>
		/// <returns>a UserPrincipal object (which implements the IPrincipal interface).</returns>
		public UserPrincipal GetPrincipal()
		{
			return new UserPrincipal(this);
		}

		/// <summary>
		/// List of user's folders
		/// </summary>
		/// <remarks>
		/// This method returns a list of folders the user owns. Folders hold slideshows.
		/// </remarks>
		/// <returns>A SortedList containing folder names as keys and the internal identifiers of the folders
		/// as values</returns>
		public SortedList GetFolders()
		{
			if (folders == null)
			{
				folders = new SortedList();
				if (id > 0) 
					using (DBConnection conn = DBConnector.GetConnection())
					{
						Query query = new Query(conn,
							"SELECT ID,Title FROM Folders WHERE UserID={userid}");
						query.AddParam("userid", id);
						DataTable table = conn.SelectQuery(query);
						foreach (DataRow row in table.Rows)
							folders.Add(row["Title"], row["ID"]);
					}
			}
			return (folders == null ? null : (SortedList)folders.Clone());
		}

		/// <summary>
		/// Folder name
		/// </summary>
		/// <remarks>
		/// This method retrieves the name of a folder identified by its identifier.
		/// </remarks>
		/// <param name="folder">The identifier of the folder</param>
		/// <returns>The name of the folder, or <c>null</c> if the identifier is for a non-existant
		/// folder or a folder owned by another user</returns>
		public string GetFolderName(int folder)
		{
			if (folders == null)
				GetFolders();
			try
			{
				return (string)folders.GetKey(folders.IndexOfValue(folder));
			}
			catch
			{
				return null;
			}
		}

		/// <summary>
		/// Creates a new folder 
		/// </summary>
		/// <remarks>
		/// This method creates a new folder for this user. Folders hold slideshows.
		/// </remarks>
		/// <exception cref="CoreException">Thrown if the user object has not yet
		/// been saved to the database</exception>
		/// <exception cref="CoreException">Thrown if no folder title is specified.</exception>
		/// <param name="title">The title of the new folder</param>
		/// <returns>The internal identifier of the newly created folder</returns>
		public int CreateFolder(string title)
		{
			if (id > 0)
			{
				if (title != null && title.Length > 0)
					using (DBConnection conn = DBConnector.GetConnection())
					{
						string newtitle = conn.Encode(title.Length > 50 ? title.Substring(0, 50) : title);
					
						Query query = new Query(conn,
							@"SELECT COUNT(*) FROM Folders WHERE UserID={userid} AND Title={title}");
						query.AddParam("userid", id);
						query.AddParam("title", newtitle);
						int result = conn.DataToInt(conn.ExecScalar(query), 0);
						if (result != 0)
							throw new CoreException("A folder with this title already exists.");

						query = new Query(conn,
							@"INSERT INTO Folders (UserID,Title) VALUES ({userid},{title})");
						query.AddParam("userid", id);
						query.AddParam("title", newtitle);
						conn.ExecQuery(query);
						folders = null;
						return conn.LastIdentity("Folders");
					}
				else
					throw new CoreException("A title must be specified for a new folder.");
			}
			else
				throw new CoreException("User must be saved before folders can be created.");
		}

		/// <summary>
		/// Remove folder
		/// </summary>
		/// <remarks>
		/// This method removes one of the user's folders. If the folder is not empty,
		/// slideshows in that folder will be moved to the default folder.</remarks>
		/// <param name="folder">The internal identifier of the folder to remove.</param>
		/// <exception cref="CoreException">Thrown if the specified folder does not exist or
		/// belongs to a different user.</exception>
		/// <exception cref="CoreException">Thrown if this method is called on a newly
		/// created user object.</exception>
		public void DeleteFolder(int folder)
		{
			if (id > 0)
			{
				int result;
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"UPDATE Slideshows SET FolderID=0 WHERE UserID={userid} AND FolderID={folderid}");
					query.AddParam("userid", id);
					query.AddParam("folderid", folder);
					conn.ExecQuery(query);

					query = new Query(conn,
						@"DELETE FROM Folders WHERE UserID={userid} AND ID={folderid}");
					query.AddParam("userid", id);
					query.AddParam("folderid", folder);
					result = conn.ExecQuery(query);

					folders = null;
				}
				if (result != 1)
					throw new CoreException("The specified folder does not exist or does not belong to this user.");
			}
			else
				throw new CoreException("User must be saved before folders can be removed.");
		}

		/// <summary>
		/// Rename folder
		/// </summary>
		/// <remarks>
		/// This method renames one of the user's folders.
		/// </remarks>
		/// <param name="folder">The internal identifier of the folder to rename.</param>
		/// <param name="title">The new title of the folder.</param>
		/// <exception cref="CoreException">Thrown if the specified folder does not exist
		/// or does not belong to the current user</exception>
		/// <exception cref="CoreException">Thrown if this method is called on a newly
		/// created user object.</exception>
		public void RenameFolder(int folder, string title)
		{
			if (id > 0)
			{
				int result;
				using (DBConnection conn = DBConnector.GetConnection())
				{
					string newtitle = conn.Encode(title.Length > 50 ? title.Substring(0, 50) : title);

					Query query = new Query(conn,
						@"SELECT COUNT(*) FROM Folders 
						WHERE UserID={userid} AND Title={title} AND ID<>{id}");
					query.AddParam("userid", id);
					query.AddParam("title", newtitle);
					query.AddParam("id", folder);
					result = conn.DataToInt(conn.ExecScalar(query), 0);
					if (result != 0)
						throw new CoreException("A folder with this title already exists.");

					query = new Query(conn,
						@"UPDATE Folders SET Title={title} WHERE UserID={userid} AND ID={id}");
					query.AddParam("userid", id);
					query.AddParam("title", newtitle);
					query.AddParam("id", folder);
					result = conn.ExecQuery(query);

					folders = null;
				}
				if (result != 1)
					throw new CoreException("The specified folder does not exist or does not belong to this user.");
			}
			else
				throw new CoreException("User must be saved before folders can be renamed.");
		}

		/// <summary>
		/// Retrieve number of slideshows per folder
		/// </summary>
		/// <remarks>
		/// This utility function gives more efficient access to the number of slideshows
		/// in a folder than actually retrieving the complete list of slideshows.
		/// </remarks>
		/// <returns>A hashtable with internal identifiers of folders as keys and the number
		/// of slideshows as values.</returns>
		public Hashtable GetFolderSlideCount()
		{
			Hashtable folders = new Hashtable();
			if (id > 0) 
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"SELECT FolderID,COUNT(ID) AS C FROM Slideshows 
						WHERE UserID={userid} GROUP BY FolderID");
					query.AddParam("userid", id);
					DataTable table = conn.SelectQuery(query);
					foreach (DataRow row in table.Rows)
						folders.Add(row["FolderID"], row["C"]);
				}
			return folders;
		}

		/// <summary>
		/// User's privileges on given object
		/// </summary>
		/// <remarks>
		/// This method determines what the effective privileges of this user on a given
		/// access controlled object are. All roles of the user and all user groups
		/// the user is a member of are considered.
		/// </remarks>
		/// <param name="obj">The object to determine privileges for, or <c>null</c>
		/// if only system-level privileges should be determined.</param>
		/// <returns>A combined set of granted privileges</returns>
		public Privilege GetPrivileges(IAccessControlled obj)
		{
			if (obj == null)
				throw new CoreException("IAccessControlled parameter must not be null.");
			Privilege effective = Privilege.None;
			IAccessControl ac = obj.GetAccessControl();
			if (ac != null)
			{
				Privilege granted = Privilege.None;
				Privilege denied = Privilege.None;
				UserGroup[] groups = UserGroup.FindUser(this);
				if (groups != null)
					foreach (UserGroup group in groups)
					{
						granted |= ac.GetGrantedPrivileges(group);
						denied |= ac.GetDeniedPrivileges(group);
					}
				effective = granted & ~denied;
				effective = effective | ac.GetGrantedPrivileges(this);
				effective = effective & ~ac.GetDeniedPrivileges(this);
			}
			return effective;
		}

		/// <summary>
		/// User's system-level privileges
		/// </summary>
		/// <remarks>
		/// This method determines what the effective system-level privileges of this user are. 
		/// All roles of the user and all user groups the user is a member of are considered.
		/// </remarks>
		/// <returns>A combined set of granted privileges</returns>
		public Privilege GetPrivileges()
		{
			return GetPrivileges(SystemAccess.GetSystemAccess());
		}

		/// <summary>
		/// Current user
		/// </summary>
		/// <remarks>
		/// This method returns the currently active user or <c>null</c> if no user has been activated.
		/// </remarks>
		/// <returns>The current active user.</returns>
		public static User CurrentUser()
		{
			return Thread.CurrentPrincipal.Identity as User;
		}

		/// <summary>
		/// Check if a privilege is granted
		/// </summary>
		/// <remarks>
		/// This method checks if a certain <see cref="Privilege"/> or set of privileges is granted to
		/// a <see cref="User"/> on a specified object. If system-level privileges are checked, the 
		/// object can be set to <c>null</c>, which then defaults to <see cref="SystemAccess.GetSystemAccess"/>.
		/// Privileges of anonymous users can be checked by specifying <c>null</c> as the user.
		/// If a privilege is granted on the system-level, it is automatically granted for all objects in the system.
		/// </remarks>
		/// <param name="priv">The privilege or set of privileges to check for</param>
		/// <param name="obj">The object the privileges need to be granted for, or <c>null</c>
		/// to check system-level privileges</param>
		/// <param name="user">The user to check the privileges for, or <c>null</c> to
		/// check privileges for an anonymous user</param>
		/// <returns><c>true</c> if the privileges are granted, <c>false</c> otherwise</returns>
		public static bool HasPrivilege(Privilege priv, IAccessControlled obj, User user)
		{
			IAccessControlled o = (obj != null ? obj : SystemAccess.GetSystemAccess());
			if ((o.GetRelevantPrivileges() & priv) != priv)
				throw new CoreException("Irrelevant privilege required.");
			if (user == null)
				return (AnonymousUser.User.GetPrivileges(o) & priv) == priv;
			else
			{
				if (user.administrator || 
					(user.id != 0 && o.GetOwner() == user.id))
					return true;
				Privilege granted = user.GetPrivileges(o);
				return (granted & priv) == priv;
			}
		}

		/// <summary>
		/// Check if a privilege is granted
		/// </summary>
		/// <remarks>
		/// This method checks if a certain <see cref="Privilege"/> or set of privileges is granted to
		/// the current <see cref="User"/> on a specified object. If system-level privileges are checked, the 
		/// object can be set to <c>null</c>, which then defaults to <see cref="SystemAccess.GetSystemAccess"/>.
		/// </remarks>
		/// <param name="priv">The privilege or set of privileges to check for</param>
		/// <param name="obj">The object the privileges need to be granted for, or <c>null</c>
		/// to check system-level privileges</param>
		/// <returns><c>true</c> if the privileges are granted, <c>false</c> otherwise</returns>
		public static bool HasPrivilege(Privilege priv, IAccessControlled obj)
		{
			return HasPrivilege(priv, obj, CurrentUser());
		}

		/// <summary>
		/// Check if a system-level privilege is granted
		/// </summary>
		/// <remarks>
		/// This method checks if a certain system-level <see cref="Privilege"/> or set of privileges is granted to
		/// the current user.
		/// </remarks>
		/// <param name="priv">The privilege or set of privileges to check for</param>
		/// <returns><c>true</c> if the privileges are granted, <c>false</c> otherwise</returns>
		public static bool HasPrivilege(Privilege priv)
		{
			return HasPrivilege(priv, null, CurrentUser());
		}

		/// <summary>
		/// Check if a system-level privilege is granted
		/// </summary>
		/// <remarks>
		/// This method checks if a certain system-level <see cref="Privilege"/> or set of privileges is granted to
		/// a <see cref="User"/>.
		/// Privileges of anonymous users can be checked by specifying <c>null</c> as the user.
		/// </remarks>
		/// <param name="priv">The privilege or set of privileges to check for</param>
		/// <param name="user">The user to check the privileges for, or <c>null</c> to
		/// check privileges for an anonymous user</param>
		/// <returns><c>true</c> if the privileges are granted, <c>false</c> otherwise</returns>
		public static bool HasPrivilege(Privilege priv, User user)
		{
			return HasPrivilege(priv, null, user);
		}

		/// <summary>
		/// Check if a privilege is granted and fail if not
		/// </summary>
		/// <remarks>
		/// This method works similar to <see cref="HasPrivilege(Privilege, IAccessControlled, User)"/>, except that instead of returning a
		/// boolean value, this method throws an exception if the requested privileges are not granted.
		/// The method checks if a certain <see cref="Privilege"/> or set of privileges is granted to
		/// a <see cref="User"/> on a specified object. If system-level privileges are checked, the 
		/// object can be set to <c>null</c>, which then defaults to <see cref="SystemAccess.GetSystemAccess"/>.
		/// Privileges of anonymous users can be checked by specifying <c>null</c> as the user.
        /// <seealso cref="HasPrivilege(Privilege, IAccessControlled, User)"/>
		/// </remarks>
		/// <param name="priv">The privilege or set of privileges to check for</param>
		/// <param name="obj">The object the privileges need to be granted for, or <c>null</c>
		/// to check system-level privileges</param>
		/// <param name="user">The user to check the privileges for, or <c>null</c> to
		/// check privileges for an anonymous user</param>
		/// <exception cref="PrivilegeException">Thrown if the privileges are not granted.</exception>
		public static void RequirePrivilege(Privilege priv, IAccessControlled obj, User user)
		{
			IAccessControlled o = (obj != null ? obj : SystemAccess.GetSystemAccess());
			if (!User.HasPrivilege(priv, o, user))
				throw new PrivilegeException("Insufficient privileges.", priv, 
					(user != null ? user.GetPrivileges(o) : Privilege.None));
		}

		/// <summary>
		/// Check if a privilege is granted and fail if not
		/// </summary>
		/// <remarks>
        /// This method works similar to <see cref="HasPrivilege(Privilege, IAccessControlled)"/>, except that instead of returning a
		/// boolean value, this method throws an exception if the requested privileges are not granted.
		/// The method checks if a certain <see cref="Privilege"/> or set of privileges is granted to
		/// the current <see cref="User"/> on a specified object. If system-level privileges are checked, the 
		/// object can be set to <c>null</c>, which then defaults to <see cref="SystemAccess.GetSystemAccess"/>.
        /// <seealso cref="HasPrivilege(Privilege, IAccessControlled)"/>
		/// </remarks>
		/// <param name="priv">The privilege or set of privileges to check for</param>
		/// <param name="obj">The object the privileges need to be granted for, or <c>null</c>
		/// to check system-level privileges</param>
		public static void RequirePrivilege(Privilege priv, IAccessControlled obj)
		{
			RequirePrivilege(priv, obj, CurrentUser());
		}

		/// <summary>
		/// Check if a system-level privilege is granted and fail if not
		/// </summary>
		/// <remarks>
        /// This method works similar to <see cref="HasPrivilege(Privilege)"/>, except that instead of returning a
		/// boolean value, this method throws an exception if the requested privileges are not granted.
		/// The method checks if a certain system-level <see cref="Privilege"/> or set of privileges is granted to
		/// the current <see cref="User"/>.
        /// <seealso cref="HasPrivilege(Privilege)"/>
		/// </remarks>
		/// <param name="priv">The privilege or set of privileges to check for</param>
		public static void RequirePrivilege(Privilege priv)
		{
			RequirePrivilege(priv, null, CurrentUser());
		}

		/// <summary>
		/// Check if a system-level privilege is granted and fail if not
		/// </summary>
		/// <remarks>
        /// This method works similar to <see cref="HasPrivilege(Privilege, User)"/>, except that instead of returning a
		/// boolean value, this method throws an exception if the requested privileges are not granted.
		/// The method checks if a certain system-level <see cref="Privilege"/> or set of privileges is granted to
		/// a <see cref="User"/>.
		/// Privileges of anonymous users can be checked by specifying <c>null</c> as the user.
        /// <seealso cref="HasPrivilege(Privilege, User)"/>
		/// </remarks>
		/// <param name="priv">The privilege or set of privileges to check for</param>
		/// <param name="user">The user to check the privileges for, or <c>null</c> to
		/// check privileges for an anonymous user</param>
		public static void RequirePrivilege(Privilege priv, User user)
		{
			RequirePrivilege(priv, null, user);
		}

		/// <summary>
		/// Check for privileges on any object of a certain type
		/// </summary>
		/// <remarks>
		/// This method can be used to determine if a user has certain privileges on any object
		/// of a certain type without having to access those objects directly. For example, to see
		/// if a user has modification privileges for at least one collection, use 
		/// <c>User.HasPrivilegeOnAny('C', someuser, Privilege.ModifyCollection);</c>. This can
		/// be useful to build and/or activate menu items.
		/// </remarks>
		/// <param name="objecttype">A character identifying the object type to test</param>
		/// <param name="priv">The privilege or privileges to test</param>
		/// <param name="user">The user to test</param>
		/// <returns><c>true</c> if the user has the given privileges for at least one object
		/// of the given type, <c>false</c> otherwise</returns>
		public static bool HasPrivilegeOnAny(char objecttype, User user, params Privilege[] priv)
		{
			// Handle default cases
			if (user == null || priv == null || priv.Length == 0)
				return false;
			if (user.administrator)
				return true;
			// Build list of ids of groups that the user is member of
			UserGroup[] groups = UserGroup.FindUser(user);
			int[] groupids;
			if (groups == null || groups.Length == 0)
				groupids = new int[] { 0 };
			else
			{
				groupids = new int[groups.Length];
				for (int i = 0; i < groups.Length; i++)
					groupids[i] = groups[i].ID;
			}
			// Build list of required privilege checks
			string[] groupclauses = new string[priv.Length];
			string[] userclauses = new string[priv.Length];
			for (int i = 0; i < priv.Length; i++)
			{
				groupclauses[i] = String.Format(@"(GrantPriv&{0}={0} AND ObjectID NOT IN 
(SELECT ObjectID FROM AccessControl WHERE 
(ObjectType={{objecttype}} AND (UserID={{userid}} OR GroupID IN {{groupids}}) AND (DenyPriv&{0}={0})))
)", (int)priv[i]);
				userclauses[i] = String.Format(@"(GrantPriv&{0}={0} AND ObjectID NOT IN 
(SELECT ObjectID FROM AccessControl WHERE 
(ObjectType={{objecttype}} AND UserID={{userid}} AND (DenyPriv&{0}={0})))
)", (int)priv[i]);
			}

			// run query on AccessControl
			using (DBConnection conn = DBConnector.GetConnection())
			{
				// check if privilege is granted for user group and not denied for user group or user
				// these following two queries could be done with a WHERE EXISTS clause, but
				// that it not implemented in MySQL yet
				Query query = new Query(conn,
					@"SELECT TOP 1 ObjectID FROM AccessControl 
					WHERE (ObjectType={objecttype} AND GroupID IN {groupids}) AND (" + 
					String.Join(" OR ", groupclauses) + ")");
				query.AddParam("objecttype", objecttype);
				query.AddParam("userid", user.id);
				query.AddParam("groupids", groupids);
				if (conn.DataToInt(conn.ExecScalar(query), 0) != 0)
					return true;
	
				// check if privilege is granted for user and not denied for user
				query = new Query(conn,
					@"SELECT TOP 1 ObjectID FROM AccessControl 
					WHERE (ObjectType={objecttype} AND UserID={userid}) AND (" +
					String.Join(" OR ", userclauses) + ")");
					
				query.AddParam("objecttype", objecttype);
				query.AddParam("userid", user.id);
				return conn.DataToInt(conn.ExecScalar(query), 0) != 0;
			}
		}

		/// <summary>
		/// Check for privileges on any object of a certain type
		/// </summary>
		/// <remarks>
		/// This method can be used to determine if the current user has certain privileges on any object
		/// of a certain type without having to access those objects directly. For example, to see
		/// if the current user has modification privileges for at least one collection, use 
		/// <c>User.HasPrivilegeOnAny('C', someuser, Privilege.ModifyCollection);</c>. This can
		/// be useful to build and/or activate menu items.
		/// </remarks>
		/// <param name="objecttype">A character identifying the object type to test</param>
		/// <param name="priv">The privilege or privileges to test</param>
		/// <returns><c>true</c> if the current user has the given privileges for at least one object
		/// of the given type, <c>false</c> otherwise</returns>
		public static bool HasPrivilegeOnAny(char objecttype, params Privilege[] priv)
		{
			return HasPrivilegeOnAny(objecttype, User.CurrentUser(), priv);
		}

		/// <summary>
		/// Retrieve Favorite Images
		/// </summary>
		/// <remarks>
		/// For every user, a list of image identifiers of favorite images is kept.
		/// </remarks>
		/// <returns>An array of image identifiers.</returns>
		/// <exception cref="CoreException">Thrown if this method is called on a newly
		/// created user object.</exception>
		public ImageIdentifier[] GetFavoriteImages()
		{
			if (id == 0)
				throw new CoreException("User object must be committed to database first.");
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT ImageID,CollectionID FROM FavoriteImages WHERE UserID={userid}");
				query.AddParam("userid", id);
				DataTable table = conn.SelectQuery(query);
				ImageIdentifier[] result = new ImageIdentifier[table.Rows.Count];
				for (int i = 0; i < table.Rows.Count; i++)
				{
					DataRow row = table.Rows[i];
					result[i].ID = conn.DataToInt(row["ImageID"], 0);
					result[i].CollectionID = conn.DataToInt(row["CollectionID"], 0);
				}
				return result;
			}
		}

		/// <summary>
		/// Delete Favorite Images
		/// </summary>
		/// <remarks>
		/// For every user, a list of image identifiers of favorite images is kept. This method
		/// deletes all specified favorite image entries.
		/// </remarks>
		/// <param name="ids">The internal identifiers of the favorite images to be removed.</param>
		/// <exception cref="CoreException">Thrown if this method is called on a newly
		/// created user object.</exception>
		public void DeleteFavoriteImages(params ImageIdentifier[] ids)
		{
			if (id == 0)
				throw new CoreException("User object must be committed to database first.");
			if (ids == null || ids.Length == 0)
				return;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				string[] wheres = new string[ids.Length];
				for (int i = 0; i < ids.Length; i++)
					wheres[i] = String.Format("(ImageID={0} AND CollectionID={1})", 
						ids[i].ID, ids[i].CollectionID);
				Query query = new Query(conn,
					@"DELETE FROM FavoriteImages WHERE UserID={userid} AND ({@where})");
				query.AddParam("userid", id);
				query.AddParam("@where", string.Join(" OR ", wheres));
				conn.ExecQuery(query);
			}
		}

		/// <summary>
		/// Add Favorite Images
		/// </summary>
		/// <remarks>
		/// For every user, a list of image identifiers of favorite images is kept. This method
		/// adds new favorite image entries.
		/// </remarks>
		/// <param name="ids">The internal identifiers of the favorite images to be added.</param>
		/// <exception cref="CoreException">Thrown if this method is called on a newly
		/// created user object.</exception>
		public void AddFavoriteImages(params ImageIdentifier[] ids)
		{
			if (id == 0)
				throw new CoreException("User object must be committed to database first.");
			using (DBConnection conn = DBConnector.GetConnection())
				foreach (ImageIdentifier image in ids)
				{
					try
					{
						Query query = new Query(conn,
							@"INSERT INTO FavoriteImages (UserID,ImageID,CollectionID) 
							VALUES ({userid},{imageid},{collectionid})");
						query.AddParam("userid", id);
						query.AddParam("imageid", image.ID);
						query.AddParam("collectionid", image.CollectionID);
						conn.ExecQuery(query);
					}
					catch (Exception)
					{
					}
				}
		}

		/// <summary>
		/// Compare two user objects
		/// </summary>
		/// <remarks>
		/// This method compares two user objects by their <c>FullName</c> properties.
		/// </remarks>
		/// <param name="obj">Another user object to compare this one to</param>
		/// <returns>The result of the string comparison of the objects' <c>FullName</c> 
		/// properties.</returns>
		public int CompareTo(object obj)
		{
			User u = obj as User;
			if (u != null)
			{
				if (u.id == this.id)
					return 0;
				else
					return this.FullName.CompareTo(u.FullName);
			}
			throw new Exception("User object expected");
		}

		/// <summary>
		/// Full user name
		/// </summary>
		/// <remarks>
		/// This method returns the value of the <c>FullName</c> property.
		/// </remarks>
		/// <returns>The full name of the user</returns>
		public string GetName()
		{
			return String.Format("{0} ({1})", Login, FullName);
		}

		/// <summary>
		/// Type identifier
		/// </summary>
		/// <remarks>
		/// The type identifier must be unique in the system.
		/// </remarks>
		/// <returns><c>'U'</c></returns>
		public char GetObjectTypeIdentifier()
		{
			return 'U';
		}
	}

	/// <summary>
	/// The anonymous user
	/// </summary>
	/// <remarks>
	/// This class represents the anonymous user account. It always has an ID of <c>-1</c>.
	/// There can be only one instance of this class, which can be accessed using the <see cref="User"/>
	/// method.
	/// </remarks>
	public class AnonymousUser:
		User
	{
		/// <summary>
		/// The only instance of this class
		/// </summary>
		private static AnonymousUser anonymous = new AnonymousUser();

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// This constructor sets the internal identifier to <c>-1</c> and the name to <c>"Anonymous"</c>.
		/// It is private to guarantee no second instance of this class is created.
		/// </remarks>
		private AnonymousUser():
			base(false)
		{
			id = -1;
			name = "Guest User";
		}

		/// <summary>
		/// Get the anonymous user object
		/// </summary>
		/// <remarks>
		/// This class always has one and only one instance, which can be accessed via this read-only property.
		/// </remarks>
		/// <value>
		/// The anonymous user object
		/// </value>
		public static AnonymousUser User
		{
			get
			{
				return anonymous;
			}
		}

		/// <summary>
		/// Checks authentication status
		/// </summary>
		/// <remarks>
		/// The anonymous user object is always authenticated
		/// </remarks>
		/// <value><c>true</c></value>
		public override bool IsAuthenticated
		{
			get
			{
				return true;
			}
		}
	}

	/// <summary>
	/// Principal for User objects within the .NET security context
	/// </summary>
	/// <remarks>
	/// This class allows <see cref="User"/> objects to be used within the .NET security context, especially
	/// as values for the Thread.CurrentPrincipal property.
	/// </remarks>
	public class UserPrincipal:
		IPrincipal
	{
		/// <summary>
		/// Identified User
		/// </summary>
		/// <remarks>
		/// The <see cref="User"/> identified by this principal object.
		/// </remarks>
		private User identity;

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Creates a new principal object for a specified <see cref="User"/>.
		/// </remarks>
		/// <param name="user">The user to create the UserPrincipal for.</param>
		public UserPrincipal(User user)
		{
			identity = user;
		}

		/// <summary>
		/// Identified User.
		/// </summary>
		/// <remarks>
		/// Returns the <see cref="User"/> object (which implements the IIdentity interface) for this UserPrincipal.
		/// </remarks>
		/// <value>
		/// The user identified by this principal.
		/// </value>
		public IIdentity Identity
		{
			get
			{
				return identity;
			}
		}

		/// <summary>
		/// Check role membership
		/// </summary>
		/// <remarks>
        /// This method always returns true. To check for privileges, use <see cref="User.HasPrivilege(Privilege, IAccessControlled, User)"/> or
        /// <see cref="User.RequirePrivilege(Privilege, IAccessControlled, User)"/>.
		/// </remarks>
		/// <param name="role">Name of the role membership to check</param>
		/// <returns><c>true</c></returns>
		public bool IsInRole(string role)
		{
			return true;
		}
	}

#if DEMO
#warning Compiling for Demo site

	public class DemoUserCleanup
	{
		public static void Run()
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			int[] ids;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				// hardcoded IDs are for special accounts in JMU's demo site installation
				Query query = new Query(conn,
					@"SELECT ID FROM Users 
					WHERE Administrator=0 AND ID NOT IN (8, 22) 
					AND LastAuthenticated<{fourweeksago}");
				query.AddParam("fourweeksago", DateTime.Now - TimeSpan.FromDays(28));
				ids = conn.TableToArray(conn.SelectQuery(query));
			}
			if (ids != null && ids.Length > 0)
				foreach (User user in User.GetByID(ids))
					user.Delete();
		}
	}

#endif

}
