/*
 * Copyright (c) 1998-2008 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 Emil Ong
 */

package com.caucho.quercus.lib;

import com.caucho.quercus.env.*;
import com.caucho.util.URLUtil;

import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * Implements the built-in URL rewriter for passing session ids and other
 * variables.
 */
public class UrlRewriterCallback extends CallbackFunction {
  private StringBuilder _rewriterQuery = new StringBuilder();
  private ArrayList<String[]> _rewriterVars = new ArrayList<String[]>();

  public UrlRewriterCallback(Env env)
  {
    super(env, "URL-Rewriter");

    try {
      Method rewriterMethod = 
        UrlRewriterCallback.class.getMethod("_internal_url_rewriter",
                                            Env.class, Value.class);
      setFunction(new JavaMethod(env.getModuleContext(), rewriterMethod));
    } catch (NoSuchMethodException e) {
    } catch (SecurityException e) {
    }
  }

  /**
   * Returns the unique rewriter.
   */
  public static UrlRewriterCallback getInstance(Env env)
  {
    OutputBuffer ob = env.getOutputBuffer();

    for (; ob != null; ob = ob.getNext()) {
      Callback callback = ob.getCallback();

      if (callback instanceof UrlRewriterCallback)
        return (UrlRewriterCallback)callback;
    }

    return null;
  }

  /**
   * Adds a rewrite variable.  Intended for 
   * <code>output_add_rewrite_var()</code>.
   */
  public void addRewriterVar(String var, String value)
  {
    if (_rewriterQuery.length() > 0)
      _rewriterQuery.append("&");

    String encodedVar = URLUtil.encodeURL(var.replaceAll(" ", "+"));
    String encodedValue = URLUtil.encodeURL(value.replaceAll(" ", "+"));

    _rewriterQuery.append(encodedVar + "=" + encodedValue);
    _rewriterVars.add(new String[] {encodedVar, encodedValue});
  }

  /**
   * Resets (clears) all the rewrite variables.  Intended for 
   * <code>output_reset_rewrite_vars()</code>.
   */
  public void resetRewriterVars()
  {
    _rewriterQuery = new StringBuilder();
    _rewriterVars.clear();
  }
  
  /**
   * Callback function to rewrite URLs to include session information.
   * Note that this function should return BooleanValue.FALSE in the
   * case where data should be discarded.
   */
  public static Value _internal_url_rewriter(Env env, Value buffer)
  {
    Value result;
    UrlRewriterCallback rewriter = getInstance(env);

    // We should never have been called in this case, but 
    // return the buffer unmodified anyway.

    if (rewriter == null)
      result = buffer;
    else {
      // Return the buffer unmodified when no urls are rewritten
      // php/1k6x

      Parser parser = rewriter.new Parser(buffer.toString(), env);
      result = parser.parse();

      if (result == NullValue.NULL)
        result = buffer;
    }

    return result;
  }

  private class Parser {
    private Env _env;

    private boolean _includeSessionInfo = false;
    private String _sessionName = null;
    private String _sessionId = null;
    private String _javaSessionName = null;
    private String _javaSessionId = null;

    private int _index;
    private String _value;
    private boolean _quoted;

    private String _input;
    private StringValue _output;

    public Parser(String input, Env env)
    {
      _input = input;
      _env = env;
      _index = 0;
      _output = env.createUnicodeBuilder();
    }

    public Value parse()
    {
      if (_env.getSession() != null && _env.getJavaSession() != null &&
          _env.getIni("session.use_trans_sid").toBoolean()) {
        _includeSessionInfo = true;

        _sessionName = _env.getIni("session.name").toString();
        _sessionId = _env.getSession().getId();

	_javaSessionName = _env.getQuercus().getCookieName();

        _javaSessionId = _env.getJavaSession().getId();
      }

      if (_includeSessionInfo == false && _rewriterVars.isEmpty())
        return NullValue.NULL;

      String [] tagPairs = 
        _env.getIni("url_rewriter.tags").toString().split(",");
      HashMap<String,String> tags = new HashMap<String,String>();

      for (String tagPair : tagPairs) {
        String [] tagAttribute = tagPair.split("=");

        switch (tagAttribute.length) {
          case 1:
            tags.put(tagAttribute[0], null);
            break;

          case 2:
            tags.put(tagAttribute[0], tagAttribute[1]);
            break;

          default:
            break;
        }
      }

      for (String tag = getNextTag(); tag != null; tag = getNextTag()) {
        if (tags.containsKey(tag)) {
          String attribute = tags.get(tag);
          
          if (attribute == null) {
            consumeToEndOfTag();
            
            if (_includeSessionInfo) {
              String phpSessionInputTag = 
                "<input type=\"hidden\" name=\"" + _sessionName + "\"" +
                " value=\"" + _sessionId + "\" />";

              _output.append(phpSessionInputTag );
            }

            for (String[] entry : _rewriterVars) {
              String inputTag = 
                "<input type=\"hidden\" name=\"" + entry[0] + "\"" +
                " value=\"" + entry[1] + "\" />";
              _output.append(inputTag);
            }
          } else {
            int valueEnd = 0;

            for (valueEnd = getNextAttribute(attribute);
                 valueEnd == 0;
                 valueEnd = getNextAttribute(attribute))
            {}

            if (valueEnd > 0) {
              _output.append(rewriteUrl(_value));

              if (_quoted)
                consumeOneCharacter();
            }
          }
        }
      }

      return _output;
    }

