1/* Copyright 2012 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/* jshint esnext:true */ 16/* globals Components, Services, XPCOMUtils */ 17 18'use strict'; 19 20var EXPORTED_SYMBOLS = ['PdfjsChromeUtils']; 21 22const Cc = Components.classes; 23const Ci = Components.interfaces; 24const Cr = Components.results; 25const Cu = Components.utils; 26 27const PREF_PREFIX = 'pdfjs'; 28const PDF_CONTENT_TYPE = 'application/pdf'; 29 30Cu.import('resource://gre/modules/XPCOMUtils.jsm'); 31Cu.import('resource://gre/modules/Services.jsm'); 32 33var Svc = {}; 34XPCOMUtils.defineLazyServiceGetter(Svc, 'mime', 35 '@mozilla.org/mime;1', 36 'nsIMIMEService'); 37 38var DEFAULT_PREFERENCES = 39{ 40 "showPreviousViewOnLoad": true, 41 "defaultZoomValue": "", 42 "sidebarViewOnLoad": 0, 43 "enableHandToolOnLoad": false, 44 "enableWebGL": false, 45 "pdfBugEnabled": false, 46 "disableRange": false, 47 "disableStream": false, 48 "disableAutoFetch": false, 49 "disableFontFace": false, 50 "disableTextLayer": false, 51 "useOnlyCssZoom": false, 52 "externalLinkTarget": 0, 53 "enhanceTextSelection": false, 54 "renderInteractiveForms": false, 55 "disablePageLabels": false 56} 57 58 59var PdfjsChromeUtils = { 60 // For security purposes when running remote, we restrict preferences 61 // content can access. 62 _allowedPrefNames: Object.keys(DEFAULT_PREFERENCES), 63 _ppmm: null, 64 _mmg: null, 65 66 /* 67 * Public API 68 */ 69 70 init: function () { 71 this._browsers = new WeakSet(); 72 if (!this._ppmm) { 73 // global parent process message manager (PPMM) 74 this._ppmm = Cc['@mozilla.org/parentprocessmessagemanager;1']. 75 getService(Ci.nsIMessageBroadcaster); 76 this._ppmm.addMessageListener('PDFJS:Parent:clearUserPref', this); 77 this._ppmm.addMessageListener('PDFJS:Parent:setIntPref', this); 78 this._ppmm.addMessageListener('PDFJS:Parent:setBoolPref', this); 79 this._ppmm.addMessageListener('PDFJS:Parent:setCharPref', this); 80 this._ppmm.addMessageListener('PDFJS:Parent:setStringPref', this); 81 this._ppmm.addMessageListener('PDFJS:Parent:isDefaultHandlerApp', this); 82 83 // global dom message manager (MMg) 84 this._mmg = Cc['@mozilla.org/globalmessagemanager;1']. 85 getService(Ci.nsIMessageListenerManager); 86 this._mmg.addMessageListener('PDFJS:Parent:displayWarning', this); 87 88 this._mmg.addMessageListener('PDFJS:Parent:addEventListener', this); 89 this._mmg.addMessageListener('PDFJS:Parent:removeEventListener', this); 90 this._mmg.addMessageListener('PDFJS:Parent:updateControlState', this); 91 92 // observer to handle shutdown 93 Services.obs.addObserver(this, 'quit-application', false); 94 } 95 }, 96 97 uninit: function () { 98 if (this._ppmm) { 99 this._ppmm.removeMessageListener('PDFJS:Parent:clearUserPref', this); 100 this._ppmm.removeMessageListener('PDFJS:Parent:setIntPref', this); 101 this._ppmm.removeMessageListener('PDFJS:Parent:setBoolPref', this); 102 this._ppmm.removeMessageListener('PDFJS:Parent:setCharPref', this); 103 this._ppmm.removeMessageListener('PDFJS:Parent:setStringPref', this); 104 this._ppmm.removeMessageListener('PDFJS:Parent:isDefaultHandlerApp', 105 this); 106 107 this._mmg.removeMessageListener('PDFJS:Parent:displayWarning', this); 108 109 this._mmg.removeMessageListener('PDFJS:Parent:addEventListener', this); 110 this._mmg.removeMessageListener('PDFJS:Parent:removeEventListener', this); 111 this._mmg.removeMessageListener('PDFJS:Parent:updateControlState', this); 112 113 Services.obs.removeObserver(this, 'quit-application', false); 114 115 this._mmg = null; 116 this._ppmm = null; 117 } 118 }, 119 120 /* 121 * Called by the main module when preference changes are picked up 122 * in the parent process. Observers don't propagate so we need to 123 * instruct the child to refresh its configuration and (possibly) 124 * the module's registration. 125 */ 126 notifyChildOfSettingsChange: function () { 127 if (Services.appinfo.processType === 128 Services.appinfo.PROCESS_TYPE_DEFAULT && this._ppmm) { 129 // XXX kinda bad, we want to get the parent process mm associated 130 // with the content process. _ppmm is currently the global process 131 // manager, which means this is going to fire to every child process 132 // we have open. Unfortunately I can't find a way to get at that 133 // process specific mm from js. 134 this._ppmm.broadcastAsyncMessage('PDFJS:Child:refreshSettings', {}); 135 } 136 }, 137 138 /* 139 * Events 140 */ 141 142 observe: function(aSubject, aTopic, aData) { 143 if (aTopic === 'quit-application') { 144 this.uninit(); 145 } 146 }, 147 148 receiveMessage: function (aMsg) { 149 switch (aMsg.name) { 150 case 'PDFJS:Parent:clearUserPref': 151 this._clearUserPref(aMsg.data.name); 152 break; 153 case 'PDFJS:Parent:setIntPref': 154 this._setIntPref(aMsg.data.name, aMsg.data.value); 155 break; 156 case 'PDFJS:Parent:setBoolPref': 157 this._setBoolPref(aMsg.data.name, aMsg.data.value); 158 break; 159 case 'PDFJS:Parent:setCharPref': 160 this._setCharPref(aMsg.data.name, aMsg.data.value); 161 break; 162 case 'PDFJS:Parent:setStringPref': 163 this._setStringPref(aMsg.data.name, aMsg.data.value); 164 break; 165 case 'PDFJS:Parent:isDefaultHandlerApp': 166 return this.isDefaultHandlerApp(); 167 case 'PDFJS:Parent:displayWarning': 168 this._displayWarning(aMsg); 169 break; 170 171 172 case 'PDFJS:Parent:updateControlState': 173 return this._updateControlState(aMsg); 174 case 'PDFJS:Parent:addEventListener': 175 return this._addEventListener(aMsg); 176 case 'PDFJS:Parent:removeEventListener': 177 return this._removeEventListener(aMsg); 178 } 179 }, 180 181 /* 182 * Internal 183 */ 184 185 _findbarFromMessage: function(aMsg) { 186 let browser = aMsg.target; 187 let tabbrowser = browser.getTabBrowser(); 188 let tab = tabbrowser.getTabForBrowser(browser); 189 return tabbrowser.getFindBar(tab); 190 }, 191 192 _updateControlState: function (aMsg) { 193 let data = aMsg.data; 194 this._findbarFromMessage(aMsg) 195 .updateControlState(data.result, data.findPrevious); 196 }, 197 198 handleEvent: function(aEvent) { 199 // To avoid forwarding the message as a CPOW, create a structured cloneable 200 // version of the event for both performance, and ease of usage, reasons. 201 let type = aEvent.type; 202 let detail = { 203 query: aEvent.detail.query, 204 caseSensitive: aEvent.detail.caseSensitive, 205 highlightAll: aEvent.detail.highlightAll, 206 findPrevious: aEvent.detail.findPrevious 207 }; 208 209 let browser = aEvent.currentTarget.browser; 210 if (!this._browsers.has(browser)) { 211 throw new Error('FindEventManager was not bound ' + 212 'for the current browser.'); 213 } 214 // Only forward the events if the current browser is a registered browser. 215 let mm = browser.messageManager; 216 mm.sendAsyncMessage('PDFJS:Child:handleEvent', 217 { type: type, detail: detail }); 218 aEvent.preventDefault(); 219 }, 220 221 _types: ['find', 222 'findagain', 223 'findhighlightallchange', 224 'findcasesensitivitychange'], 225 226 _addEventListener: function (aMsg) { 227 let browser = aMsg.target; 228 if (this._browsers.has(browser)) { 229 throw new Error('FindEventManager was bound 2nd time ' + 230 'without unbinding it first.'); 231 } 232 233 // Since this jsm is global, we need to store all the browsers 234 // we have to forward the messages for. 235 this._browsers.add(browser); 236 237 // And we need to start listening to find events. 238 for (var i = 0; i < this._types.length; i++) { 239 var type = this._types[i]; 240 this._findbarFromMessage(aMsg) 241 .addEventListener(type, this, true); 242 } 243 }, 244 245 _removeEventListener: function (aMsg) { 246 let browser = aMsg.target; 247 if (!this._browsers.has(browser)) { 248 throw new Error('FindEventManager was unbound without binding it first.'); 249 } 250 251 this._browsers.delete(browser); 252 253 // No reason to listen to find events any longer. 254 for (var i = 0; i < this._types.length; i++) { 255 var type = this._types[i]; 256 this._findbarFromMessage(aMsg) 257 .removeEventListener(type, this, true); 258 } 259 }, 260 261 _ensurePreferenceAllowed: function (aPrefName) { 262 let unPrefixedName = aPrefName.split(PREF_PREFIX + '.'); 263 if (unPrefixedName[0] !== '' || 264 this._allowedPrefNames.indexOf(unPrefixedName[1]) === -1) { 265 let msg = '"' + aPrefName + '" ' + 266 'can\'t be accessed from content. See PdfjsChromeUtils.'; 267 throw new Error(msg); 268 } 269 }, 270 271 _clearUserPref: function (aPrefName) { 272 this._ensurePreferenceAllowed(aPrefName); 273 Services.prefs.clearUserPref(aPrefName); 274 }, 275 276 _setIntPref: function (aPrefName, aPrefValue) { 277 this._ensurePreferenceAllowed(aPrefName); 278 Services.prefs.setIntPref(aPrefName, aPrefValue); 279 }, 280 281 _setBoolPref: function (aPrefName, aPrefValue) { 282 this._ensurePreferenceAllowed(aPrefName); 283 Services.prefs.setBoolPref(aPrefName, aPrefValue); 284 }, 285 286 _setCharPref: function (aPrefName, aPrefValue) { 287 this._ensurePreferenceAllowed(aPrefName); 288 Services.prefs.setCharPref(aPrefName, aPrefValue); 289 }, 290 291 _setStringPref: function (aPrefName, aPrefValue) { 292 this._ensurePreferenceAllowed(aPrefName); 293 let str = Cc['@mozilla.org/supports-string;1'] 294 .createInstance(Ci.nsISupportsString); 295 str.data = aPrefValue; 296 Services.prefs.setComplexValue(aPrefName, Ci.nsISupportsString, str); 297 }, 298 299 /* 300 * Svc.mime doesn't have profile information in the child, so 301 * we bounce this pdfjs enabled configuration check over to the 302 * parent. 303 */ 304 isDefaultHandlerApp: function () { 305 var handlerInfo = Svc.mime.getFromTypeAndExtension(PDF_CONTENT_TYPE, 'pdf'); 306 return (!handlerInfo.alwaysAskBeforeHandling && 307 handlerInfo.preferredAction === Ci.nsIHandlerInfo.handleInternally); 308 }, 309 310 /* 311 * Display a notification warning when the renderer isn't sure 312 * a pdf displayed correctly. 313 */ 314 _displayWarning: function (aMsg) { 315 let data = aMsg.data; 316 let browser = aMsg.target; 317 318 let tabbrowser = browser.getTabBrowser(); 319 let notificationBox = tabbrowser.getNotificationBox(browser); 320 321 // Flag so we don't send the message twice, since if the user clicks 322 // "open with different viewer" both the button callback and 323 // eventCallback will be called. 324 let messageSent = false; 325 function sendMessage(download) { 326 let mm = browser.messageManager; 327 mm.sendAsyncMessage('PDFJS:Child:fallbackDownload', 328 { download: download }); 329 } 330 let buttons = [{ 331 label: data.label, 332 accessKey: data.accessKey, 333 callback: function() { 334 messageSent = true; 335 sendMessage(true); 336 } 337 }]; 338 notificationBox.appendNotification(data.message, 'pdfjs-fallback', null, 339 notificationBox.PRIORITY_INFO_LOW, 340 buttons, 341 function eventsCallback(eventType) { 342 // Currently there is only one event "removed" but if there are any other 343 // added in the future we still only care about removed at the moment. 344 if (eventType !== 'removed') { 345 return; 346 } 347 // Don't send a response again if we already responded when the button was 348 // clicked. 349 if (messageSent) { 350 return; 351 } 352 sendMessage(false); 353 }); 354 } 355}; 356 357 358