//
// System.Web.SessionState.SesionStateModule
//
// Authors:
//	Gonzalo Paniagua Javier (gonzalo@ximian.com)
//	Stefan Görling (stefan@gorling.se)
//	Jackson Harper (jackson@ximian.com)
//      Marek Habersack (grendello@gmail.com)
//
// Copyright (C) 2002-2006 Novell, Inc (http://www.novell.com)
// (C) 2003 Stefan Görling (http://www.gorling.se)
//
// 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.
//

using System.Collections.Specialized;
using System.ComponentModel;
using System.Web.Configuration;
using System.Web.Caching;
using System.Web.Util;
using System.Security.Permissions;
using System.Threading;
using System.Configuration;
using System.Diagnostics;

namespace System.Web.SessionState
{	
	// CAS - no InheritanceDemand here as the class is sealed
	[AspNetHostingPermission (SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
	public sealed class SessionStateModule : IHttpModule
	{
		class CallbackState
		{
			public readonly HttpContext Context;
			public readonly AutoResetEvent AutoEvent;
			public readonly string SessionId;
			public readonly bool IsReadOnly;

			public CallbackState (HttpContext context, AutoResetEvent e, string sessionId, bool isReadOnly) {
				this.Context = context;
				this.AutoEvent = e;
				this.SessionId = sessionId;
				this.IsReadOnly = isReadOnly;
			}
		}

		internal const string HeaderName = "AspFilterSessionId";
		internal const string CookielessFlagName = "_SessionIDManager_IsCookieLess";

		static readonly object startEvent = new object ();
		static readonly object endEvent = new object ();
		
		SessionStateSection config;

		SessionStateStoreProviderBase handler;
		ISessionIDManager idManager;
		bool supportsExpiration;

		HttpApplication app;

		// Store state
		bool storeLocked;
		TimeSpan storeLockAge;
		object storeLockId;
		SessionStateActions storeSessionAction;
		bool storeIsNew;
		
		// Session state
		SessionStateStoreData storeData;
		HttpSessionStateContainer container;

		// config
		TimeSpan executionTimeout;
		//int executionTimeoutMS;

		EventHandlerList events = new EventHandlerList ();
		
		public event EventHandler Start {
			add { events.AddHandler (startEvent, value); }
			remove { events.RemoveHandler (startEvent, value); }
		}

		// This event is public, but only Session_[On]End in global.asax will be invoked if present.
		public event EventHandler End {
			add { events.AddHandler (endEvent, value); }
			remove { events.RemoveHandler (endEvent, value); }
		}

		[SecurityPermission (SecurityAction.Demand, UnmanagedCode = true)]
		public SessionStateModule () {
		}

		public void Dispose () {
			app.BeginRequest -= new EventHandler (OnBeginRequest);
			app.AcquireRequestState -= new EventHandler (OnAcquireRequestState);
			app.ReleaseRequestState -= new EventHandler (OnReleaseRequestState);
			app.EndRequest -= new EventHandler (OnEndRequest);
			handler.Dispose ();
		}

		[EnvironmentPermission (SecurityAction.Assert, Read = "MONO_XSP_STATIC_SESSION")]
		public void Init (HttpApplication app)
		{
			config = (SessionStateSection) WebConfigurationManager.GetSection ("system.web/sessionState");

			ProviderSettings settings;
			switch (config.Mode) {
				case SessionStateMode.Custom:
					settings = config.Providers [config.CustomProvider];
					if (settings == null)
						throw new HttpException (String.Format ("Cannot find '{0}' provider.", config.CustomProvider));
					break;
				case SessionStateMode.Off:
					return;
				case SessionStateMode.InProc:
					settings = new ProviderSettings (null, typeof (SessionInProcHandler).AssemblyQualifiedName);
					break;

				case SessionStateMode.SQLServer:
					settings = new ProviderSettings (null, typeof (SessionSQLServerHandler).AssemblyQualifiedName);
					break;

				case SessionStateMode.StateServer:
					settings = new ProviderSettings (null, typeof (SessionStateServerHandler).AssemblyQualifiedName);
					break;

				default:
					throw new NotImplementedException (String.Format ("The mode '{0}' is not implemented.", config.Mode));
			
			}

			handler = (SessionStateStoreProviderBase) ProvidersHelper.InstantiateProvider (settings, typeof (SessionStateStoreProviderBase));

			if (String.IsNullOrEmpty(config.SessionIDManagerType)) {
				idManager = new SessionIDManager ();
			} else {
				Type idManagerType = HttpApplication.LoadType (config.SessionIDManagerType, true);
				idManager = (ISessionIDManager)Activator.CreateInstance (idManagerType);
			}

			try {				
				idManager.Initialize ();
			} catch (Exception ex) {
				throw new HttpException ("Failed to initialize session ID manager.", ex);
			}

			supportsExpiration = handler.SetItemExpireCallback (OnSessionExpired);
			HttpRuntimeSection runtime = HttpRuntime.Section;
			executionTimeout = runtime.ExecutionTimeout;
			//executionTimeoutMS = executionTimeout.Milliseconds;

			this.app = app;

			app.BeginRequest += new EventHandler (OnBeginRequest);
			app.AcquireRequestState += new EventHandler (OnAcquireRequestState);
			app.ReleaseRequestState += new EventHandler (OnReleaseRequestState);
			app.EndRequest += new EventHandler (OnEndRequest);
		}

		internal static bool IsCookieLess (HttpContext context, SessionStateSection config) {
			if (config.Cookieless == HttpCookieMode.UseCookies)
				return false;
			if (config.Cookieless == HttpCookieMode.UseUri)
				return true;
			object cookieless = context.Items [CookielessFlagName];
			if (cookieless == null)
				return false;
			return (bool) cookieless;
		}

		void OnBeginRequest (object o, EventArgs args)
		{
			HttpApplication application = (HttpApplication) o;
			HttpContext context = application.Context;
			string file_path = context.Request.FilePath;
			string base_path = VirtualPathUtility.GetDirectory (file_path);
			string id = UrlUtils.GetSessionId (base_path);

			if (id == null)
				return;

			string new_path = UrlUtils.RemoveSessionId (base_path, file_path);
			context.Request.SetFilePath (new_path);
			context.Request.SetHeader (HeaderName, id);
			context.Response.SetAppPathModifier (id);
		}

		void OnAcquireRequestState (object o, EventArgs args) {
			Trace.WriteLine ("SessionStateModule.OnAcquireRequestState (hash " + this.GetHashCode ().ToString ("x") + ")");
			HttpApplication application = (HttpApplication) o;
			HttpContext context = application.Context;

			if (!(context.Handler is IRequiresSessionState)) {
				Trace.WriteLine ("Handler (" + context.Handler + ") does not require session state");
				return;
			}
			bool isReadOnly = (context.Handler is IReadOnlySessionState);

			bool supportSessionIDReissue;
			if (idManager.InitializeRequest (context, false, out supportSessionIDReissue))
				return; // Redirected, will come back here in a while
			string sessionId = idManager.GetSessionID (context);

			handler.InitializeRequest (context);

			storeData = GetStoreData (context, sessionId, isReadOnly);

			storeIsNew = false;
			if (storeData == null && !storeLocked) {
				storeIsNew = true;
				sessionId = idManager.CreateSessionID (context);
				Trace.WriteLine ("New session ID allocated: " + sessionId);
				bool redirected;
				bool cookieAdded;
				idManager.SaveSessionID (context, sessionId, out redirected, out cookieAdded);
				if (redirected) {
					if (supportSessionIDReissue)
						handler.CreateUninitializedItem (context, sessionId, (int)config.Timeout.TotalMinutes);
					context.Response.End ();
					return;
				}
				else
					storeData = handler.CreateNewStoreData (context, (int)config.Timeout.TotalMinutes);
			}
			else if (storeData == null && storeLocked) {
				WaitForStoreUnlock (context, sessionId, isReadOnly);
			}
			else if (storeData != null &&
				 !storeLocked &&
				 storeSessionAction == SessionStateActions.InitializeItem &&
				 IsCookieLess (context, config)) {
				storeData = handler.CreateNewStoreData (context, (int)config.Timeout.TotalMinutes);
			}

			container = CreateContainer (sessionId, storeData, storeIsNew, isReadOnly);
			SessionStateUtility.AddHttpSessionStateToContext (app.Context, container);
			if (storeIsNew) {
				OnSessionStart ();
				HttpSessionState hss = app.Session;

				if (hss != null)
					storeData.Timeout = hss.Timeout;
			}

			// Whenever a container is abandoned, we temporarily disable the expire call back.
			// So in this case we are quite sure we have a brand new container, so we make sure it works again.
			supportsExpiration = handler.SetItemExpireCallback (OnSessionExpired);
		}

		void OnReleaseRequestState (object o, EventArgs args) {

			Trace.WriteLine ("SessionStateModule.OnReleaseRequestState (hash " + this.GetHashCode ().ToString ("x") + ")");

			HttpApplication application = (HttpApplication) o;
			HttpContext context = application.Context;
			if (!(context.Handler is IRequiresSessionState))
				return;

			Trace.WriteLine ("\tsessionId == " + container.SessionID);
			Trace.WriteLine ("\trequest path == " + context.Request.FilePath);
			Trace.WriteLine ("\tHandler (" + context.Handler + ") requires session state");
			try {
				if (!container.IsAbandoned) {
					Trace.WriteLine ("\tnot abandoned");
					if (!container.IsReadOnly) {
						Trace.WriteLine ("\tnot read only, storing and releasing");
						handler.SetAndReleaseItemExclusive (context, container.SessionID, storeData, storeLockId, storeIsNew);
					}
					else {
						Trace.WriteLine ("\tread only, releasing");
						handler.ReleaseItemExclusive (context, container.SessionID, storeLockId);
					}
					handler.ResetItemTimeout (context, container.SessionID);
				}
				else {
					handler.RemoveItem (context, container.SessionID, storeLockId, storeData);
					handler.ReleaseItemExclusive (context, container.SessionID, storeLockId);
					if (supportsExpiration)
						// Make sure the expiration handler is not called after we will have raised
						// the session end event.
						handler.SetItemExpireCallback (null);
					SessionStateUtility.RaiseSessionEnd (container, this, args);
				}
				SessionStateUtility.RemoveHttpSessionStateFromContext (context);
			}
			finally {
				container = null;
				storeData = null;
			}
		}

		void OnEndRequest (object o, EventArgs args) {
			if (handler == null)
				return;

			if (container != null)
				OnReleaseRequestState (o, args);

			HttpApplication application = o as HttpApplication;
			if (application == null)
				return;
			if (handler != null)
				handler.EndRequest (application.Context);
		}

		SessionStateStoreData GetStoreData (HttpContext context, string sessionId, bool isReadOnly) {
			SessionStateStoreData item;
			item = (isReadOnly) ?
				handler.GetItem (context,
								 sessionId,
								 out storeLocked,
								 out storeLockAge,
								 out storeLockId,
								 out storeSessionAction)
								 :
				handler.GetItemExclusive (context,
									  sessionId,
									  out storeLocked,
									  out storeLockAge,
									  out storeLockId,
									  out storeSessionAction);
			if (storeLockId == null)
				storeLockId = 0;

			return item;
		}

		void WaitForStoreUnlock (HttpContext context, string sessionId, bool isReadonly) {
			AutoResetEvent are = new AutoResetEvent (false);
			TimerCallback tc = new TimerCallback (StoreUnlockWaitCallback);
			CallbackState cs = new CallbackState (context, are, sessionId, isReadonly);
			using (Timer timer = new Timer (tc, cs, 500, 500)) {
				try {
					are.WaitOne (executionTimeout, false);
				}
				catch {
					storeData = null;
				}
			}
		}

		void StoreUnlockWaitCallback (object s) {
			CallbackState state = (CallbackState) s;

			SessionStateStoreData item = GetStoreData (state.Context, state.SessionId, state.IsReadOnly);

			if (item == null && storeLocked && (storeLockAge > executionTimeout)) {
				handler.ReleaseItemExclusive (state.Context, state.SessionId, storeLockId);
				storeData = null; // Create new state
				state.AutoEvent.Set ();
			}
			else if (item != null && !storeLocked) {
				storeData = item;
				state.AutoEvent.Set ();
			}
		}

		HttpSessionStateContainer CreateContainer (string sessionId, SessionStateStoreData data, bool isNew, bool isReadOnly) {
			if (data == null)
				return new HttpSessionStateContainer (
					sessionId, null, null, 0, isNew,
					config.Cookieless, config.Mode, isReadOnly);
			
			return new HttpSessionStateContainer (
				sessionId,
				data.Items,
				data.StaticObjects,
				data.Timeout,
				isNew,
				config.Cookieless,
				config.Mode,
				isReadOnly);
		}

		void OnSessionExpired (string id, SessionStateStoreData item) {
			SessionStateUtility.RaiseSessionEnd (
				CreateContainer (id, item, false, true),
				this, EventArgs.Empty);
		}

		void OnSessionStart () {
			EventHandler eh = events [startEvent] as EventHandler;
			if (eh != null)
				eh (this, EventArgs.Empty);
		}
	}
}