1/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2/* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6/* eslint no-unused-vars: ["error", {args: "none"}] */
7
8var EXPORTED_SYMBOLS = ["PopupBlockingChild"];
9
10// The maximum number of popup information we'll send to the parent.
11const MAX_SENT_POPUPS = 15;
12
13const { XPCOMUtils } = ChromeUtils.import(
14  "resource://gre/modules/XPCOMUtils.jsm"
15);
16
17class PopupBlockingChild extends JSWindowActorChild {
18  constructor() {
19    super();
20    this.weakDocStates = new WeakMap();
21  }
22
23  actorCreated() {
24    this.contentWindow.addEventListener("pageshow", this);
25  }
26
27  willDestroy() {
28    this.contentWindow.removeEventListener("pageshow", this);
29  }
30
31  /**
32   * Returns the state for the current document referred to via
33   * this.document. If no such state exists, creates it, stores it
34   * and returns it.
35   */
36  get docState() {
37    let state = this.weakDocStates.get(this.document);
38    if (!state) {
39      state = {
40        popupData: [],
41      };
42      this.weakDocStates.set(this.document, state);
43    }
44
45    return state;
46  }
47
48  receiveMessage(msg) {
49    switch (msg.name) {
50      case "UnblockPopup": {
51        let i = msg.data.index;
52        let state = this.docState;
53        let popupData = state.popupData[i];
54        if (popupData) {
55          let dwi = popupData.requestingWindow;
56
57          // If we have a requesting window and the requesting document is
58          // still the current document, open the popup.
59          if (dwi && dwi.document == popupData.requestingDocument) {
60            dwi.open(
61              popupData.popupWindowURISpec,
62              popupData.popupWindowName,
63              popupData.popupWindowFeatures
64            );
65          }
66        }
67        break;
68      }
69
70      case "GetBlockedPopupList": {
71        let state = this.docState;
72        let length = Math.min(state.popupData.length, MAX_SENT_POPUPS);
73
74        let result = [];
75
76        for (let i = 0; i < length; ++i) {
77          let popup = state.popupData[i];
78
79          let popupWindowURISpec = popup.popupWindowURISpec;
80
81          if (this.contentWindow.location.href == popupWindowURISpec) {
82            popupWindowURISpec = "<self>";
83          } else {
84            // Limit 500 chars to be sent because the URI will be cropped
85            // by the UI anyway, and data: URIs can be significantly larger.
86            popupWindowURISpec = popupWindowURISpec.substring(0, 500);
87          }
88
89          result.push({
90            popupWindowURISpec,
91          });
92        }
93
94        return result;
95      }
96    }
97
98    return null;
99  }
100
101  handleEvent(event) {
102    switch (event.type) {
103      case "DOMPopupBlocked":
104        this.onPopupBlocked(event);
105        break;
106      case "pageshow": {
107        this.onPageShow(event);
108        break;
109      }
110    }
111  }
112
113  onPopupBlocked(event) {
114    if (event.target != this.document) {
115      return;
116    }
117
118    let state = this.docState;
119
120    // Avoid spamming the parent process with too many blocked popups.
121    if (state.popupData.length >= PopupBlockingChild.maxReportedPopups) {
122      return;
123    }
124
125    let popup = {
126      popupWindowURISpec: event.popupWindowURI
127        ? event.popupWindowURI.spec
128        : "about:blank",
129      popupWindowFeatures: event.popupWindowFeatures,
130      popupWindowName: event.popupWindowName,
131      requestingWindow: event.requestingWindow,
132      requestingDocument: event.requestingWindow.document,
133    };
134
135    state.popupData.push(popup);
136    this.updateBlockedPopups(true);
137  }
138
139  onPageShow(event) {
140    if (event.target != this.document) {
141      return;
142    }
143
144    this.updateBlockedPopups(false);
145  }
146
147  updateBlockedPopups(shouldNotify) {
148    this.sendAsyncMessage("UpdateBlockedPopups", {
149      shouldNotify,
150      count: this.docState.popupData.length,
151    });
152  }
153}
154
155XPCOMUtils.defineLazyPreferenceGetter(
156  PopupBlockingChild,
157  "maxReportedPopups",
158  "privacy.popups.maxReported"
159);
160