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