PopupBlocker.cpp (13730B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "mozilla/dom/PopupBlocker.h" 8 9 #include "mozilla/BasePrincipal.h" 10 #include "mozilla/Components.h" 11 #include "mozilla/EventForwards.h" 12 #include "mozilla/MouseEvents.h" 13 #include "mozilla/Preferences.h" 14 #include "mozilla/StaticPrefs_dom.h" 15 #include "mozilla/TextEvents.h" 16 #include "mozilla/TimeStamp.h" 17 #include "mozilla/dom/UserActivation.h" 18 #include "nsIPermissionManager.h" 19 #include "nsXULPopupManager.h" 20 21 namespace mozilla::dom { 22 23 namespace { 24 25 static char* sPopupAllowedEvents; 26 27 static PopupBlocker::PopupControlState sPopupControlState = 28 PopupBlocker::openAbused; 29 static uint32_t sPopupStatePusherCount = 0; 30 31 static TimeStamp sLastAllowedExternalProtocolIFrameTimeStamp; 32 33 static uint32_t sOpenPopupSpamCount = 0; 34 35 void PopupAllowedEventsChanged() { 36 if (sPopupAllowedEvents) { 37 free(sPopupAllowedEvents); 38 } 39 40 nsAutoCString str; 41 Preferences::GetCString("dom.popup_allowed_events", str); 42 43 // We'll want to do this even if str is empty to avoid looking up 44 // this pref all the time if it's not set. 45 sPopupAllowedEvents = ToNewCString(str); 46 } 47 48 // return true if eventName is contained within events, delimited by 49 // spaces 50 bool PopupAllowedForEvent(const char* eventName) { 51 if (!sPopupAllowedEvents) { 52 PopupAllowedEventsChanged(); 53 54 if (!sPopupAllowedEvents) { 55 return false; 56 } 57 } 58 59 nsDependentCString events(sPopupAllowedEvents); 60 61 nsCString::const_iterator start, end; 62 nsCString::const_iterator startiter(events.BeginReading(start)); 63 events.EndReading(end); 64 65 while (startiter != end) { 66 nsCString::const_iterator enditer(end); 67 68 if (!FindInReadable(nsDependentCString(eventName), startiter, enditer)) 69 return false; 70 71 // the match is surrounded by spaces, or at a string boundary 72 if ((startiter == start || *--startiter == ' ') && 73 (enditer == end || *enditer == ' ')) { 74 return true; 75 } 76 77 // Move on and see if there are other matches. (The delimitation 78 // requirement makes it pointless to begin the next search before 79 // the end of the invalid match just found.) 80 startiter = enditer; 81 } 82 83 return false; 84 } 85 86 // static 87 void OnPrefChange(const char* aPrefName, void*) { 88 nsDependentCString prefName(aPrefName); 89 if (prefName.EqualsLiteral("dom.popup_allowed_events")) { 90 PopupAllowedEventsChanged(); 91 } 92 } 93 94 } // namespace 95 96 /* static */ 97 PopupBlocker::PopupControlState PopupBlocker::PushPopupControlState( 98 PopupBlocker::PopupControlState aState, bool aForce) { 99 MOZ_ASSERT(NS_IsMainThread()); 100 PopupBlocker::PopupControlState old = sPopupControlState; 101 if (aState < old || aForce) { 102 sPopupControlState = aState; 103 } 104 return old; 105 } 106 107 /* static */ 108 void PopupBlocker::PopPopupControlState( 109 PopupBlocker::PopupControlState aState) { 110 MOZ_ASSERT(NS_IsMainThread()); 111 sPopupControlState = aState; 112 } 113 114 /* static */ PopupBlocker::PopupControlState 115 PopupBlocker::GetPopupControlState() { 116 return sPopupControlState; 117 } 118 119 /* static */ 120 uint32_t PopupBlocker::GetPopupPermission(nsIPrincipal* aPrincipal) { 121 uint32_t permit = nsIPermissionManager::UNKNOWN_ACTION; 122 nsCOMPtr<nsIPermissionManager> permissionManager = 123 components::PermissionManager::Service(); 124 125 if (permissionManager) { 126 permissionManager->TestPermissionFromPrincipal(aPrincipal, "popup"_ns, 127 &permit); 128 } 129 130 return permit; 131 } 132 133 /* static */ 134 void PopupBlocker::PopupStatePusherCreated() { ++sPopupStatePusherCount; } 135 136 /* static */ 137 void PopupBlocker::PopupStatePusherDestroyed() { 138 MOZ_ASSERT(sPopupStatePusherCount); 139 --sPopupStatePusherCount; 140 } 141 142 // static 143 PopupBlocker::PopupControlState PopupBlocker::GetEventPopupControlState( 144 WidgetEvent* aEvent, Event* aDOMEvent) { 145 // generally if an event handler is running, new windows are disallowed. 146 // check for exceptions: 147 PopupBlocker::PopupControlState abuse = PopupBlocker::openBlocked; 148 149 if (aDOMEvent && aDOMEvent->GetWantsPopupControlCheck()) { 150 nsAutoString type; 151 aDOMEvent->GetType(type); 152 if (PopupAllowedForEvent(NS_ConvertUTF16toUTF8(type).get())) { 153 return PopupBlocker::openAllowed; 154 } 155 } 156 157 switch (aEvent->mClass) { 158 case eBasicEventClass: 159 // For these following events only allow popups if they're 160 // triggered while handling user input. See 161 // UserActivation::IsUserInteractionEvent() for details. 162 if (UserActivation::IsHandlingUserInput()) { 163 switch (aEvent->mMessage) { 164 case eFormSelect: 165 if (PopupAllowedForEvent("select")) { 166 abuse = PopupBlocker::openControlled; 167 } 168 break; 169 case eFormChange: 170 if (PopupAllowedForEvent("change")) { 171 abuse = PopupBlocker::openControlled; 172 } 173 break; 174 default: 175 break; 176 } 177 } 178 break; 179 case eEditorInputEventClass: 180 // For this following event only allow popups if it's triggered 181 // while handling user input. See 182 // UserActivation::IsUserInteractionEvent() for details. 183 if (UserActivation::IsHandlingUserInput()) { 184 switch (aEvent->mMessage) { 185 case eEditorInput: 186 if (PopupAllowedForEvent("input")) { 187 abuse = PopupBlocker::openControlled; 188 } 189 break; 190 default: 191 break; 192 } 193 } 194 break; 195 case eInputEventClass: 196 // For this following event only allow popups if it's triggered 197 // while handling user input. See 198 // UserActivation::IsUserInteractionEvent() for details. 199 if (UserActivation::IsHandlingUserInput()) { 200 switch (aEvent->mMessage) { 201 case eFormChange: 202 if (PopupAllowedForEvent("change")) { 203 abuse = PopupBlocker::openControlled; 204 } 205 break; 206 case eUnidentifiedEvent: 207 if (aEvent->mSpecifiedEventType == nsGkAtoms::oncommand) { 208 abuse = PopupBlocker::openControlled; 209 } 210 break; 211 default: 212 break; 213 } 214 } 215 break; 216 case eKeyboardEventClass: 217 if (aEvent->IsTrusted()) { 218 uint32_t key = aEvent->AsKeyboardEvent()->mKeyCode; 219 switch (aEvent->mMessage) { 220 case eKeyPress: 221 // return key on focused button. see note at ePointerClick. 222 if (key == NS_VK_RETURN) { 223 abuse = PopupBlocker::openAllowed; 224 } else if (PopupAllowedForEvent("keypress")) { 225 abuse = PopupBlocker::openControlled; 226 } 227 break; 228 case eKeyUp: 229 // space key on focused button. see note at ePointerClick. 230 if (key == NS_VK_SPACE) { 231 abuse = PopupBlocker::openAllowed; 232 } else if (PopupAllowedForEvent("keyup")) { 233 abuse = PopupBlocker::openControlled; 234 } 235 break; 236 case eKeyDown: 237 if (PopupAllowedForEvent("keydown")) { 238 abuse = PopupBlocker::openControlled; 239 } 240 break; 241 default: 242 break; 243 } 244 } 245 break; 246 case eTouchEventClass: 247 if (aEvent->IsTrusted()) { 248 switch (aEvent->mMessage) { 249 case eTouchStart: 250 if (PopupAllowedForEvent("touchstart")) { 251 abuse = PopupBlocker::openControlled; 252 } 253 break; 254 case eTouchEnd: 255 if (PopupAllowedForEvent("touchend")) { 256 abuse = PopupBlocker::openControlled; 257 } 258 break; 259 default: 260 break; 261 } 262 } 263 break; 264 case eMouseEventClass: 265 if (aEvent->IsTrusted()) { 266 // Let's ignore MouseButton::eSecondary because that is handled as 267 // context menu. 268 if (aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary || 269 aEvent->AsMouseEvent()->mButton == MouseButton::eMiddle) { 270 switch (aEvent->mMessage) { 271 case eMouseUp: 272 if (PopupAllowedForEvent("mouseup")) { 273 abuse = PopupBlocker::openControlled; 274 } 275 break; 276 case eMouseDown: 277 if (PopupAllowedForEvent("mousedown")) { 278 abuse = PopupBlocker::openControlled; 279 } 280 break; 281 case eMouseDoubleClick: 282 if (PopupAllowedForEvent("dblclick")) { 283 abuse = PopupBlocker::openControlled; 284 } 285 break; 286 default: 287 break; 288 } 289 } 290 } 291 break; 292 case ePointerEventClass: 293 if (aEvent->IsTrusted()) { 294 if ((aEvent->AsPointerEvent()->mButton == MouseButton::ePrimary || 295 aEvent->AsPointerEvent()->mButton == MouseButton::eMiddle)) { 296 switch (aEvent->mMessage) { 297 case ePointerClick: 298 /* Click events get special treatment because of their 299 historical status as a more legitimate event handler. If 300 click popups are enabled in the prefs, clear the popup 301 status completely. */ 302 if (PopupAllowedForEvent("click")) { 303 abuse = PopupBlocker::openAllowed; 304 } 305 break; 306 case ePointerUp: 307 if (PopupAllowedForEvent("pointerup")) { 308 abuse = PopupBlocker::openControlled; 309 } 310 break; 311 case ePointerDown: 312 if (PopupAllowedForEvent("pointerdown")) { 313 abuse = PopupBlocker::openControlled; 314 } 315 break; 316 default: 317 break; 318 } 319 } 320 // XXX Why don't we handle auxclick if the button is the middle button? 321 else if (aEvent->mMessage == ePointerAuxClick) { 322 // Not MouseButton::ePrimary: 323 // There's not a strong reason to ignore other events (eg eMouseUp) 324 // for non-primary clicks as far as we know, so we could add them if 325 // it becomes a compat issue 326 if (PopupAllowedForEvent("auxclick")) { 327 abuse = PopupBlocker::openControlled; 328 } 329 } 330 331 // XXX However, `contextmenu` event is handled even if the button is the 332 // primary button which may occur if the context menu is opened by 333 // keyboard. 334 if (aEvent->mMessage == eContextMenu) { 335 if (PopupAllowedForEvent("contextmenu")) { 336 abuse = PopupBlocker::openControlled; 337 } 338 } 339 } 340 break; 341 case eFormEventClass: 342 // For these following events only allow popups if they're 343 // triggered while handling user input. See 344 // UserActivation::IsUserInteractionEvent() for details. 345 if (UserActivation::IsHandlingUserInput()) { 346 switch (aEvent->mMessage) { 347 case eFormSubmit: 348 if (PopupAllowedForEvent("submit")) { 349 abuse = PopupBlocker::openControlled; 350 } 351 break; 352 case eFormReset: 353 if (PopupAllowedForEvent("reset")) { 354 abuse = PopupBlocker::openControlled; 355 } 356 break; 357 default: 358 break; 359 } 360 } 361 break; 362 default: 363 break; 364 } 365 366 return abuse; 367 } 368 369 /* static */ 370 void PopupBlocker::Initialize() { 371 DebugOnly<nsresult> rv = 372 Preferences::RegisterCallback(OnPrefChange, "dom.popup_allowed_events"); 373 MOZ_ASSERT(NS_SUCCEEDED(rv), 374 "Failed to observe \"dom.popup_allowed_events\""); 375 } 376 377 /* static */ 378 void PopupBlocker::Shutdown() { 379 MOZ_ASSERT(sOpenPopupSpamCount == 0); 380 381 if (sPopupAllowedEvents) { 382 free(sPopupAllowedEvents); 383 } 384 385 Preferences::UnregisterCallback(OnPrefChange, "dom.popup_allowed_events"); 386 } 387 388 /* static */ 389 bool PopupBlocker::ConsumeTimerTokenForExternalProtocolIframe() { 390 if (!StaticPrefs::dom_delay_block_external_protocol_in_iframes_enabled()) { 391 return false; 392 } 393 394 TimeStamp now = TimeStamp::Now(); 395 396 if (sLastAllowedExternalProtocolIFrameTimeStamp.IsNull()) { 397 sLastAllowedExternalProtocolIFrameTimeStamp = now; 398 return true; 399 } 400 401 if ((now - sLastAllowedExternalProtocolIFrameTimeStamp).ToSeconds() < 402 StaticPrefs::dom_delay_block_external_protocol_in_iframes()) { 403 return false; 404 } 405 406 sLastAllowedExternalProtocolIFrameTimeStamp = now; 407 return true; 408 } 409 410 /* static */ 411 TimeStamp PopupBlocker::WhenLastExternalProtocolIframeAllowed() { 412 return sLastAllowedExternalProtocolIFrameTimeStamp; 413 } 414 415 /* static */ 416 void PopupBlocker::ResetLastExternalProtocolIframeAllowed() { 417 sLastAllowedExternalProtocolIFrameTimeStamp = TimeStamp(); 418 } 419 420 /* static */ 421 void PopupBlocker::RegisterOpenPopupSpam() { sOpenPopupSpamCount++; } 422 423 /* static */ 424 void PopupBlocker::UnregisterOpenPopupSpam() { 425 MOZ_ASSERT(sOpenPopupSpamCount); 426 sOpenPopupSpamCount--; 427 } 428 429 /* static */ 430 uint32_t PopupBlocker::GetOpenPopupSpamCount() { return sOpenPopupSpamCount; } 431 432 } // namespace mozilla::dom 433 434 AutoPopupStatePusherInternal::AutoPopupStatePusherInternal( 435 mozilla::dom::PopupBlocker::PopupControlState aState, bool aForce) 436 : mOldState( 437 mozilla::dom::PopupBlocker::PushPopupControlState(aState, aForce)) { 438 mozilla::dom::PopupBlocker::PopupStatePusherCreated(); 439 } 440 441 AutoPopupStatePusherInternal::~AutoPopupStatePusherInternal() { 442 mozilla::dom::PopupBlocker::PopPopupControlState(mOldState); 443 mozilla::dom::PopupBlocker::PopupStatePusherDestroyed(); 444 }