diff --git a/extensions/chromium/.eslintrc b/extensions/chromium/.eslintrc index ba6fdbd4bf4ce..dd74b7b7c64ab 100644 --- a/extensions/chromium/.eslintrc +++ b/extensions/chromium/.eslintrc @@ -14,4 +14,23 @@ "rules": { "no-var": "off", }, + + "overrides": [ + { + // Include all files referenced in background.js + "files": [ + "options/migration.js", + "preserve-referer.js", + "pdfHandler.js", + "extension-router.js", + "suppress-update.js", + "telemetry.js" + ], + "env": { + // Background script is a service worker. + "browser": false, + "serviceworker": true + } + } + ] } diff --git a/extensions/chromium/restoretab.html b/extensions/chromium/background.js similarity index 71% rename from extensions/chromium/restoretab.html rename to extensions/chromium/background.js index 6e6fa425dd559..bb448be549c0c 100644 --- a/extensions/chromium/restoretab.html +++ b/extensions/chromium/background.js @@ -1,6 +1,5 @@ - - - +*/ + +"use strict"; + +importScripts( + "options/migration.js", + "preserve-referer.js", + "pdfHandler.js", + "extension-router.js", + "suppress-update.js", + "telemetry.js" +); diff --git a/extensions/chromium/contentscript.js b/extensions/chromium/contentscript.js index aa6edb7b3f3aa..83ebf96a225c7 100644 --- a/extensions/chromium/contentscript.js +++ b/extensions/chromium/contentscript.js @@ -16,13 +16,16 @@ limitations under the License. "use strict"; -var VIEWER_URL = chrome.extension.getURL("content/web/viewer.html"); +var VIEWER_URL = chrome.runtime.getURL("content/web/viewer.html"); function getViewerURL(pdf_url) { return VIEWER_URL + "?file=" + encodeURIComponent(pdf_url); } document.addEventListener("animationstart", onAnimationStart, true); +if (document.contentType === "application/pdf") { + chrome.runtime.sendMessage({ action: "canRequestBody" }, maybeRenderPdfDoc); +} function onAnimationStart(event) { if (event.animationName === "pdfjs-detected-object-or-embed") { @@ -221,3 +224,38 @@ function getEmbeddedViewerURL(path) { path = a.href; return getViewerURL(path) + fragment; } + +function maybeRenderPdfDoc(isNotPOST) { + if (!isNotPOST) { + // The document was loaded through a POST request, but we cannot access the + // original response body, nor safely send a new request to fetch the PDF. + // Until #4483 is fixed, POST requests should be ignored. + return; + } + + // Detected PDF that was not redirected by the declarativeNetRequest rules. + // Maybe because this was served without Content-Type and sniffed as PDF. + // Or because this is Chrome 127-, which does not support responseHeaders + // condition in declarativeNetRequest (DNR), and PDF requests are therefore + // not redirected via DNR. + + // In any case, load the viewer. + console.log(`Detected PDF via document, opening viewer for ${document.URL}`); + + // Ideally we would use logic consistent with the DNR logic, like this: + // location.href = getEmbeddedViewerURL(document.URL); + // ... unfortunately, this causes Chrome to crash until version 129, fixed by + // https://chromium.googlesource.com/chromium/src/+/8c42358b2cc549553d939efe7d36515d80563da7%5E%21/ + // Work around this by replacing the body with an iframe of the viewer. + // Interestingly, Chrome's built-in PDF viewer uses a similar technique. + const shadowRoot = document.body.attachShadow({ mode: "closed" }); + const iframe = document.createElement("iframe"); + iframe.style.position = "absolute"; + iframe.style.top = "0"; + iframe.style.left = "0"; + iframe.style.width = "100%"; + iframe.style.height = "100%"; + iframe.style.border = "0 none"; + iframe.src = getEmbeddedViewerURL(document.URL); + shadowRoot.append(iframe); +} diff --git a/extensions/chromium/extension-router.js b/extensions/chromium/extension-router.js index a128a30d111e0..5bbb0d1a580c7 100644 --- a/extensions/chromium/extension-router.js +++ b/extensions/chromium/extension-router.js @@ -17,8 +17,8 @@ limitations under the License. "use strict"; (function ExtensionRouterClosure() { - var VIEWER_URL = chrome.extension.getURL("content/web/viewer.html"); - var CRX_BASE_URL = chrome.extension.getURL("/"); + var VIEWER_URL = chrome.runtime.getURL("content/web/viewer.html"); + var CRX_BASE_URL = chrome.runtime.getURL("/"); var schemes = [ "http", @@ -55,73 +55,50 @@ limitations under the License. return undefined; } - // TODO(rob): Use declarativeWebRequest once declared URL-encoding is - // supported, see http://crbug.com/273589 - // (or rewrite the query string parser in viewer.js to get it to - // recognize the non-URL-encoded PDF URL.) - chrome.webRequest.onBeforeRequest.addListener( - function (details) { + function resolveViewerURL(originalUrl) { + if (originalUrl.startsWith(CRX_BASE_URL)) { // This listener converts chrome-extension://.../http://...pdf to // chrome-extension://.../content/web/viewer.html?file=http%3A%2F%2F...pdf - var url = parseExtensionURL(details.url); + var url = parseExtensionURL(originalUrl); if (url) { url = VIEWER_URL + "?file=" + url; - var i = details.url.indexOf("#"); + var i = originalUrl.indexOf("#"); if (i > 0) { - url += details.url.slice(i); + url += originalUrl.slice(i); } - console.log("Redirecting " + details.url + " to " + url); - return { redirectUrl: url }; - } - return undefined; - }, - { - types: ["main_frame", "sub_frame"], - urls: schemes.map(function (scheme) { - // Format: "chrome-extension://[EXTENSIONID]/*" - return CRX_BASE_URL + scheme + "*"; - }), - }, - ["blocking"] - ); - - // When session restore is used, viewer pages may be loaded before the - // webRequest event listener is attached (= page not found). - // Or the extension could have been crashed (OOM), leaving a sad tab behind. - // Reload these tabs. - chrome.tabs.query( - { - url: CRX_BASE_URL + "*:*", - }, - function (tabsFromLastSession) { - for (const { id } of tabsFromLastSession) { - chrome.tabs.reload(id); + return url; } } - ); - console.log("Set up extension URL router."); + return undefined; + } - Object.keys(localStorage).forEach(function (key) { - // The localStorage item is set upon unload by chromecom.js. - var parsedKey = /^unload-(\d+)-(true|false)-(.+)/.exec(key); - if (parsedKey) { - var timeStart = parseInt(parsedKey[1], 10); - var isHidden = parsedKey[2] === "true"; - var url = parsedKey[3]; - if (Date.now() - timeStart < 3000) { - // Is it a new item (younger than 3 seconds)? Assume that the extension - // just reloaded, so restore the tab (work-around for crbug.com/511670). - chrome.tabs.create({ - url: - chrome.runtime.getURL("restoretab.html") + - "?" + - encodeURIComponent(url) + - "#" + - encodeURIComponent(localStorage.getItem(key)), - active: !isHidden, - }); + self.addEventListener("fetch", event => { + const req = event.request; + if (req.destination === "document") { + var url = resolveViewerURL(req.url); + if (url) { + console.log("Redirecting " + req.url + " to " + url); + event.respondWith(Response.redirect(url)); } - localStorage.removeItem(key); } }); + + // Ctrl + F5 bypasses service worker. the pretty extension URLs will fail to + // resolve in that case. Catch this and redirect to destination. + chrome.webNavigation.onErrorOccurred.addListener( + details => { + if (details.frameId !== 0) { + // Not a top-level frame. Cannot easily navigate a specific child frame. + return; + } + const url = resolveViewerURL(details.url); + if (url) { + console.log(`Redirecting ${details.url} to ${url} (fallback)`); + chrome.tabs.update(details.tabId, { url }); + } + }, + { url: [{ urlPrefix: CRX_BASE_URL }] } + ); + + console.log("Set up extension URL router."); })(); diff --git a/extensions/chromium/manifest.json b/extensions/chromium/manifest.json index 77c39c7770226..27be0ea96ac9f 100644 --- a/extensions/chromium/manifest.json +++ b/extensions/chromium/manifest.json @@ -1,6 +1,6 @@ { - "minimum_chrome_version": "88", - "manifest_version": 2, + "minimum_chrome_version": "103", + "manifest_version": 3, "name": "PDF Viewer", "version": "PDFJSSCRIPT_VERSION", "description": "Uses HTML5 to display PDF files directly in the browser.", @@ -10,13 +10,14 @@ "16": "icon16.png" }, "permissions": [ + "alarms", + "declarativeNetRequestWithHostAccess", "webRequest", - "webRequestBlocking", - "", "tabs", "webNavigation", "storage" ], + "host_permissions": [""], "content_scripts": [ { "matches": ["http://*/*", "https://*/*", "file://*/*"], @@ -30,23 +31,28 @@ "managed_schema": "preferences_schema.json" }, "options_ui": { - "page": "options/options.html", - "chrome_style": true + "page": "options/options.html" }, "options_page": "options/options.html", "background": { - "page": "pdfHandler.html" + "service_worker": "background.js" }, "incognito": "split", "web_accessible_resources": [ - "content/web/viewer.html", - "http:/*", - "https:/*", - "file:/*", - "chrome-extension:/*", - "blob:*", - "data:*", - "filesystem:/*", - "drive:*" + { + "resources": [ + "content/web/viewer.html", + "http:/*", + "https:/*", + "file:/*", + "chrome-extension:/*", + "blob:*", + "data:*", + "filesystem:/*", + "drive:*" + ], + "matches": [""], + "extension_ids": ["*"] + } ] } diff --git a/extensions/chromium/options/migration.js b/extensions/chromium/options/migration.js index dd8fb6ef73d1a..9b084e45ae3f9 100644 --- a/extensions/chromium/options/migration.js +++ b/extensions/chromium/options/migration.js @@ -13,10 +13,14 @@ 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 strict: ["error", "function"] */ +"use strict"; -(function () { - "use strict"; +chrome.runtime.onInstalled.addListener(({ reason }) => { + if (reason !== "update") { + // We only need to run migration logic for extension updates, not for new + // installs or browser updates. + return; + } var storageLocal = chrome.storage.local; var storageSync = chrome.storage.sync; @@ -37,16 +41,12 @@ limitations under the License. }); }); - function getStorageNames(callback) { - var x = new XMLHttpRequest(); + async function getStorageNames(callback) { var schema_location = chrome.runtime.getManifest().storage.managed_schema; - x.open("get", chrome.runtime.getURL(schema_location)); - x.onload = function () { - var storageKeys = Object.keys(x.response.properties); - callback(storageKeys); - }; - x.responseType = "json"; - x.send(); + var res = await fetch(chrome.runtime.getURL(schema_location)); + var storageManifest = await res.json(); + var storageKeys = Object.keys(storageManifest.properties); + callback(storageKeys); } // Save |values| to storage.sync and delete the values with that key from @@ -150,4 +150,4 @@ limitations under the License. } ); } -})(); +}); diff --git a/extensions/chromium/options/options.html b/extensions/chromium/options/options.html index 78385926fc98f..bd18c2456465f 100644 --- a/extensions/chromium/options/options.html +++ b/extensions/chromium/options/options.html @@ -19,13 +19,19 @@ PDF.js viewer options @@ -34,8 +40,7 @@