diff --git a/AUTHORS b/AUTHORS index d7fdf1b59..42e747ef8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,6 +14,7 @@ Jakob Miland Julian Viereck Justin D'Arcangelo Kalervo Kujala +Rob Wu Shaon Barman Vivien Nicolas <21@vingtetun.org> Yury Delendik diff --git a/extensions/chrome/hide-xhtml-error.css b/extensions/chrome/hide-xhtml-error.css new file mode 100644 index 000000000..b917c6b8c --- /dev/null +++ b/extensions/chrome/hide-xhtml-error.css @@ -0,0 +1,3 @@ +parsererror { + display: none; +} diff --git a/extensions/chrome/insertviewer.js b/extensions/chrome/insertviewer.js new file mode 100644 index 000000000..9cd0687e8 --- /dev/null +++ b/extensions/chrome/insertviewer.js @@ -0,0 +1,136 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* +Copyright 2012 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. +*/ +/* globals chrome */ + +'use strict'; + +var VIEWER_URL = chrome.extension.getURL('content/web/viewer.html'); +var BASE_URL = VIEWER_URL.replace(/[^\/]+$/, ''); + +function getViewerURL(pdf_url) { + return VIEWER_URL + '?file=' + encodeURIComponent(pdf_url); +} + +function showViewer(url) { + // Cancel page load and empty document. + window.stop(); + document.body.textContent = ''; + + replaceDocumentWithViewer(url); +} +function makeLinksAbsolute(doc) { + normalize('href', 'link[href]'); + normalize('src', 'style[src],script[src]'); + + function normalize(attribute, selector) { + var nodes = doc.querySelectorAll(selector); + for (var i=0; i elements (added back later). + // I assumed that no inline script tags exist. + var scripts = [], script; + + // new Worker('chrome-extension://..../pdf.js') fails, despite having + // the correct permissions. Fix it: + script = document.createElement('script'); + script.onload = loadNextScript; + script.src = chrome.extension.getURL('patch-worker.js'); + scripts.push(script); + + while (x.response.scripts.length) { + script = x.response.scripts[0]; + var newScript = document.createElement('script'); + newScript.onload = loadNextScript; + newScript.src = script.src; + script.parentNode.removeChild(script); + scripts.push(newScript); + } + + // Replace document with viewer + var docEl = document.adoptNode(x.response.documentElement); + document.replaceChild(docEl, document.documentElement); + // Force Chrome to render content + // (without this line, the layout is broken and querySelector + // fails to find elements, even when they appear in the doc) + document.body.innerHTML += ''; + + // Load all scripts + loadNextScript(); + + function loadNextScript() { + if (scripts.length > 0) + document.head.appendChild(scripts.shift()); + else + renderPDF(url); + } + }; + x.send(); +} +function renderPDF(url) { + var args = { + BASE_URL: BASE_URL, + pdf_url: url + }; + // The following technique is explained at + // http://stackoverflow.com/a/9517879/938089 + var script = document.createElement('script'); + script.textContent = + '(function(args) {' + + ' PDFJS.workerSrc = args.BASE_URL + PDFJS.workerSrc;' + + ' window.DEFAULT_URL = args.pdf_url;' + + ' window.IMAGE_DIR = args.BASE_URL + window.IMAGE_DIR;' + + '})(' + JSON.stringify(args) + ');'; + document.head.appendChild(script); + + // Trigger domready + if (document.readyState === 'complete') { + var event = document.createEvent('Event'); + event.initEvent('DOMContentLoaded', true, true); + document.dispatchEvent(event); + } +} + + +// Activate the content script only once per frame (until reload) +if (!window.hasRun) { + window.hasRun = true; + chrome.extension.onMessage.addListener(function listener(message) { + if (message && message.type === 'showPDFViewer' && + message.url === location.href) { + chrome.extension.onMessage.removeListener(listener); + showViewer(message.url); + } + }); +} diff --git a/extensions/chrome/manifest.json b/extensions/chrome/manifest.json index b66f8d41f..26dea5de1 100644 --- a/extensions/chrome/manifest.json +++ b/extensions/chrome/manifest.json @@ -10,14 +10,21 @@ }, "permissions": [ "webRequest", "webRequestBlocking", - "http://*/*.pdf", - "https://*/*.pdf", - "file:///*/*.pdf", - "http://*/*.PDF", - "https://*/*.PDF", - "file://*/*.PDF" + "", + "tabs" ], + "content_scripts": [{ + "matches": [ + "*://*/*.pdf*", + "*://*/*.PDF*" + ], + "css": ["hide-xhtml-error.css"] + }], "background": { "page": "pdfHandler.html" - } + }, + "web_accessible_resources": [ + "patch-worker.js", + "content/*" + ] } diff --git a/extensions/chrome/patch-worker.js b/extensions/chrome/patch-worker.js new file mode 100644 index 000000000..67389eada --- /dev/null +++ b/extensions/chrome/patch-worker.js @@ -0,0 +1,135 @@ +/* +Copyright 2013 Rob Wu +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. +*/ +// Target: Chrome 20+ + +// W3-compliant Worker proxy. +// This module replaces the global Worker object. +// When invoked, the default Worker object is called. +// If this call fails with SECURITY_ERR, the script is fetched +// using async XHR, and transparently proxies all calls and +// setters/getters to the new Worker object. +// Note: This script does not magically circumvent the Same origin policy. + +(function() { + 'use strict'; + var Worker_ = window.Worker; + var URL = window.URL || window.webkitURL; + // Create dummy worker for the following purposes: + // 1. Don't override the global Worker object if the fallback isn't + // going to work (future API changes?) + // 2. Use it to trigger early validation of postMessage calls + // Note: Blob constructor is supported since Chrome 20, but since + // some of the used Chrome APIs are only supported as of Chrome 20, + // I don't bother adding a BlobBuilder fallback. + var dummyWorker = new Worker_( + URL.createObjectURL(new Blob([], {type: 'text/javascript'}))); + window.Worker = function Worker(scriptURL) { + if (arguments.length === 0) { + throw new TypeError('Not enough arguments'); + } + try { + return new Worker_(scriptURL); + } catch (e) { + if (e.code === 18/*DOMException.SECURITY_ERR*/) { + return new WorkerXHR(scriptURL); + } else { + throw e; + } + } + }; + // Bind events and replay queued messages + function bindWorker(worker, workerURL) { + if (worker._terminated) { + return; + } + worker.Worker = new Worker_(workerURL); + worker.Worker.onerror = worker._onerror; + worker.Worker.onmessage = worker._onmessage; + var o; + while ( (o = worker._replayQueue.shift()) ) { + worker.Worker[o.method].apply(worker.Worker, o.arguments); + } + while ( (o = worker._messageQueue.shift()) ) { + worker.Worker.postMessage.apply(worker.Worker, o); + } + } + function WorkerXHR(scriptURL) { + var worker = this; + var x = new XMLHttpRequest(); + x.responseType = 'blob'; + x.onload = function() { + // http://stackoverflow.com/a/10372280/938089 + var workerURL = URL.createObjectURL(x.response); + bindWorker(worker, workerURL); + }; + x.open('GET', scriptURL); + x.send(); + worker._replayQueue = []; + worker._messageQueue = []; + } + WorkerXHR.prototype = { + constructor: Worker_, + terminate: function() { + if (!this._terminated) { + this._terminated = true; + if (this.Worker) + this.Worker.terminate(); + } + }, + postMessage: function(message, transfer) { + if (!(this instanceof WorkerXHR)) + throw new TypeError('Illegal invocation'); + if (this.Worker) { + this.Worker.postMessage.apply(this.Worker, arguments); + } else { + // Trigger validation: + dummyWorker.postMessage(message); + // Alright, push the valid message to the queue. + this._messageQueue.push(arguments); + } + } + }; + // Implement the EventTarget interface + [ + 'addEventListener', + 'removeEventListener', + 'dispatchEvent' + ].forEach(function(method) { + WorkerXHR.prototype[method] = function() { + if (!(this instanceof WorkerXHR)) { + throw new TypeError('Illegal invocation'); + } + if (this.Worker) { + this.Worker[method].apply(this.Worker, arguments); + } else { + this._replayQueue.push({method: method, arguments: arguments}); + } + }; + }); + Object.defineProperties(WorkerXHR.prototype, { + onmessage: { + get: function() {return this._onmessage || null;}, + set: function(func) { + this._onmessage = typeof func === 'function' ? func : null; + } + }, + onerror: { + get: function() {return this._onerror || null;}, + set: function(func) { + this._onerror = typeof func === 'function' ? func : null; + } + } + }); +})(); diff --git a/extensions/chrome/pdfHandler-local.js b/extensions/chrome/pdfHandler-local.js new file mode 100644 index 000000000..8fe33bf00 --- /dev/null +++ b/extensions/chrome/pdfHandler-local.js @@ -0,0 +1,69 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* +Copyright 2012 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. +*/ +/* globals chrome, isPdfDownloadable */ + +'use strict'; + +// The onHeadersReceived event is not generated for local resources. +// Fortunately, local PDF files will have the .pdf extension, so there's +// no need to detect the Content-Type +// Unfortunately, the omnibox won't show the URL. +// Unfortunately, this method will not work for pages in incognito mode, +// unless "incognito":"split" is used AND http:/crbug.com/224094 is fixed. + +// Keeping track of incognito tab IDs will become obsolete when +// "incognito":"split" can be used. +var incognitoTabIds = []; +chrome.windows.getAll({ populate: true }, function(windows) { + windows.forEach(function(win) { + if (win.incognito) { + win.tabs.forEach(function(tab) { + incognitoTabIds.push(tab.id); + }); + } + }); +}); +chrome.tabs.onCreated.addListener(function(tab) { + if (tab.incognito) incognitoTabIds.push(tab.id); +}); +chrome.tabs.onRemoved.addListener(function(tabId) { + var index = incognitoTabIds.indexOf(tabId); + if (index !== -1) incognitoTabIds.splice(index, 1); +}); + +chrome.webRequest.onBeforeRequest.addListener( + function(details) { + if (isPdfDownloadable(details)) // Defined in pdfHandler.js + return; + + if (incognitoTabIds.indexOf(details.tabId) !== -1) + return; // Doesn't work in incognito mode, so don't redirect. + + var viewerPage = 'content/web/viewer.html'; + var url = chrome.extension.getURL(viewerPage) + + '?file=' + encodeURIComponent(details.url); + return { redirectUrl: url }; + }, + { + urls: [ + 'file://*/*.pdf', + 'file://*/*.PDF' + ], + types: ['main_frame', 'sub_frame'] + }, + ['blocking']); diff --git a/extensions/chrome/pdfHandler.html b/extensions/chrome/pdfHandler.html index 7a64ecd16..821f4c884 100644 --- a/extensions/chrome/pdfHandler.html +++ b/extensions/chrome/pdfHandler.html @@ -15,3 +15,4 @@ See the License for the specific language governing permissions and limitations under the License. --> + diff --git a/extensions/chrome/pdfHandler.js b/extensions/chrome/pdfHandler.js index 87d7bb439..76811f0aa 100644 --- a/extensions/chrome/pdfHandler.js +++ b/extensions/chrome/pdfHandler.js @@ -23,25 +23,94 @@ function isPdfDownloadable(details) { return details.url.indexOf('pdfjs.action=download') >= 0; } -chrome.webRequest.onBeforeRequest.addListener( +function insertPDFJSForTab(tabId, url) { + chrome.tabs.executeScript(tabId, { + file: 'insertviewer.js', + allFrames: true, + runAt: 'document_start' + }, function() { + chrome.tabs.sendMessage(tabId, { + type: 'showPDFViewer', + url: url + }); + }); +} +function activatePDFJSForTab(tabId, url) { + chrome.tabs.onUpdated.addListener(function listener(_tabId) { + if (tabId === _tabId) { + insertPDFJSForTab(tabId, url); + chrome.tabs.onUpdated.removeListener(listener); + } + }); +} + +chrome.webRequest.onHeadersReceived.addListener( function(details) { - if (isPdfDownloadable(details)) + // Check if the response is a PDF file + var isPDF = false; + var headers = details.responseHeaders; + var header, i; + var cdHeader; + if (!headers) + return; + for (i=0; i 0; + break; + } + } + if (!isPDF) return; - var viewerPage = 'content/web/viewer.html'; - var url = chrome.extension.getURL(viewerPage) + - '?file=' + encodeURIComponent(details.url); - return { redirectUrl: url }; + if (isPdfDownloadable(details)) { + // Force download by ensuring that Content-Disposition: attachment is set + if (!cdHeader) { + for (; i' ], - types: ['main_frame'] + types: ['main_frame', 'sub_frame'] }, - ['blocking']); + ['blocking','responseHeaders']); diff --git a/make.js b/make.js index b2a76a90d..78d07538a 100644 --- a/make.js +++ b/make.js @@ -591,6 +591,7 @@ target.chrome = function() { [['extensions/chrome/*.json', 'extensions/chrome/*.html', 'extensions/chrome/*.js', + 'extensions/chrome/*.css', 'extensions/chrome/icon*.png',], CHROME_BUILD_DIR], ['external/webL10n/l10n.js', CHROME_BUILD_CONTENT_DIR + '/web'], @@ -607,6 +608,22 @@ target.chrome = function() { sed('-i', /PDFJSSCRIPT_VERSION/, EXTENSION_VERSION, CHROME_BUILD_DIR + '/manifest.json'); + // Allow PDF.js resources to be loaded by adding the files to + // the "web_accessible_resources" section. + var file_list = ls('-RA', CHROME_BUILD_CONTENT_DIR); + var public_chrome_files = file_list.reduce(function(war, file) { + // Exclude directories (naive: Exclude paths without dot) + if (file.indexOf('.') !== -1) { + // Only add a comma after the first file + if (war) + war += ',\n'; + war += JSON.stringify('content/' + file); + } + return war; + }, ''); + sed('-i', /"content\/\*"/, public_chrome_files, + CHROME_BUILD_DIR + '/manifest.json'); + // Bundle the files to a Chrome extension file .crx if path to key is set var pem = env['PDFJS_CHROME_KEY']; if (!pem) { diff --git a/web/debugger.js b/web/debugger.js index c14ad3fda..2305bb773 100644 --- a/web/debugger.js +++ b/web/debugger.js @@ -46,7 +46,7 @@ var FontInspector = (function FontInspectorClosure() { } } function textLayerClick(e) { - if (!e.target.dataset.fontName || e.target.tagName != 'DIV') + if (!e.target.dataset.fontName || e.target.tagName.toUpperCase() !== 'DIV') return; var fontName = e.target.dataset.fontName; var selects = document.getElementsByTagName('input'); diff --git a/web/viewer.js b/web/viewer.js index 7fc0e670c..bd65b37b0 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -1063,8 +1063,43 @@ var PDFView = { } var url = this.url.split('#')[0]; //#if !(FIREFOX || MOZCENTRAL) + + var a = document.createElement('a'); + + // If _parent == self, then opening an identical URL with different + // location hash will only cause a navigation, not a download. + if (window.top === window && !('download' in a) && + url === window.location.href.split('#')[0]) { + url += url.indexOf('?') === -1 ? '?' : '&'; + } + url += '#pdfjs.action=download'; - window.open(url, '_parent'); + if (a.click) { + // Use a.click() if available. Otherwise, Chrome might show + // "Unsafe JavaScript attempt to initiate a navigation change + // for frame with URL" and not open the PDF at all. + // Supported by (not mentioned = untested): + // - Firefox 6 - 19 (4- does not support a.click, 5 ignores a.click) + // - Chrome 19 - 26 (18- does not support a.click) + // - Opera 9 - 12.15 + // - Internet Explorer 6 - 10 + // - Safari 6 (5.1- does not support a.click) + a.href = url; + a.target = '_parent'; + // Use a.download if available. This increases the likelihood that + // the file is downloaded instead of opened by another PDF plugin. + if ('download' in a) { + var filename = url.match(/([^\/?#=]+\.pdf)/i); + a.download = filename ? filename[1] : 'file.pdf'; + } + // must be in the document for IE and recent Firefox versions. + // (otherwise .click() is ignored) + (document.body || document.documentElement).appendChild(a); + a.click(); + a.parentNode.removeChild(a); + } else { + window.open(url, '_parent'); + } //#else // // Document isn't ready just try to download with the url. // if (!this.pdfDocument) { @@ -3494,9 +3529,9 @@ window.addEventListener('keydown', function keydown(evt) { // Some shortcuts should not get handled if a control/input element // is selected. - var curElement = document.activeElement; - if (curElement && (curElement.tagName == 'INPUT' || - curElement.tagName == 'SELECT')) { + var curElement = document.activeElement || document.querySelector(':focus'); + if (curElement && (curElement.tagName.toUpperCase() === 'INPUT' || + curElement.tagName.toUpperCase() === 'SELECT')) { return; } var controlsElement = document.getElementById('toolbar');