diff --git a/.puppeteerrc b/.puppeteerrc new file mode 100644 index 000000000..4457c47d7 --- /dev/null +++ b/.puppeteerrc @@ -0,0 +1,9 @@ +{ + "chrome": { + "skipDownload": false + }, + "firefox": { + "skipDownload": false, + "version": "nightly" + } +} diff --git a/.svglintrc.js b/.svglintrc.js new file mode 100644 index 000000000..25dad0276 --- /dev/null +++ b/.svglintrc.js @@ -0,0 +1,21 @@ +export default { + rules: { + valid: true, + + custom: [ + (reporter, $, ast, { filename }) => { + reporter.name = "no-svg-fill-context-fill"; + + const svg = $.find("svg"); + const fill = svg.attr("fill"); + if (fill === "context-fill") { + reporter.error( + "Fill attribute on svg element must not be set to 'context-fill'", + svg[0], + ast + ); + } + }, + ], + }, +}; diff --git a/README.md b/README.md index d47ff5102..23a543599 100644 --- a/README.md +++ b/README.md @@ -137,4 +137,4 @@ Talk to us on Matrix: File an issue: -+ https://github.com/mozilla/pdf.js/issues/new ++ https://github.com/mozilla/pdf.js/issues/new/choose diff --git a/examples/node/pdf2png/pdf2png.mjs b/examples/node/pdf2png/pdf2png.mjs index 3dfd9b089..359fc807b 100644 --- a/examples/node/pdf2png/pdf2png.mjs +++ b/examples/node/pdf2png/pdf2png.mjs @@ -13,41 +13,9 @@ * limitations under the License. */ -import { strict as assert } from "assert"; -import Canvas from "canvas"; import fs from "fs"; import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; -class NodeCanvasFactory { - create(width, height) { - assert(width > 0 && height > 0, "Invalid canvas size"); - const canvas = Canvas.createCanvas(width, height); - const context = canvas.getContext("2d"); - return { - canvas, - context, - }; - } - - reset(canvasAndContext, width, height) { - assert(canvasAndContext.canvas, "Canvas is not specified"); - assert(width > 0 && height > 0, "Invalid canvas size"); - canvasAndContext.canvas.width = width; - canvasAndContext.canvas.height = height; - } - - destroy(canvasAndContext) { - assert(canvasAndContext.canvas, "Canvas is not specified"); - - // Zeroing the width and height cause Firefox to release graphics - // resources immediately, which can greatly reduce memory consumption. - canvasAndContext.canvas.width = 0; - canvasAndContext.canvas.height = 0; - canvasAndContext.canvas = null; - canvasAndContext.context = null; - } -} - // Some PDFs need external cmaps. const CMAP_URL = "../../../node_modules/pdfjs-dist/cmaps/"; const CMAP_PACKED = true; @@ -56,8 +24,6 @@ const CMAP_PACKED = true; const STANDARD_FONT_DATA_URL = "../../../node_modules/pdfjs-dist/standard_fonts/"; -const canvasFactory = new NodeCanvasFactory(); - // Loading file from file system into typed array. const pdfPath = process.argv[2] || "../../../web/compressed.tracemonkey-pldi-09.pdf"; @@ -69,7 +35,6 @@ const loadingTask = getDocument({ cMapUrl: CMAP_URL, cMapPacked: CMAP_PACKED, standardFontDataUrl: STANDARD_FONT_DATA_URL, - canvasFactory, }); try { @@ -78,6 +43,7 @@ try { // Get the first page. const page = await pdfDocument.getPage(1); // Render the page on a Node canvas with 100% scale. + const canvasFactory = pdfDocument.canvasFactory; const viewport = page.getViewport({ scale: 1.0 }); const canvasAndContext = canvasFactory.create( viewport.width, diff --git a/extensions/chromium/.eslintrc b/extensions/chromium/.eslintrc index ba6fdbd4b..dd74b7b7c 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 6e6fa425d..bb448be54 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 aa6edb7b3..83ebf96a2 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 ecb9004d8..5bbb0d1a5 100644 --- a/extensions/chromium/extension-router.js +++ b/extensions/chromium/extension-router.js @@ -17,13 +17,12 @@ 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", "https", - "ftp", "file", "chrome-extension", "blob", @@ -56,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/icon19.png b/extensions/chromium/icon19.png deleted file mode 100644 index 1f67a1288..000000000 Binary files a/extensions/chromium/icon19.png and /dev/null differ diff --git a/extensions/chromium/icon38.png b/extensions/chromium/icon38.png deleted file mode 100644 index 227452fb7..000000000 Binary files a/extensions/chromium/icon38.png and /dev/null differ diff --git a/extensions/chromium/manifest.json b/extensions/chromium/manifest.json index e90ae5700..60c99d9ac 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,61 +10,52 @@ "16": "icon16.png" }, "permissions": [ - "fileBrowserHandler", + "alarms", + "declarativeNetRequestWithHostAccess", "webRequest", - "webRequestBlocking", - "", "tabs", "webNavigation", "storage" ], + "host_permissions": [""], "content_scripts": [ { - "matches": ["http://*/*", "https://*/*", "ftp://*/*", "file://*/*"], + "matches": ["http://*/*", "https://*/*", "file://*/*"], "run_at": "document_start", "all_frames": true, "css": ["contentstyle.css"], "js": ["contentscript.js"] } ], - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", - "file_browser_handlers": [ - { - "id": "open-as-pdf", - "default_title": "Open with PDF Viewer", - "file_filters": ["filesystem:*.pdf"] - } - ], + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" + }, "storage": { "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" - }, - "page_action": { - "default_icon": { - "19": "icon19.png", - "38": "icon38.png" - }, - "default_title": "Show PDF URL", - "default_popup": "pageActionPopup.html" + "service_worker": "background.js" }, "incognito": "split", "web_accessible_resources": [ - "content/web/viewer.html", - "http:/*", - "https:/*", - "ftp:/*", - "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 dd8fb6ef7..9b084e45a 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 78385926f..bd18c2456 100644 --- a/extensions/chromium/options/options.html +++ b/extensions/chromium/options/options.html @@ -19,13 +19,19 @@ limitations under the License. PDF.js viewer options @@ -34,8 +40,7 @@ body {