first commit
Some checks failed
Types tests / Test (lts/*) (push) Has been cancelled
Lint / Lint (lts/*) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CI / Test (20) (push) Has been cancelled
CI / Test (22) (push) Has been cancelled
CI / Test (24) (push) Has been cancelled

This commit is contained in:
2025-10-03 22:20:19 +08:00
commit 44db9807a1
2172 changed files with 526822 additions and 0 deletions

1
extensions/chromium/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
content/

View File

@@ -0,0 +1,26 @@
/*
Copyright 2024 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.
*/
"use strict";
importScripts(
"options/migration.js",
"preserve-referer.js",
"pdfHandler.js",
"extension-router.js",
"suppress-update.js",
"telemetry.js"
);

View File

@@ -0,0 +1,261 @@
/*
Copyright 2014 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.
*/
"use strict";
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") {
watchObjectOrEmbed(event.target);
}
}
// Called for every <object> or <embed> element in the page.
// This may change the type, src/data attributes and/or the child nodes of the
// element. This function only affects elements for the first call. Subsequent
// invocations have no effect.
function watchObjectOrEmbed(elem) {
var mimeType = elem.type;
if (mimeType && mimeType.toLowerCase() !== "application/pdf") {
return;
}
// <embed src> <object data>
var srcAttribute = "src" in elem ? "src" : "data";
var path = elem[srcAttribute];
if (!mimeType && !/\.pdf($|[?#])/i.test(path)) {
return;
}
if (
elem.tagName === "EMBED" &&
elem.name === "plugin" &&
elem.parentNode === document.body &&
elem.parentNode.childElementCount === 1 &&
elem.src === location.href
) {
// This page is most likely Chrome's default page that embeds a PDF file.
// The fact that the extension's background page did not intercept and
// redirect this PDF request means that this PDF cannot be opened by PDF.js,
// e.g. because it is a response to a POST request (as in #6174).
// A reduced test case to test PDF response to POST requests is available at
// https://robwu.nl/pdfjs/issue6174/.
// Until #4483 is fixed, POST requests should be ignored.
return;
}
if (elem.tagName === "EMBED" && elem.src === "about:blank") {
// Starting from Chrome 76, internal embeds do not have the original URL,
// but "about:blank" instead.
// See https://github.com/mozilla/pdf.js/issues/11137
return;
}
if (elem.__I_saw_this_element) {
return;
}
elem.__I_saw_this_element = true;
var tagName = elem.tagName.toUpperCase();
var updateEmbedOrObject;
if (tagName === "EMBED") {
updateEmbedOrObject = updateEmbedElement;
} else if (tagName === "OBJECT") {
updateEmbedOrObject = updateObjectElement;
} else {
return;
}
var lastSrc;
var isUpdating = false;
function updateViewerFrame() {
if (!isUpdating) {
isUpdating = true;
try {
if (lastSrc !== elem[srcAttribute]) {
updateEmbedOrObject(elem);
lastSrc = elem[srcAttribute];
}
} finally {
isUpdating = false;
}
}
}
updateViewerFrame();
// Watch for page-initiated changes of the src/data attribute.
var srcObserver = new MutationObserver(updateViewerFrame);
srcObserver.observe(elem, {
attributes: true,
childList: false,
characterData: false,
attributeFilter: [srcAttribute],
});
}
// Display the PDF Viewer in an <embed>.
function updateEmbedElement(elem) {
if (elem.type === "text/html" && elem.src.lastIndexOf(VIEWER_URL, 0) === 0) {
// The viewer is already shown.
return;
}
// The <embed> tag needs to be removed and re-inserted before any src changes
// are effective.
var parentNode = elem.parentNode;
var nextSibling = elem.nextSibling;
if (parentNode) {
elem.remove();
}
elem.type = "text/html";
elem.src = getEmbeddedViewerURL(elem.src);
if (parentNode) {
// Suppress linter warning: insertBefore is preferable to
// nextSibling.before(elem) because nextSibling may be null.
// eslint-disable-next-line unicorn/prefer-modern-dom-apis
parentNode.insertBefore(elem, nextSibling);
}
}
// Display the PDF Viewer in an <object>.
function updateObjectElement(elem) {
// <object> elements are terrible. Experiments (in49.0.2623.75) show that the
// following happens:
// - When fallback content is shown (e.g. because the built-in PDF Viewer is
// disabled), updating the "data" attribute has no effect. Not surprising
// considering that HTMLObjectElement::m_useFallbackContent is not reset
// once it is set to true. Source:
// WebKit/Source/core/html/HTMLObjectElement.cpp#378 (rev 749fe30d676b6c14).
// - When the built-in PDF Viewer plugin is enabled, updating the "data"
// attribute reloads the content (provided that the type was correctly set).
// - When <object type=text/html data="chrome-extension://..."> is used
// (tested with a data-URL, data:text/html,<object...>, the extension's
// origin allowlist is not set up, so the viewer can't load the PDF file.
// - The content of the <object> tag may be affected by <param> tags.
//
// To make sure that our solution works for all cases, we will insert a frame
// as fallback content and force the <object> tag to render its fallback
// content.
var iframe = elem.firstElementChild;
if (!iframe || !iframe.__inserted_by_pdfjs) {
iframe = createFullSizeIframe();
elem.textContent = "";
elem.append(iframe);
iframe.__inserted_by_pdfjs = true;
}
iframe.src = getEmbeddedViewerURL(elem.data);
// Some bogus content type that is not handled by any plugin.
elem.type = "application/not-a-pee-dee-eff-type";
// Force the <object> to reload and render its fallback content.
elem.data += "";
// Usually the browser renders plugin content in this tag, which is completely
// oblivious of styles such as padding, but we insert and render child nodes,
// so force padding to be zero to avoid undesired dimension changes.
elem.style.padding = "0";
// <object> and <embed> elements have a "display:inline" style by default.
// Despite this property, when a plugin is loaded in the tag, the tag is
// treated like "display:inline-block". However, when the browser does not
// render plugin content, the <object> tag does not behave like that, and as
// a result the width and height is ignored.
// Force "display:inline-block" to make sure that the width/height as set by
// web pages is respected.
// (<embed> behaves as expected with the default display value, but setting it
// to display:inline-block doesn't hurt).
elem.style.display = "inline-block";
}
// Create an <iframe> element without borders that takes the full width and
// height.
function createFullSizeIframe() {
var iframe = document.createElement("iframe");
iframe.style.background = "none";
iframe.style.border = "none";
iframe.style.borderRadius = "none";
iframe.style.boxShadow = "none";
iframe.style.cssFloat = "none";
iframe.style.display = "block";
iframe.style.height = "100%";
iframe.style.margin = "0";
iframe.style.maxHeight = "none";
iframe.style.maxWidth = "none";
iframe.style.position = "static";
iframe.style.transform = "none";
iframe.style.visibility = "visible";
iframe.style.width = "100%";
return iframe;
}
// Get the viewer URL, provided that the path is a valid URL.
function getEmbeddedViewerURL(path) {
var fragment = /^([^#]*)(#.*)?$/.exec(path);
path = fragment[1];
fragment = fragment[2] || "";
// Resolve relative path to document.
var a = document.createElement("a");
a.href = document.baseURI;
a.href = 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);
}

View File

@@ -0,0 +1,14 @@
/**
* Detect creation of <embed> and <object> tags.
*/
@keyframes pdfjs-detected-object-or-embed {
from {
/* empty */
}
}
object,
embed {
animation-delay: 0s !important;
animation-name: pdfjs-detected-object-or-embed !important;
animation-play-state: running !important;
}

View File

@@ -0,0 +1,105 @@
/*
Copyright 2013 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.
*/
"use strict";
(function ExtensionRouterClosure() {
var VIEWER_URL = chrome.runtime.getURL("content/web/viewer.html");
var CRX_BASE_URL = chrome.runtime.getURL("/");
var schemes = [
"http",
"https",
"file",
"chrome-extension",
"blob",
"data",
// Chromium OS
"filesystem",
// Chromium OS, shorthand for filesystem:<origin>/external/
"drive",
];
/**
* @param {string} url The URL prefixed with chrome-extension://.../
* @returns {string|undefined} The percent-encoded URL of the (PDF) file.
*/
function parseExtensionURL(url) {
url = url.substring(CRX_BASE_URL.length);
// Find the (url-encoded) colon and verify that the scheme is allowed.
var schemeIndex = url.search(/:|%3A/i);
if (schemeIndex === -1) {
return undefined;
}
var scheme = url.slice(0, schemeIndex).toLowerCase();
if (schemes.includes(scheme)) {
// NOTE: We cannot use the `updateUrlHash` function in this context.
url = url.split("#", 1)[0];
if (url.charAt(schemeIndex) === ":") {
url = encodeURIComponent(url);
}
return url;
}
return undefined;
}
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(originalUrl);
if (url) {
url = VIEWER_URL + "?file=" + url;
var i = originalUrl.indexOf("#");
if (i > 0) {
url += originalUrl.slice(i);
}
return url;
}
}
return undefined;
}
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));
}
}
});
// 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.");
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,61 @@
{
"minimum_chrome_version": "103",
"manifest_version": 3,
"name": "PDF Viewer",
"version": "PDFJSSCRIPT_VERSION",
"description": "Uses HTML5 to display PDF files directly in the browser.",
"icons": {
"128": "icon128.png",
"48": "icon48.png",
"16": "icon16.png"
},
"permissions": [
"alarms",
"declarativeNetRequestWithHostAccess",
"webRequest",
"tabs",
"webNavigation",
"storage"
],
"host_permissions": ["<all_urls>"],
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*", "file://*/*"],
"run_at": "document_start",
"all_frames": true,
"css": ["contentstyle.css"],
"js": ["contentscript.js"]
}
],
"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"
},
"options_page": "options/options.html",
"background": {
"service_worker": "background.js"
},
"incognito": "split",
"web_accessible_resources": [
{
"resources": [
"content/web/viewer.html",
"http:/*",
"https:/*",
"file:/*",
"chrome-extension:/*",
"blob:*",
"data:*",
"filesystem:/*",
"drive:*"
],
"matches": ["<all_urls>"],
"extension_ids": ["*"]
}
]
}

