fork from https://github.com/mozilla/pdf.js.git
This commit is contained in:
368
extensions/chromium/pdfHandler.js
Normal file
368
extensions/chromium/pdfHandler.js
Normal file
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
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 priority (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;
|
||||
});
|
||||
Reference in New Issue
Block a user