Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
7 var gContextMenuContentData = null;
9 function nsContextMenu(aXulMenu, aIsShift) {
10 this.shouldDisplay = true;
11 this.initMenu(aXulMenu, aIsShift);
12 }
14 // Prototype for nsContextMenu "class."
15 nsContextMenu.prototype = {
16 initMenu: function CM_initMenu(aXulMenu, aIsShift) {
17 // Get contextual info.
18 this.setTarget(document.popupNode, document.popupRangeParent,
19 document.popupRangeOffset);
20 if (!this.shouldDisplay)
21 return;
23 this.hasPageMenu = false;
24 if (!aIsShift) {
25 this.hasPageMenu = PageMenu.maybeBuildAndAttachMenu(this.target,
26 aXulMenu);
27 }
29 this.isFrameImage = document.getElementById("isFrameImage");
30 this.ellipsis = "\u2026";
31 try {
32 this.ellipsis = gPrefService.getComplexValue("intl.ellipsis",
33 Ci.nsIPrefLocalizedString).data;
34 } catch (e) { }
36 this.isContentSelected = this.isContentSelection();
37 this.onPlainTextLink = false;
39 // Initialize (disable/remove) menu items.
40 this.initItems();
41 },
43 hiding: function CM_hiding() {
44 gContextMenuContentData = null;
45 InlineSpellCheckerUI.clearSuggestionsFromMenu();
46 InlineSpellCheckerUI.clearDictionaryListFromMenu();
47 InlineSpellCheckerUI.uninit();
48 },
50 initItems: function CM_initItems() {
51 this.initPageMenuSeparator();
52 this.initOpenItems();
53 this.initNavigationItems();
54 this.initViewItems();
55 this.initMiscItems();
56 this.initSpellingItems();
57 this.initSaveItems();
58 this.initClipboardItems();
59 this.initMediaPlayerItems();
60 this.initLeaveDOMFullScreenItems();
61 this.initClickToPlayItems();
62 },
64 initPageMenuSeparator: function CM_initPageMenuSeparator() {
65 this.showItem("page-menu-separator", this.hasPageMenu);
66 },
68 initOpenItems: function CM_initOpenItems() {
69 var isMailtoInternal = false;
70 if (this.onMailtoLink) {
71 var mailtoHandler = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
72 getService(Ci.nsIExternalProtocolService).
73 getProtocolHandlerInfo("mailto");
74 isMailtoInternal = (!mailtoHandler.alwaysAskBeforeHandling &&
75 mailtoHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp &&
76 (mailtoHandler.preferredApplicationHandler instanceof Ci.nsIWebHandlerApp));
77 }
79 // Time to do some bad things and see if we've highlighted a URL that
80 // isn't actually linked.
81 if (this.isTextSelected && !this.onLink) {
82 // Ok, we have some text, let's figure out if it looks like a URL.
83 let selection = this.focusedWindow.getSelection();
84 let linkText = selection.toString().trim();
85 let uri;
86 if (/^(?:https?|ftp):/i.test(linkText)) {
87 try {
88 uri = makeURI(linkText);
89 } catch (ex) {}
90 }
91 // Check if this could be a valid url, just missing the protocol.
92 else if (/^(?:[a-z\d-]+\.)+[a-z]+$/i.test(linkText)) {
93 // Now let's see if this is an intentional link selection. Our guess is
94 // based on whether the selection begins/ends with whitespace or is
95 // preceded/followed by a non-word character.
97 // selection.toString() trims trailing whitespace, so we look for
98 // that explicitly in the first and last ranges.
99 let beginRange = selection.getRangeAt(0);
100 let delimitedAtStart = /^\s/.test(beginRange);
101 if (!delimitedAtStart) {
102 let container = beginRange.startContainer;
103 let offset = beginRange.startOffset;
104 if (container.nodeType == Node.TEXT_NODE && offset > 0)
105 delimitedAtStart = /\W/.test(container.textContent[offset - 1]);
106 else
107 delimitedAtStart = true;
108 }
110 let delimitedAtEnd = false;
111 if (delimitedAtStart) {
112 let endRange = selection.getRangeAt(selection.rangeCount - 1);
113 delimitedAtEnd = /\s$/.test(endRange);
114 if (!delimitedAtEnd) {
115 let container = endRange.endContainer;
116 let offset = endRange.endOffset;
117 if (container.nodeType == Node.TEXT_NODE &&
118 offset < container.textContent.length)
119 delimitedAtEnd = /\W/.test(container.textContent[offset]);
120 else
121 delimitedAtEnd = true;
122 }
123 }
125 if (delimitedAtStart && delimitedAtEnd) {
126 let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"]
127 .getService(Ci.nsIURIFixup);
128 try {
129 uri = uriFixup.createFixupURI(linkText, uriFixup.FIXUP_FLAG_NONE);
130 } catch (ex) {}
131 }
132 }
134 if (uri && uri.host) {
135 this.linkURI = uri;
136 this.linkURL = this.linkURI.spec;
137 this.onPlainTextLink = true;
138 }
139 }
141 var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink;
142 var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
143 this.showItem("context-openlink", shouldShow && !isWindowPrivate);
144 this.showItem("context-openlinkprivate", shouldShow);
145 this.showItem("context-openlinkintab", shouldShow);
146 this.showItem("context-openlinkincurrent", this.onPlainTextLink);
147 this.showItem("context-sep-open", shouldShow);
148 },
150 initNavigationItems: function CM_initNavigationItems() {
151 var shouldShow = !(this.isContentSelected || this.onLink || this.onImage ||
152 this.onCanvas || this.onVideo || this.onAudio ||
153 this.onTextInput || this.onSocial);
154 this.showItem("context-back", shouldShow);
155 this.showItem("context-forward", shouldShow);
157 let stopped = XULBrowserWindow.stopCommand.getAttribute("disabled") == "true";
159 let stopReloadItem = "";
160 if (shouldShow || this.onSocial) {
161 stopReloadItem = (stopped || this.onSocial) ? "reload" : "stop";
162 }
164 this.showItem("context-reload", stopReloadItem == "reload");
165 this.showItem("context-stop", stopReloadItem == "stop");
166 this.showItem("context-sep-stop", !!stopReloadItem);
168 // XXX: Stop is determined in browser.js; the canStop broadcaster is broken
169 //this.setItemAttrFromNode( "context-stop", "disabled", "canStop" );
170 },
172 initLeaveDOMFullScreenItems: function CM_initLeaveFullScreenItem() {
173 // only show the option if the user is in DOM fullscreen
174 var shouldShow = (this.target.ownerDocument.mozFullScreenElement != null);
175 this.showItem("context-leave-dom-fullscreen", shouldShow);
177 // Explicitly show if in DOM fullscreen, but do not hide it has already been shown
178 if (shouldShow)
179 this.showItem("context-media-sep-commands", true);
180 },
182 initSaveItems: function CM_initSaveItems() {
183 var shouldShow = !(this.onTextInput || this.onLink ||
184 this.isContentSelected || this.onImage ||
185 this.onCanvas || this.onVideo || this.onAudio);
186 this.showItem("context-savepage", shouldShow);
188 // Save link depends on whether we're in a link, or selected text matches valid URL pattern.
189 this.showItem("context-savelink", this.onSaveableLink || this.onPlainTextLink);
191 // Save image depends on having loaded its content, video and audio don't.
192 this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas);
193 this.showItem("context-savevideo", this.onVideo);
194 this.showItem("context-saveaudio", this.onAudio);
195 this.showItem("context-video-saveimage", this.onVideo);
196 this.setItemAttr("context-savevideo", "disabled", !this.mediaURL);
197 this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL);
198 // Send media URL (but not for canvas, since it's a big data: URL)
199 this.showItem("context-sendimage", this.onImage);
200 this.showItem("context-sendvideo", this.onVideo);
201 this.showItem("context-sendaudio", this.onAudio);
202 this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL);
203 this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL);
204 },
206 initViewItems: function CM_initViewItems() {
207 // View source is always OK, unless in directory listing.
208 this.showItem("context-viewpartialsource-selection",
209 this.isContentSelected);
210 this.showItem("context-viewpartialsource-mathml",
211 this.onMathML && !this.isContentSelected);
213 var shouldShow = !(this.isContentSelected ||
214 this.onImage || this.onCanvas ||
215 this.onVideo || this.onAudio ||
216 this.onLink || this.onTextInput);
217 var showInspect = !this.onSocial && gPrefService.getBoolPref("devtools.inspector.enabled");
218 this.showItem("context-viewsource", shouldShow);
219 this.showItem("context-viewinfo", shouldShow);
220 this.showItem("inspect-separator", showInspect);
221 this.showItem("context-inspect", showInspect);
223 this.showItem("context-sep-viewsource", shouldShow);
225 // Set as Desktop background depends on whether an image was clicked on,
226 // and only works if we have a shell service.
227 var haveSetDesktopBackground = false;
228 #ifdef HAVE_SHELL_SERVICE
229 // Only enable Set as Desktop Background if we can get the shell service.
230 var shell = getShellService();
231 if (shell)
232 haveSetDesktopBackground = shell.canSetDesktopBackground;
233 #endif
234 this.showItem("context-setDesktopBackground",
235 haveSetDesktopBackground && this.onLoadedImage);
237 if (haveSetDesktopBackground && this.onLoadedImage) {
238 document.getElementById("context-setDesktopBackground")
239 .disabled = this.disableSetDesktopBackground();
240 }
242 // Reload image depends on an image that's not fully loaded
243 this.showItem("context-reloadimage", (this.onImage && !this.onCompletedImage));
245 // View image depends on having an image that's not standalone
246 // (or is in a frame), or a canvas.
247 this.showItem("context-viewimage", (this.onImage &&
248 (!this.inSyntheticDoc || this.inFrame)) || this.onCanvas);
250 // View video depends on not having a standalone video.
251 this.showItem("context-viewvideo", this.onVideo && (!this.inSyntheticDoc || this.inFrame));
252 this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL);
254 // View background image depends on whether there is one, but don't make
255 // background images of a stand-alone media document available.
256 this.showItem("context-viewbgimage", shouldShow &&
257 !this._hasMultipleBGImages &&
258 !this.inSyntheticDoc);
259 this.showItem("context-sep-viewbgimage", shouldShow &&
260 !this._hasMultipleBGImages &&
261 !this.inSyntheticDoc);
262 document.getElementById("context-viewbgimage")
263 .disabled = !this.hasBGImage;
265 this.showItem("context-viewimageinfo", this.onImage);
266 this.showItem("context-viewimagedesc", this.onImage && this.imageDescURL !== "");
267 },
269 initMiscItems: function CM_initMiscItems() {
270 // Use "Bookmark This Link" if on a link.
271 this.showItem("context-bookmarkpage",
272 !(this.isContentSelected || this.onTextInput || this.onLink ||
273 this.onImage || this.onVideo || this.onAudio || this.onSocial));
274 this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink &&
275 !this.onSocial) || this.onPlainTextLink);
276 this.showItem("context-keywordfield",
277 this.onTextInput && this.onKeywordField);
278 this.showItem("frame", this.inFrame);
280 let showSearchSelect = (this.isTextSelected || this.onLink) && !this.onImage;
281 this.showItem("context-searchselect", showSearchSelect);
282 if (showSearchSelect) {
283 this.formatSearchContextItem();
284 }
286 // srcdoc cannot be opened separately due to concerns about web
287 // content with about:srcdoc in location bar masquerading as trusted
288 // chrome/addon content.
289 // No need to also test for this.inFrame as this is checked in the parent
290 // submenu.
291 this.showItem("context-showonlythisframe", !this.inSrcdocFrame);
292 this.showItem("context-openframeintab", !this.inSrcdocFrame);
293 this.showItem("context-openframe", !this.inSrcdocFrame);
294 this.showItem("context-bookmarkframe", !this.inSrcdocFrame);
295 this.showItem("open-frame-sep", !this.inSrcdocFrame);
297 this.showItem("frame-sep", this.inFrame && this.isTextSelected);
299 // Hide menu entries for images, show otherwise
300 if (this.inFrame) {
301 if (mimeTypeIsTextBased(this.target.ownerDocument.contentType))
302 this.isFrameImage.removeAttribute('hidden');
303 else
304 this.isFrameImage.setAttribute('hidden', 'true');
305 }
307 // BiDi UI
308 this.showItem("context-sep-bidi", top.gBidiUI);
309 this.showItem("context-bidi-text-direction-toggle",
310 this.onTextInput && top.gBidiUI);
311 this.showItem("context-bidi-page-direction-toggle",
312 !this.onTextInput && top.gBidiUI);
314 // SocialMarks. Marks does not work with text selections, only links. If
315 // there is more than MENU_LIMIT providers, we show a submenu for them,
316 // otherwise we have a menuitem per provider (added in SocialMarks class).
317 let markProviders = SocialMarks.getProviders();
318 let enablePageMarks = markProviders.length > 0 && !(this.onLink || this.onImage
319 || this.onVideo || this.onAudio);
320 this.showItem("context-markpageMenu", enablePageMarks && markProviders.length > SocialMarks.MENU_LIMIT);
321 let enablePageMarkItems = enablePageMarks && markProviders.length <= SocialMarks.MENU_LIMIT;
322 let linkmenus = document.getElementsByClassName("context-markpage");
323 [m.hidden = !enablePageMarkItems for (m of linkmenus)];
325 let enableLinkMarks = markProviders.length > 0 &&
326 ((this.onLink && !this.onMailtoLink) || this.onPlainTextLink);
327 this.showItem("context-marklinkMenu", enableLinkMarks && markProviders.length > SocialMarks.MENU_LIMIT);
328 let enableLinkMarkItems = enableLinkMarks && markProviders.length <= SocialMarks.MENU_LIMIT;
329 linkmenus = document.getElementsByClassName("context-marklink");
330 [m.hidden = !enableLinkMarkItems for (m of linkmenus)];
332 // SocialShare
333 let shareButton = SocialShare.shareButton;
334 let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial;
335 let pageShare = shareEnabled && !(this.isContentSelected ||
336 this.onTextInput || this.onLink || this.onImage ||
337 this.onVideo || this.onAudio);
338 this.showItem("context-sharepage", pageShare);
339 this.showItem("context-shareselect", shareEnabled && this.isContentSelected);
340 this.showItem("context-sharelink", shareEnabled && (this.onLink || this.onPlainTextLink) && !this.onMailtoLink);
341 this.showItem("context-shareimage", shareEnabled && this.onImage);
342 this.showItem("context-sharevideo", shareEnabled && this.onVideo);
343 this.setItemAttr("context-sharevideo", "disabled", !this.mediaURL);
344 },
346 initSpellingItems: function() {
347 var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
348 var onMisspelling = InlineSpellCheckerUI.overMisspelling;
349 var showUndo = canSpell && InlineSpellCheckerUI.canUndo();
350 this.showItem("spell-check-enabled", canSpell);
351 this.showItem("spell-separator", canSpell || this.onEditableArea);
352 document.getElementById("spell-check-enabled")
353 .setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled);
355 this.showItem("spell-add-to-dictionary", onMisspelling);
356 this.showItem("spell-undo-add-to-dictionary", showUndo);
358 // suggestion list
359 this.showItem("spell-suggestions-separator", onMisspelling || showUndo);
360 if (onMisspelling) {
361 var suggestionsSeparator =
362 document.getElementById("spell-add-to-dictionary");
363 var numsug =
364 InlineSpellCheckerUI.addSuggestionsToMenu(suggestionsSeparator.parentNode,
365 suggestionsSeparator, 5);
366 this.showItem("spell-no-suggestions", numsug == 0);
367 }
368 else
369 this.showItem("spell-no-suggestions", false);
371 // dictionary list
372 this.showItem("spell-dictionaries", canSpell && InlineSpellCheckerUI.enabled);
373 if (canSpell) {
374 var dictMenu = document.getElementById("spell-dictionaries-menu");
375 var dictSep = document.getElementById("spell-language-separator");
376 InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep);
377 this.showItem("spell-add-dictionaries-main", false);
378 }
379 else if (this.onEditableArea) {
380 // when there is no spellchecker but we might be able to spellcheck
381 // add the add to dictionaries item. This will ensure that people
382 // with no dictionaries will be able to download them
383 this.showItem("spell-add-dictionaries-main", true);
384 }
385 else
386 this.showItem("spell-add-dictionaries-main", false);
387 },
389 initClipboardItems: function() {
390 // Copy depends on whether there is selected text.
391 // Enabling this context menu item is now done through the global
392 // command updating system
393 // this.setItemAttr( "context-copy", "disabled", !this.isTextSelected() );
394 goUpdateGlobalEditMenuItems();
396 this.showItem("context-undo", this.onTextInput);
397 this.showItem("context-sep-undo", this.onTextInput);
398 this.showItem("context-cut", this.onTextInput);
399 this.showItem("context-copy",
400 this.isContentSelected || this.onTextInput);
401 this.showItem("context-paste", this.onTextInput);
402 this.showItem("context-delete", this.onTextInput);
403 this.showItem("context-sep-paste", this.onTextInput);
404 this.showItem("context-selectall", !(this.onLink || this.onImage ||
405 this.onVideo || this.onAudio ||
406 this.inSyntheticDoc) ||
407 this.isDesignMode);
408 this.showItem("context-sep-selectall", this.isContentSelected );
410 // XXX dr
411 // ------
412 // nsDocumentViewer.cpp has code to determine whether we're
413 // on a link or an image. we really ought to be using that...
415 // Copy email link depends on whether we're on an email link.
416 this.showItem("context-copyemail", this.onMailtoLink);
418 // Copy link location depends on whether we're on a non-mailto link.
419 this.showItem("context-copylink", this.onLink && !this.onMailtoLink);
420 this.showItem("context-sep-copylink", this.onLink &&
421 (this.onImage || this.onVideo || this.onAudio));
423 #ifdef CONTEXT_COPY_IMAGE_CONTENTS
424 // Copy image contents depends on whether we're on an image.
425 this.showItem("context-copyimage-contents", this.onImage);
426 #endif
427 // Copy image location depends on whether we're on an image.
428 this.showItem("context-copyimage", this.onImage);
429 this.showItem("context-copyvideourl", this.onVideo);
430 this.showItem("context-copyaudiourl", this.onAudio);
431 this.setItemAttr("context-copyvideourl", "disabled", !this.mediaURL);
432 this.setItemAttr("context-copyaudiourl", "disabled", !this.mediaURL);
433 this.showItem("context-sep-copyimage", this.onImage ||
434 this.onVideo || this.onAudio);
435 },
437 initMediaPlayerItems: function() {
438 var onMedia = (this.onVideo || this.onAudio);
439 // Several mutually exclusive items... play/pause, mute/unmute, show/hide
440 this.showItem("context-media-play", onMedia && (this.target.paused || this.target.ended));
441 this.showItem("context-media-pause", onMedia && !this.target.paused && !this.target.ended);
442 this.showItem("context-media-mute", onMedia && !this.target.muted);
443 this.showItem("context-media-unmute", onMedia && this.target.muted);
444 this.showItem("context-media-playbackrate", onMedia);
445 this.showItem("context-media-showcontrols", onMedia && !this.target.controls);
446 this.showItem("context-media-hidecontrols", onMedia && this.target.controls);
447 this.showItem("context-video-fullscreen", this.onVideo && this.target.ownerDocument.mozFullScreenElement == null);
448 var statsShowing = this.onVideo && this.target.mozMediaStatisticsShowing;
449 this.showItem("context-video-showstats", this.onVideo && this.target.controls && !statsShowing);
450 this.showItem("context-video-hidestats", this.onVideo && this.target.controls && statsShowing);
452 // Disable them when there isn't a valid media source loaded.
453 if (onMedia) {
454 this.setItemAttr("context-media-playbackrate-050x", "checked", this.target.playbackRate == 0.5);
455 this.setItemAttr("context-media-playbackrate-100x", "checked", this.target.playbackRate == 1.0);
456 this.setItemAttr("context-media-playbackrate-150x", "checked", this.target.playbackRate == 1.5);
457 this.setItemAttr("context-media-playbackrate-200x", "checked", this.target.playbackRate == 2.0);
458 var hasError = this.target.error != null ||
459 this.target.networkState == this.target.NETWORK_NO_SOURCE;
460 this.setItemAttr("context-media-play", "disabled", hasError);
461 this.setItemAttr("context-media-pause", "disabled", hasError);
462 this.setItemAttr("context-media-mute", "disabled", hasError);
463 this.setItemAttr("context-media-unmute", "disabled", hasError);
464 this.setItemAttr("context-media-playbackrate", "disabled", hasError);
465 this.setItemAttr("context-media-playbackrate-050x", "disabled", hasError);
466 this.setItemAttr("context-media-playbackrate-100x", "disabled", hasError);
467 this.setItemAttr("context-media-playbackrate-150x", "disabled", hasError);
468 this.setItemAttr("context-media-playbackrate-200x", "disabled", hasError);
469 this.setItemAttr("context-media-showcontrols", "disabled", hasError);
470 this.setItemAttr("context-media-hidecontrols", "disabled", hasError);
471 if (this.onVideo) {
472 let canSaveSnapshot = this.target.readyState >= this.target.HAVE_CURRENT_DATA;
473 this.setItemAttr("context-video-saveimage", "disabled", !canSaveSnapshot);
474 this.setItemAttr("context-video-fullscreen", "disabled", hasError);
475 this.setItemAttr("context-video-showstats", "disabled", hasError);
476 this.setItemAttr("context-video-hidestats", "disabled", hasError);
477 }
478 }
479 this.showItem("context-media-sep-commands", onMedia);
480 },
482 initClickToPlayItems: function() {
483 this.showItem("context-ctp-play", this.onCTPPlugin);
484 this.showItem("context-ctp-hide", this.onCTPPlugin);
485 this.showItem("context-sep-ctp", this.onCTPPlugin);
486 },
488 inspectNode: function CM_inspectNode() {
489 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
490 let gBrowser = this.browser.ownerDocument.defaultView.gBrowser;
491 let tt = devtools.TargetFactory.forTab(gBrowser.selectedTab);
492 return gDevTools.showToolbox(tt, "inspector").then(function(toolbox) {
493 let inspector = toolbox.getCurrentPanel();
494 inspector.selection.setNode(this.target, "browser-context-menu");
495 }.bind(this));
496 },
498 // Set various context menu attributes based on the state of the world.
499 setTarget: function (aNode, aRangeParent, aRangeOffset) {
500 // If gContextMenuContentData is not null, this event was forwarded from a
501 // child process, so use that information instead.
502 if (gContextMenuContentData) {
503 this.isRemote = true;
504 aNode = gContextMenuContentData.event.target;
505 } else {
506 this.isRemote = false;
507 }
509 const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
510 if (aNode.namespaceURI == xulNS ||
511 aNode.nodeType == Node.DOCUMENT_NODE ||
512 this.isDisabledForEvents(aNode)) {
513 this.shouldDisplay = false;
514 return;
515 }
517 // Initialize contextual info.
518 this.onImage = false;
519 this.onLoadedImage = false;
520 this.onCompletedImage = false;
521 this.imageDescURL = "";
522 this.onCanvas = false;
523 this.onVideo = false;
524 this.onAudio = false;
525 this.onTextInput = false;
526 this.onKeywordField = false;
527 this.mediaURL = "";
528 this.onLink = false;
529 this.onMailtoLink = false;
530 this.onSaveableLink = false;
531 this.link = null;
532 this.linkURL = "";
533 this.linkURI = null;
534 this.linkProtocol = "";
535 this.onMathML = false;
536 this.inFrame = false;
537 this.inSrcdocFrame = false;
538 this.inSyntheticDoc = false;
539 this.hasBGImage = false;
540 this.bgImageURL = "";
541 this.onEditableArea = false;
542 this.isDesignMode = false;
543 this.onCTPPlugin = false;
544 this.canSpellCheck = false;
545 this.textSelected = getBrowserSelection();
546 this.isTextSelected = this.textSelected.length != 0;
548 // Remember the node that was clicked.
549 this.target = aNode;
551 let [elt, win] = BrowserUtils.getFocusSync(document);
552 this.focusedWindow = win;
553 this.focusedElement = elt;
555 // If this is a remote context menu event, use the information from
556 // gContextMenuContentData instead.
557 if (this.isRemote) {
558 this.browser = gContextMenuContentData.browser;
559 } else {
560 this.browser = this.target.ownerDocument.defaultView
561 .QueryInterface(Ci.nsIInterfaceRequestor)
562 .getInterface(Ci.nsIWebNavigation)
563 .QueryInterface(Ci.nsIDocShell)
564 .chromeEventHandler;
565 }
566 this.onSocial = !!this.browser.getAttribute("origin");
568 // Check if we are in a synthetic document (stand alone image, video, etc.).
569 this.inSyntheticDoc = this.target.ownerDocument.mozSyntheticDocument;
570 // First, do checks for nodes that never have children.
571 if (this.target.nodeType == Node.ELEMENT_NODE) {
572 // See if the user clicked on an image.
573 if (this.target instanceof Ci.nsIImageLoadingContent &&
574 this.target.currentURI) {
575 this.onImage = true;
577 var request =
578 this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
579 if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
580 this.onLoadedImage = true;
581 if (request && (request.imageStatus & request.STATUS_LOAD_COMPLETE))
582 this.onCompletedImage = true;
584 this.mediaURL = this.target.currentURI.spec;
586 var descURL = this.target.getAttribute("longdesc");
587 if (descURL) {
588 this.imageDescURL = makeURLAbsolute(this.target.ownerDocument.body.baseURI, descURL);
589 }
590 }
591 else if (this.target instanceof HTMLCanvasElement) {
592 this.onCanvas = true;
593 }
594 else if (this.target instanceof HTMLVideoElement) {
595 this.mediaURL = this.target.currentSrc || this.target.src;
596 // Firefox always creates a HTMLVideoElement when loading an ogg file
597 // directly. If the media is actually audio, be smarter and provide a
598 // context menu with audio operations.
599 if (this.target.readyState >= this.target.HAVE_METADATA &&
600 (this.target.videoWidth == 0 || this.target.videoHeight == 0)) {
601 this.onAudio = true;
602 } else {
603 this.onVideo = true;
604 }
605 }
606 else if (this.target instanceof HTMLAudioElement) {
607 this.onAudio = true;
608 this.mediaURL = this.target.currentSrc || this.target.src;
609 }
610 else if (this.target instanceof HTMLInputElement ) {
611 this.onTextInput = this.isTargetATextBox(this.target);
612 // Allow spellchecking UI on all text and search inputs.
613 if (this.onTextInput && ! this.target.readOnly &&
614 (this.target.type == "text" || this.target.type == "search")) {
615 this.onEditableArea = true;
616 InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
617 InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
618 }
619 this.onKeywordField = this.isTargetAKeywordField(this.target);
620 }
621 else if (this.target instanceof HTMLTextAreaElement) {
622 this.onTextInput = true;
623 if (!this.target.readOnly) {
624 this.onEditableArea = true;
625 InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
626 InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
627 }
628 }
629 else if (this.target instanceof HTMLHtmlElement) {
630 var bodyElt = this.target.ownerDocument.body;
631 if (bodyElt) {
632 let computedURL;
633 try {
634 computedURL = this.getComputedURL(bodyElt, "background-image");
635 this._hasMultipleBGImages = false;
636 } catch (e) {
637 this._hasMultipleBGImages = true;
638 }
639 if (computedURL) {
640 this.hasBGImage = true;
641 this.bgImageURL = makeURLAbsolute(bodyElt.baseURI,
642 computedURL);
643 }
644 }
645 }
646 else if ((this.target instanceof HTMLEmbedElement ||
647 this.target instanceof HTMLObjectElement ||
648 this.target instanceof HTMLAppletElement) &&
649 this.target.mozMatchesSelector(":-moz-handler-clicktoplay")) {
650 this.onCTPPlugin = true;
651 }
653 this.canSpellCheck = this._isSpellCheckEnabled(this.target);
654 }
655 else if (this.target.nodeType == Node.TEXT_NODE) {
656 // For text nodes, look at the parent node to determine the spellcheck attribute.
657 this.canSpellCheck = this.target.parentNode &&
658 this._isSpellCheckEnabled(this.target);
659 }
661 // Second, bubble out, looking for items of interest that can have childen.
662 // Always pick the innermost link, background image, etc.
663 const XMLNS = "http://www.w3.org/XML/1998/namespace";
664 var elem = this.target;
665 while (elem) {
666 if (elem.nodeType == Node.ELEMENT_NODE) {
667 // Link?
668 if (!this.onLink &&
669 // Be consistent with what hrefAndLinkNodeForClickEvent
670 // does in browser.js
671 ((elem instanceof HTMLAnchorElement && elem.href) ||
672 (elem instanceof HTMLAreaElement && elem.href) ||
673 elem instanceof HTMLLinkElement ||
674 elem.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple")) {
676 // Target is a link or a descendant of a link.
677 this.onLink = true;
679 // Remember corresponding element.
680 this.link = elem;
681 this.linkURL = this.getLinkURL();
682 this.linkURI = this.getLinkURI();
683 this.linkProtocol = this.getLinkProtocol();
684 this.onMailtoLink = (this.linkProtocol == "mailto");
685 this.onSaveableLink = this.isLinkSaveable( this.link );
686 }
688 // Background image? Don't bother if we've already found a
689 // background image further down the hierarchy. Otherwise,
690 // we look for the computed background-image style.
691 if (!this.hasBGImage &&
692 !this._hasMultipleBGImages) {
693 let bgImgUrl;
694 try {
695 bgImgUrl = this.getComputedURL(elem, "background-image");
696 this._hasMultipleBGImages = false;
697 } catch (e) {
698 this._hasMultipleBGImages = true;
699 }
700 if (bgImgUrl) {
701 this.hasBGImage = true;
702 this.bgImageURL = makeURLAbsolute(elem.baseURI,
703 bgImgUrl);
704 }
705 }
706 }
708 elem = elem.parentNode;
709 }
711 // See if the user clicked on MathML
712 const NS_MathML = "http://www.w3.org/1998/Math/MathML";
713 if ((this.target.nodeType == Node.TEXT_NODE &&
714 this.target.parentNode.namespaceURI == NS_MathML)
715 || (this.target.namespaceURI == NS_MathML))
716 this.onMathML = true;
718 // See if the user clicked in a frame.
719 var docDefaultView = this.target.ownerDocument.defaultView;
720 if (docDefaultView != docDefaultView.top) {
721 this.inFrame = true;
723 if (this.target.ownerDocument.isSrcdocDocument) {
724 this.inSrcdocFrame = true;
725 }
726 }
728 // if the document is editable, show context menu like in text inputs
729 if (!this.onEditableArea) {
730 var win = this.target.ownerDocument.defaultView;
731 if (win) {
732 var isEditable = false;
733 try {
734 var editingSession = win.QueryInterface(Ci.nsIInterfaceRequestor)
735 .getInterface(Ci.nsIWebNavigation)
736 .QueryInterface(Ci.nsIInterfaceRequestor)
737 .getInterface(Ci.nsIEditingSession);
738 if (editingSession.windowIsEditable(win) &&
739 this.getComputedStyle(this.target, "-moz-user-modify") == "read-write") {
740 isEditable = true;
741 }
742 }
743 catch(ex) {
744 // If someone built with composer disabled, we can't get an editing session.
745 }
747 if (isEditable) {
748 this.onTextInput = true;
749 this.onKeywordField = false;
750 this.onImage = false;
751 this.onLoadedImage = false;
752 this.onCompletedImage = false;
753 this.onMathML = false;
754 this.inFrame = false;
755 this.inSrcdocFrame = false;
756 this.hasBGImage = false;
757 this.isDesignMode = true;
758 this.onEditableArea = true;
759 InlineSpellCheckerUI.init(editingSession.getEditorForWindow(win));
760 var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
761 InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
762 this.showItem("spell-check-enabled", canSpell);
763 this.showItem("spell-separator", canSpell);
764 }
765 }
766 }
767 },
769 // Returns the computed style attribute for the given element.
770 getComputedStyle: function(aElem, aProp) {
771 return aElem.ownerDocument
772 .defaultView
773 .getComputedStyle(aElem, "").getPropertyValue(aProp);
774 },
776 // Returns a "url"-type computed style attribute value, with the url() stripped.
777 getComputedURL: function(aElem, aProp) {
778 var url = aElem.ownerDocument
779 .defaultView.getComputedStyle(aElem, "")
780 .getPropertyCSSValue(aProp);
781 if (url instanceof CSSValueList) {
782 if (url.length != 1)
783 throw "found multiple URLs";
784 url = url[0];
785 }
786 return url.primitiveType == CSSPrimitiveValue.CSS_URI ?
787 url.getStringValue() : null;
788 },
790 // Returns true if clicked-on link targets a resource that can be saved.
791 isLinkSaveable: function(aLink) {
792 // We don't do the Right Thing for news/snews yet, so turn them off
793 // until we do.
794 return this.linkProtocol && !(
795 this.linkProtocol == "mailto" ||
796 this.linkProtocol == "javascript" ||
797 this.linkProtocol == "news" ||
798 this.linkProtocol == "snews" );
799 },
801 _unremotePrincipal: function(aRemotePrincipal) {
802 if (this.isRemote) {
803 return Cc["@mozilla.org/scriptsecuritymanager;1"]
804 .getService(Ci.nsIScriptSecurityManager)
805 .getAppCodebasePrincipal(aRemotePrincipal.URI,
806 aRemotePrincipal.appId,
807 aRemotePrincipal.isInBrowserElement);
808 }
810 return aRemotePrincipal;
811 },
813 _isSpellCheckEnabled: function(aNode) {
814 // We can always force-enable spellchecking on textboxes
815 if (this.isTargetATextBox(aNode)) {
816 return true;
817 }
818 // We can never spell check something which is not content editable
819 var editable = aNode.isContentEditable;
820 if (!editable && aNode.ownerDocument) {
821 editable = aNode.ownerDocument.designMode == "on";
822 }
823 if (!editable) {
824 return false;
825 }
826 // Otherwise make sure that nothing in the parent chain disables spellchecking
827 return aNode.spellcheck;
828 },
830 // Open linked-to URL in a new window.
831 openLink : function () {
832 var doc = this.target.ownerDocument;
833 urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
834 openLinkIn(this.linkURL, "window",
835 { charset: doc.characterSet,
836 referrerURI: doc.documentURIObject });
837 },
839 // Open linked-to URL in a new private window.
840 openLinkInPrivateWindow : function () {
841 var doc = this.target.ownerDocument;
842 urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
843 openLinkIn(this.linkURL, "window",
844 { charset: doc.characterSet,
845 referrerURI: doc.documentURIObject,
846 private: true });
847 },
849 // Open linked-to URL in a new tab.
850 openLinkInTab: function() {
851 var doc = this.target.ownerDocument;
852 urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
853 var referrerURI = doc.documentURIObject;
855 // if the mixedContentChannel is present and the referring URI passes
856 // a same origin check with the target URI, we can preserve the users
857 // decision of disabling MCB on a page for it's child tabs.
858 var persistDisableMCBInChildTab = false;
860 if (this.browser.docShell && this.browser.docShell.mixedContentChannel) {
861 const sm = Services.scriptSecurityManager;
862 try {
863 var targetURI = this.linkURI;
864 sm.checkSameOriginURI(referrerURI, targetURI, false);
865 persistDisableMCBInChildTab = true;
866 }
867 catch (e) { }
868 }
870 openLinkIn(this.linkURL, "tab",
871 { charset: doc.characterSet,
872 referrerURI: referrerURI,
873 disableMCB: persistDisableMCBInChildTab});
874 },
876 // open URL in current tab
877 openLinkInCurrent: function() {
878 var doc = this.target.ownerDocument;
879 urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
880 openLinkIn(this.linkURL, "current",
881 { charset: doc.characterSet,
882 referrerURI: doc.documentURIObject });
883 },
885 // Open frame in a new tab.
886 openFrameInTab: function() {
887 var doc = this.target.ownerDocument;
888 var frameURL = doc.location.href;
889 var referrer = doc.referrer;
890 openLinkIn(frameURL, "tab",
891 { charset: doc.characterSet,
892 referrerURI: referrer ? makeURI(referrer) : null });
893 },
895 // Reload clicked-in frame.
896 reloadFrame: function() {
897 this.target.ownerDocument.location.reload();
898 },
900 // Open clicked-in frame in its own window.
901 openFrame: function() {
902 var doc = this.target.ownerDocument;
903 var frameURL = doc.location.href;
904 var referrer = doc.referrer;
905 openLinkIn(frameURL, "window",
906 { charset: doc.characterSet,
907 referrerURI: referrer ? makeURI(referrer) : null });
908 },
910 // Open clicked-in frame in the same window.
911 showOnlyThisFrame: function() {
912 var doc = this.target.ownerDocument;
913 var frameURL = doc.location.href;
915 urlSecurityCheck(frameURL,
916 this._unremotePrincipal(this.browser.contentPrincipal),
917 Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
918 var referrer = doc.referrer;
919 openUILinkIn(frameURL, "current", { disallowInheritPrincipal: true,
920 referrerURI: referrer ? makeURI(referrer) : null });
921 },
923 reload: function(event) {
924 if (this.onSocial) {
925 // full reload of social provider
926 Social._getProviderFromOrigin(this.browser.getAttribute("origin")).reload();
927 } else {
928 BrowserReloadOrDuplicate(event);
929 }
930 },
932 // View Partial Source
933 viewPartialSource: function(aContext) {
934 var focusedWindow = document.commandDispatcher.focusedWindow;
935 if (focusedWindow == window)
936 focusedWindow = content;
938 var docCharset = null;
939 if (focusedWindow)
940 docCharset = "charset=" + focusedWindow.document.characterSet;
942 // "View Selection Source" and others such as "View MathML Source"
943 // are mutually exclusive, with the precedence given to the selection
944 // when there is one
945 var reference = null;
946 if (aContext == "selection")
947 reference = focusedWindow.getSelection();
948 else if (aContext == "mathml")
949 reference = this.target;
950 else
951 throw "not reached";
953 // unused (and play nice for fragments generated via XSLT too)
954 var docUrl = null;
955 window.openDialog("chrome://global/content/viewPartialSource.xul",
956 "_blank", "scrollbars,resizable,chrome,dialog=no",
957 docUrl, docCharset, reference, aContext);
958 },
960 // Open new "view source" window with the frame's URL.
961 viewFrameSource: function() {
962 BrowserViewSourceOfDocument(this.target.ownerDocument);
963 },
965 viewInfo: function() {
966 BrowserPageInfo(this.target.ownerDocument.defaultView.top.document);
967 },
969 viewImageInfo: function() {
970 BrowserPageInfo(this.target.ownerDocument.defaultView.top.document,
971 "mediaTab", this.target);
972 },
974 viewImageDesc: function(e) {
975 var doc = this.target.ownerDocument;
976 urlSecurityCheck(this.imageDescURL,
977 this._unremotePrincipal(this.browser.contentPrincipal),
978 Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
979 openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true,
980 referrerURI: doc.documentURIObject });
981 },
983 viewFrameInfo: function() {
984 BrowserPageInfo(this.target.ownerDocument);
985 },
987 reloadImage: function(e) {
988 urlSecurityCheck(this.mediaURL,
989 this._unremotePrincipal(this.browser.contentPrincipal),
990 Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
992 if (this.target instanceof Ci.nsIImageLoadingContent)
993 this.target.forceReload();
994 },
996 // Change current window to the URL of the image, video, or audio.
997 viewMedia: function(e) {
998 var viewURL;
1000 if (this.onCanvas)
1001 viewURL = this.target.toDataURL();
1002 else {
1003 viewURL = this.mediaURL;
1004 urlSecurityCheck(viewURL,
1005 this._unremotePrincipal(this.browser.contentPrincipal),
1006 Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
1007 }
1009 var doc = this.target.ownerDocument;
1010 openUILink(viewURL, e, { disallowInheritPrincipal: true,
1011 referrerURI: doc.documentURIObject });
1012 },
1014 saveVideoFrameAsImage: function () {
1015 let name = "";
1016 if (this.mediaURL) {
1017 try {
1018 let uri = makeURI(this.mediaURL);
1019 let url = uri.QueryInterface(Ci.nsIURL);
1020 if (url.fileBaseName)
1021 name = decodeURI(url.fileBaseName) + ".jpg";
1022 } catch (e) { }
1023 }
1024 if (!name)
1025 name = "snapshot.jpg";
1026 var video = this.target;
1027 var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
1028 canvas.width = video.videoWidth;
1029 canvas.height = video.videoHeight;
1030 var ctxDraw = canvas.getContext("2d");
1031 ctxDraw.drawImage(video, 0, 0);
1032 saveImageURL(canvas.toDataURL("image/jpeg", ""), name, "SaveImageTitle", true, false, document.documentURIObject, this.target.ownerDocument);
1033 },
1035 fullScreenVideo: function () {
1036 let video = this.target;
1037 if (document.mozFullScreenEnabled)
1038 video.mozRequestFullScreen();
1039 },
1041 leaveDOMFullScreen: function() {
1042 document.mozCancelFullScreen();
1043 },
1045 // Change current window to the URL of the background image.
1046 viewBGImage: function(e) {
1047 urlSecurityCheck(this.bgImageURL,
1048 this._unremotePrincipal(this.browser.contentPrincipal),
1049 Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
1050 var doc = this.target.ownerDocument;
1051 openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true,
1052 referrerURI: doc.documentURIObject });
1053 },
1055 disableSetDesktopBackground: function() {
1056 // Disable the Set as Desktop Background menu item if we're still trying
1057 // to load the image or the load failed.
1058 if (!(this.target instanceof Ci.nsIImageLoadingContent))
1059 return true;
1061 if (("complete" in this.target) && !this.target.complete)
1062 return true;
1064 if (this.target.currentURI.schemeIs("javascript"))
1065 return true;
1067 var request = this.target
1068 .QueryInterface(Ci.nsIImageLoadingContent)
1069 .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
1070 if (!request)
1071 return true;
1073 return false;
1074 },
1076 setDesktopBackground: function() {
1077 // Paranoia: check disableSetDesktopBackground again, in case the
1078 // image changed since the context menu was initiated.
1079 if (this.disableSetDesktopBackground())
1080 return;
1082 var doc = this.target.ownerDocument;
1083 urlSecurityCheck(this.target.currentURI.spec,
1084 this._unremotePrincipal(doc.nodePrincipal));
1086 // Confirm since it's annoying if you hit this accidentally.
1087 const kDesktopBackgroundURL =
1088 "chrome://browser/content/setDesktopBackground.xul";
1089 #ifdef XP_MACOSX
1090 // On Mac, the Set Desktop Background window is not modal.
1091 // Don't open more than one Set Desktop Background window.
1092 var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
1093 .getService(Components.interfaces.nsIWindowMediator);
1094 var dbWin = wm.getMostRecentWindow("Shell:SetDesktopBackground");
1095 if (dbWin) {
1096 dbWin.gSetBackground.init(this.target);
1097 dbWin.focus();
1098 }
1099 else {
1100 openDialog(kDesktopBackgroundURL, "",
1101 "centerscreen,chrome,dialog=no,dependent,resizable=no",
1102 this.target);
1103 }
1104 #else
1105 // On non-Mac platforms, the Set Wallpaper dialog is modal.
1106 openDialog(kDesktopBackgroundURL, "",
1107 "centerscreen,chrome,dialog,modal,dependent",
1108 this.target);
1109 #endif
1110 },
1112 // Save URL of clicked-on frame.
1113 saveFrame: function () {
1114 saveDocument(this.target.ownerDocument);
1115 },
1117 // Helper function to wait for appropriate MIME-type headers and
1118 // then prompt the user with a file picker
1119 saveHelper: function(linkURL, linkText, dialogTitle, bypassCache, doc) {
1120 // canonical def in nsURILoader.h
1121 const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
1123 // an object to proxy the data through to
1124 // nsIExternalHelperAppService.doContent, which will wait for the
1125 // appropriate MIME-type headers and then prompt the user with a
1126 // file picker
1127 function saveAsListener() {}
1128 saveAsListener.prototype = {
1129 extListener: null,
1131 onStartRequest: function saveLinkAs_onStartRequest(aRequest, aContext) {
1133 // if the timer fired, the error status will have been caused by that,
1134 // and we'll be restarting in onStopRequest, so no reason to notify
1135 // the user
1136 if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT)
1137 return;
1139 timer.cancel();
1141 // some other error occured; notify the user...
1142 if (!Components.isSuccessCode(aRequest.status)) {
1143 try {
1144 const sbs = Cc["@mozilla.org/intl/stringbundle;1"].
1145 getService(Ci.nsIStringBundleService);
1146 const bundle = sbs.createBundle(
1147 "chrome://mozapps/locale/downloads/downloads.properties");
1149 const title = bundle.GetStringFromName("downloadErrorAlertTitle");
1150 const msg = bundle.GetStringFromName("downloadErrorGeneric");
1152 const promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].
1153 getService(Ci.nsIPromptService);
1154 promptSvc.alert(doc.defaultView, title, msg);
1155 } catch (ex) {}
1156 return;
1157 }
1159 var extHelperAppSvc =
1160 Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
1161 getService(Ci.nsIExternalHelperAppService);
1162 var channel = aRequest.QueryInterface(Ci.nsIChannel);
1163 this.extListener =
1164 extHelperAppSvc.doContent(channel.contentType, aRequest,
1165 doc.defaultView, true);
1166 this.extListener.onStartRequest(aRequest, aContext);
1167 },
1169 onStopRequest: function saveLinkAs_onStopRequest(aRequest, aContext,
1170 aStatusCode) {
1171 if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
1172 // do it the old fashioned way, which will pick the best filename
1173 // it can without waiting.
1174 saveURL(linkURL, linkText, dialogTitle, bypassCache, false, doc.documentURIObject, doc);
1175 }
1176 if (this.extListener)
1177 this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
1178 },
1180 onDataAvailable: function saveLinkAs_onDataAvailable(aRequest, aContext,
1181 aInputStream,
1182 aOffset, aCount) {
1183 this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
1184 aOffset, aCount);
1185 }
1186 }
1188 function callbacks() {}
1189 callbacks.prototype = {
1190 getInterface: function sLA_callbacks_getInterface(aIID) {
1191 if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) {
1192 // If the channel demands authentication prompt, we must cancel it
1193 // because the save-as-timer would expire and cancel the channel
1194 // before we get credentials from user. Both authentication dialog
1195 // and save as dialog would appear on the screen as we fall back to
1196 // the old fashioned way after the timeout.
1197 timer.cancel();
1198 channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
1199 }
1200 throw Cr.NS_ERROR_NO_INTERFACE;
1201 }
1202 }
1204 // if it we don't have the headers after a short time, the user
1205 // won't have received any feedback from their click. that's bad. so
1206 // we give up waiting for the filename.
1207 function timerCallback() {}
1208 timerCallback.prototype = {
1209 notify: function sLA_timer_notify(aTimer) {
1210 channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
1211 return;
1212 }
1213 }
1215 // set up a channel to do the saving
1216 var ioService = Cc["@mozilla.org/network/io-service;1"].
1217 getService(Ci.nsIIOService);
1218 var channel = ioService.newChannelFromURI(makeURI(linkURL));
1219 if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
1220 let docIsPrivate = PrivateBrowsingUtils.isWindowPrivate(doc.defaultView);
1221 channel.setPrivate(docIsPrivate);
1222 }
1223 channel.notificationCallbacks = new callbacks();
1225 let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS;
1227 if (bypassCache)
1228 flags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
1230 if (channel instanceof Ci.nsICachingChannel)
1231 flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
1233 channel.loadFlags |= flags;
1235 if (channel instanceof Ci.nsIHttpChannel) {
1236 channel.referrer = doc.documentURIObject;
1237 if (channel instanceof Ci.nsIHttpChannelInternal)
1238 channel.forceAllowThirdPartyCookie = true;
1239 }
1241 // fallback to the old way if we don't see the headers quickly
1242 var timeToWait =
1243 gPrefService.getIntPref("browser.download.saveLinkAsFilenameTimeout");
1244 var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
1245 timer.initWithCallback(new timerCallback(), timeToWait,
1246 timer.TYPE_ONE_SHOT);
1248 // kick off the channel with our proxy object as the listener
1249 channel.asyncOpen(new saveAsListener(), null);
1250 },
1252 // Save URL of clicked-on link.
1253 saveLink: function() {
1254 var doc = this.target.ownerDocument;
1255 var linkText;
1256 // If selected text is found to match valid URL pattern.
1257 if (this.onPlainTextLink)
1258 linkText = this.focusedWindow.getSelection().toString().trim();
1259 else
1260 linkText = this.linkText();
1261 urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
1263 this.saveHelper(this.linkURL, linkText, null, true, doc);
1264 },
1266 // Backwards-compatibility wrapper
1267 saveImage : function() {
1268 if (this.onCanvas || this.onImage)
1269 this.saveMedia();
1270 },
1272 // Save URL of the clicked upon image, video, or audio.
1273 saveMedia: function() {
1274 var doc = this.target.ownerDocument;
1275 if (this.onCanvas) {
1276 // Bypass cache, since it's a data: URL.
1277 saveImageURL(this.target.toDataURL(), "canvas.png", "SaveImageTitle",
1278 true, false, doc.documentURIObject, doc);
1279 }
1280 else if (this.onImage) {
1281 urlSecurityCheck(this.mediaURL,
1282 this._unremotePrincipal(doc.nodePrincipal));
1283 saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
1284 false, doc.documentURIObject, doc);
1285 }
1286 else if (this.onVideo || this.onAudio) {
1287 urlSecurityCheck(this.mediaURL,
1288 this._unremotePrincipal(doc.nodePrincipal));
1289 var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
1290 this.saveHelper(this.mediaURL, null, dialogTitle, false, doc);
1291 }
1292 },
1294 // Backwards-compatibility wrapper
1295 sendImage : function() {
1296 if (this.onCanvas || this.onImage)
1297 this.sendMedia();
1298 },
1300 sendMedia: function() {
1301 MailIntegration.sendMessage(this.mediaURL, "");
1302 },
1304 playPlugin: function() {
1305 gPluginHandler._showClickToPlayNotification(this.browser, this.target, true);
1306 },
1308 hidePlugin: function() {
1309 gPluginHandler.hideClickToPlayOverlay(this.target);
1310 },
1312 // Generate email address and put it on clipboard.
1313 copyEmail: function() {
1314 // Copy the comma-separated list of email addresses only.
1315 // There are other ways of embedding email addresses in a mailto:
1316 // link, but such complex parsing is beyond us.
1317 var url = this.linkURL;
1318 var qmark = url.indexOf("?");
1319 var addresses;
1321 // 7 == length of "mailto:"
1322 addresses = qmark > 7 ? url.substring(7, qmark) : url.substr(7);
1324 // Let's try to unescape it using a character set
1325 // in case the address is not ASCII.
1326 try {
1327 var characterSet = this.target.ownerDocument.characterSet;
1328 const textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
1329 getService(Ci.nsITextToSubURI);
1330 addresses = textToSubURI.unEscapeURIForUI(characterSet, addresses);
1331 }
1332 catch(ex) {
1333 // Do nothing.
1334 }
1336 var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
1337 getService(Ci.nsIClipboardHelper);
1338 clipboard.copyString(addresses, document);
1339 },
1341 copyLink: function() {
1342 var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
1343 getService(Ci.nsIClipboardHelper);
1344 clipboard.copyString(this.linkURL, document);
1345 },
1347 ///////////////
1348 // Utilities //
1349 ///////////////
1351 // Show/hide one item (specified via name or the item element itself).
1352 showItem: function(aItemOrId, aShow) {
1353 var item = aItemOrId.constructor == String ?
1354 document.getElementById(aItemOrId) : aItemOrId;
1355 if (item)
1356 item.hidden = !aShow;
1357 },
1359 // Set given attribute of specified context-menu item. If the
1360 // value is null, then it removes the attribute (which works
1361 // nicely for the disabled attribute).
1362 setItemAttr: function(aID, aAttr, aVal ) {
1363 var elem = document.getElementById(aID);
1364 if (elem) {
1365 if (aVal == null) {
1366 // null indicates attr should be removed.
1367 elem.removeAttribute(aAttr);
1368 }
1369 else {
1370 // Set attr=val.
1371 elem.setAttribute(aAttr, aVal);
1372 }
1373 }
1374 },
1376 // Set context menu attribute according to like attribute of another node
1377 // (such as a broadcaster).
1378 setItemAttrFromNode: function(aItem_id, aAttr, aOther_id) {
1379 var elem = document.getElementById(aOther_id);
1380 if (elem && elem.getAttribute(aAttr) == "true")
1381 this.setItemAttr(aItem_id, aAttr, "true");
1382 else
1383 this.setItemAttr(aItem_id, aAttr, null);
1384 },
1386 // Temporary workaround for DOM api not yet implemented by XUL nodes.
1387 cloneNode: function(aItem) {
1388 // Create another element like the one we're cloning.
1389 var node = document.createElement(aItem.tagName);
1391 // Copy attributes from argument item to the new one.
1392 var attrs = aItem.attributes;
1393 for (var i = 0; i < attrs.length; i++) {
1394 var attr = attrs.item(i);
1395 node.setAttribute(attr.nodeName, attr.nodeValue);
1396 }
1398 // Voila!
1399 return node;
1400 },
1402 // Generate fully qualified URL for clicked-on link.
1403 getLinkURL: function() {
1404 var href = this.link.href;
1405 if (href)
1406 return href;
1408 href = this.link.getAttributeNS("http://www.w3.org/1999/xlink",
1409 "href");
1411 if (!href || !href.match(/\S/)) {
1412 // Without this we try to save as the current doc,
1413 // for example, HTML case also throws if empty
1414 throw "Empty href";
1415 }
1417 return makeURLAbsolute(this.link.baseURI, href);
1418 },
1420 getLinkURI: function() {
1421 try {
1422 return makeURI(this.linkURL);
1423 }
1424 catch (ex) {
1425 // e.g. empty URL string
1426 }
1428 return null;
1429 },
1431 getLinkProtocol: function() {
1432 if (this.linkURI)
1433 return this.linkURI.scheme; // can be |undefined|
1435 return null;
1436 },
1438 // Get text of link.
1439 linkText: function() {
1440 var text = gatherTextUnder(this.link);
1441 if (!text || !text.match(/\S/)) {
1442 text = this.link.getAttribute("title");
1443 if (!text || !text.match(/\S/)) {
1444 text = this.link.getAttribute("alt");
1445 if (!text || !text.match(/\S/))
1446 text = this.linkURL;
1447 }
1448 }
1450 return text;
1451 },
1453 // Returns true if anything is selected.
1454 isContentSelection: function() {
1455 return !this.focusedWindow.getSelection().isCollapsed;
1456 },
1458 toString: function () {
1459 return "contextMenu.target = " + this.target + "\n" +
1460 "contextMenu.onImage = " + this.onImage + "\n" +
1461 "contextMenu.onLink = " + this.onLink + "\n" +
1462 "contextMenu.link = " + this.link + "\n" +
1463 "contextMenu.inFrame = " + this.inFrame + "\n" +
1464 "contextMenu.hasBGImage = " + this.hasBGImage + "\n";
1465 },
1467 isDisabledForEvents: function(aNode) {
1468 let ownerDoc = aNode.ownerDocument;
1469 return
1470 ownerDoc.defaultView &&
1471 ownerDoc.defaultView
1472 .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
1473 .getInterface(Components.interfaces.nsIDOMWindowUtils)
1474 .isNodeDisabledForEvents(aNode);
1475 },
1477 isTargetATextBox: function(node) {
1478 if (node instanceof HTMLInputElement)
1479 return node.mozIsTextField(false);
1481 return (node instanceof HTMLTextAreaElement);
1482 },
1484 isTargetAKeywordField: function(aNode) {
1485 if (!(aNode instanceof HTMLInputElement))
1486 return false;
1488 var form = aNode.form;
1489 if (!form || aNode.type == "password")
1490 return false;
1492 var method = form.method.toUpperCase();
1494 // These are the following types of forms we can create keywords for:
1495 //
1496 // method encoding type can create keyword
1497 // GET * YES
1498 // * YES
1499 // POST YES
1500 // POST application/x-www-form-urlencoded YES
1501 // POST text/plain NO (a little tricky to do)
1502 // POST multipart/form-data NO
1503 // POST everything else YES
1504 return (method == "GET" || method == "") ||
1505 (form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
1506 },
1508 // Determines whether or not the separator with the specified ID should be
1509 // shown or not by determining if there are any non-hidden items between it
1510 // and the previous separator.
1511 shouldShowSeparator: function (aSeparatorID) {
1512 var separator = document.getElementById(aSeparatorID);
1513 if (separator) {
1514 var sibling = separator.previousSibling;
1515 while (sibling && sibling.localName != "menuseparator") {
1516 if (!sibling.hidden)
1517 return true;
1518 sibling = sibling.previousSibling;
1519 }
1520 }
1521 return false;
1522 },
1524 addDictionaries: function() {
1525 var uri = formatURL("browser.dictionaries.download.url", true);
1527 var locale = "-";
1528 try {
1529 locale = gPrefService.getComplexValue("intl.accept_languages",
1530 Ci.nsIPrefLocalizedString).data;
1531 }
1532 catch (e) { }
1534 var version = "-";
1535 try {
1536 version = Cc["@mozilla.org/xre/app-info;1"].
1537 getService(Ci.nsIXULAppInfo).version;
1538 }
1539 catch (e) { }
1541 uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version);
1543 var newWindowPref = gPrefService.getIntPref("browser.link.open_newwindow");
1544 var where = newWindowPref == 3 ? "tab" : "window";
1546 openUILinkIn(uri, where);
1547 },
1549 bookmarkThisPage: function CM_bookmarkThisPage() {
1550 window.top.PlacesCommandHook.bookmarkPage(this.browser, PlacesUtils.bookmarksMenuFolderId, true);
1551 },
1553 bookmarkLink: function CM_bookmarkLink() {
1554 var linkText;
1555 // If selected text is found to match valid URL pattern.
1556 if (this.onPlainTextLink)
1557 linkText = this.focusedWindow.getSelection().toString().trim();
1558 else
1559 linkText = this.linkText();
1560 window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId, this.linkURL,
1561 linkText);
1562 },
1564 addBookmarkForFrame: function CM_addBookmarkForFrame() {
1565 var doc = this.target.ownerDocument;
1566 var uri = doc.documentURIObject;
1568 var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
1569 if (itemId == -1) {
1570 var title = doc.title;
1571 var description = PlacesUIUtils.getDescriptionFromDocument(doc);
1572 PlacesUIUtils.showBookmarkDialog({ action: "add"
1573 , type: "bookmark"
1574 , uri: uri
1575 , title: title
1576 , description: description
1577 , hiddenRows: [ "description"
1578 , "location"
1579 , "loadInSidebar"
1580 , "keyword" ]
1581 }, window.top);
1582 }
1583 else {
1584 PlacesUIUtils.showBookmarkDialog({ action: "edit"
1585 , type: "bookmark"
1586 , itemId: itemId
1587 }, window.top);
1588 }
1589 },
1590 markLink: function CM_markLink(origin) {
1591 // send link to social, if it is the page url linkURI will be null
1592 SocialMarks.markLink(origin, this.linkURI ? this.linkURI.spec : null);
1593 },
1594 shareLink: function CM_shareLink() {
1595 SocialShare.sharePage(null, { url: this.linkURI.spec });
1596 },
1598 shareImage: function CM_shareImage() {
1599 SocialShare.sharePage(null, { url: this.imageURL, previews: [ this.mediaURL ] });
1600 },
1602 shareVideo: function CM_shareVideo() {
1603 SocialShare.sharePage(null, { url: this.mediaURL, source: this.mediaURL });
1604 },
1606 shareSelect: function CM_shareSelect(selection) {
1607 SocialShare.sharePage(null, { url: this.browser.currentURI.spec, text: selection });
1608 },
1610 savePageAs: function CM_savePageAs() {
1611 saveDocument(this.browser.contentDocument);
1612 },
1614 printFrame: function CM_printFrame() {
1615 PrintUtils.print(this.target.ownerDocument.defaultView);
1616 },
1618 switchPageDirection: function CM_switchPageDirection() {
1619 SwitchDocumentDirection(this.browser.contentWindow);
1620 },
1622 mediaCommand : function CM_mediaCommand(command, data) {
1623 var media = this.target;
1625 switch (command) {
1626 case "play":
1627 media.play();
1628 break;
1629 case "pause":
1630 media.pause();
1631 break;
1632 case "mute":
1633 media.muted = true;
1634 break;
1635 case "unmute":
1636 media.muted = false;
1637 break;
1638 case "playbackRate":
1639 media.playbackRate = data;
1640 break;
1641 case "hidecontrols":
1642 media.removeAttribute("controls");
1643 break;
1644 case "showcontrols":
1645 media.setAttribute("controls", "true");
1646 break;
1647 case "hidestats":
1648 case "showstats":
1649 var event = media.ownerDocument.createEvent("CustomEvent");
1650 event.initCustomEvent("media-showStatistics", false, true, command == "showstats");
1651 media.dispatchEvent(event);
1652 break;
1653 }
1654 },
1656 copyMediaLocation : function () {
1657 var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
1658 getService(Ci.nsIClipboardHelper);
1659 clipboard.copyString(this.mediaURL, document);
1660 },
1662 get imageURL() {
1663 if (this.onImage)
1664 return this.mediaURL;
1665 return "";
1666 },
1668 // Formats the 'Search <engine> for "<selection or link text>"' context menu.
1669 formatSearchContextItem: function() {
1670 var menuItem = document.getElementById("context-searchselect");
1671 var selectedText = this.isTextSelected ? this.textSelected : this.linkText();
1673 // Store searchTerms in context menu item so we know what to search onclick
1674 menuItem.searchTerms = selectedText;
1676 if (selectedText.length > 15)
1677 selectedText = selectedText.substr(0,15) + this.ellipsis;
1679 // Use the current engine if the search bar is visible, the default
1680 // engine otherwise.
1681 var engineName = "";
1682 var ss = Cc["@mozilla.org/browser/search-service;1"].
1683 getService(Ci.nsIBrowserSearchService);
1684 if (isElementVisible(BrowserSearch.searchBar))
1685 engineName = ss.currentEngine.name;
1686 else
1687 engineName = ss.defaultEngine.name;
1689 // format "Search <engine> for <selection>" string to show in menu
1690 var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch",
1691 [engineName,
1692 selectedText]);
1693 menuItem.label = menuLabel;
1694 menuItem.accessKey = gNavigatorBundle.getString("contextMenuSearch.accesskey");
1695 }
1696 };