    /**
     * Finds the next tag in the string returns it.
     */
    private String getNextTag()
    {
      int tagStart = _input.indexOf('<', _index);

      if (tagStart < 0) {
        _output.append(_input.substring(_index));
        return null;
      }

      // consume everything upto the tag opening
      _output.append(_input.substring(_index, tagStart + 1));

      // skip the '<'
      _index = tagStart + 1;
      
      consumeNonWhiteSpace();

      return _input.substring(tagStart + 1, _index);
    }

    /**
     * Finds the next attribute matching the given name.
     *
     * @return -1 if no more valid attributes can be found, 0 if the next
     * attribute is not the one sought, and 1 if the attribute was found.  
     *
     * The _index pointer will refer to the end position for the value
     * in the _input in the final case, but only those characters up to 
     * the beginning of the value will have been copied to the output.
     */
    private int getNextAttribute(String attribute)
    {
      consumeWhiteSpace();

      int attributeStart = _index;

      while (_index < _input.length() &&
             isValidAttributeCharacter(_input.charAt(_index)))
        consumeOneCharacter();
        
      // no valid attribute was found (we're probably at the end of the tag)
      if (_index == attributeStart)
        return -1;

      String foundAttribute = _input.substring(attributeStart, _index);

      consumeWhiteSpace();

      // Any attributes that we will affect are of the form attr=value
      if (_input.length() <= _index || _input.charAt(_index) != '=')
        return -1;

      consumeOneCharacter();

      consumeWhiteSpace();

      // check for quoting
      char quote = ' ';
      
      if (_input.charAt(_index) == '"' || _input.charAt(_index) == '\'') {
        _quoted = true;
        quote = _input.charAt(_index);

        consumeOneCharacter();
      }

      int valueEnd = _index;
      
      if (_quoted) {
        valueEnd = _input.indexOf(quote, _index);
      
        // try to account for unclosed quotes
        int tagEnd = _input.indexOf('>', _index);

        if (valueEnd < 0) {
          if (tagEnd > 0) 
            valueEnd = tagEnd;
          else
            valueEnd = _input.length();
        }
      } else {
        // skip to the end of the value
        for (valueEnd = _index; 
             valueEnd < _input.length() &&
             _input.charAt(valueEnd) != '/' &&
             _input.charAt(valueEnd) != '>' &&
             _input.charAt(valueEnd) != ' ';
             valueEnd++) {}
      }

      if (foundAttribute.equals(attribute)) {
        _value = _input.substring(_index, valueEnd);

        _index = valueEnd;

        return 1;
      } else {
        // make sure to skip the complete attribute if it's not the
        // one we're looking for.
        if (_quoted)
          valueEnd += 1;

        _output.append(_input.substring(_index, valueEnd));

        _index = valueEnd;

        return 0;
      }
    }

    private void consumeOneCharacter()
    {
      if (_index < _input.length()) {
        _output.append(_input.charAt(_index));
        _index += 1;
      }
    }

    private void consumeWhiteSpace()
    {
      while (_index < _input.length() && 
             Character.isWhitespace(_input.charAt(_index)))
        consumeOneCharacter();
    }

    private void consumeNonWhiteSpace()
    {
      while (_index < _input.length() && 
             !Character.isWhitespace(_input.charAt(_index)))
        consumeOneCharacter();
    }

    private void consumeToEndOfTag()
    {
      while (_input.charAt(_index) != '>')
        consumeOneCharacter();

      // consume the '>'
      consumeOneCharacter();
    }

    private boolean isValidAttributeCharacter(char ch)
    {
      return Character.isLetterOrDigit(ch) || 
             (ch == '-') || (ch == '.') || (ch == '_') || (ch == ':');
    }

    private String rewriteUrl(String urlString)
    {
      // according to php documentation, it only adds tags to the
      // end of relative URLs, but according to RFC 2396, any
      // URI beginning with '/' (e.g. <a href="/foo">link</a>) is
      // absolute.  Nonetheless, php does add session ids to these
      // links.  Thus php must be defining "relative" as relative
      // to the host, not the hierarchy.  Thus we only check to make
      // sure that the scheme and authority are undefined, not that
      // the first character of the path begins with '/'.

      URI uri;

      try {
        uri = new URI(urlString);
      } catch (URISyntaxException e) {
        return urlString;
      }

      if ((uri.getScheme() != null) || (uri.getAuthority() != null)) {
        return urlString;
      }

      StringBuilder query = new StringBuilder();

      if (uri.getQuery() != null) {
        query.append("?");
        query.append(uri.getQuery());
        query.append("&");
      } 
      else
        query.append("?");

      if (_includeSessionInfo) {
        query.append(_sessionName);
        query.append("=");
        query.append(_sessionId);
      }

      if (_rewriterQuery.length() != 0) {
        if (_includeSessionInfo)
          query.append("&");

        query.append(_rewriterQuery);
      }

      StringBuilder newUri = new StringBuilder();

      if (uri.getPath() != null)
        newUri.append(uri.getPath());

      newUri.append(query);

      if (uri.getFragment() != null) {
        newUri.append("#");
        newUri.append(uri.getFragment());
      }

      return newUri.toString();
    }
  }
}
