summaryrefslogtreecommitdiffstats
path: root/dom/base/ContentAreaDropListener.sys.mjs
blob: 9f7bc500a1eadc9062abb91db73788af091d51bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// This component is used for handling dragover and drop of urls.
//
// It checks to see whether a drop of a url is allowed. For instance, a url
// cannot be dropped if it is not a valid uri or the source of the drag cannot
// access the uri. This prevents, for example, a source document from tricking
// the user into dragging a chrome url.

export function ContentAreaDropListener() {}

ContentAreaDropListener.prototype = {
  classID: Components.ID("{1f34bc80-1bc7-11d6-a384-d705dd0746fc}"),
  QueryInterface: ChromeUtils.generateQI(["nsIDroppedLinkHandler"]),

  _addLink(links, url, name, type) {
    links.push({ url, name, type });
  },

  _addLinksFromItem(links, dt, i) {
    let types = dt.mozTypesAt(i);
    let type, data;

    type = "text/uri-list";
    if (types.contains(type)) {
      data = dt.mozGetDataAt(type, i);
      if (data) {
        let urls = data.split("\n");
        for (let url of urls) {
          // lines beginning with # are comments
          if (url.startsWith("#")) {
            continue;
          }
          url = url.replace(/^\s+|\s+$/g, "");
          this._addLink(links, url, url, type);
        }
        return;
      }
    }

    type = "text/x-moz-url";
    if (types.contains(type)) {
      data = dt.mozGetDataAt(type, i);
      if (data) {
        let lines = data.split("\n");
        for (let i = 0, length = lines.length; i < length; i += 2) {
          this._addLink(links, lines[i], lines[i + 1], type);
        }
        return;
      }
    }

    for (let type of ["text/plain", "text/x-moz-text-internal"]) {
      if (types.contains(type)) {
        data = dt.mozGetDataAt(type, i);
        if (data) {
          let lines = data.replace(/^\s+|\s+$/gm, "").split("\n");
          if (!lines.length) {
            return;
          }

          // For plain text, there are 2 cases:
          //   * if there is at least one URI:
          //       Add all URIs, ignoring non-URI lines, so that all URIs
          //       are opened in tabs.
          //   * if there's no URI:
          //       Add the entire text as a single entry, so that the entire
          //       text is searched.
          let hasURI = false;
          // We don't care whether we are in a private context, because we are
          // only using fixedURI and thus there's no risk to use the wrong
          // search engine.
          let flags =
            Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
            Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
          for (let line of lines) {
            let info = Services.uriFixup.getFixupURIInfo(line, flags);
            if (info.fixedURI) {
              // Use the original line here, and let the caller decide
              // whether to perform fixup or not.
              hasURI = true;
              this._addLink(links, line, line, type);
            }
          }

          if (!hasURI) {
            this._addLink(links, data, data, type);
          }
          return;
        }
      }
    }

    // For shortcuts, we want to check for the file type last, so that the
    // url pointed to in one of the url types is found first before the file
    // type, which points to the actual file.
    let files = dt.files;
    if (files && i < files.length) {
      this._addLink(
        links,
        PathUtils.toFileURI(files[i].mozFullPath),
        files[i].name,
        "application/x-moz-file"
      );
    }
  },

  _getDropLinks(dt) {
    let links = [];
    for (let i = 0; i < dt.mozItemCount; i++) {
      this._addLinksFromItem(links, dt, i);
    }
    return links;
  },

  _validateURI(dataTransfer, uriString, disallowInherit, triggeringPrincipal) {
    if (!uriString) {
      return "";
    }

    // Strip leading and trailing whitespace, then try to create a
    // URI from the dropped string. If that succeeds, we're
    // dropping a URI and we need to do a security check to make
    // sure the source document can load the dropped URI.
    uriString = uriString.replace(/^\s*|\s*$/g, "");

    // Apply URI fixup so that this validation prevents bad URIs even if the
    // similar fixup is applied later, especialy fixing typos up will convert
    // non-URI to URI.
    // We don't know if the uri comes from a private context, but luckily we
    // are only using fixedURI, so there's no risk to use the wrong search
    // engine.
    let fixupFlags =
      Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS |
      Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
    let info = Services.uriFixup.getFixupURIInfo(uriString, fixupFlags);
    if (!info.fixedURI || info.keywordProviderName) {
      // Loading a keyword search should always be fine for all cases.
      return uriString;
    }
    let uri = info.fixedURI;

    let secMan = Services.scriptSecurityManager;
    let flags = secMan.STANDARD;
    if (disallowInherit) {
      flags |= secMan.DISALLOW_INHERIT_PRINCIPAL;
    }

    secMan.checkLoadURIWithPrincipal(triggeringPrincipal, uri, flags);

    // Once we validated, return the URI after fixup, instead of the original
    // uriString.
    return uri.spec;
  },

  _getTriggeringPrincipalFromDataTransfer(
    aDataTransfer,
    fallbackToSystemPrincipal
  ) {
    let sourceNode = aDataTransfer.mozSourceNode;
    if (
      sourceNode &&
      (sourceNode.localName !== "browser" ||
        sourceNode.namespaceURI !==
          "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul")
    ) {
      // Use sourceNode's principal only if the sourceNode is not browser.
      //
      // If sourceNode is browser, the actual triggering principal may be
      // differ than sourceNode's principal, since sourceNode's principal is
      // top level document's one and the drag may be triggered from a frame
      // with different principal.
      if (sourceNode.nodePrincipal) {
        return sourceNode.nodePrincipal;
      }
    }

    // First, fallback to mozTriggeringPrincipalURISpec that is set when the
    // drop comes from another content process.
    let principalURISpec = aDataTransfer.mozTriggeringPrincipalURISpec;
    if (!principalURISpec) {
      // Fallback to either system principal or file principal, supposing
      // the drop comes from outside of the browser, so that drops of file
      // URIs are always allowed.
      //
      // TODO: Investigate and describe the difference between them,
      //       or use only one principal. (Bug 1367038)
      if (fallbackToSystemPrincipal) {
        return Services.scriptSecurityManager.getSystemPrincipal();
      }

      principalURISpec = "file:///";
    }
    return Services.scriptSecurityManager.createContentPrincipal(
      Services.io.newURI(principalURISpec),
      {}
    );
  },

  getTriggeringPrincipal(aEvent) {
    let dataTransfer = aEvent.dataTransfer;
    return this._getTriggeringPrincipalFromDataTransfer(dataTransfer, true);
  },

  getCsp(aEvent) {
    let sourceNode = aEvent.dataTransfer.mozSourceNode;
    if (aEvent.dataTransfer.mozCSP !== null) {
      return aEvent.dataTransfer.mozCSP;
    }

    if (
      sourceNode &&
      (sourceNode.localName !== "browser" ||
        sourceNode.namespaceURI !==
          "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul")
    ) {
      // Use sourceNode's csp only if the sourceNode is not browser.
      //
      // If sourceNode is browser, the actual triggering csp may be differ than sourceNode's csp,
      // since sourceNode's csp is top level document's one and the drag may be triggered from a
      // frame with different csp.
      return sourceNode.csp;
    }
    return null;
  },

  canDropLink(aEvent, aAllowSameDocument) {
    if (this._eventTargetIsDisabled(aEvent)) {
      return false;
    }

    let dataTransfer = aEvent.dataTransfer;
    let types = dataTransfer.types;
    if (
      !types.includes("application/x-moz-file") &&
      !types.includes("text/x-moz-url") &&
      !types.includes("text/uri-list") &&
      !types.includes("text/x-moz-text-internal") &&
      !types.includes("text/plain")
    ) {
      return false;
    }

    if (aAllowSameDocument) {
      return true;
    }

    // If this is an external drag, allow drop.
    let sourceTopWC = dataTransfer.sourceTopWindowContext;
    if (!sourceTopWC) {
      return true;
    }

    // If drag source and drop target are in the same top window, don't allow.
    let eventWC =
      aEvent.originalTarget.ownerGlobal.browsingContext.currentWindowContext;
    if (eventWC && sourceTopWC == eventWC.topWindowContext) {
      return false;
    }

    return true;
  },

  dropLinks(aEvent, aDisallowInherit) {
    if (aEvent && this._eventTargetIsDisabled(aEvent)) {
      return [];
    }

    let dataTransfer = aEvent.dataTransfer;
    let links = this._getDropLinks(dataTransfer);
    let triggeringPrincipal = this._getTriggeringPrincipalFromDataTransfer(
      dataTransfer,
      false
    );

    for (let link of links) {
      try {
        link.url = this._validateURI(
          dataTransfer,
          link.url,
          aDisallowInherit,
          triggeringPrincipal
        );
      } catch (ex) {
        // Prevent the drop entirely if any of the links are invalid even if
        // one of them is valid.
        aEvent.stopPropagation();
        aEvent.preventDefault();
        throw ex;
      }
    }

    return links;
  },

  validateURIsForDrop(aEvent, aURIs, aDisallowInherit) {
    let dataTransfer = aEvent.dataTransfer;
    let triggeringPrincipal = this._getTriggeringPrincipalFromDataTransfer(
      dataTransfer,
      false
    );

    for (let uri of aURIs) {
      this._validateURI(
        dataTransfer,
        uri,
        aDisallowInherit,
        triggeringPrincipal
      );
    }
  },

  queryLinks(aDataTransfer) {
    return this._getDropLinks(aDataTransfer);
  },

  _eventTargetIsDisabled(aEvent) {
    let ownerDoc = aEvent.originalTarget.ownerDocument;
    if (!ownerDoc || !ownerDoc.defaultView) {
      return false;
    }

    return ownerDoc.defaultView.windowUtils.isNodeDisabledForEvents(
      aEvent.originalTarget
    );
  },
};