// Copyright (c) 2008, NTT DATA Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using TERASOLUNA.Fw.Common.Logging;
using TERASOLUNA.Fw.Web.Configuration.Page;

namespace TERASOLUNA.Fw.Web.HttpModule
{
    /// <summary>
    /// 画面遷移時にトークンの発行、トークンの確認処理を行う機能を提供します。
    /// </summary>
    /// <remarks>
    /// トークンの確認が有効に設定された画面で、トークンが確認できない場合は、
    /// <see cref="InvalidTransitionException"/> がスローされます。
    /// <para>web.config に &lt;httpModules&gt; 設定が必要です。</para>
    /// </remarks>
    public class TokenProcessorImpl : IHttpModule
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(TokenProcessorImpl));

        /// <summary>
        /// Session、HttpContext、Hidden フィールドでトークン保持のキーに使用する文字列。
        /// </summary>
        /// <remarks>
        /// この定数の値は "TERASOLUNA_Token" です。
        /// </remarks>
        protected static readonly string TOKEN_NAME = "TERASOLUNA_Token";

        /// <summary>
        /// クエリ文字列でトークン保持のキーに使用する文字列。
        /// </summary>
        /// <remarks>
        /// この定数の値は "__Token" です。
        /// </remarks>
        protected static readonly string QUERY_TOKEN_NAME = "__Token";

        /// <summary>
        /// 最新のトークン保持に使用する文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "TERASOLUNA_Current_Token" です。
        /// </remarks>
        protected static readonly string CURRENT_TOKEN_NAME = "TERASOLUNA_Current_Token";

        /// <summary>
        /// <see cref="TokenProcessorImpl"/> オブジェクトで使用されるリソース (メモリを除く) を解放します。 
        /// </summary>
        public virtual void Dispose()
        {
        }

        /// <summary>
        /// <see cref="TokenProcessorImpl"/> オブジェクトの作成時に初期化コードを呼び出します。 
        /// </summary>
        /// <remarks>
        /// <para><see cref="HttpApplication"/> オブジェクトの
        /// <see cref="HttpApplication.PreRequestHandlerExecute"/> イベントのイベントハンドラを登録します。</para>
        /// <para><see cref="HttpApplication"/> オブジェクトの <see cref="HttpApplication.EndRequest"/> イベントの
        /// イベントハンドラを登録します。</para>
        /// </remarks>
        /// <param name="context">
        /// ASP.NET アプリケーション内のすべてのアプリケーションオブジェクトに共通のメソッド、
        /// プロパティ、およびイベントへのアクセスを提供する <see cref="HttpApplication"/>。
        /// </param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")]
        public virtual void Init(HttpApplication context)
        {
            context.PreRequestHandlerExecute += new EventHandler(context_PreRequestHandlerExecute);
            context.EndRequest += new EventHandler(context_EndRequest);
        }

        /// <summary>
        /// トークンの生成を行い、トークンの確認が有効に設定されている場合はトークンチェックを行います。
        /// </summary>
        /// <param name="sender">イベントのソース。</param>
        /// <param name="e">イベントデータを格納している<see cref="EventArgs"/>。</param>
        /// <remarks>
        /// <para>セッションに格納している最新のトークン、もしくは各ページごとに保持している最新のトークンと
        /// リクエストから送信されたトークンを比較します。異なる値であった場合に<see cref="InvalidTransitionException"/>
        /// をスローします。</para>
        /// <para>リクエストからのトークンは、ポストバック時には ViewState から、リダイレクト時にはクエリ文字列から取得します。</para>
        /// <para>ページ設定ファイルで updateToken 属性が off であるページに対してポストバックがあった場合、セッションに格納する最新のトークン、ならびに
        /// ページごとに保持している最新のトークンは更新せず、ViewState には当該ページの最新のトークンを設定します。<br/>
        /// この設定はファイルダウンロード処理を含むページや、戻るボタン押下後のポストバック処理を許可する場合に利用します。
        /// </para>
        /// </remarks>
        /// <exception cref="SessionTimeoutException">
        /// 初回リクエスト、またはセッションがタイムアウトしている状態で、トークンの確認を行うページにアクセスされました。
        /// </exception>
        protected virtual void context_PreRequestHandlerExecute(object sender, EventArgs e)
        {
            HttpApplication application = (HttpApplication)sender;
            Page page = application.Context.Handler as Page;
            bool isKeepToken = false;

            // ハンドラがPageか判定
            if (page != null)
            {
                HttpRequest req = application.Request;
                HttpSessionState session = application.Session;
                HttpContext context = application.Context;

                // sessionからトークンを取得
                Hashtable tokens;
                tokens = (Hashtable)session[TOKEN_NAME];
                if (tokens == null)
                {
                    tokens = new Hashtable();
                }
                // トークンを取得する
                string currentSessionToken = (string)tokens[CURRENT_TOKEN_NAME];
                string pageSessionToken = (string)tokens[req.CurrentExecutionFilePath];

                // トークンを更新するかをチェック
                string newToken;
                if (req.Params["__VIEWSTATE"] != null && !PageConfiguration.IsUpdateTokenEnabled(WebUtils.GetCurrentPagePath()))
                {   // トークンの更新をしない場合(ポストバックでかつ、ページ設定ファイルのupdateToken属性がoffの場合)
                    // 新トークンには現在のトークンを設定
                    newToken = currentSessionToken;
                    isKeepToken = true;
                }
                else
                {   // トークンの更新処理
                    // 新しいトークンを生成
                    newToken = CreateNewToken(currentSessionToken);
                    // セッションのトークン設定
                    tokens[req.CurrentExecutionFilePath] = newToken;
                    tokens[CURRENT_TOKEN_NAME] = newToken;
                    session[TOKEN_NAME] = tokens;

                    if (_log.IsDebugEnabled)
                    {
                        _log.Debug(string.Format(Properties.Resources.D_UPDATE_TOKEN, req.CurrentExecutionFilePath, newToken));
                    }
                }
                // EndRequestで使用するのでトークンをコンテキストに保存
                context.Items[TOKEN_NAME] = newToken;

                // customErrorsに設定されているページへのリクエストの場合は終了する
                if (WebUtils.IsInCustomErrorPages())
                {
                    return;
                }

                if (PageConfiguration.IsCheckTokenEnabled(WebUtils.GetCurrentPagePath()))
                {
                    // セッションタイムアウトチェック
                    if (session.IsNewSession)
                    {
                        // セッションがタイムアウトしている、または初回リクエスト時に
                        // トークンチェックを行うページにアクセスされたとき
                        SessionTimeoutException exception = new SessionTimeoutException(Properties.Resources.E_SESSION_TIMEOUT);
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }
                    // requestからトークンを取得
                    string requestToken = req.Form[TOKEN_NAME];
                    if (requestToken == null)
                    {
                        requestToken = req.QueryString[QUERY_TOKEN_NAME];
                    }

                    // トークンチェック
                    CheckToken(requestToken, currentSessionToken, pageSessionToken);

                    // ページのhiddenフィールドにトークン設定
                    ClientScriptManager csm = page.ClientScript;
                    if (!isKeepToken)
                    {   // トークンを更新する場合、最新のトークンを設定
                        csm.RegisterHiddenField(TOKEN_NAME, newToken);
                    }
                    else
                    {   // トークンを更新しない場合、当該ページの最新トークンを設定
                        csm.RegisterHiddenField(TOKEN_NAME, pageSessionToken);
                    }
                }

            }
        }

        /// <summary>
        /// トークンの発行が有効に設定されている場合はクエリ文字列にトークンを設定します。
        /// </summary>
        /// <param name="sender">イベントのソース。</param>
        /// <param name="e">イベントデータを格納している<see cref="EventArgs"/> オブジェクト。</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")]
        protected virtual void context_EndRequest(object sender, EventArgs e)
        {
            HttpApplication application = (HttpApplication)sender;
            HttpContext context = application.Context;
            Page page = context.Handler as Page;

            // ハンドラがPageか判定
            if (page != null)
            {
                HttpResponse response = context.Response;
                string redirectLocation = response.RedirectLocation;
                if (redirectLocation == null)
                {
                    // 画面遷移（リダイレクト）しない場合は何もしない。
                    return;
                }

                bool isErrorTransition = false;
                if (context.Items.Contains("TERASOLUNA_ErrorTransition"))
                {
                    // 例外が発生して ExceptionTransitionListenerImpl 内で Response.Redirect が呼ばれた場合は、
                    // isErrorTransition が true
                    bool.TryParse(context.Items["TERASOLUNA_ErrorTransition"].ToString(), out isErrorTransition);
                }

                bool isCheckToken = false;
                HttpRequest request = context.Request;
                string appPath = request.ApplicationPath;
                if (redirectLocation.StartsWith(appPath))
                {
                    int index = redirectLocation.IndexOf('?');
                    string nextPagePath = null;
                    if (index < 0)
                    {
                        nextPagePath = redirectLocation;
                    }
                    else
                    {
                        nextPagePath = redirectLocation.Substring(0, index);
                    }
                    if (!"/".Equals(appPath))
                    {
                        nextPagePath = nextPagePath.Substring(appPath.Length);
                    }
                    isCheckToken = PageConfiguration.IsCheckTokenEnabled(nextPagePath);
                }

                if (!isErrorTransition && isCheckToken)
                {
                    // クエリ文字列へのトークンの設定
                    string token = (string)context.Items[TOKEN_NAME];
                    if (_log.IsDebugEnabled)
                    {
                        _log.Debug(string.Format(Properties.Resources.D_SET_TOKEN, redirectLocation, token));
                    }
                    response.RedirectLocation =
                        WebUtils.AppendParameter(redirectLocation, "__Token", token);
                }
            }
        }

        /// <summary>
        /// リクエスト、セッションから取得したトークンの比較を行います。
        /// </summary>
        /// <param name="requestToken">リクエストから取得したトークン。</param>
        /// <param name="currentSessionToken">セッションから取得した現在のトークン。</param>
        /// <param name="pageSessionToken">セッションから取得したページ固有のトークン。</param>
        /// <remarks>特になし。</remarks>
        /// <exception cref="InvalidTransitionException">
        /// requestToken パラメータが null 参照の場合。currentSessionToken パラメータが null 参照の場合。
        /// </exception>
        protected static void CheckToken(string requestToken, string currentSessionToken, string pageSessionToken)
        {
            if (currentSessionToken == null)
            {
                InvalidTransitionException exception = new InvalidTransitionException(
                    string.Format(Properties.Resources.E_INVALID_TRANSITION, HttpContext.Current.Request.Url.AbsoluteUri));
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            if (requestToken == null)
            {
                InvalidTransitionException exception = new InvalidTransitionException(
                    string.Format(Properties.Resources.E_INVALID_TRANSITION, HttpContext.Current.Request.Url.AbsoluteUri));
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            if (!requestToken.Equals(currentSessionToken))
            {
                if (pageSessionToken == null)
                {
                    InvalidTransitionException exception = new InvalidTransitionException(
                        string.Format(Properties.Resources.E_INVALID_TRANSITION, HttpContext.Current.Request.Url.AbsoluteUri));
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }

                if (!requestToken.Equals(pageSessionToken))
                {
                    InvalidTransitionException exception = new InvalidTransitionException(
                        string.Format(Properties.Resources.E_INVALID_TRANSITION, HttpContext.Current.Request.Url.AbsoluteUri));
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
            }
        }

        /// <summary>
        /// 現在のトークンと違う値の新しいトークンを生成します。
        /// </summary>
        /// <param name="oldToken">現在のトークン。</param>
        /// <returns>新しいトークン。</returns>
        protected static string CreateNewToken(string oldToken)
        {
            string newToken = (Guid.NewGuid().ToString()).Replace("-", string.Empty);
            while (oldToken == newToken)
            {
                newToken = (Guid.NewGuid().ToString()).Replace("-", string.Empty);
            }

            return newToken;
        }
    }
}