View File

@@ -0,0 +1,153 @@
/*
Copyright 2016 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.
*/
"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;
if (!storageSync) {
// No sync storage area - nothing to migrate to.
return;
}
getStorageNames(function (storageKeys) {
storageLocal.get(storageKeys, function (values) {
if (!values || !Object.keys(values).length) {
// No local storage - nothing to migrate.
// ... except possibly for a renamed preference name.
migrateRenamedStorage();
return;
}
migrateToSyncStorage(values);
});
});
async function getStorageNames(callback) {
var schema_location = chrome.runtime.getManifest().storage.managed_schema;
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
// storage.local.
function migrateToSyncStorage(values) {
storageSync.set(values, function () {
if (chrome.runtime.lastError) {
console.error(
"Failed to migrate settings due to an error: " +
chrome.runtime.lastError.message
);
return;
}
// Migration successful. Delete local settings.
storageLocal.remove(Object.keys(values), function () {
// In theory remove() could fail (e.g. if the browser's storage
// backend is corrupt), but since storageSync.set succeeded, consider
// the migration successful.
console.log(
"Successfully migrated preferences from local to sync storage."
);
migrateRenamedStorage();
});
});
}
// TODO: Remove this migration code somewhere in the future, when most users
// have had their chance of migrating to the new preference format.
// Note: We cannot modify managed preferences, so the migration logic is
// duplicated in web/chromecom.js too.
function migrateRenamedStorage() {
storageSync.get(
[
"enableHandToolOnLoad",
"cursorToolOnLoad",
"disableTextLayer",
"enhanceTextSelection",
"textLayerMode",
"showPreviousViewOnLoad",
"disablePageMode",
"viewOnLoad",
],
function (items) {
// Migration code for https://github.com/mozilla/pdf.js/pull/7635.
if (typeof items.enableHandToolOnLoad === "boolean") {
if (items.enableHandToolOnLoad) {
storageSync.set(
{
cursorToolOnLoad: 1,
},
function () {
if (!chrome.runtime.lastError) {
storageSync.remove("enableHandToolOnLoad");
}
}
);
} else {
storageSync.remove("enableHandToolOnLoad");
}
}
// Migration code for https://github.com/mozilla/pdf.js/pull/9479.
if (typeof items.disableTextLayer === "boolean") {
if (items.disableTextLayer) {
storageSync.set(
{
textLayerMode: 0,
},
function () {
if (!chrome.runtime.lastError) {
storageSync.remove([
"disableTextLayer",
"enhanceTextSelection",
]);
}
}
);
} else {
storageSync.remove(["disableTextLayer", "enhanceTextSelection"]);
}
}
// Migration code for https://github.com/mozilla/pdf.js/pull/10502.
if (typeof items.showPreviousViewOnLoad === "boolean") {
if (!items.showPreviousViewOnLoad) {
storageSync.set(
{
viewOnLoad: 1,
},
function () {
if (!chrome.runtime.lastError) {
storageSync.remove([
"showPreviousViewOnLoad",
"disablePageMode",
]);
}
}
);
} else {
storageSync.remove(["showPreviousViewOnLoad", "disablePageMode"]);
}
}
}
);
}
});

