Author: jasvir
Date: Thu Dec 9 01:51:43 2010
New Revision: 1043815
URL: http://svn.apache.org/viewvc?rev=1043815&view=rev
Log:
Description
This is an endpoint that will be useful for inlining and for supporting safe
dynamic script tags and iframes. Given a url, the end point returns santized
html, cajoled js and lint messages and errors.
Modified:
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/CajaContentRewriter.java
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerService.java
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerServiceTest.java
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java
Modified:
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/CajaContentRewriter.java
URL:
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/CajaContentRewriter.java?rev=1043815&r1=1043814&r2=1043815&view=diff
==============================================================================
---
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/CajaContentRewriter.java
(original)
+++
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/CajaContentRewriter.java
Thu Dec 9 01:51:43 2010
@@ -18,14 +18,23 @@
*/
package org.apache.shindig.gadgets.servlet;
+import com.google.caja.lexer.CharProducer;
import com.google.caja.lexer.ExternalReference;
import com.google.caja.lexer.FetchedData;
+import com.google.caja.lexer.FilePosition;
+import com.google.caja.lexer.HtmlLexer;
import com.google.caja.lexer.InputSource;
+import com.google.caja.lexer.JsLexer;
+import com.google.caja.lexer.JsTokenQueue;
+import com.google.caja.lexer.ParseException;
import com.google.caja.lexer.TokenConsumer;
import com.google.caja.lexer.escaping.Escaping;
+import com.google.caja.parser.ParseTreeNode;
import com.google.caja.parser.html.Dom;
+import com.google.caja.parser.html.DomParser;
import com.google.caja.parser.html.Namespaces;
import com.google.caja.parser.js.CajoledModule;
+import com.google.caja.parser.js.Parser;
import com.google.caja.plugin.PipelineMaker;
import com.google.caja.plugin.PluginCompiler;
import com.google.caja.plugin.PluginMeta;
@@ -33,6 +42,7 @@ import com.google.caja.plugin.LoaderType
import com.google.caja.plugin.UriEffect;
import com.google.caja.plugin.UriFetcher;
import com.google.caja.plugin.UriPolicy;
+import com.google.caja.plugin.UriFetcher.UriFetchException;
import com.google.caja.render.Concatenator;
import com.google.caja.render.JsMinimalPrinter;
import com.google.caja.render.JsPrettyPrinter;
@@ -40,10 +50,14 @@ import com.google.caja.reporting.BuildIn
import com.google.caja.reporting.Message;
import com.google.caja.reporting.MessageContext;
import com.google.caja.reporting.MessageLevel;
+import com.google.caja.reporting.MessagePart;
import com.google.caja.reporting.MessageQueue;
+import com.google.caja.reporting.MessageType;
import com.google.caja.reporting.RenderContext;
import com.google.caja.reporting.SimpleMessageQueue;
import com.google.caja.reporting.SnippetProducer;
+import com.google.caja.service.ServiceMessageType;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
@@ -68,7 +82,9 @@ import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
import java.net.URI;
+import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -80,7 +96,7 @@ public class CajaContentRewriter impleme
public static final String CAJOLED_DOCUMENTS = "cajoledDocuments";
//class name for logging purpose
- private static final String classname =
"org.apache.shindig.gadgets.servlet.CajaContentRewriter";
+ private static final String classname = CajaContentRewriter.class.getName();
private static final Logger LOG =
Logger.getLogger(classname,MessageKeys.MESSAGES);
@@ -91,13 +107,109 @@ public class CajaContentRewriter impleme
@Inject
public CajaContentRewriter(CacheProvider cacheProvider, RequestPipeline
requestPipeline,
HtmlSerializer htmlSerializer) {
- this.cajoledCache = cacheProvider.createCache(CAJOLED_DOCUMENTS);
+ if (null == cacheProvider) {
+ this.cajoledCache = null;
+ } else {
+ this.cajoledCache = cacheProvider.createCache(CAJOLED_DOCUMENTS);
+ }
if (LOG.isLoggable(Level.INFO)) {
- LOG.logp(Level.INFO, classname, "CajaContentRewriter",
MessageKeys.CAJOLED_CACHE_CREATED, new Object[] {cajoledCache});
+ LOG.logp(Level.INFO, classname, "CajaContentRewriter",
MessageKeys.CAJOLED_CACHE_CREATED,
+ new Object[] {cajoledCache});
}
this.requestPipeline = requestPipeline;
this.htmlSerializer = htmlSerializer;
}
+
+ public class CajoledResult {
+ public final Node html;
+ public final CajoledModule js;
+ public final List<Message> messages;
+ public final boolean hasErrors;
+ CajoledResult(Node html, CajoledModule js, List<Message> messages, boolean
hasErrors) {
+ this.html = html;
+ this.js = js;
+ this.messages = messages;
+ this.hasErrors= hasErrors;
+ }
+ public String toString() {
+ return "[html:'" + html + "', js: '" + js + "', messages: '" + messages
+ "']";
+ }
+ }
+
+ @VisibleForTesting
+ ParseTreeNode parse(InputSource is, CharProducer cp, String mime,
MessageQueue mq)
+ throws ParseException {
+ ParseTreeNode ptn;
+ if (mime.contains("javascript")) {
+ JsLexer lexer = new JsLexer(cp);
+ JsTokenQueue tq = new JsTokenQueue(lexer, is);
+ if (tq.isEmpty()) { return null; }
+ Parser p = new Parser(tq, mq);
+ ptn = p.parse();
+ tq.expectEmpty();
+ } else {
+ DomParser p = new DomParser(new HtmlLexer(cp), false, is, mq);
+ ptn = new Dom(p.parseFragment());
+ p.getTokenQueue().expectEmpty();
+ }
+ return ptn;
+ }
+
+ public CajoledResult rewrite(Uri uri, String container, String mime,
+ boolean es53, boolean debug) {
+ URI javaUri = uri.toJavaUri();
+ InputSource is = new InputSource(javaUri);
+ MessageQueue mq = new SimpleMessageQueue();
+ try {
+ UriFetcher fetcher = makeFetcher(uri, container);
+ ExternalReference extRef = new ExternalReference(javaUri,
+ FilePosition.instance(is, /*lineNo*/ 1, /*charInFile*/ 1,
/*charInLine*/ 1));
+ // If the fetch fails, a UriFetchException is thrown and serialized as
part of the
+ // message queue.
+ CharProducer cp = fetcher.fetch(extRef, mime).getTextualContent();
+ ParseTreeNode ptn = parse(is, cp, mime, mq);
+ return rewrite(uri, container, ptn, es53, debug);
+ } catch (UnsupportedEncodingException e) {
+ LOG.severe("Unexpected inability to recognize mime type: " + mime);
+ mq.addMessage(ServiceMessageType.UNEXPECTED_INPUT_MIME_TYPE,
+ MessagePart.Factory.valueOf(mime));
+ } catch (UriFetchException e) {
+ LOG.info("Failed to retrieve: " + e.toString());
+ } catch (ParseException e) {
+ mq.addMessage(MessageType.PARSE_ERROR, FilePosition.UNKNOWN);
+ }
+ return new CajoledResult(null, null, mq.getMessages(), /* hasErrors */
true);
+ }
+
+ public CajoledResult rewrite(Uri gadgetUri, String container,
+ ParseTreeNode root, boolean es53, boolean debug) {
+ UriFetcher fetcher = makeFetcher(gadgetUri, container);
+ UriPolicy policy = makePolicy(gadgetUri);
+ URI javaGadgetUri = gadgetUri.toJavaUri();
+ MessageQueue mq = new SimpleMessageQueue();
+ MessageContext context = new MessageContext();
+ PluginMeta meta = new PluginMeta(fetcher, policy);
+ PluginCompiler compiler = makePluginCompiler(meta, mq);
+ compiler.setMessageContext(context);
+
+ if (debug) {
+ compiler.setGoals(compiler.getGoals()
+ .without(PipelineMaker.ONE_CAJOLED_MODULE)
+ .with(PipelineMaker.ONE_CAJOLED_MODULE_DEBUG));
+ }
+
+ compiler.addInput(root, javaGadgetUri);
+
+ boolean hasErrors = false;
+ if (!compiler.run()) {
+ hasErrors = true;
+ }
+
+ return new CajoledResult(compiler.getStaticHtml(),
+ compiler.getJavascript(),
+ compiler.getMessageQueue().getMessages(),
+ hasErrors);
+ }
public void rewrite(Gadget gadget, MutableContent mc) {
if (!cajaEnabled(gadget)) return;
@@ -123,37 +235,19 @@ public class CajaContentRewriter impleme
}
if (cajoledData == null) {
- UriFetcher fetcher = makeFetcher(gadget);
- UriPolicy policy = makePolicy(gadget);
- URI javaGadgetUri = gadgetContext.getUrl().toJavaUri();
- MessageQueue mq = new SimpleMessageQueue();
- MessageContext context = new MessageContext();
- PluginMeta meta = new PluginMeta(fetcher, policy);
- PluginCompiler compiler = makePluginCompiler(meta, mq);
-
- compiler.setMessageContext(context);
-
if (debug) {
// This will load cajita-debugmode.js
gadget.addFeature("caja-debug");
- compiler.setGoals(compiler.getGoals()
- .without(PipelineMaker.ONE_CAJOLED_MODULE)
- .with(PipelineMaker.ONE_CAJOLED_MODULE_DEBUG));
}
- InputSource is = new InputSource(javaGadgetUri);
- boolean safe = false;
-
- compiler.addInput(new Dom(root), javaGadgetUri);
-
- try {
- if (!compiler.run()) {
- throw new GadgetException(
- GadgetException.Code.MALFORMED_FOR_SAFE_INLINING,
- "Gadget has compile errors");
- }
+ InputSource is = new InputSource(gadgetContext.getUrl().toJavaUri());
+ // TODO(jasvir): Turn on es53 once gadgets apis support it
+ CajoledResult result =
+ rewrite(gadgetContext.getUrl(), gadgetContext.getContainer(),
+ new Dom(root), /* es53 */ false, debug);
+ if (!result.hasErrors) {
StringBuilder scriptBody = new StringBuilder();
- CajoledModule cajoled = compiler.getJavascript();
+ CajoledModule cajoled = result.js;
TokenConsumer tc = debug
? new JsPrettyPrinter(new Concatenator(scriptBody))
: new JsMinimalPrinter(new Concatenator(scriptBody));
@@ -163,7 +257,7 @@ public class CajaContentRewriter impleme
tc.noMoreTokens();
- Node html = compiler.getStaticHtml();
+ Node html = result.html;
Element script = doc.createElementNS(
Namespaces.HTML_NAMESPACE_URI, "script");
@@ -180,9 +274,10 @@ public class CajaContentRewriter impleme
cajoledOutput.appendChild(doc.adoptNode(html));
cajoledOutput.appendChild(tameCajaClientApi(doc));
cajoledOutput.appendChild(doc.adoptNode(script));
-
- Element messagesNode = formatErrors(doc, is, docContent, mq,
- /* is invisible */ false);
+
+ List<Message> messages = result.messages;
+ Element messagesNode = formatErrors(doc, is, docContent, messages,
+ /* invisible */ false);
cajoledOutput.appendChild(messagesNode);
if (cajoledCache != null && !debug) {
cajoledCache.addElement(cacheKey, cajoledOutput);
@@ -191,22 +286,15 @@ public class CajaContentRewriter impleme
cajoledData = cajoledOutput;
createContainerFor(doc, cajoledData);
mc.documentChanged();
- safe = true;
HtmlSerialization.attach(doc, htmlSerializer, null);
- } catch (GadgetException e) {
+ } else {
// There were cajoling errors
// Content is only used to produce useful snippets with error messages
+ List<Message> messages = result.messages;
createContainerFor(doc,
- formatErrors(doc, is, docContent, mq, true /* visible */));
+ formatErrors(doc, is, docContent, messages, true /* visible */));
mc.documentChanged();
- logException("rewrite", e, mq);
- safe = true;
- } finally {
- if (!safe) {
- // Fail safe
- mc.setContent("");
- mc.documentChanged();
- }
+ logException("rewrite", messages);
}
}
}
@@ -216,15 +304,13 @@ public class CajaContentRewriter impleme
"1".equals(gadget.getContext().getParameter("caja")));
}
- private UriFetcher makeFetcher(Gadget gadget) {
- final Uri gadgetUri = gadget.getContext().getUrl();
- final String container = gadget.getContext().getContainer();
-
+ UriFetcher makeFetcher(final Uri gadgetUri, final String container) {
return new UriFetcher() {
public FetchedData fetch(ExternalReference ref, String mimeType)
throws UriFetchException {
if (LOG.isLoggable(Level.INFO)) {
- LOG.logp(Level.INFO, classname, "makeFetcher",
MessageKeys.RETRIEVE_REFERENCE, new Object[] {ref.toString()});
+ LOG.logp(Level.INFO, classname, "makeFetcher",
MessageKeys.RETRIEVE_REFERENCE,
+ new Object[] {ref.toString()});
}
Uri resourceUri = gadgetUri.resolve(Uri.fromJavaUri(ref.getUri()));
HttpRequest request =
@@ -236,23 +322,23 @@ public class CajaContentRewriter impleme
new InputSource(ref.getUri()));
} catch (GadgetException e) {
if (LOG.isLoggable(Level.INFO)) {
- LOG.logp(Level.INFO, classname, "makeFetcher",
MessageKeys.FAILED_TO_RETRIEVE, new Object[] {ref.toString()});
+ LOG.logp(Level.INFO, classname, "makeFetcher",
MessageKeys.FAILED_TO_RETRIEVE,
+ new Object[] {ref.toString()});
}
- return null;
+ throw new UriFetchException(ref, mimeType, e);
} catch (IOException e) {
if (LOG.isLoggable(Level.INFO)) {
- LOG.logp(Level.INFO, classname, "makeFetcher",
MessageKeys.FAILED_TO_READ, new Object[] {ref.toString()});
+ LOG.logp(Level.INFO, classname, "makeFetcher",
MessageKeys.FAILED_TO_READ,
+ new Object[] {ref.toString()});
}
- return null;
+ throw new UriFetchException(ref, mimeType, e);
}
}
};
}
- private UriPolicy makePolicy(Gadget gadget) {
- final Uri gadgetUri = gadget.getContext().getUrl();
-
+ private UriPolicy makePolicy(final Uri gadgetUri) {
return new UriPolicy() {
public String rewriteUri(ExternalReference ref, UriEffect effect,
LoaderType loader, Map<String, ?> hints) {
@@ -277,7 +363,7 @@ public class CajaContentRewriter impleme
}
private Element formatErrors(Document doc, InputSource is,
- CharSequence orig, MessageQueue mq, boolean visible) {
+ CharSequence orig, List<Message> messages, boolean visible) {
MessageContext mc = new MessageContext();
Map<InputSource, CharSequence> originalSrc = Maps.newHashMap();
originalSrc.put(is, orig);
@@ -290,7 +376,7 @@ public class CajaContentRewriter impleme
if (!visible) {
errElement.setAttribute("style", "display: none");
}
- for (Message msg : mq.getMessages()) {
+ for (Message msg : messages) {
// Ignore LINT messages
if (MessageLevel.LINT.compareTo(msg.getMessageLevel()) <= 0) {
String snippet = sp.getSnippet(msg);
@@ -317,17 +403,15 @@ public class CajaContentRewriter impleme
return scriptElement;
}
- private void logException(String methodname, Exception cause, MessageQueue
mq) {
+ private void logException(String methodname, List<Message> messages) {
StringBuilder errbuilder = new StringBuilder();
MessageContext mc = new MessageContext();
- if (cause != null) {
- errbuilder.append(cause).append('\n');
- }
- for (Message m : mq.getMessages()) {
+ for (Message m : messages) {
errbuilder.append(m.format(mc)).append('\n');
}
if (LOG.isLoggable(Level.INFO)) {
- LOG.logp(Level.INFO, classname, methodname,
MessageKeys.UNABLE_TO_CAJOLE, new Object[] {errbuilder});
+ LOG.logp(Level.INFO, classname, methodname, MessageKeys.UNABLE_TO_CAJOLE,
+ new Object[] {errbuilder});
}
}
Modified:
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java
URL:
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java?rev=1043815&r1=1043814&r2=1043815&view=diff
==============================================================================
---
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java
(original)
+++
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java
Thu Dec 9 01:51:43 2010
@@ -68,6 +68,8 @@ public class GadgetsHandler {
@VisibleForTesting
static final String FAILURE_PROXY = "Failed to get proxy data.";
@VisibleForTesting
+ static final String FAILURE_CAJA = "Failed to cajole data.";
+ @VisibleForTesting
static final String FAILURE_JS = "Failed to get js data.";
private static final List<String> DEFAULT_METADATA_FIELDS =
@@ -77,8 +79,9 @@ public class GadgetsHandler {
private static final List<String> DEFAULT_PROXY_FIELDS =
ImmutableList.of("proxyUrl");
+ private static final List<String> DEFAULT_CAJA_FIELDS =
ImmutableList.of("*");
private static final List<String> DEFAULT_JS_FIELDS =
ImmutableList.of("jsUrl");
-
+
private static final Logger LOG =
Logger.getLogger(GadgetsHandler.class.getName());
protected final ExecutorService executor;
@@ -151,6 +154,18 @@ public class GadgetsHandler {
}.execute(request);
}
+ @Operation(httpMethods = {"POST", "GET"}, path = "cajole")
+ public Map<String, GadgetsHandlerApi.BaseResponse> cajole(BaseRequestItem
request)
+ throws ProtocolException {
+ return new AbstractExecutor() {
+ @Override
+ protected Callable<CallableData> createJob(String url, BaseRequestItem
request)
+ throws ProcessingException {
+ return createCajaJob(url, request);
+ }
+ }.execute(request);
+ }
+
@Operation(httpMethods = "GET", path = "/@metadata.supportedFields")
public Set<String> supportedFields(RequestItem request) {
return ImmutableSet.copyOf(beanFilter
@@ -175,6 +190,12 @@ public class GadgetsHandler {
beanFilter.getBeanFields(GadgetsHandlerApi.ProxyResponse.class, 5));
}
+ @Operation(httpMethods = "GET", path = "/@cajole.supportedFields")
+ public Set<String> cajaSupportedFields(RequestItem request) {
+ return ImmutableSet.copyOf(beanFilter
+ .getBeanFields(GadgetsHandlerApi.CajaResponse.class, 5));
+ }
+
/**
* Class to handle threaded reply.
* Mainly it made to support filtering the id (url)
@@ -287,6 +308,22 @@ public class GadgetsHandler {
};
}
+ // Hook to override in sub-class.
+ protected Callable<CallableData> createCajaJob(final String url,
+ BaseRequestItem request) throws ProcessingException {
+ final CajaRequestData cajaRequest = new CajaRequestData(url, request);
+ return new Callable<CallableData>() {
+ public CallableData call() throws Exception {
+ try {
+ return new CallableData(url, handlerService.getCaja(cajaRequest));
+ } catch (Exception e) {
+ return new CallableData(url,
+ handlerService.createErrorResponse(null, e, FAILURE_CAJA));
+ }
+ }
+ };
+ }
+
/**
* Gadget context classes used to translate JSON BaseRequestItem into a more
@@ -492,6 +529,27 @@ public class GadgetsHandler {
return type;
}
+ protected class CajaRequestData extends AbstractRequest
+ implements GadgetsHandlerApi.CajaRequest {
+ private final String mimeType;
+ private final boolean debug;
+
+ public CajaRequestData(String url, BaseRequestItem request)
+ throws ProcessingException {
+ super(url, request, DEFAULT_CAJA_FIELDS);
+ this.mimeType = request.getParameter("mime-type", "text/html");
+ this.debug = getBooleanParam(request, "debug");
+ }
+
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ public boolean getDebug() {
+ return debug;
+ }
+ }
+
protected class MetadataRequestData extends AbstractRequest
implements GadgetsHandlerApi.MetadataRequest {
protected final Locale locale;
Modified:
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java
URL:
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java?rev=1043815&r1=1043814&r2=1043815&view=diff
==============================================================================
---
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java
(original)
+++
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java
Thu Dec 9 01:51:43 2010
@@ -182,6 +182,43 @@ public class GadgetsHandlerApi {
public String getToken();
}
+ // TODO(jasvir): Support getRefresh and noCache
+ public interface CajaRequest extends BaseRequest {
+ public String getMimeType();
+ public boolean getDebug();
+ }
+
+ public interface CajaResponse extends BaseResponse {
+ public String getHtml();
+ public String getJs();
+ public List<Message> getMessages();
+ }
+
+ public interface Message {
+ public MessageLevel getLevel();
+ public String getName();
+ public String getMessage();
+ }
+
+ public enum MessageLevel {
+ UNKNOWN,
+ // Fine grained info about internal progress
+ LOG,
+ // Broad info about internal progress
+ SUMMARY,
+ // Information inferred about source files
+ INFERENCE,
+ // Indicative of a possible problem in an input source file
+ LINT,
+ // Indicative of a probable problem in an input source file
+ WARNING,
+ // Indicative of a problem which prevents production of usable output
+ // but progress should continue in case further messages shed more info
+ ERROR,
+ // Indicative of a problem that prevents usable further processing
+ FATAL_ERROR;
+ }
+
public interface ProxyRequest extends BaseRequest {
// The BaseRequest.url store the resource to proxy
public String getGadget();
Modified:
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerService.java
URL:
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerService.java?rev=1043815&r1=1043814&r2=1043815&view=diff
==============================================================================
---
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerService.java
(original)
+++
shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerService.java
Thu Dec 9 01:51:43 2010
@@ -18,6 +18,13 @@
*/
package org.apache.shindig.gadgets.servlet;
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
@@ -25,6 +32,8 @@ import com.google.common.collect.Immutab
import com.google.inject.Inject;
import com.google.inject.name.Named;
+import javax.servlet.http.HttpServletResponse;
+
import org.apache.commons.codec.binary.Base64InputStream;
import org.apache.commons.io.IOUtils;
import org.apache.shindig.auth.SecurityToken;
@@ -40,6 +49,7 @@ import org.apache.shindig.gadgets.Render
import org.apache.shindig.gadgets.http.HttpResponse;
import org.apache.shindig.gadgets.process.ProcessingException;
import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.servlet.CajaContentRewriter.CajoledResult;
import org.apache.shindig.gadgets.spec.Feature;
import org.apache.shindig.gadgets.spec.GadgetSpec;
import org.apache.shindig.gadgets.spec.LinkSpec;
@@ -55,15 +65,14 @@ import org.apache.shindig.gadgets.uri.Pr
import org.apache.shindig.protocol.conversion.BeanDelegator;
import org.apache.shindig.protocol.conversion.BeanFilter;
-import java.io.IOException;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import javax.servlet.http.HttpServletResponse;
+import com.google.caja.lexer.TokenConsumer;
+import com.google.caja.parser.html.Nodes;
+import com.google.caja.render.Concatenator;
+import com.google.caja.render.JsMinimalPrinter;
+import com.google.caja.render.JsPrettyPrinter;
+import com.google.caja.reporting.MessageContext;
+import com.google.caja.reporting.RenderContext;
+import com.google.caja.util.Lists;
/**
* Service that interfaces with the system to provide information about
gadgets.
@@ -117,6 +126,7 @@ public class GadgetsHandlerService {
protected final BeanDelegator beanDelegator;
protected final long specRefreshInterval;
protected final BeanFilter beanFilter;
+ protected final CajaContentRewriter cajaContentRewriter;
@Inject
public GadgetsHandlerService(TimeSource timeSource, Processor processor,
@@ -124,7 +134,7 @@ public class GadgetsHandlerService {
ProxyUriManager proxyUriManager, JsUriManager jsUriManager, ProxyHandler
proxyHandler,
JsHandler jsHandler,
@Named("shindig.cache.xml.refreshInterval") long specRefreshInterval,
- BeanFilter beanFilter) {
+ BeanFilter beanFilter, CajaContentRewriter cajaContentRewriter) {
this.timeSource = timeSource;
this.processor = processor;
this.iframeUriManager = iframeUriManager;
@@ -135,6 +145,7 @@ public class GadgetsHandlerService {
this.jsHandler = jsHandler;
this.specRefreshInterval = specRefreshInterval;
this.beanFilter = beanFilter;
+ this.cajaContentRewriter = cajaContentRewriter;
this.beanDelegator = new BeanDelegator(apiClasses, enumConversionMap);
}
@@ -240,6 +251,73 @@ public class GadgetsHandlerService {
HttpResponse.SC_INTERNAL_SERVER_ERROR);
}
}
+
+ /**
+ * Convert message level to Shindig's serializable message type
+ */
+ public static GadgetsHandlerApi.MessageLevel convertMessageLevel(String
name) {
+ try {
+ return GadgetsHandlerApi.MessageLevel.valueOf(name);
+ }
+ catch (Exception ex) {
+ return GadgetsHandlerApi.MessageLevel.UNKNOWN;
+ }
+ }
+
+ /**
+ * Convert messages from Caja's internal message type to Shindig's
serializable message type
+ */
+ private List<GadgetsHandlerApi.Message> convertMessages(
+ List<com.google.caja.reporting.Message> msgs,
+ final MessageContext mc) {
+ List<GadgetsHandlerApi.Message> result = Lists.newArrayList(msgs.size());
+ for (final com.google.caja.reporting.Message m : msgs) {
+ MessageImpl msg = new MessageImpl(m.getMessageType().name(),
+ m.format(mc), convertMessageLevel(m.getMessageLevel().name()));
+ result.add(msg);
+ }
+ return result;
+ }
+
+ public GadgetsHandlerApi.CajaResponse getCaja(GadgetsHandlerApi.CajaRequest
request)
+ throws ProcessingException {
+ verifyBaseParams(request, true);
+ Set<String> fields = beanFilter.processBeanFields(request.getFields());
+
+ HttpResponse httpResponse = null;
+ try {
+ MessageContext mc = new MessageContext();
+ CajoledResult result =
+ cajaContentRewriter.rewrite(request.getUrl(), request.getContainer(),
+ request.getMimeType(), true /* only support es53 */,
request.getDebug());
+ String html = null;
+ String js = null;
+ if (!result.hasErrors && null != result.html) {
+ html = Nodes.render(result.html);
+ }
+
+ if (!result.hasErrors && null != result.js) {
+ StringBuilder builder = new StringBuilder();
+ TokenConsumer tc = request.getDebug() ?
+ new JsPrettyPrinter(new Concatenator(builder))
+ : new JsMinimalPrinter(new Concatenator(builder));
+ RenderContext rc = new RenderContext(tc)
+ .withAsciiOnly(true)
+ .withEmbeddable(true);
+ result.js.render(rc);
+ rc.getOut().noMoreTokens();
+ js = builder.toString();
+ }
+
+ // TODO(jasvir): Improve Caja responses expiration handling
+ return createCajaResponse(request.getUrl(),
+ html, js, convertMessages(result.messages, mc), fields,
+ timeSource.currentTimeMillis() + specRefreshInterval);
+ } catch (IOException e) {
+ LOG.log(Level.WARNING, "Error creating cajoled response", e);
+ throw new ProcessingException("Error getting response content",
HttpResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
/**
* Verify request parameter are defined.
@@ -247,13 +325,13 @@ public class GadgetsHandlerService {
protected void verifyBaseParams(GadgetsHandlerApi.BaseRequest request,
boolean checkUrl)
throws ProcessingException {
if (checkUrl && request.getUrl() == null) {
- throw new ProcessingException("Missing url paramater",
HttpResponse.SC_BAD_REQUEST);
+ throw new ProcessingException("Missing url parameter",
HttpResponse.SC_BAD_REQUEST);
}
if (request.getContainer() == null) {
- throw new ProcessingException("Missing container paramater",
HttpResponse.SC_BAD_REQUEST);
+ throw new ProcessingException("Missing container parameter",
HttpResponse.SC_BAD_REQUEST);
}
if (request.getFields() == null) {
- throw new ProcessingException("Missing fields paramater",
HttpResponse.SC_BAD_REQUEST);
+ throw new ProcessingException("Missing fields parameter",
HttpResponse.SC_BAD_REQUEST);
}
}
@@ -482,4 +560,56 @@ public class GadgetsHandlerService {
.build()),
fields);
}
+
+ @VisibleForTesting
+ GadgetsHandlerApi.CajaResponse createCajaResponse(Uri uri,
+ String html, String js, List<GadgetsHandlerApi.Message> messages,
+ Set<String> fields, Long expireMs) throws IOException {
+ ImmutableList.Builder<GadgetsHandlerApi.Message> msgBuilder =
+ ImmutableList.builder();
+ for (final GadgetsHandlerApi.Message m : messages) {
+ msgBuilder.add(
+ beanDelegator.createDelegator(null, GadgetsHandlerApi.Message.class,
+ ImmutableMap.<String, Object>of("name", m.getName(),
+ "level", m.getLevel(), "message", m.getMessage())));
+ }
+
+ return (GadgetsHandlerApi.CajaResponse) beanFilter.createFilteredBean(
+ beanDelegator.createDelegator(null,
GadgetsHandlerApi.CajaResponse.class,
+ ImmutableMap.<String, Object>builder()
+ .put("url", uri)
+ .put("html", BeanDelegator.nullable(html))
+ .put("js", BeanDelegator.nullable(js))
+ .put("messages", msgBuilder.build())
+ .put("error", BeanDelegator.NULL)
+ .put("responsetimems", timeSource.currentTimeMillis())
+ .put("expiretimems", BeanDelegator.nullable(expireMs))
+ .build()),
+ fields);
+ }
+
+ private class MessageImpl implements GadgetsHandlerApi.Message {
+ private GadgetsHandlerApi.MessageLevel level;
+ private String message;
+ private String name;
+
+ public MessageImpl(String name, String message,
GadgetsHandlerApi.MessageLevel level) {
+ this.name = name;
+ this.message = message;
+ this.level = level;
+ }
+
+ public GadgetsHandlerApi.MessageLevel getLevel() {
+ return level;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ }
}
Modified:
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerServiceTest.java
URL:
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerServiceTest.java?rev=1043815&r1=1043814&r2=1043815&view=diff
==============================================================================
---
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerServiceTest.java
(original)
+++
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerServiceTest.java
Thu Dec 9 01:51:43 2010
@@ -40,6 +40,8 @@ import org.apache.shindig.gadgets.featur
import org.apache.shindig.gadgets.http.HttpResponse;
import org.apache.shindig.gadgets.http.HttpResponseBuilder;
import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.servlet.GadgetsHandlerApi.Message;
+import org.apache.shindig.gadgets.servlet.GadgetsHandlerApi.MessageLevel;
import org.apache.shindig.gadgets.uri.JsUriManager;
import org.apache.shindig.gadgets.uri.ProxyUriManager;
import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
@@ -78,6 +80,7 @@ public class GadgetsHandlerServiceTest e
private final ProxyUriManager proxyUriManager = mock(ProxyUriManager.class);
private final JsUriManager jsUriManager = mock(JsUriManager.class);
private final ProxyHandler proxyHandler = mock(ProxyHandler.class);
+ private final CajaContentRewriter cajaContentRewriter =
mock(CajaContentRewriter.class);
private final JsHandler jsHandler = mock(JsHandler.class);
private FakeSecurityTokenCodec tokenCodec;
@@ -88,7 +91,7 @@ public class GadgetsHandlerServiceTest e
tokenCodec = new FakeSecurityTokenCodec();
gadgetHandler = new GadgetsHandlerService(timeSource, processor,
urlGenerator,
tokenCodec, proxyUriManager, jsUriManager, proxyHandler, jsHandler,
- SPEC_REFRESH_INTERVAL_MS, new BeanFilter());
+ SPEC_REFRESH_INTERVAL_MS, new BeanFilter(), cajaContentRewriter);
}
// Next test verify that the API data classes are configured correctly.
@@ -493,6 +496,37 @@ public class GadgetsHandlerServiceTest e
replay();
GadgetsHandlerApi.ProxyResponse response = gadgetHandler.getProxy(request);
}
+
+ @Test
+ public void testCreateCajaResponse() throws Exception {
+ String goldenEntries[][] = {{"name1", "LINT", "msg1"}, {"name2", "LINT",
"msg2"}};
+ List<Message> goldenMessages = Lists.newArrayList();
+
+ for (String[] goldenEntry : goldenEntries) {
+ Message m = mock(Message.class);
+ expect(m.getName()).andReturn(goldenEntry[0]);
+ expect(m.getLevel()).andReturn(MessageLevel.valueOf(goldenEntry[1]));
+ expect(m.getMessage()).andReturn(goldenEntry[2]);
+ goldenMessages.add(m);
+ }
+ replay();
+
+ Uri jsUri = Uri.parse("http://www.shindig.com/js");
+ GadgetsHandlerApi.CajaResponse jsResponse =
+ gadgetHandler.createCajaResponse(jsUri, "html", "js",
+ goldenMessages, ImmutableSet.of("*"), null);
+ BeanDelegator.validateDelegator(jsResponse);
+
+ assertEquals("html", jsResponse.getHtml());
+ assertEquals("js", jsResponse.getJs());
+ List<Message> response = jsResponse.getMessages();
+ assertEquals(goldenMessages.size(), response.size());
+ for (int i=0; i < response.size(); i++) {
+ assertEquals(goldenEntries[i][0], response.get(i).getName());
+ assertEquals(goldenEntries[i][1], response.get(i).getLevel().name());
+ assertEquals(goldenEntries[i][2], response.get(i).getMessage());
+ }
+ }
private GadgetsHandlerApi.TokenData createTokenData(String ownerId, String
viewerId) {
GadgetsHandlerApi.TokenData token =
mock(GadgetsHandlerApi.TokenData.class);
@@ -545,6 +579,15 @@ public class GadgetsHandlerServiceTest e
return request;
}
+ private GadgetsHandlerApi.CajaRequest createCajaRequest(Uri url, String
container,
+ List<String> fields) {
+ GadgetsHandlerApi.CajaRequest request =
mock(GadgetsHandlerApi.CajaRequest.class);
+ EasyMock.expect(request.getFields()).andStubReturn(fields);
+ EasyMock.expect(request.getContainer()).andStubReturn(container);
+ EasyMock.expect(request.getUrl()).andStubReturn(url);
+ return request;
+ }
+
private class FakeSecurityTokenCodec implements SecurityTokenCodec {
public SecurityToken inputToken = null;
public SecurityTokenException exc = null;
Modified:
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java
URL:
http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java?rev=1043815&r1=1043814&r2=1043815&view=diff
==============================================================================
---
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java
(original)
+++
shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java
Thu Dec 9 01:51:43 2010
@@ -17,10 +17,12 @@
package org.apache.shindig.gadgets.servlet;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
import org.apache.shindig.auth.SecurityToken;
@@ -35,6 +37,7 @@ import org.apache.shindig.common.util.Fa
import org.apache.shindig.gadgets.RenderingContext;
import org.apache.shindig.gadgets.http.HttpResponse;
import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.servlet.CajaContentRewriter.CajoledResult;
import org.apache.shindig.gadgets.spec.GadgetSpec;
import org.apache.shindig.gadgets.uri.JsUriManager;
import org.apache.shindig.gadgets.uri.ProxyUriManager;
@@ -55,15 +58,21 @@ import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import javax.servlet.http.HttpServletResponse;
+import com.google.caja.lexer.CharProducer;
+import com.google.caja.lexer.InputSource;
+import com.google.caja.lexer.ParseException;
+import com.google.caja.reporting.MessageQueue;
+import com.google.caja.reporting.SimpleMessageQueue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
public class GadgetsHandlerTest extends EasyMockTestCase {
private static final String GADGET1_URL = FakeProcessor.SPEC_URL.toString();
private static final String GADGET2_URL = FakeProcessor.SPEC_URL2
.toString();
+ private static final Uri HTML_URL =
Uri.parse("http://www.example.com/a.html");
+ private static final Uri JS_URL = Uri.parse("http://www.example.com/a.js");
private static final String CONTAINER = "container";
private static final String TOKEN = "_nekot_";
private static final Long SPEC_REFRESH_INTERVAL = 123L;
@@ -76,6 +85,7 @@ public class GadgetsHandlerTest extends
private final ProxyUriManager proxyUriManager = mock(ProxyUriManager.class);
private final JsUriManager jsUriManager = mock(JsUriManager.class);
private final ProxyHandler proxyHandler = mock(ProxyHandler.class);
+ private final CajaContentRewriter cajaContentRewriter =
mock(CajaContentRewriter.class);
private final JsHandler jsHandler = mock(JsHandler.class);
private Injector injector;
@@ -95,7 +105,7 @@ public class GadgetsHandlerTest extends
BeanFilter beanFilter = new BeanFilter();
GadgetsHandlerService service = new GadgetsHandlerService(timeSource,
processor,
urlGenerator, codec, proxyUriManager, jsUriManager, proxyHandler,
jsHandler,
- SPEC_REFRESH_INTERVAL, beanFilter);
+ SPEC_REFRESH_INTERVAL, beanFilter, cajaContentRewriter);
GadgetsHandler handler =
new GadgetsHandler(new TestExecutorService(), service, beanFilter);
registry = new DefaultHandlerRegistry(
@@ -150,6 +160,134 @@ public class GadgetsHandlerTest extends
}
}
+ private JSONObject makeCajaRequest(String mime, String... uris)
+ throws JSONException {
+ JSONObject params = new JSONObject()
+ .put("container", CONTAINER)
+ .put("ids", ImmutableList.copyOf(uris));
+
+ if (null != mime) {
+ params.put("mime-type", mime);
+ }
+
+ return new JSONObject()
+ .put("id", "req1")
+ .put("method", "gadgets.cajole")
+ .put("params", params);
+ }
+
+ @Test
+ public void testCajaEmptyRequest() throws Exception {
+ registerGadgetsHandler(null);
+ JSONObject request = makeCajaRequest(null);
+ RpcHandler operation = registry.getRpcHandler(request);
+ Object empty = operation.execute(emptyFormItems, token, converter).get();
+ JsonAssert.assertJsonEquals("{}", converter.convertToString(empty));
+ }
+
+ private CajoledResult cajole(Uri uri, String mime, String content) throws
ParseException {
+ CajaContentRewriter rw = new CajaContentRewriter(null, null, null);
+ InputSource is = new InputSource(uri.toJavaUri());
+ MessageQueue mq = new SimpleMessageQueue();
+ CharProducer cp = CharProducer.Factory.create(new StringReader(content),
is);
+ return rw.rewrite(uri, CONTAINER, rw.parse(is, cp, mime, mq), false,
false);
+ }
+
+ @Test
+ public void testCajaJsRequest() throws Exception {
+ registerGadgetsHandler(null);
+ Capture<Uri> uriCapture = new Capture<Uri>();
+ Capture<String> containerCapture = new Capture<String>();
+ Capture<String> mimeCapture = new Capture<String>();
+
+ String goldenMime = "text/javascript";
+
+ CajoledResult golden = cajole(JS_URL, goldenMime, "alert('hi');");
+
+ EasyMock.expect(cajaContentRewriter.rewrite(
+ EasyMock.capture(uriCapture),
+ EasyMock.capture(containerCapture),
+ EasyMock.capture(mimeCapture),
+ EasyMock.eq(true),
+ EasyMock.anyBoolean()))
+ .andReturn(golden)
+ .anyTimes();
+ replay();
+
+ JSONObject request = makeCajaRequest(goldenMime, JS_URL.toString());
+ RpcHandler operation = registry.getRpcHandler(request);
+ Object result = operation.execute(emptyFormItems, token, converter).get();
+
+ assertEquals(CONTAINER, containerCapture.getValue());
+ assertEquals(JS_URL, uriCapture.getValue());
+ assertTrue(mimeCapture.getValue().contains("javascript"));
+
+ JSONObject response = new JSONObject(converter.convertToString(result));
+ assertTrue(response.has(JS_URL.toString()));
+ JSONObject cajaResponse = response.getJSONObject(JS_URL.toString());
+
+ // Pure js url - no html produced
+ assertFalse(cajaResponse.has("html"));
+ assertTrue(cajaResponse.has("js"));
+ assertTrue(cajaResponse.has("messages"));
+ assertTrue(cajaResponse.getString("js").contains("alert"));
+ assertTrue(cajaResponse.getJSONArray("messages").length() > 0);
+ }
+
+ @Test
+ public void testCajaHtmlRequest() throws Exception {
+ registerGadgetsHandler(null);
+ Capture<Uri> uriCapture = new Capture<Uri>();
+ Capture<String> containerCapture = new Capture<String>();
+ Capture<String> mimeCapture = new Capture<String>();
+
+ String goldenMime = "text/html";
+
+ CajoledResult golden = cajole(HTML_URL, goldenMime,
+ "<b>hello</b>world<script>evilFunc1()</script><div
onclick='evilFunc2'></div>");
+
+ EasyMock.expect(cajaContentRewriter.rewrite(
+ EasyMock.capture(uriCapture),
+ EasyMock.capture(containerCapture),
+ EasyMock.capture(mimeCapture),
+ EasyMock.eq(true),
+ EasyMock.anyBoolean()))
+ .andReturn(golden)
+ .anyTimes();
+ replay();
+
+ JSONObject request = makeCajaRequest(goldenMime, HTML_URL.toString());
+ RpcHandler operation = registry.getRpcHandler(request);
+ Object result = operation.execute(emptyFormItems, token, converter).get();
+
+ assertEquals(CONTAINER, containerCapture.getValue());
+ assertEquals(HTML_URL, uriCapture.getValue());
+ assertTrue(mimeCapture.getValue().contains("html"));
+
+ JSONObject response = new JSONObject(converter.convertToString(result));
+ assertTrue(response.has(HTML_URL.toString()));
+
+ JSONObject cajaResponse = response.getJSONObject(HTML_URL.toString());
+ assertTrue(cajaResponse.has("html"));
+ assertTrue(cajaResponse.has("js"));
+ assertTrue(cajaResponse.has("messages"));
+
+ // HTML is sanitized
+ assertTrue(cajaResponse.getString("html").contains("<b>hello</b>world"));
+ assertFalse(cajaResponse.getString("html").contains("evilFunc"));
+ assertTrue(cajaResponse.getString("js").contains("evilFunc"));
+ assertTrue(cajaResponse.getJSONArray("messages").length() > 0);
+ }
+
+ @Test
+ public void testCajaES53Request() throws Exception {
+ registerGadgetsHandler(null);
+ JSONObject request = makeCajaRequest(null);
+ RpcHandler operation = registry.getRpcHandler(request);
+ Object empty = operation.execute(emptyFormItems, token, converter).get();
+ JsonAssert.assertJsonEquals("{}", converter.convertToString(empty));
+ }
+
@Test
public void testGetRenderingType() throws Exception {
assertEquals(GadgetsHandlerApi.RenderingType.DEFAULT,
GadgetsHandler.getRenderingType(null));
@@ -568,5 +706,4 @@ public class GadgetsHandlerTest extends
assertFalse(gadget1.has("error"));
verify();
}
-
}