Source code

Revision control

Other Tools

1
/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
2
/* vim: set ts=2 sw=2 sts=2 et tw=80: */
3
/* This Source Code Form is subject to the terms of the Mozilla Public
4
* License, v. 2.0. If a copy of the MPL was not distributed with this
5
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7
"use strict";
8
9
var EXPORTED_SYMBOLS = ["ContextMenuChild"];
10
11
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
12
const { XPCOMUtils } = ChromeUtils.import(
14
);
15
16
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
17
18
XPCOMUtils.defineLazyModuleGetters(this, {
25
InlineSpellCheckerContent:
28
});
29
30
XPCOMUtils.defineLazyGetter(this, "PageMenuChild", () => {
31
let tmp = {};
32
ChromeUtils.import("resource://gre/modules/PageMenu.jsm", tmp);
33
return new tmp.PageMenuChild();
34
});
35
36
let contextMenus = new WeakMap();
37
38
class ContextMenuChild extends JSWindowActorChild {
39
// PUBLIC
40
constructor() {
41
super();
42
43
this.target = null;
44
this.context = null;
45
this.lastMenuTarget = null;
46
}
47
48
static getTarget(browsingContext, message, key) {
49
let actor = contextMenus.get(browsingContext);
50
if (!actor) {
51
throw new Error(
52
"Can't find ContextMenu actor for browsing context with " +
53
"ID: " +
54
browsingContext.id
55
);
56
}
57
return actor.getTarget(message, key);
58
}
59
60
static getLastTarget(browsingContext) {
61
let contextMenu = contextMenus.get(browsingContext);
62
return contextMenu && contextMenu.lastMenuTarget;
63
}
64
65
receiveMessage(message) {
66
switch (message.name) {
67
case "ContextMenu:GetFrameTitle": {
68
let target = ContentDOMReference.resolve(message.data.targetIdentifier);
69
return Promise.resolve(target.ownerDocument.title);
70
}
71
72
case "ContextMenu:Canvas:ToBlobURL": {
73
let target = ContentDOMReference.resolve(message.data.targetIdentifier);
74
return new Promise(resolve => {
75
target.toBlob(blob => {
76
let blobURL = URL.createObjectURL(blob);
77
resolve(blobURL);
78
});
79
});
80
}
81
82
case "ContextMenu:DoCustomCommand": {
83
E10SUtils.wrapHandlingUserInput(
84
this.contentWindow,
85
message.data.handlingUserInput,
86
() => PageMenuChild.executeMenu(message.data.generatedItemId)
87
);
88
break;
89
}
90
91
case "ContextMenu:Hiding": {
92
this.context = null;
93
this.target = null;
94
break;
95
}
96
97
case "ContextMenu:MediaCommand": {
98
E10SUtils.wrapHandlingUserInput(
99
this.contentWindow,
100
message.data.handlingUserInput,
101
() => {
102
let media = ContentDOMReference.resolve(
103
message.data.targetIdentifier
104
);
105
106
switch (message.data.command) {
107
case "play":
108
media.play();
109
break;
110
case "pause":
111
media.pause();
112
break;
113
case "loop":
114
media.loop = !media.loop;
115
break;
116
case "mute":
117
media.muted = true;
118
break;
119
case "unmute":
120
media.muted = false;
121
break;
122
case "playbackRate":
123
media.playbackRate = message.data.data;
124
break;
125
case "hidecontrols":
126
media.removeAttribute("controls");
127
break;
128
case "showcontrols":
129
media.setAttribute("controls", "true");
130
break;
131
case "fullscreen":
132
if (this.document.fullscreenEnabled) {
133
media.requestFullscreen();
134
}
135
break;
136
case "pictureinpicture":
137
Services.telemetry.keyedScalarAdd(
138
"pictureinpicture.opened_method",
139
"contextmenu",
140
1
141
);
142
let event = new this.contentWindow.CustomEvent(
143
"MozTogglePictureInPicture",
144
{
145
bubbles: true,
146
},
147
this.contentWindow
148
);
149
media.dispatchEvent(event);
150
break;
151
}
152
}
153
);
154
break;
155
}
156
157
case "ContextMenu:ReloadFrame": {
158
let target = ContentDOMReference.resolve(message.data.targetIdentifier);
159
target.ownerDocument.location.reload(message.data.forceReload);
160
break;
161
}
162
163
case "ContextMenu:ReloadImage": {
164
let image = ContentDOMReference.resolve(message.data.targetIdentifier);
165
166
if (image instanceof Ci.nsIImageLoadingContent) {
167
image.forceReload();
168
}
169
break;
170
}
171
172
case "ContextMenu:SearchFieldBookmarkData": {
173
let node = ContentDOMReference.resolve(message.data.targetIdentifier);
174
let charset = node.ownerDocument.characterSet;
175
let formBaseURI = Services.io.newURI(node.form.baseURI, charset);
176
let formURI = Services.io.newURI(
177
node.form.getAttribute("action"),
178
charset,
179
formBaseURI
180
);
181
let spec = formURI.spec;
182
let isURLEncoded =
183
node.form.method.toUpperCase() == "POST" &&
184
(node.form.enctype == "application/x-www-form-urlencoded" ||
185
node.form.enctype == "");
186
let title = node.ownerDocument.title;
187
188
function escapeNameValuePair([aName, aValue]) {
189
if (isURLEncoded) {
190
return escape(aName + "=" + aValue);
191
}
192
193
return escape(aName) + "=" + escape(aValue);
194
}
195
let formData = new this.contentWindow.FormData(node.form);
196
formData.delete(node.name);
197
formData = Array.from(formData).map(escapeNameValuePair);
198
formData.push(
199
escape(node.name) + (isURLEncoded ? escape("=%s") : "=%s")
200
);
201
202
let postData;
203
204
if (isURLEncoded) {
205
postData = formData.join("&");
206
} else {
207
let separator = spec.includes("?") ? "&" : "?";
208
spec += separator + formData.join("&");
209
}
210
211
return Promise.resolve({ spec, title, postData, charset });
212
}
213
214
case "ContextMenu:SaveVideoFrameAsImage": {
215
let video = ContentDOMReference.resolve(message.data.targetIdentifier);
216
let canvas = this.document.createElementNS(
218
"canvas"
219
);
220
canvas.width = video.videoWidth;
221
canvas.height = video.videoHeight;
222
223
let ctxDraw = canvas.getContext("2d");
224
ctxDraw.drawImage(video, 0, 0);
225
226
// Note: if changing the content type, don't forget to update
227
// consumers that also hardcode this content type.
228
return Promise.resolve(canvas.toDataURL("image/jpeg", ""));
229
}
230
231
case "ContextMenu:SetAsDesktopBackground": {
232
let target = ContentDOMReference.resolve(message.data.targetIdentifier);
233
234
// Paranoia: check disableSetDesktopBackground again, in case the
235
// image changed since the context menu was initiated.
236
let disable = this._disableSetDesktopBackground(target);
237
238
if (!disable) {
239
try {
240
BrowserUtils.urlSecurityCheck(
241
target.currentURI.spec,
242
target.ownerDocument.nodePrincipal
243
);
244
let canvas = this.document.createElement("canvas");
245
canvas.width = target.naturalWidth;
246
canvas.height = target.naturalHeight;
247
let ctx = canvas.getContext("2d");
248
ctx.drawImage(target, 0, 0);
249
let dataURL = canvas.toDataURL();
250
let url = new URL(target.ownerDocument.location.href).pathname;
251
let imageName = url.substr(url.lastIndexOf("/") + 1);
252
return Promise.resolve({ failed: false, dataURL, imageName });
253
} catch (e) {
254
Cu.reportError(e);
255
}
256
}
257
258
return Promise.resolve({
259
failed: true,
260
dataURL: null,
261
imageName: null,
262
});
263
}
264
265
case "ContextMenu:PluginCommand": {
266
let target = ContentDOMReference.resolve(message.data.targetIdentifier);
267
let actor = this.manager.getActor("Plugin");
268
let { command } = message.data;
269
if (command == "play") {
270
actor.showClickToPlayNotification(target, true);
271
} else if (command == "hide") {
272
actor.hideClickToPlayOverlay(target);
273
}
274
break;
275
}
276
}
277
278
return undefined;
279
}
280
281
/**
282
* Returns the event target of the context menu, using a locally stored
283
* reference if possible. If not, and aMessage.objects is defined,
284
* aMessage.objects[aKey] is returned. Otherwise null.
285
* @param {Object} aMessage Message with a objects property
286
* @param {String} aKey Key for the target on aMessage.objects
287
* @return {Object} Context menu target
288
*/
289
getTarget(aMessage, aKey = "target") {
290
return this.target || (aMessage.objects && aMessage.objects[aKey]);
291
}
292
293
// PRIVATE
294
_isXULTextLinkLabel(aNode) {
295
const XUL_NS =
297
return (
298
aNode.namespaceURI == XUL_NS &&
299
aNode.tagName == "label" &&
300
aNode.classList.contains("text-link") &&
301
aNode.href
302
);
303
}
304
305
// Generate fully qualified URL for clicked-on link.
306
_getLinkURL() {
307
let href = this.context.link.href;
308
309
if (href) {
310
// Handle SVG links:
311
if (typeof href == "object" && href.animVal) {
312
return this._makeURLAbsolute(this.context.link.baseURI, href.animVal);
313
}
314
315
return href;
316
}
317
318
href =
319
this.context.link.getAttribute("href") ||
320
this.context.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
321
322
if (!href || !href.match(/\S/)) {
323
// Without this we try to save as the current doc,
324
// for example, HTML case also throws if empty
325
throw new Error("Empty href");
326
}
327
328
return this._makeURLAbsolute(this.context.link.baseURI, href);
329
}
330
331
_getLinkURI() {
332
try {
333
return Services.io.newURI(this.context.linkURL);
334
} catch (ex) {
335
// e.g. empty URL string
336
}
337
338
return null;
339
}
340
341
// Get text of link.
342
_getLinkText() {
343
let text = this._gatherTextUnder(this.context.link);
344
345
if (!text || !text.match(/\S/)) {
346
text = this.context.link.getAttribute("title");
347
if (!text || !text.match(/\S/)) {
348
text = this.context.link.getAttribute("alt");
349
if (!text || !text.match(/\S/)) {
350
text = this.context.linkURL;
351
}
352
}
353
}
354
355
return text;
356
}
357
358
_getLinkProtocol() {
359
if (this.context.linkURI) {
360
return this.context.linkURI.scheme; // can be |undefined|
361
}
362
363
return null;
364
}
365
366
// Returns true if clicked-on link targets a resource that can be saved.
367
_isLinkSaveable(aLink) {
368
// We don't do the Right Thing for news/snews yet, so turn them off
369
// until we do.
370
return (
371
this.context.linkProtocol &&
372
!(
373
this.context.linkProtocol == "mailto" ||
374
this.context.linkProtocol == "javascript" ||
375
this.context.linkProtocol == "news" ||
376
this.context.linkProtocol == "snews"
377
)
378
);
379
}
380
381
// Gather all descendent text under given document node.
382
_gatherTextUnder(root) {
383
let text = "";
384
let node = root.firstChild;
385
let depth = 1;
386
while (node && depth > 0) {
387
// See if this node is text.
388
if (node.nodeType == node.TEXT_NODE) {
389
// Add this text to our collection.
390
text += " " + node.data;
391
} else if (node instanceof this.contentWindow.HTMLImageElement) {
392
// If it has an "alt" attribute, add that.
393
let altText = node.getAttribute("alt");
394
if (altText && altText != "") {
395
text += " " + altText;
396
}
397
}
398
// Find next node to test.
399
// First, see if this node has children.
400
if (node.hasChildNodes()) {
401
// Go to first child.
402
node = node.firstChild;
403
depth++;
404
} else {
405
// No children, try next sibling (or parent next sibling).
406
while (depth > 0 && !node.nextSibling) {
407
node = node.parentNode;
408
depth--;
409
}
410
if (node.nextSibling) {
411
node = node.nextSibling;
412
}
413
}
414
}
415
416
// Strip leading and tailing whitespace.
417
text = text.trim();
418
// Compress remaining whitespace.
419
text = text.replace(/\s+/g, " ");
420
return text;
421
}
422
423
// Returns a "url"-type computed style attribute value, with the url() stripped.
424
_getComputedURL(aElem, aProp) {
425
let urls = aElem.ownerGlobal.getComputedStyle(aElem).getCSSImageURLs(aProp);
426
427
if (!urls.length) {
428
return null;
429
}
430
431
if (urls.length != 1) {
432
throw new Error("found multiple URLs");
433
}
434
435
return urls[0];
436
}
437
438
_makeURLAbsolute(aBase, aUrl) {
439
return Services.io.newURI(aUrl, null, Services.io.newURI(aBase)).spec;
440
}
441
442
_isProprietaryDRM() {
443
return (
444
this.context.target.isEncrypted &&
445
this.context.target.mediaKeys &&
446
this.context.target.mediaKeys.keySystem != "org.w3.clearkey"
447
);
448
}
449
450
_isMediaURLReusable(aURL) {
451
if (aURL.startsWith("blob:")) {
452
return URL.isValidURL(aURL);
453
}
454
455
return true;
456
}
457
458
_isTargetATextBox(node) {
459
if (node instanceof this.contentWindow.HTMLInputElement) {
460
return node.mozIsTextField(false);
461
}
462
463
return node instanceof this.contentWindow.HTMLTextAreaElement;
464
}
465
466
/**
467
* Check if we are in the parent process and the current iframe is the RDM iframe.
468
*/
469
_isTargetRDMFrame(node) {
470
return (
471
Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT &&
472
node.tagName === "iframe" &&
473
node.hasAttribute("mozbrowser")
474
);
475
}
476
477
_isSpellCheckEnabled(aNode) {
478
// We can always force-enable spellchecking on textboxes
479
if (this._isTargetATextBox(aNode)) {
480
return true;
481
}
482
483
// We can never spell check something which is not content editable
484
let editable = aNode.isContentEditable;
485
486
if (!editable && aNode.ownerDocument) {
487
editable = aNode.ownerDocument.designMode == "on";
488
}
489
490
if (!editable) {
491
return false;
492
}
493
494
// Otherwise make sure that nothing in the parent chain disables spellchecking
495
return aNode.spellcheck;
496
}
497
498
_disableSetDesktopBackground(aTarget) {
499
// Disable the Set as Desktop Background menu item if we're still trying
500
// to load the image or the load failed.
501
if (!(aTarget instanceof Ci.nsIImageLoadingContent)) {
502
return true;
503
}
504
505
if ("complete" in aTarget && !aTarget.complete) {
506
return true;
507
}
508
509
if (aTarget.currentURI.schemeIs("javascript")) {
510
return true;
511
}
512
513
let request = aTarget.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
514
515
if (!request) {
516
return true;
517
}
518
519
return false;
520
}
521
522
handleEvent(aEvent) {
523
contextMenus.set(this.browsingContext, this);
524
525
let defaultPrevented = aEvent.defaultPrevented;
526
527
if (
528
// If the event is not from a chrome-privileged document, and if
529
// `dom.event.contextmenu.enabled` is false, force defaultPrevented=false.
530
!aEvent.composedTarget.nodePrincipal.isSystemPrincipal &&
531
!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")
532
) {
533
let plugin = null;
534
535
try {
536
plugin = aEvent.composedTarget.QueryInterface(
537
Ci.nsIObjectLoadingContent
538
);
539
} catch (e) {}
540
541
if (
542
plugin &&
543
plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN
544
) {
545
// Don't open a context menu for plugins.
546
return;
547
}
548
549
defaultPrevented = false;
550
}
551
552
if (defaultPrevented) {
553
return;
554
}
555
556
if (this._isTargetRDMFrame(aEvent.composedTarget)) {
557
// The target is in the DevTools RDM iframe, a proper context menu event
558
// will be created from the RDM browser.
559
return;
560
}
561
562
let doc = aEvent.composedTarget.ownerDocument;
563
let {
564
mozDocumentURIIfNotForErrorPages: docLocation,
565
characterSet: charSet,
566
baseURI,
567
} = doc;
568
docLocation = docLocation && docLocation.spec;
569
let frameOuterWindowID = WebNavigationFrames.getFrameId(doc.defaultView);
570
let loginFillInfo = LoginManagerChild.forWindow(
571
doc.defaultView
572
).getFieldContext(aEvent.composedTarget);
573
574
// The same-origin check will be done in nsContextMenu.openLinkInTab.
575
let parentAllowsMixedContent = !!this.docShell.mixedContentChannel;
576
577
let disableSetDesktopBackground = null;
578
579
// Media related cache info parent needs for saving
580
let contentType = null;
581
let contentDisposition = null;
582
if (
583
aEvent.composedTarget.nodeType == aEvent.composedTarget.ELEMENT_NODE &&
584
aEvent.composedTarget instanceof Ci.nsIImageLoadingContent &&
585
aEvent.composedTarget.currentURI
586
) {
587
disableSetDesktopBackground = this._disableSetDesktopBackground(
588
aEvent.composedTarget
589
);
590
591
try {
592
let imageCache = Cc["@mozilla.org/image/tools;1"]
593
.getService(Ci.imgITools)
594
.getImgCacheForDocument(doc);
595
// The image cache's notion of where this image is located is
596
// the currentURI of the image loading content.
597
let props = imageCache.findEntryProperties(
598
aEvent.composedTarget.currentURI,
599
doc
600
);
601
602
try {
603
contentType = props.get("type", Ci.nsISupportsCString).data;
604
} catch (e) {}
605
606
try {
607
contentDisposition = props.get(
608
"content-disposition",
609
Ci.nsISupportsCString
610
).data;
611
} catch (e) {}
612
} catch (e) {}
613
}
614
615
let selectionInfo = BrowserUtils.getSelectionDetails(this.contentWindow);
616
let loadContext = this.docShell.QueryInterface(Ci.nsILoadContext);
617
let userContextId = loadContext.originAttributes.userContextId;
618
619
this._setContext(aEvent);
620
let context = this.context;
621
this.target = context.target;
622
623
let spellInfo = null;
624
let editFlags = null;
625
let principal = null;
626
let customMenuItems = null;
627
628
let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
629
Ci.nsIReferrerInfo
630
);
631
referrerInfo.initWithNode(aEvent.composedTarget);
632
referrerInfo = E10SUtils.serializeReferrerInfo(referrerInfo);
633
634
// In the case "onLink" we may have to send link referrerInfo to use in
635
// _openLinkInParameters
636
let linkReferrerInfo = null;
637
if (context.onLink) {
638
linkReferrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
639
Ci.nsIReferrerInfo
640
);
641
linkReferrerInfo.initWithNode(context.link);
642
}
643
644
let target = context.target;
645
if (target) {
646
this._cleanContext();
647
}
648
649
editFlags = SpellCheckHelper.isEditable(
650
aEvent.composedTarget,
651
this.contentWindow
652
);
653
654
if (editFlags & SpellCheckHelper.SPELLCHECKABLE) {
655
spellInfo = InlineSpellCheckerContent.initContextMenu(
656
aEvent,
657
editFlags,
658
this
659
);
660
}
661
662
// Set the event target first as the copy image command needs it to
663
// determine what was context-clicked on. Then, update the state of the
664
// commands on the context menu.
665
this.docShell.contentViewer
666
.QueryInterface(Ci.nsIContentViewerEdit)
667
.setCommandNode(aEvent.composedTarget);
668
aEvent.composedTarget.ownerGlobal.updateCommands("contentcontextmenu");
669
670
let data = {
671
context,
672
charSet,
673
baseURI,
674
referrerInfo,
675
editFlags,
676
principal,
677
spellInfo,
678
contentType,
679
docLocation,
680
loginFillInfo,
681
selectionInfo,
682
userContextId,
683
customMenuItems,
684
contentDisposition,
685
frameOuterWindowID,
686
disableSetDesktopBackground,
687
parentAllowsMixedContent,
688
};
689
690
if (context.inFrame && !context.inSrcdocFrame) {
691
data.frameReferrerInfo = E10SUtils.serializeReferrerInfo(
692
doc.referrerInfo
693
);
694
}
695
696
if (linkReferrerInfo) {
697
data.linkReferrerInfo = E10SUtils.serializeReferrerInfo(linkReferrerInfo);
698
}
699
700
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
701
data.customMenuItems = PageMenuChild.build(aEvent.composedTarget);
702
}
703
704
Services.obs.notifyObservers(
705
{ wrappedJSObject: data },
706
"on-prepare-contextmenu"
707
);
708
709
data.principal = doc.nodePrincipal;
710
data.context.principal = context.principal;
711
data.storagePrincipal = doc.effectiveStoragePrincipal;
712
data.context.storagePrincipal = context.storagePrincipal;
713
714
// In the event that the content is running in the parent process, we don't
715
// actually want the contextmenu events to reach the parent - we'll dispatch
716
// a new contextmenu event after the async message has reached the parent
717
// instead.
718
aEvent.stopPropagation();
719
720
this.sendAsyncMessage("contextmenu", data);
721
}
722
723
/**
724
* Some things are not serializable, so we either have to only send
725
* their needed data or regenerate them in nsContextMenu.js
726
* - target and target.ownerDocument
727
* - link
728
* - linkURI
729
*/
730
_cleanContext(aEvent) {
731
const context = this.context;
732
const cleanTarget = Object.create(null);
733
734
cleanTarget.ownerDocument = {
735
// used for nsContextMenu.initLeaveDOMFullScreenItems and
736
// nsContextMenu.initMediaPlayerItems
737
fullscreen: context.target.ownerDocument.fullscreen,
738
739
// used for nsContextMenu.initMiscItems
740
contentType: context.target.ownerDocument.contentType,
741
742
// used for nsContextMenu.saveLink
743
isPrivate: PrivateBrowsingUtils.isContentWindowPrivate(
744
context.target.ownerGlobal
745
),
746
};
747
748
// used for nsContextMenu.initMediaPlayerItems
749
Object.assign(cleanTarget, {
750
ended: context.target.ended,
751
muted: context.target.muted,
752
paused: context.target.paused,
753
controls: context.target.controls,
754
duration: context.target.duration,
755
});
756
757
const onMedia = context.onVideo || context.onAudio;
758
759
if (onMedia) {
760
Object.assign(cleanTarget, {
761
loop: context.target.loop,
762
error: context.target.error,
763
networkState: context.target.networkState,
764
playbackRate: context.target.playbackRate,
765
NETWORK_NO_SOURCE: context.target.NETWORK_NO_SOURCE,
766
});
767
768
if (context.onVideo) {
769
Object.assign(cleanTarget, {
770
readyState: context.target.readyState,
771
HAVE_CURRENT_DATA: context.target.HAVE_CURRENT_DATA,
772
});
773
}
774
}
775
776
context.target = cleanTarget;
777
778
if (context.link) {
779
context.link = { href: context.linkURL };
780
}
781
782
delete context.linkURI;
783
}
784
785
_setContext(aEvent) {
786
this.context = Object.create(null);
787
const context = this.context;
788
789
context.timeStamp = aEvent.timeStamp;
790
context.screenX = aEvent.screenX;
791
context.screenY = aEvent.screenY;
792
context.mozInputSource = aEvent.mozInputSource;
793
794
let node = aEvent.composedTarget;
795
796
// Set the node to containing <video>/<audio>/<embed>/<object> if the node
797
// is in the videocontrols/pluginProblem UA Widget.
798
if (this.contentWindow.ShadowRoot) {
799
let n = node;
800
while (n) {
801
if (n instanceof this.contentWindow.ShadowRoot) {
802
if (
803
n.host instanceof this.contentWindow.HTMLMediaElement ||
804
n.host instanceof this.contentWindow.HTMLEmbedElement ||
805
n.host instanceof this.contentWindow.HTMLObjectElement
806
) {
807
node = n.host;
808
break;
809
}
810
break;
811
}
812
n = n.parentNode;
813
}
814
}
815
816
const XUL_NS =
818
819
context.shouldDisplay = true;
820
821
if (
822
node.nodeType == node.DOCUMENT_NODE ||
823
// Don't display for XUL element unless <label class="text-link">
824
(node.namespaceURI == XUL_NS && !this._isXULTextLinkLabel(node))
825
) {
826
context.shouldDisplay = false;
827
return;
828
}
829
830
const isAboutDevtoolsToolbox = this.document.documentURI.startsWith(
831
"about:devtools-toolbox"
832
);
833
const editFlags = SpellCheckHelper.isEditable(node, this.contentWindow);
834
835
if (
836
isAboutDevtoolsToolbox &&
837
(editFlags & SpellCheckHelper.TEXTINPUT) === 0
838
) {
839
// Don't display for about:devtools-toolbox page unless the source was text input.
840
context.shouldDisplay = false;
841
return;
842
}
843
844
// Initialize context to be sent to nsContextMenu
845
// Keep this consistent with the similar code in nsContextMenu's setContext
846
context.bgImageURL = "";
847
context.imageDescURL = "";
848
context.imageInfo = null;
849
context.mediaURL = "";
850
context.webExtBrowserType = "";
851
852
context.canSpellCheck = false;
853
context.hasBGImage = false;
854
context.hasMultipleBGImages = false;
855
context.isDesignMode = false;
856
context.inFrame = false;
857
context.inSrcdocFrame = false;
858
context.inSyntheticDoc = false;
859
context.inTabBrowser = true;
860
context.inWebExtBrowser = false;
861
862
context.link = null;
863
context.linkDownload = "";
864
context.linkProtocol = "";
865
context.linkTextStr = "";
866
context.linkURL = "";
867
context.linkURI = null;
868
869
context.onAudio = false;
870
context.onCanvas = false;
871
context.onCompletedImage = false;
872
context.onCTPPlugin = false;
873
context.onDRMMedia = false;
874
context.onPiPVideo = false;
875
context.onMediaStreamVideo = false;
876
context.onEditable = false;
877
context.onImage = false;
878
context.onKeywordField = false;
879
context.onLink = false;
880
context.onLoadedImage = false;
881
context.onMailtoLink = false;
882
context.onMozExtLink = false;
883
context.onNumeric = false;
884
context.onPassword = false;
885
context.onSaveableLink = false;
886
context.onSpellcheckable = false;
887
context.onTextInput = false;
888
context.onVideo = false;
889
890
// Remember the node and its owner document that was clicked
891
// This may be modifed before sending to nsContextMenu
892
context.target = node;
893
context.targetIdentifier = ContentDOMReference.get(node);
894
895
context.principal = context.target.ownerDocument.nodePrincipal;
896
context.storagePrincipal =
897
context.target.ownerDocument.effectiveStoragePrincipal;
898
context.csp = E10SUtils.serializeCSP(context.target.ownerDocument.csp);
899
900
context.frameOuterWindowID = WebNavigationFrames.getFrameId(
901
context.target.ownerGlobal
902
);
903
904
// Check if we are in a synthetic document (stand alone image, video, etc.).
905
context.inSyntheticDoc = context.target.ownerDocument.mozSyntheticDocument;
906
907
context.shouldInitInlineSpellCheckerUINoChildren = false;
908
context.shouldInitInlineSpellCheckerUIWithChildren = false;
909
910
this._setContextForNodesNoChildren(editFlags);
911
this._setContextForNodesWithChildren(editFlags);
912
913
this.lastMenuTarget = {
914
// Remember the node for extensions.
915
targetRef: Cu.getWeakReference(node),
916
// The timestamp is used to verify that the target wasn't changed since the observed menu event.
917
timeStamp: context.timeStamp,
918
};
919
920
if (isAboutDevtoolsToolbox) {
921
// Setup the menu items on text input in about:devtools-toolbox.
922
context.inAboutDevtoolsToolbox = true;
923
context.canSpellCheck = false;
924
context.inTabBrowser = false;
925
context.inFrame = false;
926
context.inSrcdocFrame = false;
927
context.onSpellcheckable = false;
928
}
929
}
930
931
/**
932
* Sets up the parts of the context menu for when when nodes have no children.
933
*
934
* @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
935
* for the details.
936
*/
937
_setContextForNodesNoChildren(editFlags) {
938
const context = this.context;
939
940
if (context.target.nodeType == context.target.TEXT_NODE) {
941
// For text nodes, look at the parent node to determine the spellcheck attribute.
942
context.canSpellCheck =
943
context.target.parentNode && this._isSpellCheckEnabled(context.target);
944
return;
945
}
946
947
// We only deal with TEXT_NODE and ELEMENT_NODE in this function, so return
948
// early if we don't have one.
949
if (context.target.nodeType != context.target.ELEMENT_NODE) {
950
return;
951
}
952
953
// See if the user clicked on an image. This check mirrors
954
// nsDocumentViewer::GetInImage. Make sure to update both if this is
955
// changed.
956
if (
957
context.target instanceof Ci.nsIImageLoadingContent &&
958
(context.target.currentRequestFinalURI || context.target.currentURI)
959
) {
960
context.onImage = true;
961
962
context.imageInfo = {
963
currentSrc: context.target.currentSrc,
964
width: context.target.width,
965
height: context.target.height,
966
imageText: context.target.title || context.target.alt,
967
};
968
969
const request = context.target.getRequest(
970
Ci.nsIImageLoadingContent.CURRENT_REQUEST
971
);
972
973
if (request && request.imageStatus & request.STATUS_SIZE_AVAILABLE) {
974
context.onLoadedImage = true;
975
}
976
977
if (
978
request &&
979
request.imageStatus & request.STATUS_LOAD_COMPLETE &&
980
!(request.imageStatus & request.STATUS_ERROR)
981
) {
982
context.onCompletedImage = true;
983
}
984
985
// The actual URL the image was loaded from (after redirects) is the
986
// currentRequestFinalURI. We should use that as the URL for purposes of
987
// deciding on the filename, if it is present. It might not be present
988
// if images are blocked.
989
context.mediaURL = (
990
context.target.currentRequestFinalURI || context.target.currentURI
991
).spec;
992
993
const descURL = context.target.getAttribute("longdesc");
994
995
if (descURL) {
996
context.imageDescURL = this._makeURLAbsolute(
997
context.target.ownerDocument.body.baseURI,
998
descURL
999
);
1000
}
1001
} else if (context.target instanceof this.contentWindow.HTMLCanvasElement) {
1002
context.onCanvas = true;
1003
} else if (context.target instanceof this.contentWindow.HTMLVideoElement) {
1004
const mediaURL = context.target.currentSrc || context.target.src;
1005
1006
if (this._isMediaURLReusable(mediaURL)) {
1007
context.mediaURL = mediaURL;
1008
}
1009
1010
if (this._isProprietaryDRM()) {
1011
context.onDRMMedia = true;
1012
}
1013
1014
if (context.target.isCloningElementVisually) {
1015
context.onPiPVideo = true;
1016
}
1017
1018
context.onMediaStreamVideo = !!context.target.srcObject;
1019
1020
// Firefox always creates a HTMLVideoElement when loading an ogg file
1021
// directly. If the media is actually audio, be smarter and provide a
1022
// context menu with audio operations.
1023
if (
1024
context.target.readyState >= context.target.HAVE_METADATA &&
1025
(context.target.videoWidth == 0 || context.target.videoHeight == 0)
1026
) {
1027
context.onAudio = true;
1028
} else {
1029
context.onVideo = true;
1030
}
1031
} else if (context.target instanceof this.contentWindow.HTMLAudioElement) {
1032
context.onAudio = true;
1033
const mediaURL = context.target.currentSrc || context.target.src;
1034
1035
if (this._isMediaURLReusable(mediaURL)) {
1036
context.mediaURL = mediaURL;
1037
}
1038
1039
if (this._isProprietaryDRM()) {
1040
context.onDRMMedia = true;
1041
}
1042
} else if (
1043
editFlags &
1044
(SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)
1045
) {
1046
context.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
1047
context.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
1048
context.onEditable = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
1049
context.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
1050
context.onSpellcheckable =
1051
(editFlags & SpellCheckHelper.SPELLCHECKABLE) !== 0;
1052
1053
// This is guaranteed to be an input or textarea because of the condition above,
1054
// so the no-children flag is always correct. We deal with contenteditable elsewhere.
1055
if (context.onSpellcheckable) {
1056
context.shouldInitInlineSpellCheckerUINoChildren = true;
1057
}
1058
1059
context.onKeywordField = editFlags & SpellCheckHelper.KEYWORD;
1060
} else if (context.target instanceof this.contentWindow.HTMLHtmlElement) {
1061
const bodyElt = context.target.ownerDocument.body;
1062
1063
if (bodyElt) {
1064
let computedURL;
1065
1066
try {
1067
computedURL = this._getComputedURL(bodyElt, "background-image");
1068
context.hasMultipleBGImages = false;
1069
} catch (e) {
1070
context.hasMultipleBGImages = true;
1071
}
1072
1073
if (computedURL) {
1074
context.hasBGImage = true;
1075
context.bgImageURL = this._makeURLAbsolute(
1076
bodyElt.baseURI,
1077
computedURL
1078
);
1079
}
1080
}
1081
} else if (
1082
(context.target instanceof this.contentWindow.HTMLEmbedElement ||
1083
context.target instanceof this.contentWindow.HTMLObjectElement) &&
1084
context.target.displayedType ==
1085
this.contentWindow.HTMLObjectElement.TYPE_NULL &&
1086
context.target.pluginFallbackType ==
1087
this.contentWindow.HTMLObjectElement.PLUGIN_CLICK_TO_PLAY
1088
) {
1089
context.onCTPPlugin = true;
1090
}
1091
1092
context.canSpellCheck = this._isSpellCheckEnabled(context.target);
1093
}
1094
1095
/**
1096
* Sets up the parts of the context menu for when when nodes have children.
1097
*
1098
* @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
1099
* for the details.
1100
*/
1101
_setContextForNodesWithChildren(editFlags) {
1102
const context = this.context;
1103
1104
// Second, bubble out, looking for items of interest that can have childen.
1105
// Always pick the innermost link, background image, etc.
1106
let elem = context.target;
1107
1108
while (elem) {
1109
if (elem.nodeType == elem.ELEMENT_NODE) {
1110
// Link?
1111
const XLINK_NS = "http://www.w3.org/1999/xlink";
1112
1113
if (
1114
!context.onLink &&
1115
// Be consistent with what hrefAndLinkNodeForClickEvent
1116
// does in browser.js
1117
(this._isXULTextLinkLabel(elem) ||
1118
(elem instanceof this.contentWindow.HTMLAnchorElement &&
1119
elem.href) ||
1120
(elem instanceof this.contentWindow.SVGAElement &&
1121
(elem.href || elem.hasAttributeNS(XLINK_NS, "href"))) ||
1122
(elem instanceof this.contentWindow.HTMLAreaElement && elem.href) ||
1123
elem instanceof this.contentWindow.HTMLLinkElement ||
1124
elem.getAttributeNS(XLINK_NS, "type") == "simple")
1125
) {
1126
// Target is a link or a descendant of a link.
1127
context.onLink = true;
1128
1129
// Remember corresponding element.
1130
context.link = elem;
1131
context.linkURL = this._getLinkURL();
1132
context.linkURI = this._getLinkURI();
1133
context.linkTextStr = this._getLinkText();
1134
context.linkProtocol = this._getLinkProtocol();
1135
context.onMailtoLink = context.linkProtocol == "mailto";
1136
context.onMozExtLink = context.linkProtocol == "moz-extension";
1137
context.onSaveableLink = this._isLinkSaveable(context.link);
1138
1139
try {
1140
if (elem.download) {
1141
// Ignore download attribute on cross-origin links
1142
context.principal.checkMayLoad(context.linkURI, true);
1143
context.linkDownload = elem.download;
1144
}
1145
} catch (ex) {}
1146
}
1147
1148
// Background image? Don't bother if we've already found a
1149
// background image further down the hierarchy. Otherwise,
1150
// we look for the computed background-image style.
1151
if (!context.hasBGImage && !context.hasMultipleBGImages) {
1152
let bgImgUrl = null;
1153
1154
try {
1155
bgImgUrl = this._getComputedURL(elem, "background-image");
1156
context.hasMultipleBGImages = false;
1157
} catch (e) {
1158
context.hasMultipleBGImages = true;
1159
}
1160
1161
if (bgImgUrl) {
1162
context.hasBGImage = true;
1163
context.bgImageURL = this._makeURLAbsolute(elem.baseURI, bgImgUrl);
1164
}
1165
}
1166
}
1167
1168
elem = elem.parentNode;
1169
}
1170
1171
// See if the user clicked in a frame.
1172
const docDefaultView = context.target.ownerGlobal;
1173
1174
if (docDefaultView != docDefaultView.top) {
1175
context.inFrame = true;
1176
1177
if (context.target.ownerDocument.isSrcdocDocument) {
1178
context.inSrcdocFrame = true;
1179
}
1180
}
1181
1182
// if the document is editable, show context menu like in text inputs
1183
if (!context.onEditable) {
1184
if (editFlags & SpellCheckHelper.CONTENTEDITABLE) {
1185
// If this.onEditable is false but editFlags is CONTENTEDITABLE, then
1186
// the document itself must be editable.
1187
context.onTextInput = true;
1188
context.onKeywordField = false;
1189
context.onImage = false;
1190
context.onLoadedImage = false;
1191
context.onCompletedImage = false;
1192
context.inFrame = false;
1193
context.inSrcdocFrame = false;
1194
context.hasBGImage = false;
1195
context.isDesignMode = true;
1196
context.onEditable = true;
1197
context.onSpellcheckable = true;
1198
context.shouldInitInlineSpellCheckerUIWithChildren = true;
1199
}
1200
}
1201
}
1202
}