/*
* Copyright (c) 1998-2011 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()) {
Callable 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.isNull())
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))
{
// intentionally empty
// TODO: thats a bad smell! refactor
}
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++) {
// intentionally left empty
}
}
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();
}
}
}