From 26b8cff5f97b0f1fb2c0f3d131ae442ec8ce9eba Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 3 Jul 2018 18:32:39 +0200 Subject: [PATCH] Stop exposing the `URL` polyfill in the global scope This moves/exposes the `URL` polyfill similarily to the existing `ReadableStream` polyfill, rather than exposing it globally, to avoid interfering with any "outside" code. Both the `URL` and `ReadableStream` polyfills are now exposed on the `pdfjsLib` object, such that they are accessible to the viewer components. Furthermore, the `no-restricted-globals` ESLint rule is also enabled to prevent accidental usage of the native `URL`/`ReadableStream` implementations directly in the `src/` and `web/` folders; see also https://eslint.org/docs/rules/no-restricted-globals Addresses the remaining TODO in https://github.com/mozilla/pdf.js/projects/6 --- .eslintrc | 1 + external/url/url-lib.js | 627 +++++++++++++++++++++++++++++++ gulpfile.js | 4 + src/.eslintrc | 14 + src/display/api.js | 2 +- src/pdf.js | 4 +- src/shared/compatibility.js | 659 --------------------------------- src/shared/streams_polyfill.js | 1 + src/shared/url_polyfill.js | 63 ++++ src/shared/util.js | 2 + web/.eslintrc | 7 + web/app.js | 8 +- web/chromecom.js | 1 + web/debugger.js | 9 +- web/download_manager.js | 2 +- web/firefoxcom.js | 2 +- web/pdf_print_service.js | 1 + 17 files changed, 736 insertions(+), 671 deletions(-) create mode 100644 external/url/url-lib.js create mode 100644 src/.eslintrc create mode 100644 src/shared/url_polyfill.js diff --git a/.eslintrc b/.eslintrc index 43d0da0fbe35c..e1e2d05bdbd94 100644 --- a/.eslintrc +++ b/.eslintrc @@ -98,6 +98,7 @@ "no-catch-shadow": "error", "no-delete-var": "error", "no-label-var": "error", + "no-restricted-globals": "off", "no-shadow-restricted-names": "error", "no-shadow": "off", "no-undef-init": "error", diff --git a/external/url/url-lib.js b/external/url/url-lib.js new file mode 100644 index 0000000000000..1d946e8410a4b --- /dev/null +++ b/external/url/url-lib.js @@ -0,0 +1,627 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Polyfill obtained from: https://github.com/Polymer/URL + +(function URLConstructorClosure() { + 'use strict'; + + var relative = Object.create(null); + relative['ftp'] = 21; + relative['file'] = 0; + relative['gopher'] = 70; + relative['http'] = 80; + relative['https'] = 443; + relative['ws'] = 80; + relative['wss'] = 443; + + var relativePathDotMapping = Object.create(null); + relativePathDotMapping['%2e'] = '.'; + relativePathDotMapping['.%2e'] = '..'; + relativePathDotMapping['%2e.'] = '..'; + relativePathDotMapping['%2e%2e'] = '..'; + + function isRelativeScheme(scheme) { + return relative[scheme] !== undefined; + } + + function invalid() { + clear.call(this); + this._isInvalid = true; + } + + function IDNAToASCII(h) { + if (h === '') { + invalid.call(this); + } + // XXX + return h.toLowerCase(); + } + + function percentEscape(c) { + var unicode = c.charCodeAt(0); + if (unicode > 0x20 && + unicode < 0x7F && + // " # < > ? ` + [0x22, 0x23, 0x3C, 0x3E, 0x3F, 0x60].indexOf(unicode) === -1 + ) { + return c; + } + return encodeURIComponent(c); + } + + function percentEscapeQuery(c) { + // XXX This actually needs to encode c using encoding and then + // convert the bytes one-by-one. + + var unicode = c.charCodeAt(0); + if (unicode > 0x20 && + unicode < 0x7F && + // " # < > ` (do not escape '?') + [0x22, 0x23, 0x3C, 0x3E, 0x60].indexOf(unicode) === -1 + ) { + return c; + } + return encodeURIComponent(c); + } + + var EOF, ALPHA = /[a-zA-Z]/, + ALPHANUMERIC = /[a-zA-Z0-9\+\-\.]/; + + function parse(input, stateOverride, base) { + function err(message) { + errors.push(message); + } + + var state = stateOverride || 'scheme start', + cursor = 0, + buffer = '', + seenAt = false, + seenBracket = false, + errors = []; + + loop: while ((input[cursor - 1] !== EOF || cursor === 0) && + !this._isInvalid) { + var c = input[cursor]; + switch (state) { + case 'scheme start': + if (c && ALPHA.test(c)) { + buffer += c.toLowerCase(); // ASCII-safe + state = 'scheme'; + } else if (!stateOverride) { + buffer = ''; + state = 'no scheme'; + continue; + } else { + err('Invalid scheme.'); + break loop; + } + break; + + case 'scheme': + if (c && ALPHANUMERIC.test(c)) { + buffer += c.toLowerCase(); // ASCII-safe + } else if (c === ':') { + this._scheme = buffer; + buffer = ''; + if (stateOverride) { + break loop; + } + if (isRelativeScheme(this._scheme)) { + this._isRelative = true; + } + if (this._scheme === 'file') { + state = 'relative'; + } else if (this._isRelative && base && + base._scheme === this._scheme) { + state = 'relative or authority'; + } else if (this._isRelative) { + state = 'authority first slash'; + } else { + state = 'scheme data'; + } + } else if (!stateOverride) { + buffer = ''; + cursor = 0; + state = 'no scheme'; + continue; + } else if (c === EOF) { + break loop; + } else { + err('Code point not allowed in scheme: ' + c); + break loop; + } + break; + + case 'scheme data': + if (c === '?') { + this._query = '?'; + state = 'query'; + } else if (c === '#') { + this._fragment = '#'; + state = 'fragment'; + } else { + // XXX error handling + if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') { + this._schemeData += percentEscape(c); + } + } + break; + + case 'no scheme': + if (!base || !(isRelativeScheme(base._scheme))) { + err('Missing scheme.'); + invalid.call(this); + } else { + state = 'relative'; + continue; + } + break; + + case 'relative or authority': + if (c === '/' && input[cursor + 1] === '/') { + state = 'authority ignore slashes'; + } else { + err('Expected /, got: ' + c); + state = 'relative'; + continue; + } + break; + + case 'relative': + this._isRelative = true; + if (this._scheme !== 'file') { + this._scheme = base._scheme; + } + if (c === EOF) { + this._host = base._host; + this._port = base._port; + this._path = base._path.slice(); + this._query = base._query; + this._username = base._username; + this._password = base._password; + break loop; + } else if (c === '/' || c === '\\') { + if (c === '\\') { + err('\\ is an invalid code point.'); + } + state = 'relative slash'; + } else if (c === '?') { + this._host = base._host; + this._port = base._port; + this._path = base._path.slice(); + this._query = '?'; + this._username = base._username; + this._password = base._password; + state = 'query'; + } else if (c === '#') { + this._host = base._host; + this._port = base._port; + this._path = base._path.slice(); + this._query = base._query; + this._fragment = '#'; + this._username = base._username; + this._password = base._password; + state = 'fragment'; + } else { + var nextC = input[cursor + 1]; + var nextNextC = input[cursor + 2]; + if (this._scheme !== 'file' || !ALPHA.test(c) || + (nextC !== ':' && nextC !== '|') || + (nextNextC !== EOF && nextNextC !== '/' && nextNextC !== '\\' && + nextNextC !== '?' && nextNextC !== '#')) { + this._host = base._host; + this._port = base._port; + this._username = base._username; + this._password = base._password; + this._path = base._path.slice(); + this._path.pop(); + } + state = 'relative path'; + continue; + } + break; + + case 'relative slash': + if (c === '/' || c === '\\') { + if (c === '\\') { + err('\\ is an invalid code point.'); + } + if (this._scheme === 'file') { + state = 'file host'; + } else { + state = 'authority ignore slashes'; + } + } else { + if (this._scheme !== 'file') { + this._host = base._host; + this._port = base._port; + this._username = base._username; + this._password = base._password; + } + state = 'relative path'; + continue; + } + break; + + case 'authority first slash': + if (c === '/') { + state = 'authority second slash'; + } else { + err('Expected \'/\', got: ' + c); + state = 'authority ignore slashes'; + continue; + } + break; + + case 'authority second slash': + state = 'authority ignore slashes'; + if (c !== '/') { + err('Expected \'/\', got: ' + c); + continue; + } + break; + + case 'authority ignore slashes': + if (c !== '/' && c !== '\\') { + state = 'authority'; + continue; + } else { + err('Expected authority, got: ' + c); + } + break; + + case 'authority': + if (c === '@') { + if (seenAt) { + err('@ already seen.'); + buffer += '%40'; + } + seenAt = true; + for (var i = 0; i < buffer.length; i++) { + var cp = buffer[i]; + if (cp === '\t' || cp === '\n' || cp === '\r') { + err('Invalid whitespace in authority.'); + continue; + } + // XXX check URL code points + if (cp === ':' && this._password === null) { + this._password = ''; + continue; + } + var tempC = percentEscape(cp); + if (this._password !== null) { + this._password += tempC; + } else { + this._username += tempC; + } + } + buffer = ''; + } else if (c === EOF || c === '/' || c === '\\' || + c === '?' || c === '#') { + cursor -= buffer.length; + buffer = ''; + state = 'host'; + continue; + } else { + buffer += c; + } + break; + + case 'file host': + if (c === EOF || c === '/' || c === '\\' || c === '?' || c === '#') { + if (buffer.length === 2 && ALPHA.test(buffer[0]) && + (buffer[1] === ':' || buffer[1] === '|')) { + state = 'relative path'; + } else if (buffer.length === 0) { + state = 'relative path start'; + } else { + this._host = IDNAToASCII.call(this, buffer); + buffer = ''; + state = 'relative path start'; + } + continue; + } else if (c === '\t' || c === '\n' || c === '\r') { + err('Invalid whitespace in file host.'); + } else { + buffer += c; + } + break; + + case 'host': + case 'hostname': + if (c === ':' && !seenBracket) { + // XXX host parsing + this._host = IDNAToASCII.call(this, buffer); + buffer = ''; + state = 'port'; + if (stateOverride === 'hostname') { + break loop; + } + } else if (c === EOF || c === '/' || + c === '\\' || c === '?' || c === '#') { + this._host = IDNAToASCII.call(this, buffer); + buffer = ''; + state = 'relative path start'; + if (stateOverride) { + break loop; + } + continue; + } else if (c !== '\t' && c !== '\n' && c !== '\r') { + if (c === '[') { + seenBracket = true; + } else if (c === ']') { + seenBracket = false; + } + buffer += c; + } else { + err('Invalid code point in host/hostname: ' + c); + } + break; + + case 'port': + if (/[0-9]/.test(c)) { + buffer += c; + } else if (c === EOF || c === '/' || c === '\\' || + c === '?' || c === '#' || stateOverride) { + if (buffer !== '') { + var temp = parseInt(buffer, 10); + if (temp !== relative[this._scheme]) { + this._port = temp + ''; + } + buffer = ''; + } + if (stateOverride) { + break loop; + } + state = 'relative path start'; + continue; + } else if (c === '\t' || c === '\n' || c === '\r') { + err('Invalid code point in port: ' + c); + } else { + invalid.call(this); + } + break; + + case 'relative path start': + if (c === '\\') { + err('\'\\\' not allowed in path.'); + } + state = 'relative path'; + if (c !== '/' && c !== '\\') { + continue; + } + break; + + case 'relative path': + if (c === EOF || c === '/' || c === '\\' || + (!stateOverride && (c === '?' || c === '#'))) { + if (c === '\\') { + err('\\ not allowed in relative path.'); + } + var tmp; + if ((tmp = relativePathDotMapping[buffer.toLowerCase()])) { + buffer = tmp; + } + if (buffer === '..') { + this._path.pop(); + if (c !== '/' && c !== '\\') { + this._path.push(''); + } + } else if (buffer === '.' && c !== '/' && c !== '\\') { + this._path.push(''); + } else if (buffer !== '.') { + if (this._scheme === 'file' && this._path.length === 0 && + buffer.length === 2 && ALPHA.test(buffer[0]) && + buffer[1] === '|') { + buffer = buffer[0] + ':'; + } + this._path.push(buffer); + } + buffer = ''; + if (c === '?') { + this._query = '?'; + state = 'query'; + } else if (c === '#') { + this._fragment = '#'; + state = 'fragment'; + } + } else if (c !== '\t' && c !== '\n' && c !== '\r') { + buffer += percentEscape(c); + } + break; + + case 'query': + if (!stateOverride && c === '#') { + this._fragment = '#'; + state = 'fragment'; + } else if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') { + this._query += percentEscapeQuery(c); + } + break; + + case 'fragment': + if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') { + this._fragment += c; + } + break; + } + + cursor++; + } + } + + function clear() { + this._scheme = ''; + this._schemeData = ''; + this._username = ''; + this._password = null; + this._host = ''; + this._port = ''; + this._path = []; + this._query = ''; + this._fragment = ''; + this._isInvalid = false; + this._isRelative = false; + } + + // Does not process domain names or IP addresses. + // Does not handle encoding for the query parameter. + function JURL(url, base /* , encoding */) { + if (base !== undefined && !(base instanceof JURL)) { + base = new JURL(String(base)); + } + + this._url = url; + clear.call(this); + + var input = url.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, ''); + // encoding = encoding || 'utf-8' + + parse.call(this, input, null, base); + } + + JURL.prototype = { + toString() { + return this.href; + }, + get href() { + if (this._isInvalid) { + return this._url; + } + var authority = ''; + if (this._username !== '' || this._password !== null) { + authority = this._username + + (this._password !== null ? ':' + this._password : '') + '@'; + } + + return this.protocol + + (this._isRelative ? '//' + authority + this.host : '') + + this.pathname + this._query + this._fragment; + }, + // The named parameter should be different from the setter's function name. + // Otherwise Safari 5 will throw an error (see issue 8541) + set href(value) { + clear.call(this); + parse.call(this, value); + }, + + get protocol() { + return this._scheme + ':'; + }, + set protocol(value) { + if (this._isInvalid) { + return; + } + parse.call(this, value + ':', 'scheme start'); + }, + + get host() { + return this._isInvalid ? '' : this._port ? + this._host + ':' + this._port : this._host; + }, + set host(value) { + if (this._isInvalid || !this._isRelative) { + return; + } + parse.call(this, value, 'host'); + }, + + get hostname() { + return this._host; + }, + set hostname(value) { + if (this._isInvalid || !this._isRelative) { + return; + } + parse.call(this, value, 'hostname'); + }, + + get port() { + return this._port; + }, + set port(value) { + if (this._isInvalid || !this._isRelative) { + return; + } + parse.call(this, value, 'port'); + }, + + get pathname() { + return this._isInvalid ? '' : this._isRelative ? + '/' + this._path.join('/') : this._schemeData; + }, + set pathname(value) { + if (this._isInvalid || !this._isRelative) { + return; + } + this._path = []; + parse.call(this, value, 'relative path start'); + }, + + get search() { + return this._isInvalid || !this._query || this._query === '?' ? + '' : this._query; + }, + set search(value) { + if (this._isInvalid || !this._isRelative) { + return; + } + this._query = '?'; + if (value[0] === '?') { + value = value.slice(1); + } + parse.call(this, value, 'query'); + }, + + get hash() { + return this._isInvalid || !this._fragment || this._fragment === '#' ? + '' : this._fragment; + }, + set hash(value) { + if (this._isInvalid) { + return; + } + this._fragment = '#'; + if (value[0] === '#') { + value = value.slice(1); + } + parse.call(this, value, 'fragment'); + }, + + get origin() { + var host; + if (this._isInvalid || !this._scheme) { + return ''; + } + // javascript: Gecko returns String(""), WebKit/Blink String("null") + // Gecko throws error for "data://" + // data: Gecko returns "", Blink returns "data://", WebKit returns "null" + // Gecko returns String("") for file: mailto: + // WebKit/Blink returns String("SCHEME://") for file: mailto: + switch (this._scheme) { + case 'data': + case 'file': + case 'javascript': + case 'mailto': + return 'null'; + case 'blob': + // Special case of blob: -- returns valid origin of _schemeData. + try { + return new JURL(this._schemeData).origin || 'null'; + } catch (_) { + // Invalid _schemeData origin -- ignoring errors. + } + return 'null'; + } + host = this.host; + if (!host) { + return ''; + } + return this._scheme + '://' + host; + }, + }; + + exports.URL = JURL; +})(); diff --git a/gulpfile.js b/gulpfile.js index 919201ee21971..dbf0c68e40baa 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -941,6 +941,8 @@ gulp.task('lib', ['buildnumber'], function () { buildLib, gulp.src('external/streams/streams-lib.js', { base: '.', }) .pipe(gulp.dest('build/')), + gulp.src('external/url/url-lib.js', { base: '.', }) + .pipe(gulp.dest('build/')), ]); }); @@ -1243,6 +1245,8 @@ gulp.task('dist-pre', return merge([ gulp.src('external/streams/streams-lib.js', { base: '.', }) .pipe(gulp.dest('build/dist/')), + gulp.src('external/url/url-lib.js', { base: '.', }) + .pipe(gulp.dest('build/dist/')), packageJsonSrc.pipe(gulp.dest(DIST_DIR)), bowerJsonSrc.pipe(gulp.dest(DIST_DIR)), vfs.src('external/dist/**/*', diff --git a/src/.eslintrc b/src/.eslintrc new file mode 100644 index 0000000000000..e83cec899f459 --- /dev/null +++ b/src/.eslintrc @@ -0,0 +1,14 @@ +{ + "extends": [ + ../.eslintrc + ], + + "rules": { + // Variables + "no-restricted-globals": ["error", + "ReadableStream", + "URL", + "WritableStream", + ], + }, +} diff --git a/src/display/api.js b/src/display/api.js index 92b418b1083a6..f462d5bc762b7 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -18,7 +18,7 @@ import { assert, createPromiseCapability, getVerbosityLevel, info, InvalidPDFException, isArrayBuffer, isSameOrigin, MissingPDFException, NativeImageDecoding, PasswordException, setVerbosityLevel, shadow, stringToBytes, - UnexpectedResponseException, UnknownErrorException, unreachable, warn + UnexpectedResponseException, UnknownErrorException, unreachable, URL, warn } from '../shared/util'; import { DOMCanvasFactory, DOMCMapReaderFactory, DummyStatTimer, loadScript, diff --git a/src/pdf.js b/src/pdf.js index cc4c2c5e6d11d..b1a1a4cdbd3a7 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 */ +/* eslint-disable no-unused-vars, no-restricted-globals */ 'use strict'; @@ -103,6 +103,8 @@ exports.removeNullCharacters = pdfjsSharedUtil.removeNullCharacters; exports.shadow = pdfjsSharedUtil.shadow; exports.createBlob = pdfjsSharedUtil.createBlob; exports.Util = pdfjsSharedUtil.Util; +exports.ReadableStream = pdfjsSharedUtil.ReadableStream; +exports.URL = pdfjsSharedUtil.URL; exports.RenderingCancelledException = pdfjsDisplayDOMUtils.RenderingCancelledException; exports.getFilenameFromUrl = pdfjsDisplayDOMUtils.getFilenameFromUrl; diff --git a/src/shared/compatibility.js b/src/shared/compatibility.js index b33831e825d6e..6855c2faa16ae 100644 --- a/src/shared/compatibility.js +++ b/src/shared/compatibility.js @@ -12,7 +12,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* eslint-disable mozilla/use-includes-instead-of-indexOf */ const globalScope = require('./global_scope'); @@ -166,664 +165,6 @@ const hasDOM = typeof window === 'object' && typeof document === 'object'; globalScope.WeakMap = require('core-js/fn/weak-map'); })(); -// Support: IE, Chrome<32 -// Polyfill from https://github.com/Polymer/URL -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ -(function checkURLConstructor() { - if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('IMAGE_DECODERS')) { - // The current image decoders doesn't utilize the `URL` constructor, hence - // it shouldn't need to be polyfilled for the IMAGE_DECODERS build target. - return; - } - // feature detect for URL constructor - var hasWorkingUrl = false; - try { - if (typeof URL === 'function' && - typeof URL.prototype === 'object' && - ('origin' in URL.prototype)) { - var u = new URL('b', 'http://a'); - u.pathname = 'c%20d'; - hasWorkingUrl = u.href === 'http://a/c%20d'; - } - } catch (e) { } - - if (hasWorkingUrl) { - return; - } - - var relative = Object.create(null); - relative['ftp'] = 21; - relative['file'] = 0; - relative['gopher'] = 70; - relative['http'] = 80; - relative['https'] = 443; - relative['ws'] = 80; - relative['wss'] = 443; - - var relativePathDotMapping = Object.create(null); - relativePathDotMapping['%2e'] = '.'; - relativePathDotMapping['.%2e'] = '..'; - relativePathDotMapping['%2e.'] = '..'; - relativePathDotMapping['%2e%2e'] = '..'; - - function isRelativeScheme(scheme) { - return relative[scheme] !== undefined; - } - - function invalid() { - clear.call(this); - this._isInvalid = true; - } - - function IDNAToASCII(h) { - if (h === '') { - invalid.call(this); - } - // XXX - return h.toLowerCase(); - } - - function percentEscape(c) { - var unicode = c.charCodeAt(0); - if (unicode > 0x20 && - unicode < 0x7F && - // " # < > ? ` - [0x22, 0x23, 0x3C, 0x3E, 0x3F, 0x60].indexOf(unicode) === -1 - ) { - return c; - } - return encodeURIComponent(c); - } - - function percentEscapeQuery(c) { - // XXX This actually needs to encode c using encoding and then - // convert the bytes one-by-one. - - var unicode = c.charCodeAt(0); - if (unicode > 0x20 && - unicode < 0x7F && - // " # < > ` (do not escape '?') - [0x22, 0x23, 0x3C, 0x3E, 0x60].indexOf(unicode) === -1 - ) { - return c; - } - return encodeURIComponent(c); - } - - var EOF, ALPHA = /[a-zA-Z]/, - ALPHANUMERIC = /[a-zA-Z0-9\+\-\.]/; - - function parse(input, stateOverride, base) { - function err(message) { - errors.push(message); - } - - var state = stateOverride || 'scheme start', - cursor = 0, - buffer = '', - seenAt = false, - seenBracket = false, - errors = []; - - loop: while ((input[cursor - 1] !== EOF || cursor === 0) && - !this._isInvalid) { - var c = input[cursor]; - switch (state) { - case 'scheme start': - if (c && ALPHA.test(c)) { - buffer += c.toLowerCase(); // ASCII-safe - state = 'scheme'; - } else if (!stateOverride) { - buffer = ''; - state = 'no scheme'; - continue; - } else { - err('Invalid scheme.'); - break loop; - } - break; - - case 'scheme': - if (c && ALPHANUMERIC.test(c)) { - buffer += c.toLowerCase(); // ASCII-safe - } else if (c === ':') { - this._scheme = buffer; - buffer = ''; - if (stateOverride) { - break loop; - } - if (isRelativeScheme(this._scheme)) { - this._isRelative = true; - } - if (this._scheme === 'file') { - state = 'relative'; - } else if (this._isRelative && base && - base._scheme === this._scheme) { - state = 'relative or authority'; - } else if (this._isRelative) { - state = 'authority first slash'; - } else { - state = 'scheme data'; - } - } else if (!stateOverride) { - buffer = ''; - cursor = 0; - state = 'no scheme'; - continue; - } else if (c === EOF) { - break loop; - } else { - err('Code point not allowed in scheme: ' + c); - break loop; - } - break; - - case 'scheme data': - if (c === '?') { - this._query = '?'; - state = 'query'; - } else if (c === '#') { - this._fragment = '#'; - state = 'fragment'; - } else { - // XXX error handling - if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') { - this._schemeData += percentEscape(c); - } - } - break; - - case 'no scheme': - if (!base || !(isRelativeScheme(base._scheme))) { - err('Missing scheme.'); - invalid.call(this); - } else { - state = 'relative'; - continue; - } - break; - - case 'relative or authority': - if (c === '/' && input[cursor + 1] === '/') { - state = 'authority ignore slashes'; - } else { - err('Expected /, got: ' + c); - state = 'relative'; - continue; - } - break; - - case 'relative': - this._isRelative = true; - if (this._scheme !== 'file') { - this._scheme = base._scheme; - } - if (c === EOF) { - this._host = base._host; - this._port = base._port; - this._path = base._path.slice(); - this._query = base._query; - this._username = base._username; - this._password = base._password; - break loop; - } else if (c === '/' || c === '\\') { - if (c === '\\') { - err('\\ is an invalid code point.'); - } - state = 'relative slash'; - } else if (c === '?') { - this._host = base._host; - this._port = base._port; - this._path = base._path.slice(); - this._query = '?'; - this._username = base._username; - this._password = base._password; - state = 'query'; - } else if (c === '#') { - this._host = base._host; - this._port = base._port; - this._path = base._path.slice(); - this._query = base._query; - this._fragment = '#'; - this._username = base._username; - this._password = base._password; - state = 'fragment'; - } else { - var nextC = input[cursor + 1]; - var nextNextC = input[cursor + 2]; - if (this._scheme !== 'file' || !ALPHA.test(c) || - (nextC !== ':' && nextC !== '|') || - (nextNextC !== EOF && nextNextC !== '/' && nextNextC !== '\\' && - nextNextC !== '?' && nextNextC !== '#')) { - this._host = base._host; - this._port = base._port; - this._username = base._username; - this._password = base._password; - this._path = base._path.slice(); - this._path.pop(); - } - state = 'relative path'; - continue; - } - break; - - case 'relative slash': - if (c === '/' || c === '\\') { - if (c === '\\') { - err('\\ is an invalid code point.'); - } - if (this._scheme === 'file') { - state = 'file host'; - } else { - state = 'authority ignore slashes'; - } - } else { - if (this._scheme !== 'file') { - this._host = base._host; - this._port = base._port; - this._username = base._username; - this._password = base._password; - } - state = 'relative path'; - continue; - } - break; - - case 'authority first slash': - if (c === '/') { - state = 'authority second slash'; - } else { - err('Expected \'/\', got: ' + c); - state = 'authority ignore slashes'; - continue; - } - break; - - case 'authority second slash': - state = 'authority ignore slashes'; - if (c !== '/') { - err('Expected \'/\', got: ' + c); - continue; - } - break; - - case 'authority ignore slashes': - if (c !== '/' && c !== '\\') { - state = 'authority'; - continue; - } else { - err('Expected authority, got: ' + c); - } - break; - - case 'authority': - if (c === '@') { - if (seenAt) { - err('@ already seen.'); - buffer += '%40'; - } - seenAt = true; - for (var i = 0; i < buffer.length; i++) { - var cp = buffer[i]; - if (cp === '\t' || cp === '\n' || cp === '\r') { - err('Invalid whitespace in authority.'); - continue; - } - // XXX check URL code points - if (cp === ':' && this._password === null) { - this._password = ''; - continue; - } - var tempC = percentEscape(cp); - if (this._password !== null) { - this._password += tempC; - } else { - this._username += tempC; - } - } - buffer = ''; - } else if (c === EOF || c === '/' || c === '\\' || - c === '?' || c === '#') { - cursor -= buffer.length; - buffer = ''; - state = 'host'; - continue; - } else { - buffer += c; - } - break; - - case 'file host': - if (c === EOF || c === '/' || c === '\\' || c === '?' || c === '#') { - if (buffer.length === 2 && ALPHA.test(buffer[0]) && - (buffer[1] === ':' || buffer[1] === '|')) { - state = 'relative path'; - } else if (buffer.length === 0) { - state = 'relative path start'; - } else { - this._host = IDNAToASCII.call(this, buffer); - buffer = ''; - state = 'relative path start'; - } - continue; - } else if (c === '\t' || c === '\n' || c === '\r') { - err('Invalid whitespace in file host.'); - } else { - buffer += c; - } - break; - - case 'host': - case 'hostname': - if (c === ':' && !seenBracket) { - // XXX host parsing - this._host = IDNAToASCII.call(this, buffer); - buffer = ''; - state = 'port'; - if (stateOverride === 'hostname') { - break loop; - } - } else if (c === EOF || c === '/' || - c === '\\' || c === '?' || c === '#') { - this._host = IDNAToASCII.call(this, buffer); - buffer = ''; - state = 'relative path start'; - if (stateOverride) { - break loop; - } - continue; - } else if (c !== '\t' && c !== '\n' && c !== '\r') { - if (c === '[') { - seenBracket = true; - } else if (c === ']') { - seenBracket = false; - } - buffer += c; - } else { - err('Invalid code point in host/hostname: ' + c); - } - break; - - case 'port': - if (/[0-9]/.test(c)) { - buffer += c; - } else if (c === EOF || c === '/' || c === '\\' || - c === '?' || c === '#' || stateOverride) { - if (buffer !== '') { - var temp = parseInt(buffer, 10); - if (temp !== relative[this._scheme]) { - this._port = temp + ''; - } - buffer = ''; - } - if (stateOverride) { - break loop; - } - state = 'relative path start'; - continue; - } else if (c === '\t' || c === '\n' || c === '\r') { - err('Invalid code point in port: ' + c); - } else { - invalid.call(this); - } - break; - - case 'relative path start': - if (c === '\\') { - err('\'\\\' not allowed in path.'); - } - state = 'relative path'; - if (c !== '/' && c !== '\\') { - continue; - } - break; - - case 'relative path': - if (c === EOF || c === '/' || c === '\\' || - (!stateOverride && (c === '?' || c === '#'))) { - if (c === '\\') { - err('\\ not allowed in relative path.'); - } - var tmp; - if ((tmp = relativePathDotMapping[buffer.toLowerCase()])) { - buffer = tmp; - } - if (buffer === '..') { - this._path.pop(); - if (c !== '/' && c !== '\\') { - this._path.push(''); - } - } else if (buffer === '.' && c !== '/' && c !== '\\') { - this._path.push(''); - } else if (buffer !== '.') { - if (this._scheme === 'file' && this._path.length === 0 && - buffer.length === 2 && ALPHA.test(buffer[0]) && - buffer[1] === '|') { - buffer = buffer[0] + ':'; - } - this._path.push(buffer); - } - buffer = ''; - if (c === '?') { - this._query = '?'; - state = 'query'; - } else if (c === '#') { - this._fragment = '#'; - state = 'fragment'; - } - } else if (c !== '\t' && c !== '\n' && c !== '\r') { - buffer += percentEscape(c); - } - break; - - case 'query': - if (!stateOverride && c === '#') { - this._fragment = '#'; - state = 'fragment'; - } else if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') { - this._query += percentEscapeQuery(c); - } - break; - - case 'fragment': - if (c !== EOF && c !== '\t' && c !== '\n' && c !== '\r') { - this._fragment += c; - } - break; - } - - cursor++; - } - } - - function clear() { - this._scheme = ''; - this._schemeData = ''; - this._username = ''; - this._password = null; - this._host = ''; - this._port = ''; - this._path = []; - this._query = ''; - this._fragment = ''; - this._isInvalid = false; - this._isRelative = false; - } - - // Does not process domain names or IP addresses. - // Does not handle encoding for the query parameter. - function JURL(url, base /* , encoding */) { - if (base !== undefined && !(base instanceof JURL)) { - base = new JURL(String(base)); - } - - this._url = url; - clear.call(this); - - var input = url.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, ''); - // encoding = encoding || 'utf-8' - - parse.call(this, input, null, base); - } - - JURL.prototype = { - toString() { - return this.href; - }, - get href() { - if (this._isInvalid) { - return this._url; - } - var authority = ''; - if (this._username !== '' || this._password !== null) { - authority = this._username + - (this._password !== null ? ':' + this._password : '') + '@'; - } - - return this.protocol + - (this._isRelative ? '//' + authority + this.host : '') + - this.pathname + this._query + this._fragment; - }, - // The named parameter should be different from the setter's function name. - // Otherwise Safari 5 will throw an error (see issue 8541) - set href(value) { - clear.call(this); - parse.call(this, value); - }, - - get protocol() { - return this._scheme + ':'; - }, - set protocol(value) { - if (this._isInvalid) { - return; - } - parse.call(this, value + ':', 'scheme start'); - }, - - get host() { - return this._isInvalid ? '' : this._port ? - this._host + ':' + this._port : this._host; - }, - set host(value) { - if (this._isInvalid || !this._isRelative) { - return; - } - parse.call(this, value, 'host'); - }, - - get hostname() { - return this._host; - }, - set hostname(value) { - if (this._isInvalid || !this._isRelative) { - return; - } - parse.call(this, value, 'hostname'); - }, - - get port() { - return this._port; - }, - set port(value) { - if (this._isInvalid || !this._isRelative) { - return; - } - parse.call(this, value, 'port'); - }, - - get pathname() { - return this._isInvalid ? '' : this._isRelative ? - '/' + this._path.join('/') : this._schemeData; - }, - set pathname(value) { - if (this._isInvalid || !this._isRelative) { - return; - } - this._path = []; - parse.call(this, value, 'relative path start'); - }, - - get search() { - return this._isInvalid || !this._query || this._query === '?' ? - '' : this._query; - }, - set search(value) { - if (this._isInvalid || !this._isRelative) { - return; - } - this._query = '?'; - if (value[0] === '?') { - value = value.slice(1); - } - parse.call(this, value, 'query'); - }, - - get hash() { - return this._isInvalid || !this._fragment || this._fragment === '#' ? - '' : this._fragment; - }, - set hash(value) { - if (this._isInvalid) { - return; - } - this._fragment = '#'; - if (value[0] === '#') { - value = value.slice(1); - } - parse.call(this, value, 'fragment'); - }, - - get origin() { - var host; - if (this._isInvalid || !this._scheme) { - return ''; - } - // javascript: Gecko returns String(""), WebKit/Blink String("null") - // Gecko throws error for "data://" - // data: Gecko returns "", Blink returns "data://", WebKit returns "null" - // Gecko returns String("") for file: mailto: - // WebKit/Blink returns String("SCHEME://") for file: mailto: - switch (this._scheme) { - case 'data': - case 'file': - case 'javascript': - case 'mailto': - return 'null'; - case 'blob': - // Special case of blob: -- returns valid origin of _schemeData. - try { - return new JURL(this._schemeData).origin || 'null'; - } catch (_) { - // Invalid _schemeData origin -- ignoring errors. - } - return 'null'; - } - host = this.host; - if (!host) { - return ''; - } - return this._scheme + '://' + host; - }, - }; - - // Copy over the static methods - var OriginalURL = globalScope.URL; - if (OriginalURL) { - JURL.createObjectURL = function(blob) { - // IE extension allows a second optional options argument. - // http://msdn.microsoft.com/en-us/library/ie/hh772302(v=vs.85).aspx - return OriginalURL.createObjectURL.apply(OriginalURL, arguments); - }; - JURL.revokeObjectURL = function(url) { - OriginalURL.revokeObjectURL(url); - }; - } - - globalScope.URL = JURL; -})(); - } // End of !PDFJSDev.test('CHROME') // Provides support for Object.values in legacy browsers. diff --git a/src/shared/streams_polyfill.js b/src/shared/streams_polyfill.js index 962a26f3be2fb..9dbaaf5c8a25f 100644 --- a/src/shared/streams_polyfill.js +++ b/src/shared/streams_polyfill.js @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/* eslint-disable no-restricted-globals */ let isReadableStreamSupported = false; if (typeof ReadableStream !== 'undefined') { diff --git a/src/shared/url_polyfill.js b/src/shared/url_polyfill.js new file mode 100644 index 0000000000000..b16ba9b78f9f2 --- /dev/null +++ b/src/shared/url_polyfill.js @@ -0,0 +1,63 @@ +/* Copyright 2018 Mozilla Foundation + * + * 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. + */ +/* eslint-disable no-restricted-globals */ + +if (typeof PDFJSDev !== 'undefined' && !PDFJSDev.test('GENERIC')) { + // The `URL` constructor is assumed to be available in the extension builds. + exports.URL = URL; +} else { + let isURLSupported = false; + try { + if (typeof URL === 'function' && typeof URL.prototype === 'object' && + ('origin' in URL.prototype)) { + const u = new URL('b', 'http://a'); + u.pathname = 'c%20d'; + isURLSupported = (u.href === 'http://a/c%20d'); + } + } catch (ex) { + // The `URL` constructor cannot be used. + } + + if (isURLSupported) { + exports.URL = URL; + } else { + if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('IMAGE_DECODERS')) { + class DummyURL { + constructor() { + throw new Error('The current image decoders doesn\'t utilize the ' + + '`URL` constructor, hence it shouldn\'t need to be ' + + 'polyfilled for the IMAGE_DECODERS build target.'); + } + } + exports.URL = DummyURL; + } else { + const PolyfillURL = require('../../external/url/url-lib').URL; + + // Attempt to copy over the static methods. + const OriginalURL = require('./global_scope').URL; + if (OriginalURL) { + PolyfillURL.createObjectURL = function(blob) { + // IE extension allows a second optional options argument, see + // http://msdn.microsoft.com/en-us/library/ie/hh772302(v=vs.85).aspx + return OriginalURL.createObjectURL.apply(OriginalURL, arguments); + }; + PolyfillURL.revokeObjectURL = function(url) { + OriginalURL.revokeObjectURL(url); + }; + } + exports.URL = PolyfillURL; + } + } +} diff --git a/src/shared/util.js b/src/shared/util.js index 19a331f6709c0..611dfe6ccfdef 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -15,6 +15,7 @@ import './compatibility'; import { ReadableStream } from './streams_polyfill'; +import { URL } from './url_polyfill'; var FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0]; @@ -1075,6 +1076,7 @@ export { readUint32, removeNullCharacters, ReadableStream, + URL, setVerbosityLevel, shadow, string32, diff --git a/web/.eslintrc b/web/.eslintrc index 1c31729d52da6..4add1b7105a6a 100644 --- a/web/.eslintrc +++ b/web/.eslintrc @@ -4,6 +4,13 @@ ], "rules": { + // Variables + "no-restricted-globals": ["error", + "ReadableStream", + "URL", + "WritableStream", + ], + // ECMAScript 6 "no-var": "error", }, diff --git a/web/app.js b/web/app.js index d6fb3e7c5f54f..4121b8f11dff6 100644 --- a/web/app.js +++ b/web/app.js @@ -21,9 +21,10 @@ import { TextLayerMode } from './ui_utils'; import { - build, createBlob, getDocument, getFilenameFromUrl, GlobalWorkerOptions, - InvalidPDFException, LinkTarget, loadScript, MissingPDFException, OPS, - PDFWorker, shadow, UnexpectedResponseException, UNSUPPORTED_FEATURES, version + build, createBlob, createObjectURL, getDocument, getFilenameFromUrl, + GlobalWorkerOptions, InvalidPDFException, LinkTarget, loadScript, + MissingPDFException, OPS, PDFWorker, shadow, UnexpectedResponseException, + UNSUPPORTED_FEATURES, URL, version } from 'pdfjs-lib'; import { CursorTool, PDFCursorTools } from './pdf_cursor_tools'; import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue'; @@ -1583,6 +1584,7 @@ function loadAndEnablePDFBug(enabledTabs) { PDFBug.enable(enabledTabs); PDFBug.init({ OPS, + createObjectURL, }, appConfig.mainContainer); }); } diff --git a/web/chromecom.js b/web/chromecom.js index 55dec5be82781..da39d2b527cc7 100644 --- a/web/chromecom.js +++ b/web/chromecom.js @@ -19,6 +19,7 @@ import { AppOptions } from './app_options'; import { BasePreferences } from './preferences'; import { DownloadManager } from './download_manager'; import { GenericL10n } from './genericl10n'; +import { URL } from 'pdfjs-lib'; if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('CHROME')) { throw new Error('Module "pdfjs-web/chromecom" shall not be used outside ' + diff --git a/web/debugger.js b/web/debugger.js index 57a4ed4e4cf69..630272f61a941 100644 --- a/web/debugger.js +++ b/web/debugger.js @@ -17,7 +17,7 @@ 'use strict'; var FontInspector = (function FontInspectorClosure() { - var fonts; + var fonts, createObjectURL; var active = false; var fontAttribute = 'data-font-name'; function removeSelection() { @@ -75,6 +75,8 @@ var FontInspector = (function FontInspectorClosure() { fonts = document.createElement('div'); panel.appendChild(fonts); + + createObjectURL = pdfjsLib.createObjectURL; }, cleanup: function cleanup() { fonts.textContent = ''; @@ -119,10 +121,7 @@ var FontInspector = (function FontInspectorClosure() { url = /url\(['"]?([^\)"']+)/.exec(url); download.href = url[1]; } else if (fontObj.data) { - url = URL.createObjectURL(new Blob([fontObj.data], { - type: fontObj.mimeType, - })); - download.href = url; + download.href = createObjectURL(fontObj.data, fontObj.mimeType); } download.textContent = 'Download'; var logIt = document.createElement('a'); diff --git a/web/download_manager.js b/web/download_manager.js index f0809ae309dec..6b266a2b1819f 100644 --- a/web/download_manager.js +++ b/web/download_manager.js @@ -14,7 +14,7 @@ */ import { - apiCompatibilityParams, createObjectURL, createValidAbsoluteUrl + apiCompatibilityParams, createObjectURL, createValidAbsoluteUrl, URL } from 'pdfjs-lib'; if (typeof PDFJSDev !== 'undefined' && !PDFJSDev.test('CHROME || GENERIC')) { diff --git a/web/firefoxcom.js b/web/firefoxcom.js index 9435bc5626085..99026e9faac07 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -14,7 +14,7 @@ */ import '../extensions/firefox/tools/l10n'; -import { createObjectURL, PDFDataRangeTransport, shadow } from 'pdfjs-lib'; +import { createObjectURL, PDFDataRangeTransport, shadow, URL } from 'pdfjs-lib'; import { BasePreferences } from './preferences'; import { PDFViewerApplication } from './app'; diff --git a/web/pdf_print_service.js b/web/pdf_print_service.js index f6470745d16fc..4ec3ea6613098 100644 --- a/web/pdf_print_service.js +++ b/web/pdf_print_service.js @@ -15,6 +15,7 @@ import { CSS_UNITS, NullL10n } from './ui_utils'; import { PDFPrintServiceFactory, PDFViewerApplication } from './app'; +import { URL } from 'pdfjs-lib'; let activeService = null; let overlayManager = null;