Merge pull request #18681 from Rob--W/crx-mv3-migration
[CRX] Migrate Chrome extension to Manifest Version 3
This commit is contained in:
commit
a1b45d6e69
@ -14,4 +14,23 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"no-var": "off",
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
<!doctype html>
|
/*
|
||||||
<!--
|
Copyright 2024 Mozilla Foundation
|
||||||
Copyright 2015 Mozilla Foundation
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -13,5 +12,15 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
|||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
*/
|
||||||
<script src="restoretab.js"></script>
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
importScripts(
|
||||||
|
"options/migration.js",
|
||||||
|
"preserve-referer.js",
|
||||||
|
"pdfHandler.js",
|
||||||
|
"extension-router.js",
|
||||||
|
"suppress-update.js",
|
||||||
|
"telemetry.js"
|
||||||
|
);
|
||||||
@ -16,13 +16,16 @@ limitations under the License.
|
|||||||
|
|
||||||
"use strict";
|
"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) {
|
function getViewerURL(pdf_url) {
|
||||||
return VIEWER_URL + "?file=" + encodeURIComponent(pdf_url);
|
return VIEWER_URL + "?file=" + encodeURIComponent(pdf_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("animationstart", onAnimationStart, true);
|
document.addEventListener("animationstart", onAnimationStart, true);
|
||||||
|
if (document.contentType === "application/pdf") {
|
||||||
|
chrome.runtime.sendMessage({ action: "canRequestBody" }, maybeRenderPdfDoc);
|
||||||
|
}
|
||||||
|
|
||||||
function onAnimationStart(event) {
|
function onAnimationStart(event) {
|
||||||
if (event.animationName === "pdfjs-detected-object-or-embed") {
|
if (event.animationName === "pdfjs-detected-object-or-embed") {
|
||||||
@ -221,3 +224,38 @@ function getEmbeddedViewerURL(path) {
|
|||||||
path = a.href;
|
path = a.href;
|
||||||
return getViewerURL(path) + fragment;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -17,8 +17,8 @@ limitations under the License.
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
(function ExtensionRouterClosure() {
|
(function ExtensionRouterClosure() {
|
||||||
var VIEWER_URL = chrome.extension.getURL("content/web/viewer.html");
|
var VIEWER_URL = chrome.runtime.getURL("content/web/viewer.html");
|
||||||
var CRX_BASE_URL = chrome.extension.getURL("/");
|
var CRX_BASE_URL = chrome.runtime.getURL("/");
|
||||||
|
|
||||||
var schemes = [
|
var schemes = [
|
||||||
"http",
|
"http",
|
||||||
@ -55,73 +55,50 @@ limitations under the License.
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(rob): Use declarativeWebRequest once declared URL-encoding is
|
function resolveViewerURL(originalUrl) {
|
||||||
// supported, see http://crbug.com/273589
|
if (originalUrl.startsWith(CRX_BASE_URL)) {
|
||||||
// (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) {
|
|
||||||
// This listener converts chrome-extension://.../http://...pdf to
|
// This listener converts chrome-extension://.../http://...pdf to
|
||||||
// chrome-extension://.../content/web/viewer.html?file=http%3A%2F%2F...pdf
|
// chrome-extension://.../content/web/viewer.html?file=http%3A%2F%2F...pdf
|
||||||
var url = parseExtensionURL(details.url);
|
var url = parseExtensionURL(originalUrl);
|
||||||
if (url) {
|
if (url) {
|
||||||
url = VIEWER_URL + "?file=" + url;
|
url = VIEWER_URL + "?file=" + url;
|
||||||
var i = details.url.indexOf("#");
|
var i = originalUrl.indexOf("#");
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
url += details.url.slice(i);
|
url += originalUrl.slice(i);
|
||||||
}
|
}
|
||||||
console.log("Redirecting " + details.url + " to " + url);
|
return url;
|
||||||
return { redirectUrl: url };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
types: ["main_frame", "sub_frame"],
|
|
||||||
urls: schemes.map(function (scheme) {
|
|
||||||
// Format: "chrome-extension://[EXTENSIONID]/<scheme>*"
|
|
||||||
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 undefined;
|
||||||
console.log("Set up extension URL router.");
|
}
|
||||||
|
|
||||||
Object.keys(localStorage).forEach(function (key) {
|
self.addEventListener("fetch", event => {
|
||||||
// The localStorage item is set upon unload by chromecom.js.
|
const req = event.request;
|
||||||
var parsedKey = /^unload-(\d+)-(true|false)-(.+)/.exec(key);
|
if (req.destination === "document") {
|
||||||
if (parsedKey) {
|
var url = resolveViewerURL(req.url);
|
||||||
var timeStart = parseInt(parsedKey[1], 10);
|
if (url) {
|
||||||
var isHidden = parsedKey[2] === "true";
|
console.log("Redirecting " + req.url + " to " + url);
|
||||||
var url = parsedKey[3];
|
event.respondWith(Response.redirect(url));
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
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.");
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"minimum_chrome_version": "88",
|
"minimum_chrome_version": "103",
|
||||||
"manifest_version": 2,
|
"manifest_version": 3,
|
||||||
"name": "PDF Viewer",
|
"name": "PDF Viewer",
|
||||||
"version": "PDFJSSCRIPT_VERSION",
|
"version": "PDFJSSCRIPT_VERSION",
|
||||||
"description": "Uses HTML5 to display PDF files directly in the browser.",
|
"description": "Uses HTML5 to display PDF files directly in the browser.",
|
||||||
@ -10,13 +10,14 @@
|
|||||||
"16": "icon16.png"
|
"16": "icon16.png"
|
||||||
},
|
},
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
"alarms",
|
||||||
|
"declarativeNetRequestWithHostAccess",
|
||||||
"webRequest",
|
"webRequest",
|
||||||
"webRequestBlocking",
|
|
||||||
"<all_urls>",
|
|
||||||
"tabs",
|
"tabs",
|
||||||
"webNavigation",
|
"webNavigation",
|
||||||
"storage"
|
"storage"
|
||||||
],
|
],
|
||||||
|
"host_permissions": ["<all_urls>"],
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["http://*/*", "https://*/*", "file://*/*"],
|
"matches": ["http://*/*", "https://*/*", "file://*/*"],
|
||||||
@ -30,23 +31,28 @@
|
|||||||
"managed_schema": "preferences_schema.json"
|
"managed_schema": "preferences_schema.json"
|
||||||
},
|
},
|
||||||
"options_ui": {
|
"options_ui": {
|
||||||
"page": "options/options.html",
|
"page": "options/options.html"
|
||||||
"chrome_style": true
|
|
||||||
},
|
},
|
||||||
"options_page": "options/options.html",
|
"options_page": "options/options.html",
|
||||||
"background": {
|
"background": {
|
||||||
"page": "pdfHandler.html"
|
"service_worker": "background.js"
|
||||||
},
|
},
|
||||||
"incognito": "split",
|
"incognito": "split",
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
"content/web/viewer.html",
|
{
|
||||||
"http:/*",
|
"resources": [
|
||||||
"https:/*",
|
"content/web/viewer.html",
|
||||||
"file:/*",
|
"http:/*",
|
||||||
"chrome-extension:/*",
|
"https:/*",
|
||||||
"blob:*",
|
"file:/*",
|
||||||
"data:*",
|
"chrome-extension:/*",
|
||||||
"filesystem:/*",
|
"blob:*",
|
||||||
"drive:*"
|
"data:*",
|
||||||
|
"filesystem:/*",
|
||||||
|
"drive:*"
|
||||||
|
],
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"extension_ids": ["*"]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
/* eslint strict: ["error", "function"] */
|
"use strict";
|
||||||
|
|
||||||
(function () {
|
chrome.runtime.onInstalled.addListener(({ reason }) => {
|
||||||
"use strict";
|
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 storageLocal = chrome.storage.local;
|
||||||
var storageSync = chrome.storage.sync;
|
var storageSync = chrome.storage.sync;
|
||||||
|
|
||||||
@ -37,16 +41,12 @@ limitations under the License.
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getStorageNames(callback) {
|
async function getStorageNames(callback) {
|
||||||
var x = new XMLHttpRequest();
|
|
||||||
var schema_location = chrome.runtime.getManifest().storage.managed_schema;
|
var schema_location = chrome.runtime.getManifest().storage.managed_schema;
|
||||||
x.open("get", chrome.runtime.getURL(schema_location));
|
var res = await fetch(chrome.runtime.getURL(schema_location));
|
||||||
x.onload = function () {
|
var storageManifest = await res.json();
|
||||||
var storageKeys = Object.keys(x.response.properties);
|
var storageKeys = Object.keys(storageManifest.properties);
|
||||||
callback(storageKeys);
|
callback(storageKeys);
|
||||||
};
|
|
||||||
x.responseType = "json";
|
|
||||||
x.send();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save |values| to storage.sync and delete the values with that key from
|
// Save |values| to storage.sync and delete the values with that key from
|
||||||
@ -150,4 +150,4 @@ limitations under the License.
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})();
|
});
|
||||||
|
|||||||
@ -19,13 +19,19 @@ limitations under the License.
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>PDF.js viewer options</title>
|
<title>PDF.js viewer options</title>
|
||||||
<style>
|
<style>
|
||||||
/* TODO: Remove as much custom CSS as possible - crbug.com/446511 */
|
|
||||||
body {
|
body {
|
||||||
min-width: 400px; /* a page at the settings page is at least 400px wide */
|
min-width: 400px; /* a page at the settings page is at least 400px wide */
|
||||||
margin: 14px 17px; /* already added by default in Chrome 40.0.2212.0 */
|
margin: 14px 17px; /* already added by default in Chrome 40.0.2212.0 */
|
||||||
}
|
}
|
||||||
.settings-row {
|
.settings-row {
|
||||||
margin: 0.65em 0;
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
.checkbox label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.checkbox label input {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -34,8 +40,7 @@ body {
|
|||||||
<button id="reset-button" type="button">Restore default settings</button>
|
<button id="reset-button" type="button">Restore default settings</button>
|
||||||
|
|
||||||
<template id="checkbox-template">
|
<template id="checkbox-template">
|
||||||
<!-- Chromium's style: //src/extensions/renderer/resources/extension.css -->
|
<div class="settings-row checkbox">
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<span></span>
|
<span></span>
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
-->
|
|
||||||
<script src="options/migration.js"></script>
|
|
||||||
<script src="preserve-referer.js"></script>
|
|
||||||
<script src="pdfHandler.js"></script>
|
|
||||||
<script src="extension-router.js"></script>
|
|
||||||
<script src="suppress-update.js"></script>
|
|
||||||
<script src="telemetry.js"></script>
|
|
||||||
@ -13,11 +13,203 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
/* globals saveReferer */
|
|
||||||
|
/* globals canRequestBody */ // From preserve-referer.js
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var VIEWER_URL = chrome.extension.getURL("content/web/viewer.html");
|
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 {
|
||||||
|
await chrome.declarativeNetRequest.updateDynamicRules({ addRules });
|
||||||
|
// 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 do not bother with detecting that because we fall back to catching
|
||||||
|
// PDF documents via maybeRenderPdfDoc in contentscript.js.
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to register rules to redirect PDF requests.");
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getViewerURL(pdf_url) {
|
function getViewerURL(pdf_url) {
|
||||||
// |pdf_url| may contain a fragment such as "#page=2". That should be passed
|
// |pdf_url| may contain a fragment such as "#page=2". That should be passed
|
||||||
@ -31,174 +223,42 @@ function getViewerURL(pdf_url) {
|
|||||||
return VIEWER_URL + "?file=" + encodeURIComponent(pdf_url) + hash;
|
return VIEWER_URL + "?file=" + encodeURIComponent(pdf_url) + hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// If the user has not granted access to file:-URLs, then declarativeNetRequest
|
||||||
* @param {Object} details First argument of the webRequest.onHeadersReceived
|
// will not catch the request. It is still visible through the webNavigation
|
||||||
* event. The property "url" is read.
|
// API though, and we can replace the tab with the viewer.
|
||||||
* @returns {boolean} True if the PDF file should be downloaded.
|
// The viewer will detect that it has no access to file:-URLs, and prompt the
|
||||||
*/
|
// user to activate file permissions.
|
||||||
function isPdfDownloadable(details) {
|
chrome.webNavigation.onBeforeNavigate.addListener(
|
||||||
if (details.url.includes("pdfjs.action=download")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// 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.
|
|
||||||
// If the query string contains "=download", do not unconditionally force the
|
|
||||||
// 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).
|
|
||||||
if (details.type === "main_frame" && !details.url.includes("=download")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var cdHeader =
|
|
||||||
details.responseHeaders &&
|
|
||||||
getHeaderFromHeaders(details.responseHeaders, "content-disposition");
|
|
||||||
return cdHeader && /^attachment/i.test(cdHeader.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the header from the list of headers for a given name.
|
|
||||||
* @param {Array} headers responseHeaders of webRequest.onHeadersReceived
|
|
||||||
* @returns {undefined|{name: string, value: string}} The header, if found.
|
|
||||||
*/
|
|
||||||
function getHeaderFromHeaders(headers, headerName) {
|
|
||||||
for (const header of headers) {
|
|
||||||
if (header.name.toLowerCase() === headerName) {
|
|
||||||
return header;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the request is a PDF file.
|
|
||||||
* @param {Object} details First argument of the webRequest.onHeadersReceived
|
|
||||||
* event. The properties "responseHeaders" and "url"
|
|
||||||
* are read.
|
|
||||||
* @returns {boolean} True if the resource is a PDF file.
|
|
||||||
*/
|
|
||||||
function isPdfFile(details) {
|
|
||||||
var header = getHeaderFromHeaders(details.responseHeaders, "content-type");
|
|
||||||
if (header) {
|
|
||||||
var headerValue = header.value.toLowerCase().split(";", 1)[0].trim();
|
|
||||||
if (headerValue === "application/pdf") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (headerValue === "application/octet-stream") {
|
|
||||||
if (details.url.toLowerCase().indexOf(".pdf") > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
var cdHeader = getHeaderFromHeaders(
|
|
||||||
details.responseHeaders,
|
|
||||||
"content-disposition"
|
|
||||||
);
|
|
||||||
if (cdHeader && /\.pdf(["']|$)/i.test(cdHeader.value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a set of headers, and set "Content-Disposition: attachment".
|
|
||||||
* @param {Object} details First argument of the webRequest.onHeadersReceived
|
|
||||||
* event. The property "responseHeaders" is read and
|
|
||||||
* modified if needed.
|
|
||||||
* @returns {Object|undefined} The return value for the onHeadersReceived event.
|
|
||||||
* Object with key "responseHeaders" if the headers
|
|
||||||
* have been modified, undefined otherwise.
|
|
||||||
*/
|
|
||||||
function getHeadersWithContentDispositionAttachment(details) {
|
|
||||||
var headers = details.responseHeaders;
|
|
||||||
var cdHeader = getHeaderFromHeaders(headers, "content-disposition");
|
|
||||||
if (!cdHeader) {
|
|
||||||
cdHeader = { name: "Content-Disposition" };
|
|
||||||
headers.push(cdHeader);
|
|
||||||
}
|
|
||||||
if (!/^attachment/i.test(cdHeader.value)) {
|
|
||||||
cdHeader.value = "attachment" + cdHeader.value.replace(/^[^;]+/i, "");
|
|
||||||
return { responseHeaders: headers };
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.webRequest.onHeadersReceived.addListener(
|
|
||||||
function (details) {
|
function (details) {
|
||||||
if (details.method !== "GET") {
|
// Note: pdfjs.action=download is not checked here because that code path
|
||||||
// Don't intercept POST requests until http://crbug.com/104058 is fixed.
|
// is not reachable for local files through the viewer when we do not have
|
||||||
return undefined;
|
// file:-access.
|
||||||
}
|
if (details.frameId === 0) {
|
||||||
if (!isPdfFile(details)) {
|
chrome.extension.isAllowedFileSchemeAccess(function (isAllowedAccess) {
|
||||||
return undefined;
|
if (isAllowedAccess) {
|
||||||
}
|
// Expected to be handled by DNR. Don't do anything.
|
||||||
if (isPdfDownloadable(details)) {
|
return;
|
||||||
// Force download by ensuring that Content-Disposition: attachment is set
|
}
|
||||||
return getHeadersWithContentDispositionAttachment(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
var viewerUrl = getViewerURL(details.url);
|
|
||||||
|
|
||||||
// Implemented in preserve-referer.js
|
|
||||||
saveReferer(details);
|
|
||||||
|
|
||||||
return { redirectUrl: viewerUrl };
|
|
||||||
},
|
|
||||||
{
|
|
||||||
urls: ["<all_urls>"],
|
|
||||||
types: ["main_frame", "sub_frame"],
|
|
||||||
},
|
|
||||||
["blocking", "responseHeaders"]
|
|
||||||
);
|
|
||||||
|
|
||||||
chrome.webRequest.onBeforeRequest.addListener(
|
|
||||||
function (details) {
|
|
||||||
if (isPdfDownloadable(details)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
var viewerUrl = getViewerURL(details.url);
|
|
||||||
|
|
||||||
return { redirectUrl: viewerUrl };
|
|
||||||
},
|
|
||||||
{
|
|
||||||
urls: ["file://*/*.pdf", "file://*/*.PDF"],
|
|
||||||
types: ["main_frame", "sub_frame"],
|
|
||||||
},
|
|
||||||
["blocking"]
|
|
||||||
);
|
|
||||||
|
|
||||||
chrome.extension.isAllowedFileSchemeAccess(function (isAllowedAccess) {
|
|
||||||
if (isAllowedAccess) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If the user has not granted access to file:-URLs, then the webRequest API
|
|
||||||
// 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) {
|
|
||||||
if (details.frameId === 0 && !isPdfDownloadable(details)) {
|
|
||||||
chrome.tabs.update(details.tabId, {
|
chrome.tabs.update(details.tabId, {
|
||||||
url: getViewerURL(details.url),
|
url: getViewerURL(details.url),
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
},
|
|
||||||
{
|
|
||||||
url: [
|
|
||||||
{
|
|
||||||
urlPrefix: "file://",
|
|
||||||
pathSuffix: ".pdf",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
urlPrefix: "file://",
|
|
||||||
pathSuffix: ".PDF",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
);
|
},
|
||||||
});
|
{
|
||||||
|
url: [
|
||||||
|
{
|
||||||
|
urlPrefix: "file://",
|
||||||
|
pathSuffix: ".pdf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPrefix: "file://",
|
||||||
|
pathSuffix: ".PDF",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
|
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
|
||||||
if (message && message.action === "getParentOrigin") {
|
if (message && message.action === "getParentOrigin") {
|
||||||
@ -245,6 +305,11 @@ chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
|
|||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (message && message.action === "canRequestBody") {
|
||||||
|
sendResponse(canRequestBody(sender.tab.id, sender.frameId));
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,20 +13,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
/* globals getHeaderFromHeaders */
|
|
||||||
/* exported saveReferer */
|
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
/**
|
/**
|
||||||
* This file is one part of the Referer persistency implementation. The other
|
* This file is one part of the Referer persistency implementation. The other
|
||||||
* part resides in chromecom.js.
|
* part resides in chromecom.js.
|
||||||
*
|
*
|
||||||
* This file collects request headers for every http(s) request, and temporarily
|
* This file collects Referer headers for every http(s) request, and temporarily
|
||||||
* stores the request headers in a dictionary. Upon completion of the request
|
* stores the request headers in a dictionary, for REFERRER_IN_MEMORY_TIME ms.
|
||||||
* (success or failure), the headers are discarded.
|
|
||||||
* pdfHandler.js will call saveReferer(details) when it is about to redirect to
|
|
||||||
* the viewer. Upon calling saveReferer, the Referer header is extracted from
|
|
||||||
* the request headers and saved.
|
|
||||||
*
|
*
|
||||||
* When the viewer is opened, it opens a port ("chromecom-referrer"). This port
|
* When the viewer is opened, it opens a port ("chromecom-referrer"). This port
|
||||||
* is used to set up the webRequest listeners that stick the Referer headers to
|
* is used to set up the webRequest listeners that stick the Referer headers to
|
||||||
@ -36,49 +30,64 @@ limitations under the License.
|
|||||||
* See setReferer in chromecom.js for more explanation of this logic.
|
* See setReferer in chromecom.js for more explanation of this logic.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Remembers the request headers for every http(s) page request for the duration
|
/* exported canRequestBody */ // Used in pdfHandler.js
|
||||||
// of the request.
|
|
||||||
var g_requestHeaders = {};
|
|
||||||
// g_referrers[tabId][frameId] = referrer of PDF frame.
|
// g_referrers[tabId][frameId] = referrer of PDF frame.
|
||||||
var g_referrers = {};
|
var g_referrers = {};
|
||||||
|
var g_referrerTimers = {};
|
||||||
|
// The background script will eventually suspend after 30 seconds of inactivity.
|
||||||
|
// This can be delayed when extension events are firing. To prevent the data
|
||||||
|
// from being kept in memory for too long, cap the data duration to 5 minutes.
|
||||||
|
var REFERRER_IN_MEMORY_TIME = 300000;
|
||||||
|
|
||||||
(function () {
|
// g_postRequests[tabId] = Set of frameId that were loaded via POST.
|
||||||
var requestFilter = {
|
var g_postRequests = {};
|
||||||
urls: ["*://*/*"],
|
|
||||||
types: ["main_frame", "sub_frame"],
|
|
||||||
};
|
|
||||||
chrome.webRequest.onSendHeaders.addListener(
|
|
||||||
function (details) {
|
|
||||||
g_requestHeaders[details.requestId] = details.requestHeaders;
|
|
||||||
},
|
|
||||||
requestFilter,
|
|
||||||
["requestHeaders", "extraHeaders"]
|
|
||||||
);
|
|
||||||
chrome.webRequest.onBeforeRedirect.addListener(forgetHeaders, requestFilter);
|
|
||||||
chrome.webRequest.onCompleted.addListener(forgetHeaders, requestFilter);
|
|
||||||
chrome.webRequest.onErrorOccurred.addListener(forgetHeaders, requestFilter);
|
|
||||||
function forgetHeaders(details) {
|
|
||||||
delete g_requestHeaders[details.requestId];
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
/**
|
var rIsReferer = /^referer$/i;
|
||||||
* @param {object} details - onHeadersReceived event data.
|
chrome.webRequest.onSendHeaders.addListener(
|
||||||
*/
|
function saveReferer(details) {
|
||||||
function saveReferer(details) {
|
const { tabId, frameId, requestHeaders, method } = details;
|
||||||
var referer =
|
g_referrers[tabId] ??= {};
|
||||||
g_requestHeaders[details.requestId] &&
|
g_referrers[tabId][frameId] = requestHeaders.find(h =>
|
||||||
getHeaderFromHeaders(g_requestHeaders[details.requestId], "referer");
|
rIsReferer.test(h.name)
|
||||||
referer = (referer && referer.value) || "";
|
)?.value;
|
||||||
if (!g_referrers[details.tabId]) {
|
setCanRequestBody(tabId, frameId, method !== "GET");
|
||||||
g_referrers[details.tabId] = {};
|
forgetReferrerEventually(tabId);
|
||||||
|
},
|
||||||
|
{ urls: ["*://*/*"], types: ["main_frame", "sub_frame"] },
|
||||||
|
["requestHeaders", "extraHeaders"]
|
||||||
|
);
|
||||||
|
|
||||||
|
function forgetReferrerEventually(tabId) {
|
||||||
|
if (g_referrerTimers[tabId]) {
|
||||||
|
clearTimeout(g_referrerTimers[tabId]);
|
||||||
}
|
}
|
||||||
g_referrers[details.tabId][details.frameId] = referer;
|
g_referrerTimers[tabId] = setTimeout(() => {
|
||||||
|
delete g_referrers[tabId];
|
||||||
|
delete g_referrerTimers[tabId];
|
||||||
|
delete g_postRequests[tabId];
|
||||||
|
}, REFERRER_IN_MEMORY_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.tabs.onRemoved.addListener(function (tabId) {
|
// Keeps track of whether a document in tabId + frameId is loaded through a
|
||||||
delete g_referrers[tabId];
|
// POST form submission. Although this logic has nothing to do with referrer
|
||||||
});
|
// tracking, it is still here to enable re-use of the webRequest listener above.
|
||||||
|
function setCanRequestBody(tabId, frameId, isPOST) {
|
||||||
|
if (isPOST) {
|
||||||
|
g_postRequests[tabId] ??= new Set();
|
||||||
|
g_postRequests[tabId].add(frameId);
|
||||||
|
} else {
|
||||||
|
g_postRequests[tabId]?.delete(frameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canRequestBody(tabId, frameId) {
|
||||||
|
// Returns true unless the frame is known to be loaded through a POST request.
|
||||||
|
// If the background suspends, the information may be lost. This is acceptable
|
||||||
|
// because the information is only potentially needed shortly after document
|
||||||
|
// load, by contentscript.js.
|
||||||
|
return !g_postRequests[tabId]?.has(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
// This method binds a webRequest event handler which adds the Referer header
|
// This method binds a webRequest event handler which adds the Referer header
|
||||||
// to matching PDF resource requests (only if the Referer is non-empty). The
|
// to matching PDF resource requests (only if the Referer is non-empty). The
|
||||||
@ -89,8 +98,11 @@ chrome.runtime.onConnect.addListener(function onReceivePort(port) {
|
|||||||
}
|
}
|
||||||
var tabId = port.sender.tab.id;
|
var tabId = port.sender.tab.id;
|
||||||
var frameId = port.sender.frameId;
|
var frameId = port.sender.frameId;
|
||||||
|
var dnrRequestId;
|
||||||
|
|
||||||
// If the PDF is viewed for the first time, then the referer will be set here.
|
// If the PDF is viewed for the first time, then the referer will be set here.
|
||||||
|
// Note: g_referrers could be empty if the background script was suspended by
|
||||||
|
// the browser. In that case, chromecom.js may send us the referer (below).
|
||||||
var referer = (g_referrers[tabId] && g_referrers[tabId][frameId]) || "";
|
var referer = (g_referrers[tabId] && g_referrers[tabId][frameId]) || "";
|
||||||
port.onMessage.addListener(function (data) {
|
port.onMessage.addListener(function (data) {
|
||||||
// If the viewer was opened directly (without opening a PDF URL first), then
|
// If the viewer was opened directly (without opening a PDF URL first), then
|
||||||
@ -99,49 +111,49 @@ chrome.runtime.onConnect.addListener(function onReceivePort(port) {
|
|||||||
if (data.referer) {
|
if (data.referer) {
|
||||||
referer = data.referer;
|
referer = data.referer;
|
||||||
}
|
}
|
||||||
chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);
|
dnrRequestId = data.dnrRequestId;
|
||||||
if (referer) {
|
setStickyReferrer(dnrRequestId, tabId, data.requestUrl, referer, () => {
|
||||||
// Only add a blocking request handler if the referer has to be rewritten.
|
// Acknowledge the message, and include the latest referer for this frame.
|
||||||
chrome.webRequest.onBeforeSendHeaders.addListener(
|
port.postMessage(referer);
|
||||||
onBeforeSendHeaders,
|
});
|
||||||
{
|
|
||||||
urls: [data.requestUrl],
|
|
||||||
types: ["xmlhttprequest"],
|
|
||||||
tabId,
|
|
||||||
},
|
|
||||||
["blocking", "requestHeaders", "extraHeaders"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Acknowledge the message, and include the latest referer for this frame.
|
|
||||||
port.postMessage(referer);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// The port is only disconnected when the other end reloads.
|
// The port is only disconnected when the other end reloads.
|
||||||
port.onDisconnect.addListener(function () {
|
port.onDisconnect.addListener(function () {
|
||||||
if (g_referrers[tabId]) {
|
unsetStickyReferrer(dnrRequestId);
|
||||||
delete g_referrers[tabId][frameId];
|
|
||||||
}
|
|
||||||
chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function onBeforeSendHeaders(details) {
|
|
||||||
if (details.frameId !== frameId) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
var headers = details.requestHeaders;
|
|
||||||
var refererHeader = getHeaderFromHeaders(headers, "referer");
|
|
||||||
if (!refererHeader) {
|
|
||||||
refererHeader = { name: "Referer" };
|
|
||||||
headers.push(refererHeader);
|
|
||||||
} else if (
|
|
||||||
refererHeader.value &&
|
|
||||||
refererHeader.value.lastIndexOf("chrome-extension:", 0) !== 0
|
|
||||||
) {
|
|
||||||
// Sanity check. If the referer is set, and the value is not the URL of
|
|
||||||
// this extension, then the request was not initiated by this extension.
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
refererHeader.value = referer;
|
|
||||||
return { requestHeaders: headers };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setStickyReferrer(dnrRequestId, tabId, url, referer, callback) {
|
||||||
|
if (!referer) {
|
||||||
|
unsetStickyReferrer(dnrRequestId);
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rule = {
|
||||||
|
id: dnrRequestId,
|
||||||
|
condition: {
|
||||||
|
urlFilter: `|${url}|`,
|
||||||
|
// The viewer and background are presumed to have the same origin:
|
||||||
|
initiatorDomains: [location.hostname], // = chrome.runtime.id.
|
||||||
|
resourceTypes: ["xmlhttprequest"],
|
||||||
|
tabIds: [tabId],
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: "modifyHeaders",
|
||||||
|
requestHeaders: [{ operation: "set", header: "referer", value: referer }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
chrome.declarativeNetRequest.updateSessionRules(
|
||||||
|
{ removeRuleIds: [dnrRequestId], addRules: [rule] },
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsetStickyReferrer(dnrRequestId) {
|
||||||
|
if (dnrRequestId) {
|
||||||
|
chrome.declarativeNetRequest.updateSessionRules({
|
||||||
|
removeRuleIds: [dnrRequestId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2015 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is part of the work-around for crbug.com/511670.
|
|
||||||
* - chromecom.js sets the URL and history state upon unload.
|
|
||||||
* - extension-router.js retrieves the saved state and opens restoretab.html
|
|
||||||
* - restoretab.html (this script) restores the URL and history state.
|
|
||||||
*/
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var url = decodeURIComponent(location.search.slice(1));
|
|
||||||
var historyState = decodeURIComponent(location.hash.slice(1));
|
|
||||||
|
|
||||||
historyState = historyState === "undefined" ? null : JSON.parse(historyState);
|
|
||||||
|
|
||||||
history.replaceState(historyState, null, url);
|
|
||||||
location.reload();
|
|
||||||
@ -20,7 +20,10 @@ limitations under the License.
|
|||||||
// viewer is not displaying any PDF files. Otherwise the tabs would close, which
|
// viewer is not displaying any PDF files. Otherwise the tabs would close, which
|
||||||
// is quite disruptive (crbug.com/511670).
|
// is quite disruptive (crbug.com/511670).
|
||||||
chrome.runtime.onUpdateAvailable.addListener(function () {
|
chrome.runtime.onUpdateAvailable.addListener(function () {
|
||||||
if (chrome.extension.getViews({ type: "tab" }).length === 0) {
|
chrome.tabs.query({ url: chrome.runtime.getURL("*") }, tabs => {
|
||||||
|
if (tabs?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
chrome.runtime.reload();
|
chrome.runtime.reload();
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -42,8 +42,35 @@ limitations under the License.
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
maybeSendPing();
|
// The localStorage API is unavailable in service workers. We store data in
|
||||||
setInterval(maybeSendPing, 36e5);
|
// chrome.storage.local and use this "localStorage" object to enable
|
||||||
|
// synchronous access in the logic.
|
||||||
|
const localStorage = {
|
||||||
|
telemetryLastTime: 0,
|
||||||
|
telemetryDeduplicationId: "",
|
||||||
|
telemetryLastVersion: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.alarms.onAlarm.addListener(alarm => {
|
||||||
|
if (alarm.name === "maybeSendPing") {
|
||||||
|
maybeSendPing();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
chrome.storage.session.get({ didPingCheck: false }, async items => {
|
||||||
|
if (items?.didPingCheck) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
maybeSendPing();
|
||||||
|
await chrome.alarms.clear("maybeSendPing");
|
||||||
|
await chrome.alarms.create("maybeSendPing", { periodInMinutes: 60 });
|
||||||
|
chrome.storage.session.set({ didPingCheck: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateLocalStorage(key, value) {
|
||||||
|
localStorage[key] = value;
|
||||||
|
// Note: We mirror the data in localStorage because the following is async.
|
||||||
|
chrome.storage.local.set({ [key]: value });
|
||||||
|
}
|
||||||
|
|
||||||
function maybeSendPing() {
|
function maybeSendPing() {
|
||||||
getLoggingPref(function (didOptOut) {
|
getLoggingPref(function (didOptOut) {
|
||||||
@ -61,12 +88,20 @@ limitations under the License.
|
|||||||
// send more pings.
|
// send more pings.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
doSendPing();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSendPing() {
|
||||||
|
chrome.storage.local.get(localStorage, items => {
|
||||||
|
Object.assign(localStorage, items);
|
||||||
|
|
||||||
var lastTime = parseInt(localStorage.telemetryLastTime) || 0;
|
var lastTime = parseInt(localStorage.telemetryLastTime) || 0;
|
||||||
var wasUpdated = didUpdateSinceLastCheck();
|
var wasUpdated = didUpdateSinceLastCheck();
|
||||||
if (!wasUpdated && Date.now() - lastTime < MINIMUM_TIME_BETWEEN_PING) {
|
if (!wasUpdated && Date.now() - lastTime < MINIMUM_TIME_BETWEEN_PING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
localStorage.telemetryLastTime = Date.now();
|
updateLocalStorage("telemetryLastTime", Date.now());
|
||||||
|
|
||||||
var deduplication_id = getDeduplicationId(wasUpdated);
|
var deduplication_id = getDeduplicationId(wasUpdated);
|
||||||
var extension_version = chrome.runtime.getManifest().version;
|
var extension_version = chrome.runtime.getManifest().version;
|
||||||
@ -104,7 +139,7 @@ limitations under the License.
|
|||||||
for (const c of buf) {
|
for (const c of buf) {
|
||||||
id += (c >>> 4).toString(16) + (c & 0xf).toString(16);
|
id += (c >>> 4).toString(16) + (c & 0xf).toString(16);
|
||||||
}
|
}
|
||||||
localStorage.telemetryDeduplicationId = id;
|
updateLocalStorage("telemetryDeduplicationId", id);
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@ -119,7 +154,7 @@ limitations under the License.
|
|||||||
if (!chromeVersion || localStorage.telemetryLastVersion === chromeVersion) {
|
if (!chromeVersion || localStorage.telemetryLastVersion === chromeVersion) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
localStorage.telemetryLastVersion = chromeVersion;
|
updateLocalStorage("telemetryLastVersion", chromeVersion);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,11 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME")) {
|
|||||||
// is rewritten as soon as possible.
|
// is rewritten as soon as possible.
|
||||||
const queryString = document.location.search.slice(1);
|
const queryString = document.location.search.slice(1);
|
||||||
const m = /(^|&)file=([^&]*)/.exec(queryString);
|
const m = /(^|&)file=([^&]*)/.exec(queryString);
|
||||||
const defaultUrl = m ? decodeURIComponent(m[2]) : "";
|
let defaultUrl = m ? decodeURIComponent(m[2]) : "";
|
||||||
|
if (!defaultUrl && queryString.startsWith("DNR:")) {
|
||||||
|
// Redirected via DNR, see registerPdfRedirectRule in pdfHandler.js.
|
||||||
|
defaultUrl = queryString.slice(4);
|
||||||
|
}
|
||||||
|
|
||||||
// Example: chrome-extension://.../http://example.com/file.pdf
|
// Example: chrome-extension://.../http://example.com/file.pdf
|
||||||
const humanReadableUrl = "/" + defaultUrl + location.hash;
|
const humanReadableUrl = "/" + defaultUrl + location.hash;
|
||||||
@ -249,24 +253,7 @@ function requestAccessToLocalFile(fileUrl, overlayManager, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window === top) {
|
let dnrRequestId;
|
||||||
// Chrome closes all extension tabs (crbug.com/511670) when the extension
|
|
||||||
// reloads. To counter this, the tab URL and history state is saved to
|
|
||||||
// localStorage and restored by extension-router.js.
|
|
||||||
// Unfortunately, the window and tab index are not restored. And if it was
|
|
||||||
// the only tab in an incognito window, then the tab is not restored either.
|
|
||||||
addEventListener("unload", function () {
|
|
||||||
// If the runtime is still available, the unload is most likely a normal
|
|
||||||
// tab closure. Otherwise it is most likely an extension reload.
|
|
||||||
if (!isRuntimeAvailable()) {
|
|
||||||
localStorage.setItem(
|
|
||||||
"unload-" + Date.now() + "-" + document.hidden + "-" + location.href,
|
|
||||||
JSON.stringify(history.state)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This port is used for several purposes:
|
// This port is used for several purposes:
|
||||||
// 1. When disconnected, the background page knows that the frame has unload.
|
// 1. When disconnected, the background page knows that the frame has unload.
|
||||||
// 2. When the referrer was saved in history.state.chromecomState, it is sent
|
// 2. When the referrer was saved in history.state.chromecomState, it is sent
|
||||||
@ -281,6 +268,7 @@ let port;
|
|||||||
// 3. Background -> page: Send latest referer and save to history.
|
// 3. Background -> page: Send latest referer and save to history.
|
||||||
// 4. Page: Invoke callback.
|
// 4. Page: Invoke callback.
|
||||||
function setReferer(url, callback) {
|
function setReferer(url, callback) {
|
||||||
|
dnrRequestId ??= crypto.getRandomValues(new Uint32Array(1))[0] % 0x80000000;
|
||||||
if (!port) {
|
if (!port) {
|
||||||
// The background page will accept the port, and keep adding the Referer
|
// The background page will accept the port, and keep adding the Referer
|
||||||
// request header to requests to |url| until the port is disconnected.
|
// request header to requests to |url| until the port is disconnected.
|
||||||
@ -290,6 +278,7 @@ function setReferer(url, callback) {
|
|||||||
port.onMessage.addListener(onMessage);
|
port.onMessage.addListener(onMessage);
|
||||||
// Initiate the information exchange.
|
// Initiate the information exchange.
|
||||||
port.postMessage({
|
port.postMessage({
|
||||||
|
dnrRequestId,
|
||||||
referer: window.history.state?.chromecomState,
|
referer: window.history.state?.chromecomState,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user