diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 511740513..faaeedd9d 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -15,7 +15,7 @@ import { assert, CMapCompressionType, removeNullCharacters, stringToBytes, - unreachable, Util, warn + unreachable, URL, Util, warn } from '../shared/util'; const DEFAULT_LINK_REL = 'noopener noreferrer nofollow'; @@ -66,19 +66,41 @@ class DOMCMapReaderFactory { this.isCompressed = isCompressed; } - fetch({ name, }) { + async fetch({ name, }) { if (!this.baseUrl) { - return Promise.reject(new Error( + throw new Error( 'The CMap "baseUrl" parameter must be specified, ensure that ' + - 'the "cMapUrl" and "cMapPacked" API parameters are provided.')); + 'the "cMapUrl" and "cMapPacked" API parameters are provided.'); } if (!name) { - return Promise.reject(new Error('CMap name must be specified.')); + throw new Error('CMap name must be specified.'); } - return new Promise((resolve, reject) => { - let url = this.baseUrl + name + (this.isCompressed ? '.bcmap' : ''); + const url = this.baseUrl + name + (this.isCompressed ? '.bcmap' : ''); + const compressionType = (this.isCompressed ? CMapCompressionType.BINARY : + CMapCompressionType.NONE); - let request = new XMLHttpRequest(); + if ((typeof PDFJSDev !== 'undefined' && PDFJSDev.test('MOZCENTRAL')) || + (isFetchSupported() && isValidFetchUrl(url, document.baseURI))) { + return fetch(url).then(async (response) => { + if (!response.ok) { + throw new Error(response.statusText); + } + let cMapData; + if (this.isCompressed) { + cMapData = new Uint8Array(await response.arrayBuffer()); + } else { + cMapData = stringToBytes(await response.text()); + } + return { cMapData, compressionType, }; + }).catch((reason) => { + throw new Error(`Unable to load ${this.isCompressed ? 'binary ' : ''}` + + `CMap at: ${url}`); + }); + } + + // The Fetch API is not supported. + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest(); request.open('GET', url, true); if (this.isCompressed) { @@ -89,27 +111,24 @@ class DOMCMapReaderFactory { return; } if (request.status === 200 || request.status === 0) { - let data; + let cMapData; if (this.isCompressed && request.response) { - data = new Uint8Array(request.response); + cMapData = new Uint8Array(request.response); } else if (!this.isCompressed && request.responseText) { - data = stringToBytes(request.responseText); + cMapData = stringToBytes(request.responseText); } - if (data) { - resolve({ - cMapData: data, - compressionType: this.isCompressed ? - CMapCompressionType.BINARY : CMapCompressionType.NONE, - }); + if (cMapData) { + resolve({ cMapData, compressionType, }); return; } } - reject(new Error('Unable to load ' + - (this.isCompressed ? 'binary ' : '') + - 'CMap at: ' + url)); + reject(new Error(request.statusText)); }; request.send(null); + }).catch((reason) => { + throw new Error(`Unable to load ${this.isCompressed ? 'binary ' : ''}` + + `CMap at: ${url}`); }); } } @@ -428,6 +447,23 @@ class DummyStatTimer { } } +function isFetchSupported() { + return (typeof fetch !== 'undefined' && + typeof Response !== 'undefined' && 'body' in Response.prototype && + // eslint-disable-next-line no-restricted-globals + typeof ReadableStream !== 'undefined'); +} + +function isValidFetchUrl(url, baseUrl) { + try { + const { protocol, } = baseUrl ? new URL(url, baseUrl) : new URL(url); + // The Fetch API only supports the http/https protocols, and not file/ftp. + return (protocol === 'http:' || protocol === 'https:'); + } catch (ex) { + return false; // `new URL()` will throw on incorrect data. + } +} + function loadScript(src) { return new Promise((resolve, reject) => { let script = document.createElement('script'); @@ -453,5 +489,7 @@ export { DOMSVGFactory, StatTimer, DummyStatTimer, + isFetchSupported, + isValidFetchUrl, loadScript, }; diff --git a/src/pdf.js b/src/pdf.js index 3b84f475c..32895bae3 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* eslint-disable no-unused-vars, no-restricted-globals */ +/* eslint-disable no-unused-vars */ 'use strict'; @@ -37,15 +37,17 @@ if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) { pdfjsDisplayAPI.setPDFNetworkStreamFactory((params) => { return new PDFNodeStream(params); }); - } else if (typeof Response !== 'undefined' && 'body' in Response.prototype && - typeof ReadableStream !== 'undefined') { - let PDFFetchStream = require('./display/fetch_stream.js').PDFFetchStream; - pdfjsDisplayAPI.setPDFNetworkStreamFactory((params) => { - return new PDFFetchStream(params); - }); } else { let PDFNetworkStream = require('./display/network.js').PDFNetworkStream; + let PDFFetchStream; + if (pdfjsDisplayDisplayUtils.isFetchSupported()) { + PDFFetchStream = require('./display/fetch_stream.js').PDFFetchStream; + } pdfjsDisplayAPI.setPDFNetworkStreamFactory((params) => { + if (PDFFetchStream && + pdfjsDisplayDisplayUtils.isValidFetchUrl(params.url)) { + return new PDFFetchStream(params); + } return new PDFNetworkStream(params); }); } @@ -65,13 +67,13 @@ if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) { return true; } }; - if (typeof Response !== 'undefined' && 'body' in Response.prototype && - typeof ReadableStream !== 'undefined' && isChromeWithFetchCredentials()) { + if (pdfjsDisplayDisplayUtils.isFetchSupported() && + isChromeWithFetchCredentials()) { PDFFetchStream = require('./display/fetch_stream.js').PDFFetchStream; } pdfjsDisplayAPI.setPDFNetworkStreamFactory((params) => { - if (PDFFetchStream && /^https?:/i.test(params.url)) { - // "fetch" is only supported for http(s), not file/ftp. + if (PDFFetchStream && + pdfjsDisplayDisplayUtils.isValidFetchUrl(params.url)) { return new PDFFetchStream(params); } return new PDFNetworkStream(params); diff --git a/test/unit/display_utils_spec.js b/test/unit/display_utils_spec.js index 474cefe8e..67f339fa7 100644 --- a/test/unit/display_utils_spec.js +++ b/test/unit/display_utils_spec.js @@ -14,7 +14,7 @@ */ import { - DOMSVGFactory, getFilenameFromUrl + DOMSVGFactory, getFilenameFromUrl, isValidFetchUrl } from '../../src/display/display_utils'; import isNodeJS from '../../src/shared/is_node'; @@ -94,4 +94,28 @@ describe('display_utils', function() { expect(result).toEqual(expected); }); }); + + describe('isValidFetchUrl', function() { + it('handles invalid Fetch URLs', function() { + expect(isValidFetchUrl(null)).toEqual(false); + expect(isValidFetchUrl(100)).toEqual(false); + expect(isValidFetchUrl('foo')).toEqual(false); + expect(isValidFetchUrl('/foo', 100)).toEqual(false); + }); + + it('handles relative Fetch URLs', function() { + expect(isValidFetchUrl('/foo', 'file://www.example.com')).toEqual(false); + expect(isValidFetchUrl('/foo', 'http://www.example.com')).toEqual(true); + }); + + it('handles unsupported Fetch protocols', function() { + expect(isValidFetchUrl('file://www.example.com')).toEqual(false); + expect(isValidFetchUrl('ftp://www.example.com')).toEqual(false); + }); + + it('handles supported Fetch protocols', function() { + expect(isValidFetchUrl('http://www.example.com')).toEqual(true); + expect(isValidFetchUrl('https://www.example.com')).toEqual(true); + }); + }); }); diff --git a/test/unit/test_utils.js b/test/unit/test_utils.js index 0b590b1cc..32c43f682 100644 --- a/test/unit/test_utils.js +++ b/test/unit/test_utils.js @@ -99,32 +99,31 @@ class NodeCMapReaderFactory { this.isCompressed = isCompressed; } - fetch({ name, }) { + async fetch({ name, }) { if (!this.baseUrl) { - return Promise.reject(new Error( + throw new Error( 'The CMap "baseUrl" parameter must be specified, ensure that ' + - 'the "cMapUrl" and "cMapPacked" API parameters are provided.')); + 'the "cMapUrl" and "cMapPacked" API parameters are provided.'); } if (!name) { - return Promise.reject(new Error('CMap name must be specified.')); + throw new Error('CMap name must be specified.'); } - return new Promise((resolve, reject) => { - let url = this.baseUrl + name + (this.isCompressed ? '.bcmap' : ''); + const url = this.baseUrl + name + (this.isCompressed ? '.bcmap' : ''); + const compressionType = (this.isCompressed ? CMapCompressionType.BINARY : + CMapCompressionType.NONE); - let fs = require('fs'); + return new Promise((resolve, reject) => { + const fs = require('fs'); fs.readFile(url, (error, data) => { if (error || !data) { - reject(new Error('Unable to load ' + - (this.isCompressed ? 'binary ' : '') + - 'CMap at: ' + url)); + reject(new Error(error)); return; } - resolve({ - cMapData: new Uint8Array(data), - compressionType: this.isCompressed ? - CMapCompressionType.BINARY : CMapCompressionType.NONE, - }); + resolve({ cMapData: new Uint8Array(data), compressionType, }); }); + }).catch((reason) => { + throw new Error(`Unable to load ${this.isCompressed ? 'binary ' : ''}` + + `CMap at: ${url}`); }); } }