mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
WEB-15327 Can you support server side includes in preview server
This commit is contained in:
@@ -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"));
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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;
|
||||
}
|
||||
+448
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
+182
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -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();
|
||||
}
|
||||
}
|
||||
+202
@@ -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("<", "<");
|
||||
val = val.replace(">", ">");
|
||||
val = val.replace(""", "\"");
|
||||
val = val.replace("&", "&");
|
||||
|
||||
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 == '`';
|
||||
}
|
||||
}
|
||||
+29
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user