browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
author Michael Kaply <mozilla@kaply.com>
Thu, 02 Jan 2020 18:30:16 +0000
changeset 508628 7d0cec2c6c40d070d643112bc348e632d19d4cc8
parent 481371 feb5267686912acdc1fa83dea843c3878b80fd58
child 512758 0e6e758f47b5d2fd9ab1631570ef765ddfc6ca3d
permissions -rw-r--r--
Bug 1599935 - Make unsupported PDF features a warning. r=bdahl Differential Revision: https://phabricator.services.mozilla.com/D55527

/* 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 = ["PdfjsChromeUtils"];

const PREF_PREFIX = "pdfjs";
const PDF_CONTENT_TYPE = "application/pdf";

const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");

ChromeUtils.defineModuleGetter(
  this,
  "PdfJsDefaultPreferences",
  "resource://pdf.js/PdfJsDefaultPreferences.jsm"
);

var Svc = {};
XPCOMUtils.defineLazyServiceGetter(
  Svc,
  "mime",
  "@mozilla.org/mime;1",
  "nsIMIMEService"
);

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "matchesCountLimit",
  "accessibility.typeaheadfind.matchesCountLimit"
);

var PdfjsChromeUtils = {
  // For security purposes when running remote, we restrict preferences
  // content can access.
  _allowedPrefNames: Object.keys(PdfJsDefaultPreferences),
  _ppmm: null,
  _mmg: null,

  /*
   * Public API
   */

  init() {
    this._browsers = new WeakSet();
    if (!this._ppmm) {
      // global parent process message manager (PPMM)
      this._ppmm = Services.ppmm;
      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 = Services.mm;
      this._mmg.addMessageListener("PDFJS:Parent:displayWarning", this);

      this._mmg.addMessageListener("PDFJS:Parent:addEventListener", this);
      this._mmg.addMessageListener("PDFJS:Parent:removeEventListener", this);
      this._mmg.addMessageListener("PDFJS:Parent:updateControlState", this);
      this._mmg.addMessageListener("PDFJS:Parent:updateMatchesCount", this);

      // Observer to handle shutdown.
      Services.obs.addObserver(this, "quit-application");
    }
  },

  uninit() {
    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:displayWarning", this);

      this._mmg.removeMessageListener("PDFJS:Parent:addEventListener", this);
      this._mmg.removeMessageListener("PDFJS:Parent:removeEventListener", this);
      this._mmg.removeMessageListener("PDFJS:Parent:updateControlState", this);
      this._mmg.removeMessageListener("PDFJS:Parent:updateMatchesCount", this);

      Services.obs.removeObserver(this, "quit-application");

      this._mmg = null;
      this._ppmm = null;
    }
  },

  /*
   * Events
   */

  observe(aSubject, aTopic, aData) {
    if (aTopic === "quit-application") {
      this.uninit();
    }
  },

  receiveMessage(aMsg) {
    switch (aMsg.name) {
      case "PDFJS:Parent:clearUserPref":
        this._clearUserPref(aMsg.data.name);
        break;
      case "PDFJS:Parent:setIntPref":
        this._setIntPref(aMsg.data.name, aMsg.data.value);
        break;
      case "PDFJS:Parent:setBoolPref":
        this._setBoolPref(aMsg.data.name, aMsg.data.value);
        break;
      case "PDFJS:Parent:setCharPref":
        this._setCharPref(aMsg.data.name, aMsg.data.value);
        break;
      case "PDFJS:Parent:setStringPref":
        this._setStringPref(aMsg.data.name, aMsg.data.value);
        break;
      case "PDFJS:Parent:isDefaultHandlerApp":
        return this.isDefaultHandlerApp();
      case "PDFJS:Parent:displayWarning":
        this._displayWarning(aMsg);
        break;

      case "PDFJS:Parent:updateControlState":
        return this._updateControlState(aMsg);
      case "PDFJS:Parent:updateMatchesCount":
        return this._updateMatchesCount(aMsg);
      case "PDFJS:Parent:addEventListener":
        return this._addEventListener(aMsg);
      case "PDFJS:Parent:removeEventListener":
        return this._removeEventListener(aMsg);
    }
    return undefined;
  },

  /*
   * Internal
   */

  _updateControlState(aMsg) {
    let data = aMsg.data;
    let browser = aMsg.target;
    let tabbrowser = browser.getTabBrowser();
    let tab = tabbrowser.getTabForBrowser(browser);
    tabbrowser.getFindBar(tab).then(fb => {
      if (!fb) {
        // The tab or window closed.
        return;
      }
      fb.updateControlState(data.result, data.findPrevious);

      const matchesCount = this._requestMatchesCount(data.matchesCount);
      fb.onMatchesCountResult(matchesCount);
    });
  },

  _updateMatchesCount(aMsg) {
    let data = aMsg.data;
    let browser = aMsg.target;
    let tabbrowser = browser.getTabBrowser();
    let tab = tabbrowser.getTabForBrowser(browser);
    tabbrowser.getFindBar(tab).then(fb => {
      if (!fb) {
        // The tab or window closed.
        return;
      }
      const matchesCount = this._requestMatchesCount(data);
      fb.onMatchesCountResult(matchesCount);
    });
  },

  _requestMatchesCount(data) {
    if (!data) {
      return { current: 0, total: 0 };
    }
    let result = {
      current: data.current,
      total: data.total,
      limit: typeof matchesCountLimit === "number" ? matchesCountLimit : 0,
    };
    if (result.total > result.limit) {
      result.total = -1;
    }
    return result;
  },

  handleEvent(aEvent) {
    const type = aEvent.type;
    // Handle the tab find initialized event specially:
    if (type == "TabFindInitialized") {
      let browser = aEvent.target.linkedBrowser;
      this._hookupEventListeners(browser);
      aEvent.target.removeEventListener(type, this);
      return;
    }

    // To avoid forwarding the message as a CPOW, create a structured cloneable
    // version of the event for both performance, and ease of usage, reasons.
    let detail = null;
    if (type !== "findbarclose") {
      detail = {
        query: aEvent.detail.query,
        caseSensitive: aEvent.detail.caseSensitive,
        entireWord: aEvent.detail.entireWord,
        highlightAll: aEvent.detail.highlightAll,
        findPrevious: aEvent.detail.findPrevious,
      };
    }

    let browser = aEvent.currentTarget.browser;
    if (!this._browsers.has(browser)) {
      throw new Error(
        "FindEventManager was not bound for the current browser."
      );
    }
    // Only forward the events if the current browser is a registered browser.
    let mm = browser.messageManager;
    mm.sendAsyncMessage("PDFJS:Child:handleEvent", { type, detail });
    aEvent.preventDefault();
  },

  _types: [
    "find",
    "findagain",
    "findhighlightallchange",
    "findcasesensitivitychange",
    "findbarclose",
  ],

  _addEventListener(aMsg) {
    let browser = aMsg.target;
    if (this._browsers.has(browser)) {
      throw new Error(
        "FindEventManager was bound 2nd time without unbinding it first."
      );
    }

    // Since this jsm is global, we need to store all the browsers
    // we have to forward the messages for.
    this._browsers.add(browser);

    this._hookupEventListeners(browser);
  },

  /**
   * Either hook up all the find event listeners if a findbar exists,
   * or listen for a find bar being created and hook up event listeners
   * when it does get created.
   */
  _hookupEventListeners(aBrowser) {
    let tabbrowser = aBrowser.getTabBrowser();
    let tab = tabbrowser.getTabForBrowser(aBrowser);
    let findbar = tabbrowser.getCachedFindBar(tab);
    if (findbar) {
      // And we need to start listening to find events.
      for (var i = 0; i < this._types.length; i++) {
        var type = this._types[i];
        findbar.addEventListener(type, this, true);
      }
    } else {
      tab.addEventListener("TabFindInitialized", this);
    }
    return !!findbar;
  },

  _removeEventListener(aMsg) {
    let browser = aMsg.target;
    if (!this._browsers.has(browser)) {
      throw new Error("FindEventManager was unbound without binding it first.");
    }

    this._browsers.delete(browser);

    let tabbrowser = browser.getTabBrowser();
    let tab = tabbrowser.getTabForBrowser(browser);
    tab.removeEventListener("TabFindInitialized", this);
    let findbar = tabbrowser.getCachedFindBar(tab);
    if (findbar) {
      // No reason to listen to find events any longer.
      for (var i = 0; i < this._types.length; i++) {
        var type = this._types[i];
        findbar.removeEventListener(type, this, true);
      }
    }
  },

  _ensurePreferenceAllowed(aPrefName) {
    let unPrefixedName = aPrefName.split(PREF_PREFIX + ".");
    if (
      unPrefixedName[0] !== "" ||
      !this._allowedPrefNames.includes(unPrefixedName[1])
    ) {
      let msg =
        '"' +
        aPrefName +
        '" ' +
        "can't be accessed from content. See PdfjsChromeUtils.";
      throw new Error(msg);
    }
  },

  _clearUserPref(aPrefName) {
    this._ensurePreferenceAllowed(aPrefName);
    Services.prefs.clearUserPref(aPrefName);
  },

  _setIntPref(aPrefName, aPrefValue) {
    this._ensurePreferenceAllowed(aPrefName);
    Services.prefs.setIntPref(aPrefName, aPrefValue);
  },

  _setBoolPref(aPrefName, aPrefValue) {
    this._ensurePreferenceAllowed(aPrefName);
    Services.prefs.setBoolPref(aPrefName, aPrefValue);
  },

  _setCharPref(aPrefName, aPrefValue) {
    this._ensurePreferenceAllowed(aPrefName);
    Services.prefs.setCharPref(aPrefName, aPrefValue);
  },

  _setStringPref(aPrefName, aPrefValue) {
    this._ensurePreferenceAllowed(aPrefName);
    Services.prefs.setStringPref(aPrefName, aPrefValue);
  },

  /*
   * Svc.mime doesn't have profile information in the child, so
   * we bounce this pdfjs enabled configuration check over to the
   * parent.
   */
  isDefaultHandlerApp() {
    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(aMsg) {
    let data = aMsg.data;
    let browser = aMsg.target;

    let tabbrowser = browser.getTabBrowser();
    let notificationBox = tabbrowser.getNotificationBox(browser);

    // Flag so we don't send the message twice, since if the user clicks
    // "open with different viewer" both the button callback and
    // eventCallback will be called.
    let messageSent = false;
    function sendMessage(download) {
      let mm = browser.messageManager;
      mm.sendAsyncMessage("PDFJS:Child:fallbackDownload", { download });
    }
    let buttons = [
      {
        label: data.label,
        accessKey: data.accessKey,
        callback() {
          messageSent = true;
          sendMessage(true);
        },
      },
    ];
    notificationBox.appendNotification(
      data.message,
      "pdfjs-fallback",
      null,
      notificationBox.PRIORITY_WARNING_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 (messageSent) {
          return;
        }
        sendMessage(false);
      }
    );
  },
};