diff --git a/extensions/firefox/bootstrap.js b/extensions/firefox/bootstrap.js index ebd6ff682..6d1f3dd92 100644 --- a/extensions/firefox/bootstrap.js +++ b/extensions/firefox/bootstrap.js @@ -16,7 +16,8 @@ */ /* jshint esnext:true */ /* globals Components, Services, dump, XPCOMUtils, PdfStreamConverter, - PdfRedirector, APP_SHUTDOWN, DEFAULT_PREFERENCES */ + PdfRedirector, APP_SHUTDOWN, PdfjsChromeUtils, PdfjsContentUtils, + DEFAULT_PREFERENCES */ 'use strict'; @@ -108,10 +109,10 @@ Factory.prototype = { } }; -var pdfStreamConverterUrl = null; var pdfStreamConverterFactory = new Factory(); -var pdfRedirectorUrl = null; +var pdfBaseUrl = null; var pdfRedirectorFactory = new Factory(); +var e10sEnabled = false; // As of Firefox 13 bootstrapped add-ons don't support automatic registering and // unregistering of resource urls and components/contracts. Until then we do @@ -125,15 +126,20 @@ function startup(aData, aReason) { var aliasURI = ioService.newURI('content/', 'UTF-8', aData.resourceURI); resProt.setSubstitution(RESOURCE_NAME, aliasURI); + pdfBaseUrl = aData.resourceURI.spec; + + Cu.import(pdfBaseUrl + 'content/PdfjsChromeUtils.jsm'); + PdfjsChromeUtils.init(); + Cu.import(pdfBaseUrl + 'content/PdfjsContentUtils.jsm'); + PdfjsContentUtils.init(); + // Load the component and register it. - pdfStreamConverterUrl = aData.resourceURI.spec + - 'content/PdfStreamConverter.jsm'; + var pdfStreamConverterUrl = pdfBaseUrl + 'content/PdfStreamConverter.jsm'; Cu.import(pdfStreamConverterUrl); pdfStreamConverterFactory.register(PdfStreamConverter); if (registerOverlayPreview) { - pdfRedirectorUrl = aData.resourceURI.spec + - 'content/PdfRedirector.jsm'; + var pdfRedirectorUrl = pdfBaseUrl + 'content/PdfRedirector.jsm'; Cu.import(pdfRedirectorUrl); pdfRedirectorFactory.register(PdfRedirector); @@ -141,6 +147,14 @@ function startup(aData, aReason) { 'data:application/x-moz-playpreview-pdfjs;,'); } + try { + let globalMM = Cc['@mozilla.org/globalmessagemanager;1'] + .getService(Ci.nsIFrameScriptLoader); + globalMM.loadFrameScript('chrome://pdf.js/content/content.js', true); + e10sEnabled = true; + } catch (ex) { + } + initializeDefaultPreferences(); } @@ -148,6 +162,14 @@ function shutdown(aData, aReason) { if (aReason === APP_SHUTDOWN) { return; } + + if (e10sEnabled) { + let globalMM = Cc['@mozilla.org/globalmessagemanager;1'] + .getService(Ci.nsIMessageBroadcaster); + globalMM.broadcastAsyncMessage('PDFJS:Child:shutdown'); + globalMM.removeDelayedFrameScript('chrome://pdf.js/content/content.js'); + } + var ioService = Services.io; var resProt = ioService.getProtocolHandler('resource') .QueryInterface(Ci.nsIResProtocolHandler); @@ -156,16 +178,22 @@ function shutdown(aData, aReason) { // Remove the contract/component. pdfStreamConverterFactory.unregister(); // Unload the converter + var pdfStreamConverterUrl = pdfBaseUrl + 'content/PdfStreamConverter.jsm'; Cu.unload(pdfStreamConverterUrl); - pdfStreamConverterUrl = null; if (registerOverlayPreview) { pdfRedirectorFactory.unregister(); + var pdfRedirectorUrl = pdfBaseUrl + 'content/PdfRedirector.jsm'; Cu.unload(pdfRedirectorUrl); pdfRedirectorUrl = null; Ph.unregisterPlayPreviewMimeType('application/pdf'); } + + PdfjsContentUtils.uninit(); + Cu.unload(pdfBaseUrl + 'content/PdfjsContentUtils.jsm'); + PdfjsChromeUtils.uninit(); + Cu.unload(pdfBaseUrl + 'content/PdfjsChromeUtils.jsm'); } function install(aData, aReason) { diff --git a/extensions/firefox/chrome.manifest b/extensions/firefox/chrome.manifest index 97b76306b..1a92e73ac 100644 --- a/extensions/firefox/chrome.manifest +++ b/extensions/firefox/chrome.manifest @@ -1,3 +1,5 @@ # Additional resources for pdf.js +content pdf.js chrome/ + # PDFJS_SUPPORTED_LOCALES diff --git a/extensions/firefox/chrome/content.js b/extensions/firefox/chrome/content.js new file mode 100644 index 000000000..982aa7d8a --- /dev/null +++ b/extensions/firefox/chrome/content.js @@ -0,0 +1,98 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* 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. + */ +/* jshint esnext:true */ +/* globals Components, Services, XPCOMUtils, PdfjsContentUtils, + PdfjsContentUtils, PdfStreamConverter, addMessageListener */ + +'use strict'; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cm = Components.manager; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); + +var isRemote = Services.appinfo.processType === + Services.appinfo.PROCESS_TYPE_CONTENT; + +// Factory that registers/unregisters a constructor as a component. +function Factory() {} + +Factory.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]), + _targetConstructor: null, + + register: function register(targetConstructor) { + this._targetConstructor = targetConstructor; + var proto = targetConstructor.prototype; + var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(proto.classID, proto.classDescription, + proto.contractID, this); + }, + + unregister: function unregister() { + var proto = this._targetConstructor.prototype; + var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(proto.classID, this); + this._targetConstructor = null; + }, + + // nsIFactory + createInstance: function createInstance(aOuter, iid) { + if (aOuter !== null) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + return (new (this._targetConstructor)()).QueryInterface(iid); + }, + + // nsIFactory + lockFactory: function lockFactory(lock) { + // No longer used as of gecko 1.7. + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } +}; + +var pdfStreamConverterFactory = new Factory(); + +function startup() { + Cu.import('resource://pdf.js/PdfjsContentUtils.jsm'); + PdfjsContentUtils.init(); + + Cu.import('resource://pdf.js/PdfStreamConverter.jsm'); + pdfStreamConverterFactory.register(PdfStreamConverter); +} + +function shutdown() { + // Remove the contract/component. + pdfStreamConverterFactory.unregister(); + // Unload the converter + Cu.unload('resource://pdf.js/PdfStreamConverter.jsm'); + + PdfjsContentUtils.uninit(); + Cu.unload('resource://pdf.js/PdfjsContentUtils.jsm'); +} + +if (isRemote) { + startup(); + + addMessageListener('PDFJS:Child:shutdown', function (e) { + shutdown(); + }); +} diff --git a/extensions/firefox/content/PdfJs.jsm b/extensions/firefox/content/PdfJs.jsm index 9537ade28..679160e2e 100644 --- a/extensions/firefox/content/PdfJs.jsm +++ b/extensions/firefox/content/PdfJs.jsm @@ -42,6 +42,12 @@ XPCOMUtils.defineLazyServiceGetter(Svc, 'mime', XPCOMUtils.defineLazyServiceGetter(Svc, 'pluginHost', '@mozilla.org/plugin/host;1', 'nsIPluginHost'); +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PdfjsChromeUtils", + "resource://pdf.js/PdfjsChromeUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PdfjsContentUtils", + "resource://pdf.js/PdfjsContentUtils.jsm"); function getBoolPref(aPref, aDefaultValue) { try { @@ -59,6 +65,13 @@ function getIntPref(aPref, aDefaultValue) { } } +function isDefaultHandler() { + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + return PdfjsContentUtils.isDefaultHandlerApp(); + } + return PdfjsChromeUtils.isDefaultHandlerApp(); +} + function initializeDefaultPreferences() { //#include ../../../web/default_preferences.js @@ -105,17 +118,30 @@ Factory.prototype = { let PdfJs = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), _registered: false, + _initialized: false, + + init: function init(remote) { + if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { + throw new Error("PdfJs.init should only get called in the parent process."); + } + PdfjsChromeUtils.init(); + if (!remote) { + PdfjsContentUtils.init(); + } + this.initPrefs(); + this.updateRegistration(); + }, + + initPrefs: function initPrefs() { + if (this._initialized) { + return; + } + this._initialized = true; - init: function init() { if (!getBoolPref(PREF_DISABLED, true)) { this._migrate(); } - if (this.enabled) - this._ensureRegistered(); - else - this._ensureUnregistered(); - // Listen for when pdf.js is completely disabled or a different pdf handler // is chosen. Services.prefs.addObserver(PREF_DISABLED, this, false); @@ -127,6 +153,26 @@ let PdfJs = { initializeDefaultPreferences(); }, + updateRegistration: function updateRegistration() { + if (this.enabled) { + this._ensureRegistered(); + } else { + this._ensureUnregistered(); + } + }, + + uninit: function uninit() { + if (this._initialized) { + Services.prefs.removeObserver(PREF_DISABLED, this, false); + Services.prefs.removeObserver(PREF_DISABLED_PLUGIN_TYPES, this, false); + Services.obs.removeObserver(this, TOPIC_PDFJS_HANDLER_CHANGED, false); + Services.obs.removeObserver(this, TOPIC_PLUGINS_LIST_UPDATED, false); + Services.obs.removeObserver(this, TOPIC_PLUGIN_INFO_UPDATED, false); + this._initialized = false; + } + this._ensureUnregistered(); + }, + _migrate: function migrate() { const VERSION = 2; var currentVersion = getIntPref(PREF_MIGRATION_VERSION, 0); @@ -189,10 +235,12 @@ let PdfJs = { // nsIObserver observe: function observe(aSubject, aTopic, aData) { - if (this.enabled) - this._ensureRegistered(); - else - this._ensureUnregistered(); + this.updateRegistration(); + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { + let jsm = "resource://pdf.js/PdfjsChromeUtils.jsm"; + let PdfjsChromeUtils = Components.utils.import(jsm, {}).PdfjsChromeUtils; + PdfjsChromeUtils.notifyChildOfSettingsChange(); + } }, /** @@ -206,11 +254,8 @@ let PdfJs = { return false; } - // the 'application/pdf' handler is selected as internal? - var handlerInfo = Svc.mime - .getFromTypeAndExtension(PDF_CONTENT_TYPE, 'pdf'); - if (handlerInfo.alwaysAskBeforeHandling || - handlerInfo.preferredAction !== Ci.nsIHandlerInfo.handleInternally) { + // Check if the 'application/pdf' preview handler is configured properly. + if (!isDefaultHandler()) { return false; } diff --git a/extensions/firefox/content/PdfStreamConverter.jsm b/extensions/firefox/content/PdfStreamConverter.jsm index 086cd7cf1..1c2bb5257 100644 --- a/extensions/firefox/content/PdfStreamConverter.jsm +++ b/extensions/firefox/content/PdfStreamConverter.jsm @@ -48,6 +48,12 @@ XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils', XPCOMUtils.defineLazyModuleGetter(this, 'PdfJsTelemetry', 'resource://pdf.js/PdfJsTelemetry.jsm'); +XPCOMUtils.defineLazyModuleGetter(this, 'PdfjsContentUtils', + 'resource://pdf.js/PdfjsContentUtils.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + 'resource://gre/modules/BrowserUtils.jsm'); + var Svc = {}; XPCOMUtils.defineLazyServiceGetter(Svc, 'mime', '@mozilla.org/mime;1', @@ -61,10 +67,16 @@ function getContainingBrowser(domWindow) { } function getChromeWindow(domWindow) { + if (PdfjsContentUtils.isRemote) { + return PdfjsContentUtils.getChromeWindow(domWindow); + } return getContainingBrowser(domWindow).ownerDocument.defaultView; } function getFindBar(domWindow) { + if (PdfjsContentUtils.isRemote) { + return PdfjsContentUtils.getFindBar(domWindow); + } var browser = getContainingBrowser(domWindow); try { var tabbrowser = browser.getTabBrowser(); @@ -77,10 +89,6 @@ function getFindBar(domWindow) { } } -function setBoolPref(pref, value) { - Services.prefs.setBoolPref(pref, value); -} - function getBoolPref(pref, def) { try { return Services.prefs.getBoolPref(pref); @@ -89,10 +97,6 @@ function getBoolPref(pref, def) { } } -function setIntPref(pref, value) { - Services.prefs.setIntPref(pref, value); -} - function getIntPref(pref, def) { try { return Services.prefs.getIntPref(pref); @@ -101,13 +105,6 @@ function getIntPref(pref, def) { } } -function setStringPref(pref, value) { - var str = Cc['@mozilla.org/supports-string;1'] - .createInstance(Ci.nsISupportsString); - str.data = value; - Services.prefs.setComplexValue(pref, Ci.nsISupportsString, str); -} - function getStringPref(pref, def) { try { return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data; @@ -117,8 +114,6 @@ function getStringPref(pref, def) { } function log(aMsg) { - if (!getBoolPref(PREF_PREFIX + '.pdfBugEnabled', false)) - return; var msg = 'PdfStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg); Services.console.logStringMessage(msg); dump(msg + '\n'); @@ -438,53 +433,10 @@ ChromeActions.prototype = { } else { message = getLocalizedString(strings, 'unsupported_feature'); } - PdfJsTelemetry.onFallback(); - - var notificationBox = null; - try { - // Based on MDN's "Working with windows in chrome code" - var mainWindow = domWindow - .QueryInterface(Components.interfaces.nsIInterfaceRequestor) - .getInterface(Components.interfaces.nsIWebNavigation) - .QueryInterface(Components.interfaces.nsIDocShellTreeItem) - .rootTreeItem - .QueryInterface(Components.interfaces.nsIInterfaceRequestor) - .getInterface(Components.interfaces.nsIDOMWindow); - var browser = mainWindow.gBrowser - .getBrowserForDocument(domWindow.top.document); - notificationBox = mainWindow.gBrowser.getNotificationBox(browser); - } catch (e) { - log('Unable to get a notification box for the fallback message'); - return; - } - - // Flag so we don't call the response callback twice, since if the user - // clicks open with different viewer both the button callback and - // eventCallback will be called. - var sentResponse = false; - var buttons = [{ - label: getLocalizedString(strings, 'open_with_different_viewer'), - accessKey: getLocalizedString(strings, 'open_with_different_viewer', - 'accessKey'), - callback: function() { - sentResponse = true; - sendResponse(true); - } - }]; - notificationBox.appendNotification(message, 'pdfjs-fallback', null, - notificationBox.PRIORITY_INFO_LOW, - buttons, - function eventsCallback(eventType) { - // Currently there is only one event "removed" but if there are any other - // added in the future we still only care about removed at the moment. - if (eventType !== 'removed') - return; - // Don't send a response again if we already responded when the button was - // clicked. - if (!sentResponse) - sendResponse(false); - }); + PdfjsContentUtils.displayWarning(domWindow, message, sendResponse, + getLocalizedString(strings, 'open_with_different_viewer'), + getLocalizedString(strings, 'open_with_different_viewer', 'accessKey')); }, updateFindControlState: function(data) { if (!this.supportsIntegratedFind()) @@ -515,17 +467,17 @@ ChromeActions.prototype = { prefName = (PREF_PREFIX + '.' + key); switch (typeof prefValue) { case 'boolean': - setBoolPref(prefName, prefValue); + PdfjsContentUtils.setBoolPref(prefName, prefValue); break; case 'number': - setIntPref(prefName, prefValue); + PdfjsContentUtils.setIntPref(prefName, prefValue); break; case 'string': if (prefValue.length > MAX_STRING_PREF_LENGTH) { log('setPreferences - Exceeded the maximum allowed length ' + 'for a string preference.'); } else { - setStringPref(prefName, prefValue); + PdfjsContentUtils.setStringPref(prefName, prefValue); } break; } @@ -808,7 +760,12 @@ FindEventManager.prototype.handleEvent = function(e) { detail = makeContentReadable(detail, contentWindow); var forward = contentWindow.document.createEvent('CustomEvent'); forward.initCustomEvent(e.type, true, true, detail); - contentWindow.dispatchEvent(forward); + // Due to restrictions with cpow use, we can't dispatch + // dom events with an urgent message on the stack. So bounce + // this off the main thread to make it async. + Services.tm.mainThread.dispatch(function () { + contentWindow.dispatchEvent(forward); + }, Ci.nsIThread.DISPATCH_NORMAL); e.preventDefault(); } }; @@ -929,7 +886,6 @@ PdfStreamConverter.prototype = { PdfJsTelemetry.onViewerIsUsed(); PdfJsTelemetry.onDocumentSize(aRequest.contentLength); - // Creating storage for PDF data var contentLength = aRequest.contentLength; this.dataListener = new PdfDataListener(contentLength); diff --git a/extensions/firefox/content/PdfjsChromeUtils.jsm b/extensions/firefox/content/PdfjsChromeUtils.jsm new file mode 100644 index 000000000..89e995b3a --- /dev/null +++ b/extensions/firefox/content/PdfjsChromeUtils.jsm @@ -0,0 +1,313 @@ +/* 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 DEFAULT_PREFERENCES */ + +'use strict'; + +var EXPORTED_SYMBOLS = ['PdfjsChromeUtils']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +const PREF_PREFIX = 'pdfjs'; +const PDF_CONTENT_TYPE = 'application/pdf'; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); + +let Svc = {}; +XPCOMUtils.defineLazyServiceGetter(Svc, 'mime', + '@mozilla.org/mime;1', + 'nsIMIMEService'); + +//#include ../../../web/default_preferences.js + +let PdfjsChromeUtils = { + // For security purposes when running remote, we restrict preferences + // content can access. + _allowedPrefNames: Object.keys(DEFAULT_PREFERENCES), + _ppmm: null, + _mmg: null, + + /* + * Public API + */ + + init: function () { + if (!this._ppmm) { + // global parent process message manager (PPMM) + this._ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster); + this._ppmm.addMessageListener("PDFJS:Parent:clearUserPref", this); + this._ppmm.addMessageListener("PDFJS:Parent:setIntPref", this); + this._ppmm.addMessageListener("PDFJS:Parent:setBoolPref", this); + this._ppmm.addMessageListener("PDFJS:Parent:setCharPref", this); + this._ppmm.addMessageListener("PDFJS:Parent:setStringPref", this); + this._ppmm.addMessageListener("PDFJS:Parent:isDefaultHandlerApp", this); + + // global dom message manager (MMg) + this._mmg = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager); + this._mmg.addMessageListener("PDFJS:Parent:getChromeWindow", this); + this._mmg.addMessageListener("PDFJS:Parent:getFindBar", this); + this._mmg.addMessageListener("PDFJS:Parent:displayWarning", this); + + // observer to handle shutdown + Services.obs.addObserver(this, "quit-application", false); + } + }, + + uninit: function () { + if (this._ppmm) { + this._ppmm.removeMessageListener("PDFJS:Parent:clearUserPref", this); + this._ppmm.removeMessageListener("PDFJS:Parent:setIntPref", this); + this._ppmm.removeMessageListener("PDFJS:Parent:setBoolPref", this); + this._ppmm.removeMessageListener("PDFJS:Parent:setCharPref", this); + this._ppmm.removeMessageListener("PDFJS:Parent:setStringPref", this); + this._ppmm.removeMessageListener("PDFJS:Parent:isDefaultHandlerApp", this); + + this._mmg.removeMessageListener("PDFJS:Parent:getChromeWindow", this); + this._mmg.removeMessageListener("PDFJS:Parent:getFindBar", this); + this._mmg.removeMessageListener("PDFJS:Parent:displayWarning", this); + + Services.obs.removeObserver(this, "quit-application", false); + + this._mmg = null; + this._ppmm = null; + } + }, + + /* + * Called by the main module when preference changes are picked up + * in the parent process. Observers don't propagate so we need to + * instruct the child to refresh its configuration and (possibly) + * the module's registration. + */ + notifyChildOfSettingsChange: function () { + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT && + this._ppmm) { + // XXX kinda bad, we want to get the parent process mm associated + // with the content process. _ppmm is currently the global process + // manager, which means this is going to fire to every child process + // we have open. Unfortunately I can't find a way to get at that + // process specific mm from js. + this._ppmm.broadcastAsyncMessage("PDFJS:Child:refreshSettings", {}); + } + }, + + /* + * Events + */ + + observe: function(aSubject, aTopic, aData) { + if (aTopic == "quit-application") { + this.uninit(); + } + }, + + receiveMessage: function (aMsg) { + switch (aMsg.name) { + case "PDFJS:Parent:clearUserPref": + this._clearUserPref(aMsg.json.name); + break; + case "PDFJS:Parent:setIntPref": + this._setIntPref(aMsg.json.name, aMsg.json.value); + break; + case "PDFJS:Parent:setBoolPref": + this._setBoolPref(aMsg.json.name, aMsg.json.value); + break; + case "PDFJS:Parent:setCharPref": + this._setCharPref(aMsg.json.name, aMsg.json.value); + break; + case "PDFJS:Parent:setStringPref": + this._setStringPref(aMsg.json.name, aMsg.json.value); + break; + case "PDFJS:Parent:isDefaultHandlerApp": + return this.isDefaultHandlerApp(); + case "PDFJS:Parent:displayWarning": + this._displayWarning(aMsg); + break; + + // CPOW getters + case "PDFJS:Parent:getChromeWindow": + return this._getChromeWindow(aMsg); + case "PDFJS:Parent:getFindBar": + return this._getFindBar(aMsg); + } + }, + + /* + * Internal + */ + + _getChromeWindow: function (aMsg) { + // See the child module, our return result here can't be the element + // since return results don't get auto CPOW'd. + let browser = aMsg.target; + let wrapper = new PdfjsWindowWrapper(browser); + let suitcase = aMsg.objects.suitcase; + suitcase.setChromeWindow(wrapper); + return true; + }, + + _getFindBar: function (aMsg) { + // We send this over via the window's message manager, so target should + // be the dom window. + let browser = aMsg.target; + let wrapper = new PdfjsFindbarWrapper(browser); + let suitcase = aMsg.objects.suitcase; + suitcase.setFindBar(wrapper); + return true; + }, + + _isPrefAllowed: function (aPrefName) { + if (this._allowedPrefNames.indexOf(aPrefName) == -1) { + let msg = "'" + aPrefName + "' "; + msg += "can't be accessed from content. See PdfjsChromeUtils." + throw new Error(msg); + } + }, + + _clearUserPref: function (aPrefName) { + this._isPrefAllowed(aPrefName); + Services.prefs.clearUserPref(aPrefName); + }, + + _setIntPref: function (aPrefName, aPrefValue) { + this._isPrefAllowed(aPrefName); + Services.prefs.setIntPref(aPrefName, aPrefValue); + }, + + _setBoolPref: function (aPrefName, aPrefValue) { + this._isPrefAllowed(aPrefName); + Services.prefs.setBoolPref(aPrefName, aPrefValue); + }, + + _setCharPref: function (aPrefName, aPrefValue) { + this._isPrefAllowed(aPrefName); + Services.prefs.setCharPref(aPrefName, aPrefValue); + }, + + _setStringPref: function (aPrefName, aPrefValue) { + this._isPrefAllowed(aPrefName); + let str = Cc['@mozilla.org/supports-string;1'] + .createInstance(Ci.nsISupportsString); + str.data = aPrefValue; + Services.prefs.setComplexValue(aPrefName, Ci.nsISupportsString, str); + }, + + /* + * Svc.mime doesn't have profile information in the child, so + * we bounce this pdfjs enabled configuration check over to the + * parent. + */ + isDefaultHandlerApp: function () { + var handlerInfo = Svc.mime.getFromTypeAndExtension(PDF_CONTENT_TYPE, 'pdf'); + return !handlerInfo.alwaysAskBeforeHandling && + handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally; + }, + + /* + * Display a notification warning when the renderer isn't sure + * a pdf displayed correctly. + */ + _displayWarning: function (aMsg) { + let json = aMsg.json; + let browser = aMsg.target; + let cpowCallback = aMsg.objects.callback; + let tabbrowser = browser.getTabBrowser(); + let notificationBox = tabbrowser.getNotificationBox(browser); + // Flag so we don't call the response callback twice, since if the user + // clicks open with different viewer both the button callback and + // eventCallback will be called. + let responseSent = false; + let buttons = [{ + label: json.label, + accessKey: json.accessKey, + callback: function() { + responseSent = true; + cpowCallback(true); + } + }]; + notificationBox.appendNotification(json.message, 'pdfjs-fallback', null, + notificationBox.PRIORITY_INFO_LOW, + buttons, + function eventsCallback(eventType) { + // Currently there is only one event "removed" but if there are any other + // added in the future we still only care about removed at the moment. + if (eventType !== 'removed') { + return; + } + // Don't send a response again if we already responded when the button was + // clicked. + if (responseSent) { + return; + } + cpowCallback(false); + }); + } +}; + +/* + * CPOW security features require chrome objects declare exposed + * properties via __exposedProps__. We don't want to expose things + * directly on the findbar, so we wrap the findbar in a smaller + * object here that supports the features pdf.js needs. + */ +function PdfjsFindbarWrapper(aBrowser) { + let tabbrowser = aBrowser.getTabBrowser(); + let tab = tabbrowser._getTabForBrowser(aBrowser); + this._findbar = tabbrowser.getFindBar(tab); +}; + +PdfjsFindbarWrapper.prototype = { + __exposedProps__: { + addEventListener: "r", + removeEventListener: "r", + updateControlState: "r", + }, + _findbar: null, + + updateControlState: function (aResult, aFindPrevious) { + this._findbar.updateControlState(aResult, aFindPrevious); + }, + + addEventListener: function (aType, aListener, aUseCapture, aWantsUntrusted) { + this._findbar.addEventListener(aType, aListener, aUseCapture, aWantsUntrusted); + }, + + removeEventListener: function (aType, aListener, aUseCapture) { + this._findbar.removeEventListener(aType, aListener, aUseCapture); + } +}; + +function PdfjsWindowWrapper(aBrowser) { + this._window = aBrowser.ownerDocument.defaultView; +}; + +PdfjsWindowWrapper.prototype = { + __exposedProps__: { + valueOf: "r", + }, + _window: null, + + valueOf: function () { + return this._window.valueOf(); + } +}; + diff --git a/extensions/firefox/content/PdfjsContentUtils.jsm b/extensions/firefox/content/PdfjsContentUtils.jsm new file mode 100644 index 000000000..d8a5acd43 --- /dev/null +++ b/extensions/firefox/content/PdfjsContentUtils.jsm @@ -0,0 +1,197 @@ +/* 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. + */ + +'use strict'; + +var EXPORTED_SYMBOLS = ['PdfjsContentUtils']; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); + +let PdfjsContentUtils = { + _mm: null, + + /* + * Public API + */ + + get isRemote() { + return Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; + }, + + init: function () { + // child *process* mm, or when loaded into the parent for in-content + // support the psuedo child process mm 'child PPMM'. + if (!this._mm) { + this._mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender); + this._mm.addMessageListener("PDFJS:Child:refreshSettings", this); + Services.obs.addObserver(this, "quit-application", false); + } + }, + + uninit: function () { + if (this._mm) { + this._mm.removeMessageListener("PDFJS:Child:refreshSettings", this); + Services.obs.removeObserver(this, "quit-application"); + } + this._mm = null; + }, + + /* + * prefs utilities - the child does not have write access to prefs. + * note, the pref names here are cross-checked against a list of + * approved pdfjs prefs in chrome utils. + */ + + clearUserPref: function (aPrefName) { + this._mm.sendSyncMessage("PDFJS:Parent:clearUserPref", { + name: aPrefName + }); + }, + + setIntPref: function (aPrefName, aPrefValue) { + this._mm.sendSyncMessage("PDFJS:Parent:setIntPref", { + name: aPrefName, + value: aPrefValue + }); + }, + + setBoolPref: function (aPrefName, aPrefValue) { + this._mm.sendSyncMessage("PDFJS:Parent:setBoolPref", { + name: aPrefName, + value: aPrefValue + }); + }, + + setCharPref: function (aPrefName, aPrefValue) { + this._mm.sendSyncMessage("PDFJS:Parent:setCharPref", { + name: aPrefName, + value: aPrefValue + }); + }, + + setStringPref: function (aPrefName, aPrefValue) { + this._mm.sendSyncMessage("PDFJS:Parent:setStringPref", { + name: aPrefName, + value: aPrefValue + }); + }, + + /* + * Forwards default app query to the parent where we check various + * handler app settings only available in the parent process. + */ + isDefaultHandlerApp: function () { + return this._mm.sendSyncMessage("PDFJS:Parent:isDefaultHandlerApp")[0]; + }, + + /* + * Request the display of a notification warning in the associated window + * when the renderer isn't sure a pdf displayed correctly. + */ + displayWarning: function (aWindow, aMessage, aCallback, aLabel, accessKey) { + // the child's dom frame mm associated with the window. + let winmm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .sameTypeRootTreeItem + .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + winmm.sendAsyncMessage("PDFJS:Parent:displayWarning", { + message: aMessage, + label: aLabel, + accessKey: accessKey + }, { + callback: aCallback + }); + }, + + /* + * Events + */ + + observe: function(aSubject, aTopic, aData) { + if (aTopic == "quit-application") { + this.uninit(); + } + }, + + receiveMessage: function (aMsg) { + switch (aMsg.name) { + case "PDFJS:Child:refreshSettings": + // Only react to this if we are remote. + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + let jsm = "resource://pdf.js/PdfJs.jsm"; + let pdfjs = Components.utils.import(jsm, {}).PdfJs; + pdfjs.updateRegistration(); + } + break; + } + }, + + /* + * CPOWs + */ + + getChromeWindow: function (aWindow) { + let winmm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .sameTypeRootTreeItem + .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + // Sync calls don't support cpow wrapping of returned results, so we + // send over a small container for the object we want. + let suitcase = { + _window: null, + setChromeWindow: function (aObj) { this._window = aObj; } + } + if (!winmm.sendSyncMessage("PDFJS:Parent:getChromeWindow", {}, + { suitcase: suitcase })[0]) { + Cu.reportError("A request for a CPOW wrapped chrome window " + + "failed for unknown reasons."); + return null; + } + return suitcase._window; + }, + + getFindBar: function (aWindow) { + let winmm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .sameTypeRootTreeItem + .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + let suitcase = { + _findbar: null, + setFindBar: function (aObj) { this._findbar = aObj; } + } + if (!winmm.sendSyncMessage("PDFJS:Parent:getFindBar", {}, + { suitcase: suitcase })[0]) { + Cu.reportError("A request for a CPOW wrapped findbar " + + "failed for unknown reasons."); + return null; + } + return suitcase._findbar; + } +}; diff --git a/extensions/firefox/content/pdfjschildbootstrap.js b/extensions/firefox/content/pdfjschildbootstrap.js new file mode 100644 index 000000000..8e5a48aa4 --- /dev/null +++ b/extensions/firefox/content/pdfjschildbootstrap.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* jshint esnext:true */ +/* globals Components, PdfjsContentUtils, PdfJs */ + +'use strict'; + +/* + * pdfjschildbootstrap.js loads into the content process to take care of + * initializing our built-in version of pdfjs when running remote. + */ + +Components.utils.import('resource://pdf.js/PdfJs.jsm'); +Components.utils.import('resource://pdf.js/PdfjsContentUtils.jsm'); + +// init content utils shim pdfjs will use to access privileged apis. +PdfjsContentUtils.init(); + +// register various pdfjs factories that hook us into content loading. +PdfJs.updateRegistration(); diff --git a/extensions/firefox/install.rdf b/extensions/firefox/install.rdf index 8b18b783b..041e58934 100644 --- a/extensions/firefox/install.rdf +++ b/extensions/firefox/install.rdf @@ -46,6 +46,7 @@ true + true Mozilla Uses HTML5 to display PDF files directly in Firefox. https://github.com/mozilla/pdf.js/ diff --git a/make.js b/make.js index 002d4de16..7ab11f765 100644 --- a/make.js +++ b/make.js @@ -44,6 +44,7 @@ var ROOT_DIR = __dirname + '/', // absolute path to project's root B2G_BUILD_DIR = BUILD_DIR + '/b2g/', JSDOC_DIR = BUILD_DIR + 'jsdoc', EXTENSION_SRC_DIR = 'extensions/', + FIREFOX_CONTENT_DIR = EXTENSION_SRC_DIR + '/firefox/content/', LOCALE_SRC_DIR = 'l10n/', GH_PAGES_DIR = BUILD_DIR + 'gh-pages/', GENERIC_DIR = BUILD_DIR + 'generic/', @@ -91,8 +92,12 @@ var COMMON_WEB_FILES = 'web/debugger.js'], COMMON_WEB_FILES_PREPROCESS = ['web/viewer.js', - 'web/viewer.html']; - + 'web/viewer.html'], + COMMON_FIREFOX_FILES_PREPROCESS = + [FIREFOX_CONTENT_DIR + 'PdfStreamConverter.jsm', + FIREFOX_CONTENT_DIR + 'PdfjsContentUtils.jsm', + FIREFOX_CONTENT_DIR + 'PdfjsChromeUtils.jsm', + FIREFOX_CONTENT_DIR + 'PdfRedirector.jsm']; // // make generic // Builds the generic production viewer that should be compatible with most @@ -586,7 +591,6 @@ target.firefox = function() { var FIREFOX_BUILD_CONTENT_DIR = FIREFOX_BUILD_DIR + '/content/', FIREFOX_EXTENSION_DIR = 'extensions/firefox/', - FIREFOX_CONTENT_DIR = EXTENSION_SRC_DIR + '/firefox/content/', FIREFOX_EXTENSION_FILES_TO_COPY = ['*.js', '*.rdf', @@ -594,6 +598,7 @@ target.firefox = function() { '*.png', '*.manifest', 'locale', + 'chrome', '../../LICENSE'], FIREFOX_EXTENSION_FILES = ['bootstrap.js', @@ -602,6 +607,7 @@ target.firefox = function() { 'icon.png', 'icon64.png', 'content', + 'chrome', 'locale', 'LICENSE'], FIREFOX_EXTENSION_NAME = 'pdf.js.xpi', @@ -641,9 +647,7 @@ target.firefox = function() { preprocess: [ [COMMON_WEB_FILES_PREPROCESS, FIREFOX_BUILD_CONTENT_DIR + '/web'], [BUILD_TARGETS, FIREFOX_BUILD_CONTENT_DIR + BUILD_DIR], - [FIREFOX_CONTENT_DIR + 'PdfStreamConverter.jsm', - FIREFOX_BUILD_CONTENT_DIR], - [FIREFOX_CONTENT_DIR + 'PdfRedirector.jsm', FIREFOX_BUILD_CONTENT_DIR], + [COMMON_FIREFOX_FILES_PREPROCESS, FIREFOX_BUILD_CONTENT_DIR], [SRC_DIR + 'core/network.js', FIREFOX_BUILD_CONTENT_DIR], [FIREFOX_EXTENSION_DIR + 'bootstrap.js', FIREFOX_BUILD_DIR] ], @@ -761,8 +765,7 @@ target.mozcentral = function() { [COMMON_WEB_FILES_PREPROCESS, MOZCENTRAL_CONTENT_DIR + '/web'], [BUILD_TARGETS, MOZCENTRAL_CONTENT_DIR + BUILD_DIR], [SRC_DIR + 'core/network.js', MOZCENTRAL_CONTENT_DIR], - [FIREFOX_CONTENT_DIR + 'PdfStreamConverter.jsm', MOZCENTRAL_CONTENT_DIR], - [FIREFOX_CONTENT_DIR + 'PdfRedirector.jsm', MOZCENTRAL_CONTENT_DIR], + [COMMON_FIREFOX_FILES_PREPROCESS, MOZCENTRAL_CONTENT_DIR], [FIREFOX_CONTENT_DIR + 'PdfJs.jsm', MOZCENTRAL_CONTENT_DIR] ], preprocessCSS: [