From 5ad8639bc62f3072fe186ce137709fda750ab17e Mon Sep 17 00:00:00 2001 From: MoyuScript Date: Mon, 4 Sep 2017 23:36:19 +0800 Subject: [PATCH] Add support for loading cross origin images using proxy --- karma.js | 6 ++- package.json | 1 + src/Feature.js | 20 ++++++-- src/ImageLoader.js | 24 ++++++++-- src/Proxy.js | 62 +++++++++++++++++++++++++ tests/reftests/images/base.html | 6 +-- tests/reftests/images/cross-origin.html | 2 +- tests/server.js | 6 ++- tests/test.js | 11 ++++- tests/testrunner.js | 1 + 10 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 src/Proxy.js diff --git a/karma.js b/karma.js index 7cfb47d..3595b0c 100644 --- a/karma.js +++ b/karma.js @@ -1,6 +1,7 @@ const Server = require('karma').Server; const cfg = require('karma').config; const path = require('path'); +const proxy = require('html2canvas-proxy'); const karmaConfig = cfg.parseConfig(path.resolve('./karma.conf.js')); const server = new Server(karmaConfig, (exitCode) => { console.log('Karma has exited with ' + exitCode); @@ -15,8 +16,9 @@ const filenamifyUrl = require('filenamify-url'); const CORS_PORT = 8081; const corsApp = express(); -corsApp.use(cors()); -corsApp.use('/', express.static(path.resolve(__dirname))); +corsApp.use('/proxy', proxy()); +corsApp.use('/cors', cors(), express.static(path.resolve(__dirname))); +corsApp.use('/', express.static(path.resolve(__dirname, '/tests'))); corsApp.use((error, req, res, next) => { console.error(error); next(); diff --git a/package.json b/package.json index 9e08dc2..96383f1 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "filenamify-url": "1.0.0", "flow-bin": "0.50.0", "glob": "7.1.2", + "html2canvas-proxy": "1.0.0", "jquery": "3.2.1", "karma": "1.7.0", "karma-chrome-launcher": "2.2.0", diff --git a/src/Feature.js b/src/Feature.js index 44002c0..a0117eb 100644 --- a/src/Feature.js +++ b/src/Feature.js @@ -59,9 +59,9 @@ const testBase64 = (document: Document, src: string): Promise => { }); }; -const testCORS = () => { - return typeof new Image().crossOrigin !== 'undefined'; -}; +const testCORS = () => typeof new Image().crossOrigin !== 'undefined'; + +const testResponseType = () => typeof new XMLHttpRequest().responseType === 'string'; const testSVG = document => { const img = new Image(); @@ -156,6 +156,20 @@ const FEATURES = { const value = testCORS(); Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value}); return value; + }, + // $FlowFixMe - get/set properties not yet supported + get SUPPORT_RESPONSE_TYPE() { + 'use strict'; + const value = testResponseType(); + Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', {value}); + return value; + }, + // $FlowFixMe - get/set properties not yet supported + get SUPPORT_CORS_XHR() { + 'use strict'; + const value = 'withCredentials' in new XMLHttpRequest(); + Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value}); + return value; } }; diff --git a/src/ImageLoader.js b/src/ImageLoader.js index 016676d..a53fa12 100644 --- a/src/ImageLoader.js +++ b/src/ImageLoader.js @@ -8,6 +8,7 @@ export type ImageElement = Image | HTMLCanvasElement; type ImageCache = {[string]: Promise}; import FEATURES from './Feature'; +import {Proxy} from './Proxy'; // $FlowFixMe export default class ImageLoader { @@ -46,7 +47,10 @@ export default class ImageLoader { return this.addImage(src, src, false); } else if (!this.isSameOrigin(src)) { if (typeof this.options.proxy === 'string') { - // TODO proxy + this.cache[src] = Proxy(src, this.options).then(src => + loadImage(src, this.options.imageTimeout || 0) + ); + return src; } else if (this.options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES) { return this.addImage(src, src, true); } @@ -61,7 +65,12 @@ export default class ImageLoader { if (this.hasImageInCache(src)) { return this.cache[src]; } - // TODO proxy + if (!this.isSameOrigin(src) && typeof this.options.proxy === 'string') { + return (this.cache[src] = Proxy(src, this.options).then(src => + loadImage(src, this.options.imageTimeout || 0) + )); + } + return this.xhrImage(src); } @@ -71,7 +80,12 @@ export default class ImageLoader { xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status !== 200) { - reject(`Failed to fetch image ${src} with status code ${xhr.status}`); + reject( + `Failed to fetch image ${src.substring( + 0, + 256 + )} with status code ${xhr.status}` + ); } else { const reader = new FileReader(); // $FlowFixMe @@ -87,7 +101,9 @@ export default class ImageLoader { const timeout = this.options.imageTimeout; xhr.timeout = timeout; xhr.ontimeout = () => - reject(__DEV__ ? `Timed out (${timeout}ms) fetching ${src}` : ''); + reject( + __DEV__ ? `Timed out (${timeout}ms) fetching ${src.substring(0, 256)}` : '' + ); } xhr.open('GET', src, true); xhr.send(); diff --git a/src/Proxy.js b/src/Proxy.js new file mode 100644 index 0000000..9d3572b --- /dev/null +++ b/src/Proxy.js @@ -0,0 +1,62 @@ +/* @flow */ +'use strict'; + +import type Options from './index'; + +import FEATURES from './Feature'; + +export const Proxy = (src: string, options: Options): Promise => { + if (!options.proxy) { + return Promise.reject(__DEV__ ? 'No proxy defined' : null); + } + const proxy = options.proxy; + + return new Promise((resolve, reject) => { + const responseType = + FEATURES.SUPPORT_CORS_XHR && FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text'; + const xhr = FEATURES.SUPPORT_CORS_XHR ? new XMLHttpRequest() : new XDomainRequest(); + xhr.onload = () => { + if (xhr instanceof XMLHttpRequest) { + if (xhr.status === 200) { + if (responseType === 'text') { + resolve(xhr.response); + } else { + const reader = new FileReader(); + // $FlowFixMe + reader.addEventListener('load', () => resolve(reader.result), false); + // $FlowFixMe + reader.addEventListener('error', e => reject(e), false); + reader.readAsDataURL(xhr.response); + } + } else { + reject( + __DEV__ + ? `Failed to proxy image ${src.substring( + 0, + 256 + )} with status code ${xhr.status}` + : '' + ); + } + } else { + resolve(xhr.responseText); + } + }; + + xhr.onerror = reject; + xhr.open('GET', `${proxy}?url=${encodeURIComponent(src)}&responseType=${responseType}`); + + if (responseType !== 'text' && xhr instanceof XMLHttpRequest) { + xhr.responseType = responseType; + } + + if (options.imageTimeout) { + const timeout = options.imageTimeout; + xhr.timeout = timeout; + xhr.ontimeout = () => + reject(__DEV__ ? `Timed out (${timeout}ms) proxying ${src.substring(0, 256)}` : ''); + } + + xhr.send(); + }); +}; diff --git a/tests/reftests/images/base.html b/tests/reftests/images/base.html index 98dd0a1..02b080c 100644 --- a/tests/reftests/images/base.html +++ b/tests/reftests/images/base.html @@ -9,13 +9,13 @@ font-family: Arial; } - +

External image

- +

External image (using <base> href)

- + diff --git a/tests/reftests/images/cross-origin.html b/tests/reftests/images/cross-origin.html index 23f3ca0..4ae8f2b 100644 --- a/tests/reftests/images/cross-origin.html +++ b/tests/reftests/images/cross-origin.html @@ -15,6 +15,6 @@

External image (CORS)

- + diff --git a/tests/server.js b/tests/server.js index 52d0fcb..654a461 100644 --- a/tests/server.js +++ b/tests/server.js @@ -5,6 +5,7 @@ const fs = require('fs'); const webpack = require('webpack'); const config = require('../webpack.config'); const serveIndex = require('serve-index'); +const proxy = require('html2canvas-proxy'); const PORT = 8080; const CORS_PORT = 8081; @@ -17,8 +18,9 @@ app.listen(PORT, () => { }); const corsApp = express(); -corsApp.use(cors()); -corsApp.use('/', express.static(path.resolve(__dirname, '../'))); +corsApp.use('/proxy', proxy()); +corsApp.use('/cors', cors(), express.static(path.resolve(__dirname, '../'))); +corsApp.use('/', express.static(path.resolve(__dirname, '.'))); corsApp.listen(CORS_PORT, () => { console.log(`CORS server running on port ${CORS_PORT}`); }); diff --git a/tests/test.js b/tests/test.js index 18b8dfb..f2d49ef 100644 --- a/tests/test.js +++ b/tests/test.js @@ -6,7 +6,14 @@ var REFTEST = window.location.search.indexOf('reftest') !== -1; (function(document, window) { function appendScript(src) { document.write( - '' + '' ); } @@ -147,7 +154,7 @@ var REFTEST = window.location.search.indexOf('reftest') !== -1; $.extend( { logging: true, - proxy: 'http://localhost:8082', + proxy: 'http://localhost:8081/proxy', useCORS: false, removeContainer: false, target: targets diff --git a/tests/testrunner.js b/tests/testrunner.js index 4037289..bfe6f0e 100644 --- a/tests/testrunner.js +++ b/tests/testrunner.js @@ -118,6 +118,7 @@ const assertPath = (result, expected, desc) => { .html2canvas(testContainer.contentWindow.document.documentElement, { removeContainer: true, backgroundColor: '#ffffff', + proxy: 'http://localhost:8081/proxy', ...(testContainer.contentWindow.h2cOptions || {}) }) .then(canvas => {