tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

RefreshBlockerChild.sys.mjs (7726B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * This file has two actors, RefreshBlockerChild js a window actor which
      7 * handles the refresh notifications. RefreshBlockerObserverChild is a process
      8 * actor that enables refresh blocking on each docshell that is created.
      9 */
     10 
     11 import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
     12 
     13 const REFRESHBLOCKING_PREF = "accessibility.blockautorefresh";
     14 
     15 var progressListener = {
     16  // Bug 1247100 - When a refresh is caused by an HTTP header,
     17  // onRefreshAttempted will be fired before onLocationChange.
     18  // When a refresh is caused by a <meta> tag in the document,
     19  // onRefreshAttempted will be fired after onLocationChange.
     20  //
     21  // We only ever want to send a message to the parent after
     22  // onLocationChange has fired, since the parent uses the
     23  // onLocationChange update to clear transient notifications.
     24  // Sending the message before onLocationChange will result in
     25  // us creating the notification, and then clearing it very
     26  // soon after.
     27  //
     28  // To account for both cases (onRefreshAttempted before
     29  // onLocationChange, and onRefreshAttempted after onLocationChange),
     30  // we'll hold a mapping of DOM Windows that we see get
     31  // sent through both onLocationChange and onRefreshAttempted.
     32  // When either run, they'll check the WeakMap for the existence
     33  // of the DOM Window. If it doesn't exist, it'll add it. If
     34  // it finds it, it'll know that it's safe to send the message
     35  // to the parent, since we know that both have fired.
     36  //
     37  // The DOM Window is removed from blockedWindows when we notice
     38  // the nsIWebProgress change state to STATE_STOP for the
     39  // STATE_IS_WINDOW case.
     40  //
     41  // DOM Windows are mapped to a JS object that contains the data
     42  // to be sent to the parent to show the notification. Since that
     43  // data is only known when onRefreshAttempted is fired, it's only
     44  // ever stashed in the map if onRefreshAttempted fires first -
     45  // otherwise, null is set as the value of the mapping.
     46  blockedWindows: new WeakMap(),
     47 
     48  /**
     49   * Notices when the nsIWebProgress transitions to STATE_STOP for
     50   * the STATE_IS_WINDOW case, which will clear any mappings from
     51   * blockedWindows.
     52   */
     53  onStateChange(aWebProgress, aRequest, aStateFlags) {
     54    if (
     55      aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
     56      aStateFlags & Ci.nsIWebProgressListener.STATE_STOP
     57    ) {
     58      this.blockedWindows.delete(aWebProgress.DOMWindow);
     59    }
     60  },
     61 
     62  /**
     63   * Notices when the location has changed. If, when running,
     64   * onRefreshAttempted has already fired for this DOM Window, will
     65   * send the appropriate refresh blocked data to the parent.
     66   */
     67  onLocationChange(aWebProgress) {
     68    let win = aWebProgress.DOMWindow;
     69    if (this.blockedWindows.has(win)) {
     70      let data = this.blockedWindows.get(win);
     71      if (data) {
     72        // We saw onRefreshAttempted before onLocationChange, so
     73        // send the message to the parent to show the notification.
     74        this.send(win, data);
     75      }
     76    } else {
     77      this.blockedWindows.set(win, null);
     78    }
     79  },
     80 
     81  /**
     82   * Notices when a refresh / reload was attempted. If, when running,
     83   * onLocationChange has not yet run, will stash the appropriate data
     84   * into the blockedWindows map to be sent when onLocationChange fires.
     85   */
     86  onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
     87    let win = aWebProgress.DOMWindow;
     88 
     89    let data = {
     90      browsingContext: win.browsingContext,
     91      URI: aURI.spec,
     92      delay: aDelay,
     93      sameURI: aSameURI,
     94    };
     95 
     96    if (this.blockedWindows.has(win)) {
     97      // onLocationChange must have fired before, so we can tell the
     98      // parent to show the notification.
     99      this.send(win, data);
    100    } else {
    101      // onLocationChange hasn't fired yet, so stash the data in the
    102      // map so that onLocationChange can send it when it fires.
    103      this.blockedWindows.set(win, data);
    104    }
    105 
    106    return false;
    107  },
    108 
    109  send(win, data) {
    110    // Due to the |nsDocLoader| calling its |nsIWebProgressListener|s in
    111    // reverse order, this will occur *before* the |BrowserChild| can send its
    112    // |OnLocationChange| event to the parent, but we need this message to
    113    // arrive after to ensure that the refresh blocker notification is not
    114    // immediately cleared by the |OnLocationChange| from |BrowserChild|.
    115    setTimeout(() => {
    116      // An exception can occur if refresh blocking was turned off
    117      // during a pageload.
    118      try {
    119        let actor = win.windowGlobalChild.getActor("RefreshBlocker");
    120        if (actor) {
    121          actor.sendAsyncMessage("RefreshBlocker:Blocked", data);
    122        }
    123      } catch (ex) {}
    124    }, 0);
    125  },
    126 
    127  QueryInterface: ChromeUtils.generateQI([
    128    "nsIWebProgressListener2",
    129    "nsIWebProgressListener",
    130    "nsISupportsWeakReference",
    131  ]),
    132 };
    133 
    134 export class RefreshBlockerChild extends JSWindowActorChild {
    135  didDestroy() {
    136    // If the refresh blocking preference is turned off, all of the
    137    // RefreshBlockerChild actors will get destroyed, so disable
    138    // refresh blocking only in this case.
    139    if (!Services.prefs.getBoolPref(REFRESHBLOCKING_PREF)) {
    140      this.disable(this.docShell);
    141    }
    142  }
    143 
    144  enable() {
    145    ChromeUtils.domProcessChild
    146      .getActor("RefreshBlockerObserver")
    147      .enable(this.docShell);
    148  }
    149 
    150  disable() {
    151    ChromeUtils.domProcessChild
    152      .getActor("RefreshBlockerObserver")
    153      .disable(this.docShell);
    154  }
    155 
    156  receiveMessage(message) {
    157    let data = message.data;
    158 
    159    switch (message.name) {
    160      case "RefreshBlocker:Refresh": {
    161        let docShell = data.browsingContext.docShell;
    162        let refreshURI = docShell.QueryInterface(Ci.nsIRefreshURI);
    163        let URI = Services.io.newURI(data.URI);
    164        refreshURI.forceRefreshURI(URI, null, data.delay);
    165        break;
    166      }
    167 
    168      case "PreferenceChanged":
    169        if (data.isEnabled) {
    170          this.enable(this.docShell);
    171        } else {
    172          this.disable(this.docShell);
    173        }
    174    }
    175  }
    176 }
    177 
    178 export class RefreshBlockerObserverChild extends JSProcessActorChild {
    179  constructor() {
    180    super();
    181    this.filtersMap = new Map();
    182  }
    183 
    184  observe(subject, topic) {
    185    switch (topic) {
    186      case "webnavigation-create":
    187      case "chrome-webnavigation-create":
    188        if (Services.prefs.getBoolPref(REFRESHBLOCKING_PREF)) {
    189          this.enable(subject.QueryInterface(Ci.nsIDocShell));
    190        }
    191        break;
    192 
    193      case "webnavigation-destroy":
    194      case "chrome-webnavigation-destroy":
    195        if (Services.prefs.getBoolPref(REFRESHBLOCKING_PREF)) {
    196          this.disable(subject.QueryInterface(Ci.nsIDocShell));
    197        }
    198        break;
    199    }
    200  }
    201 
    202  enable(docShell) {
    203    if (this.filtersMap.has(docShell)) {
    204      return;
    205    }
    206 
    207    let filter = Cc[
    208      "@mozilla.org/appshell/component/browser-status-filter;1"
    209    ].createInstance(Ci.nsIWebProgress);
    210 
    211    filter.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL);
    212 
    213    this.filtersMap.set(docShell, filter);
    214 
    215    let webProgress = docShell
    216      .QueryInterface(Ci.nsIInterfaceRequestor)
    217      .getInterface(Ci.nsIWebProgress);
    218    webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
    219  }
    220 
    221  disable(docShell) {
    222    let filter = this.filtersMap.get(docShell);
    223    if (!filter) {
    224      return;
    225    }
    226 
    227    let webProgress = docShell
    228      .QueryInterface(Ci.nsIInterfaceRequestor)
    229      .getInterface(Ci.nsIWebProgress);
    230    webProgress.removeProgressListener(filter);
    231 
    232    filter.removeProgressListener(progressListener);
    233    this.filtersMap.delete(docShell);
    234  }
    235 }