Add SafeUri type, similar to SafeHtml but for values in a URL attribute context. Support SafeUri-typed arguments in SafeHtmlTemplates. Also added a few overloads in c.g.g.user.client. Note that this is a breaking change in the sense that DataResource and ImageResource have a new getSafeUri method, as well as overloaded constructors taking URIs in the form of SafeUri. Patch by: tbroyer Review by: xtof Fixes issues: 6145 Review at http://gwt-code-reviews.appspot.com/1447812 git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10282 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/tools/api-checker/config/gwt23_24userApi.conf b/tools/api-checker/config/gwt23_24userApi.conf index 4f061ad..595e1e1 100644 --- a/tools/api-checker/config/gwt23_24userApi.conf +++ b/tools/api-checker/config/gwt23_24userApi.conf
@@ -100,6 +100,7 @@ :user/src/com/google/gwt/rpc/client/impl/EscapeUtil.java\ :user/src/com/google/gwt/rpc/client/impl/RpcCallbackAdapter.java\ :user/src/com/google/gwt/safehtml/shared/SafeHtmlHostedModeUtils.java\ +:user/src/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java\ :user/src/com/google/gwt/user/client/rpc/core/**\ :user/src/com/google/gwt/user/client/rpc/impl/**\ :user/src/com/google/gwt/uibinder/attributeparsers/**\ @@ -120,6 +121,7 @@ :com.google.gwt.editor.client.impl\ :com.google.gwt.junit.client.impl\ :com.google.gwt.benchmarks.client.impl\ +:com.google.gwt.user.client.ui.impl\ ############################################## #Api whitelist @@ -147,3 +149,11 @@ com.google.gwt.user.client.Event::ONCANPLAYTHROUGH MISSING com.google.gwt.user.client.Event::ONENDED MISSING com.google.gwt.user.client.Event::ONPROGRESS MISSING + +# Addition of SafeUri variants +com.google.gwt.user.client.ui.FormPanel::setAction(Ljava/lang/String;) OVERLOADED_METHOD_CALL +com.google.gwt.user.client.ui.Frame::setUrl(Ljava/lang/String;) OVERLOADED_METHOD_CALL +com.google.gwt.user.client.ui.Image::Image(Ljava/lang/String;IIII) OVERLOADED_METHOD_CALL +com.google.gwt.user.client.ui.Image::prefetch(Ljava/lang/String;) OVERLOADED_METHOD_CALL +com.google.gwt.user.client.ui.Image::setUrl(Ljava/lang/String;) OVERLOADED_METHOD_CALL +com.google.gwt.user.client.ui.Image::setUrlAndVisibleRect(Ljava/lang/String;IIII) OVERLOADED_METHOD_CALL
diff --git a/user/src/com/google/gwt/cell/client/ImageResourceCell.java b/user/src/com/google/gwt/cell/client/ImageResourceCell.java index e368759..2b00182 100644 --- a/user/src/com/google/gwt/cell/client/ImageResourceCell.java +++ b/user/src/com/google/gwt/cell/client/ImageResourceCell.java
@@ -21,13 +21,6 @@ /** * An {@link AbstractCell} used to render an {@link ImageResource}. - * - * <p> - * This class assumes that the URL returned from ImageResource is safe from - * script attacks. If you do not generate the ImageResource from a - * {@link com.google.gwt.resources.client.ClientBundle ClientBundle}, you should - * use {@link com.google.gwt.safehtml.shared.UriUtils UriUtils} to sanitize the - * URL before returning it from {@link ImageResource#getURL()}. */ public class ImageResourceCell extends AbstractCell<ImageResource> { private static ImageResourceRenderer renderer;
diff --git a/user/src/com/google/gwt/resources/Resources.gwt.xml b/user/src/com/google/gwt/resources/Resources.gwt.xml index f46be7b..689d147 100644 --- a/user/src/com/google/gwt/resources/Resources.gwt.xml +++ b/user/src/com/google/gwt/resources/Resources.gwt.xml
@@ -16,6 +16,8 @@ <module> <!-- Pull in the necessary base support, including user.agent detection --> <inherits name="com.google.gwt.core.Core" /> + <!-- Pull in SafeUri and SafeUriUtils --> + <inherits name="com.google.gwt.safehtml.SafeHtml" /> <!-- Pull in StyleInjector for CssResource --> <inherits name="com.google.gwt.dom.DOM" /> <!-- Used by ExternalTextResource -->
diff --git a/user/src/com/google/gwt/resources/client/DataResource.java b/user/src/com/google/gwt/resources/client/DataResource.java index e87d0c6..c27ff91 100644 --- a/user/src/com/google/gwt/resources/client/DataResource.java +++ b/user/src/com/google/gwt/resources/client/DataResource.java
@@ -1,12 +1,12 @@ /* * Copyright 2008 Google Inc. - * + * * 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 @@ -17,6 +17,7 @@ import com.google.gwt.resources.ext.ResourceGeneratorType; import com.google.gwt.resources.rg.DataResourceGenerator; +import com.google.gwt.safehtml.shared.SafeUri; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -58,5 +59,14 @@ * Retrieves a URL by which the contents of the resource can be obtained. This * will be an absolute URL. */ + SafeUri getSafeUri(); + + /** + * Retrieves a URL by which the contents of the resource can be obtained. This + * will be an absolute URL. + * + * @deprecated Use {@link #getSafeUri()} instead. + */ + @Deprecated String getUrl(); }
diff --git a/user/src/com/google/gwt/resources/client/ImageResource.java b/user/src/com/google/gwt/resources/client/ImageResource.java index 0266048..df63fc1 100644 --- a/user/src/com/google/gwt/resources/client/ImageResource.java +++ b/user/src/com/google/gwt/resources/client/ImageResource.java
@@ -18,6 +18,7 @@ import com.google.gwt.resources.ext.DefaultExtensions; import com.google.gwt.resources.ext.ResourceGeneratorType; import com.google.gwt.resources.rg.ImageResourceGenerator; +import com.google.gwt.safehtml.shared.SafeUri; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -119,13 +120,21 @@ int getLeft(); /** + * Returns the URL for the composite image that contains the ImageResource. + */ + SafeUri getSafeUri(); + + /** * Returns the vertical position of the image within the composite image. */ int getTop(); /** * Returns the URL for the composite image that contains the ImageResource. + * + * @deprecated Use {@link #getSafeUri()} instead. */ + @Deprecated String getURL(); /**
diff --git a/user/src/com/google/gwt/resources/client/impl/DataResourcePrototype.java b/user/src/com/google/gwt/resources/client/impl/DataResourcePrototype.java new file mode 100644 index 0000000..f75c900 --- /dev/null +++ b/user/src/com/google/gwt/resources/client/impl/DataResourcePrototype.java
@@ -0,0 +1,47 @@ +/* + * Copyright 2011 Google Inc. + * + * 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 com.google.gwt.resources.client.impl; + +import com.google.gwt.resources.client.DataResource; +import com.google.gwt.safehtml.shared.SafeUri; + +/** + * Simple implementation of {@link DataResource}. + */ +public class DataResourcePrototype implements DataResource { + private final String name; + private final SafeUri uri; + + /** + * Only called by generated code. + */ + public DataResourcePrototype(String name, SafeUri uri) { + this.name = name; + this.uri = uri; + } + + public String getName() { + return name; + } + + public SafeUri getSafeUri() { + return uri; + } + + public String getUrl() { + return uri.asString(); + } +}
diff --git a/user/src/com/google/gwt/resources/client/impl/ExternalTextResourcePrototype.java b/user/src/com/google/gwt/resources/client/impl/ExternalTextResourcePrototype.java index ca8f381..4015391 100644 --- a/user/src/com/google/gwt/resources/client/impl/ExternalTextResourcePrototype.java +++ b/user/src/com/google/gwt/resources/client/impl/ExternalTextResourcePrototype.java
@@ -26,6 +26,7 @@ import com.google.gwt.resources.client.ResourceCallback; import com.google.gwt.resources.client.ResourceException; import com.google.gwt.resources.client.TextResource; +import com.google.gwt.safehtml.shared.SafeUri; import com.google.gwt.user.client.rpc.AsyncCallback; /** @@ -129,9 +130,9 @@ private final int index; private final String md5Hash; private final String name; - private final String url; + private final SafeUri url; - public ExternalTextResourcePrototype(String name, String url, + public ExternalTextResourcePrototype(String name, SafeUri url, TextResource[] cache, int index) { this.name = name; this.url = url; @@ -140,7 +141,7 @@ this.md5Hash = null; } - public ExternalTextResourcePrototype(String name, String url, + public ExternalTextResourcePrototype(String name, SafeUri url, TextResource[] cache, int index, String md5Hash) { this.name = name; this.url = url; @@ -169,14 +170,13 @@ // If we have an md5Hash, we should be using JSONP JsonpRequestBuilder rb = new JsonpRequestBuilder(); rb.setPredeterminedId(md5Hash); - rb.requestObject(url, new ETRCallback(callback)); + rb.requestObject(url.asString(), new ETRCallback(callback)); } else { - RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, url); + RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, url.asString()); try { rb.sendRequest("", new ETRCallback(callback)); } catch (RequestException e) { - throw new ResourceException(this, - "Unable to initiate request for external resource", e); + throw new ResourceException(this, "Unable to initiate request for external resource", e); } } }
diff --git a/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java b/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java index 0438ced..7a422d4 100644 --- a/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java +++ b/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java
@@ -16,6 +16,7 @@ package com.google.gwt.resources.client.impl; import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.safehtml.shared.SafeUri; /** * This is part of an implementation of the ImageBundle optimization implemented @@ -26,7 +27,7 @@ private final boolean animated; private final boolean lossy; private final String name; - private final String url; + private final SafeUri url; private final int left; private final int top; private final int width; @@ -35,8 +36,8 @@ /** * Only called by generated code. */ - public ImageResourcePrototype(String name, String url, int left, int top, - int width, int height, boolean animated, boolean lossy) { + public ImageResourcePrototype(String name, SafeUri url, int left, int top, int width, int height, + boolean animated, boolean lossy) { this.name = name; this.left = left; this.top = top; @@ -65,18 +66,16 @@ return name; } - /** - * Exists for testing purposes, not part of the ImageResource interface. - */ + public SafeUri getSafeUri() { + return url; + } + public int getTop() { return top; } - /** - * Exists for testing purposes, not part of the ImageResource interface. - */ public String getURL() { - return url; + return url.asString(); } /** @@ -90,6 +89,9 @@ return animated; } + /** + * Exists for testing purposes, not part of the ImageResource interface. + */ public boolean isLossy() { return lossy; }
diff --git a/user/src/com/google/gwt/resources/css/Spriter.java b/user/src/com/google/gwt/resources/css/Spriter.java index 52231ab..d821fa1 100644 --- a/user/src/com/google/gwt/resources/css/Spriter.java +++ b/user/src/com/google/gwt/resources/css/Spriter.java
@@ -125,7 +125,7 @@ } String backgroundExpression = "\"url(\\\"\" + " + instance - + ".getURL() + \"\\\") -\" + " + instance + ".getLeft() + \"px -\" + " + + ".getSafeUri().asString() + \"\\\") -\" + " + instance + ".getLeft() + \"px -\" + " + instance + ".getTop() + \"px " + repeatText + "\""; properties.add(new CssProperty("background", new ExpressionValue( backgroundExpression), false));
diff --git a/user/src/com/google/gwt/resources/rg/DataResourceGenerator.java b/user/src/com/google/gwt/resources/rg/DataResourceGenerator.java index 416ca28..c5392d7 100644 --- a/user/src/com/google/gwt/resources/rg/DataResourceGenerator.java +++ b/user/src/com/google/gwt/resources/rg/DataResourceGenerator.java
@@ -18,13 +18,14 @@ import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.core.ext.typeinfo.JMethod; -import com.google.gwt.resources.client.DataResource.MimeType; -import com.google.gwt.resources.client.DataResource; import com.google.gwt.resources.client.DataResource.DoNotEmbed; +import com.google.gwt.resources.client.DataResource.MimeType; +import com.google.gwt.resources.client.impl.DataResourcePrototype; import com.google.gwt.resources.ext.AbstractResourceGenerator; import com.google.gwt.resources.ext.ResourceContext; import com.google.gwt.resources.ext.ResourceGeneratorUtil; import com.google.gwt.resources.ext.SupportsGeneratorResultCaching; +import com.google.gwt.safehtml.shared.UriUtils; import com.google.gwt.user.rebind.SourceWriter; import com.google.gwt.user.rebind.StringSourceWriter; @@ -33,56 +34,39 @@ /** * Provides implementations of DataResource. */ -public final class DataResourceGenerator extends AbstractResourceGenerator - implements SupportsGeneratorResultCaching { +public final class DataResourceGenerator extends AbstractResourceGenerator implements + SupportsGeneratorResultCaching { @Override - public String createAssignment(TreeLogger logger, ResourceContext context, - JMethod method) throws UnableToCompleteException { + public String createAssignment(TreeLogger logger, ResourceContext context, JMethod method) + throws UnableToCompleteException { - URL[] resources = ResourceGeneratorUtil.findResources(logger, context, - method); + URL[] resources = ResourceGeneratorUtil.findResources(logger, context, method); if (resources.length != 1) { - logger.log(TreeLogger.ERROR, "Exactly one resource must be specified", - null); + logger.log(TreeLogger.ERROR, "Exactly one resource must be specified", null); throw new UnableToCompleteException(); } // Determine if a MIME Type has been specified MimeType mimeTypeAnnotation = method.getAnnotation(MimeType.class); - String mimeType = mimeTypeAnnotation != null - ? mimeTypeAnnotation.value() : null; + String mimeType = mimeTypeAnnotation != null ? mimeTypeAnnotation.value() : null; // Determine if resource should not be embedded DoNotEmbed doNotEmbed = method.getAnnotation(DoNotEmbed.class); boolean forceExternal = (doNotEmbed != null); URL resource = resources[0]; - String outputUrlExpression = context.deploy( - resource, mimeType, forceExternal); + String outputUrlExpression = context.deploy(resource, mimeType, forceExternal); SourceWriter sw = new StringSourceWriter(); - // Write the expression to create the subtype. - sw.println("new " + DataResource.class.getName() + "() {"); - sw.indent(); - // Convenience when examining the generated code. sw.println("// " + resource.toExternalForm()); - - sw.println("public String getUrl() {"); + sw.println("new " + DataResourcePrototype.class.getName() + "("); sw.indent(); - sw.println("return " + outputUrlExpression + ";"); + sw.println('"' + method.getName() + "\","); + sw.println(UriUtils.class.getName() + ".fromTrustedString(" + outputUrlExpression + ")"); sw.outdent(); - sw.println("}"); - - sw.println("public String getName() {"); - sw.indent(); - sw.println("return \"" + method.getName() + "\";"); - sw.outdent(); - sw.println("}"); - - sw.outdent(); - sw.println("}"); + sw.print(")"); return sw.toString(); }
diff --git a/user/src/com/google/gwt/resources/rg/ExternalTextResourceGenerator.java b/user/src/com/google/gwt/resources/rg/ExternalTextResourceGenerator.java index 04bc77b..7bb445e 100644 --- a/user/src/com/google/gwt/resources/rg/ExternalTextResourceGenerator.java +++ b/user/src/com/google/gwt/resources/rg/ExternalTextResourceGenerator.java
@@ -1,12 +1,12 @@ /* * Copyright 2008 Google Inc. - * + * * 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 @@ -33,6 +33,7 @@ import com.google.gwt.resources.ext.ResourceContext; import com.google.gwt.resources.ext.ResourceGeneratorUtil; import com.google.gwt.resources.ext.SupportsGeneratorResultCaching; +import com.google.gwt.safehtml.shared.UriUtils; import com.google.gwt.user.rebind.SourceWriter; import com.google.gwt.user.rebind.StringSourceWriter; @@ -50,7 +51,7 @@ * generator will use JSONP to fetch the files. */ static final String USE_JSONP = "ExternalTextResource.useJsonp"; - + // This string must stay in sync with the values in JsonpRequest.java static final String JSONP_CALLBACK_PREFIX = "__gwt_jsonp__.P"; @@ -75,7 +76,8 @@ sw.indent(); sw.println('"' + name + "\","); // These are field names - sw.println(externalTextUrlIdent + ", " + externalTextCacheIdent + ", "); + sw.println(UriUtils.class.getName() + ".fromTrustedString(" + externalTextUrlIdent + "),"); + sw.println(externalTextCacheIdent + ", "); sw.println(offsets.get(method.getName()).toString()); if (shouldUseJsonp(context, logger)) { sw.println(", \"" + getMd5HashOfData() + "\""); @@ -178,7 +180,7 @@ ConfigurationProperty prop = context.getGeneratorContext() .getPropertyOracle().getConfigurationProperty(USE_JSONP); useJsonpProp = prop.getValues().get(0); - + // add this configuration property to our requirements context.getRequirements().addConfigurationProperty(USE_JSONP); } catch (BadPropertyValueException e) {
diff --git a/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java index 1d86ad1..6d38fb6 100644 --- a/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java +++ b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
@@ -35,6 +35,7 @@ import com.google.gwt.resources.ext.SupportsGeneratorResultCaching; import com.google.gwt.resources.rg.ImageBundleBuilder.Arranger; import com.google.gwt.resources.rg.ImageBundleBuilder.ImageRect; +import com.google.gwt.safehtml.shared.UriUtils; import com.google.gwt.user.rebind.SourceWriter; import com.google.gwt.user.rebind.StringSourceWriter; @@ -75,8 +76,7 @@ CannotBundleImageException { LocalizedImage localized = LocalizedImage.create(logger, context, image); - localizedByImageResource = Maps.put(localizedByImageResource, image, - localized); + localizedByImageResource = Maps.put(localizedByImageResource, image, localized); if (images.containsKey(localized)) { return localized; } @@ -140,8 +140,7 @@ throw new UnableToCompleteException(); } URL normalContents = renderToTempPngFile(logger, builder, arranger); - normalContentsUrlExpression = context.deploy( - normalContents, MIME_TYPE_IMAGE_PNG, false); + normalContentsUrlExpression = context.deploy(normalContents, MIME_TYPE_IMAGE_PNG, false); if (!rtlImages.isEmpty()) { for (LocalizedImage rtlImage : rtlImages) { @@ -151,22 +150,20 @@ tx.setTransform(-1, 0, 0, 1, imageRect.getWidth(), 0); imageRect.setTransform(tx); } - URL rtlContents = renderToTempPngFile(logger, builder, - new ImageBundleBuilder.IdentityArranger()); + URL rtlContents = + renderToTempPngFile(logger, builder, new ImageBundleBuilder.IdentityArranger()); assert rtlContents != null; - rtlContentsUrlExpression = context.deploy( - rtlContents, MIME_TYPE_IMAGE_PNG, false); + rtlContentsUrlExpression = context.deploy(rtlContents, MIME_TYPE_IMAGE_PNG, false); } dirty = false; if (logger.isLoggable(TreeLogger.DEBUG)) { - logger.log(TreeLogger.DEBUG, "Composited " + builder.getImageCount() - + " images"); + logger.log(TreeLogger.DEBUG, "Composited " + builder.getImageCount() + " images"); } } - JClassType stringType = context.getGeneratorContext().getTypeOracle().findType( - String.class.getCanonicalName()); + JClassType stringType = + context.getGeneratorContext().getTypeOracle().findType(String.class.getCanonicalName()); // Create the field that holds the normal contents assert normalContentsUrlExpression != null; @@ -222,8 +219,10 @@ * ClientBundle . */ static class CachedState { - public final Map<BundleKey, BundledImage> bundledImages = new LinkedHashMap<BundleKey, BundledImage>(); - public final Map<BundleKey, ExternalImage> externalImages = new LinkedHashMap<BundleKey, ExternalImage>(); + public final Map<BundleKey, BundledImage> bundledImages = + new LinkedHashMap<BundleKey, BundledImage>(); + public final Map<BundleKey, ExternalImage> externalImages = + new LinkedHashMap<BundleKey, ExternalImage>(); } /** @@ -233,8 +232,7 @@ private final ImageRect imageRect; private final LocalizedImage localized; - public CannotBundleImageException(LocalizedImage localized, - ImageRect imageRect) { + public CannotBundleImageException(LocalizedImage localized, ImageRect imageRect) { this.localized = localized; this.imageRect = imageRect; } @@ -291,8 +289,7 @@ /** * Create an unbundled image. */ - public ExternalImage(ImageResourceDeclaration image, - LocalizedImage localized, ImageRect rect) { + public ExternalImage(ImageResourceDeclaration image, LocalizedImage localized, ImageRect rect) { this.image = image; this.localized = localized; this.rect = rect; @@ -307,8 +304,8 @@ public void render(TreeLogger logger, ResourceContext context, ClientBundleFields fields, RepeatStyle repeatStyle) throws UnableToCompleteException { - JClassType stringType = context.getGeneratorContext().getTypeOracle().findType( - String.class.getCanonicalName()); + JClassType stringType = + context.getGeneratorContext().getTypeOracle().findType(String.class.getCanonicalName()); String contentsExpression = context.deploy( localized.getUrl(), null, image.isPreventInlining()); @@ -343,8 +340,7 @@ */ static class ImageResourceDeclaration extends StringKey { private static String key(JMethod method) { - return method.getEnclosingType().getQualifiedSourceName() + "." - + method.getName(); + return method.getEnclosingType().getQualifiedSourceName() + "." + method.getName(); } private final String name; @@ -398,8 +394,7 @@ ResourceContext context, ImageResourceDeclaration image) throws UnableToCompleteException { - URL[] resources = ResourceGeneratorUtil.findResources(logger, context, - image.getMethod()); + URL[] resources = ResourceGeneratorUtil.findResources(logger, context, image.getMethod()); if (resources.length != 1) { logger.log(TreeLogger.ERROR, "Exactly one image may be specified", null); @@ -447,8 +442,7 @@ return null; } - File file = File.createTempFile( - ImageResourceGenerator.class.getSimpleName(), ".png"); + File file = File.createTempFile(ImageResourceGenerator.class.getSimpleName(), ".png"); file.deleteOnExit(); Util.writeBytesToFile(logger, file, imageBytes); return file.toURI().toURL(); @@ -465,8 +459,8 @@ private CachedState shared; @Override - public String createAssignment(TreeLogger logger, ResourceContext context, - JMethod method) throws UnableToCompleteException { + public String createAssignment(TreeLogger logger, ResourceContext context, JMethod method) + throws UnableToCompleteException { String name = method.getName(); SourceWriter sw = new StringSourceWriter(); @@ -479,19 +473,19 @@ ImageRect rect = bundle.getImageRect(image); assert rect != null : "No ImageRect ever computed for " + name; - String[] urlExpressions = new String[] { - bundle.getNormalContentsFieldName(), bundle.getRtlContentsFieldName()}; + String[] urlExpressions = + new String[] {bundle.getNormalContentsFieldName(), bundle.getRtlContentsFieldName()}; assert urlExpressions[0] != null : "No primary URL expression for " + name; if (urlExpressions[1] == null) { - sw.println(urlExpressions[0] + ","); + sw.println(UriUtils.class.getName() + ".fromTrustedString(" + urlExpressions[0] + "),"); } else { - sw.println("com.google.gwt.i18n.client.LocaleInfo.getCurrentLocale().isRTL() ?" - + urlExpressions[1] + " : " + urlExpressions[0] + ","); + sw.println(UriUtils.class.getName() + ".fromTrustedString(" + + "com.google.gwt.i18n.client.LocaleInfo.getCurrentLocale().isRTL() ?" + + urlExpressions[1] + " : " + urlExpressions[0] + "),"); } - sw.println(rect.getLeft() + ", " + rect.getTop() + ", " + rect.getWidth() - + ", " + rect.getHeight() + ", " + rect.isAnimated() + ", " - + rect.isLossy()); + sw.println(rect.getLeft() + ", " + rect.getTop() + ", " + rect.getWidth() + ", " + + rect.getHeight() + ", " + rect.isAnimated() + ", " + rect.isLossy()); sw.outdent(); sw.print(")"); @@ -504,8 +498,8 @@ * create the bundled images. */ @Override - public void createFields(TreeLogger logger, ResourceContext context, - ClientBundleFields fields) throws UnableToCompleteException { + public void createFields(TreeLogger logger, ResourceContext context, ClientBundleFields fields) + throws UnableToCompleteException { renderImageMap(logger, context, fields, shared.bundledImages); renderImageMap(logger, context, fields, shared.externalImages); } @@ -579,18 +573,15 @@ logger.log(TreeLogger.SPAM, "Reencoded image and saved " + (originalSize - newSize) + " bytes"); } - localizedImage = new LocalizedImage(localizedImage, - reencodedContents); + localizedImage = new LocalizedImage(localizedImage, reencodedContents); } } catch (IOException e2) { // Non-fatal, but weird logger.log(TreeLogger.WARN, - "Unable to determine before/after size when re-encoding image " - + "data", e2); + "Unable to determine before/after size when re-encoding image " + "data", e2); } } - ExternalImage externalImage = new ExternalImage(image, localizedImage, - rect); + ExternalImage externalImage = new ExternalImage(image, localizedImage, rect); shared.externalImages.put(new BundleKey(image, true), externalImage); displayed = externalImage; } @@ -653,8 +644,7 @@ return null; } - File file = File.createTempFile( - ImageResourceGenerator.class.getSimpleName(), ".png"); + File file = File.createTempFile(ImageResourceGenerator.class.getSimpleName(), ".png"); file.deleteOnExit(); Util.writeBytesToFile(logger, file, imageBytes); return file.toURI().toURL();
diff --git a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java index 7bab92a..6b207f3 100644 --- a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java +++ b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java
@@ -31,6 +31,7 @@ import com.google.gwt.safehtml.shared.OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.google.gwt.safehtml.shared.SafeUri; import com.google.gwt.safehtml.shared.UriUtils; import com.google.gwt.user.rebind.AbstractGeneratorClassCreator; import com.google.gwt.user.rebind.AbstractMethodCreator; @@ -47,6 +48,16 @@ private static final String JAVA_LANG_STRING_FQCN = String.class.getName(); /** + * Simple class name of the {@link SafeUri} interface. + */ + private static final String SAFE_URI_CN = SafeUri.class.getSimpleName(); + + /** + * Fully-qualified class name of the {@link SafeUri} interface. + */ + private static final String SAFE_URI_FQCN = SafeUri.class.getName(); + + /** * Simple class name of the {@link SafeStyles} interface. */ private static final String SAFE_STYLES_CN = SafeStyles.class.getSimpleName(); @@ -75,15 +86,14 @@ /** * Fully-qualified class name of the {@link SafeHtmlUtils} class. */ - private static final String ESCAPE_UTILS_FQCN = SafeHtmlUtils.class.getName(); + private static final String SAFE_HTML_UTILS_FQCN = SafeHtmlUtils.class.getName(); /** * Fully-qualified class name of the {@link UriUtils} class. */ private static final String URI_UTILS_FQCN = UriUtils.class.getName(); - public SafeHtmlTemplatesImplMethodCreator( - AbstractGeneratorClassCreator classCreator) { + public SafeHtmlTemplatesImplMethodCreator(AbstractGeneratorClassCreator classCreator) { super(classCreator); } @@ -91,9 +101,8 @@ * {@inheritDoc} */ @Override - public void createMethodFor(TreeLogger logger, JMethod targetMethod, - String key, ResourceList resourceList, GwtLocale locale) - throws UnableToCompleteException { + public void createMethodFor(TreeLogger logger, JMethod targetMethod, String key, + ResourceList resourceList, GwtLocale locale) throws UnableToCompleteException { if (!targetMethod.getReturnType().getQualifiedSourceName().equals( SafeHtmlTemplatesImplMethodCreator.SAFE_HTML_FQCN)) { throw error(logger, "All methods in interfaces extending " @@ -102,8 +111,8 @@ } Template templateAnnotation = targetMethod.getAnnotation(Template.class); if (templateAnnotation == null) { - throw error(logger, "Required annotation @Template not present " - + "on interface method " + targetMethod.toString()); + throw error(logger, "Required annotation @Template not present on interface method " + + targetMethod.toString()); } String template = templateAnnotation.value(); @@ -122,11 +131,14 @@ * <ul> * <li>If the parameter is of type {@link SafeStyles}, it is converted to a * string using {@link SafeStyles#asString()}. - * <li>Otherwise, if the parameter is not of type {@link String}, it is first - * converted to {@link String}. - * <li>If the template parameter occurs at the start of a URI-valued attribute - * within the template, it is sanitized to ensure that it is safe in this - * context. This is done by passing the value through + * <li>Otherwise, if the parameter is of type {@link SafeUri}, it is converted to a + * string using {@link SafeUri#asString()}. + * <li>Otherwise, if the parameter is not of type {@link String}, it is + * first converted to {@link String}. + * <li>If the template parameter occurs at the start, or as the entire value, + * of a URI-valued attribute within the template, and the parameter isn't + * of type {@link SafeUri}, it is sanitized to ensure that it is safe in + * this context. This is done by passing the value through * {@link UriUtils#sanitizeUri(String)}. * <li>The result is then HTML-escaped by passing it through * {@link SafeHtmlUtils#htmlEscape(String)}. @@ -145,9 +157,8 @@ * @param parameterType the Java type of the corresponding template method's * parameter */ - private void emitAttributeContextParameterExpression(TreeLogger logger, - HtmlContext htmlContext, String formalParameterName, - JType parameterType) { + private void emitAttributeContextParameterExpression(TreeLogger logger, HtmlContext htmlContext, + String formalParameterName, JType parameterType) { /* * Build up the expression from the "inside out", i.e. start with the formal @@ -156,24 +167,26 @@ */ String expression = formalParameterName; - if (isSafeStyles(parameterType)) { - // SafeCss is safe in a CSS context, so we can use its string (but we still - // escape it). + if (isSafeUri(parameterType) || isSafeStyles(parameterType)) { + // SafeUri is safe in a URL context, and SafeStyles is safe in a CSS context, + // so we can use their string (but we still escape it). expression = expression + ".asString()"; - } else if (!JAVA_LANG_STRING_FQCN.equals(parameterType.getQualifiedSourceName())) { - // The parameter's value must be explicitly converted to String unless it - // is already of that type. - expression = "String.valueOf(" + expression + ")"; - } + } else { + if (!JAVA_LANG_STRING_FQCN.equals(parameterType.getQualifiedSourceName())) { + // The parameter's value must be explicitly converted to String unless it + // is already of that type. + expression = "String.valueOf(" + expression + ")"; + } - if ((htmlContext.getType() == HtmlContext.Type.URL_ATTRIBUTE_START) || - (htmlContext.getType() == HtmlContext.Type.URL_ATTRIBUTE_ENTIRE)) { - expression = URI_UTILS_FQCN + ".sanitizeUri(" + expression + ")"; + if ((htmlContext.getType() == HtmlContext.Type.URL_ATTRIBUTE_START) || + (htmlContext.getType() == HtmlContext.Type.URL_ATTRIBUTE_ENTIRE)) { + expression = URI_UTILS_FQCN + ".sanitizeUri(" + expression + ")"; + } } // TODO(xtof): Handle EscapedString subtype of SafeHtml, once it's been // introduced. - expression = ESCAPE_UTILS_FQCN + ".htmlEscape(" + expression + ")"; + expression = SAFE_HTML_UTILS_FQCN + ".htmlEscape(" + expression + ")"; print(expression); } @@ -199,8 +212,8 @@ * @throws UnableToCompleteException if an error occurred that prevented * code generation for the template */ - private void emitMethodBodyFromTemplate(TreeLogger logger, String template, - JParameter[] params) throws UnableToCompleteException { + private void emitMethodBodyFromTemplate(TreeLogger logger, String template, JParameter[] params) + throws UnableToCompleteException { println("StringBuilder sb = new java.lang.StringBuilder();"); HtmlTemplateParser parser = new HtmlTemplateParser(logger); @@ -214,17 +227,16 @@ int formalParameterIndex = parameterChunk.getParameterIndex(); if (formalParameterIndex < 0 || formalParameterIndex >= params.length) { - throw error(logger, "Argument " + formalParameterIndex - + " beyond range of arguments: " + template); + throw error(logger, "Argument " + formalParameterIndex + " beyond range of arguments: " + + template); } String formalParameterName = "arg" + formalParameterIndex; JType paramType = params[formalParameterIndex].getType(); - emitParameterExpression(logger, parameterChunk.getContext(), - formalParameterName, paramType); + emitParameterExpression( + logger, parameterChunk.getContext(), formalParameterName, paramType); } else { - throw error(logger, "Unexpected chunk kind in parsed template " - + template); + throw error(logger, "Unexpected chunk kind in parsed template " + template); } } outdent(); @@ -282,6 +294,24 @@ + " used in a non-CSS attribute context. Did you mean to use " + JAVA_LANG_STRING_FQCN + " or " + SAFE_HTML_CN + " instead?"); } + } else if (isSafeUri(parameterType) && HtmlContext.Type.URL_ATTRIBUTE_ENTIRE != contextType) { + // TODO(xtof): refactor HtmlContext with isStart/isEnd/isEntire accessors and simplified type. + if (HtmlContext.Type.URL_ATTRIBUTE_START == contextType) { + // SafeUri can only be used as the entire value of an URL attribute. + throw error(logger, SAFE_URI_CN + " cannot be used in a URL attribute if it isn't the " + + "entire attribute value."); + } else { + /* + * SafeUri outside a URL-attribute context (or in a URL-attribute, but + * not at start). SafeUri is only safe if it comprises the entire URL + * attribute's value. We could treat it as a normal parameter and escape + * the string value of the parameter, but it almost definitely isn't + * what the developer intended to do. + */ + throw error(logger, SAFE_URI_CN + " can only be used as the entire value of a URL " + + "attribute. Did you mean to use " + JAVA_LANG_STRING_FQCN + " or " + SAFE_HTML_CN + + " instead?"); + } } print("sb.append("); @@ -289,10 +319,10 @@ case CSS: /* * TODO(jlabanca): Handle CSS in a text context. - * + * * The stream parser does not parse CSS; we could however improve safety * via sub-formats that specify the in-css context. - * + * * SafeStyles is safe in a CSS context when used inside of a CSS style * rule, but they are not always safe. We could implement SafeCssRules, * which would consist of SafeStyles inside of a CSS style rules found @@ -315,20 +345,35 @@ * used SafeStyles in the current context. */ if (!isSafeStyles(parameterType)) { - // Warn against using unsafe parameters in a CSS attribute context. + // Warn against using unsafe parameters in a CSS attribute context. logger.log(TreeLogger.WARN, "Template with variable in CSS attribute context: The template code generator cannot" + " guarantee HTML-safety of the template -- please inspect manually or use " + SAFE_STYLES_CN + " to specify arguments in a CSS attribute context"); } - emitAttributeContextParameterExpression(logger, htmlContext, - formalParameterName, parameterType); + emitAttributeContextParameterExpression(logger, htmlContext, formalParameterName, + parameterType); break; case URL_ATTRIBUTE_START: case URL_ATTRIBUTE_ENTIRE: + /* + * We already checked if the user tried to use SafeUri in an invalid + * (non-URL_ATTRIBUTE) context, but now we check if the user could have + * used SafeUri in the current context. + */ + if (!isSafeUri(parameterType)) { + // Warn against using unsafe parameters in a URL attribute context. + logger.log(TreeLogger.WARN, + "Template with variable in URL attribute context: The template code generator cannot" + + " guarantee HTML-safety of the template -- please inspect manually or use " + + SAFE_URI_CN + " to specify arguments in a URL attribute context"); + } + emitAttributeContextParameterExpression(logger, htmlContext, formalParameterName, + parameterType); + break; case ATTRIBUTE_VALUE: - emitAttributeContextParameterExpression(logger, htmlContext, - formalParameterName, parameterType); + emitAttributeContextParameterExpression(logger, htmlContext, formalParameterName, + parameterType); break; default: @@ -360,8 +405,10 @@ * <ul> * <li>If the parameter is of a primitive (e.g., numeric, boolean) type, or * of type {@link SafeHtml}, it is emitted as is, without escaping. - * <li>Otherwise, an expression that passes the paramter's value through - * {@link SafeHtmlUtils#htmlEscape(String)} is emitted. + * <li>Otherwise, an expression that passes the parameter's value through + * {@link SafeHtmlUtils#htmlEscape(String)} is emitted. If the value is + * of type {@link SafeUri}, it is converted to string using + * {@link SafeUri#asString()}. * </ul> * * @param formalParameterName the name of the template method's formal @@ -369,13 +416,12 @@ * @param parameterType the Java type of the corresponding template method's * parameter */ - private void emitTextContextParameterExpression(String formalParameterName, - JType parameterType) { + private void emitTextContextParameterExpression(String formalParameterName, JType parameterType) { boolean parameterIsPrimitiveType = (parameterType.isPrimitive() != null); - boolean parameterIsNotStringTyped = !(JAVA_LANG_STRING_FQCN.equals( - parameterType.getQualifiedSourceName())); + boolean parameterIsNotStringTyped = + !(JAVA_LANG_STRING_FQCN.equals(parameterType.getQualifiedSourceName())); - if (SAFE_HTML_FQCN.equals(parameterType.getQualifiedSourceName())) { + if (isSafeHtml(parameterType)) { // The parameter is of type SafeHtml and its wrapped string can // therefore be emitted safely without escaping. print(formalParameterName + ".asString()"); @@ -387,21 +433,17 @@ // The parameter is of some other type, and its value must be HTML // escaped. Furthermore, unless the parameter's type is {@link String}, // it must be explicitly converted to {@link String}. - print(ESCAPE_UTILS_FQCN + ".htmlEscape("); + String expression = formalParameterName; if (parameterIsNotStringTyped) { - print("String.valueOf("); + expression = "String.valueOf(" + expression + ")"; } - print(formalParameterName); - if (parameterIsNotStringTyped) { - print(")"); - } - print(")"); + print(SAFE_HTML_UTILS_FQCN + ".htmlEscape(" + expression + ")"); } } /** * Check if the specified parameter type represents a {@link SafeHtml}. - * + * * @param parameterType the Java parameter type * @return true if the type represents a {@link SafeHtml} */ @@ -411,11 +453,21 @@ /** * Check if the specified parameter type represents a {@link SafeStyles}. - * + * * @param parameterType the Java parameter type * @return true if the type represents a {@link SafeStyles} */ private boolean isSafeStyles(JType parameterType) { return parameterType.getQualifiedSourceName().equals(SAFE_STYLES_FQCN); } + + /** + * Check if the specified parameter type represents a {@link SafeUri}. + * + * @param parameterType the Java parameter type + * @return true if the type represents a {@link SafeUri} + */ + private boolean isSafeUri(JType parameterType) { + return parameterType.getQualifiedSourceName().equals(SAFE_URI_FQCN); + } }
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java b/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java index 1ea9c38..e3a10f2 100644 --- a/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java +++ b/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java
@@ -22,8 +22,7 @@ */ public final class SafeHtmlUtils { - private static final String HTML_ENTITY_REGEX = - "[a-z]+|#[0-9]+|#x[0-9a-fA-F]+"; + private static final String HTML_ENTITY_REGEX = "[a-z]+|#[0-9]+|#x[0-9a-fA-F]+"; /** * An empty String. @@ -104,9 +103,8 @@ } /** - * HTML-escapes a character. HTML meta characters - * will be escaped as follows: - * + * HTML-escapes a character. HTML meta characters will be escaped as follows: + * * <pre> * & - &amp; * < - &lt; @@ -167,7 +165,7 @@ if (s.indexOf("\"") != -1) { s = QUOT_RE.replace(s, """); } - if (s.indexOf("\'") != -1) { + if (s.indexOf("'") != -1) { s = SQUOT_RE.replace(s, "'"); } return s; @@ -201,8 +199,7 @@ } int entityEnd = segment.indexOf(';'); - if (entityEnd > 0 - && segment.substring(0, entityEnd).matches(HTML_ENTITY_REGEX)) { + if (entityEnd > 0 && segment.substring(0, entityEnd).matches(HTML_ENTITY_REGEX)) { // Append the entity without escaping. escaped.append("&").append(segment.substring(0, entityEnd + 1));
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeUri.java b/user/src/com/google/gwt/safehtml/shared/SafeUri.java new file mode 100644 index 0000000..e09aa35 --- /dev/null +++ b/user/src/com/google/gwt/safehtml/shared/SafeUri.java
@@ -0,0 +1,80 @@ +/* + * Copyright 2011 Google Inc. + * + * 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 com.google.gwt.safehtml.shared; + +/** + * An object that implements this interface encapsulates a URI that is + * guaranteed to be safe to use (with respect to potential Cross-Site-Scripting + * vulnerabilities) in a URL context, for example in a URL-typed attribute in an + * HTML document. + * + * <p> + * Note on usage: SafeUri should be used to ensure user input is not executed in + * the browser. SafeUri should not be used to sanitize input before sending it + * to the server: The server cannot rely on the type contract of SafeUri values + * received from clients, because a malicious client could provide maliciously + * crafted serialized forms of implementations of this type that violate the + * type contract. + * + * <p> + * All implementing classes must maintain the class invariant (by design and + * implementation and/or convention of use), that invoking {@link #asString()} + * on any instance will return a string that is safe to assign to a URL-typed + * DOM or CSS property in a browser (or to use similarly in a "URL context"), in + * the sense that doing so must not cause unintended execution of script in the + * browser. + * + * <p> + * In determining safety of a URL both the value itself as well as its + * provenance matter. An arbitrary URI, including e.g. a + * <code>javascript:</code> URI, can be deemed safe in the sense of this type's + * contract if it is entirely under the program's control (e.g., a string + * literal, {@see UriUtils#fromSafeConstant}). + * + * <p> + * All implementations must implement equals() and hashCode() to behave + * consistently with the result of asString().equals() and asString.hashCode(). + * + * <p> + * Implementations must not return {@code null} from {@link #asString()}. + */ +public interface SafeUri { + + /** + * Returns this object's contained URI as a string. + * + * <p> + * Based on this class' contract, the returned value will be non-null and a + * string that is safe to use in a URL context. + * + * @return the contents as a String + */ + String asString(); + + /** + * Compares this string to the specified object. Must be equal to + * asString().equals(). + * + * @param anObject the object to compare to + */ + boolean equals(Object anObject); + + /** + * Returns a hash code for this string. Must be equal to + * asString().hashCode(). + */ + int hashCode(); +}
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java b/user/src/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java new file mode 100644 index 0000000..597e63c --- /dev/null +++ b/user/src/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java
@@ -0,0 +1,123 @@ +/* + * Copyright 2011 Google Inc. + * + * 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 com.google.gwt.safehtml.shared; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.thirdparty.guava.common.base.Preconditions; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * SafeUri utilities whose implementation differs between Development and + * Production Mode. + * + * <p> + * This class has a super-source peer that provides the Production Mode + * implementation. + * + * <p> + * Do not use this class - it is used for implementation only, and its methods + * may change in the future. + */ +public class SafeUriHostedModeUtils { + + /** + * All valid Web Addresses, i.e. the href-ucschar production from RFC 3987bis. + * + * @see <a href="http://tools.ietf.org/html/rfc3986#section-2">RFC 3986</a> + * @see <a href="http://tools.ietf.org/html/draft-ietf-iri-3987bis-05#section-7.2">RFC 3987bis Web Addresses</a> + */ + static final String HREF_UCSCHAR = "(" + + "[" + + ":/?#\\[\\]@!$&'()*+,;=" // reserved + + "a-zA-Z0-9\\-._~" // iunreserved + + " <>\"{}|\\\\^`\u0000-\u001F\u001F-\uD7FF\uE000-\uFFFD" // href-ucschar + + "]" + + "|" + + "[\uD800-\uDBFF][\uDC00-\uDFFF]" // surrogate pairs + + ")*"; + + /** + * Name of system property that if set, enables checks in server-side code + * (even if assertions are disabled). + */ + public static final String FORCE_CHECK_VALID_URI = "com.google.gwt.safehtml.ForceCheckValidUri"; + + private static boolean forceCheckValidUri; + + static { + setForceCheckValidUriFromProperty(); + } + + /** + * Checks if the provided URI is a valid Web Address (per RFC 3987bis). + * + * @param uri the URL to check + */ + public static void maybeCheckValidUri(String uri) { + if (GWT.isClient() || forceCheckValidUri) { + Preconditions.checkArgument(isValidUri(uri), "String is not a valid URI: %s", uri); + } else { + assert isValidUri(uri) : "String is not a valid URI: " + uri; + } + } + + /** + * Sets a global flag that controls whether or not + * {@link #maybeCheckValidUri(String)} should perform its check in a + * server-side environment. + * + * @param check if true, perform server-side checks. + */ + public static void setForceCheckValidUri(boolean check) { + forceCheckValidUri = check; + } + + /** + * Sets a global flag that controls whether or not + * {@link #maybeCheckValidUri(String)} should perform its check in a + * server-side environment from the value of the {@value + * FORCE_CHECK_VALID_URI} property. + */ + // The following annotation causes javadoc to crash on Mac OS X 10.5.8, + // using java 1.5.0_24. + // + // See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6442982 + // + // @VisibleForTesting + public static void setForceCheckValidUriFromProperty() { + forceCheckValidUri = System.getProperty(FORCE_CHECK_VALID_URI) != null; + } + + private static boolean isValidUri(String uri) { + if (!uri.matches(HREF_UCSCHAR)) { + return false; + } + /* + * pre-process to turn href-ucschars into ucschars, and encode to URI. + * + * This is done by encoding everything, and decoding back "%25" to "%". + */ + uri = UriUtils.encode(uri).replace("%25", "%"); + try { + new URI(uri); + return true; + } catch (URISyntaxException e) { + return false; + } + } +}
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeUriString.java b/user/src/com/google/gwt/safehtml/shared/SafeUriString.java new file mode 100644 index 0000000..92fb99c --- /dev/null +++ b/user/src/com/google/gwt/safehtml/shared/SafeUriString.java
@@ -0,0 +1,78 @@ +/* + * Copyright 2011 Google Inc. + * + * 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 com.google.gwt.safehtml.shared; + +/** + * A string wrapped as an object of type {@link SafeUri}. + * + * <p> + * This class is package-private and intended for internal use by the + * {@link com.google.gwt.safehtml} package. + * + * All implementors must implement .equals and .hashCode so that they operate + * just like String.equals() and String.hashCode(). + */ +class SafeUriString implements SafeUri { + private String uri; + + /** + * Constructs a {@link SafeUriString} from a string. Callers are responsible + * for ensuring that the string passed as the argument to this constructor + * satisfies the constraints of the contract imposed by the {@link SafeUri} + * interface. + * + * @param uri the string to be wrapped as a {@link SafeUri} + */ + SafeUriString(String uri) { + if (uri == null) { + throw new NullPointerException("uri is null"); + } + this.uri = uri; + } + + /** + * No-arg constructor for compatibility with GWT serialization. + */ + @SuppressWarnings("unused") + private SafeUriString() { + } + + /** + * {@inheritDoc} + */ + public String asString() { + return uri; + } + + /** + * Compares this string to the specified object. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SafeUri)) { + return false; + } + return uri.equals(((SafeUri) obj).asString()); + } + + /** + * Returns a hash code for this string. + */ + @Override + public int hashCode() { + return uri.hashCode(); + } +}
diff --git a/user/src/com/google/gwt/safehtml/shared/UriUtils.java b/user/src/com/google/gwt/safehtml/shared/UriUtils.java index c177518..896d1d9 100644 --- a/user/src/com/google/gwt/safehtml/shared/UriUtils.java +++ b/user/src/com/google/gwt/safehtml/shared/UriUtils.java
@@ -1,12 +1,12 @@ /* * Copyright 2010 Google Inc. - * + * * 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 @@ -15,14 +15,120 @@ */ package com.google.gwt.safehtml.shared; +import com.google.gwt.core.client.GWT; +import com.google.gwt.http.client.URL; +import com.google.gwt.regexp.shared.RegExp; + +import java.io.UnsupportedEncodingException; + /** * Utility class containing static methods for validating and sanitizing URIs. */ public final class UriUtils { - + + /** + * Characters that don't need %-escaping (minus letters and digits), according + * to ECMAScript 5th edition for the {@code encodeURI} function. + */ + static final String DONT_NEED_ENCODING = ";/?:@&=+$," // uriReserved + + "-_.!~*'()" // uriMark + + "#" + + "[]"; // could be used in IPv6 addresses + + // used in conditional code in encode() + private static final RegExp ESCAPED_LBRACKET_RE = + GWT.isScript() ? RegExp.compile("%5B", "g") : null; + private static final RegExp ESCAPED_RBRACKET_RE = + GWT.isScript() ? RegExp.compile("%5D", "g") : null; + + /** + * Encodes the URL. + * <p> + * In client code, this method delegates to {@link URL#encode(String)} and + * then unescapes brackets, as they might be used for IPv6 addresses. + * + * @param uri the URL to encode + * @return the %-escaped URL + */ + public static String encode(String uri) { + if (GWT.isScript()) { + uri = URL.encode(uri); + // Follow the same approach as SafeHtmlUtils.htmlEscape + if (uri.indexOf("%5B") != -1) { + uri = ESCAPED_LBRACKET_RE.replace(uri, "["); + } + if (uri.indexOf("%5D") != -1) { + uri = ESCAPED_RBRACKET_RE.replace(uri, "]"); + } + return uri; + } else { + StringBuilder sb = new StringBuilder(); + byte[] utf8bytes; + try { + utf8bytes = uri.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + // UTF-8 is guaranteed to be implemented, this code won't ever run. + return null; + } + for (byte b : utf8bytes) { + int c = b & 0xFF; + // This works because characters that don't need encoding are all + // expressed as a single UTF-8 byte + if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') + || DONT_NEED_ENCODING.indexOf(c) != -1) { + sb.append((char) c); + } else { + String hexByte = Integer.toHexString(c).toUpperCase(); + if (hexByte.length() == 1) { + hexByte = "0" + hexByte; + } + sb.append('%').append(hexByte); + } + } + return sb.toString(); + } + } + + /** + * Encodes the URL, preserving existing %-escapes. + * + * @param uri the URL to encode + * @return the %-escaped URL + */ + public static String encodeAllowEscapes(String uri) { + StringBuilder escaped = new StringBuilder(); + + boolean firstSegment = true; + for (String segment : uri.split("%", -1)) { + if (firstSegment) { + /* + * The first segment is never part of a percent-escape, so we always + * escape it. Note that if the input starts with a percent, we will get + * an empty segment before that. + */ + firstSegment = false; + escaped.append(encode(segment)); + continue; + } + + if (segment.length() >= 2 && segment.substring(0, 2).matches("[0-9a-fA-F]{2}")) { + // Append the escape without encoding. + escaped.append("%").append(segment.substring(0, 2)); + + // Append the rest of the segment, escaped. + escaped.append(encode(segment.substring(2))); + } else { + // The segment did not start with an escape, so encode the whole + // segment. + escaped.append("%25").append(encode(segment)); + } + } + return escaped.toString(); + } + /** * Extracts the scheme of a URI. - * + * * @param uri the URI to extract the scheme from * @return the URI's scheme, or {@code null} if the URI does not have one */ @@ -36,7 +142,7 @@ /* * The URI's prefix up to the first ':' contains other URI special * chars, and won't be interpreted as a scheme. - * + * * TODO(xtof): Consider basing this on URL#isValidProtocol or similar; * however I'm worried that being too strict here will effectively * allow dangerous schemes accepted in loosely parsing browsers. @@ -45,18 +151,66 @@ } return scheme; } - + + /** + * Returns a {@link SafeUri} constructed from a value that is fully under + * the control of the program, e.g., a constant. + * + * <p> + * The string is not sanitized and no checks are performed. The assumption + * that the resulting value adheres to the {@link SafeUri} type contract + * is entirely based on the argument being fully under program control and + * not being derived from a program input. + * + * <p> + * <strong>Convention of use:</strong> This method must only be invoked on + * values that are fully under the program's control, such as string literals. + * + * @param s the input String + * @return a SafeUri instance + */ + public static SafeUri fromSafeConstant(String s) { + SafeUriHostedModeUtils.maybeCheckValidUri(s); + return new SafeUriString(s); + } + + /** + * Returns a {@link SafeUri} obtained by sanitizing the provided string. + * + * <p> + * The input string is sanitized using {@link #sanitizeUri(String)}. + * + * @param s the input String + * @return a SafeUri instance + */ + public static SafeUri fromString(String s) { + return new SafeUriString(sanitizeUri(s)); + } + + /** + * Returns a {@link SafeUri} constructed from a trusted string, i.e., without + * sanitizing the string. No checks are performed. The calling code should be + * carefully reviewed to ensure the argument meets the SafeUri contract. + * + * @param s the input String + * @return a SafeUri instance + */ + public static SafeUri fromTrustedString(String s) { + SafeUriHostedModeUtils.maybeCheckValidUri(s); + return new SafeUriString(s); + } + /** * Determines if a {@link String} is safe to use as the value of a URI-valued * HTML attribute such as {@code src} or {@code href}. - * + * * <p> * In this context, a URI is safe if it can be established that using it as * the value of a URI-valued HTML attribute such as {@code src} or {@code * href} cannot result in script execution. Specifically, this method deems a * URI safe if it either does not have a scheme, or its scheme is one of * {@code http, https, ftp, mailto}. - * + * * @param uri the URI to validate * @return {@code true} if {@code uri} is safe in the above sense; {@code * false} otherwise @@ -77,29 +231,49 @@ return ("http".equals(schemeLc) || "https".equals(schemeLc) || "ftp".equals(schemeLc) - || "mailto".equals(schemeLc) + || "mailto".equals(schemeLc) || "MAILTO".equals(scheme.toUpperCase())); } /** * Sanitizes a URI. - * + * * <p> * This method returns the URI provided if it is safe to use as the the value * of a URI-valued HTML attribute according to {@link #isSafeUri}, or the URI * "{@code #}" otherwise. - * + * * @param uri the URI to sanitize * @return a sanitized String */ public static String sanitizeUri(String uri) { if (isSafeUri(uri)) { - return uri; + return encodeAllowEscapes(uri); } else { return "#"; } } + /** + * Returns a {@link SafeUri} constructed from an untrusted string but without + * sanitizing it. + * + * <strong>Despite this method creating a SafeUri instance, no checks are + * performed, so the returned SafeUri is absolutely NOT guaranteed to be + * safe!</strong> + * + * @param s the input String + * @return a SafeUri instance + * @deprecated This method is intended only for use in APIs that use + * {@link SafeUri} to represent URIs, but for backwards + * compatibility have methods that accept URI parameters as plain + * strings. + */ + @Deprecated + public static SafeUri unsafeCastFromUntrustedString(String s) { + return new SafeUriString(s); + } + // prevent instantiation private UriUtils() { }
diff --git a/user/src/com/google/gwt/user/client/ui/AbstractImagePrototype.java b/user/src/com/google/gwt/user/client/ui/AbstractImagePrototype.java index ee80f69..b40d913 100644 --- a/user/src/com/google/gwt/user/client/ui/AbstractImagePrototype.java +++ b/user/src/com/google/gwt/user/client/ui/AbstractImagePrototype.java
@@ -17,6 +17,7 @@ import com.google.gwt.dom.client.Element; import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.user.client.ui.impl.ClippedImagePrototype; /** @@ -70,8 +71,8 @@ * ImageResource */ public static AbstractImagePrototype create(ImageResource resource) { - return new ClippedImagePrototype(resource.getURL(), resource.getLeft(), - resource.getTop(), resource.getWidth(), resource.getHeight()); + return new ClippedImagePrototype(resource.getSafeUri(), resource.getLeft(), resource.getTop(), + resource.getWidth(), resource.getHeight()); } /** @@ -123,8 +124,28 @@ * prototype. The HTML returned is not necessarily a simple * <code><img></code> element. It may be a more complex structure that * should be treated opaquely. + * <p> + * The default implementation calls {@link #getSafeHtml()}. * * @return the HTML representation of this prototype */ - public abstract String getHTML(); + public String getHTML() { + return getSafeHtml().asString(); + } + + /** + * Gets an HTML fragment that displays the image represented by this + * prototype. The HTML returned is not necessarily a simple + * <code><img></code> element. It may be a more complex structure that + * should be treated opaquely. + * <p> + * The default implementation throws an {@link UnsupportedOperationException}. + * + * @return the HTML representation of this prototype + */ + public SafeHtml getSafeHtml() { + // Because this is a new method on an existing base class, we need to throw + // UnsupportedOperationException to avoid static errors. + throw new UnsupportedOperationException(); + } }
diff --git a/user/src/com/google/gwt/user/client/ui/FormPanel.java b/user/src/com/google/gwt/user/client/ui/FormPanel.java index 697b158..6143ff5 100644 --- a/user/src/com/google/gwt/user/client/ui/FormPanel.java +++ b/user/src/com/google/gwt/user/client/ui/FormPanel.java
@@ -22,6 +22,7 @@ import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.safehtml.shared.SafeUri; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DeferredCommand; import com.google.gwt.user.client.Event; @@ -59,13 +60,11 @@ * </p> */ @SuppressWarnings("deprecation") -public class FormPanel extends SimplePanel implements FiresFormEvents, - FormPanelImplHost { +public class FormPanel extends SimplePanel implements FiresFormEvents, FormPanelImplHost { /** * Fired when a form has been submitted successfully. */ - public static class SubmitCompleteEvent extends - GwtEvent<SubmitCompleteHandler> { + public static class SubmitCompleteEvent extends GwtEvent<SubmitCompleteHandler> { /** * The event type. */ @@ -196,7 +195,7 @@ */ public interface SubmitHandler extends EventHandler { /** - *Fired when the form is submitted. + * Fired when the form is submitted. * * <p> * The FormPanel must <em>not</em> be detached (i.e. removed from its parent @@ -418,8 +417,7 @@ * @param handler the handler * @return the handler registration used to remove the handler */ - public HandlerRegistration addSubmitCompleteHandler( - SubmitCompleteHandler handler) { + public HandlerRegistration addSubmitCompleteHandler(SubmitCompleteHandler handler) { return addHandler(handler, SubmitCompleteEvent.getType()); } @@ -514,6 +512,16 @@ } /** + * Sets the 'action' associated with this form. This is the URL to which it + * will be submitted. + * + * @param url the form's action + */ + public void setAction(SafeUri url) { + setAction(url.asString()); + } + + /** * Sets the encoding used for submitting this form. This should be either * {@link #ENCODING_MULTIPART} or {@link #ENCODING_URLENCODED}. *
diff --git a/user/src/com/google/gwt/user/client/ui/Frame.java b/user/src/com/google/gwt/user/client/ui/Frame.java index cdd8b48..0e1005c 100644 --- a/user/src/com/google/gwt/user/client/ui/Frame.java +++ b/user/src/com/google/gwt/user/client/ui/Frame.java
@@ -23,6 +23,7 @@ import com.google.gwt.event.dom.client.LoadEvent; import com.google.gwt.event.dom.client.LoadHandler; import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.safehtml.shared.SafeUri; /** * A widget that wraps an IFRAME element, which can contain an arbitrary web @@ -125,6 +126,15 @@ getFrameElement().setSrc(url); } + /** + * Sets the URL of the resource to be displayed within the frame. + * + * @param url the frame's new URL + */ + public void setUrl(SafeUri url) { + setUrl(url.asString()); + } + private FrameElement getFrameElement() { return getElement().cast(); }
diff --git a/user/src/com/google/gwt/user/client/ui/Image.java b/user/src/com/google/gwt/user/client/ui/Image.java index c8da399..a3aa6a4 100644 --- a/user/src/com/google/gwt/user/client/ui/Image.java +++ b/user/src/com/google/gwt/user/client/ui/Image.java
@@ -80,6 +80,8 @@ import com.google.gwt.event.dom.client.TouchStartHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.safehtml.shared.SafeUri; +import com.google.gwt.safehtml.shared.UriUtils; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.impl.ClippedImageImpl; @@ -123,10 +125,9 @@ * </p> */ @SuppressWarnings("deprecation") -public class Image extends Widget implements SourcesLoadEvents, HasLoadHandlers, - HasErrorHandlers, SourcesClickEvents, HasClickHandlers, - HasDoubleClickHandlers, HasAllDragAndDropHandlers, HasAllGestureHandlers, - HasAllMouseHandlers, HasAllTouchHandlers, SourcesMouseEvents { +public class Image extends Widget implements SourcesLoadEvents, HasLoadHandlers, HasErrorHandlers, + SourcesClickEvents, HasClickHandlers, HasDoubleClickHandlers, HasAllDragAndDropHandlers, + HasAllGestureHandlers, HasAllMouseHandlers, HasAllTouchHandlers, SourcesMouseEvents { /** * The attribute that is set when an image fires a native load or error event @@ -145,11 +146,10 @@ private int left = 0; private boolean pendingNativeLoadEvent = true; private int top = 0; - private String url = null; + private SafeUri url = null; private int width = 0; - ClippedState(Image image, String url, int left, int top, int width, - int height) { + ClippedState(Image image, SafeUri url, int left, int top, int width, int height) { this.left = left; this.top = top; this.width = width; @@ -183,7 +183,7 @@ } @Override - public String getUrl(Image image) { + public SafeUri getUrl(Image image) { return url; } @@ -200,7 +200,7 @@ } @Override - public void setUrl(Image image, String url) { + public void setUrl(Image image, SafeUri url) { image.changeState(new UnclippedState(image)); // Need to make sure we change the state before an onload event can fire, // or handlers will be fired while we are in the old state. @@ -208,7 +208,7 @@ } @Override - public void setUrlAndVisibleRect(Image image, String url, int left, int top, int width, + public void setUrlAndVisibleRect(Image image, SafeUri url, int left, int top, int width, int height) { /* * In the event that the clipping rectangle has not changed, we want to @@ -238,8 +238,7 @@ } @Override - public void setVisibleRect(Image image, int left, int top, int width, - int height) { + public void setVisibleRect(Image image, int left, int top, int width, int height) { setUrlAndVisibleRect(image, url, left, top, width, height); } @@ -269,7 +268,7 @@ public abstract int getOriginTop(); - public abstract String getUrl(Image image); + public abstract SafeUri getUrl(Image image); public abstract int getWidth(Image image); @@ -297,14 +296,13 @@ // Overridden by ClippedState. } - public abstract void setUrl(Image image, String url); + public abstract void setUrl(Image image, SafeUri url); - public abstract void setUrlAndVisibleRect(Image image, String url, - int left, int top, int width, int height); - - public abstract void setVisibleRect(Image image, int left, int top, + public abstract void setUrlAndVisibleRect(Image image, SafeUri url, int left, int top, int width, int height); + public abstract void setVisibleRect(Image image, int left, int top, int width, int height); + /** * We need to synthesize a load event in case the image loads synchronously, * before our handlers can be attached. @@ -360,9 +358,8 @@ UnclippedState(Element element) { // This case is relatively unusual, in that we swapped a clipped image // out, so does not need to be efficient. - Event.sinkEvents(element, Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS - | Event.ONLOAD | Event.ONERROR | Event.ONMOUSEWHEEL | Event.TOUCHEVENTS - | Event.GESTUREEVENTS); + Event.sinkEvents(element, Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS | Event.ONLOAD + | Event.ONERROR | Event.ONMOUSEWHEEL | Event.TOUCHEVENTS | Event.GESTUREEVENTS); } UnclippedState(Image image) { @@ -377,7 +374,7 @@ | Event.ONERROR | Event.ONMOUSEWHEEL | Event.TOUCHEVENTS | Event.GESTUREEVENTS); } - UnclippedState(Image image, String url) { + UnclippedState(Image image, SafeUri url) { this(image); setUrl(image, url); } @@ -403,8 +400,8 @@ } @Override - public String getUrl(Image image) { - return getImageElement(image).getSrc(); + public SafeUri getUrl(Image image) { + return UriUtils.unsafeCastFromUntrustedString(getImageElement(image).getSrc()); } @Override @@ -413,22 +410,20 @@ } @Override - public void setUrl(Image image, String url) { + public void setUrl(Image image, SafeUri url) { image.clearUnhandledEvent(); - getImageElement(image).setSrc(url); + getImageElement(image).setSrc(url.asString()); } @Override - public void setUrlAndVisibleRect(Image image, String url, int left, - int top, int width, int height) { + public void setUrlAndVisibleRect(Image image, SafeUri url, int left, int top, int width, + int height) { image.changeState(new ClippedState(image, url, left, top, width, height)); } @Override - public void setVisibleRect(Image image, int left, int top, int width, - int height) { - image.changeState(new ClippedState(image, getUrl(image), left, top, - width, height)); + public void setVisibleRect(Image image, int left, int top, int width, int height) { + image.changeState(new ClippedState(image, getUrl(image), left, top, width, height)); } // This method is used only by unit tests. @@ -457,6 +452,15 @@ } /** + * Causes the browser to pre-fetch the image at a given URL. + * + * @param url the URL of the image to be prefetched + */ + public static void prefetch(SafeUri url) { + prefetch(url.asString()); + } + + /** * Creates a Image widget that wraps an existing <img> element. * * This element must already be attached to the document. If the element is @@ -494,8 +498,8 @@ * @param resource the ImageResource to be displayed */ public Image(ImageResource resource) { - this(resource.getURL(), resource.getLeft(), resource.getTop(), - resource.getWidth(), resource.getHeight()); + this(resource.getURL(), resource.getLeft(), resource.getTop(), resource.getWidth(), + resource.getHeight()); } /** @@ -505,6 +509,16 @@ * @param url the URL of the image to be displayed */ public Image(String url) { + this(UriUtils.unsafeCastFromUntrustedString(url)); + } + + /** + * Creates an image with a specified URL. The load event will be fired once + * the image at the given URL has been retrieved by the browser. + * + * @param url the URL of the image to be displayed + */ + public Image(SafeUri url) { changeState(new UnclippedState(this, url)); setStyleName("gwt-Image"); } @@ -528,6 +542,28 @@ * @param height the height of the visibility rectangle */ public Image(String url, int left, int top, int width, int height) { + this(UriUtils.unsafeCastFromUntrustedString(url), left, top, width, height); + } + + /** + * Creates a clipped image with a specified URL and visibility rectangle. The + * visibility rectangle is declared relative to the the rectangle which + * encompasses the entire image, which has an upper-left vertex of (0,0). The + * load event will be fired immediately after the object has been constructed + * (i.e. potentially before the image has been loaded in the browser). Since + * the width and height are specified explicitly by the user, this behavior + * will not cause problems with retrieving the width and height of a clipped + * image in a load event handler. + * + * @param url the URL of the image to be displayed + * @param left the horizontal co-ordinate of the upper-left vertex of the + * visibility rectangle + * @param top the vertical co-ordinate of the upper-left vertex of the + * visibility rectangle + * @param width the width of the visibility rectangle + * @param height the height of the visibility rectangle + */ + public Image(SafeUri url, int left, int top, int width, int height) { changeState(new ClippedState(this, url, left, top, width, height)); setStyleName("gwt-Image"); } @@ -729,7 +765,7 @@ * @return the image URL */ public String getUrl() { - return state.getUrl(this); + return state.getUrl(this).asString(); } /** @@ -812,8 +848,20 @@ * @param resource the ImageResource to display */ public void setResource(ImageResource resource) { - setUrlAndVisibleRect(resource.getURL(), resource.getLeft(), - resource.getTop(), resource.getWidth(), resource.getHeight()); + setUrlAndVisibleRect(resource.getSafeUri(), resource.getLeft(), resource.getTop(), + resource.getWidth(), resource.getHeight()); + } + + /** + * Sets the URL of the image to be displayed. If the image is in the clipped + * state, a call to this method will cause a transition of the image to the + * unclipped state. Regardless of whether or not the image is in the clipped + * or unclipped state, a load event will be fired. + * + * @param url the image URL + */ + public void setUrl(SafeUri url) { + state.setUrl(this, url); } /** @@ -825,7 +873,7 @@ * @param url the image URL */ public void setUrl(String url) { - state.setUrl(this, url); + setUrl(UriUtils.unsafeCastFromUntrustedString(url)); } /** @@ -844,12 +892,31 @@ * @param width the width of the visibility rectangle * @param height the height of the visibility rectangle */ - public void setUrlAndVisibleRect(String url, int left, int top, int width, - int height) { + public void setUrlAndVisibleRect(SafeUri url, int left, int top, int width, int height) { state.setUrlAndVisibleRect(this, url, left, top, width, height); } /** + * Sets the url and the visibility rectangle for the image at the same time. A + * single load event will be fired if either the incoming url or visiblity + * rectangle co-ordinates differ from the image's current url or current + * visibility rectangle co-ordinates. If the image is currently in the + * unclipped state, a call to this method will cause a transition to the + * clipped state. + * + * @param url the image URL + * @param left the horizontal coordinate of the upper-left vertex of the + * visibility rectangle + * @param top the vertical coordinate of the upper-left vertex of the + * visibility rectangle + * @param width the width of the visibility rectangle + * @param height the height of the visibility rectangle + */ + public void setUrlAndVisibleRect(String url, int left, int top, int width, int height) { + setUrlAndVisibleRect(UriUtils.unsafeCastFromUntrustedString(url), left, top, width, height); + } + + /** * Sets the visibility rectangle of an image. The visibility rectangle is * declared relative to the the rectangle which encompasses the entire image, * which has an upper-left vertex of (0,0). Provided that any of the left,
diff --git a/user/src/com/google/gwt/user/client/ui/ImageResourceRenderer.java b/user/src/com/google/gwt/user/client/ui/ImageResourceRenderer.java index a46aed5..962e04f 100644 --- a/user/src/com/google/gwt/user/client/ui/ImageResourceRenderer.java +++ b/user/src/com/google/gwt/user/client/ui/ImageResourceRenderer.java
@@ -17,7 +17,6 @@ import com.google.gwt.resources.client.ImageResource; import com.google.gwt.safehtml.shared.SafeHtml; -import com.google.gwt.safehtml.shared.SafeHtmlUtils; import com.google.gwt.text.shared.AbstractSafeHtmlRenderer; /** @@ -26,6 +25,6 @@ public class ImageResourceRenderer extends AbstractSafeHtmlRenderer<ImageResource> { @Override public SafeHtml render(ImageResource image) { - return SafeHtmlUtils.fromTrustedString(AbstractImagePrototype.create(image).getHTML()); + return AbstractImagePrototype.create(image).getSafeHtml(); } }
diff --git a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.java b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.java index 9d112af..afa0c7c 100644 --- a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.java +++ b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.java
@@ -1,12 +1,12 @@ /* * Copyright 2008 Google Inc. - * + * * 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 @@ -18,47 +18,64 @@ import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; +import com.google.gwt.safecss.shared.SafeStyles; +import com.google.gwt.safecss.shared.SafeStylesUtils; +import com.google.gwt.safehtml.client.SafeHtmlTemplates; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeUri; +import com.google.gwt.safehtml.shared.UriUtils; import com.google.gwt.user.client.ui.Image; /** * Uses a combination of a clear image and a background image to clip all except * a desired portion of an underlying image. - * + * * Do not use this class - it is used for implementation only, and its methods * may change in the future. */ public class ClippedImageImpl { - public void adjust(Element img, String url, int left, int top, int width, - int height) { - String style = "url(" + url + ") no-repeat " + (-left + "px ") - + (-top + "px"); + interface Template extends SafeHtmlTemplates { + @SafeHtmlTemplates.Template("<img onload='this.__gwtLastUnhandledEvent=\"load\";' src='{0}' " + + "style='{1}' border='0'>") + SafeHtml image(SafeUri clearImage, SafeStyles style); + } + + protected static final SafeUri clearImage = + UriUtils.fromTrustedString(GWT.getModuleBaseURL() + "clear.cache.gif"); + private static Template template; + + public void adjust(Element img, SafeUri url, int left, int top, int width, int height) { + String style = "url(" + url.asString() + ") no-repeat " + (-left + "px ") + (-top + "px"); img.getStyle().setProperty("background", style); img.getStyle().setPropertyPx("width", width); img.getStyle().setPropertyPx("height", height); } - public Element createStructure(String url, int left, int top, int width, - int height) { + public Element createStructure(SafeUri url, int left, int top, int width, int height) { Element tmp = Document.get().createSpanElement(); - tmp.setInnerHTML(getHTML(url, left, top, width, height)); + tmp.setInnerHTML(getSafeHtml(url, left, top, width, height).asString()); return tmp.getFirstChildElement(); } - public String getHTML(String url, int left, int top, int width, int height) { - String style = "width: " + width + "px; height: " + height - + "px; background: url(" + url + ") no-repeat " + (-left + "px ") - + (-top + "px"); - - String clippedImgHtml = "<img " - + "onload='this.__gwtLastUnhandledEvent=\"load\";' src='" - + GWT.getModuleBaseURL() + "clear.cache.gif' style='" + style - + "' border='0'>"; - - return clippedImgHtml; - } - public Element getImgElement(Image image) { return image.getElement(); } + + public SafeHtml getSafeHtml(SafeUri url, int left, int top, int width, int height) { + // TODO(t.broyer): use the context-specific SafeStyles API once it's been introduced. + String style = "width: " + width + "px; height: " + height + "px; background: url(" + + url.asString() + ") " + "no-repeat " + (-left + "px ") + + (-top + "px;"); + + return getTemplate().image(clearImage, SafeStylesUtils.fromTrustedString(style)); + } + + private Template getTemplate() { + // no need to synchronize, JavaScript in the browser is single-threaded + if (template == null) { + template = GWT.create(Template.class); + } + return template; + } }
diff --git a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java index 8895b68..d69fe3b 100644 --- a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java +++ b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java
@@ -1,12 +1,12 @@ /* * Copyright 2008 Google Inc. - * + * * 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 @@ -17,6 +17,9 @@ import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Element; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.google.gwt.safehtml.shared.SafeUri; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.Image; @@ -31,7 +34,7 @@ private static String moduleBaseUrlProtocol = GWT.getHostPageBaseURL().startsWith("https") ? "https://" : "http://"; - + private static native void injectGlobalHandler() /*-{ $wnd.__gwt_transparentImgHandler = function (elem) { elem.onerror = null; @@ -48,20 +51,19 @@ } @Override - public void adjust(Element clipper, String url, int left, int top, int width, - int height) { + public void adjust(Element clipper, SafeUri url, int left, int top, int width, int height) { if (!isIE6()) { super.adjust(clipper, url, left, top, width, height); return; } - + clipper.getStyle().setPropertyPx("width", width); clipper.getStyle().setPropertyPx("height", height); // Update the nested image's url. Element img = clipper.getFirstChildElement(); img.getStyle().setProperty("filter", - "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url + "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url.asString() + "',sizingMethod='crop')"); img.getStyle().setPropertyPx("marginLeft", -left); img.getStyle().setPropertyPx("marginTop", -top); @@ -75,12 +77,11 @@ } @Override - public Element createStructure(String url, int left, int top, int width, - int height) { + public Element createStructure(SafeUri url, int left, int top, int width, int height) { if (!isIE6()) { return super.createStructure(url, left, top, width, height); } - + // We need to explicitly sink ONLOAD on the child image element, because it // can't be fired on the clipper. Element clipper = super.createStructure(url, left, top, width, height); @@ -90,16 +91,24 @@ } @Override - public String getHTML(String url, int left, int top, int width, int height) { + public Element getImgElement(Image image) { if (!isIE6()) { - return super.getHTML(url, left, top, width, height); + return super.getImgElement(image); } - + return image.getElement().getFirstChildElement(); + } + + @Override + public SafeHtml getSafeHtml(SafeUri url, int left, int top, int width, int height) { + if (!isIE6()) { + return super.getSafeHtml(url, left, top, width, height); + } + // We cannot use a SafeHtmlTemplates because there are parameters in javascript context. String clipperStyle = "overflow: hidden; width: " + width + "px; height: " + height + "px; padding: 0px; zoom: 1"; String imgStyle = "filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" - + url + + url.asString() + "',sizingMethod='crop'); margin-left: " + -left + "px; margin-top: " + -top + "px; border: none"; @@ -121,14 +130,6 @@ + imgStyle + "\" width=" + (left + width) + " height=" + (top + height) + " border='0'></gwt:clipper>"; - return clippedImgHtml; - } - - @Override - public Element getImgElement(Image image) { - if (!isIE6()) { - return super.getImgElement(image); - } - return image.getElement().getFirstChildElement(); + return SafeHtmlUtils.fromTrustedString(clippedImgHtml); } }
diff --git a/user/src/com/google/gwt/user/client/ui/impl/ClippedImagePrototype.java b/user/src/com/google/gwt/user/client/ui/impl/ClippedImagePrototype.java index 6bb5ab5..0b9b63c 100644 --- a/user/src/com/google/gwt/user/client/ui/impl/ClippedImagePrototype.java +++ b/user/src/com/google/gwt/user/client/ui/impl/ClippedImagePrototype.java
@@ -1,12 +1,12 @@ /* * Copyright 2008 Google Inc. - * + * * 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 @@ -16,6 +16,9 @@ package com.google.gwt.user.client.ui.impl; import com.google.gwt.core.client.GWT; +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeUri; +import com.google.gwt.safehtml.shared.UriUtils; import com.google.gwt.user.client.ui.AbstractImagePrototype; import com.google.gwt.user.client.ui.Image; @@ -31,11 +34,10 @@ private int height = 0; private int left = 0; private int top = 0; - private String url = null; + private SafeUri url = null; private int width = 0; - public ClippedImagePrototype(String url, int left, int top, int width, - int height) { + public ClippedImagePrototype(SafeUri url, int left, int top, int width, int height) { this.url = url; this.left = left; this.top = top; @@ -43,6 +45,11 @@ this.height = height; } + @Deprecated + public ClippedImagePrototype(String url, int left, int top, int width, int height) { + this(UriUtils.unsafeCastFromUntrustedString(url), left, top, width, height); + } + @Override public void applyTo(Image image) { image.setUrlAndVisibleRect(url, left, top, width, height); @@ -64,7 +71,7 @@ } @Override - public String getHTML() { - return impl.getHTML(url, left, top, width, height); + public SafeHtml getSafeHtml() { + return impl.getSafeHtml(url, left, top, width, height); } }
diff --git a/user/super/com/google/gwt/safehtml/super/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java b/user/super/com/google/gwt/safehtml/super/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java new file mode 100644 index 0000000..849ac5b --- /dev/null +++ b/user/super/com/google/gwt/safehtml/super/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java
@@ -0,0 +1,35 @@ +/* + * Copyright 2011 Google Inc. + * + * 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 com.google.gwt.safehtml.shared; + +import com.google.gwt.core.client.GwtScriptOnly; + +// This is the super-source peer of this class. +@GwtScriptOnly +public class SafeUriHostedModeUtils { + + // Unused in super-source; only defined to avoid compiler warnings + public static final String FORCE_CHECK_VALID_URI = null; + static final String HREF_UCSCHAR = null; + + public static void maybeCheckValidUri(String uri) { + // This check is a noop in web mode. + } + + // Unused in super-source; only defined to avoid compiler warnings + public static void setForceCheckValidUri(boolean check) { } + static void setForceCheckValidUriFromProperty() { } +}
diff --git a/user/test/com/google/gwt/resources/client/ImageResourceTest.java b/user/test/com/google/gwt/resources/client/ImageResourceTest.java index 07f1420..55141d6 100644 --- a/user/test/com/google/gwt/resources/client/ImageResourceTest.java +++ b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
@@ -116,7 +116,7 @@ assertEquals(0, a.getTop()); // Make sure the animated image is encoded separately - assertFalse(a.getURL().equals(r.i16x16().getURL())); + assertFalse(a.getSafeUri().equals(r.i16x16().getSafeUri())); } public void testDedup() { @@ -136,7 +136,7 @@ delayTestFinish(10000); // See if the size of the image strip is what we expect - Image i = new Image(a.getURL()); + Image i = new Image(a.getSafeUri()); i.addLoadHandler(new LoadHandler() { public void onLoad(LoadEvent event) { finishTest(); @@ -162,7 +162,7 @@ // Make sure that the large, lossy image isn't bundled with the rest assertTrue(((ImageResourcePrototype) lossy).isLossy()); - assertTrue(!i64.getURL().equals(lossy.getURL())); + assertTrue(!i64.getSafeUri().equals(lossy.getSafeUri())); assertEquals(16, r.i16x16Vertical().getWidth()); assertEquals(16, r.i16x16Vertical().getHeight()); @@ -194,4 +194,11 @@ assertEquals(0, b.getTop()); assertEquals(0, b.getLeft()); } + + @SuppressWarnings("deprecation") + public void testSafeUri() { + Resources r = GWT.create(Resources.class); + + assertEquals(r.i64x64().getSafeUri().asString(), r.i64x64().getURL()); + } }
diff --git a/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java b/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java index 3a7e0a3..9b83055 100644 --- a/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java +++ b/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java
@@ -1,12 +1,12 @@ /* * Copyright 2009 Google Inc. - * + * * 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 @@ -16,11 +16,13 @@ package com.google.gwt.safehtml.client; import com.google.gwt.core.client.GWT; +import com.google.gwt.junit.client.GWTTestCase; import com.google.gwt.safecss.shared.SafeStyles; import com.google.gwt.safecss.shared.SafeStylesUtils; -import com.google.gwt.safehtml.shared.SafeHtmlUtils; import com.google.gwt.safehtml.shared.SafeHtml; -import com.google.gwt.junit.client.GWTTestCase; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.google.gwt.safehtml.shared.SafeUri; +import com.google.gwt.safehtml.shared.UriUtils; import junit.framework.Assert; @@ -31,8 +33,8 @@ private static final String HTML_MARKUP = "woo <i>whee</i>"; - private static final String GOOD_URL_ESCAPED = - "http://foo.com/foo<bar>&baz=dootz"; + private static final String GOOD_URL_ESCAPED = "http://foo.com/foo<bar>&baz=dootz"; + private static final String GOOD_URL_ENCODED = "http://foo.com/foo%3Cbar%3E&baz=dootz"; private static final String GOOD_URL = "http://foo.com/foo<bar>&baz=dootz"; private static final String BAD_URL = "javascript:evil(1<2)"; private static final String BAD_URL_ESCAPED = "javascript:evil(1<2)"; @@ -54,11 +56,14 @@ */ public interface TestTemplates extends SafeHtmlTemplates { @Template("<span><b>{0}</b><span>{1}</span></span>") - SafeHtml simpleTemplate(String foo, SafeHtml bar); + SafeHtml simpleTemplate(String foo, SafeHtml bar); @Template("<span><a href=\"{0}\"><b>{1}</b></a></span>") SafeHtml templateWithUriAttribute(String url, SafeHtml html); + @Template("<span><a href=\"{0}\"><b>{1}</b></a></span>") + SafeHtml templateWithUriAttribute(SafeUri url, SafeHtml html); + @Template("<div id=\"{0}\">{1}</div>") SafeHtml templateWithRegularAttribute(String id, SafeHtml html); @@ -84,8 +89,9 @@ } public void testTemplateWithUriAttribute() { + // as String: sanitized by the template Assert.assertEquals( - "<span><a href=\"" + GOOD_URL_ESCAPED + "\"><b>" + HTML_MARKUP + "<span><a href=\"" + GOOD_URL_ENCODED + "\"><b>" + HTML_MARKUP + "</b></a></span>", templates.templateWithUriAttribute( GOOD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); @@ -93,14 +99,36 @@ "<span><a href=\"#\"><b>" + HTML_MARKUP + "</b></a></span>", templates.templateWithUriAttribute( BAD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); + // UriUtils.fromString: sanitized by fromString + Assert.assertEquals( + "<span><a href=\"" + GOOD_URL_ENCODED + "\"><b>" + HTML_MARKUP + + "</b></a></span>", + templates.templateWithUriAttribute( + UriUtils.fromString(GOOD_URL), SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); + Assert.assertEquals( + "<span><a href=\"#\"><b>" + HTML_MARKUP + "</b></a></span>", + templates.templateWithUriAttribute( + UriUtils.fromString(BAD_URL), SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); + // UriUtils.fromTrustedString: not sanitized + Assert.assertEquals( + "<span><a href=\"" + GOOD_URL_ESCAPED + "\"><b>" + HTML_MARKUP + + "</b></a></span>", + templates.templateWithUriAttribute( + UriUtils.fromTrustedString(GOOD_URL), + SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); + Assert.assertEquals( + "<span><a href=\"" + BAD_URL_ESCAPED + "\"><b>" + HTML_MARKUP + "</b></a></span>", + templates.templateWithUriAttribute( + UriUtils.fromTrustedString(BAD_URL), + SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); } - + public void testTemplateWithRegularAttribute() { Assert.assertEquals( "<div id=\"" + GOOD_URL_ESCAPED + "\">" + HTML_MARKUP + "</div>", templates.templateWithRegularAttribute( GOOD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); - + // Values inserted into non-URI-valued attributes should not be sanitized, // but still escaped. Assert.assertEquals( @@ -108,7 +136,7 @@ templates.templateWithRegularAttribute( BAD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString()); } - + public void testTemplateWithSafeStyleAttributeComplete() { Assert.assertEquals("<div style=\"width:10px;\">" + HTML_MARKUP + "</div>", templates.templateWithSafeStyleAttributeComplete( @@ -124,8 +152,9 @@ } public void testTemplateWithTwoPartUriAttribute() { + // sanitized by the template Assert.assertEquals( - "<span><img src=\"" + GOOD_URL_ESCAPED + "/x&y\"/></span>", + "<span><img src=\"" + GOOD_URL_ENCODED + "/x&y\"/></span>", templates.templateWithTwoPartUriAttribute( GOOD_URL, "x&y").asString()); Assert.assertEquals( @@ -133,7 +162,7 @@ templates.templateWithTwoPartUriAttribute( BAD_URL, "x&y").asString()); } - + public void testTemplateWithStyleAttribute() { Assert.assertEquals( "<span style='background: purple; color: green;'></span>",
diff --git a/user/test/com/google/gwt/safehtml/shared/GwtUriUtilsTest.java b/user/test/com/google/gwt/safehtml/shared/GwtUriUtilsTest.java new file mode 100644 index 0000000..f76c66f --- /dev/null +++ b/user/test/com/google/gwt/safehtml/shared/GwtUriUtilsTest.java
@@ -0,0 +1,141 @@ +/* + * Copyright 2011 Google Inc. + * + * 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 com.google.gwt.safehtml.shared; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.junit.client.GWTTestCase; + +/** + * Unit tests for {@link UriUtils}. + */ +public class GwtUriUtilsTest extends GWTTestCase { + + private static final String JAVASCRIPT_URL = "javascript:alert('BOOM!');"; + private static final String MAILTO_URL = "mailto:foo@example.com"; + private static final String CONSTANT_URL = + "http://gwt.google.com/samples/Showcase/Showcase.html?locale=fr#!CwCheckBox"; + private static final String EMPTY_GIF_DATA_URL = + "data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw=="; + + public void testEncode_noEscape() { + StringBuilder sb = new StringBuilder(UriUtils.DONT_NEED_ENCODING); + final int upcaseOffset = 'A' - 'a'; + for (char c = 'a'; c <= 'z'; c++) { + sb.append(c).append((char) (c + upcaseOffset)); + } + for (char c = '0'; c <= '9'; c++) { + sb.append(c); + } + final String expected = sb.toString(); + + assertEquals(expected, UriUtils.encode(expected)); + } + + public void testEncode_percent() { + assertEquals("foo%25bar", UriUtils.encode("foo%bar")); + } + + public void testEncode_percentAndOthers() { + assertEquals("fo%20o%25b%0Aa%22r", UriUtils.encode("fo o%b\na\"r")); + } + + public void testEncode_withEscapes1() { + assertEquals("foo%bar", UriUtils.encodeAllowEscapes("foo%bar")); + } + + public void testEncode_withEscapes2() { + assertEquals("foo%25bar", UriUtils.encodeAllowEscapes("foo%25bar")); + } + + public void testEncode_withEscapes3() { + assertEquals("foo%E2%82%ACbar", UriUtils.encodeAllowEscapes("foo\u20ACbar")); + } + + public void testEncode_withEscapes4() { + assertEquals("foo%E2%82%ACbar", UriUtils.encodeAllowEscapes("foo%E2%82%ACbar")); + } + + public void testEncode_withEscapesIncompleteEscapes() { + assertEquals("foob%25ar%25a", UriUtils.encodeAllowEscapes("foob%ar%a")); + } + + public void testEncode_withEscapesInvalidEscapes() { + assertEquals("f%25ooba%25r", UriUtils.encodeAllowEscapes("f%ooba%r")); + } + + public void testFromTrustedString() { + assertEquals(CONSTANT_URL, UriUtils.fromTrustedString(CONSTANT_URL).asString()); + assertEquals(MAILTO_URL, UriUtils.fromTrustedString(MAILTO_URL).asString()); + assertEquals(EMPTY_GIF_DATA_URL, UriUtils.fromTrustedString(EMPTY_GIF_DATA_URL).asString()); + assertEquals(JAVASCRIPT_URL, UriUtils.fromTrustedString(JAVASCRIPT_URL).asString()); + if (GWT.isClient()) { + assertEquals(GWT.getModuleBaseURL(), + UriUtils.fromTrustedString(GWT.getModuleBaseURL()).asString()); + assertEquals(GWT.getHostPageBaseURL(), + UriUtils.fromTrustedString(GWT.getHostPageBaseURL()).asString()); + } + } + + public void testFromTrustedString_withInvalidUrl() { + if (GWT.isProdMode()) { + // fromTrustedString does not parse/validate its argument in prod mode. + // Hence we short-circuit this test in prod mode. + return; + } + try { + SafeUri u = UriUtils.fromTrustedString("a\uD800b"); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @SuppressWarnings("deprecation") + public void testUnsafeCastFromUntrustedString() { + assertEquals(CONSTANT_URL, UriUtils.unsafeCastFromUntrustedString(CONSTANT_URL).asString()); + assertEquals(MAILTO_URL, UriUtils.unsafeCastFromUntrustedString(MAILTO_URL).asString()); + assertEquals(EMPTY_GIF_DATA_URL, UriUtils.unsafeCastFromUntrustedString(EMPTY_GIF_DATA_URL) + .asString()); + assertEquals(JAVASCRIPT_URL, UriUtils.unsafeCastFromUntrustedString(JAVASCRIPT_URL).asString()); + assertEquals("a\uD800b", UriUtils.unsafeCastFromUntrustedString("a\uD800b").asString()); + if (GWT.isClient()) { + assertEquals(GWT.getModuleBaseURL(), UriUtils.unsafeCastFromUntrustedString( + GWT.getModuleBaseURL()).asString()); + assertEquals(GWT.getHostPageBaseURL(), UriUtils.unsafeCastFromUntrustedString( + GWT.getHostPageBaseURL()).asString()); + } + } + + public void testFromString() { + assertEquals(CONSTANT_URL, UriUtils.fromString(CONSTANT_URL).asString()); + assertEquals(MAILTO_URL, UriUtils.fromString(MAILTO_URL).asString()); + assertEquals(UriUtils.sanitizeUri(EMPTY_GIF_DATA_URL), + UriUtils.fromString(EMPTY_GIF_DATA_URL).asString()); + assertEquals(UriUtils.sanitizeUri(JAVASCRIPT_URL), + UriUtils.fromString(JAVASCRIPT_URL).asString()); + if (GWT.isClient()) { + assertEquals(GWT.getModuleBaseURL(), + UriUtils.fromString(GWT.getModuleBaseURL()).asString()); + assertEquals(GWT.getHostPageBaseURL(), + UriUtils.fromString(GWT.getHostPageBaseURL()).asString()); + } + } + + @Override + public String getModuleName() { + return "com.google.gwt.safehtml.SafeHtmlTestsModule"; + } +}
diff --git a/user/test/com/google/gwt/safehtml/shared/UriUtilsTest.java b/user/test/com/google/gwt/safehtml/shared/UriUtilsTest.java new file mode 100644 index 0000000..71d3746 --- /dev/null +++ b/user/test/com/google/gwt/safehtml/shared/UriUtilsTest.java
@@ -0,0 +1,37 @@ +/* + * Copyright 2011 Google Inc. + * + * 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 com.google.gwt.safehtml.shared; + +/** + * Unit tests for {@link UriUtils}. + */ +public class UriUtilsTest extends GwtUriUtilsTest { + + // This forces a GWTTestCase to run as a vanilla JUnit TestCase. + @Override + public String getModuleName() { + return null; + } + + @Override + protected void gwtSetUp() throws Exception { + super.gwtSetUp(); + // Since we can't assume assertions are enabled, force + // UriUtilsImpl#maybeCheckValidUri to perform its check + // when running in JRE. + SafeUriHostedModeUtils.setForceCheckValidUri(true); + } +}
diff --git a/user/test/com/google/gwt/user/client/ui/ImageTest.java b/user/test/com/google/gwt/user/client/ui/ImageTest.java index 0827501..5fa9bf1 100644 --- a/user/test/com/google/gwt/user/client/ui/ImageTest.java +++ b/user/test/com/google/gwt/user/client/ui/ImageTest.java
@@ -782,7 +782,7 @@ } private void assertResourceWorked(Image image, ImageResource prettyPiccy) { - assertEquals(prettyPiccy.getURL(), image.getUrl()); + assertEquals(prettyPiccy.getSafeUri().asString(), image.getUrl()); assertEquals(prettyPiccy.getTop(), image.getOriginTop()); assertEquals(prettyPiccy.getHeight(), image.getHeight()); assertEquals(prettyPiccy.getLeft(), image.getOriginLeft());
diff --git a/user/test/com/google/gwt/user/client/ui/impl/ClippedImagePrototypeTest.java b/user/test/com/google/gwt/user/client/ui/impl/ClippedImagePrototypeTest.java index c9c54ba..15cadeb 100644 --- a/user/test/com/google/gwt/user/client/ui/impl/ClippedImagePrototypeTest.java +++ b/user/test/com/google/gwt/user/client/ui/impl/ClippedImagePrototypeTest.java
@@ -1,12 +1,12 @@ /* * Copyright 2007 Google Inc. - * + * * 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 @@ -22,12 +22,11 @@ import com.google.gwt.junit.DoNotRunWith; import com.google.gwt.junit.Platform; import com.google.gwt.junit.client.GWTTestCase; +import com.google.gwt.safehtml.shared.UriUtils; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.ImageTest; -import com.google.gwt.user.client.ui.LoadListener; import com.google.gwt.user.client.ui.RootPanel; -import com.google.gwt.user.client.ui.Widget; /** * Tests for the ClippedImagePrototype implementation. Tests are done to ensure @@ -38,28 +37,6 @@ * application of the prototype to the image. */ public class ClippedImagePrototypeTest extends GWTTestCase { - @Deprecated - private static class TestLoadListener implements LoadListener { - private int onloadEventFireCount = 0; - private Image image; - - public TestLoadListener(Image image) { - this.image = image; - } - - public void onError(Widget sender) { - fail("The image " + image.getUrl() + " failed to load."); - } - - public int getOnloadEventFireCount() { - return onloadEventFireCount; - } - - public void onLoad(Widget sender) { - onloadEventFireCount++; - } - } - private static class TestLoadHandler implements LoadHandler { private int onloadEventFireCount = 0; @@ -71,18 +48,18 @@ onloadEventFireCount++; } } - + @Override public String getModuleName() { return "com.google.gwt.user.UserTest"; } - + /** * Tests that a clipped image can be transformed to match a given prototype. * Also checks to make sure that a load event is fired on when * {@link com.google.gwt.user.client.ui.impl.ClippedImagePrototype#applyTo(com.google.gwt.user.client.ui.Image)} * is called. - * + * * TODO(jlabanca): Enable this test when issue 863 is fixed */ @DoNotRunWith({Platform.HtmlUnitBug}) @@ -102,7 +79,7 @@ if (image.getOriginLeft() == 12 && image.getOriginTop() == 13) { ClippedImagePrototype clippedImagePrototype = new ClippedImagePrototype( - "counting-forwards.png", 16, 16, 16, 16); + UriUtils.fromString("counting-forwards.png"), 16, 16, 16, 16); clippedImagePrototype.applyTo(image); @@ -141,43 +118,43 @@ * <code>applyTo(Image)</code> is called. */ /* - * This test has been commented out because of issue #863 - * - * public void testApplyToUnclippedImage() { + * This test has been commented out because of issue #863 + * + * public void testApplyToUnclippedImage() { * final Image image = new Image("counting-backwards.png"); - * + * * assertEquals(0, image.getOriginLeft()); assertEquals(0, * image.getOriginTop()); assertEquals("unclipped", * ImageTest.getCurrentImageStateName(image)); - * + * * final ArrayList onloadEventFireCounter = new ArrayList(); - * + * * image.addLoadListener(new LoadListener() { public void onError(Widget * sender) { fail("The image " + ((Image) sender).getUrl() + " failed to * load."); } - * + * * public void onLoad(Widget sender) { onloadEventFireCounter.add(new * Object()); - * + * * if (ImageTest.getCurrentImageStateName(image).equals("unclipped")) { - * + * * assertEquals(32, image.getWidth()); assertEquals(32, image.getHeight()); - * + * * ClippedImagePrototype clippedImagePrototype = new * ClippedImagePrototype("counting-forwards.png", 16, 16, 16, 16); - * + * * clippedImagePrototype.adjust(image); - * + * * assertEquals(16, image.getOriginLeft()); assertEquals(16, * image.getOriginTop()); assertEquals(16, image.getWidth()); assertEquals(16, * image.getHeight()); assertEquals("counting-forwards.png", image.getUrl()); * assertEquals("clipped", ImageTest.getCurrentImageStateName(image)); } } }); - * + * * RootPanel.get().add(image); delayTestFinish(2000); - * + * * Timer t = new Timer() { public void run() { assertEquals(2, * onloadEventFireCounter.size()); finishTest(); } }; - * + * * t.schedule(1000); } */ @@ -187,7 +164,7 @@ */ public void testGenerateNewImage() { ClippedImagePrototype clippedImagePrototype = new ClippedImagePrototype( - "counting-forwards.png", 16, 16, 16, 16); + UriUtils.fromString("counting-forwards.png"), 16, 16, 16, 16); Image image = clippedImagePrototype.createImage();