diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/SVGDrawer.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/SVGDrawer.java index 187f7e2b8..db4da5f6d 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/SVGDrawer.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/SVGDrawer.java @@ -12,18 +12,20 @@ import com.openhtmltopdf.render.RenderingContext; public interface SVGDrawer extends Closeable { - public void importFontFaceRules(List fontFaces, + void importFontFaceRules(List fontFaces, SharedContext shared); - public SVGImage buildSVGImage(Element svgElement, Box box, CssContext cssContext, double cssWidth, + SVGImage buildSVGImage(Element svgElement, Box box, CssContext cssContext, double cssWidth, double cssHeight, double dotsPerPixel); - public static interface SVGImage { - public int getIntrinsicWidth(); + default void withUserAgent(UserAgentCallback userAgentCallback) {} - public int getIntrinsicHeight(); + interface SVGImage { + int getIntrinsicWidth(); - public void drawSVG(OutputDevice outputDevice, RenderingContext ctx, + int getIntrinsicHeight(); + + void drawSVG(OutputDevice outputDevice, RenderingContext ctx, double x, double y); } } diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/svg-custom-protocol.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/svg-custom-protocol.pdf new file mode 100644 index 000000000..142ad2798 Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/svg-custom-protocol.pdf differ diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/svg-external-file-load-blocked.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/svg-external-file-load-blocked.pdf new file mode 100644 index 000000000..0b3d6edd9 Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/svg-external-file-load-blocked.pdf differ diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/svg-external-file-whitelist-file-protocol.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/svg-external-file-whitelist-file-protocol.pdf new file mode 100644 index 000000000..364f59930 Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/svg-external-file-whitelist-file-protocol.pdf differ diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/solid.svg b/openhtmltopdf-examples/src/main/resources/visualtest/html/solid.svg new file mode 100644 index 000000000..4749541bb --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/solid.svg @@ -0,0 +1,3 @@ + + + diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/svg-custom-protocol.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/svg-custom-protocol.html new file mode 100644 index 000000000..15a0df61b --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/svg-custom-protocol.html @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/svg-external-file-load-blocked.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/svg-external-file-load-blocked.html new file mode 100644 index 000000000..4800bf78e --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/svg-external-file-load-blocked.html @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/svg-external-file-whitelist-file-protocol.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/svg-external-file-whitelist-file-protocol.html new file mode 100644 index 000000000..4800bf78e --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/svg-external-file-whitelist-file-protocol.html @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java index 65c04c110..1bbc7745e 100644 --- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java +++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java @@ -1,8 +1,13 @@ package com.openhtmltopdf.visualregressiontests; -import java.io.File; +import java.io.*; + import static org.junit.Assert.assertTrue; -import java.io.IOException; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +import com.openhtmltopdf.extend.FSStream; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -1118,6 +1123,37 @@ public void testIssue484ImgSrcDataImageSvgBase64() throws IOException { assertTrue(vt.runTest("issue-484-data-image-svg-xml-base64", TestSupport.WITH_SVG)); } + + @Test + public void testSVGLoadBlocked() throws IOException { + assertTrue(vt.runTest("svg-external-file-load-blocked", TestSupport.WITH_SVG)); + } + + @Test + public void testSVGLoadWhiteListFileProtocol() throws IOException { + assertTrue(vt.runTest("svg-external-file-whitelist-file-protocol", + (builder) -> builder.useSVGDrawer(new BatikSVGDrawer(SvgScriptMode.SECURE, Collections.singleton("file"))))); + } + + @Test + public void testSVGLoadCustomProtocol() throws IOException { + assertTrue(vt.runTest("svg-custom-protocol", (builder -> { + builder.useProtocolsStreamImplementation(url -> new FSStream() { + + @Override + public InputStream getStream() { + return new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)); + } + + @Override + public Reader getReader() { + return new InputStreamReader(getStream(), StandardCharsets.UTF_8); + } + }, "custom"); + builder.useSVGDrawer(new BatikSVGDrawer(SvgScriptMode.SECURE, Collections.singleton("custom"))); + }))); + } + // TODO: // + Elements that appear just on generated overflow pages. // + content property (page counters, etc) diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxRenderer.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxRenderer.java index 57c798811..631d51a49 100644 --- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxRenderer.java +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxRenderer.java @@ -163,6 +163,10 @@ public class PdfBoxRenderer implements Closeable, PageSupplier { PdfBoxUserAgent userAgent = new PdfBoxUserAgent(_outputDevice); + if (_svgImpl != null) { + _svgImpl.withUserAgent(userAgent); + } + userAgent.setProtocolsStreamFactory(state._streamFactoryMap); if (state._resolver != null) { diff --git a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGDrawer.java b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGDrawer.java index 14e67c0bc..9fe79f3f8 100644 --- a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGDrawer.java +++ b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGDrawer.java @@ -1,8 +1,11 @@ package com.openhtmltopdf.svgsupport; import java.io.IOException; +import java.util.Collections; import java.util.List; +import java.util.Set; +import com.openhtmltopdf.extend.UserAgentCallback; import org.w3c.dom.Element; import com.openhtmltopdf.css.sheet.FontFaceRule; @@ -14,16 +17,18 @@ import com.openhtmltopdf.svgsupport.PDFTranscoder.OpenHtmlFontResolver; public class BatikSVGDrawer implements SVGDrawer { + private final Set allowedProtocols; public OpenHtmlFontResolver fontResolver; private final boolean allowScripts; private final boolean allowExternalResources; + private UserAgentCallback userAgentCallback; - public static enum SvgScriptMode { + public enum SvgScriptMode { SECURE, INSECURE_ALLOW_SCRIPTS; } - public static enum SvgExternalResourceMode { + public enum SvgExternalResourceMode { SECURE, INSECURE_ALLOW_EXTERNAL_RESOURCE_REQUESTS; } @@ -40,6 +45,20 @@ public static enum SvgExternalResourceMode { public BatikSVGDrawer(SvgScriptMode scriptMode, SvgExternalResourceMode externalResourceMode) { this.allowScripts = scriptMode == SvgScriptMode.INSECURE_ALLOW_SCRIPTS; this.allowExternalResources = externalResourceMode == SvgExternalResourceMode.INSECURE_ALLOW_EXTERNAL_RESOURCE_REQUESTS; + this.allowedProtocols = null; + } + + /** + * Creates a SVGDrawer that can allow arbitary scripts to run and allow the loading of + * external resources with the specified protocols. + * + * @param scriptMode + * @param allowedProtocols + */ + public BatikSVGDrawer(SvgScriptMode scriptMode, Set allowedProtocols) { + this.allowScripts = scriptMode == SvgScriptMode.INSECURE_ALLOW_SCRIPTS; + this.allowExternalResources = false; + this.allowedProtocols = Collections.unmodifiableSet(allowedProtocols); } /** @@ -59,6 +78,11 @@ public void importFontFaceRules(List fontFaces, this.fontResolver.importFontFaces(fontFaces, shared); } + @Override + public void withUserAgent(UserAgentCallback userAgentCallback) { + this.userAgentCallback = userAgentCallback; + } + @Override public SVGImage buildSVGImage(Element svgElement, Box box, CssContext c, double cssWidth, double cssHeight, double dotsPerPixel) { @@ -69,7 +93,8 @@ public SVGImage buildSVGImage(Element svgElement, Box box, CssContext c, BatikSVGImage img = new BatikSVGImage(svgElement, box, cssWidth, cssHeight, cssMaxWidth, cssMaxHeight, dotsPerPixel); img.setFontResolver(fontResolver); - img.setSecurityOptions(allowScripts, allowExternalResources); + img.setUserAgentCallback(userAgentCallback); + img.setSecurityOptions(allowScripts, allowExternalResources, allowedProtocols); return img; } diff --git a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java index cab3cf8cb..e2b7a215f 100644 --- a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java +++ b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/BatikSVGImage.java @@ -1,8 +1,10 @@ package com.openhtmltopdf.svgsupport; import java.awt.Point; +import java.util.Set; import java.util.logging.Level; +import com.openhtmltopdf.extend.UserAgentCallback; import org.apache.batik.anim.dom.SVGDOMImplementation; import org.apache.batik.transcoder.SVGAbstractTranscoder; import org.apache.batik.transcoder.TranscoderException; @@ -28,6 +30,7 @@ public class BatikSVGImage implements SVGImage { private final double dotsPerPixel; private OpenHtmlFontResolver fontResolver; private final PDFTranscoder pdfTranscoder; + private UserAgentCallback userAgentCallback; public BatikSVGImage(Element svgElement, Box box, double cssWidth, double cssHeight, double cssMaxWidth, double cssMaxHeight, double dotsPerPixel) { @@ -98,10 +101,14 @@ public void setFontResolver(OpenHtmlFontResolver fontResolver) { this.fontResolver = fontResolver; } - public void setSecurityOptions(boolean allowScripts, boolean allowExternalResources) { - this.pdfTranscoder.setSecurityOptions(allowScripts, allowExternalResources); + public void setSecurityOptions(boolean allowScripts, boolean allowExternalResources, Set allowedProtocols) { + this.pdfTranscoder.setSecurityOptions(allowScripts, allowExternalResources, allowedProtocols); this.pdfTranscoder.addTranscodingHint(SVGAbstractTranscoder.KEY_EXECUTE_ONLOAD, allowScripts); } + + public void setUserAgentCallback(UserAgentCallback userAgentCallback) { + this.userAgentCallback = userAgentCallback; + } public Integer parseLength(String attrValue) { // TODO read length with units and convert to dots. @@ -159,7 +166,7 @@ public void drawSVG(OutputDevice outputDevice, RenderingContext ctx, } pdfTranscoder.setRenderingParameters(outputDevice, ctx, x, y, - fontResolver); + fontResolver, userAgentCallback); try { DOMImplementation impl = SVGDOMImplementation diff --git a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/OpenHtmlDocumentLoader.java b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/OpenHtmlDocumentLoader.java new file mode 100644 index 000000000..2381582f5 --- /dev/null +++ b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/OpenHtmlDocumentLoader.java @@ -0,0 +1,40 @@ +package com.openhtmltopdf.svgsupport; + +import com.openhtmltopdf.extend.UserAgentCallback; +import com.openhtmltopdf.util.XRLog; +import org.apache.batik.bridge.DocumentLoader; +import org.apache.batik.bridge.UserAgent; +import org.w3c.dom.Document; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +public class OpenHtmlDocumentLoader extends DocumentLoader { + + private final UserAgentCallback userAgentCallback; + + public OpenHtmlDocumentLoader(UserAgent userAgent, UserAgentCallback userAgentCallback) { + super(userAgent); + this.userAgentCallback = userAgentCallback; + } + + + @Override + public Document loadDocument(String uri) throws IOException { + try { + // special handling of relative uri in case of file protocol, we receive something like "file:file.svg" + // The path will be null, but the scheme specific part will be not null + URI parsedURI = new URI(uri); + if ("file".equals(parsedURI.getScheme()) && parsedURI.getPath() == null && parsedURI.getSchemeSpecificPart() != null) { + uri = userAgentCallback.resolveURI(parsedURI.getSchemeSpecificPart()); + } else if (!parsedURI.isAbsolute()) { + uri = userAgentCallback.resolveURI(uri); + } + } catch (URISyntaxException uriSyntaxException) { + XRLog.exception("URI syntax exception while loading external svg resource: " + uri, uriSyntaxException); + } + return super.loadDocument(uri, new ByteArrayInputStream(userAgentCallback.getBinaryResource(uri))); + } +} diff --git a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/OpenHtmlUserAgent.java b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/OpenHtmlUserAgent.java index ee619c6a5..fe78470a3 100644 --- a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/OpenHtmlUserAgent.java +++ b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/OpenHtmlUserAgent.java @@ -7,16 +7,20 @@ import com.openhtmltopdf.svgsupport.PDFTranscoder.OpenHtmlFontResolver; import com.openhtmltopdf.util.XRLog; +import java.util.Set; + public class OpenHtmlUserAgent extends UserAgentAdapter { private final OpenHtmlFontResolver resolver; private final boolean allowScripts; private final boolean allowExternalResources; + private final Set allowedProtocols; - public OpenHtmlUserAgent(OpenHtmlFontResolver resolver, boolean allowScripts, boolean allowExternalResources) { + public OpenHtmlUserAgent(OpenHtmlFontResolver resolver, boolean allowScripts, boolean allowExternalResources, Set allowedProtocols) { this.resolver = resolver; this.allowScripts = allowScripts; this.allowExternalResources = allowExternalResources; + this.allowedProtocols = allowedProtocols; } @Override @@ -34,7 +38,7 @@ public void checkLoadScript(String scriptType, ParsedURL scriptURL, ParsedURL do @Override public void checkLoadExternalResource(ParsedURL resourceURL, ParsedURL docURL) throws SecurityException { - if (!this.allowExternalResources) { + if (!this.allowExternalResources && (allowedProtocols == null || !allowedProtocols.contains(resourceURL.getProtocol()))) { XRLog.exception("Tried to fetch external resource from SVG. Refusing. Details: " + resourceURL + ", " + docURL); throw new SecurityException("Tried to fetch external resource (" + resourceURL + ") from SVG. Refused!"); } diff --git a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/PDFTranscoder.java b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/PDFTranscoder.java index 3a8ef5939..c83d54c50 100644 --- a/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/PDFTranscoder.java +++ b/openhtmltopdf-svg-support/src/main/java/com/openhtmltopdf/svgsupport/PDFTranscoder.java @@ -7,13 +7,16 @@ import com.openhtmltopdf.css.style.FSDerivedValue; import com.openhtmltopdf.extend.OutputDevice; import com.openhtmltopdf.extend.OutputDeviceGraphicsDrawer; +import com.openhtmltopdf.extend.UserAgentCallback; import com.openhtmltopdf.layout.SharedContext; import com.openhtmltopdf.render.Box; import com.openhtmltopdf.render.RenderingContext; import com.openhtmltopdf.simple.extend.ReplacedElementScaleHelper; import com.openhtmltopdf.util.XRLog; +import org.apache.batik.bridge.BridgeContext; import org.apache.batik.bridge.FontFace; import org.apache.batik.bridge.FontFamilyResolver; +import org.apache.batik.bridge.svg12.SVG12BridgeContext; import org.apache.batik.gvt.font.GVTFontFamily; import org.apache.batik.transcoder.ErrorHandler; import org.apache.batik.transcoder.SVGAbstractTranscoder; @@ -28,6 +31,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; public class PDFTranscoder extends SVGAbstractTranscoder { private OpenHtmlFontResolver fontResolver; @@ -39,6 +43,8 @@ public class PDFTranscoder extends SVGAbstractTranscoder { private final double dotsPerPixel; private boolean allowScripts = false; private boolean allowExternalResources = false; + private UserAgentCallback userAgentCallback; + private Set allowedProtocols; public PDFTranscoder(Box box, double dotsPerPixel, double width, double height) { this.box = box; @@ -47,12 +53,13 @@ public PDFTranscoder(Box box, double dotsPerPixel, double width, double height) this.dotsPerPixel = dotsPerPixel; } - public void setRenderingParameters(OutputDevice od, RenderingContext ctx, double x, double y, OpenHtmlFontResolver fontResolver) { + public void setRenderingParameters(OutputDevice od, RenderingContext ctx, double x, double y, OpenHtmlFontResolver fontResolver, UserAgentCallback userAgentCallback) { this.x = x; this.y = y; this.outputDevice = od; this.ctx = ctx; this.fontResolver = fontResolver; + this.userAgentCallback = userAgentCallback; } @Override @@ -201,18 +208,28 @@ public void importFontFaces(List fontFaces, SharedContext ctx) { } } } - - public void setSecurityOptions(boolean allowScripts, boolean allowExternalResources) { + + public void setSecurityOptions(boolean allowScripts, boolean allowExternalResources, Set allowedProtocols) { this.allowScripts = allowScripts; this.allowExternalResources = allowExternalResources; + this.allowedProtocols = allowedProtocols; } - + + @Override + protected BridgeContext createBridgeContext(String svgVersion) { + if ("1.2".equals(svgVersion)) { + return new SVG12BridgeContext(userAgent, new OpenHtmlDocumentLoader(userAgent, userAgentCallback)); + } else { + return new BridgeContext(userAgent, new OpenHtmlDocumentLoader(userAgent, userAgentCallback)); + } + } + @Override protected void transcode(Document svg, String uri, TranscoderOutput out) throws TranscoderException { // Note: We have to initialize user agent here and not in ::createUserAgent() as method // is called before our constructor is called in the super constructor. - this.userAgent = new OpenHtmlUserAgent(this.fontResolver, this.allowScripts, this.allowExternalResources); + this.userAgent = new OpenHtmlUserAgent(this.fontResolver, this.allowScripts, this.allowExternalResources, this.allowedProtocols); super.transcode(svg, uri, out); Rectangle contentBounds = box.getContentAreaEdge(box.getAbsX(), box.getAbsY(), ctx);