View File

@@ -0,0 +1,185 @@
<!doctype html>
<!--
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.
-->
<html>
<head>
<meta charset="utf-8">
<title>PDF.js viewer options</title>
<style>
body {
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 */
}
.settings-row {
margin: 1em 0;
}
.checkbox label {
display: inline-flex;
align-items: center;
}
.checkbox label input {
flex-shrink: 0;
}
</style>
</head>
<body>
<div id="settings-boxes"></div>
<button id="reset-button" type="button">Restore default settings</button>
<template id="checkbox-template">
<div class="settings-row checkbox">
<label>
<input type="checkbox">
<span></span>
</label>
</div>
</template>
<template id="viewerCssTheme-template">
<div class="settings-row">
<label>
<span></span>
<select>
<option value="0">Use system theme</option>
<option value="1">Light theme</option>
<option value="2">Dark theme</option>
</select>
</label>
</div>
</template>
<template id="viewOnLoad-template">
<div class="settings-row">
<label>
<span></span>
<select>
<option value="-1">Default</option>
<option value="0">Show previous position</option>
<option value="1">Show initial position</option>
</select>
</label>
</div>
</template>
<template id="defaultZoomValue-template">
<div class="settings-row">
<label>
<span></span>
<select>
<option value="auto" selected="selected">Automatic Zoom</option>
<option value="page-actual">Actual Size</option>
<option value="page-fit">Page Fit</option>
<option value="page-width">Page Width</option>
<option value="custom" class="custom-zoom" hidden></option>
<option value="50">50%</option>
<option value="75">75%</option>
<option value="100">100%</option>
<option value="125">125%</option>
<option value="150">150%</option>
<option value="200">200%</option>
<option value="300">300%</option>
<option value="400">400%</option>
</select>
</label>
</div>
</template>
<template id="sidebarViewOnLoad-template">
<div class="settings-row">
<label>
<span></span>
<select>
<option value="-1">Default</option>
<option value="0">Do not show sidebar</option>
<option value="1">Show thumbnails in sidebar</option>
<option value="2">Show document outline in sidebar</option>
<option value="3">Show attachments in sidebar</option>
</select>
</label>
</div>
</template>
<template id="cursorToolOnLoad-template">
<div class="settings-row">
<label>
<span></span>
<select>
<option value="0">Text selection tool</option>
<option value="1">Hand tool</option>
</select>
</label>
</div>
</template>
<template id="textLayerMode-template">
<div class="settings-row">
<label>
<span></span>
<select>
<option value="0">Disable text selection</option>
<option value="1">Enable text selection</option>
</select>
</label>
</div>
</template>
<template id="externalLinkTarget-template">
<div class="settings-row">
<label>
<span></span>
<select>
<option value="0">Default</option>
<option value="1">Current window/tab</option>
<option value="2">New window/tab</option>
<option value="3">Parent window/tab</option>
<option value="4">Top window/tab</option>
</select>
</label>
</div>
</template>
<template id="scrollModeOnLoad-template">
<div class="settings-row">
<label>
<span></span>
<select>
<option value="-1">Default</option>
<option value="3">Page scrolling</option>
<option value="0">Vertical scrolling</option>
<option value="1">Horizontal scrolling</option>
<option value="2">Wrapped scrolling</option>
</select>
</label>
</div>
</template>
<template id="spreadModeOnLoad-template">
<div class="settings-row">
<label>
<span></span>
<select>
<option value="-1">Default</option>
<option value="0">No spreads</option>
<option value="1">Odd spreads</option>
<option value="2">Even spreads</option>
</select>
</label>
</div>
</template>
<script src="options.js"></script>
</body>
</html>

