NavigateEvent.cpp (17087B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ 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/NavigateEvent.h" 8 9 #include "mozilla/HoldDropJSObjects.h" 10 #include "mozilla/PresShell.h" 11 #include "mozilla/dom/AbortController.h" 12 #include "mozilla/dom/Document.h" 13 #include "mozilla/dom/ElementBinding.h" 14 #include "mozilla/dom/NavigateEventBinding.h" 15 #include "mozilla/dom/Navigation.h" 16 #include "mozilla/dom/NavigationHistoryEntry.h" 17 #include "mozilla/dom/SessionHistoryEntry.h" 18 #include "nsDocShell.h" 19 #include "nsFocusManager.h" 20 #include "nsGlobalWindowInner.h" 21 22 extern mozilla::LazyLogModule gNavigationAPILog; 23 24 #define LOG_FMTI(format, ...) \ 25 MOZ_LOG_FMT(gNavigationAPILog, LogLevel::Info, format, ##__VA_ARGS__); 26 27 #define LOG_FMT(format, ...) \ 28 MOZ_LOG_FMT(gNavigationAPILog, LogLevel::Debug, format, ##__VA_ARGS__); 29 30 namespace mozilla::dom { 31 32 NS_IMPL_CYCLE_COLLECTION_INHERITED_WITH_JS_MEMBERS( 33 NavigateEvent, Event, 34 (mDestination, mSignal, mFormData, mSourceElement, mNavigationHandlerList, 35 mAbortController, mNavigationPrecommitHandlerList), 36 (mInfo)) 37 38 NS_IMPL_ADDREF_INHERITED(NavigateEvent, Event) 39 NS_IMPL_RELEASE_INHERITED(NavigateEvent, Event) 40 41 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(NavigateEvent) 42 NS_INTERFACE_MAP_END_INHERITING(Event) 43 44 JSObject* NavigateEvent::WrapObjectInternal(JSContext* aCx, 45 JS::Handle<JSObject*> aGivenProto) { 46 return NavigateEvent_Binding::Wrap(aCx, this, aGivenProto); 47 } 48 49 /* static */ 50 already_AddRefed<NavigateEvent> NavigateEvent::Constructor( 51 const GlobalObject& aGlobal, const nsAString& aType, 52 const NavigateEventInit& aEventInitDict) { 53 nsCOMPtr<mozilla::dom::EventTarget> eventTarget = 54 do_QueryInterface(aGlobal.GetAsSupports()); 55 return Constructor(eventTarget, aType, aEventInitDict); 56 } 57 58 /* static */ 59 already_AddRefed<NavigateEvent> NavigateEvent::Constructor( 60 EventTarget* aEventTarget, const nsAString& aType, 61 const NavigateEventInit& aEventInitDict) { 62 RefPtr<NavigateEvent> event = new NavigateEvent(aEventTarget); 63 bool trusted = event->Init(aEventTarget); 64 event->InitEvent( 65 aType, aEventInitDict.mBubbles ? CanBubble::eYes : CanBubble::eNo, 66 aEventInitDict.mCancelable ? Cancelable::eYes : Cancelable::eNo, 67 aEventInitDict.mComposed ? Composed::eYes : Composed::eNo); 68 event->InitNavigateEvent(aEventInitDict); 69 event->SetTrusted(trusted); 70 return event.forget(); 71 } 72 73 /* static */ 74 already_AddRefed<NavigateEvent> NavigateEvent::Constructor( 75 EventTarget* aEventTarget, const nsAString& aType, 76 const NavigateEventInit& aEventInitDict, 77 nsIStructuredCloneContainer* aClassicHistoryAPIState, 78 class AbortController* aAbortController) { 79 RefPtr<NavigateEvent> event = 80 Constructor(aEventTarget, aType, aEventInitDict); 81 82 event->mAbortController = aAbortController; 83 MOZ_DIAGNOSTIC_ASSERT(event->mSignal == aAbortController->Signal()); 84 85 event->mClassicHistoryAPIState = aClassicHistoryAPIState; 86 87 return event.forget(); 88 } 89 90 NavigationType NavigateEvent::NavigationType() const { return mNavigationType; } 91 92 void NavigateEvent::SetNavigationType(enum NavigationType aNavigationType) { 93 mNavigationType = aNavigationType; 94 } 95 96 already_AddRefed<NavigationDestination> NavigateEvent::Destination() const { 97 return do_AddRef(mDestination); 98 } 99 100 bool NavigateEvent::CanIntercept() const { return mCanIntercept; } 101 102 bool NavigateEvent::UserInitiated() const { return mUserInitiated; } 103 104 bool NavigateEvent::HashChange() const { return mHashChange; } 105 106 AbortSignal* NavigateEvent::Signal() const { return mSignal; } 107 108 already_AddRefed<FormData> NavigateEvent::GetFormData() const { 109 return do_AddRef(mFormData); 110 } 111 112 void NavigateEvent::GetDownloadRequest(nsAString& aDownloadRequest) const { 113 aDownloadRequest = mDownloadRequest; 114 } 115 116 void NavigateEvent::GetInfo(JSContext* aCx, 117 JS::MutableHandle<JS::Value> aInfo) const { 118 aInfo.set(mInfo); 119 } 120 121 bool NavigateEvent::HasUAVisualTransition() const { 122 return mHasUAVisualTransition; 123 } 124 125 Element* NavigateEvent::GetSourceElement() const { return mSourceElement; } 126 127 template <typename OptionEnum> 128 static void MaybeReportWarningToConsole(Document* aDocument, 129 const nsString& aOption, 130 OptionEnum aPrevious, OptionEnum aNew) { 131 if (!aDocument) { 132 return; 133 } 134 135 nsTArray<nsString> params = {aOption, 136 NS_ConvertUTF8toUTF16(GetEnumString(aNew)), 137 NS_ConvertUTF8toUTF16(GetEnumString(aPrevious))}; 138 nsContentUtils::ReportToConsole( 139 nsIScriptError::warningFlag, "DOM"_ns, aDocument, 140 nsContentUtils::eDOM_PROPERTIES, 141 "PreviousInterceptCallOptionOverriddenWarning", params); 142 } 143 144 // https://html.spec.whatwg.org/#dom-navigateevent-intercept 145 void NavigateEvent::Intercept(const NavigationInterceptOptions& aOptions, 146 ErrorResult& aRv) { 147 LOG_FMTI("Called NavigateEvent.intercept()"); 148 149 // Step 1 150 if (PerformSharedChecks(aRv); aRv.Failed()) { 151 return; 152 } 153 154 // Step 2 155 if (!mCanIntercept) { 156 aRv.ThrowSecurityError("Event's canIntercept was initialized to false"); 157 return; 158 } 159 160 // Step 3 161 if (!IsBeingDispatched()) { 162 aRv.ThrowInvalidStateError("Event has never been dispatched"); 163 return; 164 } 165 166 // Step 4 167 if (aOptions.mPrecommitHandler.WasPassed()) { 168 // Step 4.1 169 if (!Cancelable()) { 170 aRv.ThrowInvalidStateError("Event is not cancelable"); 171 return; 172 } 173 174 // Step 4.2 175 mNavigationPrecommitHandlerList.AppendElement( 176 aOptions.mPrecommitHandler.InternalValue().get()); 177 } 178 179 // Step 5 180 MOZ_DIAGNOSTIC_ASSERT(mInterceptionState == InterceptionState::None || 181 mInterceptionState == InterceptionState::Intercepted); 182 183 // Step 6 184 mInterceptionState = InterceptionState::Intercepted; 185 186 // Step 7 187 if (aOptions.mHandler.WasPassed()) { 188 mNavigationHandlerList.AppendElement( 189 aOptions.mHandler.InternalValue().get()); 190 } 191 192 // Step 8 193 if (aOptions.mFocusReset.WasPassed()) { 194 // Step 8.1 195 if (mFocusResetBehavior && 196 *mFocusResetBehavior != aOptions.mFocusReset.Value()) { 197 RefPtr<Document> document = GetAssociatedDocument(); 198 MaybeReportWarningToConsole(document, u"focusReset"_ns, 199 *mFocusResetBehavior, 200 aOptions.mFocusReset.Value()); 201 } 202 203 // Step 8.2 204 mFocusResetBehavior = Some(aOptions.mFocusReset.Value()); 205 } 206 207 // Step 9 208 if (aOptions.mScroll.WasPassed()) { 209 // Step 9.1 210 if (mScrollBehavior && *mScrollBehavior != aOptions.mScroll.Value()) { 211 RefPtr<Document> document = GetAssociatedDocument(); 212 MaybeReportWarningToConsole(document, u"scroll"_ns, *mScrollBehavior, 213 aOptions.mScroll.Value()); 214 } 215 216 // Step 9.2 217 mScrollBehavior.emplace(aOptions.mScroll.Value()); 218 } 219 } 220 221 // https://html.spec.whatwg.org/#dom-navigateevent-scroll 222 void NavigateEvent::Scroll(ErrorResult& aRv) { 223 LOG_FMTI("Called NavigateEvent.scroll()"); 224 225 // Step 1 226 if (PerformSharedChecks(aRv); aRv.Failed()) { 227 return; 228 } 229 230 // Step 2 231 if (mInterceptionState != InterceptionState::Committed) { 232 aRv.ThrowInvalidStateError("NavigateEvent was not committed"); 233 return; 234 } 235 236 // Step 3 237 ProcessScrollBehavior(); 238 } 239 240 NavigateEvent::NavigateEvent(EventTarget* aOwner) 241 : Event(aOwner, nullptr, nullptr) { 242 mozilla::HoldJSObjects(this); 243 } 244 245 NavigateEvent::~NavigateEvent() { DropJSObjects(this); } 246 247 void NavigateEvent::InitNavigateEvent(const NavigateEventInit& aEventInitDict) { 248 mNavigationType = aEventInitDict.mNavigationType; 249 mDestination = aEventInitDict.mDestination; 250 mCanIntercept = aEventInitDict.mCanIntercept; 251 mUserInitiated = aEventInitDict.mUserInitiated; 252 mHashChange = aEventInitDict.mHashChange; 253 mSignal = aEventInitDict.mSignal; 254 mFormData = aEventInitDict.mFormData; 255 mDownloadRequest = aEventInitDict.mDownloadRequest; 256 mInfo = aEventInitDict.mInfo; 257 mHasUAVisualTransition = aEventInitDict.mHasUAVisualTransition; 258 mSourceElement = aEventInitDict.mSourceElement; 259 if (RefPtr document = GetAssociatedDocument()) { 260 mLastScrollGeneration = document->LastScrollGeneration(); 261 } 262 } 263 264 void NavigateEvent::SetCanIntercept(bool aCanIntercept) { 265 mCanIntercept = aCanIntercept; 266 } 267 268 enum NavigateEvent::InterceptionState NavigateEvent::InterceptionState() const { 269 return mInterceptionState; 270 } 271 272 void NavigateEvent::SetInterceptionState( 273 enum InterceptionState aInterceptionState) { 274 mInterceptionState = aInterceptionState; 275 } 276 277 nsIStructuredCloneContainer* NavigateEvent::ClassicHistoryAPIState() const { 278 return mClassicHistoryAPIState; 279 } 280 281 nsTArray<RefPtr<NavigationInterceptHandler>>& 282 NavigateEvent::NavigationHandlerList() { 283 return mNavigationHandlerList; 284 } 285 286 AbortController* NavigateEvent::AbortController() const { 287 return mAbortController; 288 } 289 290 bool NavigateEvent::IsBeingDispatched() const { 291 return mEvent->mFlags.mIsBeingDispatched; 292 } 293 294 // https://html.spec.whatwg.org/#navigateevent-finish 295 void NavigateEvent::Finish(bool aDidFulfill) { 296 // Step 1 297 MOZ_DIAGNOSTIC_ASSERT(mInterceptionState != InterceptionState::Finished); 298 299 // Step 2 300 if (mInterceptionState == InterceptionState::Intercepted) { 301 // Step 2.1 302 MOZ_DIAGNOSTIC_ASSERT(!aDidFulfill); 303 304 // Step 2.2 305 MOZ_DIAGNOSTIC_ASSERT(!mNavigationPrecommitHandlerList.IsEmpty()); 306 307 // Step 2.3 308 mInterceptionState = InterceptionState::Finished; 309 310 // Step 2.4 311 return; 312 } 313 314 // Step 3 315 if (mInterceptionState == InterceptionState::None) { 316 return; 317 } 318 319 // Step 4 320 PotentiallyResetFocus(); 321 322 // Step 5 323 if (aDidFulfill) { 324 PotentiallyProcessScrollBehavior(); 325 } 326 327 // Step 6 328 mInterceptionState = InterceptionState::Finished; 329 } 330 331 // https://html.spec.whatwg.org/#navigateevent-perform-shared-checks 332 void NavigateEvent::PerformSharedChecks(ErrorResult& aRv) { 333 // Step 1 334 if (RefPtr document = GetAssociatedDocument(); 335 !document || !document->IsFullyActive()) { 336 aRv.ThrowInvalidStateError("Document isn't fully active"); 337 return; 338 } 339 340 // Step 2 341 if (!IsTrusted()) { 342 aRv.ThrowSecurityError("Event is untrusted"); 343 return; 344 } 345 346 // Step 3 347 if (DefaultPrevented()) { 348 aRv.ThrowInvalidStateError("Event was canceled"); 349 } 350 } 351 352 // https://html.spec.whatwg.org/#potentially-reset-the-focus 353 void NavigateEvent::PotentiallyResetFocus() { 354 // Step 1 355 MOZ_DIAGNOSTIC_ASSERT(mInterceptionState == InterceptionState::Committed || 356 mInterceptionState == InterceptionState::Scrolled); 357 358 // Step 2 359 nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(GetParentObject()); 360 361 // If we don't have a window here, there's not much we can do. This could 362 // potentially happen in a chrome context, and in the end it's just better to 363 // be sure and null check. 364 if (NS_WARN_IF(!window)) { 365 return; 366 } 367 368 Navigation* navigation = window->Navigation(); 369 370 // Step 3 371 bool focusChanged = navigation->FocusedChangedDuringOngoingNavigation(); 372 373 // Step 4 374 navigation->SetFocusedChangedDuringOngoingNavigation(false); 375 376 // Step 5 377 if (focusChanged) { 378 return; 379 } 380 381 // Step 6 382 if (mFocusResetBehavior && 383 *mFocusResetBehavior == NavigationFocusReset::Manual) { 384 return; 385 } 386 387 // Step 7 388 Document* document = window->GetExtantDoc(); 389 390 // If we don't have a document here, there's not much we can do. 391 if (NS_WARN_IF(!document)) { 392 return; 393 } 394 395 // Step 8 396 RefPtr<Element> focusTarget = document->GetDocumentElement(); 397 if (focusTarget) { 398 focusTarget = 399 focusTarget->GetAutofocusDelegate(mozilla::IsFocusableFlags(0)); 400 } 401 402 // Step 9 403 if (!focusTarget) { 404 focusTarget = document->GetBody(); 405 } 406 407 // Step 10 408 if (!focusTarget) { 409 focusTarget = document->GetDocumentElement(); 410 } 411 412 // Step 11, step 12 413 FocusOptions options; 414 options.mPreventScroll = true; 415 focusTarget = nsFocusManager::GetTheFocusableArea( 416 focusTarget, nsFocusManager::ProgrammaticFocusFlags(options)); 417 418 if (focusTarget) { 419 LOG_FMT("Reset focus to {}", *focusTarget->AsNode()); 420 focusTarget->Focus(options, CallerType::NonSystem, IgnoredErrorResult()); 421 } else if (RefPtr<nsIFocusManager> focusManager = 422 nsFocusManager::GetFocusManager()) { 423 if (nsPIDOMWindowOuter* window = document->GetWindow()) { 424 // Now focus the document itself if focus is on an element within it. 425 nsCOMPtr<mozIDOMWindowProxy> focusedWindow; 426 focusManager->GetFocusedWindow(getter_AddRefs(focusedWindow)); 427 if (SameCOMIdentity(window, focusedWindow)) { 428 LOG_FMT("Reset focus to document viewport"); 429 focusManager->ClearFocus(focusedWindow); 430 } 431 } 432 } 433 } 434 435 // https://html.spec.whatwg.org/#potentially-process-scroll-behavior 436 void NavigateEvent::PotentiallyProcessScrollBehavior() { 437 // Step 1 438 MOZ_DIAGNOSTIC_ASSERT(mInterceptionState == InterceptionState::Committed || 439 mInterceptionState == InterceptionState::Scrolled); 440 441 // Step 2 442 if (mInterceptionState == InterceptionState::Scrolled) { 443 return; 444 } 445 446 // Step 3 447 if (mScrollBehavior && *mScrollBehavior == NavigationScrollBehavior::Manual) { 448 return; 449 } 450 451 // Process 4 452 ProcessScrollBehavior(); 453 } 454 455 // Here we want to scroll to the beginning of the document, as described in 456 // https://drafts.csswg.org/cssom-view/#scroll-to-the-beginning-of-the-document 457 MOZ_CAN_RUN_SCRIPT 458 static void ScrollToBeginningOfDocument(Document& aDocument) { 459 RefPtr<PresShell> presShell = aDocument.GetPresShell(); 460 if (!presShell) { 461 return; 462 } 463 464 RefPtr<Element> rootElement = aDocument.GetRootElement(); 465 ScrollAxis vertical(WhereToScroll::Start, WhenToScroll::Always); 466 presShell->ScrollContentIntoView(rootElement, vertical, ScrollAxis(), 467 ScrollFlags::TriggeredByScript); 468 } 469 470 // https://html.spec.whatwg.org/#restore-scroll-position-data 471 static void RestoreScrollPositionData(Document* aDocument, 472 const uint32_t& aLastScrollGeneration, 473 SessionHistoryInfo* aHistoryEntry) { 474 // 1. Let document be entry's document. 475 // 2. If document's has been scrolled by the user is true, then the user agent 476 // should return. 477 if (!aDocument || aDocument->HasBeenScrolledSince(aLastScrollGeneration)) { 478 return; 479 } 480 481 RefPtr<nsDocShell> docShell = nsDocShell::Cast(aDocument->GetDocShell()); 482 if (!docShell) { 483 return; 484 } 485 486 // 3. The user agent should attempt to use entry's scroll position data to 487 // restore the scroll positions of entry's document's restorable scrollable 488 // regions. The user agent may continue to attempt to do so periodically, 489 // until document's has been scrolled by the user becomes true. 490 docShell->RestoreScrollPositionFromTargetSessionHistoryInfo(aHistoryEntry); 491 } 492 493 // https://html.spec.whatwg.org/#process-scroll-behavior 494 void NavigateEvent::ProcessScrollBehavior() { 495 // Step 1 496 MOZ_DIAGNOSTIC_ASSERT(mInterceptionState == InterceptionState::Committed); 497 498 // Step 2 499 mInterceptionState = InterceptionState::Scrolled; 500 501 // Step 3 502 if (mNavigationType == NavigationType::Traverse || 503 mNavigationType == NavigationType::Reload) { 504 RefPtr<Document> document = GetAssociatedDocument(); 505 // SHIP changes the active entry in 506 // `nsDocShell::HandleSameDocumentNavigation`, which breaks with Navigation 507 // API spec steps as it's too late, and at this point, the actual "active 508 // session history entry" will become the target session history entry 509 // provided here, which is why we're using this instead of 510 // nsDocShell::mActiveEntry 511 RestoreScrollPositionData( 512 document, mLastScrollGeneration, 513 mDestination->GetEntry() 514 ? mDestination->GetEntry()->SessionHistoryInfo() 515 : nullptr); 516 return; 517 } 518 519 // Step 4.1 520 RefPtr<Document> document = GetAssociatedDocument(); 521 // If there is no document there's not much to do. 522 if (!document) { 523 return; 524 } 525 526 // Step 4.2 527 nsAutoCString ref; 528 if (nsIURI* uri = document->GetDocumentURI(); 529 NS_SUCCEEDED(uri->GetRef(ref)) && 530 !nsContentUtils::GetTargetElement(document, NS_ConvertUTF8toUTF16(ref))) { 531 ScrollToBeginningOfDocument(*document); 532 return; 533 } 534 535 // Step 4.3 536 // Here we need to update Document::mScrollToRef, since that is what 537 // Document::ScrollToRef will be scrolling to. 538 document->SetScrollToRef(document->GetDocumentURI()); 539 document->ScrollToRef(); 540 } 541 542 Document* NavigateEvent::GetAssociatedDocument() const { 543 if (nsCOMPtr<nsPIDOMWindowInner> globalWindow = 544 do_QueryInterface(GetParentObject())) { 545 return globalWindow->GetExtantDoc(); 546 } 547 return nullptr; 548 } 549 550 } // namespace mozilla::dom 551 552 #undef LOG_FMTI 553 #undef LOG_FMT