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