View File

@@ -0,0 +1,210 @@
/*
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.
*/
"use strict";
var storageAreaName = chrome.storage.sync ? "sync" : "local";
var storageArea = chrome.storage[storageAreaName];
Promise.all([
new Promise(function getManagedPrefs(resolve) {
if (!chrome.storage.managed) {
resolve({});
return;
}
// Get preferences as set by the system administrator.
chrome.storage.managed.get(null, function (prefs) {
// Managed storage may be disabled, e.g. in Opera.
resolve(prefs || {});
});
}),
new Promise(function getUserPrefs(resolve) {
storageArea.get(null, function (prefs) {
resolve(prefs || {});
});
}),
new Promise(function getStorageSchema(resolve) {
// Get the storage schema - a dictionary of preferences.
var x = new XMLHttpRequest();
var schema_location = chrome.runtime.getManifest().storage.managed_schema;
x.open("get", chrome.runtime.getURL(schema_location));
x.onload = function () {
resolve(x.response.properties);
};
x.responseType = "json";
x.send();
}),
])
.then(function (values) {
var managedPrefs = values[0];
var userPrefs = values[1];
var schema = values[2];
function getPrefValue(prefName) {
if (prefName in userPrefs) {
return userPrefs[prefName];
} else if (prefName in managedPrefs) {
return managedPrefs[prefName];
}
return schema[prefName].default;
}
var prefNames = Object.keys(schema);
var renderPreferenceFunctions = {};
// Render options
prefNames.forEach(function (prefName) {
var prefSchema = schema[prefName];
if (!prefSchema.title) {
// Don't show preferences if the title is missing.
return;
}
// A DOM element with a method renderPreference.
var renderPreference;
if (prefSchema.type === "boolean") {
// Most prefs are booleans, render them in a generic way.
renderPreference = renderBooleanPref(
prefSchema.title,
prefSchema.description,
prefName
);
} else if (prefSchema.type === "integer" && prefSchema.enum) {
// Most other prefs are integer-valued enumerations, render them in a
// generic way too.
// Unlike the renderBooleanPref branch, each preference handled by this
// branch still needs its own template in options.html with
// id="$prefName-template".
renderPreference = renderEnumPref(prefSchema.title, prefName);
} else if (prefName === "defaultZoomValue") {
renderPreference = renderDefaultZoomValue(prefSchema.title);
} else {
// Should NEVER be reached. Only happens if a new type of preference is
// added to the storage manifest.
console.error("Don't know how to handle " + prefName + "!");
return;
}
renderPreference(getPrefValue(prefName));
renderPreferenceFunctions[prefName] = renderPreference;
});
// Names of preferences that are displayed in the UI.
var renderedPrefNames = Object.keys(renderPreferenceFunctions);
// Reset button to restore default settings.
document.getElementById("reset-button").onclick = function () {
userPrefs = {};
storageArea.remove(prefNames, function () {
renderedPrefNames.forEach(function (prefName) {
renderPreferenceFunctions[prefName](getPrefValue(prefName));
});
});
};
// Automatically update the UI when the preferences were changed elsewhere.
chrome.storage.onChanged.addListener(function (changes, areaName) {
var prefs = null;
if (areaName === storageAreaName) {
prefs = userPrefs;
} else if (areaName === "managed") {
prefs = managedPrefs;
}
if (prefs) {
renderedPrefNames.forEach(function (prefName) {
var prefChanges = changes[prefName];
if (prefChanges) {
if ("newValue" in prefChanges) {
userPrefs[prefName] = prefChanges.newValue;
} else {
// Otherwise the pref was deleted
delete userPrefs[prefName];
}
renderPreferenceFunctions[prefName](getPrefValue(prefName));
}
});
}
});
})
.then(null, console.error.bind(console));
function importTemplate(id) {
return document.importNode(document.getElementById(id).content, true);
}
// Helpers to create UI elements that display the preference, and return a
// function which updates the UI with the preference.
function renderBooleanPref(shortDescription, description, prefName) {
var wrapper = importTemplate("checkbox-template");
wrapper.title = description;
var checkbox = wrapper.querySelector('input[type="checkbox"]');
checkbox.onchange = function () {
var pref = {};
pref[prefName] = this.checked;
storageArea.set(pref);
};
wrapper.querySelector("span").textContent = shortDescription;
document.getElementById("settings-boxes").append(wrapper);
function renderPreference(value) {
checkbox.checked = value;
}
return renderPreference;
}
function renderEnumPref(shortDescription, prefName) {
var wrapper = importTemplate(prefName + "-template");
var select = wrapper.querySelector("select");
select.onchange = function () {
var pref = {};
pref[prefName] = parseInt(this.value);
storageArea.set(pref);
};
wrapper.querySelector("span").textContent = shortDescription;
document.getElementById("settings-boxes").append(wrapper);
function renderPreference(value) {
select.value = value;
}
return renderPreference;
}
function renderDefaultZoomValue(shortDescription) {
var wrapper = importTemplate("defaultZoomValue-template");
var select = wrapper.querySelector("select");
select.onchange = function () {
storageArea.set({
defaultZoomValue: this.value,
});
};
wrapper.querySelector("span").textContent = shortDescription;
document.getElementById("settings-boxes").append(wrapper);
function renderPreference(value) {
value = value || "auto";
select.value = value;
var customOption = select.querySelector("option.custom-zoom");
if (select.selectedIndex === -1 && value) {
// Custom zoom percentage, e.g. set via managed preferences.
// [zoom] or [zoom],[left],[top]
customOption.text = value.indexOf(",") > 0 ? value : value + "%";
customOption.value = value;
customOption.hidden = false;
customOption.selected = true;
} else {
customOption.hidden = true;
}
}
return renderPreference;
}

