WEB-15327 Can you support server side includes in preview server

This commit is contained in:
Vladimir Krivosheev
2015-02-19 15:11:24 +01:00
parent 542a5eb2a5
commit 0dcc89314b
15 changed files with 1986 additions and 10 deletions
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,11 +21,15 @@ import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.util.SystemInfoRt;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.util.text.StringUtilRt;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.PathUtilRt;
import com.intellij.util.UriUtil;
import com.intellij.util.io.URLUtil;
import com.intellij.util.net.NetUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtf8Writer;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
@@ -34,6 +38,8 @@ import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedStream;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.builtInWebServer.ssi.SsiExternalResolver;
import org.jetbrains.builtInWebServer.ssi.SsiProcessor;
import org.jetbrains.ide.HttpRequestHandler;
import org.jetbrains.io.FileResponses;
import org.jetbrains.io.Responses;
@@ -43,6 +49,8 @@ import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import static org.jetbrains.io.Responses.addKeepAliveIfNeed;
public final class BuiltInWebServer extends HttpRequestHandler {
static final Logger LOG = Logger.getInstance(BuiltInWebServer.class);
@@ -59,6 +67,7 @@ public final class BuiltInWebServer extends HttpRequestHandler {
for (VirtualFile child : children) {
if (!child.isDirectory()) {
String name = child.getName();
//noinspection IfStatementWithIdenticalBranches
if (name.equals(preferredName)) {
return child;
}
@@ -81,7 +90,7 @@ public final class BuiltInWebServer extends HttpRequestHandler {
@Override
public boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) {
String host = HttpHeaders.getHost(request);
String host = request.headers().get(HttpHeaderNames.HOST);
if (StringUtil.isEmpty(host)) {
return false;
}
@@ -178,13 +187,23 @@ public final class BuiltInWebServer extends HttpRequestHandler {
}
static final class StaticFileHandler extends WebServerFileHandler {
private SsiProcessor ssiProcessor;
@Override
public boolean process(@NotNull VirtualFile file,
@NotNull CharSequence canonicalRequestPath,
@NotNull Project project,
@NotNull FullHttpRequest request,
@NotNull Channel channel) throws IOException {
@NotNull Channel channel,
boolean isCustomHost) throws IOException {
if (file.isInLocalFileSystem()) {
CharSequence nameSequence = file.getNameSequence();
//noinspection SpellCheckingInspection
if (StringUtilRt.endsWithIgnoreCase(nameSequence, ".shtml") || StringUtilRt.endsWithIgnoreCase(nameSequence, ".stm") || StringUtilRt.endsWithIgnoreCase(nameSequence, ".shtm")) {
processSsi(file, canonicalRequestPath, project, request, channel, isCustomHost);
return true;
}
File ioFile = VfsUtilCore.virtualToIoFile(file);
if (hasAccess(ioFile)) {
FileResponses.sendFile(request, channel, ioFile);
@@ -199,9 +218,9 @@ public final class BuiltInWebServer extends HttpRequestHandler {
return true;
}
boolean keepAlive = Responses.addKeepAliveIfNeed(response, request);
boolean keepAlive = addKeepAliveIfNeed(response, request);
if (request.method() != HttpMethod.HEAD) {
HttpHeaders.setContentLength(response, file.getLength());
HttpHeaderUtil.setContentLength(response, file.getLength());
}
channel.write(response);
@@ -218,6 +237,56 @@ public final class BuiltInWebServer extends HttpRequestHandler {
return true;
}
private void processSsi(@NotNull VirtualFile file,
@NotNull CharSequence canonicalRequestPath,
@NotNull Project project,
@NotNull FullHttpRequest request, @NotNull Channel channel, boolean isCustomHost) throws IOException {
String path = PathUtilRt.getParentPath(canonicalRequestPath.toString());
if (!isCustomHost) {
// remove project name - SSI resolves files only inside current project
path = path.substring(path.indexOf('/', 1) + 1);
}
if (ssiProcessor == null) {
ssiProcessor = new SsiProcessor(false);
}
ByteBuf buffer = channel.alloc().ioBuffer();
boolean keepAlive;
boolean releaseBuffer = true;
try {
long lastModified = ssiProcessor.process(new SsiExternalResolver(project, request, path, file.getParent()),
VfsUtilCore.loadText(file), file.getTimeStamp(), new ByteBufUtf8Writer(buffer));
HttpResponse response = FileResponses.prepareSend(request, channel, lastModified, file.getPath());
if (response == null) {
return;
}
keepAlive = addKeepAliveIfNeed(response, request);
if (request.method() != HttpMethod.HEAD) {
HttpHeaderUtil.setContentLength(response, buffer.readableBytes());
}
channel.write(response);
if (request.method() != HttpMethod.HEAD) {
releaseBuffer = false;
channel.write(buffer);
}
}
finally {
if (releaseBuffer) {
buffer.release();
}
}
ChannelFuture future = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
private static boolean hasAccess(File result) {
// deny access to .htaccess files
return !result.isDirectory() && result.canRead() && !(result.isHidden() || result.getName().startsWith(".ht"));
@@ -90,7 +90,7 @@ final class DefaultWebServerPathHandler extends WebServerPathHandler {
for (WebServerFileHandler fileHandler : WebServerFileHandler.EP_NAME.getExtensions()) {
try {
if (fileHandler.process(result, canonicalRequestPath, project, request, channel)) {
if (fileHandler.process(result, canonicalRequestPath, project, request, channel, isCustomHost)) {
return true;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,5 +31,6 @@ public abstract class WebServerFileHandler {
@NotNull CharSequence canonicalRequestPath,
@NotNull Project project,
@NotNull FullHttpRequest request,
@NotNull Channel channel) throws IOException;
@NotNull Channel channel,
boolean isCustomHost) throws IOException;
}
@@ -0,0 +1,448 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.jetbrains.builtInWebServer.ssi;
import java.text.ParseException;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Represents a parsed expression.
*
* @author Paul Speed
*/
final class ExpressionParseTree {
/**
* Contains the current set of completed nodes. This is a workspace for the
* parser.
*/
private final LinkedList<Node> nodeStack = new LinkedList<Node>();
/**
* Contains operator nodes that don't yet have values. This is a workspace
* for the parser.
*/
private final LinkedList<OppNode> oppStack = new LinkedList<OppNode>();
/**
* The root node after the expression has been parsed.
*/
private Node root;
/**
* The SSIMediator to use when evaluating the expressions.
*/
private final SsiProcessingState mySsiProcessingState;
/**
* Creates a new parse tree for the specified expression.
*/
public ExpressionParseTree(String expr, SsiProcessingState ssiProcessingState)
throws ParseException {
this.mySsiProcessingState = ssiProcessingState;
parseExpression(expr);
}
/**
* Evaluates the tree and returns true or false. The specified SSIMediator
* is used to resolve variable references.
*/
public boolean evaluateTree() {
return root.evaluate();
}
/**
* Pushes a new operator onto the opp stack, resolving existing opps as
* needed.
*/
private void pushOpp(OppNode node) {
// If node is null then it's just a group marker
if (node == null) {
oppStack.add(0, node);
return;
}
while (true) {
if (oppStack.size() == 0) break;
OppNode top = oppStack.get(0);
// If the top is a spacer then don't pop
// anything
//noinspection ConstantConditions
if (top == null) break;
// If the top node has a lower precedence then
// let it stay
if (top.getPrecedence() < node.getPrecedence()) break;
// Remove the top node
oppStack.remove(0);
// Let it fill its branches
top.popValues(nodeStack);
// Stick it on the resolved node stack
nodeStack.add(0, top);
}
// Add the new node to the opp stack
oppStack.add(0, node);
}
/**
* Resolves all pending opp nodes on the stack until the next group marker
* is reached.
*/
private void resolveGroup() {
OppNode top;
while ((top = oppStack.remove(0)) != null) {
// Let it fill its branches
top.popValues(nodeStack);
// Stick it on the resolved node stack
nodeStack.add(0, top);
}
}
/**
* Parses the specified expression into a tree of parse nodes.
*/
private void parseExpression(String expr) throws ParseException {
StringNode currStringNode = null;
// We cheat a little and start an artificial
// group right away. It makes finishing easier.
pushOpp(null);
ExpressionTokenizer et = new ExpressionTokenizer(expr);
while (et.hasMoreTokens()) {
int token = et.nextToken();
if (token != ExpressionTokenizer.TOKEN_STRING) {
currStringNode = null;
}
switch (token) {
case ExpressionTokenizer.TOKEN_STRING:
if (currStringNode == null) {
currStringNode = new StringNode(et.getTokenValue());
nodeStack.add(0, currStringNode);
}
else {
// Add to the existing
currStringNode.value.append(" ");
currStringNode.value.append(et.getTokenValue());
}
break;
case ExpressionTokenizer.TOKEN_AND:
pushOpp(new AndNode());
break;
case ExpressionTokenizer.TOKEN_OR:
pushOpp(new OrNode());
break;
case ExpressionTokenizer.TOKEN_NOT:
pushOpp(new NotNode());
break;
case ExpressionTokenizer.TOKEN_EQ:
pushOpp(new EqualNode());
break;
case ExpressionTokenizer.TOKEN_NOT_EQ:
pushOpp(new NotNode());
// Sneak the regular node in. The NOT will
// be resolved when the next opp comes along.
oppStack.add(0, new EqualNode());
break;
case ExpressionTokenizer.TOKEN_RBRACE:
// Closeout the current group
resolveGroup();
break;
case ExpressionTokenizer.TOKEN_LBRACE:
// Push a group marker
pushOpp(null);
break;
case ExpressionTokenizer.TOKEN_GE:
pushOpp(new NotNode());
// Similar strategy to NOT_EQ above, except this
// is NOT less than
oppStack.add(0, new LessThanNode());
break;
case ExpressionTokenizer.TOKEN_LE:
pushOpp(new NotNode());
// Similar strategy to NOT_EQ above, except this
// is NOT greater than
oppStack.add(0, new GreaterThanNode());
break;
case ExpressionTokenizer.TOKEN_GT:
pushOpp(new GreaterThanNode());
break;
case ExpressionTokenizer.TOKEN_LT:
pushOpp(new LessThanNode());
break;
case ExpressionTokenizer.TOKEN_END:
break;
}
}
// Finish off the rest of the uopps
resolveGroup();
if (nodeStack.size() == 0) {
throw new ParseException("No nodes created.", et.getIndex());
}
if (nodeStack.size() > 1) {
throw new ParseException("Extra nodes created.", et.getIndex());
}
if (oppStack.size() != 0) {
throw new ParseException("Unused opp nodes exist.", et.getIndex());
}
root = nodeStack.get(0);
}
/**
* A node in the expression parse tree.
*/
private abstract static class Node {
/**
* Return true if the node evaluates to true.
*/
public abstract boolean evaluate();
}
/**
* A node the represents a String value
*/
private class StringNode extends Node {
StringBuilder value;
String resolved = null;
public StringNode(String value) {
this.value = new StringBuilder(value);
}
/**
* Resolves any variable references and returns the value string.
*/
public String getValue() {
if (resolved == null) {
resolved = mySsiProcessingState.substituteVariables(value.toString());
}
return resolved;
}
/**
* Returns true if the string is not empty.
*/
@Override
public boolean evaluate() {
return !(getValue().length() == 0);
}
@Override
public String toString() {
return value.toString();
}
}
private static final int PRECEDENCE_NOT = 5;
private static final int PRECEDENCE_COMPARE = 4;
private static final int PRECEDENCE_LOGICAL = 1;
/**
* A node implementation that represents an operation.
*/
private abstract static class OppNode extends Node {
/**
* The left branch.
*/
Node left;
/**
* The right branch.
*/
Node right;
/**
* Returns a preference level suitable for comparison to other OppNode
* preference levels.
*/
public abstract int getPrecedence();
/**
* Lets the node pop its own branch nodes off the front of the
* specified list. The default pulls two.
*/
public void popValues(List<Node> values) {
right = values.remove(0);
left = values.remove(0);
}
}
private static final class NotNode extends OppNode {
@Override
public boolean evaluate() {
return !left.evaluate();
}
@Override
public int getPrecedence() {
return PRECEDENCE_NOT;
}
/**
* Overridden to pop only one value.
*/
@Override
public void popValues(List<Node> values) {
left = values.remove(0);
}
@Override
public String toString() {
return left + " NOT";
}
}
private static final class AndNode extends OppNode {
@Override
public boolean evaluate() {
if (!left.evaluate()) // Short circuit
{
return false;
}
return right.evaluate();
}
@Override
public int getPrecedence() {
return PRECEDENCE_LOGICAL;
}
@Override
public String toString() {
return left + " " + right + " AND";
}
}
private static final class OrNode extends OppNode {
@Override
public boolean evaluate() {
if (left.evaluate()) // Short circuit
{
return true;
}
return right.evaluate();
}
@Override
public int getPrecedence() {
return PRECEDENCE_LOGICAL;
}
@Override
public String toString() {
return left + " " + right + " OR";
}
}
private abstract static class CompareNode extends OppNode {
protected int compareBranches() {
String val1 = ((StringNode)left).getValue();
String val2 = ((StringNode)right).getValue();
int val2Len = val2.length();
if (val2Len > 1 && val2.charAt(0) == '/' &&
val2.charAt(val2Len - 1) == '/') {
// Treat as a regular expression
String expr = val2.substring(1, val2Len - 1);
try {
Pattern pattern = Pattern.compile(expr);
// Regular expressions will only ever be used with EqualNode
// so return zero for equal and non-zero for not equal
if (pattern.matcher(val1).find()) {
return 0;
}
else {
return -1;
}
}
catch (PatternSyntaxException e) {
SsiProcessor.LOG.warn("Invalid expression: " + expr, e);
return 0;
}
}
return val1.compareTo(val2);
}
}
private static final class EqualNode extends CompareNode {
@Override
public boolean evaluate() {
return (compareBranches() == 0);
}
@Override
public int getPrecedence() {
return PRECEDENCE_COMPARE;
}
@Override
public String toString() {
return left + " " + right + " EQ";
}
}
private static final class GreaterThanNode extends CompareNode {
@Override
public boolean evaluate() {
return (compareBranches() > 0);
}
@Override
public int getPrecedence() {
return PRECEDENCE_COMPARE;
}
@Override
public String toString() {
return left + " " + right + " GT";
}
}
private static final class LessThanNode extends CompareNode {
@Override
public boolean evaluate() {
return (compareBranches() < 0);
}
@Override
public int getPrecedence() {
return PRECEDENCE_COMPARE;
}
@Override
public String toString() {
return left + " " + right + " LT";
}
}
}
@@ -0,0 +1,182 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.jetbrains.builtInWebServer.ssi;
/**
* Parses an expression string to return the individual tokens. This is
* patterned similar to the StreamTokenizer in the JDK but customized for SSI
* conditional expression parsing.
*
* @author Paul Speed
*/
public class ExpressionTokenizer {
public static final int TOKEN_STRING = 0;
public static final int TOKEN_AND = 1;
public static final int TOKEN_OR = 2;
public static final int TOKEN_NOT = 3;
public static final int TOKEN_EQ = 4;
public static final int TOKEN_NOT_EQ = 5;
public static final int TOKEN_RBRACE = 6;
public static final int TOKEN_LBRACE = 7;
public static final int TOKEN_GE = 8;
public static final int TOKEN_LE = 9;
public static final int TOKEN_GT = 10;
public static final int TOKEN_LT = 11;
public static final int TOKEN_END = 12;
private final char[] expr;
private String tokenVal = null;
private int index;
private final int length;
/**
* Creates a new parser for the specified expression.
*/
public ExpressionTokenizer(String expr) {
this.expr = expr.trim().toCharArray();
this.length = this.expr.length;
}
/**
* Returns true if there are more tokens.
*/
public boolean hasMoreTokens() {
return index < length;
}
/**
* Returns the current index for error reporting purposes.
*/
public int getIndex() {
return index;
}
protected boolean isMetaChar(char c) {
return Character.isWhitespace(c) || c == '(' || c == ')' || c == '!'
|| c == '<' || c == '>' || c == '|' || c == '&' || c == '=';
}
/**
* Returns the next token type and initializes any state variables
* accordingly.
*/
public int nextToken() {
// Skip any leading white space
while (index < length && Character.isWhitespace(expr[index])) {
index++;
}
// Clear the current token val
tokenVal = null;
if (index == length) return TOKEN_END; // End of string
int start = index;
char currentChar = expr[index];
char nextChar = (char)0;
index++;
if (index < length) nextChar = expr[index];
// Check for a known token start
switch (currentChar) {
case '(':
return TOKEN_LBRACE;
case ')':
return TOKEN_RBRACE;
case '=':
return TOKEN_EQ;
case '!':
if (nextChar == '=') {
index++;
return TOKEN_NOT_EQ;
}
return TOKEN_NOT;
case '|':
if (nextChar == '|') {
index++;
return TOKEN_OR;
}
break;
case '&':
if (nextChar == '&') {
index++;
return TOKEN_AND;
}
break;
case '>':
if (nextChar == '=') {
index++;
return TOKEN_GE; // Greater than or equal
}
return TOKEN_GT; // Greater than
case '<':
if (nextChar == '=') {
index++;
return TOKEN_LE; // Less than or equal
}
return TOKEN_LT; // Less than
default:
// Otherwise it's a string
break;
}
int end = index;
if (currentChar == '"' || currentChar == '\'') {
// It's a quoted string and the end is the next unescaped quote
char endChar = currentChar;
boolean escaped = false;
start++;
for (; index < length; index++) {
if (expr[index] == '\\' && !escaped) {
escaped = true;
continue;
}
if (expr[index] == endChar && !escaped) break;
escaped = false;
}
end = index;
index++; // Skip the end quote
}
else if (currentChar == '/') {
// It's a regular expression and the end is the next unescaped /
char endChar = currentChar;
boolean escaped = false;
for (; index < length; index++) {
if (expr[index] == '\\' && !escaped) {
escaped = true;
continue;
}
if (expr[index] == endChar && !escaped) break;
escaped = false;
}
end = ++index;
}
else {
// End is the next whitespace character
for (; index < length; index++) {
if (isMetaChar(expr[index])) break;
}
end = index;
}
// Extract the string from the array
this.tokenVal = new String(expr, start, end - start);
return TOKEN_STRING;
}
/**
* Returns the String value of the token if it was type TOKEN_STRING.
* Otherwise null is returned.
*/
public String getTokenValue() {
return tokenVal;
}
}
@@ -0,0 +1,25 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* 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.
*/
package org.jetbrains.builtInWebServer.ssi;
import io.netty.buffer.ByteBufUtf8Writer;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public interface SsiCommand {
long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer);
}
@@ -0,0 +1,126 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.jetbrains.builtInWebServer.ssi;
import io.netty.buffer.ByteBufUtf8Writer;
import org.jetbrains.annotations.NotNull;
import java.text.ParseException;
import java.util.List;
/**
* SSI command that handles all conditional directives.
*
* @author Paul Speed
* @author David Becker
*/
public class SsiConditional implements SsiCommand {
@SuppressWarnings("SpellCheckingInspection")
@Override
public long process(@NotNull SsiProcessingState ssiProcessingState, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
// Assume anything using conditionals was modified by it
long lastModified = System.currentTimeMillis();
// Retrieve the current state information
SsiProcessingState.SsiConditionalState state = ssiProcessingState.conditionalState;
if ("if".equalsIgnoreCase(commandName)) {
// Do nothing if we are nested in a false branch
// except count it
if (state.processConditionalCommandsOnly) {
state.nestingCount++;
return lastModified;
}
state.nestingCount = 0;
// Evaluate the expression
if (evaluateArguments(paramNames, paramValues, ssiProcessingState)) {
// No more branches can be taken for this if block
state.branchTaken = true;
}
else {
// Do not process this branch
state.processConditionalCommandsOnly = true;
state.branchTaken = false;
}
}
else if ("elif".equalsIgnoreCase(commandName)) {
// No need to even execute if we are nested in
// a false branch
if (state.nestingCount > 0) return lastModified;
// If a branch was already taken in this if block
// then disable output and return
if (state.branchTaken) {
state.processConditionalCommandsOnly = true;
return lastModified;
}
// Evaluate the expression
if (evaluateArguments(paramNames, paramValues, ssiProcessingState)) {
// Turn back on output and mark the branch
state.processConditionalCommandsOnly = false;
state.branchTaken = true;
}
else {
// Do not process this branch
state.processConditionalCommandsOnly = true;
state.branchTaken = false;
}
}
else if ("else".equalsIgnoreCase(commandName)) {
// No need to even execute if we are nested in
// a false branch
if (state.nestingCount > 0) return lastModified;
// If we've already taken another branch then
// disable output otherwise enable it.
state.processConditionalCommandsOnly = state.branchTaken;
// And in any case, it's safe to say a branch
// has been taken.
state.branchTaken = true;
}
else if ("endif".equalsIgnoreCase(commandName)) {
// If we are nested inside a false branch then pop out
// one level on the nesting count
if (state.nestingCount > 0) {
state.nestingCount--;
return lastModified;
}
// Turn output back on
state.processConditionalCommandsOnly = false;
// Reset the branch status for any outer if blocks,
// since clearly we took a branch to have gotten here
// in the first place.
state.branchTaken = true;
}
else {
throw new SsiStopProcessingException();
}
return lastModified;
}
/**
* Retrieves the expression from the specified arguments and performs the necessary evaluation steps.
*/
private static boolean evaluateArguments(@NotNull List<String> names, @NotNull String[] values, @NotNull SsiProcessingState ssiProcessingState) {
String expression = "expr".equalsIgnoreCase(names.get(0)) ? values[0] : null;
if (expression == null) {
throw new SsiStopProcessingException();
}
try {
return new ExpressionParseTree(expression, ssiProcessingState).evaluateTree();
}
catch (ParseException e) {
throw new SsiStopProcessingException();
}
}
}
@@ -0,0 +1,97 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* 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.
*/
package org.jetbrains.builtInWebServer.ssi;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import gnu.trove.THashMap;
import io.netty.handler.codec.http.HttpRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.builtInWebServer.WebServerPathToFileManager;
import java.util.Collection;
import java.util.Map;
public final class SsiExternalResolver {
private final String[] VARIABLE_NAMES = {"AUTH_TYPE", "CONTENT_LENGTH",
"CONTENT_TYPE", "DOCUMENT_NAME", "DOCUMENT_URI",
"GATEWAY_INTERFACE", "HTTP_ACCEPT", "HTTP_ACCEPT_ENCODING",
"HTTP_ACCEPT_LANGUAGE", "HTTP_CONNECTION", "HTTP_HOST",
"HTTP_REFERER", "HTTP_USER_AGENT", "PATH_INFO", "PATH_TRANSLATED",
"QUERY_STRING", "QUERY_STRING_UNESCAPED", "REMOTE_ADDR",
"REMOTE_HOST", "REMOTE_PORT", "REMOTE_USER", "REQUEST_METHOD",
"REQUEST_URI", "SCRIPT_FILENAME", "SCRIPT_NAME", "SERVER_ADDR",
"SERVER_NAME", "SERVER_PORT", "SERVER_PROTOCOL", "SERVER_SOFTWARE",
"UNIQUE_ID"};
private final Project project;
private final HttpRequest request;
private Map<String, String> variables = new THashMap<String, String>();
private final String parentPath;
@NotNull private final VirtualFile parentFile;
public SsiExternalResolver(@NotNull Project project,
@NotNull HttpRequest request,
@NotNull String parentPath,
@NotNull VirtualFile parentFile) {
this.project = project;
this.request = request;
this.parentPath = parentPath;
this.parentFile = parentFile;
}
public void addVariableNames(@NotNull Collection<String> variableNames) {
for (String variableName : VARIABLE_NAMES) {
String variableValue = getVariableValue(variableName);
if (variableValue != null) {
variableNames.add(variableName);
}
}
}
public void setVariableValue(@NotNull String name, String value) {
variables.put(name, value);
}
public String getVariableValue(@NotNull String name) {
String value = variables.get(name);
return value == null ? request.headers().get(name) : value;
}
@Nullable
public VirtualFile findFile(@NotNull String originalPath, boolean virtual) {
String path = FileUtil.toCanonicalPath(originalPath, '/');
if (!virtual) {
return parentFile.findFileByRelativePath(path);
}
path = path.charAt(0) == '/' ? path : (parentPath + '/' + path);
return WebServerPathToFileManager.getInstance(project).get(path);
}
public long getFileLastModified(String path, boolean virtual) {
VirtualFile file = findFile(path, virtual);
return file == null ? 0 : file.getTimeStamp();
}
public long getFileSize(@NotNull String path, boolean virtual) {
VirtualFile file = findFile(path, virtual);
return file == null ? -1 : file.getLength();
}
}
@@ -0,0 +1,97 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.jetbrains.builtInWebServer.ssi;
import io.netty.buffer.ByteBufUtf8Writer;
import org.jetbrains.annotations.NotNull;
import java.text.DecimalFormat;
import java.util.List;
/**
* Implements the Server-side #fsize command
*
* @author Bip Thelin
* @author Paul Speed
* @author Dan Sandberg
* @author David Becker
*/
final class SsiFsize implements SsiCommand {
private static final int ONE_KILOBYTE = 1024;
private static final int ONE_MEGABYTE = 1024 * 1024;
@Override
public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
long lastModified = 0;
String configErrMsg = state.configErrorMessage;
for (int i = 0; i < paramNames.size(); i++) {
String paramName = paramNames.get(i);
String paramValue = paramValues[i];
String substitutedValue = state.substituteVariables(paramValue);
if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) {
boolean virtual = paramName.equalsIgnoreCase("virtual");
lastModified = state.ssiExternalResolver.getFileLastModified(substitutedValue, virtual);
writer.write(formatSize(state.ssiExternalResolver.getFileSize(substitutedValue, virtual), state.configSizeFmt));
}
else {
SsiProcessor.LOG.info("#fsize--Invalid attribute: " + paramName);
writer.write(configErrMsg);
}
}
return lastModified;
}
// We try to mimic Apache here, as we do everywhere
// All the 'magic' numbers are from the util_script.c Apache source file.
private static String formatSize(long size, @NotNull String format) {
if (format.equalsIgnoreCase("bytes")) {
return new DecimalFormat("#,##0").format(size);
}
String result;
if (size == 0) {
result = "0k";
}
else if (size < ONE_KILOBYTE) {
result = "1k";
}
else if (size < ONE_MEGABYTE) {
result = Long.toString((size + 512) / ONE_KILOBYTE) + "k";
}
else if (size < 99 * ONE_MEGABYTE) {
result = new DecimalFormat("0.0M").format(size / (double)ONE_MEGABYTE);
}
else {
result = Long.toString((size + (529 * ONE_KILOBYTE)) / ONE_MEGABYTE) + "M";
}
int charsToAdd = 5 - result.length();
if (charsToAdd < 0) {
throw new IllegalArgumentException("Num chars can't be negative");
}
if (charsToAdd == 0) {
return result;
}
StringBuilder buf = new StringBuilder();
for (int i = 0; i < charsToAdd; i++) {
buf.append(' ');
}
return buf.append(result).toString();
}
}
@@ -0,0 +1,202 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* 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.
*/
package org.jetbrains.builtInWebServer.ssi;
import com.google.common.escape.Escaper;
import com.google.common.html.HtmlEscapers;
import com.google.common.net.PercentEscaper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
public class SsiProcessingState {
protected static final String DEFAULT_CONFIG_ERR_MSG = "[an error occurred while processing this directive]";
protected static final String DEFAULT_CONFIG_TIME_FMT = "%A, %d-%b-%Y %T %Z";
protected static final String DEFAULT_CONFIG_SIZE_FMT = "abbrev";
// encode only the same characters that apache does
protected static final Escaper urlEscaper = new PercentEscaper(",:-_.*/!~'()", false);
protected String configErrorMessage = DEFAULT_CONFIG_ERR_MSG;
protected String configTimeFmt = DEFAULT_CONFIG_TIME_FMT;
protected String configSizeFmt = DEFAULT_CONFIG_SIZE_FMT;
protected final SsiExternalResolver ssiExternalResolver;
protected final long lastModifiedDate;
protected Strftime strftime;
protected final SsiConditionalState conditionalState = new SsiConditionalState();
private boolean alreadySet;
public SsiProcessingState(@NotNull SsiExternalResolver ssiExternalResolver, long lastModifiedDate) {
this.ssiExternalResolver = ssiExternalResolver;
this.lastModifiedDate = lastModifiedDate;
setConfigTimeFormat(DEFAULT_CONFIG_TIME_FMT, true);
}
static class SsiConditionalState {
/**
* Set to true if the current conditional has already been completed, i.e.:
* a branch was taken.
*/
boolean branchTaken = false;
/**
* Counts the number of nested false branches.
*/
int nestingCount = 0;
/**
* Set to true if only conditional commands ( if, elif, else, endif )
* should be processed.
*/
boolean processConditionalCommandsOnly = false;
}
public void setConfigTimeFormat(String configTimeFmt, boolean fromConstructor) {
this.configTimeFmt = configTimeFmt;
this.strftime = new Strftime(configTimeFmt, Locale.US);
setDateVariables(fromConstructor);
}
public String getVariableValue(String variableName) {
return getVariableValue(variableName, "none");
}
public String getVariableValue(@NotNull String variableName, String encoding) {
String variableValue = ssiExternalResolver.getVariableValue(variableName);
return variableValue == null ? null : encode(variableValue, encoding);
}
/**
* Applies variable substitution to the specified String and returns the
* new resolved string.
*/
@SuppressWarnings("AssignmentToForLoopParameter")
public String substituteVariables(String val) {
// If it has no references or HTML entities then no work
// need to be done
if (val.indexOf('$') < 0 && val.indexOf('&') < 0) {
return val;
}
val = val.replace("&lt;", "<");
val = val.replace("&gt;", ">");
val = val.replace("&quot;", "\"");
val = val.replace("&amp;", "&");
StringBuilder sb = new StringBuilder(val);
int charStart = sb.indexOf("&#");
while (charStart > -1) {
int charEnd = sb.indexOf(";", charStart);
if (charEnd > -1) {
char c = (char)Integer.parseInt(
sb.substring(charStart + 2, charEnd));
sb.delete(charStart, charEnd + 1);
sb.insert(charStart, c);
charStart = sb.indexOf("&#");
}
else {
break;
}
}
for (int i = 0; i < sb.length(); ) {
// Find the next $
for (; i < sb.length(); i++) {
if (sb.charAt(i) == '$') {
i++;
break;
}
}
if (i == sb.length()) break;
// Check to see if the $ is escaped
if (i > 1 && sb.charAt(i - 2) == '\\') {
sb.deleteCharAt(i - 2);
i--;
continue;
}
int nameStart = i;
int start = i - 1;
int end;
int nameEnd;
char endChar = ' ';
// Check for {} wrapped var
if (sb.charAt(i) == '{') {
nameStart++;
endChar = '}';
}
// Find the end of the var reference
for (; i < sb.length(); i++) {
if (sb.charAt(i) == endChar) break;
}
end = i;
nameEnd = end;
if (endChar == '}') end++;
// We should now have enough to extract the var name
String varName = sb.substring(nameStart, nameEnd);
String value = getVariableValue(varName);
if (value == null) value = "";
// Replace the var name with its value
sb.replace(start, end, value);
// Start searching for the next $ after the value
// that was just substituted.
i = start + value.length();
}
return sb.toString();
}
protected void setDateVariables(boolean fromConstructor) {
if (fromConstructor && alreadySet) {
return;
}
alreadySet = true;
Date date = new Date();
TimeZone timeZone = TimeZone.getTimeZone("GMT");
ssiExternalResolver.setVariableValue("DATE_GMT", formatDate(date, timeZone));
ssiExternalResolver.setVariableValue("DATE_LOCAL", formatDate(date, null));
ssiExternalResolver.setVariableValue("LAST_MODIFIED", formatDate(new Date(lastModifiedDate), null));
}
@NotNull
protected String formatDate(@NotNull Date date, @Nullable TimeZone timeZone) {
if (timeZone == null) {
return strftime.format(date);
}
else {
TimeZone oldTimeZone = strftime.getTimeZone();
strftime.setTimeZone(timeZone);
String retVal = strftime.format(date);
strftime.setTimeZone(oldTimeZone);
return retVal;
}
}
@NotNull
protected String encode(@NotNull String value, @NotNull String encoding) {
if (encoding.equalsIgnoreCase("url")) {
return urlEscaper.escape(value);
}
else if (encoding.equalsIgnoreCase("none")) {
return value;
}
else if (encoding.equalsIgnoreCase("entity")) {
return HtmlEscapers.htmlEscaper().escape(value);
}
else {
throw new IllegalArgumentException("Unknown encoding: " + encoding);
}
}
}
@@ -0,0 +1,440 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* 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.
*/
package org.jetbrains.builtInWebServer.ssi;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.SmartList;
import gnu.trove.THashMap;
import io.netty.buffer.ByteBufUtf8Writer;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
public class SsiProcessor {
static final Logger LOG = Logger.getInstance(SsiProcessor.class);
protected static final String COMMAND_START = "<!--#";
protected static final String COMMAND_END = "-->";
protected final Map<String, SsiCommand> commands = new THashMap<String, SsiCommand>();
@SuppressWarnings("SpellCheckingInspection")
public SsiProcessor(boolean allowExec) {
commands.put("config", new SsiCommand() {
@Override
public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
for (int i = 0; i < paramNames.size(); i++) {
String paramName = paramNames.get(i);
String paramValue = paramValues[i];
String substitutedValue = state.substituteVariables(paramValue);
if (paramName.equalsIgnoreCase("errmsg")) {
state.configErrorMessage = substitutedValue;
}
else if (paramName.equalsIgnoreCase("sizefmt")) {
state.configSizeFmt = substitutedValue;
}
else if (paramName.equalsIgnoreCase("timefmt")) {
state.setConfigTimeFormat(substitutedValue, false);
}
else {
LOG.info("#config--Invalid attribute: " + paramName);
// We need to fetch this value each time, since it may change during the loop
writer.write(state.configErrorMessage);
}
}
return 0;
}
});
commands.put("echo", new SsiCommand() {
@Override
public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
String encoding = "entity";
String originalValue = null;
String errorMessage = state.configErrorMessage;
for (int i = 0; i < paramNames.size(); i++) {
String paramName = paramNames.get(i);
String paramValue = paramValues[i];
if (paramName.equalsIgnoreCase("var")) {
originalValue = paramValue;
}
else if (paramName.equalsIgnoreCase("encoding")) {
if (paramValue.equalsIgnoreCase("url") || paramValue.equalsIgnoreCase("entity") || paramValue.equalsIgnoreCase("none")) {
encoding = paramValue;
}
else {
LOG.info("#echo--Invalid encoding: " + paramValue);
writer.write(errorMessage);
}
}
else {
LOG.info("#echo--Invalid attribute: " + paramName);
writer.write(errorMessage);
}
}
assert originalValue != null;
String variableValue = state.getVariableValue(originalValue, encoding);
writer.write(variableValue == null ? "(none)" : variableValue);
return System.currentTimeMillis();
}
});
//noinspection StatementWithEmptyBody
if (allowExec) {
// commands.put("exec", new SsiExec());
}
commands.put("include", new SsiCommand() {
@Override
public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
long lastModified = 0;
String configErrorMessage = state.configErrorMessage;
for (int i = 0; i < paramNames.size(); i++) {
String paramName = paramNames.get(i);
if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) {
String substitutedValue = state.substituteVariables(paramValues[i]);
try {
boolean virtual = paramName.equalsIgnoreCase("virtual");
lastModified = state.ssiExternalResolver.getFileLastModified(substitutedValue, virtual);
VirtualFile file = state.ssiExternalResolver.findFile(substitutedValue, virtual);
if (file == null) {
LOG.warn("#include-- Couldn't find file: " + substitutedValue);
return 0;
}
InputStream in = file.getInputStream();
try {
writer.write(in, (int)file.getLength());
}
finally {
in.close();
}
}
catch (IOException e) {
LOG.warn("#include--Couldn't include file: " + substitutedValue, e);
writer.write(configErrorMessage);
}
}
else {
LOG.info("#include--Invalid attribute: " + paramName);
writer.write(configErrorMessage);
}
}
return lastModified;
}
});
commands.put("flastmod", new SsiCommand() {
@Override
public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
long lastModified = 0;
String configErrMsg = state.configErrorMessage;
for (int i = 0; i < paramNames.size(); i++) {
String paramName = paramNames.get(i);
String paramValue = paramValues[i];
String substitutedValue = state.substituteVariables(paramValue);
if (paramName.equalsIgnoreCase("file") || paramName.equalsIgnoreCase("virtual")) {
boolean virtual = paramName.equalsIgnoreCase("virtual");
lastModified = state.ssiExternalResolver.getFileLastModified(substitutedValue, virtual);
Strftime strftime = new Strftime(state.configTimeFmt, Locale.US);
writer.write(strftime.format(new Date(lastModified)));
}
else {
LOG.info("#flastmod--Invalid attribute: " + paramName);
writer.write(configErrMsg);
}
}
return lastModified;
}
});
commands.put("fsize", new SsiFsize());
commands.put("printenv", new SsiCommand() {
@Override
public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
long lastModified = 0;
// any arguments should produce an error
if (paramNames.isEmpty()) {
Set<String> variableNames = new LinkedHashSet<String>();
//These built-in variables are supplied by the mediator ( if not over-written by the user ) and always exist
variableNames.add("DATE_GMT");
variableNames.add("DATE_LOCAL");
variableNames.add("LAST_MODIFIED");
state.ssiExternalResolver.addVariableNames(variableNames);
for (String variableName : variableNames) {
String variableValue = state.getVariableValue(variableName);
// This shouldn't happen, since all the variable names must have values
if (variableValue == null) {
variableValue = "(none)";
}
writer.write(variableName);
writer.write('=');
writer.write(variableValue);
writer.write('\n');
lastModified = System.currentTimeMillis();
}
}
else {
writer.write(state.configErrorMessage);
}
return lastModified;
}
});
commands.put("set", new SsiCommand() {
@Override
public long process(@NotNull SsiProcessingState state, @NotNull String commandName, @NotNull List<String> paramNames, @NotNull String[] paramValues, @NotNull ByteBufUtf8Writer writer) {
long lastModified = 0;
String errorMessage = state.configErrorMessage;
String variableName = null;
for (int i = 0; i < paramNames.size(); i++) {
String paramName = paramNames.get(i);
String paramValue = paramValues[i];
if (paramName.equalsIgnoreCase("var")) {
variableName = paramValue;
}
else if (paramName.equalsIgnoreCase("value")) {
if (variableName != null) {
String substitutedValue = state.substituteVariables(paramValue);
state.ssiExternalResolver.setVariableValue(variableName, substitutedValue);
lastModified = System.currentTimeMillis();
}
else {
LOG.info("#set--no variable specified");
writer.write(errorMessage);
throw new SsiStopProcessingException();
}
}
else {
LOG.info("#set--Invalid attribute: " + paramName);
writer.write(errorMessage);
throw new SsiStopProcessingException();
}
}
return lastModified;
}
});
SsiConditional ssiConditional = new SsiConditional();
commands.put("if", ssiConditional);
commands.put("elif", ssiConditional);
commands.put("endif", ssiConditional);
commands.put("else", ssiConditional);
}
/**
* @return the most current modified date resulting from any SSI commands
*/
public long process(@NotNull SsiExternalResolver ssiExternalResolver, @NotNull String fileContents, long lastModifiedDate, @NotNull ByteBufUtf8Writer writer) {
SsiProcessingState ssiProcessingState = new SsiProcessingState(ssiExternalResolver, lastModifiedDate);
int index = 0;
boolean inside = false;
StringBuilder command = new StringBuilder();
try {
while (index < fileContents.length()) {
char c = fileContents.charAt(index);
if (inside) {
if (c == COMMAND_END.charAt(0) && charCmp(fileContents, index, COMMAND_END)) {
inside = false;
index += COMMAND_END.length();
String commandName = parseCommand(command);
if (LOG.isDebugEnabled()) {
LOG.debug("SSIProcessor.process -- processing command: " + commandName);
}
List<String> paramNames = parseParamNames(command, commandName.length());
String[] paramValues = parseParamValues(command, commandName.length(), paramNames.size());
// We need to fetch this value each time, since it may change during the loop
String configErrMsg = ssiProcessingState.configErrorMessage;
SsiCommand ssiCommand = commands.get(commandName.toLowerCase(Locale.ENGLISH));
String errorMessage = null;
if (ssiCommand == null) {
errorMessage = "Unknown command: " + commandName;
}
else if (paramValues == null) {
errorMessage = "Error parsing directive parameters.";
}
else if (paramNames.size() != paramValues.length) {
errorMessage = "Parameter names count does not match parameter values count on command: " + commandName;
}
else {
// don't process the command if we are processing conditional commands only and the command is not conditional
if (!ssiProcessingState.conditionalState.processConditionalCommandsOnly || ssiCommand instanceof SsiConditional) {
long newLastModified = ssiCommand.process(ssiProcessingState, commandName, paramNames, paramValues, writer);
if (newLastModified > lastModifiedDate) {
lastModifiedDate = newLastModified;
}
}
}
if (errorMessage != null) {
LOG.warn(errorMessage);
writer.write(configErrMsg);
}
}
else {
command.append(c);
index++;
}
}
else if (c == COMMAND_START.charAt(0) && charCmp(fileContents, index, COMMAND_START)) {
inside = true;
index += COMMAND_START.length();
command.setLength(0);
}
else {
if (!ssiProcessingState.conditionalState.processConditionalCommandsOnly) {
writer.write(c);
}
index++;
}
}
}
catch (SsiStopProcessingException e) {
//If we are here, then we have already stopped processing, so all is good
}
return lastModifiedDate;
}
@NotNull
protected List<String> parseParamNames(@NotNull StringBuilder command, int start) {
int bIdx = start;
List<String> values = new SmartList<String>();
boolean inside = false;
StringBuilder builder = new StringBuilder();
while (bIdx < command.length()) {
if (inside) {
while (bIdx < command.length() && command.charAt(bIdx) != '=') {
builder.append(command.charAt(bIdx));
bIdx++;
}
values.add(builder.toString());
builder.setLength(0);
inside = false;
int quotes = 0;
boolean escaped = false;
for (; bIdx < command.length() && quotes != 2; bIdx++) {
char c = command.charAt(bIdx);
// Need to skip escaped characters
if (c == '\\' && !escaped) {
escaped = true;
continue;
}
if (c == '"' && !escaped) {
quotes++;
}
escaped = false;
}
}
else {
while (bIdx < command.length() && isSpace(command.charAt(bIdx))) {
bIdx++;
}
if (bIdx >= command.length()) {
break;
}
inside = true;
}
}
return values;
}
@SuppressWarnings("AssignmentToForLoopParameter")
protected String[] parseParamValues(@NotNull StringBuilder command, int start, int count) {
int valueIndex = 0;
boolean inside = false;
String[] values = new String[count];
StringBuilder builder = new StringBuilder();
char endQuote = 0;
for (int bIdx = start; bIdx < command.length(); bIdx++) {
if (!inside) {
while (bIdx < command.length() && !isQuote(command.charAt(bIdx))) {
bIdx++;
}
if (bIdx >= command.length()) {
break;
}
inside = true;
endQuote = command.charAt(bIdx);
}
else {
boolean escaped = false;
for (; bIdx < command.length(); bIdx++) {
char c = command.charAt(bIdx);
// Check for escapes
if (c == '\\' && !escaped) {
escaped = true;
continue;
}
// If we reach the other " then stop
if (c == endQuote && !escaped) {
break;
}
// Since parsing of attributes and var
// substitution is done in separate places,
// we need to leave escape in the string
if (c == '$' && escaped) {
builder.append('\\');
}
escaped = false;
builder.append(c);
}
// If we hit the end without seeing a quote
// the signal an error
if (bIdx == command.length()) {
return null;
}
values[valueIndex++] = builder.toString();
builder.setLength(0);
inside = false;
}
}
return values;
}
@NotNull
private String parseCommand(@NotNull StringBuilder instruction) {
int firstLetter = -1;
int lastLetter = -1;
for (int i = 0; i < instruction.length(); i++) {
char c = instruction.charAt(i);
if (Character.isLetter(c)) {
if (firstLetter == -1) {
firstLetter = i;
}
lastLetter = i;
}
else if (isSpace(c)) {
if (lastLetter > -1) {
break;
}
}
else {
break;
}
}
return firstLetter == -1 ? "" : instruction.substring(firstLetter, lastLetter + 1);
}
protected boolean charCmp(String buf, int index, String command) {
return buf.regionMatches(index, command, 0, command.length());
}
protected boolean isSpace(char c) {
return c == ' ' || c == '\n' || c == '\t' || c == '\r';
}
protected boolean isQuote(char c) {
return c == '\'' || c == '\"' || c == '`';
}
}
@@ -0,0 +1,29 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.jetbrains.builtInWebServer.ssi;
/**
* Exception used to tell SSIProcessor that it should stop processing SSI
* commands. This is used to mimic the Apache behavior in #set with invalid
* attributes.
*
* @author Paul Speed
* @author Dan Sandberg
*/
public class SsiStopProcessingException extends RuntimeException {
}
@@ -0,0 +1,255 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.jetbrains.builtInWebServer.ssi;
import gnu.trove.THashMap;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
/**
* Converts dates to strings using the same format specifiers as strftime
* <p/>
* Note: This does not mimic strftime perfectly. Certain strftime commands,
* are not supported, and will convert as if they were literals.
* <p/>
* Certain complicated commands, like those dealing with the week of the year
* probably don't have exactly the same behavior as strftime.
* <p/>
* These limitations are due to use SimpleDateTime. If the conversion was done
* manually, all these limitations could be eliminated.
* <p/>
* The interface looks like a subset of DateFormat. Maybe someday someone will make this class
* extend DateFormat.
*
* @author Bip Thelin
* @author Dan Sandberg
*/
@SuppressWarnings("SpellCheckingInspection")
public class Strftime {
protected static final Map<String, String> translate = new THashMap<String, String>();
protected final SimpleDateFormat simpleDateFormat;
static {
translate.put("a", "EEE");
translate.put("A", "EEEE");
translate.put("b", "MMM");
translate.put("B", "MMMM");
translate.put("c", "EEE MMM d HH:mm:ss yyyy");
//There's no way to specify the century in SimpleDateFormat. We don't want to hard-code
//20 since this could be wrong for the pre-2000 files.
//translate.put("C", "20");
translate.put("d", "dd");
translate.put("D", "MM/dd/yy");
translate.put("e", "dd"); //will show as '03' instead of ' 3'
translate.put("F", "yyyy-MM-dd");
translate.put("g", "yy");
translate.put("G", "yyyy");
translate.put("H", "HH");
translate.put("h", "MMM");
translate.put("I", "hh");
translate.put("j", "DDD");
translate.put("k", "HH"); //will show as '07' instead of ' 7'
translate.put("l", "hh"); //will show as '07' instead of ' 7'
translate.put("m", "MM");
translate.put("M", "mm");
translate.put("n", "\n");
translate.put("p", "a");
translate.put("P", "a"); //will show as pm instead of PM
translate.put("r", "hh:mm:ss a");
translate.put("R", "HH:mm");
//There's no way to specify this with SimpleDateFormat
//translate.put("s","seconds since epoch");
translate.put("S", "ss");
translate.put("t", "\t");
translate.put("T", "HH:mm:ss");
//There's no way to specify this with SimpleDateFormat
//translate.put("u","day of week ( 1-7 )");
//There's no way to specify this with SimpleDateFormat
//translate.put("U","week in year with first Sunday as first day...");
translate.put("V", "ww"); //I'm not sure this is always exactly the same
//There's no way to specify this with SimpleDateFormat
//translate.put("W","week in year with first Monday as first day...");
//There's no way to specify this with SimpleDateFormat
//translate.put("w","E");
translate.put("X", "HH:mm:ss");
translate.put("x", "MM/dd/yy");
translate.put("y", "yy");
translate.put("Y", "yyyy");
translate.put("Z", "z");
translate.put("z", "Z");
translate.put("%", "%");
}
/**
* Create an instance of this date formatting class
*
* @param origFormat the strftime-style formatting string
* @param locale the locale to use for locale-specific conversions
*/
public Strftime(String origFormat, Locale locale) {
String convertedFormat = convertDateFormat(origFormat);
simpleDateFormat = new SimpleDateFormat(convertedFormat, locale);
}
/**
* Format the date according to the strftime-style string given in the constructor.
*
* @param date the date to format
* @return the formatted date
*/
public String format(Date date) {
return simpleDateFormat.format(date);
}
/**
* Get the timezone used for formatting conversions
*
* @return the timezone
*/
public TimeZone getTimeZone() {
return simpleDateFormat.getTimeZone();
}
/**
* Change the timezone used to format dates
*
* @see SimpleDateFormat#setTimeZone
*/
public void setTimeZone(TimeZone timeZone) {
simpleDateFormat.setTimeZone(timeZone);
}
/**
* Search the provided pattern and get the C standard
* Date/Time formatting rules and convert them to the
* Java equivalent.
*
* @param pattern The pattern to search
* @return The modified pattern
*/
protected String convertDateFormat(String pattern) {
boolean inside = false;
boolean mark = false;
boolean modifiedCommand = false;
StringBuilder buf = new StringBuilder();
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
if (c == '%' && !mark) {
mark = true;
}
else {
if (mark) {
if (modifiedCommand) {
//don't do anything--we just wanted to skip a char
modifiedCommand = false;
mark = false;
}
else {
inside = translateCommand(buf, pattern, i, inside);
//It's a modifier code
if (c == 'O' || c == 'E') {
modifiedCommand = true;
}
else {
mark = false;
}
}
}
else {
if (!inside && c != ' ') {
//We start a literal, which we need to quote
buf.append("'");
inside = true;
}
buf.append(c);
}
}
}
if (buf.length() > 0) {
char lastChar = buf.charAt(buf.length() - 1);
if (lastChar != '\'' && inside) {
buf.append('\'');
}
}
return buf.toString();
}
protected String quote(String str, boolean insideQuotes) {
String retVal = str;
if (!insideQuotes) {
retVal = '\'' + retVal + '\'';
}
return retVal;
}
/**
* Try to get the Java Date/Time formatting associated with
* the C standard provided.
*
* @param buf The buffer
* @param pattern The date/time pattern
* @param index The char index
* @param oldInside Flag value
* @return True if new is inside buffer
*/
protected boolean translateCommand(StringBuilder buf, String pattern, int index, boolean oldInside) {
char firstChar = pattern.charAt(index);
boolean newInside = oldInside;
//O and E are modifiers, they mean to present an alternative representation of the next char
//we just handle the next char as if the O or E wasn't there
if (firstChar == 'O' || firstChar == 'E') {
if (index + 1 < pattern.length()) {
newInside = translateCommand(buf, pattern, index + 1, oldInside);
}
else {
buf.append(quote("%" + firstChar, oldInside));
}
}
else {
String command = translate.get(String.valueOf(firstChar));
//If we don't find a format, treat it as a literal--That's what apache does
if (command == null) {
buf.append(quote("%" + firstChar, oldInside));
}
else {
//If we were inside quotes, close the quotes
if (oldInside) {
buf.append('\'');
}
buf.append(command);
newInside = false;
}
}
return newInside;
}
}
@@ -96,7 +96,7 @@ public class FileResponses {
try {
long fileLength = raf.length();
if (request.method() != HttpMethod.HEAD) {
HttpHeaders.setContentLength(response, fileLength);
HttpHeaderUtil.setContentLength(response, fileLength);
}
channel.write(response);
@@ -19,6 +19,7 @@ import com.intellij.util.text.CharArrayCharSequence;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
public final class ByteBufUtf8Writer extends Writer {
@@ -28,6 +29,10 @@ public final class ByteBufUtf8Writer extends Writer {
this.buffer = buffer;
}
public void write(@NotNull InputStream inputStream, int length) throws IOException {
buffer.writeBytes(inputStream, length);
}
@Override
public void write(int c) {
AbstractByteBuf buffer = ByteBufUtilEx.getBuf(this.buffer);
@@ -53,7 +58,7 @@ public final class ByteBufUtf8Writer extends Writer {
}
@Override
public void write(String str) throws IOException {
public void write(String str) {
ByteBufUtilEx.writeUtf8(buffer, str);
}