Merge pull request #4126 from Rob--W/crx-using-streams-api

Chromium extension using streamsPrivate API!
This commit is contained in:
Yury Delendik 2014-01-22 05:12:07 -08:00
commit ba1eb4d1d1
5 changed files with 343 additions and 3 deletions

View File

@ -12,12 +12,22 @@
"webRequest", "webRequestBlocking",
"<all_urls>",
"tabs",
"webNavigation"
"webNavigation",
"storage",
"streamsPrivate"
],
"content_scripts": [{
"matches": ["file://*/*"],
"matches": [
"http://*/*",
"https://*/*",
"ftp://*/*",
"file://*/*"
],
"js": ["nothing.js"]
}],
"mime_types": [
"application/pdf"
],
"background": {
"page": "pdfHandler.html"
},

View File

@ -0,0 +1,249 @@
/* -*- 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 */
(function() {
'use strict';
if (!chrome.streamsPrivate) {
// Aww, PDF.js is still not whitelisted... See http://crbug.com/326949
console.warn('streamsPrivate not available, PDF from FTP or POST ' +
'requests will not be displayed using this extension! ' +
'See http://crbug.com/326949');
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message && message.action === 'getPDFStream') {
sendResponse();
}
});
return;
}
//
// Stream URL storage manager
//
// Hash map of "<tab id>": { "<pdf url>": ["<stream url>", ...], ... }
var urlToStream = {};
chrome.streamsPrivate.onExecuteMimeTypeHandler.addListener(handleStream);
// Chrome before 27 does not support tabIds on stream events.
var streamSupportsTabId = true;
// "tabId" used for Chrome before 27.
var STREAM_NO_TABID = 0;
function hasStream(tabId, pdfUrl) {
var streams = urlToStream[streamSupportsTabId ? tabId : STREAM_NO_TABID];
return streams && streams[pdfUrl] && streams[pdfUrl].length > 0;
}
/**
* Get stream URL for a given tabId and PDF url. The retrieved stream URL
* will be removed from the list.
* @return {string|undefined} The blob:-URL
*/
function getStream(tabId, pdfUrl) {
if (!streamSupportsTabId) tabId = STREAM_NO_TABID;
if (hasStream(tabId, pdfUrl)) {
var streamUrl = urlToStream[tabId][pdfUrl].shift();
if (urlToStream[tabId][pdfUrl].length === 0) {
delete urlToStream[tabId][pdfUrl];
if (Object.keys(urlToStream[tabId]).length === 0) {
delete urlToStream[tabId];
}
}
return streamUrl;
}
}
function setStream(tabId, pdfUrl, streamUrl) {
tabId = tabId || STREAM_NO_TABID;
if (!urlToStream[tabId]) urlToStream[tabId] = {};
if (!urlToStream[tabId][pdfUrl]) urlToStream[tabId][pdfUrl] = [];
urlToStream[tabId][pdfUrl].push(streamUrl);
}
// http://crbug.com/276898 - the onExecuteMimeTypeHandler event is sometimes
// dispatched in the wrong incognito profile. To work around the bug, transfer
// the stream information from the incognito session when the bug is detected.
function transferStreamToIncognitoProfile(tabId, pdfUrl) {
if (chrome.extension.inIncognitoContext) {
console.log('Already within incognito profile. Aborted stream transfer.');
return;
}
var streamUrl = getStream(tabId, pdfUrl);
console.log('Attempting to transfer stream info to a different profile...');
var itemId = 'streamInfo:' + window.performance.now();
var items = {};
items[itemId] = {
tabId: tabId,
pdfUrl: pdfUrl,
streamUrl: streamUrl
};
// The key will be removed whenever an incognito session is started,
// or when an incognito session is active.
chrome.storage.local.set(items, function() {
chrome.extension.isAllowedIncognitoAccess(function(isAllowedAccess) {
if (!isAllowedAccess) {
// If incognito is disabled, forget about the stream.
console.warn('Incognito is disabled, unexpected unknown stream.');
chrome.storage.local.remove(items);
}
});
});
}
if (chrome.extension.inIncognitoContext) {
var importStream = function(itemId, streamInfo) {
if (itemId.lastIndexOf('streamInfo:', 0) !== 0) return;
console.log('Importing stream info from non-incognito profile', streamInfo);
handleStream('', streamInfo.pdfUrl, streamInfo.streamUrl, streamInfo.tabId);
chrome.storage.local.remove(itemId);
};
var handleStorageItems = function(items) {
Object.keys(items).forEach(function(itemId) {
var item = items[itemId];
if (item.oldValue && !item.newValue) return; // storage remove event
if (item.newValue) item = item.newValue; // storage setter event
importStream(itemId, item);
});
};
// Parse information that was set before the event pages were ready.
chrome.storage.local.get(null, handleStorageItems);
chrome.storage.onChanged.addListener(handleStorageItems);
}
// End of work-around for crbug 276898
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message && message.action === 'getPDFStream') {
var pdfUrl = message.data;
var streamUrl = getStream(sender.tab.id, pdfUrl);
sendResponse({
streamUrl: streamUrl
});
}
});
//
// PDF detection and activation of PDF viewer.
//
/**
* 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);
streamSupportsTabId = typeof tabId === 'number';
setStream(tabId, pdfUrl, streamUrl);
if (!tabId) { // Chrome doesn't set the tabId before v27
// PDF.js targets Chrome 28+ because of fatal bugs in incognito mode
// for older versions of Chrome. So, don't bother implementing a fallback.
// For those who are interested, either loop through all tabs, or use the
// webNavigation.onBeforeNavigate event to map pdfUrls to tab + frame IDs.
return;
}
// Check if the frame has already been rendered.
chrome.webNavigation.getAllFrames({
tabId: tabId
}, function(details) {
if (details) {
details = details.filter(function(frame) {
return frame.url === pdfUrl;
});
if (details.length > 0) {
if (details.length !== 1) {
// (Rare case) Multiple frames with same URL.
// TODO(rob): Find a better way to handle this case
// (e.g. open in new tab).
console.warn('More than one frame found for tabId ' + tabId +
' with URL ' + pdfUrl + '. Using first frame.');
}
details = details[0];
details = {
tabId: tabId,
frameId: details.frameId,
url: details.url
};
handleWebNavigation(details);
} else {
console.warn('No webNavigation frames found for tabId ' + tabId);
}
} else {
console.warn('Unable to get frame information for tabId ' + tabId);
// This branch may occur when a new incognito session is launched.
// The event is dispatched in the non-incognito session while it should
// be dispatched in the incognito session. See http://crbug.com/276898
transferStreamToIncognitoProfile(tabId, pdfUrl);
}
});
}
/**
* This method is called 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.
*/
function handleWebNavigation(details) {
var tabId = details.tabId;
var frameId = details.frameId;
var pdfUrl = details.url;
if (!hasStream(tabId, pdfUrl)) {
console.log('No PDF stream found in tab ' + tabId + ' 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);
}
});
}
}
})();

View File

@ -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>

46
web/chromecom.js Normal file
View File

@ -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);
}
}
};
})();

View File

@ -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;
@ -1797,9 +1801,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() {