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
265
return undefined;
266
}
267
268
/**
269
* Returns the event target of the context menu, using a locally stored
270
* reference if possible. If not, and aMessage.objects is defined,
271
* aMessage.objects[aKey] is returned. Otherwise null.
272
* @param {Object} aMessage Message with a objects property
273
* @param {String} aKey Key for the target on aMessage.objects
274
* @return {Object} Context menu target
275
*/
276
getTarget(aMessage, aKey = "target") {
277
return this.target || (aMessage.objects && aMessage.objects[aKey]);
278
}
279
280
// PRIVATE
281
_isXULTextLinkLabel(aNode) {
282
const XUL_NS =
284
return (
285
aNode.namespaceURI == XUL_NS &&
286
aNode.tagName == "label" &&
287
aNode.classList.contains("text-link") &&
288
aNode.href
289
);
290
}
291
292
// Generate fully qualified URL for clicked-on link.
293
_getLinkURL() {
294
let href = this.context.link.href;
295
296
if (href) {
297
// Handle SVG links:
298
if (typeof href == "object" && href.animVal) {
299
return this._makeURLAbsolute(this.context.link.baseURI, href.animVal);
300
}
301
302
return href;
303
}
304
305
href =
306
this.context.link.getAttribute("href") ||
307
this.context.link.getAttributeNS("http://www.w3.org/1999/xlink", "href");
308
309
if (!href || !href.match(/\S/)) {
310
// Without this we try to save as the current doc,
311
// for example, HTML case also throws if empty
312
throw new Error("Empty href");
313
}
314
315
return this._makeURLAbsolute(this.context.link.baseURI, href);
316
}
317
318
_getLinkURI() {
319
try {
320
return Services.io.newURI(this.context.linkURL);
321
} catch (ex) {
322
// e.g. empty URL string
323
}
324
325
return null;
326
}
327
328
// Get text of link.
329
_getLinkText() {
330
let text = this._gatherTextUnder(this.context.link);
331
332
if (!text || !text.match(/\S/)) {
333
text = this.context.link.getAttribute("title");
334
if (!text || !text.match(/\S/)) {
335
text = this.context.link.getAttribute("alt");
336
if (!text || !text.match(/\S/)) {
337
text = this.context.linkURL;
338
}
339
}
340
}
341
342
return text;
343
}
344
345
_getLinkProtocol() {
346
if (this.context.linkURI) {
347
return this.context.linkURI.scheme; // can be |undefined|
348
}
349
350
return null;
351
}
352
353
// Returns true if clicked-on link targets a resource that can be saved.
354
_isLinkSaveable(aLink) {
355
// We don't do the Right Thing for news/snews yet, so turn them off
356
// until we do.
357
return (
358
this.context.linkProtocol &&
359
!(
360
this.context.linkProtocol == "mailto" ||
361
this.context.linkProtocol == "javascript" ||
362
this.context.linkProtocol == "news" ||
363
this.context.linkProtocol == "snews"
364
)
365
);
366
}
367
368
// Gather all descendent text under given document node.
369
_gatherTextUnder(root) {
370
let text = "";
371
let node = root.firstChild;
372
let depth = 1;
373
while (node && depth > 0) {
374
// See if this node is text.
375
if (node.nodeType == node.TEXT_NODE) {
376
// Add this text to our collection.
377
text += " " + node.data;
378
} else if (node instanceof this.contentWindow.HTMLImageElement) {
379
// If it has an "alt" attribute, add that.
380
let altText = node.getAttribute("alt");
381
if (altText && altText != "") {
382
text += " " + altText;
383
}
384
}
385
// Find next node to test.
386
// First, see if this node has children.
387
if (node.hasChildNodes()) {
388
// Go to first child.
389
node = node.firstChild;
390
depth++;
391
} else {
392
// No children, try next sibling (or parent next sibling).
393
while (depth > 0 && !node.nextSibling) {
394
node = node.parentNode;
395
depth--;
396
}
397
if (node.nextSibling) {
398
node = node.nextSibling;
399
}
400
}
401
}
402
403
// Strip leading and tailing whitespace.
404
text = text.trim();
405
// Compress remaining whitespace.
406
text = text.replace(/\s+/g, " ");
407
return text;
408
}
409
410
// Returns a "url"-type computed style attribute value, with the url() stripped.
411
_getComputedURL(aElem, aProp) {
412
let urls = aElem.ownerGlobal.getComputedStyle(aElem).getCSSImageURLs(aProp);
413
414
if (!urls.length) {
415
return null;
416
}
417
418
if (urls.length != 1) {
419
throw new Error("found multiple URLs");
420
}
421
422
return urls[0];
423
}
424
425
_makeURLAbsolute(aBase, aUrl) {
426
return Services.io.newURI(aUrl, null, Services.io.newURI(aBase)).spec;
427
}
428
429
_isProprietaryDRM() {
430
return (
431
this.context.target.isEncrypted &&
432
this.context.target.mediaKeys &&
433
this.context.target.mediaKeys.keySystem != "org.w3.clearkey"
434
);
435
}
436
437
_isMediaURLReusable(aURL) {
438
if (aURL.startsWith("blob:")) {
439
return URL.isValidURL(aURL);
440
}
441
442
return true;
443
}
444
445
_isTargetATextBox(node) {
446
if (node instanceof this.contentWindow.HTMLInputElement) {
447
return node.mozIsTextField(false);
448
}
449
450
return node instanceof this.contentWindow.HTMLTextAreaElement;
451
}
452
453
_isSpellCheckEnabled(aNode) {
454
// We can always force-enable spellchecking on textboxes
455
if (this._isTargetATextBox(aNode)) {
456
return true;
457
}
458
459
// We can never spell check something which is not content editable
460
let editable = aNode.isContentEditable;
461
462
if (!editable && aNode.ownerDocument) {
463
editable = aNode.ownerDocument.designMode == "on";
464
}
465
466
if (!editable) {
467
return false;
468
}
469
470
// Otherwise make sure that nothing in the parent chain disables spellchecking
471
return aNode.spellcheck;
472
}
473
474
_disableSetDesktopBackground(aTarget) {
475
// Disable the Set as Desktop Background menu item if we're still trying
476
// to load the image or the load failed.
477
if (!(aTarget instanceof Ci.nsIImageLoadingContent)) {
478
return true;
479
}
480
481
if ("complete" in aTarget && !aTarget.complete) {
482
return true;
483
}
484
485
if (aTarget.currentURI.schemeIs("javascript")) {
486
return true;
487
}
488
489
let request = aTarget.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
490
491
if (!request) {
492
return true;
493
}
494
495
return false;
496
}
497
498
handleEvent(aEvent) {
499
contextMenus.set(this.browsingContext, this);
500
501
let defaultPrevented = aEvent.defaultPrevented;
502
503
if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) {
504
let plugin = null;
505
506
try {
507
plugin = aEvent.composedTarget.QueryInterface(
508
Ci.nsIObjectLoadingContent
509
);
510
} catch (e) {}
511
512
if (
513
plugin &&
514
plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN
515
) {
516
// Don't open a context menu for plugins.
517
return;
518
}
519
520
defaultPrevented = false;
521
}
522
523
if (defaultPrevented) {
524
return;
525
}
526
527
let doc = aEvent.composedTarget.ownerDocument;
528
let {
529
mozDocumentURIIfNotForErrorPages: docLocation,
530
characterSet: charSet,
531
baseURI,
532
} = doc;
533
docLocation = docLocation && docLocation.spec;
534
let frameOuterWindowID = WebNavigationFrames.getFrameId(doc.defaultView);
535
let loginFillInfo = LoginManagerContent.getFieldContext(
536
aEvent.composedTarget
537
);
538
539
// The same-origin check will be done in nsContextMenu.openLinkInTab.
540
let parentAllowsMixedContent = !!this.docShell.mixedContentChannel;
541
542
let disableSetDesktopBackground = null;
543
544
// Media related cache info parent needs for saving
545
let contentType = null;
546
let contentDisposition = null;
547
if (
548
aEvent.composedTarget.nodeType == aEvent.composedTarget.ELEMENT_NODE &&
549
aEvent.composedTarget instanceof Ci.nsIImageLoadingContent &&
550
aEvent.composedTarget.currentURI
551
) {
552
disableSetDesktopBackground = this._disableSetDesktopBackground(
553
aEvent.composedTarget
554
);
555
556
try {
557
let imageCache = Cc["@mozilla.org/image/tools;1"]
558
.getService(Ci.imgITools)
559
.getImgCacheForDocument(doc);
560
// The image cache's notion of where this image is located is
561
// the currentURI of the image loading content.
562
let props = imageCache.findEntryProperties(
563
aEvent.composedTarget.currentURI,
564
doc
565
);
566
567
try {
568
contentType = props.get("type", Ci.nsISupportsCString).data;
569
} catch (e) {}
570
571
try {
572
contentDisposition = props.get(
573
"content-disposition",
574
Ci.nsISupportsCString
575
).data;
576
} catch (e) {}
577
} catch (e) {}
578
}
579
580
let selectionInfo = BrowserUtils.getSelectionDetails(this.contentWindow);
581
let loadContext = this.docShell.QueryInterface(Ci.nsILoadContext);
582
let userContextId = loadContext.originAttributes.userContextId;
583
let popupNodeSelectors = findAllCssSelectors(aEvent.composedTarget);
584
585
this._setContext(aEvent);
586
let context = this.context;
587
this.target = context.target;
588
589
let spellInfo = null;
590
let editFlags = null;
591
let principal = null;
592
let customMenuItems = null;
593
594
let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
595
Ci.nsIReferrerInfo
596
);
597
referrerInfo.initWithNode(
598
context.onLink ? context.link : aEvent.composedTarget
599
);
600
referrerInfo = E10SUtils.serializeReferrerInfo(referrerInfo);
601
602
let target = context.target;
603
if (target) {
604
this._cleanContext();
605
}
606
607
editFlags = SpellCheckHelper.isEditable(
608
aEvent.composedTarget,
609
this.contentWindow
610
);
611
612
if (editFlags & SpellCheckHelper.SPELLCHECKABLE) {
613
spellInfo = InlineSpellCheckerContent.initContextMenu(
614
aEvent,
615
editFlags,
616
this
617
);
618
}
619
620
// Set the event target first as the copy image command needs it to
621
// determine what was context-clicked on. Then, update the state of the
622
// commands on the context menu.
623
this.docShell.contentViewer
624
.QueryInterface(Ci.nsIContentViewerEdit)
625
.setCommandNode(aEvent.composedTarget);
626
aEvent.composedTarget.ownerGlobal.updateCommands("contentcontextmenu");
627
628
let data = {
629
context,
630
charSet,
631
baseURI,
632
referrerInfo,
633
editFlags,
634
principal,
635
spellInfo,
636
contentType,
637
docLocation,
638
loginFillInfo,
639
selectionInfo,
640
userContextId,
641
customMenuItems,
642
contentDisposition,
643
frameOuterWindowID,
644
popupNodeSelectors,
645
disableSetDesktopBackground,
646
parentAllowsMixedContent,
647
};
648
649
if (context.inFrame && !context.inSrcdocFrame) {
650
data.frameReferrerInfo = E10SUtils.serializeReferrerInfo(
651
doc.referrerInfo
652
);
653
}
654
655
// In the case "onLink" we may have to send target referrerInfo. This object
656
// may be used to in saveMedia function.
657
if (context.onLink) {
658
let targetReferrerInfo = Cc[
659
"@mozilla.org/referrer-info;1"
660
].createInstance(Ci.nsIReferrerInfo);
661
662
targetReferrerInfo.initWithNode(aEvent.composedTarget);
663
data.targetReferrerInfo = E10SUtils.serializeReferrerInfo(
664
targetReferrerInfo
665
);
666
}
667
668
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
669
data.customMenuItems = PageMenuChild.build(aEvent.composedTarget);
670
}
671
672
Services.obs.notifyObservers(
673
{ wrappedJSObject: data },
674
"on-prepare-contextmenu"
675
);
676
677
// For now, JS Window Actors don't serialize Principals automatically, so we
678
// have to do it ourselves. See bug 1557852.
679
data.principal = E10SUtils.serializePrincipal(doc.nodePrincipal);
680
data.context.principal = E10SUtils.serializePrincipal(context.principal);
681
data.storagePrincipal = E10SUtils.serializePrincipal(
682
doc.effectiveStoragePrincipal
683
);
684
data.context.storagePrincipal = E10SUtils.serializePrincipal(
685
context.storagePrincipal
686
);
687
688
// In the event that the content is running in the parent process, we don't
689
// actually want the contextmenu events to reach the parent - we'll dispatch
690
// a new contextmenu event after the async message has reached the parent
691
// instead.
692
aEvent.stopPropagation();
693
694
this.sendAsyncMessage("contextmenu", data);
695
}
696
697
/**
698
* Some things are not serializable, so we either have to only send
699
* their needed data or regenerate them in nsContextMenu.js
700
* - target and target.ownerDocument
701
* - link
702
* - linkURI
703
*/
704
_cleanContext(aEvent) {
705
const context = this.context;
706
const cleanTarget = Object.create(null);
707
708
cleanTarget.ownerDocument = {
709
// used for nsContextMenu.initLeaveDOMFullScreenItems and
710
// nsContextMenu.initMediaPlayerItems
711
fullscreen: context.target.ownerDocument.fullscreen,
712
713
// used for nsContextMenu.initMiscItems
714
contentType: context.target.ownerDocument.contentType,
715
716
// used for nsContextMenu.saveLink
717
isPrivate: PrivateBrowsingUtils.isContentWindowPrivate(
718
context.target.ownerGlobal
719
),
720
};
721
722
// used for nsContextMenu.initMediaPlayerItems
723
Object.assign(cleanTarget, {
724
ended: context.target.ended,
725
muted: context.target.muted,
726
paused: context.target.paused,
727
controls: context.target.controls,
728
duration: context.target.duration,
729
});
730
731
const onMedia = context.onVideo || context.onAudio;
732
733
if (onMedia) {
734
Object.assign(cleanTarget, {
735
loop: context.target.loop,
736
error: context.target.error,
737
networkState: context.target.networkState,
738
playbackRate: context.target.playbackRate,
739
NETWORK_NO_SOURCE: context.target.NETWORK_NO_SOURCE,
740
});
741
742
if (context.onVideo) {
743
Object.assign(cleanTarget, {
744
readyState: context.target.readyState,
745
HAVE_CURRENT_DATA: context.target.HAVE_CURRENT_DATA,
746
});
747
}
748
}
749
750
context.target = cleanTarget;
751
752
if (context.link) {
753
context.link = { href: context.linkURL };
754
}
755
756
delete context.linkURI;
757
}
758
759
_setContext(aEvent) {
760
this.context = Object.create(null);
761
const context = this.context;
762
763
context.timeStamp = aEvent.timeStamp;
764
context.screenX = aEvent.screenX;
765
context.screenY = aEvent.screenY;
766
context.mozInputSource = aEvent.mozInputSource;
767
768
let node = aEvent.composedTarget;
769
770
// Set the node to containing <video>/<audio>/<embed>/<object> if the node
771
// is in the videocontrols/pluginProblem UA Widget.
772
if (this.contentWindow.ShadowRoot) {
773
let n = node;
774
while (n) {
775
if (n instanceof this.contentWindow.ShadowRoot) {
776
if (
777
n.host instanceof this.contentWindow.HTMLMediaElement ||
778
n.host instanceof this.contentWindow.HTMLEmbedElement ||
779
n.host instanceof this.contentWindow.HTMLObjectElement
780
) {
781
node = n.host;
782
break;
783
}
784
break;
785
}
786
n = n.parentNode;
787
}
788
}
789
790
const XUL_NS =
792
793
context.shouldDisplay = true;
794
795
if (
796
node.nodeType == node.DOCUMENT_NODE ||
797
// Don't display for XUL element unless <label class="text-link">
798
(node.namespaceURI == XUL_NS && !this._isXULTextLinkLabel(node))
799
) {
800
context.shouldDisplay = false;
801
return;
802
}
803
804
const isAboutDevtoolsToolbox = this.document.documentURI.startsWith(
805
"about:devtools-toolbox"
806
);
807
const editFlags = SpellCheckHelper.isEditable(node, this.contentWindow);
808
809
if (
810
isAboutDevtoolsToolbox &&
811
(editFlags & SpellCheckHelper.TEXTINPUT) === 0
812
) {
813
// Don't display for about:devtools-toolbox page unless the source was text input.
814
context.shouldDisplay = false;
815
return;
816
}
817
818
// Initialize context to be sent to nsContextMenu
819
// Keep this consistent with the similar code in nsContextMenu's setContext
820
context.bgImageURL = "";
821
context.imageDescURL = "";
822
context.imageInfo = null;
823
context.mediaURL = "";
824
context.webExtBrowserType = "";
825
826
context.canSpellCheck = false;
827
context.hasBGImage = false;
828
context.hasMultipleBGImages = false;
829
context.isDesignMode = false;
830
context.inFrame = false;
831
context.inSrcdocFrame = false;
832
context.inSyntheticDoc = false;
833
context.inTabBrowser = true;
834
context.inWebExtBrowser = false;
835
836
context.link = null;
837
context.linkDownload = "";
838
context.linkProtocol = "";
839
context.linkTextStr = "";
840
context.linkURL = "";
841
context.linkURI = null;
842
843
context.onAudio = false;
844
context.onCanvas = false;
845
context.onCompletedImage = false;
846
context.onCTPPlugin = false;
847
context.onDRMMedia = false;
848
context.onPiPVideo = false;
849
context.onEditable = false;
850
context.onImage = false;
851
context.onKeywordField = false;
852
context.onLink = false;
853
context.onLoadedImage = false;
854
context.onMailtoLink = false;
855
context.onMozExtLink = false;
856
context.onNumeric = false;
857
context.onPassword = false;
858
context.onSaveableLink = false;
859
context.onSpellcheckable = false;
860
context.onTextInput = false;
861
context.onVideo = false;
862
863
// Remember the node and its owner document that was clicked
864
// This may be modifed before sending to nsContextMenu
865
context.target = node;
866
context.targetIdentifier = ContentDOMReference.get(node);
867
868
context.principal = context.target.ownerDocument.nodePrincipal;
869
context.storagePrincipal =
870
context.target.ownerDocument.effectiveStoragePrincipal;
871
context.csp = E10SUtils.serializeCSP(context.target.ownerDocument.csp);
872
873
context.frameOuterWindowID = WebNavigationFrames.getFrameId(
874
context.target.ownerGlobal
875
);
876
877
// Check if we are in a synthetic document (stand alone image, video, etc.).
878
context.inSyntheticDoc = context.target.ownerDocument.mozSyntheticDocument;
879
880
context.shouldInitInlineSpellCheckerUINoChildren = false;
881
context.shouldInitInlineSpellCheckerUIWithChildren = false;
882
883
this._setContextForNodesNoChildren(editFlags);
884
this._setContextForNodesWithChildren(editFlags);
885
886
this.lastMenuTarget = {
887
// Remember the node for extensions.
888
targetRef: Cu.getWeakReference(node),
889
// The timestamp is used to verify that the target wasn't changed since the observed menu event.
890
timeStamp: context.timeStamp,
891
};
892
893
if (isAboutDevtoolsToolbox) {
894
// Setup the menu items on text input in about:devtools-toolbox.
895
context.inAboutDevtoolsToolbox = true;
896
context.canSpellCheck = false;
897
context.inTabBrowser = false;
898
context.inFrame = false;
899
context.inSrcdocFrame = false;
900
context.onSpellcheckable = false;
901
}
902
}
903
904
/**
905
* Sets up the parts of the context menu for when when nodes have no children.
906
*
907
* @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
908
* for the details.
909
*/
910
_setContextForNodesNoChildren(editFlags) {
911
const context = this.context;
912
913
if (context.target.nodeType == context.target.TEXT_NODE) {
914
// For text nodes, look at the parent node to determine the spellcheck attribute.
915
context.canSpellCheck =
916
context.target.parentNode && this._isSpellCheckEnabled(context.target);
917
return;
918
}
919
920
// We only deal with TEXT_NODE and ELEMENT_NODE in this function, so return
921
// early if we don't have one.
922
if (context.target.nodeType != context.target.ELEMENT_NODE) {
923
return;
924
}
925
926
// See if the user clicked on an image. This check mirrors
927
// nsDocumentViewer::GetInImage. Make sure to update both if this is
928
// changed.
929
if (
930
context.target instanceof Ci.nsIImageLoadingContent &&
931
(context.target.currentRequestFinalURI || context.target.currentURI)
932
) {
933
context.onImage = true;
934
935
context.imageInfo = {
936
currentSrc: context.target.currentSrc,
937
width: context.target.width,
938
height: context.target.height,
939
imageText: context.target.title || context.target.alt,
940
};
941
942
const request = context.target.getRequest(
943
Ci.nsIImageLoadingContent.CURRENT_REQUEST
944
);
945
946
if (request && request.imageStatus & request.STATUS_SIZE_AVAILABLE) {
947
context.onLoadedImage = true;
948
}
949
950
if (
951
request &&
952
request.imageStatus & request.STATUS_LOAD_COMPLETE &&
953
!(request.imageStatus & request.STATUS_ERROR)
954
) {
955
context.onCompletedImage = true;
956
}
957
958
// The actual URL the image was loaded from (after redirects) is the
959
// currentRequestFinalURI. We should use that as the URL for purposes of
960
// deciding on the filename, if it is present. It might not be present
961
// if images are blocked.
962
context.mediaURL = (
963
context.target.currentRequestFinalURI || context.target.currentURI
964
).spec;
965
966
const descURL = context.target.getAttribute("longdesc");
967
968
if (descURL) {
969
context.imageDescURL = this._makeURLAbsolute(
970
context.target.ownerDocument.body.baseURI,
971
descURL
972
);
973
}
974
} else if (context.target instanceof this.contentWindow.HTMLCanvasElement) {
975
context.onCanvas = true;
976
} else if (context.target instanceof this.contentWindow.HTMLVideoElement) {
977
const mediaURL = context.target.currentSrc || context.target.src;
978
979
if (this._isMediaURLReusable(mediaURL)) {
980
context.mediaURL = mediaURL;
981
}
982
983
if (this._isProprietaryDRM()) {
984
context.onDRMMedia = true;
985
}
986
987
if (context.target.isCloningElementVisually) {
988
context.onPiPVideo = true;
989
}
990
991
// Firefox always creates a HTMLVideoElement when loading an ogg file
992
// directly. If the media is actually audio, be smarter and provide a
993
// context menu with audio operations.
994
if (
995
context.target.readyState >= context.target.HAVE_METADATA &&
996
(context.target.videoWidth == 0 || context.target.videoHeight == 0)
997
) {
998
context.onAudio = true;
999
} else {
1000
context.onVideo = true;
1001
}
1002
} else if (context.target instanceof this.contentWindow.HTMLAudioElement) {
1003
context.onAudio = true;
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
} else if (
1014
editFlags &
1015
(SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)
1016
) {
1017
context.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
1018
context.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
1019
context.onEditable = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
1020
context.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
1021
context.onSpellcheckable =
1022
(editFlags & SpellCheckHelper.SPELLCHECKABLE) !== 0;
1023
1024
// This is guaranteed to be an input or textarea because of the condition above,
1025
// so the no-children flag is always correct. We deal with contenteditable elsewhere.
1026
if (context.onSpellcheckable) {
1027
context.shouldInitInlineSpellCheckerUINoChildren = true;
1028
}
1029
1030
context.onKeywordField = editFlags & SpellCheckHelper.KEYWORD;
1031
} else if (context.target instanceof this.contentWindow.HTMLHtmlElement) {
1032
const bodyElt = context.target.ownerDocument.body;
1033
1034
if (bodyElt) {
1035
let computedURL;
1036
1037
try {
1038
computedURL = this._getComputedURL(bodyElt, "background-image");
1039
context.hasMultipleBGImages = false;
1040
} catch (e) {
1041
context.hasMultipleBGImages = true;
1042
}
1043
1044
if (computedURL) {
1045
context.hasBGImage = true;
1046
context.bgImageURL = this._makeURLAbsolute(
1047
bodyElt.baseURI,
1048
computedURL
1049
);
1050
}
1051
}
1052
} else if (
1053
(context.target instanceof this.contentWindow.HTMLEmbedElement ||
1054
context.target instanceof this.contentWindow.HTMLObjectElement) &&
1055
context.target.displayedType ==
1056
this.contentWindow.HTMLObjectElement.TYPE_NULL &&
1057
context.target.pluginFallbackType ==
1058
this.contentWindow.HTMLObjectElement.PLUGIN_CLICK_TO_PLAY
1059
) {
1060
context.onCTPPlugin = true;
1061
}
1062
1063
context.canSpellCheck = this._isSpellCheckEnabled(context.target);
1064
}
1065
1066
/**
1067
* Sets up the parts of the context menu for when when nodes have children.
1068
*
1069
* @param {Integer} editFlags The edit flags for the node. See SpellCheckHelper
1070
* for the details.
1071
*/
1072
_setContextForNodesWithChildren(editFlags) {
1073
const context = this.context;
1074
1075
// Second, bubble out, looking for items of interest that can have childen.
1076
// Always pick the innermost link, background image, etc.
1077
let elem = context.target;
1078
1079
while (elem) {
1080
if (elem.nodeType == elem.ELEMENT_NODE) {
1081
// Link?
1082
const XLINK_NS = "http://www.w3.org/1999/xlink";
1083
1084
if (
1085
!context.onLink &&
1086
// Be consistent with what hrefAndLinkNodeForClickEvent
1087
// does in browser.js
1088
(this._isXULTextLinkLabel(elem) ||
1089
(elem instanceof this.contentWindow.HTMLAnchorElement &&
1090
elem.href) ||
1091
(elem instanceof this.contentWindow.SVGAElement &&
1092
(elem.href || elem.hasAttributeNS(XLINK_NS, "href"))) ||
1093
(elem instanceof this.contentWindow.HTMLAreaElement && elem.href) ||
1094
elem instanceof this.contentWindow.HTMLLinkElement ||
1095
elem.getAttributeNS(XLINK_NS, "type") == "simple")
1096
) {
1097
// Target is a link or a descendant of a link.
1098
context.onLink = true;
1099
1100
// Remember corresponding element.
1101
context.link = elem;
1102
context.linkURL = this._getLinkURL();
1103
context.linkURI = this._getLinkURI();
1104
context.linkTextStr = this._getLinkText();
1105
context.linkProtocol = this._getLinkProtocol();
1106
context.onMailtoLink = context.linkProtocol == "mailto";
1107
context.onMozExtLink = context.linkProtocol == "moz-extension";
1108
context.onSaveableLink = this._isLinkSaveable(context.link);
1109
1110
try {
1111
if (elem.download) {
1112
// Ignore download attribute on cross-origin links
1113
context.principal.checkMayLoad(context.linkURI, false, true);
1114
context.linkDownload = elem.download;
1115
}
1116
} catch (ex) {}
1117
}
1118
1119
// Background image? Don't bother if we've already found a
1120
// background image further down the hierarchy. Otherwise,
1121
// we look for the computed background-image style.
1122
if (!context.hasBGImage && !context.hasMultipleBGImages) {
1123
let bgImgUrl = null;
1124
1125
try {
1126
bgImgUrl = this._getComputedURL(elem, "background-image");
1127
context.hasMultipleBGImages = false;
1128
} catch (e) {
1129
context.hasMultipleBGImages = true;
1130
}
1131
1132
if (bgImgUrl) {
1133
context.hasBGImage = true;
1134
context.bgImageURL = this._makeURLAbsolute(elem.baseURI, bgImgUrl);
1135
}
1136
}
1137
}
1138
1139
elem = elem.parentNode;
1140
}
1141
1142
// See if the user clicked in a frame.
1143
const docDefaultView = context.target.ownerGlobal;
1144
1145
if (docDefaultView != docDefaultView.top) {
1146
context.inFrame = true;
1147
1148
if (context.target.ownerDocument.isSrcdocDocument) {
1149
context.inSrcdocFrame = true;
1150
}
1151
}
1152
1153
// if the document is editable, show context menu like in text inputs
1154
if (!context.onEditable) {
1155
if (editFlags & SpellCheckHelper.CONTENTEDITABLE) {
1156
// If this.onEditable is false but editFlags is CONTENTEDITABLE, then
1157
// the document itself must be editable.
1158
context.onTextInput = true;
1159
context.onKeywordField = false;
1160
context.onImage = false;
1161
context.onLoadedImage = false;
1162
context.onCompletedImage = false;
1163
context.inFrame = false;
1164
context.inSrcdocFrame = false;
1165
context.hasBGImage = false;
1166
context.isDesignMode = true;
1167
context.onEditable = true;
1168
context.onSpellcheckable = true;
1169
context.shouldInitInlineSpellCheckerUIWithChildren = true;
1170
}
1171
}
1172
}
1173
}