From e181a3c902485a5c3e155c555abb6d686604457b Mon Sep 17 00:00:00 2001 From: Rob Wu Date: Thu, 4 Apr 2013 00:28:45 +0200 Subject: [PATCH 1/5] Highly improved Chrome extension Full list feature changes in this commit: - Support for iframes - Switched to content-type (MIME) detection instead of hard-coding a case-sensitive check for the .PDF extension - The PDF's original URL is visible in the omnibox - Support for incognito mode Note: PDF viewer is disabled for the file:// + incognito combination, because it's currently impossible to get the combination to work. See https://github.com/mozilla/pdf.js/pull/3017#issuecomment-15693432 --- extensions/chrome/hide-xhtml-error.css | 3 + extensions/chrome/insertviewer.js | 128 +++++++++++++++++++++++++ extensions/chrome/manifest.json | 20 ++-- extensions/chrome/pdfHandler-local.js | 69 +++++++++++++ extensions/chrome/pdfHandler.html | 1 + extensions/chrome/pdfHandler.js | 97 ++++++++++++++++--- make.js | 17 ++++ 7 files changed, 314 insertions(+), 21 deletions(-) create mode 100644 extensions/chrome/hide-xhtml-error.css create mode 100644 extensions/chrome/insertviewer.js create mode 100644 extensions/chrome/pdfHandler-local.js 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..3538c94f2 --- /dev/null +++ b/extensions/chrome/insertviewer.js @@ -0,0 +1,128 @@ +/* -*- 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 = []; + while (x.response.scripts.length) { + var 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..37c3385f4 100644 --- a/extensions/chrome/manifest.json +++ b/extensions/chrome/manifest.json @@ -10,14 +10,20 @@ }, "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": [ + "content/*" + ] } 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) { From b46c3751268ccdc04d5e7bafeaeb9b9f7c8e72b0 Mon Sep 17 00:00:00 2001 From: Rob Wu Date: Sat, 30 Mar 2013 18:12:32 +0100 Subject: [PATCH 2/5] Compatibility with XHTML documents Previously, when the XHTML doctype + header is active, checks would fail because a
's tag name is "div" instead of "DIV". document.activeElement does not exist in Chrome for XHTML documents == -> === --- web/debugger.js | 2 +- web/viewer.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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..4fc521459 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -3494,9 +3494,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'); From 88767e18612b35fa2a8dfe6e00b1e55d26e00829 Mon Sep 17 00:00:00 2001 From: Rob Wu Date: Sun, 24 Mar 2013 18:25:57 +0100 Subject: [PATCH 3/5] Improve download button and behaviour Fix download button for top frames and sub frames When PDF.js is the top frame, and the PDF URL is identical to the top URL, download would fail. Fixed by adding a ? or & in these cases. When PDF.js is embedded in a frame from a different origin, download would fail because window.open(url, '_parent') is ignored. Fixed by using a.click() when available. a.click() works in Chrome 25, Firefox 19, Opera 12.00 and IE 8. Safari 5.1 does not support a.click() Use a.download if available + documentation --- web/viewer.js | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/web/viewer.js b/web/viewer.js index 4fc521459..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) { From 57e6238a6e2c96058e82a4a4acc17ae5cc001bec Mon Sep 17 00:00:00 2001 From: Rob Wu Date: Mon, 1 Apr 2013 16:05:20 +0200 Subject: [PATCH 4/5] Enable Web Worker again in Chrome extension Previously, Web Workers were not created because of a security policy which does not respect relaxed privileges. --- extensions/chrome/insertviewer.js | 12 ++- extensions/chrome/manifest.json | 1 + extensions/chrome/patch-worker.js | 135 ++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 extensions/chrome/patch-worker.js diff --git a/extensions/chrome/insertviewer.js b/extensions/chrome/insertviewer.js index 3538c94f2..9cd0687e8 100644 --- a/extensions/chrome/insertviewer.js +++ b/extensions/chrome/insertviewer.js @@ -60,9 +60,17 @@ function replaceDocumentWithViewer(url) { // Remove all