1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 /*
7   This file provides the implementation for xul popup listener which
8   tracks xul popups and context menus
9  */
10 
11 #include "nsXULPopupListener.h"
12 #include "nsCOMPtr.h"
13 #include "nsGkAtoms.h"
14 #include "nsIDOMElement.h"
15 #include "nsIDOMXULElement.h"
16 #include "nsIDOMNodeList.h"
17 #include "nsIDOMDocument.h"
18 #include "nsIDOMDocumentXBL.h"
19 #include "nsContentCID.h"
20 #include "nsContentUtils.h"
21 #include "nsXULPopupManager.h"
22 #include "nsIScriptContext.h"
23 #include "nsIDOMWindow.h"
24 #include "nsIDOMXULDocument.h"
25 #include "nsIDocument.h"
26 #include "nsServiceManagerUtils.h"
27 #include "nsIPrincipal.h"
28 #include "nsIScriptSecurityManager.h"
29 #include "nsLayoutUtils.h"
30 #include "mozilla/ReflowInput.h"
31 #include "nsIObjectLoadingContent.h"
32 #include "mozilla/EventStateManager.h"
33 #include "mozilla/EventStates.h"
34 #include "mozilla/Preferences.h"
35 #include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent()
36 #include "mozilla/dom/EventTarget.h"
37 #include "mozilla/dom/FragmentOrElement.h"
38 
39 // for event firing in context menus
40 #include "nsPresContext.h"
41 #include "nsIPresShell.h"
42 #include "nsFocusManager.h"
43 #include "nsPIDOMWindow.h"
44 #include "nsViewManager.h"
45 #include "nsError.h"
46 #include "nsMenuFrame.h"
47 
48 using namespace mozilla;
49 using namespace mozilla::dom;
50 
51 // on win32 and os/2, context menus come up on mouse up. On other platforms,
52 // they appear on mouse down. Certain bits of code care about this difference.
53 #if defined(XP_WIN)
54 #define NS_CONTEXT_MENU_IS_MOUSEUP 1
55 #endif
56 
nsXULPopupListener(mozilla::dom::Element * aElement,bool aIsContext)57 nsXULPopupListener::nsXULPopupListener(mozilla::dom::Element* aElement,
58                                        bool aIsContext)
59   : mElement(aElement), mPopupContent(nullptr), mIsContext(aIsContext)
60 {
61 }
62 
~nsXULPopupListener(void)63 nsXULPopupListener::~nsXULPopupListener(void)
64 {
65   ClosePopup();
66 }
67 
68 NS_IMPL_CYCLE_COLLECTION(nsXULPopupListener, mElement, mPopupContent)
69 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULPopupListener)
70 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULPopupListener)
71 
72 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(nsXULPopupListener)
73   // If the owner, mElement, can be skipped, so can we.
74   if (tmp->mElement) {
75     return mozilla::dom::FragmentOrElement::CanSkip(tmp->mElement, true);
76   }
77 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END
78 
79 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(nsXULPopupListener)
80   if (tmp->mElement) {
81     return mozilla::dom::FragmentOrElement::CanSkipInCC(tmp->mElement);
82   }
83 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END
84 
85 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(nsXULPopupListener)
86   if (tmp->mElement) {
87     return mozilla::dom::FragmentOrElement::CanSkipThis(tmp->mElement);
88   }
89 NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END
90 
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULPopupListener)91 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULPopupListener)
92   NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
93   NS_INTERFACE_MAP_ENTRY(nsISupports)
94 NS_INTERFACE_MAP_END
95 
96 ////////////////////////////////////////////////////////////////
97 // nsIDOMEventListener
98 
99 nsresult
100 nsXULPopupListener::HandleEvent(nsIDOMEvent* aEvent)
101 {
102   nsAutoString eventType;
103   aEvent->GetType(eventType);
104 
105   if(!((eventType.EqualsLiteral("mousedown") && !mIsContext) ||
106        (eventType.EqualsLiteral("contextmenu") && mIsContext)))
107     return NS_OK;
108 
109   int16_t button;
110 
111   nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
112   if (!mouseEvent) {
113     //non-ui event passed in.  bad things.
114     return NS_OK;
115   }
116 
117   // Get the node that was clicked on.
118   EventTarget* target = mouseEvent->AsEvent()->InternalDOMEvent()->GetTarget();
119   nsCOMPtr<nsIDOMNode> targetNode = do_QueryInterface(target);
120 
121   if (!targetNode && mIsContext) {
122     // Not a DOM node, see if it's the DOM window (bug 380818).
123     nsCOMPtr<nsPIDOMWindowInner> domWin = do_QueryInterface(target);
124     if (!domWin) {
125       return NS_ERROR_DOM_WRONG_TYPE_ERR;
126     }
127     // Try to use the root node as target node.
128     nsCOMPtr<nsIDocument> doc = domWin->GetDoc();
129 
130     if (doc)
131       targetNode = do_QueryInterface(doc->GetRootElement());
132     if (!targetNode) {
133       return NS_ERROR_FAILURE;
134     }
135   }
136 
137   nsCOMPtr<nsIContent> targetContent = do_QueryInterface(target);
138   if (!targetContent) {
139     return NS_OK;
140   }
141   if (EventStateManager::IsRemoteTarget(targetContent)) {
142     return NS_OK;
143   }
144 
145   bool preventDefault;
146   mouseEvent->AsEvent()->GetDefaultPrevented(&preventDefault);
147   if (preventDefault && targetNode && mIsContext) {
148     // Someone called preventDefault on a context menu.
149     // Let's make sure they are allowed to do so.
150     bool eventEnabled =
151       Preferences::GetBool("dom.event.contextmenu.enabled", true);
152     if (!eventEnabled) {
153       // If the target node is for plug-in, we should not open XUL context
154       // menu on windowless plug-ins.
155       nsCOMPtr<nsIObjectLoadingContent> olc = do_QueryInterface(targetNode);
156       uint32_t type;
157       if (olc && NS_SUCCEEDED(olc->GetDisplayedType(&type)) &&
158           type == nsIObjectLoadingContent::TYPE_PLUGIN) {
159         return NS_OK;
160       }
161 
162       // The user wants his contextmenus.  Let's make sure that this is a website
163       // and not chrome since there could be places in chrome which don't want
164       // contextmenus.
165       nsCOMPtr<nsINode> node = do_QueryInterface(targetNode);
166       if (node) {
167         nsCOMPtr<nsIPrincipal> system;
168         nsContentUtils::GetSecurityManager()->
169           GetSystemPrincipal(getter_AddRefs(system));
170         if (node->NodePrincipal() != system) {
171           // This isn't chrome.  Cancel the preventDefault() and
172           // let the event go forth.
173           preventDefault = false;
174         }
175       }
176     }
177   }
178 
179   if (preventDefault) {
180     // someone called preventDefault. bail.
181     return NS_OK;
182   }
183 
184   // prevent popups on menu and menuitems as they handle their own popups
185   // This was added for bug 96920.
186   // If a menu item child was clicked on that leads to a popup needing
187   // to show, we know (guaranteed) that we're dealing with a menu or
188   // submenu of an already-showing popup.  We don't need to do anything at all.
189   if (!mIsContext) {
190     if (targetContent &&
191         targetContent->IsAnyOfXULElements(nsGkAtoms::menu, nsGkAtoms::menuitem))
192       return NS_OK;
193   }
194 
195   if (mIsContext) {
196 #ifndef NS_CONTEXT_MENU_IS_MOUSEUP
197     uint16_t inputSource = nsIDOMMouseEvent::MOZ_SOURCE_UNKNOWN;
198     mouseEvent->GetMozInputSource(&inputSource);
199     bool isTouch = inputSource == nsIDOMMouseEvent::MOZ_SOURCE_TOUCH;
200     // If the context menu launches on mousedown,
201     // we have to fire focus on the content we clicked on
202     FireFocusOnTargetContent(targetNode, isTouch);
203 #endif
204   }
205   else {
206     // Only open popups when the left mouse button is down.
207     mouseEvent->GetButton(&button);
208     if (button != 0)
209       return NS_OK;
210   }
211 
212   // Open the popup. LaunchPopup will call StopPropagation and PreventDefault
213   // in the right situations.
214   LaunchPopup(aEvent, targetContent);
215 
216   return NS_OK;
217 }
218 
219 #ifndef NS_CONTEXT_MENU_IS_MOUSEUP
220 nsresult
FireFocusOnTargetContent(nsIDOMNode * aTargetNode,bool aIsTouch)221 nsXULPopupListener::FireFocusOnTargetContent(nsIDOMNode* aTargetNode, bool aIsTouch)
222 {
223   nsresult rv;
224   nsCOMPtr<nsIDOMDocument> domDoc;
225   rv = aTargetNode->GetOwnerDocument(getter_AddRefs(domDoc));
226   if(NS_SUCCEEDED(rv) && domDoc)
227   {
228     nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc);
229 
230     // Get nsIDOMElement for targetNode
231     nsIPresShell *shell = doc->GetShell();
232     if (!shell)
233       return NS_ERROR_FAILURE;
234 
235     // strong reference to keep this from going away between events
236     // XXXbz between what events?  We don't use this local at all!
237     RefPtr<nsPresContext> context = shell->GetPresContext();
238 
239     nsCOMPtr<nsIContent> content = do_QueryInterface(aTargetNode);
240     nsIFrame* targetFrame = content->GetPrimaryFrame();
241     if (!targetFrame) return NS_ERROR_FAILURE;
242 
243     const nsStyleUserInterface* ui = targetFrame->StyleUserInterface();
244     bool suppressBlur = (ui->mUserFocus == StyleUserFocus::Ignore);
245 
246     nsCOMPtr<nsIDOMElement> element;
247     nsCOMPtr<nsIContent> newFocus = do_QueryInterface(content);
248 
249     nsIFrame* currFrame = targetFrame;
250     // Look for the nearest enclosing focusable frame.
251     while (currFrame) {
252         int32_t tabIndexUnused;
253         if (currFrame->IsFocusable(&tabIndexUnused, true)) {
254           newFocus = currFrame->GetContent();
255           nsCOMPtr<nsIDOMElement> domElement(do_QueryInterface(newFocus));
256           if (domElement) {
257             element = domElement;
258             break;
259           }
260         }
261         currFrame = currFrame->GetParent();
262     }
263 
264     nsIFocusManager* fm = nsFocusManager::GetFocusManager();
265     if (fm) {
266       if (element) {
267         uint32_t focusFlags = nsIFocusManager::FLAG_BYMOUSE |
268                               nsIFocusManager::FLAG_NOSCROLL;
269         if (aIsTouch) {
270           focusFlags |= nsIFocusManager::FLAG_BYTOUCH;
271         }
272         fm->SetFocus(element, focusFlags);
273       } else if (!suppressBlur) {
274         nsPIDOMWindowOuter *window = doc->GetWindow();
275         fm->ClearFocus(window);
276       }
277     }
278 
279     EventStateManager* esm = context->EventStateManager();
280     nsCOMPtr<nsIContent> focusableContent = do_QueryInterface(element);
281     esm->SetContentState(focusableContent, NS_EVENT_STATE_ACTIVE);
282   }
283   return rv;
284 }
285 #endif
286 
287 // ClosePopup
288 //
289 // Do everything needed to shut down the popup.
290 //
291 // NOTE: This routine is safe to call even if the popup is already closed.
292 //
293 void
ClosePopup()294 nsXULPopupListener::ClosePopup()
295 {
296   if (mPopupContent) {
297     // this is called when the listener is going away, so make sure that the
298     // popup is hidden. Use asynchronous hiding just to be safe so we don't
299     // fire events during destruction.
300     nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
301     if (pm)
302       pm->HidePopup(mPopupContent, false, true, true, false);
303     mPopupContent = nullptr;  // release the popup
304   }
305 } // ClosePopup
306 
307 static already_AddRefed<nsIContent>
GetImmediateChild(nsIContent * aContent,nsIAtom * aTag)308 GetImmediateChild(nsIContent* aContent, nsIAtom *aTag)
309 {
310   for (nsIContent* child = aContent->GetFirstChild();
311        child;
312        child = child->GetNextSibling()) {
313     if (child->IsXULElement(aTag)) {
314       nsCOMPtr<nsIContent> ret = child;
315       return ret.forget();
316     }
317   }
318 
319   return nullptr;
320 }
321 
322 //
323 // LaunchPopup
324 //
325 // Given the element on which the event was triggered and the mouse locations in
326 // Client and widget coordinates, popup a new window showing the appropriate
327 // content.
328 //
329 // aTargetContent is the target of the mouse event aEvent that triggered the
330 // popup. mElement is the element that the popup menu is attached to.
331 // aTargetContent may be equal to mElement or it may be a descendant.
332 //
333 // This looks for an attribute on |mElement| of the appropriate popup type
334 // (popup, context) and uses that attribute's value as an ID for
335 // the popup content in the document.
336 //
337 nsresult
LaunchPopup(nsIDOMEvent * aEvent,nsIContent * aTargetContent)338 nsXULPopupListener::LaunchPopup(nsIDOMEvent* aEvent, nsIContent* aTargetContent)
339 {
340   nsresult rv = NS_OK;
341 
342   nsAutoString identifier;
343   nsIAtom* type = mIsContext ? nsGkAtoms::context : nsGkAtoms::popup;
344   bool hasPopupAttr = mElement->GetAttr(kNameSpaceID_None, type, identifier);
345 
346   if (identifier.IsEmpty()) {
347     hasPopupAttr = mElement->GetAttr(kNameSpaceID_None,
348                           mIsContext ? nsGkAtoms::contextmenu : nsGkAtoms::menu,
349                           identifier) || hasPopupAttr;
350   }
351 
352   if (hasPopupAttr) {
353     aEvent->StopPropagation();
354     aEvent->PreventDefault();
355   }
356 
357   if (identifier.IsEmpty())
358     return rv;
359 
360   // Try to find the popup content and the document.
361   nsCOMPtr<nsIDocument> document = mElement->GetComposedDoc();
362   if (!document) {
363     NS_WARNING("No document!");
364     return NS_ERROR_FAILURE;
365   }
366 
367   // Handle the _child case for popups and context menus
368   nsCOMPtr<nsIContent> popup;
369   if (identifier.EqualsLiteral("_child")) {
370     popup = GetImmediateChild(mElement, nsGkAtoms::menupopup);
371     if (!popup) {
372       nsCOMPtr<nsIDOMDocumentXBL> nsDoc(do_QueryInterface(document));
373       nsCOMPtr<nsIDOMNodeList> list;
374       nsCOMPtr<nsIDOMElement> el = do_QueryInterface(mElement);
375       nsDoc->GetAnonymousNodes(el, getter_AddRefs(list));
376       if (list) {
377         uint32_t ctr,listLength;
378         nsCOMPtr<nsIDOMNode> node;
379         list->GetLength(&listLength);
380         for (ctr = 0; ctr < listLength; ctr++) {
381           list->Item(ctr, getter_AddRefs(node));
382           nsCOMPtr<nsIContent> childContent(do_QueryInterface(node));
383 
384           if (childContent->NodeInfo()->Equals(nsGkAtoms::menupopup,
385                                                kNameSpaceID_XUL)) {
386             popup.swap(childContent);
387             break;
388           }
389         }
390       }
391     }
392   } else if (!mElement->IsInUncomposedDoc() ||
393              !(popup = document->GetElementById(identifier))) {
394     // XXXsmaug Should we try to use ShadowRoot::GetElementById in case
395     //          mElement is in shadow DOM?
396     //
397     // Use getElementById to obtain the popup content and gracefully fail if
398     // we didn't find any popup content in the document.
399     NS_WARNING("GetElementById had some kind of spasm.");
400     return rv;
401   }
402 
403   // return if no popup was found or the popup is the element itself.
404   if (!popup || popup == mElement)
405     return NS_OK;
406 
407   // Submenus can't be used as context menus or popups, bug 288763.
408   // Similar code also in nsXULTooltipListener::GetTooltipFor.
409   nsIContent* parent = popup->GetParent();
410   if (parent) {
411     nsMenuFrame* menu = do_QueryFrame(parent->GetPrimaryFrame());
412     if (menu)
413       return NS_OK;
414   }
415 
416   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
417   if (!pm)
418     return NS_OK;
419 
420   // For left-clicks, if the popup has an position attribute, or both the
421   // popupanchor and popupalign attributes are used, anchor the popup to the
422   // element, otherwise just open it at the screen position where the mouse
423   // was clicked. Context menus always open at the mouse position.
424   mPopupContent = popup;
425   if (!mIsContext &&
426       (mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::position) ||
427        (mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupanchor) &&
428         mPopupContent->HasAttr(kNameSpaceID_None, nsGkAtoms::popupalign)))) {
429     pm->ShowPopup(mPopupContent, mElement, EmptyString(), 0, 0,
430                   false, true, false, aEvent);
431   }
432   else {
433     int32_t xPos = 0, yPos = 0;
434     nsCOMPtr<nsIDOMMouseEvent> mouseEvent = do_QueryInterface(aEvent);
435     mouseEvent->GetScreenX(&xPos);
436     mouseEvent->GetScreenY(&yPos);
437 
438     pm->ShowPopupAtScreen(mPopupContent, xPos, yPos, mIsContext, aEvent);
439   }
440 
441   return NS_OK;
442 }
443