View 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;
});

View File

@@ -0,0 +1,252 @@
{
"type": "object",
"properties": {
"viewerCssTheme": {
"title": "Theme",
"description": "The theme to use.\n0 = Use system theme.\n1 = Light theme.\n2 = Dark theme.",
"type": "integer",
"enum": [0, 1, 2],
"default": 2
},
"showPreviousViewOnLoad": {
"description": "DEPRECATED. Set viewOnLoad to 1 to disable showing the last page/position on load.",
"type": "boolean",
"default": true
},
"viewOnLoad": {
"title": "View position on load",
"description": "The position in the document upon load.\n -1 = Default (uses OpenAction if available, otherwise equal to `viewOnLoad = 0`).\n 0 = The last viewed page/position.\n 1 = The initial page/position.",
"type": "integer",
"enum": [-1, 0, 1],
"default": 0
},
"defaultZoomDelay": {
"title": "Default zoom delay",
"description": "Delay (in ms) to wait before redrawing the canvas.",
"type": "integer",
"default": 400
},
"defaultZoomValue": {
"title": "Default zoom level",
"description": "Default zoom level of the viewer. Accepted values: 'auto', 'page-actual', 'page-width', 'page-height', 'page-fit', or a zoom level in percents.",
"type": "string",
"pattern": "|auto|page-actual|page-width|page-height|page-fit|[0-9]+\\.?[0-9]*(,[0-9]+\\.?[0-9]*){0,2}",
"default": ""
},
"sidebarViewOnLoad": {
"title": "Sidebar state on load",
"description": "Controls the state of the sidebar upon load.\n -1 = Default (uses PageMode if available, otherwise the last position if available/enabled).\n 0 = Do not show sidebar.\n 1 = Show thumbnails in sidebar.\n 2 = Show document outline in sidebar.\n 3 = Show attachments in sidebar.",
"type": "integer",
"enum": [-1, 0, 1, 2, 3],
"default": -1
},
"enableHandToolOnLoad": {
"description": "DEPRECATED. Set cursorToolOnLoad to 1 to enable the hand tool by default.",
"type": "boolean",
"default": false
},
"enableHWA": {
"title": "Enable hardware acceleration",
"description": "Whether to enable hardware acceleration.",
"type": "boolean",
"default": true
},
"enableAltText": {
"type": "boolean",
"default": false
},
"enableGuessAltText": {
"type": "boolean",
"default": true
},
"enableAltTextModelDownload": {
"type": "boolean",
"default": true
},
"enableNewAltTextWhenAddingImage": {
"type": "boolean",
"default": true
},
"altTextLearnMoreUrl": {
"type": "string",
"default": ""
},
"commentLearnMoreUrl": {
"type": "string",
"default": ""
},
"enableSignatureEditor": {
"type": "boolean",
"default": false
},
"enableUpdatedAddImage": {
"type": "boolean",
"default": false
},
"cursorToolOnLoad": {
"title": "Cursor tool on load",
"description": "The cursor tool that is enabled upon load.\n 0 = Text selection tool.\n 1 = Hand tool.",
"type": "integer",
"enum": [0, 1],
"default": 0
},
"pdfBugEnabled": {
"title": "Enable debugging tools",
"description": "Whether to enable debugging tools.",
"type": "boolean",
"default": false
},
"enableScripting": {
"title": "Enable active content (JavaScript) in PDFs",
"type": "boolean",
"description": "Whether to allow execution of active content (JavaScript) by PDF files.",
"default": false
},
"enableHighlightFloatingButton": {
"type": "boolean",
"default": false
},
"highlightEditorColors": {
"type": "string",
"default": "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F,yellow_HCM=#FFFFCC,green_HCM=#53FFBC,blue_HCM=#80EBFF,pink_HCM=#F6B8FF,red_HCM=#C50043"
},
"disableRange": {
"title": "Disable range requests",
"description": "Whether to disable range requests (not recommended).",
"type": "boolean",
"default": false
},
"disableStream": {
"title": "Disable streaming for requests",
"description": "Whether to disable streaming for requests (not recommended).",
"type": "boolean",
"default": false
},
"disableAutoFetch": {
"type": "boolean",
"default": false
},
"disableFontFace": {
"title": "Disable @font-face",
"description": "Whether to disable @font-face and fall back to canvas rendering (this is more resource-intensive).",
"type": "boolean",
"default": false
},
"disableTextLayer": {
"description": "DEPRECATED. Set textLayerMode to 0 to disable the text selection layer by default.",
"type": "boolean",
"default": false
},
"textLayerMode": {
"title": "Text layer mode",
"description": "Controls if the text layer is enabled, and the selection mode that is used.\n 0 = Disabled.\n 1 = Enabled.",
"type": "integer",
"enum": [0, 1],
"default": 1
},
"externalLinkTarget": {
"title": "External links target window",
"description": "Controls how external links will be opened.\n 0 = default.\n 1 = replaces current window.\n 2 = new window/tab.\n 3 = parent.\n 4 = in top window.",
"type": "integer",
"enum": [0, 1, 2, 3, 4],
"default": 0
},
"disablePageLabels": {
"type": "boolean",
"default": false
},
"disablePageMode": {
"description": "DEPRECATED.",
"type": "boolean",
"default": false
},
"disableTelemetry": {
"title": "Disable telemetry",
"type": "boolean",
"description": "Whether to prevent the extension from reporting the extension and browser version to the extension developers.",
"default": false
},
"annotationMode": {
"type": "integer",
"enum": [0, 1, 2, 3],
"default": 2
},
"annotationEditorMode": {
"type": "integer",
"enum": [-1, 0, 3, 15],
"default": 0
},
"capCanvasAreaFactor": {
"type": "integer",
"default": 200
},
"enablePermissions": {
"type": "boolean",
"default": false
},
"enableXfa": {
"type": "boolean",
"default": true
},
"historyUpdateUrl": {
"type": "boolean",
"default": false
},
"ignoreDestinationZoom": {
"title": "Ignore the zoom argument in destinations",
"description": "When enabled it will maintain the currently active zoom level, rather than letting the PDF document modify it, when navigating to internal destinations.",
"type": "boolean",
"default": false
},
"enablePrintAutoRotate": {
"title": "Automatically rotate printed pages",
"description": "When enabled, landscape pages are rotated when printed.",
"type": "boolean",
"default": true
},
"scrollModeOnLoad": {
"title": "Scroll mode on load",
"description": "Controls how the viewer scrolls upon load.\n -1 = Default (uses the last position if available/enabled).\n 3 = Page scrolling.\n 0 = Vertical scrolling.\n 1 = Horizontal scrolling.\n 2 = Wrapped scrolling.",
"type": "integer",
"enum": [-1, 0, 1, 2, 3],
"default": -1
},
"spreadModeOnLoad": {
"title": "Spread mode on load",
"description": "Whether the viewer should join pages into spreads upon load.\n -1 = Default (uses the last position if available/enabled).\n 0 = No spreads.\n 1 = Odd spreads.\n 2 = Even spreads.",
"type": "integer",
"enum": [-1, 0, 1, 2],
"default": -1
},
"forcePageColors": {
"description": "When enabled, the pdf rendering will use the high contrast mode colors",
"type": "boolean",
"default": false
},
"pageColorsBackground": {
"description": "The color is a string as defined in CSS. Its goal is to help improve readability in high contrast mode",
"type": "string",
"default": "Canvas"
},
"pageColorsForeground": {
"description": "The color is a string as defined in CSS. Its goal is to help improve readability in high contrast mode",
"type": "string",
"default": "CanvasText"
},
"enableAutoLinking": {
"description": "Enable creation of hyperlinks from text that look like URLs.",
"type": "boolean",
"default": true
},
"enableComment": {
"description": "Enable creation of comment annotations.",
"type": "boolean",
"default": false
},
"enableOptimizedPartialRendering": {
"description": "Enable tracking of PDF operations to optimize partial rendering.",
"type": "boolean",
"default": false
}
}
}

