chromecom.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. /* Copyright 2013 Mozilla Foundation
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. /* globals chrome */
  16. import { DefaultExternalServices, PDFViewerApplication } from "./app.js";
  17. import { AppOptions } from "./app_options.js";
  18. import { BasePreferences } from "./preferences.js";
  19. import { DownloadManager } from "./download_manager.js";
  20. import { GenericL10n } from "./genericl10n.js";
  21. import { GenericScripting } from "./generic_scripting.js";
  22. if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME")) {
  23. throw new Error(
  24. 'Module "pdfjs-web/chromecom" shall not be used outside CHROME build.'
  25. );
  26. }
  27. const ChromeCom = {
  28. /**
  29. * Creates an event that the extension is listening for and will
  30. * asynchronously respond by calling the callback.
  31. *
  32. * @param {string} action - The action to trigger.
  33. * @param {string} [data] - The data to send.
  34. * @param {Function} [callback] - Response callback that will be called with
  35. * one data argument. When the request cannot be handled, the callback is
  36. * immediately invoked with no arguments.
  37. */
  38. request(action, data, callback) {
  39. const message = {
  40. action,
  41. data,
  42. };
  43. if (!chrome.runtime) {
  44. console.error("chrome.runtime is undefined.");
  45. callback?.();
  46. } else if (callback) {
  47. chrome.runtime.sendMessage(message, callback);
  48. } else {
  49. chrome.runtime.sendMessage(message);
  50. }
  51. },
  52. /**
  53. * Resolves a PDF file path and attempts to detects length.
  54. *
  55. * @param {string} file - Absolute URL of PDF file.
  56. * @param {OverlayManager} overlayManager - Manager for the viewer overlays.
  57. * @param {Function} callback - A callback with resolved URL and file length.
  58. */
  59. resolvePDFFile(file, overlayManager, callback) {
  60. // Expand drive:-URLs to filesystem URLs (Chrome OS)
  61. file = file.replace(
  62. /^drive:/i,
  63. "filesystem:" + location.origin + "/external/"
  64. );
  65. if (/^https?:/.test(file)) {
  66. // Assumption: The file being opened is the file that was requested.
  67. // There is no UI to input a different URL, so this assumption will hold
  68. // for now.
  69. setReferer(file, function () {
  70. callback(file);
  71. });
  72. return;
  73. }
  74. if (/^file?:/.test(file)) {
  75. getEmbedderOrigin(function (origin) {
  76. // If the origin cannot be determined, let Chrome decide whether to
  77. // allow embedding files. Otherwise, only allow local files to be
  78. // embedded from local files or Chrome extensions.
  79. // Even without this check, the file load in frames is still blocked,
  80. // but this may change in the future (https://crbug.com/550151).
  81. if (origin && !/^file:|^chrome-extension:/.test(origin)) {
  82. PDFViewerApplication._documentError(
  83. "Blocked " +
  84. origin +
  85. " from loading " +
  86. file +
  87. ". Refused to load a local file in a non-local page " +
  88. "for security reasons."
  89. );
  90. return;
  91. }
  92. isAllowedFileSchemeAccess(function (isAllowedAccess) {
  93. if (isAllowedAccess) {
  94. callback(file);
  95. } else {
  96. requestAccessToLocalFile(file, overlayManager, callback);
  97. }
  98. });
  99. });
  100. return;
  101. }
  102. callback(file);
  103. },
  104. };
  105. function getEmbedderOrigin(callback) {
  106. const origin = window === top ? location.origin : location.ancestorOrigins[0];
  107. if (origin === "null") {
  108. // file:-URLs, data-URLs, sandboxed frames, etc.
  109. getParentOrigin(callback);
  110. } else {
  111. callback(origin);
  112. }
  113. }
  114. function getParentOrigin(callback) {
  115. ChromeCom.request("getParentOrigin", null, callback);
  116. }
  117. function isAllowedFileSchemeAccess(callback) {
  118. ChromeCom.request("isAllowedFileSchemeAccess", null, callback);
  119. }
  120. function isRuntimeAvailable() {
  121. try {
  122. // When the extension is reloaded, the extension runtime is destroyed and
  123. // the extension APIs become unavailable.
  124. if (chrome.runtime?.getManifest()) {
  125. return true;
  126. }
  127. } catch (e) {}
  128. return false;
  129. }
  130. function reloadIfRuntimeIsUnavailable() {
  131. if (!isRuntimeAvailable()) {
  132. location.reload();
  133. }
  134. }
  135. let chromeFileAccessOverlayPromise;
  136. function requestAccessToLocalFile(fileUrl, overlayManager, callback) {
  137. const dialog = document.getElementById("chromeFileAccessDialog");
  138. if (top !== window) {
  139. // When the extension reloads after receiving new permissions, the pages
  140. // have to be reloaded to restore the extension runtime. Auto-reload
  141. // frames, because users should not have to reload the whole page just to
  142. // update the viewer.
  143. // Top-level frames are closed by Chrome upon reload, so there is no need
  144. // for detecting unload of the top-level frame. Should this ever change
  145. // (crbug.com/511670), then the user can just reload the tab.
  146. window.addEventListener("focus", reloadIfRuntimeIsUnavailable);
  147. dialog.addEventListener("close", function () {
  148. window.removeEventListener("focus", reloadIfRuntimeIsUnavailable);
  149. reloadIfRuntimeIsUnavailable();
  150. });
  151. }
  152. chromeFileAccessOverlayPromise ||= overlayManager.register(
  153. dialog,
  154. /* canForceClose = */ true
  155. );
  156. chromeFileAccessOverlayPromise.then(function () {
  157. const iconPath = chrome.runtime.getManifest().icons[48];
  158. document.getElementById("chrome-pdfjs-logo-bg").style.backgroundImage =
  159. "url(" + chrome.runtime.getURL(iconPath) + ")";
  160. // Use Chrome's definition of UI language instead of PDF.js's #lang=...,
  161. // because the shown string should match the UI at chrome://extensions.
  162. // These strings are from chrome/app/resources/generated_resources_*.xtb.
  163. const i18nFileAccessLabel = PDFJSDev.json(
  164. "$ROOT/web/chrome-i18n-allow-access-to-file-urls.json"
  165. )[chrome.i18n.getUILanguage?.()];
  166. if (i18nFileAccessLabel) {
  167. document.getElementById("chrome-file-access-label").textContent =
  168. i18nFileAccessLabel;
  169. }
  170. const link = document.getElementById("chrome-link-to-extensions-page");
  171. link.href = "chrome://extensions/?id=" + chrome.runtime.id;
  172. link.onclick = function (e) {
  173. // Direct navigation to chrome:// URLs is blocked by Chrome, so we
  174. // have to ask the background page to open chrome://extensions/?id=...
  175. e.preventDefault();
  176. // Open in the current tab by default, because toggling the file access
  177. // checkbox causes the extension to reload, and Chrome will close all
  178. // tabs upon reload.
  179. ChromeCom.request("openExtensionsPageForFileAccess", {
  180. newTab: e.ctrlKey || e.metaKey || e.button === 1 || window !== top,
  181. });
  182. };
  183. // Show which file is being opened to help the user with understanding
  184. // why this permission request is shown.
  185. document.getElementById("chrome-url-of-local-file").textContent = fileUrl;
  186. document.getElementById("chrome-file-fallback").onchange = function () {
  187. const file = this.files[0];
  188. if (file) {
  189. const originalFilename = decodeURIComponent(fileUrl.split("/").pop());
  190. let originalUrl = fileUrl;
  191. if (originalFilename !== file.name) {
  192. const msg =
  193. "The selected file does not match the original file." +
  194. "\nOriginal: " +
  195. originalFilename +
  196. "\nSelected: " +
  197. file.name +
  198. "\nDo you want to open the selected file?";
  199. // eslint-disable-next-line no-alert
  200. if (!confirm(msg)) {
  201. this.value = "";
  202. return;
  203. }
  204. // There is no way to retrieve the original URL from the File object.
  205. // So just generate a fake path.
  206. originalUrl = "file:///fakepath/to/" + encodeURIComponent(file.name);
  207. }
  208. callback(URL.createObjectURL(file), file.size, originalUrl);
  209. overlayManager.close(dialog);
  210. }
  211. };
  212. overlayManager.open(dialog);
  213. });
  214. }
  215. if (window === top) {
  216. // Chrome closes all extension tabs (crbug.com/511670) when the extension
  217. // reloads. To counter this, the tab URL and history state is saved to
  218. // localStorage and restored by extension-router.js.
  219. // Unfortunately, the window and tab index are not restored. And if it was
  220. // the only tab in an incognito window, then the tab is not restored either.
  221. addEventListener("unload", function () {
  222. // If the runtime is still available, the unload is most likely a normal
  223. // tab closure. Otherwise it is most likely an extension reload.
  224. if (!isRuntimeAvailable()) {
  225. localStorage.setItem(
  226. "unload-" + Date.now() + "-" + document.hidden + "-" + location.href,
  227. JSON.stringify(history.state)
  228. );
  229. }
  230. });
  231. }
  232. // This port is used for several purposes:
  233. // 1. When disconnected, the background page knows that the frame has unload.
  234. // 2. When the referrer was saved in history.state.chromecomState, it is sent
  235. // to the background page.
  236. // 3. When the background page knows the referrer of the page, the referrer is
  237. // saved in history.state.chromecomState.
  238. let port;
  239. // Set the referer for the given URL.
  240. // 0. Background: If loaded via a http(s) URL: Save referer.
  241. // 1. Page -> background: send URL and referer from history.state
  242. // 2. Background: Bind referer to URL (via webRequest).
  243. // 3. Background -> page: Send latest referer and save to history.
  244. // 4. Page: Invoke callback.
  245. function setReferer(url, callback) {
  246. if (!port) {
  247. // The background page will accept the port, and keep adding the Referer
  248. // request header to requests to |url| until the port is disconnected.
  249. port = chrome.runtime.connect({ name: "chromecom-referrer" });
  250. }
  251. port.onDisconnect.addListener(onDisconnect);
  252. port.onMessage.addListener(onMessage);
  253. // Initiate the information exchange.
  254. port.postMessage({
  255. referer: window.history.state?.chromecomState,
  256. requestUrl: url,
  257. });
  258. function onMessage(referer) {
  259. if (referer) {
  260. // The background extracts the Referer from the initial HTTP request for
  261. // the PDF file. When the viewer is reloaded or when the user navigates
  262. // back and forward, the background page will not observe a HTTP request
  263. // with Referer. To make sure that the Referer is preserved, store it in
  264. // history.state, which is preserved across reloads/navigations.
  265. const state = window.history.state || {};
  266. state.chromecomState = referer;
  267. window.history.replaceState(state, "");
  268. }
  269. onCompleted();
  270. }
  271. function onDisconnect() {
  272. // When the connection fails, ignore the error and call the callback.
  273. port = null;
  274. callback();
  275. }
  276. function onCompleted() {
  277. port.onDisconnect.removeListener(onDisconnect);
  278. port.onMessage.removeListener(onMessage);
  279. callback();
  280. }
  281. }
  282. // chrome.storage.sync is not supported in every Chromium-derivate.
  283. // Note: The background page takes care of migrating values from
  284. // chrome.storage.local to chrome.storage.sync when needed.
  285. const storageArea = chrome.storage.sync || chrome.storage.local;
  286. class ChromePreferences extends BasePreferences {
  287. async _writeToStorage(prefObj) {
  288. return new Promise(resolve => {
  289. if (prefObj === this.defaults) {
  290. const keysToRemove = Object.keys(this.defaults);
  291. // If the storage is reset, remove the keys so that the values from
  292. // managed storage are applied again.
  293. storageArea.remove(keysToRemove, function () {
  294. resolve();
  295. });
  296. } else {
  297. storageArea.set(prefObj, function () {
  298. resolve();
  299. });
  300. }
  301. });
  302. }
  303. async _readFromStorage(prefObj) {
  304. return new Promise(resolve => {
  305. const getPreferences = defaultPrefs => {
  306. if (chrome.runtime.lastError) {
  307. // Managed storage not supported, e.g. in Opera.
  308. defaultPrefs = this.defaults;
  309. }
  310. storageArea.get(defaultPrefs, function (readPrefs) {
  311. resolve(readPrefs);
  312. });
  313. };
  314. if (chrome.storage.managed) {
  315. // Get preferences as set by the system administrator.
  316. // See extensions/chromium/preferences_schema.json for more information.
  317. // These preferences can be overridden by the user.
  318. // Deprecated preferences are removed from web/default_preferences.json,
  319. // but kept in extensions/chromium/preferences_schema.json for backwards
  320. // compatibility with managed preferences.
  321. const defaultManagedPrefs = Object.assign(
  322. {
  323. enableHandToolOnLoad: false,
  324. disableTextLayer: false,
  325. enhanceTextSelection: false,
  326. showPreviousViewOnLoad: true,
  327. disablePageMode: false,
  328. },
  329. this.defaults
  330. );
  331. chrome.storage.managed.get(defaultManagedPrefs, function (items) {
  332. items = items || defaultManagedPrefs;
  333. // Migration logic for deprecated preferences: If the new preference
  334. // is not defined by an administrator (i.e. the value is the same as
  335. // the default value), and a deprecated preference is set with a
  336. // non-default value, migrate the deprecated preference value to the
  337. // new preference value.
  338. // Never remove this, because we have no means of modifying managed
  339. // preferences.
  340. // Migration code for https://github.com/mozilla/pdf.js/pull/7635.
  341. if (items.enableHandToolOnLoad && !items.cursorToolOnLoad) {
  342. items.cursorToolOnLoad = 1;
  343. }
  344. delete items.enableHandToolOnLoad;
  345. // Migration code for https://github.com/mozilla/pdf.js/pull/9479.
  346. if (items.textLayerMode !== 1 && items.disableTextLayer) {
  347. items.textLayerMode = 0;
  348. }
  349. delete items.disableTextLayer;
  350. delete items.enhanceTextSelection;
  351. // Migration code for https://github.com/mozilla/pdf.js/pull/10502.
  352. if (!items.showPreviousViewOnLoad && !items.viewOnLoad) {
  353. items.viewOnLoad = 1;
  354. }
  355. delete items.showPreviousViewOnLoad;
  356. delete items.disablePageMode;
  357. getPreferences(items);
  358. });
  359. } else {
  360. // Managed storage not supported, e.g. in old Chromium versions.
  361. getPreferences(this.defaults);
  362. }
  363. });
  364. }
  365. }
  366. class ChromeExternalServices extends DefaultExternalServices {
  367. static initPassiveLoading(callbacks) {
  368. // defaultUrl is set in viewer.js
  369. ChromeCom.resolvePDFFile(
  370. AppOptions.get("defaultUrl"),
  371. PDFViewerApplication.overlayManager,
  372. function (url, length, originalUrl) {
  373. callbacks.onOpenWithURL(url, length, originalUrl);
  374. }
  375. );
  376. }
  377. static createDownloadManager() {
  378. return new DownloadManager();
  379. }
  380. static createPreferences() {
  381. return new ChromePreferences();
  382. }
  383. static createL10n(options) {
  384. return new GenericL10n(navigator.language);
  385. }
  386. static createScripting({ sandboxBundleSrc }) {
  387. return new GenericScripting(sandboxBundleSrc);
  388. }
  389. }
  390. PDFViewerApplication.externalServices = ChromeExternalServices;
  391. export { ChromeCom };