From e181a3c902485a5c3e155c555abb6d686604457b Mon Sep 17 00:00:00 2001 From: Rob Wu Date: Thu, 4 Apr 2013 00:28:45 +0200 Subject: [PATCH] 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) {