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