From cdadb0db4d5eaaa2d7bd62a5551d191eeb598ef4 Mon Sep 17 00:00:00 2001
From: Rob Wu <gwnRob@gmail.com>
Date: Tue, 13 Aug 2013 18:28:13 +0200
Subject: [PATCH] Proof of concept using chrome.streamsPrivate API

This method captures all application/pdf streams, loads the viewer
and passes the stream to the PDF.js viewer.

This commit shows a proof of concept using the chrome.streamsPrivate API.

Advantages of new method:
- Access to the response body of the original request, thus fewer
  network requests.
- PDFs from non-GET requests (e.g. POST) are now supported.
- FTP files are also supported.

Possible improvements:
- Use declared content scripts instead of dynamic chrome.tabs.executeScript.
  This allows the extension to render the viewer in frames when the
  extension is disallowed to run executeScript for the top URL.
- Use chrome.declarativeWebRequest instead of webRequest, and replace
  background page with event page (don't forget to profile the
  difference & will the background/event page still work as intended?).
---
 extensions/chromium/manifest.json    |  12 ++-
 extensions/chromium/pdfHandler-v2.js | 105 +++++++++++++++++++++++++++
 extensions/chromium/pdfHandler.html  |   1 +
 web/chromecom.js                     |  46 ++++++++++++
 web/viewer.js                        |  36 ++++++++-
 5 files changed, 198 insertions(+), 2 deletions(-)
 create mode 100644 extensions/chromium/pdfHandler-v2.js
 create mode 100644 web/chromecom.js

diff --git a/extensions/chromium/manifest.json b/extensions/chromium/manifest.json
index b709b780d..f19986246 100644
--- a/extensions/chromium/manifest.json
+++ b/extensions/chromium/manifest.json
@@ -12,12 +12,22 @@
     "webRequest", "webRequestBlocking",
     "<all_urls>",
     "tabs",
-    "webNavigation"
+    "webNavigation",
+    "streamsPrivate"
   ],
+  /* FOR demo & debugging purposes only! This key is required to get access to the streams API.
+   * This key forces the extension ID to be gbkeegbaiigmenfmjfclcdgdpimamgkj (= Chrome Office Viewer)
+   * This comment has been added to prevent it from being uploaded to the Chrome Web Store.
+   * Remove it when the PDF.js extensionID is whitelisted for the streamsPrivate API.
+   */
+  "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4zyYTii0VTKI7W2U6fDeAvs3YCVZeAt7C62IC64IDCMHvWy7SKMpOPjfg5v1PgYkFm+fGsCsVLN8NaF7fzYMVtjLc5bqhqPAi56Qidrqh1HxPAAYhwFQd5BVGhZmh1fySHXFPE8VI2tIHwRrASOtx67jbSEk4nBAcJz6n+eGq8QIDAQAB",
   "content_scripts": [{
     "matches": ["file://*/*"],
     "js": ["nothing.js"]
   }],
+  "mime_types": [
+    "application/pdf"
+  ],
   "background": {
     "page": "pdfHandler.html"
   },
diff --git a/extensions/chromium/pdfHandler-v2.js b/extensions/chromium/pdfHandler-v2.js
new file mode 100644
index 000000000..155eb0e53
--- /dev/null
+++ b/extensions/chromium/pdfHandler-v2.js
@@ -0,0 +1,105 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/*
+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.
+*/
+/* globals chrome, URL, getViewerURL */
+
+'use strict';
+
+// Hash map of "<pdf url>": "<stream url>"
+var urlToStream = {};
+
+// Note: Execution of this script stops when the streamsPrivate API is
+// not available, because an error will be thrown. Don't bother with
+// catching and handling the error, because it is a great way to see
+// when the streamsPrivate API is unavailable.
+chrome.streamsPrivate.onExecuteMimeTypeHandler.addListener(handleStream);
+
+chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
+  if (message && message.action === 'getPDFStream') {
+    var pdfUrl = message.data;
+    var streamUrl = urlToStream[pdfUrl];
+    // The stream can be used only once.
+    delete urlToStream[pdfUrl];
+    sendResponse({
+      streamUrl: streamUrl
+    });
+  }
+});
+
+/**
+ * Callback for when we receive a stream
+ *
+ * @param mimeType {string} The mime type of the incoming stream
+ * @param pdfUrl {string} The full URL to the file
+ * @param streamUrl {string} The url pointing to the open stream
+ * @param tabId {number} The ID of the tab in which the stream has been opened
+ *                       (undefined before Chrome 27, http://crbug.com/225605)
+ */
+function handleStream(mimeType, pdfUrl, streamUrl, tabId) {
+  console.log('Intercepted ' + mimeType + ' in tab ' + tabId + ' with URL ' +
+              pdfUrl + '\nAvailable as: ' + streamUrl);
+  urlToStream[pdfUrl] = streamUrl;
+}
+
+/**
+ * Callback for when a navigation error has occurred.
+ * This event is triggered when the chrome.streamsPrivate API has intercepted
+ *  the PDF stream. This method detects such streams, finds the frame where
+ *  the request was made, and loads the viewer in that frame.
+ *
+ * @param details {object}
+ * @param details.tabId {number} The ID of the tab
+ * @param details.url {string} The URL being navigated when the error occurred.
+ * @param details.frameId {number} 0 indicates the navigation happens in the tab
+ *                                 content window; a positive value indicates
+ *                                 navigation in a subframe.
+ * @param details.error {string}
+ */
+function webNavigationOnErrorOccurred(details) {
+  var tabId = details.tabId;
+  var frameId = details.frameId;
+  var pdfUrl = details.url;
+
+  if (details.error === 'net::ERR_ABORTED') {
+    if (!urlToStream[pdfUrl]) {
+      console.log('No saved PDF stream found for ' + pdfUrl);
+      return;
+    }
+    var viewerUrl = getViewerURL(pdfUrl);
+
+    if (frameId === 0) { // Main frame
+      console.log('Going to render PDF Viewer in main frame for ' + pdfUrl);
+      chrome.tabs.update(tabId, {
+        url: viewerUrl
+      });
+    } else {
+      console.log('Going to render PDF Viewer in sub frame for ' + pdfUrl);
+      // Non-standard Chrome API. chrome.tabs.executeScriptInFrame and docs
+      // is available at https://github.com/Rob--W/chrome-api
+      chrome.tabs.executeScriptInFrame(tabId, {
+        frameId: frameId,
+        code: 'location.href = ' + JSON.stringify(viewerUrl) + ';'
+      }, function(result) {
+        if (!result) { // Did the tab disappear? Is the frame inaccessible?
+          console.warn('Frame not found, viewer not rendered in tab ' + tabId);
+        }
+      });
+    }
+  }
+}
+
+chrome.webNavigation.onErrorOccurred.addListener(webNavigationOnErrorOccurred);
diff --git a/extensions/chromium/pdfHandler.html b/extensions/chromium/pdfHandler.html
index dcb70cb31..df35754e0 100644
--- a/extensions/chromium/pdfHandler.html
+++ b/extensions/chromium/pdfHandler.html
@@ -17,3 +17,4 @@ limitations under the License.
 <script src="chrome.tabs.executeScriptInFrame.js"></script>
 <script src="pdfHandler.js"></script>
 <script src="extension-router.js"></script>
