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