mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-18 14:18:23 +02:00
Fix regression from #18711. `urlFilter` is a key of `condition`, not of the `rule`. Consequently, the feature detection method failed to detect the availability of the feature in Chrome 128+.
368 lines
13 KiB
JavaScript
368 lines
13 KiB
JavaScript
/*
|
|
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 canRequestBody */ // From preserve-referer.js
|
|
|
|
"use strict";
|
|
|
|
var VIEWER_URL = chrome.runtime.getURL("content/web/viewer.html");
|
|
|
|
// Use in-memory storage to ensure that the DNR rules have been registered at
|
|
// least once per session. runtime.onInstalled would have been the most fitting
|
|
// event to ensure that, except there are cases where it does not fire when
|
|
// needed. E.g. in incognito mode: https://issues.chromium.org/issues/41029550
|
|
chrome.storage.session.get({ hasPdfRedirector: false }, async items => {
|
|
if (items?.hasPdfRedirector) {
|
|
return;
|
|
}
|
|
const rules = await chrome.declarativeNetRequest.getDynamicRules();
|
|
if (rules.length) {
|
|
// Dynamic rules persist across extension updates. We don't expect other
|
|
// dynamic rules, so just remove them all.
|
|
await chrome.declarativeNetRequest.updateDynamicRules({
|
|
removeRuleIds: rules.map(r => r.id),
|
|
});
|
|
}
|
|
await registerPdfRedirectRule();
|
|
|
|
// Only set the flag in the end, so that we know for sure that all
|
|
// asynchronous initialization logic has run. If not, then we will run the
|
|
// logic again at the next background wakeup.
|
|
chrome.storage.session.set({ hasPdfRedirector: true });
|
|
});
|
|
|
|
/**
|
|
* Registers declarativeNetRequest rules to redirect PDF requests to the viewer.
|
|
* The caller should clear any previously existing dynamic DNR rules.
|
|
*
|
|
* The logic here is the declarative version of the runtime logic in the
|
|
* webRequest.onHeadersReceived implementation at
|
|
* https://github.com/mozilla/pdf.js/blob/0676ea19cf17023ec8c2d6ad69a859c345c01dc1/extensions/chromium/pdfHandler.js#L34-L152
|
|
*/
|
|
async function registerPdfRedirectRule() {
|
|
// "allow" means to ignore rules (from this extension) with lower priority.
|
|
const ACTION_IGNORE_OTHER_RULES = { type: "allow" };
|
|
|
|
// Redirect to viewer. The rule condition is expected to specify regexFilter
|
|
// that matches the full request URL.
|
|
const ACTION_REDIRECT_TO_VIEWER = {
|
|
type: "redirect",
|
|
redirect: {
|
|
// DNR does not support transformations such as encodeURIComponent on the
|
|
// match, so we just concatenate the URL as is without modifications.
|
|
// TODO: use "?file=\\0" when DNR supports transformations as proposed at
|
|
// https://github.com/w3c/webextensions/issues/636#issuecomment-2165978322
|
|
regexSubstitution: VIEWER_URL + "?DNR:\\0",
|
|
},
|
|
};
|
|
|
|
// Rules in order of prority (highest priority rule first).
|
|
// The required "id" fields will be auto-generated later.
|
|
const addRules = [
|
|
{
|
|
// Do not redirect for URLs containing pdfjs.action=download.
|
|
condition: {
|
|
urlFilter: "pdfjs.action=download",
|
|
resourceTypes: ["main_frame", "sub_frame"],
|
|
},
|
|
action: ACTION_IGNORE_OTHER_RULES,
|
|
},
|
|
{
|
|
// Redirect local PDF files if isAllowedFileSchemeAccess is true. No-op
|
|
// otherwise and then handled by webNavigation.onBeforeNavigate below.
|
|
condition: {
|
|
regexFilter: "^file://.*\\.pdf$",
|
|
resourceTypes: ["main_frame", "sub_frame"],
|
|
},
|
|
action: ACTION_REDIRECT_TO_VIEWER,
|
|
},
|
|
{
|
|
// Respect the Content-Disposition:attachment header in sub_frame. But:
|
|
// Display the PDF viewer regardless of the Content-Disposition header if
|
|
// the file is displayed in the main frame, since most often users want to
|
|
// view a PDF, and servers are often misconfigured.
|
|
condition: {
|
|
urlFilter: "*",
|
|
resourceTypes: ["sub_frame"], // Note: no main_frame, handled below.
|
|
responseHeaders: [
|
|
{
|
|
header: "content-disposition",
|
|
values: ["attachment*"],
|
|
},
|
|
],
|
|
},
|
|
action: ACTION_IGNORE_OTHER_RULES,
|
|
},
|
|
{
|
|
// If the query string contains "=download", do not unconditionally force
|
|
// viewer to open the PDF, but first check whether the Content-Disposition
|
|
// header specifies an attachment. This allows sites like Google Drive to
|
|
// operate correctly (#6106).
|
|
condition: {
|
|
urlFilter: "=download",
|
|
resourceTypes: ["main_frame"], // No sub_frame, was handled before.
|
|
responseHeaders: [
|
|
{
|
|
header: "content-disposition",
|
|
values: ["attachment*"],
|
|
},
|
|
],
|
|
},
|
|
action: ACTION_IGNORE_OTHER_RULES,
|
|
},
|
|
{
|
|
// Regular http(s) PDF requests.
|
|
condition: {
|
|
regexFilter: "^.*$",
|
|
// The viewer does not have the original request context and issues a
|
|
// GET request. The original response to POST requests is unavailable.
|
|
excludedRequestMethods: ["post"],
|
|
resourceTypes: ["main_frame", "sub_frame"],
|
|
responseHeaders: [
|
|
{
|
|
header: "content-type",
|
|
values: ["application/pdf", "application/pdf;*"],
|
|
},
|
|
],
|
|
},
|
|
action: ACTION_REDIRECT_TO_VIEWER,
|
|
},
|
|
{
|
|
// Wrong MIME-type, but a PDF file according to the file name in the URL.
|
|
condition: {
|
|
regexFilter: "^.*\\.pdf\\b.*$",
|
|
// The viewer does not have the original request context and issues a
|
|
// GET request. The original response to POST requests is unavailable.
|
|
excludedRequestMethods: ["post"],
|
|
resourceTypes: ["main_frame", "sub_frame"],
|
|
responseHeaders: [
|
|
{
|
|
header: "content-type",
|
|
values: ["application/octet-stream", "application/octet-stream;*"],
|
|
},
|
|
],
|
|
},
|
|
action: ACTION_REDIRECT_TO_VIEWER,
|
|
},
|
|
{
|
|
// Wrong MIME-type, but a PDF file according to Content-Disposition.
|
|
condition: {
|
|
regexFilter: "^.*$",
|
|
// The viewer does not have the original request context and issues a
|
|
// GET request. The original response to POST requests is unavailable.
|
|
excludedRequestMethods: ["post"],
|
|
resourceTypes: ["main_frame", "sub_frame"],
|
|
responseHeaders: [
|
|
{
|
|
header: "content-disposition",
|
|
values: ["*.pdf", '*.pdf"*', "*.pdf'*"],
|
|
},
|
|
],
|
|
// We only want to match by content-disposition if Content-Type is set
|
|
// to application/octet-stream. The responseHeaders condition is a
|
|
// logical OR instead of AND, so to simulate the AND condition we use
|
|
// the double negation of excludedResponseHeaders + excludedValues.
|
|
// This matches any request whose content-type header is set and not
|
|
// "application/octet-stream". It will also match if "content-type" is
|
|
// not set, but we are okay with that since the browser would usually
|
|
// try to sniff the MIME type in that case.
|
|
excludedResponseHeaders: [
|
|
{
|
|
header: "content-type",
|
|
excludedValues: [
|
|
"application/octet-stream",
|
|
"application/octet-stream;*",
|
|
],
|
|
},
|
|
],
|
|
},
|
|
action: ACTION_REDIRECT_TO_VIEWER,
|
|
},
|
|
];
|
|
for (const [i, rule] of addRules.entries()) {
|
|
// id must be unique and at least 1, but i starts at 0. So add +1.
|
|
rule.id = i + 1;
|
|
rule.priority = addRules.length - i;
|
|
}
|
|
try {
|
|
// Note: condition.responseHeaders is only supported in Chrome 128+, but
|
|
// does not trigger errors in Chrome 123 - 127 as explained at:
|
|
// https://github.com/w3c/webextensions/issues/638#issuecomment-2181124486
|
|
// We need to detect this and avoid registering rules, because otherwise all
|
|
// requests are redirected to the viewer instead of just PDF requests,
|
|
// because Chrome accepts rules while ignoring the responseHeaders condition
|
|
// - also reported at https://crbug.com/347186592
|
|
if (!(await isHeaderConditionSupported())) {
|
|
throw new Error("DNR responseHeaders condition is not supported.");
|
|
}
|
|
await chrome.declarativeNetRequest.updateDynamicRules({ addRules });
|
|
} catch (e) {
|
|
// When we do not register DNR rules for any reason, fall back to catching
|
|
// PDF documents via maybeRenderPdfDoc in contentscript.js.
|
|
console.error("Failed to register rules to redirect PDF requests.");
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
// For the source and explanation of this logic, see
|
|
// https://github.com/w3c/webextensions/issues/638#issuecomment-2181124486
|
|
async function isHeaderConditionSupported() {
|
|
const ruleId = 123456; // Some rule ID that is not already used elsewhere.
|
|
try {
|
|
// Throws synchronously if not supported.
|
|
await chrome.declarativeNetRequest.updateSessionRules({
|
|
addRules: [
|
|
{
|
|
id: ruleId,
|
|
condition: {
|
|
responseHeaders: [{ header: "whatever" }],
|
|
urlFilter: "|does_not_match_anything",
|
|
},
|
|
action: { type: "block" },
|
|
},
|
|
],
|
|
});
|
|
} catch {
|
|
return false; // responseHeaders condition not supported.
|
|
}
|
|
// Chrome may recognize the properties but have the implementation behind a
|
|
// flag. When the implementation is disabled, validation is skipped too.
|
|
try {
|
|
await chrome.declarativeNetRequest.updateSessionRules({
|
|
removeRuleIds: [ruleId],
|
|
addRules: [
|
|
{
|
|
id: ruleId,
|
|
condition: {
|
|
responseHeaders: [],
|
|
urlFilter: "|does_not_match_anything",
|
|
},
|
|
action: { type: "block" },
|
|
},
|
|
],
|
|
});
|
|
return false; // Validation skipped = feature disabled.
|
|
} catch {
|
|
return true; // Validation worked = feature enabled.
|
|
} finally {
|
|
await chrome.declarativeNetRequest.updateSessionRules({
|
|
removeRuleIds: [ruleId],
|
|
});
|
|
}
|
|
}
|
|
|
|
function getViewerURL(pdf_url) {
|
|
// |pdf_url| may contain a fragment such as "#page=2". That should be passed
|
|
// as a fragment to the viewer, not encoded in pdf_url.
|
|
var hash = "";
|
|
var i = pdf_url.indexOf("#");
|
|
if (i > 0) {
|
|
hash = pdf_url.slice(i);
|
|
pdf_url = pdf_url.slice(0, i);
|
|
}
|
|
return VIEWER_URL + "?file=" + encodeURIComponent(pdf_url) + hash;
|
|
}
|
|
|
|
// If the user has not granted access to file:-URLs, then declarativeNetRequest
|
|
// will not catch the request. It is still visible through the webNavigation
|
|
// API though, and we can replace the tab with the viewer.
|
|
// The viewer will detect that it has no access to file:-URLs, and prompt the
|
|
// user to activate file permissions.
|
|
chrome.webNavigation.onBeforeNavigate.addListener(
|
|
function (details) {
|
|
// Note: pdfjs.action=download is not checked here because that code path
|
|
// is not reachable for local files through the viewer when we do not have
|
|
// file:-access.
|
|
if (details.frameId === 0) {
|
|
chrome.extension.isAllowedFileSchemeAccess(function (isAllowedAccess) {
|
|
if (isAllowedAccess) {
|
|
// Expected to be handled by DNR. Don't do anything.
|
|
return;
|
|
}
|
|
|
|
chrome.tabs.update(details.tabId, {
|
|
url: getViewerURL(details.url),
|
|
});
|
|
});
|
|
}
|
|
},
|
|
{
|
|
url: [
|
|
{
|
|
urlPrefix: "file://",
|
|
pathSuffix: ".pdf",
|
|
},
|
|
{
|
|
urlPrefix: "file://",
|
|
pathSuffix: ".PDF",
|
|
},
|
|
],
|
|
}
|
|
);
|
|
|
|
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
|
|
if (message && message.action === "getParentOrigin") {
|
|
// getParentOrigin is used to determine whether it is safe to embed a
|
|
// sensitive (local) file in a frame.
|
|
if (!sender.tab) {
|
|
sendResponse("");
|
|
return undefined;
|
|
}
|
|
// TODO: This should be the URL of the parent frame, not the tab. But
|
|
// chrome-extension:-URLs are not visible in the webNavigation API
|
|
// (https://crbug.com/326768), so the next best thing is using the tab's URL
|
|
// for making security decisions.
|
|
var parentUrl = sender.tab.url;
|
|
if (!parentUrl) {
|
|
sendResponse("");
|
|
return undefined;
|
|
}
|
|
if (parentUrl.lastIndexOf("file:", 0) === 0) {
|
|
sendResponse("file://");
|
|
return undefined;
|
|
}
|
|
// The regexp should always match for valid URLs, but in case it doesn't,
|
|
// just give the full URL (e.g. data URLs).
|
|
var origin = /^[^:]+:\/\/[^/]+/.exec(parentUrl);
|
|
sendResponse(origin ? origin[1] : parentUrl);
|
|
return true;
|
|
}
|
|
if (message && message.action === "isAllowedFileSchemeAccess") {
|
|
chrome.extension.isAllowedFileSchemeAccess(sendResponse);
|
|
return true;
|
|
}
|
|
if (message && message.action === "openExtensionsPageForFileAccess") {
|
|
var url = "chrome://extensions/?id=" + chrome.runtime.id;
|
|
if (message.data.newTab) {
|
|
chrome.tabs.create({
|
|
windowId: sender.tab.windowId,
|
|
index: sender.tab.index + 1,
|
|
url,
|
|
openerTabId: sender.tab.id,
|
|
});
|
|
} else {
|
|
chrome.tabs.update(sender.tab.id, {
|
|
url,
|
|
});
|
|
}
|
|
return undefined;
|
|
}
|
|
if (message && message.action === "canRequestBody") {
|
|
sendResponse(canRequestBody(sender.tab.id, sender.frameId));
|
|
return undefined;
|
|
}
|
|
return undefined;
|
|
});
|