+<script src="pdfHandler-v2.js"></script>
diff --git a/web/chromecom.js b/web/chromecom.js
new file mode 100644
index 000000000..6e1a12b59
--- /dev/null
+++ b/web/chromecom.js
@@ -0,0 +1,46 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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.
+ */
+
+/* globals chrome */
+'use strict';
+
+var ChromeCom = (function ChromeComClosure() {
+  return {
+    /**
+     * Creates an event that the extension is listening for and will
+     * asynchronously respond by calling the callback.
+     * @param {String} action The action to trigger.
+     * @param {String} data Optional data to send.
+     * @param {Function} callback Optional response callback that will be called
+     * with one data argument. When the request cannot be handled, the callback
+     * is immediately invoked with no arguments.
+     */
+    request: function(action, data, callback) {
+      var message = {
+        action: action,
+        data: data
+      };
+      if (!chrome.runtime) {
+        console.error('chrome.runtime is undefined.');
+        if (callback) callback();
+      } else if (callback) {
+        chrome.runtime.sendMessage(message, callback);
+      } else {
+        chrome.runtime.sendMessage(message);
+      }
+    }
+  };
+})();
diff --git a/web/viewer.js b/web/viewer.js
index 516e75ba3..37e45a40e 100644
--- a/web/viewer.js
+++ b/web/viewer.js
@@ -79,6 +79,10 @@ var mozL10n = document.mozL10n || document.webL10n;
 //#include firefoxcom.js
 //#endif
 
+//#if CHROME
+//#include chromecom.js
+//#endif
+
 var cache = new Cache(CACHE_SIZE);
 var currentPageNumber = 1;
 
@@ -1770,9 +1774,39 @@ document.addEventListener('DOMContentLoaded', function webViewerLoad(evt) {
 //return;
 //#endif
 
-//#if !B2G
+//#if !B2G && !CHROME
   PDFView.open(file, 0);
 //#endif
+
+//#if CHROME
+//ChromeCom.request('getPDFStream', file, function(response) {
+//  if (response) {
+//    // We will only get a response when the streamsPrivate API is available.
+//
+//    var isFTPFile = /^ftp:/i.test(file);
+//    var streamUrl = response.streamUrl;
+//    if (streamUrl) {
+//      console.log('Found data stream for ' + file);
+//      // The blob stream can be used only once, so disable range requests.
+//      PDFJS.disableRange = true;
+//      PDFView.open(streamUrl, 0);
+//      PDFView.setTitleUsingUrl(file);
+//      return;
+//    }
+//    if (isFTPFile) {
+//      // Stream not found, and it's loaded from FTP. Reload the page, because
+//      // it is not possible to get resources over ftp using XMLHttpRequest.
+//      // NOTE: This will not lead to an infinite redirect loop, because
+//      // if the file exists, then the streamsPrivate API will capture the
+//      // stream and send back the response. If the stream does not exist, then
+//      // a "Webpage not available" error will be shown (not the PDF Viewer).
+//      location.replace(file);
+//      return;
+//    }
+//  }
+//  PDFView.open(file, 0);
+//});
+//#endif
 }, true);
 
 function updateViewarea() {