/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.quercus.lib.session;

import com.caucho.quercus.annotation.Hide;
import com.caucho.quercus.annotation.Optional;
import com.caucho.quercus.env.*;
import com.caucho.quercus.lib.OutputModule;
import com.caucho.quercus.module.AbstractQuercusModule;
import com.caucho.quercus.module.ModuleStartupListener;
import com.caucho.quercus.module.IniDefinitions;
import com.caucho.quercus.module.IniDefinition;
import com.caucho.quercus.servlet.api.QuercusCookie;
import com.caucho.quercus.servlet.api.QuercusCookieImpl;
import com.caucho.quercus.servlet.api.QuercusHttpServletResponse;
import com.caucho.util.L10N;

import java.util.logging.Logger;
import java.util.Iterator;

/**
 * Quercus session handling
 */
public class SessionModule extends AbstractQuercusModule
  implements ModuleStartupListener {
  private static final L10N L = new L10N(SessionModule.class);
  private static final Logger log
    = Logger.getLogger(SessionModule.class.getName());

  private static final IniDefinitions _iniDefinitions = new IniDefinitions();

  public static final int PHP_SESSION_DISABLED = 0;
  public static final int PHP_SESSION_NONE = 1;
  public static final int PHP_SESSION_ACTIVE = 2;

  /**
   * Returns the default php.ini values.
   */
  public IniDefinitions getIniDefinitions()
  {
    return _iniDefinitions;
  }

  public String []getLoadedExtensions()
  {
    return new String[] { "session" };
  }

  @Hide
  public void startup(Env env)
  {
    if (env.getConfigVar("session.auto_start").toBoolean())
      session_start(env);
  }

  /**
   * Returns and/or sets the value of session.cache_limiter, affecting the
   * cache related headers that are sent as a result of a call to
   * {@link #session_start(Env)}.
   *
   * If the optional parameter is not supplied, this function
   * simply returns the existing value.
   * If the optional parameter is supplied, the returned value
   * is the old value that was set before the new value is applied.
   *
   * Valid values are "nocache" (the default), "private", "private_no_expire",
   * and "public". If a value other than these values is supplied,
   * then a warning is produced
   * and no cache related headers will be sent to the client.
   */
  public Value session_cache_limiter(Env env, @Optional Value newValue)
  {
    Value value = env.getIni("session.cache_limiter");

    if (newValue.isDefault())
      return value;

    env.setIni("session.cache_limiter", newValue);

    return value;
  }

  public Value session_cache_expire(Env env, @Optional Value newValue)
  {
    Value value = (LongValue) env.getSpecialValue("cache_expire");

    if (value == null)
      value = env.getIni("session.cache_expire");

    if (newValue != null && ! newValue.isDefault())
      env.setSpecialValue("cache_expire", newValue);

    return LongValue.create(value.toLong());
  }

  /**
   * Alias of session_write_close.
   */
  public static Value session_commit(Env env)
  {
    return session_write_close(env);
  }

  /**
   * Encodes the session values.
   */
  public static boolean session_decode(Env env, StringValue value)
  {
    SessionArrayValue session = env.getSession();

    if (session == null) {
      env.warning(L.l("session_decode requires valid session"));
      return false;
    }

    return session.decode(env, value);
  }

  /**
   * Encodes the session values.
   */
  public static String session_encode(Env env)
  {
    SessionArrayValue session = env.getSession();

    if (session == null) {
      env.warning(L.l("session_encode requires valid session"));
      return null;
    }

    return session.encode(env);
  }

  /**
   * Destroys the session
   */
  public static boolean session_destroy(Env env)
  {
    SessionArrayValue session = env.getSession();

    if (session == null)
      return false;

    env.destroySession(session.getId());

    return true;
  }

  /**
   * Returns the session cookie parameters
   */
  public static ArrayValue session_get_cookie_params(Env env)
  {
    ArrayValue array = new ArrayValueImpl();

    array.put(env, "lifetime", env.getIniLong("session.cookie_lifetime"));
    array.put(env, "path", env.getIniString("session.cookie_path"));
    array.put(env, "domain", env.getIniString("session.cookie_domain"));
    array.put(env, "secure", env.getIniBoolean("session.cookie_secure"));
    array.put(env, "httponly", env.getIniBoolean("session.cookie_httponly"));

    return array;
  }

  /**
   * Returns the session id
   */
  public static String session_id(Env env, @Optional String id)
  {
    Value sessionIdValue = (Value) env.getSpecialValue("caucho.session_id");

    String oldValue;

    if (sessionIdValue != null) {
      oldValue = sessionIdValue.toString();
    }
    else {
      oldValue = "";
    }

    if (id != null && id.length() > 0) {
      env.setSpecialValue("caucho.session_id", env.createString(id));

      SessionArrayValue session = env.getSession();

      // php/1k6y
      if (session != null) {
        session.setId(id);
      }
    }

    return oldValue;
  }

  /**
   * Returns true if a session variable is registered.
   */
  public static boolean session_is_registered(Env env, StringValue name)
  {
    SessionArrayValue session = env.getSession();

    return session != null && session.get(name).isset();
  }

  /**
   * Returns the object's class name
   */
  public Value session_module_name(Env env, @Optional String newValue)
  {
    Value value = env.getIni("session.save_handler");

    if (newValue != null && ! newValue.equals(""))
      env.setIni("session.save_handler", newValue);

    return value;
  }

  /**
   * Returns the object's class name
   */
  public Value session_name(Env env, @Optional String newValue)
  {
    Value value = env.getIni("session.name");

    if (newValue != null && newValue.length() > 0)
      env.setIni("session.name", newValue);

    return value;
  }

  /**
   * Regenerates the session id.
   *
   * This function first creates a new session id.  The old session
   * values are migrated to this new session.  Then a new session cookie
   * is sent (XXX: send only if URL rewriting is off?).  Changing the
   * session ID should be transparent.
   * Therefore, session callbacks should not be called.
   */
  public static boolean session_regenerate_id(Env env,
                                              @Optional boolean deleteOld)
  {
    // php/1k82, php/1k83
    if (env.getSession() == null)
      return ! deleteOld;

    String sessionId = env.generateSessionId();

    if (deleteOld) {
      SessionArrayValue session = env.getSession();

      session_destroy(env);

      // php/1k6z
      env.setSession(session);
    }
    else {
      SessionArrayValue session = env.getSession();

      session.setId(sessionId);
    }

    // update environment to new session id
    session_id(env, sessionId);

    if (env.getIni("session.use_cookies").toBoolean())
      generateSessionCookie(env, sessionId);

    return true;
  }

  /**
   * Registers global variables in the session.
   */
  public boolean session_register(Env env, Value []values)
  {
    SessionArrayValue session = env.getSession();

    if (session == null) {
      session_start(env);
      session = env.getSession();
    }

    for (int i = 0; i < values.length; i++)
      sessionRegisterImpl(env, (ArrayValue) session, values[i]);

    return true;
  }

  /**
   * Registers global variables in the session.
   */
  private void sessionRegisterImpl(Env env, ArrayValue session, Value nameV)
  {
    nameV = nameV.toValue();

    if (nameV instanceof StringValue) {
      String name = nameV.toString();

      Value var = env.getGlobalVar(name);

      Value value = session.get(nameV);

      if (value.isset())
        var.set(value);

      session.put(nameV, var);
    } else if (nameV.isArray()) {
      ArrayValue array = (ArrayValue) nameV.toValue();

      for (Value subValue : array.values()) {
        sessionRegisterImpl(env, session, subValue);
      }
    }
  }

  /**
   * Returns the session's save path
   */
  public Value session_save_path(Env env, @Optional String newValue)
  {
    Value value = env.getIni("session.save_path");

    if (newValue != null && ! newValue.equals(""))
      env.setIni("session.save_path", newValue);

    if (value.isNull() || value.length() == 0) {
      // XXX: should we create work directory if does not exist?
      value = env.createString(env.getWorkDir().getPath());
    }

    return value;
  }

  /**
   * Sets the session cookie parameters
   */
  public Value session_set_cookie_params(Env env,
                                         long lifetime,
                                         @Optional Value path,
                                         @Optional Value domain,
                                         @Optional Value isSecure,
                                         @Optional Value isHttpOnly)
  {
    env.setIni("session.cookie_lifetime", String.valueOf(lifetime));

    if (path.isset()) {
      env.setIni("session.cookie_path", path.toString());
    }

    if (domain.isset()) {
      env.setIni("session.cookie_domain", domain.toString());
    }

    if (isSecure.isset()) {
      env.setIni("session.cookie_secure", isSecure.toBoolean() ? "1" : "0");
    }

    if (isHttpOnly.isset()) {
      env.setIni("session.cookie_httponly", isHttpOnly.toBoolean() ? "1" : "0");
    }

    return NullValue.NULL;
  }

  /**
   * Sets the session save handler
   */
  public boolean session_set_save_handler(Env env,
                                          Callable open,
                                          Callable close,
                                          Callable read,
                                          Callable write,
                                          Callable destroy,
                                          Callable gc)

  {
    SessionCallback cb
      = new SessionCallback(open, close, read, write, destroy, gc);

    env.setSessionCallback(cb);

    return true;
  }

  /**
   * Returns the status of the session.
   */
  public static int session_status(Env env)
  {
    if (env.getSession() != null) {
      return PHP_SESSION_ACTIVE;
    }
    else {
      return PHP_SESSION_NONE;
    }
  }

  /**
   * Start the session
   */
  public static boolean session_start(Env env)
  {
    if (env.getRequest() == null) {
      env.notice(L.l("cannot start session without a request context"));

      return false;
    }

    if (env.getSession() != null) {
      env.notice(L.l("session has already been started"));
      return true;
    }

    SessionCallback callback = env.getSessionCallback();

    Value sessionIdValue = (Value) env.getSpecialValue("caucho.session_id");
    String sessionId = null;

    env.removeConstant("SID");

    String cookieName = env.getIni("session.name").toString();
    boolean generateCookie = true;
    boolean create = false;

    if (callback != null) {
      String savePath = env.getIni("session.save_path").toString();

      if (savePath == null || "".equals(savePath))
        callback.open(env, env.getWorkDir().getPath(), cookieName);
      else
        callback.open(env, savePath, cookieName);
    }

    //
    // Use cookies to transmit session id
    //
    if (env.getIni("session.use_cookies").toBoolean()) {
      if (sessionIdValue != null)
        sessionId = sessionIdValue.toString();

      if (sessionId == null || "".equals(sessionId)) {
        QuercusCookie []cookies = env.getRequest().getCookies();

        for (int i = 0; cookies != null && i < cookies.length; i++) {
          if (cookies[i].getName().equals(cookieName)
              && ! "".equals(cookies[i].getValue())) {
            sessionId = cookies[i].getValue();
            generateCookie = false;
          }
        }
      }

      if (! generateCookie)
        env.addConstant("SID", env.getEmptyString(), false);
    }

    //
    // Use URL rewriting to transmit session id
    //

    if (env.getIniBoolean("session.use_trans_sid")
        && ! env.getIniBoolean("session.use_only_cookies")) {
      if (sessionId == null) {
        if (sessionIdValue != null)
          sessionId = sessionIdValue.toString();

        if (sessionId == null || "".equals(sessionId))
          sessionId = env.getRequest().getParameter(cookieName);

        if (sessionId == null || "".equals(sessionId)) {
          sessionId = env.generateSessionId();
          create = true;
        }
      }
      env.removeConstant("SID");
      env.addConstant("SID", env.createString(cookieName + '=' + sessionId),
                      false);

      OutputModule.pushUrlRewriter(env);
    }

    if (sessionId == null || "".equals(sessionId)) {
      sessionId = env.generateSessionId();
      create = true;
    }

    QuercusHttpServletResponse response = env.getResponse();

    if (response == null) {
    }
    else if (response.isCommitted())
      env.warning(
          L.l("cannot send session cache limiter headers "
              + "because response is committed"));
    else {
      Value cacheLimiterValue = env.getIni("session.cache_limiter");
      String cacheLimiter = String.valueOf(cacheLimiterValue);

      Value cacheExpireValue = (LongValue)env.getSpecialValue("cache_expire");

      if (cacheExpireValue == null)
        cacheExpireValue = env.getIni("session.cache_expire");

      int cacheExpire = cacheExpireValue.toInt() * 60;

      if ("nocache".equals(cacheLimiter)) {
        response.setHeader("Expires", "Thu, 19 Nov 1981 08:52:00 GMT");
        response.setHeader("Cache-Control",
                           "no-store, no-cache, must-revalidate, post-check=0, pre-check=0");
        response.setHeader("Pragma", "no-cache");
      }
      else if ("private".equals(cacheLimiter)) {
        response.setHeader("Cache-Control", "private, max-age="
                           + cacheExpire + ", pre-check=" + cacheExpire);
      }
      else if ("private_no_expire".equals(cacheLimiter)) {
        response.setHeader("Cache-Control", "private, max-age="
                           + cacheExpire + ", pre-check=" + cacheExpire);
      }
      else if ("public".equals(cacheLimiter)) {
        response.setHeader("Cache-Control", "public, max-age=" + cacheExpire
                           + ", pre-check=" + cacheExpire);
      }
      else if ("none".equals(cacheLimiter)) {
      }
      else {
        // php/1k16
        //response.setHeader(
        // "Cache-Control", cacheLimiter + ", max-age=" + cacheExpire
        // + ", pre-check=" + cacheExpire);
      }
    }

    SessionArrayValue session = env.createSession(sessionId, create);
    sessionId = session.getId();

    if (env.getIni("session.use_cookies").toBoolean() && generateCookie) {
      generateSessionCookie(env, sessionId);
    }
    env.setSpecialValue("caucho.session_id", env.createString(sessionId));

    return true;
  }

  /**
   * Sends a new session cookie.
   */
  private static void generateSessionCookie(Env env, String sessionId)
  {
    final QuercusHttpServletResponse response = env.getResponse();

    String cookieName = env.getIni("session.name").toString();

    StringValue cookieValue
      = env.createString(cookieName + '=' + sessionId);
    env.removeConstant("SID");
    env.addConstant("SID", cookieValue, false);

    QuercusCookie cookie = new QuercusCookieImpl(cookieName, sessionId);
    // #2649
    String cookieVersion = env.getIniString("session.cookie_version");
    if (! "0".equals(cookieVersion))
      cookie.setVersion(1);

    if (response.isCommitted()) {
      env.warning(L.l("cannot send session cookie because response is committed"));
    }
    else {
      Value path = env.getIni("session.cookie_path");
      cookie.setPath(path.toString());

      Value maxAge = env.getIni("session.cookie_lifetime");

      if (maxAge.toInt() != 0)
        cookie.setMaxAge(maxAge.toInt());

      Value domain = env.getIni("session.cookie_domain");

      // this is for 3rd party servlet containers that don't check the domain
      // before sending the cookie
      if (domain.length() > 0) {
        cookie.setDomain(domain.toString());
      }

      Value secure = env.getIni("session.cookie_secure");
      cookie.setSecure(secure.toBoolean());

      Value httpOnly = env.getIni("session.cookie_httponly");

      if (httpOnly.toBoolean()) {
        try {
          cookie.setHttpOnly(true);
        }
        catch (Throwable e) {
          env.warning(L.l("HttpOnly requires Servlet 3.0"), e);
        }
      }

      response.addCookie(cookie);
    }
  }

  /**
   * Unsets the specified session values
   */
  public boolean session_unregister(Env env, Value key)
  {
    SessionArrayValue session = env.getSession();

    if (session == null)
      return false;

    session.remove(key);

    return true;
  }

  /**
   * Unsets the session values
   */
  public Value session_unset(Env env)
  {
    SessionArrayValue session = env.getSession();

    if (session == null)
      return NullValue.NULL;

    session.clear();

    return NullValue.NULL;
  }

  /**
   * Writes the session and closes it.
   */
  public static Value session_write_close(Env env)
  {
    env.sessionWriteClose();

    return NullValue.NULL;
  }

  /**
   * Converts an integer to a printable character
   */
  private static char encode(long code)
  {
    code = code & 0x3f;

    if (code < 26)
      return (char) ('a' + code);
    else if (code < 52)
      return (char) ('A' + code - 26);
    else if (code < 62)
      return (char) ('0' + code - 52);
    else if (code == 62)
      return '_';
    else
      return '-';
  }


  static final IniDefinition INI_SESSION_SAVE_PATH
    = _iniDefinitions.add("session.save_path", "", PHP_INI_ALL);
  static final IniDefinition INI_SESSION_NAME
    = _iniDefinitions.add("session.name", "PHPSESSID", PHP_INI_ALL);
  static final IniDefinition INI_SESSION_SAVE_HANDLER
    = _iniDefinitions.add("session.save_handler", "files", PHP_INI_ALL);
  static final IniDefinition INI_SESSION_AUTO_START
    = _iniDefinitions.add("session.auto_start", false, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_GC_PROBABILITY_START
    = _iniDefinitions.add("session.gc_probability_start", true, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_GC_DIVISOR
    = _iniDefinitions.add("session.gc_divisor", 100, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_GC_MAXLIFETIME
    = _iniDefinitions.add("session.gc_maxlifetime", 1440, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_SERIALIZE_HANDLER
    = _iniDefinitions.add("session.serialize_handler", "quercus", PHP_INI_ALL);
  static final IniDefinition INI_SESSION_COOKIE_LIFETIME
    = _iniDefinitions.add("session.cookie_lifetime", 0, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_COOKIE_PATH
    = _iniDefinitions.add("session.cookie_path", "/", PHP_INI_ALL);
  static final IniDefinition INI_SESSION_COOKIE_DOMAIN
    = _iniDefinitions.add("session.cookie_domain", "", PHP_INI_ALL);
  static final IniDefinition INI_SESSION_COOKIE_SECURE
    = _iniDefinitions.add("session.cookie_secure", "", PHP_INI_ALL);
  static final IniDefinition INI_SESSION_USE_COOKIES
    = _iniDefinitions.add("session.use_cookies", true, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_USE_ONLY_COOKIES
    = _iniDefinitions.add("session.use_only_cookies", true, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_REFERER_CHECK
    = _iniDefinitions.add("session.referer_check", "", PHP_INI_ALL);
  static final IniDefinition INI_SESSION_ENTROPY_FILE
    = _iniDefinitions.add("session.entropy_file", "", PHP_INI_ALL);
  static final IniDefinition INI_SESSION_ENTROPY_LENGTH
    = _iniDefinitions.add("session.entropy_length", false, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_CACHE_LIMITER
    = _iniDefinitions.add("session.cache_limiter", "nocache", PHP_INI_ALL);
  static final IniDefinition INI_SESSION_CACHE_EXPIRE
    = _iniDefinitions.add("session.cache_expire", 180, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_USE_TRANS_SID
    = _iniDefinitions.add("session.use_trans_sid", false, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_BUG_COMPAT_42
    = _iniDefinitions.add("session.bug_compat_42", true, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_BUG_COMPAT_WARN
    = _iniDefinitions.add("session.bug_compat_warn", true, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_HASH_FUNCTION
    = _iniDefinitions.add("session.hash_function", false, PHP_INI_ALL);
  static final IniDefinition INI_SESSION_HASH_BITS_PER_CHARACTER
    = _iniDefinitions.add("session.hash_bits_per_character", 4, PHP_INI_ALL);
  static final IniDefinition INI_URL_REWRITER_TAGS
    = _iniDefinitions.add(
      "url_rewriter.tags",
      "a=href,area=href,frame=src,form=,fieldset=", PHP_INI_ALL);
}
