pdfHandler.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. /*
  2. Copyright 2012 Mozilla Foundation
  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. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. /* import-globals-from preserve-referer.js */
  14. "use strict";
  15. var VIEWER_URL = chrome.extension.getURL("content/web/viewer.html");
  16. function getViewerURL(pdf_url) {
  17. return VIEWER_URL + "?file=" + encodeURIComponent(pdf_url);
  18. }
  19. /**
  20. * @param {Object} details First argument of the webRequest.onHeadersReceived
  21. * event. The property "url" is read.
  22. * @returns {boolean} True if the PDF file should be downloaded.
  23. */
  24. function isPdfDownloadable(details) {
  25. if (details.url.includes("pdfjs.action=download")) {
  26. return true;
  27. }
  28. // Display the PDF viewer regardless of the Content-Disposition header if the
  29. // file is displayed in the main frame, since most often users want to view
  30. // a PDF, and servers are often misconfigured.
  31. // If the query string contains "=download", do not unconditionally force the
  32. // viewer to open the PDF, but first check whether the Content-Disposition
  33. // header specifies an attachment. This allows sites like Google Drive to
  34. // operate correctly (#6106).
  35. if (details.type === "main_frame" && !details.url.includes("=download")) {
  36. return false;
  37. }
  38. var cdHeader =
  39. details.responseHeaders &&
  40. getHeaderFromHeaders(details.responseHeaders, "content-disposition");
  41. return cdHeader && /^attachment/i.test(cdHeader.value);
  42. }
  43. /**
  44. * Get the header from the list of headers for a given name.
  45. * @param {Array} headers responseHeaders of webRequest.onHeadersReceived
  46. * @returns {undefined|{name: string, value: string}} The header, if found.
  47. */
  48. function getHeaderFromHeaders(headers, headerName) {
  49. for (const header of headers) {
  50. if (header.name.toLowerCase() === headerName) {
  51. return header;
  52. }
  53. }
  54. return undefined;
  55. }
  56. /**
  57. * Check if the request is a PDF file.
  58. * @param {Object} details First argument of the webRequest.onHeadersReceived
  59. * event. The properties "responseHeaders" and "url"
  60. * are read.
  61. * @returns {boolean} True if the resource is a PDF file.
  62. */
  63. function isPdfFile(details) {
  64. var header = getHeaderFromHeaders(details.responseHeaders, "content-type");
  65. if (header) {
  66. var headerValue = header.value.toLowerCase().split(";", 1)[0].trim();
  67. if (headerValue === "application/pdf") {
  68. return true;
  69. }
  70. if (headerValue === "application/octet-stream") {
  71. if (details.url.toLowerCase().indexOf(".pdf") > 0) {
  72. return true;
  73. }
  74. var cdHeader = getHeaderFromHeaders(
  75. details.responseHeaders,
  76. "content-disposition"
  77. );
  78. if (cdHeader && /\.pdf(["']|$)/i.test(cdHeader.value)) {
  79. return true;
  80. }
  81. }
  82. }
  83. return false;
  84. }
  85. /**
  86. * Takes a set of headers, and set "Content-Disposition: attachment".
  87. * @param {Object} details First argument of the webRequest.onHeadersReceived
  88. * event. The property "responseHeaders" is read and
  89. * modified if needed.
  90. * @returns {Object|undefined} The return value for the onHeadersReceived event.
  91. * Object with key "responseHeaders" if the headers
  92. * have been modified, undefined otherwise.
  93. */
  94. function getHeadersWithContentDispositionAttachment(details) {
  95. var headers = details.responseHeaders;
  96. var cdHeader = getHeaderFromHeaders(headers, "content-disposition");
  97. if (!cdHeader) {
  98. cdHeader = { name: "Content-Disposition" };
  99. headers.push(cdHeader);
  100. }
  101. if (!/^attachment/i.test(cdHeader.value)) {
  102. cdHeader.value = "attachment" + cdHeader.value.replace(/^[^;]+/i, "");
  103. return { responseHeaders: headers };
  104. }
  105. return undefined;
  106. }
  107. chrome.webRequest.onHeadersReceived.addListener(
  108. function (details) {
  109. if (details.method !== "GET") {
  110. // Don't intercept POST requests until http://crbug.com/104058 is fixed.
  111. return undefined;
  112. }
  113. if (!isPdfFile(details)) {
  114. return undefined;
  115. }
  116. if (isPdfDownloadable(details)) {
  117. // Force download by ensuring that Content-Disposition: attachment is set
  118. return getHeadersWithContentDispositionAttachment(details);
  119. }
  120. var viewerUrl = getViewerURL(details.url);
  121. // Implemented in preserve-referer.js
  122. saveReferer(details);
  123. return { redirectUrl: viewerUrl };
  124. },
  125. {
  126. urls: ["<all_urls>"],
  127. types: ["main_frame", "sub_frame"],
  128. },
  129. ["blocking", "responseHeaders"]
  130. );
  131. chrome.webRequest.onBeforeRequest.addListener(
  132. function (details) {
  133. if (isPdfDownloadable(details)) {
  134. return undefined;
  135. }
  136. var viewerUrl = getViewerURL(details.url);
  137. return { redirectUrl: viewerUrl };
  138. },
  139. {
  140. urls: [
  141. "file://*/*.pdf",
  142. "file://*/*.PDF",
  143. ...// Duck-typing: MediaError.prototype.message was added in Chrome 59.
  144. (MediaError.prototype.hasOwnProperty("message")
  145. ? []
  146. : [
  147. // Note: Chrome 59 has disabled ftp resource loading by default:
  148. // https://www.chromestatus.com/feature/5709390967472128
  149. "ftp://*/*.pdf",
  150. "ftp://*/*.PDF",
  151. ]),
  152. ],
  153. types: ["main_frame", "sub_frame"],
  154. },
  155. ["blocking"]
  156. );
  157. chrome.extension.isAllowedFileSchemeAccess(function (isAllowedAccess) {
  158. if (isAllowedAccess) {
  159. return;
  160. }
  161. // If the user has not granted access to file:-URLs, then the webRequest API
  162. // will not catch the request. It is still visible through the webNavigation
  163. // API though, and we can replace the tab with the viewer.
  164. // The viewer will detect that it has no access to file:-URLs, and prompt the
  165. // user to activate file permissions.
  166. chrome.webNavigation.onBeforeNavigate.addListener(
  167. function (details) {
  168. if (details.frameId === 0 && !isPdfDownloadable(details)) {
  169. chrome.tabs.update(details.tabId, {
  170. url: getViewerURL(details.url),
  171. });
  172. }
  173. },
  174. {
  175. url: [
  176. {
  177. urlPrefix: "file://",
  178. pathSuffix: ".pdf",
  179. },
  180. {
  181. urlPrefix: "file://",
  182. pathSuffix: ".PDF",
  183. },
  184. ],
  185. }
  186. );
  187. });
  188. chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
  189. if (message && message.action === "getParentOrigin") {
  190. // getParentOrigin is used to determine whether it is safe to embed a
  191. // sensitive (local) file in a frame.
  192. if (!sender.tab) {
  193. sendResponse("");
  194. return undefined;
  195. }
  196. // TODO: This should be the URL of the parent frame, not the tab. But
  197. // chrome-extension:-URLs are not visible in the webNavigation API
  198. // (https://crbug.com/326768), so the next best thing is using the tab's URL
  199. // for making security decisions.
  200. var parentUrl = sender.tab.url;
  201. if (!parentUrl) {
  202. sendResponse("");
  203. return undefined;
  204. }
  205. if (parentUrl.lastIndexOf("file:", 0) === 0) {
  206. sendResponse("file://");
  207. return undefined;
  208. }
  209. // The regexp should always match for valid URLs, but in case it doesn't,
  210. // just give the full URL (e.g. data URLs).
  211. var origin = /^[^:]+:\/\/[^/]+/.exec(parentUrl);
  212. sendResponse(origin ? origin[1] : parentUrl);
  213. return true;
  214. }
  215. if (message && message.action === "isAllowedFileSchemeAccess") {
  216. chrome.extension.isAllowedFileSchemeAccess(sendResponse);
  217. return true;
  218. }
  219. if (message && message.action === "openExtensionsPageForFileAccess") {
  220. var url = "chrome://extensions/?id=" + chrome.runtime.id;
  221. if (message.data.newTab) {
  222. chrome.tabs.create({
  223. windowId: sender.tab.windowId,
  224. index: sender.tab.index + 1,
  225. url,
  226. openerTabId: sender.tab.id,
  227. });
  228. } else {
  229. chrome.tabs.update(sender.tab.id, {
  230. url,
  231. });
  232. }
  233. }
  234. return undefined;
  235. });
  236. // Remove keys from storage that were once part of the deleted feature-detect.js
  237. chrome.storage.local.remove([
  238. "featureDetectLastUA",
  239. "webRequestRedirectUrl",
  240. "extensionSupportsFTP",
  241. ]);