View File

@@ -0,0 +1,159 @@
/*
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.
*/
"use strict";
/**
* This file is one part of the Referer persistency implementation. The other
* part resides in chromecom.js.
*
* This file collects Referer headers for every http(s) request, and temporarily
* stores the request headers in a dictionary, for REFERRER_IN_MEMORY_TIME ms.
*
* 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
* the HTTP requests created by this extension. When the port is disconnected,
* the webRequest listeners and the referrer information is discarded.
*
* See setReferer in chromecom.js for more explanation of this logic.
*/
/* exported canRequestBody */ // Used in pdfHandler.js
// g_referrers[tabId][frameId] = referrer of PDF frame.
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;
// g_postRequests[tabId] = Set of frameId that were loaded via POST.
var g_postRequests = {};
var rIsReferer = /^referer$/i;
chrome.webRequest.onSendHeaders.addListener(
function saveReferer(details) {
const { tabId, frameId, requestHeaders, method } = details;
g_referrers[tabId] ??= {};
g_referrers[tabId][frameId] = requestHeaders.find(h =>
rIsReferer.test(h.name)
)?.value;
setCanRequestBody(tabId, frameId, method !== "GET");
forgetReferrerEventually(tabId);
},
{ urls: ["*://*/*"], types: ["main_frame", "sub_frame"] },
["requestHeaders", "extraHeaders"]
);
function forgetReferrerEventually(tabId) {
if (g_referrerTimers[tabId]) {
clearTimeout(g_referrerTimers[tabId]);
}
g_referrerTimers[tabId] = setTimeout(() => {
delete g_referrers[tabId];
delete g_referrerTimers[tabId];
delete g_postRequests[tabId];
}, REFERRER_IN_MEMORY_TIME);
}
// Keeps track of whether a document in tabId + frameId is loaded through a
// 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
// to matching PDF resource requests (only if the Referer is non-empty). The
// handler is removed as soon as the PDF viewer frame is unloaded.
chrome.runtime.onConnect.addListener(function onReceivePort(port) {
if (port.name !== "chromecom-referrer") {
return;
}
var tabId = port.sender.tab.id;
var frameId = port.sender.frameId;
var dnrRequestId;
// 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]) || "";
port.onMessage.addListener(function (data) {
// If the viewer was opened directly (without opening a PDF URL first), then
// the background script does not know about g_referrers, but the viewer may
// know about the referer if stored in the history state (see chromecom.js).
if (data.referer) {
referer = data.referer;
}
dnrRequestId = data.dnrRequestId;
setStickyReferrer(dnrRequestId, tabId, data.requestUrl, referer, () => {
// Acknowledge the message, and include the latest referer for this frame.
port.postMessage(referer);
});
});
// The port is only disconnected when the other end reloads.
port.onDisconnect.addListener(function () {
unsetStickyReferrer(dnrRequestId);
});
});
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],
});
}
}

View File

@@ -0,0 +1,29 @@
/*
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.
*/
"use strict";
// Do not reload the extension when an update becomes available, UNLESS the PDF
// viewer is not displaying any PDF files. Otherwise the tabs would close, which
// is quite disruptive (crbug.com/511670).
chrome.runtime.onUpdateAvailable.addListener(function () {
chrome.tabs.query({ url: chrome.runtime.getURL("*") }, tabs => {
if (tabs?.length) {
return;
}
chrome.runtime.reload();
});
});

View File

@@ -0,0 +1,187 @@
/*
Copyright 2016 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.
*/
/* eslint strict: ["error", "function"] */
(function () {
"use strict";
// This module sends the browser and extension version to a server, to
// determine whether it is safe to drop support for old Chrome versions in
// future extension updates.
//
// The source code for the server is available at:
// https://github.com/Rob--W/pdfjs-telemetry
var LOG_URL = "https://pdfjs.robwu.nl/logpdfjs";
// The minimum time to wait before sending a ping, so that we don't send too
// many requests even if the user restarts their browser very often.
// We want one ping a day, so a minimum delay of 12 hours should be OK.
var MINIMUM_TIME_BETWEEN_PING = 12 * 36e5;
if (chrome.extension.inIncognitoContext) {
// The extension uses incognito split mode, so there are two background
// pages. Only send telemetry when not in incognito mode.
return;
}
if (chrome.runtime.id !== "oemmndcbldboiebfnladdacbdfmadadm") {
// Only send telemetry for the official PDF.js extension.
console.warn("Disabled telemetry because this is not an official build.");
return;
}
// The localStorage API is unavailable in service workers. We store data in
// 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() {
getLoggingPref(function (didOptOut) {
if (didOptOut) {
// Respect the user's decision to not send statistics.
return;
}
if (!navigator.onLine) {
// No network available; Wait until the next scheduled ping opportunity.
// Even if onLine is true, the server may still be unreachable. But
// because it is impossible to tell whether a request failed due to the
// inability to connect, or a deliberate connection termination by the
// server, we don't validate the response and assume that the request
// succeeded. This ensures that the server cannot ask the client to
// send more pings.
return;
}
doSendPing();
});
}
function doSendPing() {
chrome.storage.local.get(localStorage, items => {
Object.assign(localStorage, items);
var lastTime = parseInt(localStorage.telemetryLastTime) || 0;
var wasUpdated = didUpdateSinceLastCheck();
if (!wasUpdated && Date.now() - lastTime < MINIMUM_TIME_BETWEEN_PING) {
return;
}
updateLocalStorage("telemetryLastTime", Date.now());
var deduplication_id = getDeduplicationId(wasUpdated);
var extension_version = chrome.runtime.getManifest().version;
fetch(LOG_URL, {
method: "POST",
headers: new Headers({
"Deduplication-Id": deduplication_id,
"Extension-Version": extension_version,
}),
// Set mode=cors so that the above custom headers are included in the
// request.
mode: "cors",
// Omits credentials such as cookies in the requests, which guarantees
// that the server cannot track the client via HTTP cookies.
credentials: "omit",
cache: "no-store",
});
});
}
/**
* Generate a 40-bit hexadecimal string (=10 letters, 1.1E12 possibilities).
* This is used by the server to discard duplicate entries of the same browser
* version when the log data is aggregated.
*/
function getDeduplicationId(wasUpdated) {
var id = localStorage.telemetryDeduplicationId;
// The ID is only used to deduplicate reports for the same browser version,
// so it is OK to change the ID if the browser is updated. By changing the
// ID, the server cannot track users for a long period even if it wants to.
if (!id || !/^[0-9a-f]{10}$/.test(id) || wasUpdated) {
id = "";
var buf = new Uint8Array(5);
crypto.getRandomValues(buf);
for (const c of buf) {
id += (c >>> 4).toString(16) + (c & 0xf).toString(16);
}
updateLocalStorage("telemetryDeduplicationId", id);
}
return id;
}
/**
* Returns whether the browser has received a major update since the last call
* to this function.
*/
function didUpdateSinceLastCheck() {
var chromeVersion = /Chrome\/(\d+)\./.exec(navigator.userAgent);
chromeVersion = chromeVersion && chromeVersion[1];
if (!chromeVersion || localStorage.telemetryLastVersion === chromeVersion) {
return false;
}
updateLocalStorage("telemetryLastVersion", chromeVersion);
return true;
}
/**
* Get the value of the telemetry preference. The callback is invoked with a
* boolean if a preference is found, and with the undefined value otherwise.
*/
function getLoggingPref(callback) {
// Try to look up the preference in the storage, in the following order:
var areas = ["sync", "local", "managed"];
next();
function next(result) {
var storageAreaName = areas.shift();
if (typeof result === "boolean" || !storageAreaName) {
callback(result);
return;
}
if (!chrome.storage[storageAreaName]) {
next();
return;
}
chrome.storage[storageAreaName].get("disableTelemetry", function (items) {
next(items && items.disableTelemetry);
});
}
}
})();