| /* |
| * Copyright (C) 2023 Igalia S.L. All rights reserved. |
| * Copyright (C) 2024-2025 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "Navigation.h" |
| |
| #include "AbortController.h" |
| #include "BackForwardController.h" |
| #include "CallbackResult.h" |
| #include "CommonVM.h" |
| #include "DOMFormData.h" |
| #include "DocumentInlines.h" |
| #include "DocumentLoader.h" |
| #include "ErrorEvent.h" |
| #include "EventNames.h" |
| #include "EventTargetInterfaces.h" |
| #include "Exception.h" |
| #include "ExceptionOr.h" |
| #include "FormState.h" |
| #include "FrameLoadRequest.h" |
| #include "FrameLoader.h" |
| #include "HTMLFormControlElement.h" |
| #include "HTMLFormElement.h" |
| #include "HistoryController.h" |
| #include "HistoryItem.h" |
| #include "JSDOMGlobalObject.h" |
| #include "JSDOMPromise.h" |
| #include "JSNavigationHistoryEntry.h" |
| #include "MessagePort.h" |
| #include "NavigateEvent.h" |
| #include "NavigationActivation.h" |
| #include "NavigationCurrentEntryChangeEvent.h" |
| #include "NavigationDestination.h" |
| #include "NavigationHistoryEntry.h" |
| #include "NavigationNavigationType.h" |
| #include "NavigationScheduler.h" |
| #include "ScriptExecutionContextInlines.h" |
| #include "SecurityOrigin.h" |
| #include "SerializedScriptValue.h" |
| #include "ShouldTreatAsContinuingLoad.h" |
| #include "UserGestureIndicator.h" |
| #include <optional> |
| #include <wtf/Assertions.h> |
| #include <wtf/TZoneMallocInlines.h> |
| |
| namespace WebCore { |
| |
| WTF_MAKE_TZONE_OR_ISO_ALLOCATED_IMPL(Navigation); |
| |
| Navigation::Navigation(LocalDOMWindow& window) |
| : LocalDOMWindowProperty(&window) |
| { |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-cangoback |
| bool Navigation::canGoBack() const |
| { |
| if (hasEntriesAndEventsDisabled()) |
| return false; |
| ASSERT(m_currentEntryIndex); |
| if (!m_currentEntryIndex || !*m_currentEntryIndex) |
| return false; |
| return true; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-cangoforward |
| bool Navigation::canGoForward() const |
| { |
| if (hasEntriesAndEventsDisabled()) |
| return false; |
| ASSERT(m_currentEntryIndex); |
| if (!m_currentEntryIndex || *m_currentEntryIndex == m_entries.size() - 1) |
| return false; |
| return true; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#getting-the-navigation-api-entry-index |
| static std::optional<size_t> getEntryIndexOfHistoryItem(const Vector<Ref<NavigationHistoryEntry>>& entries, const HistoryItem& item, size_t start = 0) |
| { |
| // FIXME: We could have a more efficient solution than iterating through a list. |
| for (size_t index = start; index < entries.size(); index++) { |
| if (entries[index]->associatedHistoryItem().itemSequenceNumber() == item.itemSequenceNumber()) |
| return index; |
| } |
| |
| return std::nullopt; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#initialize-the-navigation-api-entries-for-a-new-document |
| void Navigation::initializeForNewWindow(std::optional<NavigationNavigationType> navigationType, LocalDOMWindow* previousWindow) |
| { |
| ASSERT(m_entries.isEmpty()); |
| ASSERT(!m_currentEntryIndex); |
| |
| if (hasEntriesAndEventsDisabled()) |
| return; |
| |
| RefPtr page = frame()->page(); |
| if (!page) |
| return; |
| |
| RefPtr currentItem = frame()->loader().history().currentItem(); |
| if (!currentItem) |
| return; |
| // For main frames we can still rely on the page b/f list. However for subframes we need below logic to not lose the bookkeeping done in the previous window. |
| if (previousWindow && !frame()->isMainFrame()) { |
| Ref previousNavigation = previousWindow->navigation(); |
| |
| bool shouldProcessPreviousNavigationEntries = [&]() { |
| if (!previousNavigation->m_currentEntryIndex) |
| return false; |
| |
| if (!previousNavigation->m_entries.size()) |
| return false; |
| |
| if (!frame()->protectedDocument()->protectedSecurityOrigin()->isSameOriginAs(previousWindow->protectedDocument()->protectedSecurityOrigin())) |
| return false; |
| |
| return true; |
| }(); |
| |
| if (shouldProcessPreviousNavigationEntries) { |
| for (auto& entry : previousNavigation->m_entries) |
| m_entries.append(NavigationHistoryEntry::create(*this, entry.get())); |
| |
| RELEASE_ASSERT(m_entries.size() > previousNavigation->m_currentEntryIndex); |
| |
| if (navigationType == NavigationNavigationType::Traverse) { |
| m_currentEntryIndex = getEntryIndexOfHistoryItem(m_entries, *currentItem); |
| if (m_currentEntryIndex) { |
| updateForActivation(frame()->loader().history().protectedPreviousItem().get(), navigationType); |
| return; |
| } |
| // We are doing a cross document subframe traversal, we can't rely on previous window, so clear |
| // m_entries and fall back to the normal algorithm for new windows. |
| m_entries = { }; |
| } else if (navigationType == NavigationNavigationType::Push) |
| m_entries.shrink(*previousNavigation->m_currentEntryIndex + 1); // Prune forward entries. |
| else { |
| auto previousEntry = m_entries[*previousNavigation->m_currentEntryIndex]; |
| |
| if (navigationType == NavigationNavigationType::Replace) |
| m_entries[*previousNavigation->m_currentEntryIndex] = NavigationHistoryEntry::create(*this, *currentItem); |
| |
| m_currentEntryIndex = getEntryIndexOfHistoryItem(m_entries, *currentItem); |
| |
| if (navigationType) // Unset in the case of forms/POST requests. |
| m_activation = NavigationActivation::create(*navigationType, *currentEntry(), WTFMove(previousEntry)); |
| |
| return; |
| } |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-session-history-entries-for-the-navigation-api |
| Vector<Ref<HistoryItem>> items; |
| auto rawEntries = page->backForward().itemsForFrame(frame()->frameID()); |
| auto startingIndex = rawEntries.find(*currentItem); |
| if (startingIndex != notFound) { |
| Ref startingOrigin = SecurityOrigin::create(Ref { rawEntries[startingIndex] }->url()); |
| |
| for (size_t i = 0; i < startingIndex; i++) { |
| Ref item = rawEntries[i]; |
| |
| if (!SecurityOrigin::create(item->url())->isSameOriginAs(startingOrigin)) |
| break; |
| items.append(WTFMove(item)); |
| } |
| |
| items.append(*currentItem); |
| |
| for (size_t i = startingIndex + 1; i < rawEntries.size(); i++) { |
| Ref item = rawEntries[i]; |
| if (!SecurityOrigin::create(item->url())->isSameOriginAs(startingOrigin)) |
| break; |
| items.append(WTFMove(item)); |
| } |
| } else |
| items.append(*currentItem); |
| |
| size_t start = m_entries.size(); |
| |
| for (Ref item : items) |
| m_entries.append(NavigationHistoryEntry::create(*this, WTFMove(item))); |
| |
| m_currentEntryIndex = getEntryIndexOfHistoryItem(m_entries, *currentItem, start); |
| updateForActivation(frame()->loader().history().protectedPreviousItem().get(), navigationType); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-activation |
| void Navigation::updateForActivation(HistoryItem* previousItem, std::optional<NavigationNavigationType> type) |
| { |
| ASSERT(!m_activation); |
| if (hasEntriesAndEventsDisabled() || !type) |
| return; |
| |
| ASSERT(m_currentEntryIndex); |
| if (currentEntry()->associatedHistoryItem().url().isAboutBlank()) |
| return; |
| |
| bool wasAboutBlank = previousItem && previousItem->url().isAboutBlank(); // FIXME: *Initial* about:blank |
| if (wasAboutBlank) // FIXME: For navigations on the initial about blank this should already be the type. |
| type = NavigationNavigationType::Replace; |
| |
| bool isSameOrigin = frame()->document() && previousItem && SecurityOrigin::create(previousItem->url())->isSameOriginAs(frame()->protectedDocument()->protectedSecurityOrigin()); |
| auto previousEntryIndex = previousItem ? getEntryIndexOfHistoryItem(m_entries, *previousItem) : std::nullopt; |
| |
| RefPtr<NavigationHistoryEntry> previousEntry = nullptr; |
| if (previousEntryIndex && isSameOrigin) |
| previousEntry = m_entries.at(previousEntryIndex.value()).ptr(); |
| if (type == NavigationNavigationType::Reload) |
| previousEntry = currentEntry(); |
| else if (type == NavigationNavigationType::Replace && (isSameOrigin || wasAboutBlank)) |
| previousEntry = NavigationHistoryEntry::create(*this, *previousItem); |
| |
| m_activation = NavigationActivation::create(*type, *currentEntry(), WTFMove(previousEntry)); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/browsing-the-web.html#fire-the-pageswap-event |
| RefPtr<NavigationActivation> Navigation::createForPageswapEvent(HistoryItem* newItem, DocumentLoader* documentLoader, bool fromBackForwardCache) |
| { |
| auto type = documentLoader->triggeringAction().navigationAPIType(); |
| if (!type || !frame()) |
| return nullptr; |
| |
| // Skip cross-origin requests, or if any cross-origin redirects have been made. |
| bool isSameOrigin = SecurityOrigin::create(documentLoader->documentURL())->isSameOriginAs(protectedWindow()->protectedDocument()->protectedSecurityOrigin()); |
| if (!isSameOrigin || (!documentLoader->request().isSameSite() && !fromBackForwardCache)) |
| return nullptr; |
| |
| RefPtr<NavigationHistoryEntry> oldEntry; |
| if (frame()->document() && frame()->document()->settings().navigationAPIEnabled()) |
| oldEntry = currentEntry(); |
| else if (RefPtr currentItem = frame()->loader().history().currentItem()) |
| oldEntry = NavigationHistoryEntry::create(*this, *currentItem); |
| |
| RefPtr<NavigationHistoryEntry> newEntry; |
| if (*type == NavigationNavigationType::Reload) { |
| newEntry = oldEntry; |
| } else if (*type == NavigationNavigationType::Traverse) { |
| ASSERT(newItem); |
| // FIXME: For a traverse navigation, we should be identifying the right existing history |
| // entry for 'newEntry' instead of allocating a new one. |
| if (newItem) |
| newEntry = NavigationHistoryEntry::create(*this, *newItem); |
| } else { |
| ASSERT(newItem); |
| if (newItem) |
| newEntry = NavigationHistoryEntry::create(*this, *newItem); |
| } |
| |
| if (newEntry) |
| return NavigationActivation::create(*type, newEntry.releaseNonNull(), WTFMove(oldEntry)); |
| return nullptr; |
| } |
| |
| const Vector<Ref<NavigationHistoryEntry>>& Navigation::entries() const |
| { |
| static NeverDestroyed<Vector<Ref<NavigationHistoryEntry>>> emptyEntries; |
| if (hasEntriesAndEventsDisabled()) |
| return emptyEntries; |
| return m_entries; |
| } |
| |
| NavigationHistoryEntry* Navigation::currentEntry() const |
| { |
| if (!hasEntriesAndEventsDisabled() && m_currentEntryIndex) |
| return m_entries.at(*m_currentEntryIndex).ptr(); |
| return nullptr; |
| } |
| |
| Navigation::~Navigation() = default; |
| |
| ScriptExecutionContext* Navigation::scriptExecutionContext() const |
| { |
| RefPtr window = this->window(); |
| return window ? window->document() : nullptr; |
| } |
| |
| RefPtr<ScriptExecutionContext> Navigation::protectedScriptExecutionContext() const |
| { |
| return scriptExecutionContext(); |
| } |
| |
| enum EventTargetInterfaceType Navigation::eventTargetInterface() const |
| { |
| return EventTargetInterfaceType::Navigation; |
| } |
| |
| static RefPtr<DOMPromise> createDOMPromise(const DeferredPromise& deferredPromise) |
| { |
| Locker<JSC::JSLock> locker(commonVM().apiLock()); |
| |
| auto promiseValue = deferredPromise.promise(); |
| auto& jsPromise = *JSC::jsCast<JSC::JSPromise*>(promiseValue); |
| auto& globalObject = *JSC::jsCast<JSDOMGlobalObject*>(jsPromise.globalObject()); |
| |
| return DOMPromise::create(globalObject, jsPromise); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-api-early-error-result |
| static Navigation::Result createErrorResult(Ref<DeferredPromise>&& committed, Ref<DeferredPromise>&& finished, Exception&& exception) |
| { |
| Navigation::Result result = { |
| createDOMPromise(committed), |
| createDOMPromise(finished) |
| }; |
| |
| JSC::JSValue exceptionObject; |
| committed->reject(exception, RejectAsHandled::No, exceptionObject); |
| finished->reject(exception, RejectAsHandled::No, exceptionObject); |
| |
| return result; |
| } |
| |
| static Navigation::Result createErrorResult(Ref<DeferredPromise>&& committed, Ref<DeferredPromise>&& finished, ExceptionCode exceptionCode, const String& errorMessage) |
| { |
| return createErrorResult(WTFMove(committed), WTFMove(finished), Exception { exceptionCode, errorMessage }); |
| } |
| |
| ExceptionOr<RefPtr<SerializedScriptValue>> Navigation::serializeState(JSC::JSValue state) |
| { |
| if (state.isUndefined()) |
| return { nullptr }; |
| |
| if (!frame()) |
| return Exception(ExceptionCode::DataCloneError, "Cannot serialize state: Detached frame"_s); |
| |
| Vector<Ref<MessagePort>> dummyPorts; |
| auto serializeResult = SerializedScriptValue::create(*protectedScriptExecutionContext()->globalObject(), state, { }, dummyPorts, SerializationForStorage::Yes); |
| if (serializeResult.hasException()) |
| return serializeResult.releaseException(); |
| |
| return { serializeResult.releaseReturnValue().ptr() }; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#maybe-set-the-upcoming-non-traverse-api-method-tracker |
| RefPtr<NavigationAPIMethodTracker> Navigation::maybeSetUpcomingNonTraversalTracker(Ref<DeferredPromise>&& committed, Ref<DeferredPromise>&& finished, JSC::JSValue info, RefPtr<SerializedScriptValue>&& serializedState) |
| { |
| RefPtr apiMethodTracker = NavigationAPIMethodTracker::create(WTFMove(committed), WTFMove(finished), WTFMove(info), WTFMove(serializedState)); |
| |
| Ref { apiMethodTracker->finishedPromise }->markAsHandled(); |
| |
| // FIXME: We should be able to assert m_upcomingNonTraverseMethodTracker is empty. |
| if (!hasEntriesAndEventsDisabled()) { |
| Locker locker { m_apiMethodTrackersLock }; |
| m_upcomingNonTraverseMethodTracker = apiMethodTracker; |
| } |
| |
| return apiMethodTracker; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#add-an-upcoming-traverse-api-method-tracker |
| RefPtr<NavigationAPIMethodTracker> Navigation::addUpcomingTraverseAPIMethodTracker(Ref<DeferredPromise>&& committed, Ref<DeferredPromise>&& finished, const String& key, JSC::JSValue info) |
| { |
| RefPtr apiMethodTracker = NavigationAPIMethodTracker::create(WTFMove(committed), WTFMove(finished), WTFMove(info), nullptr); |
| apiMethodTracker->key = key; |
| |
| Ref { apiMethodTracker->finishedPromise }->markAsHandled(); |
| |
| { |
| Locker locker { m_apiMethodTrackersLock }; |
| m_upcomingTraverseMethodTrackers.add(key, *apiMethodTracker); |
| } |
| |
| return apiMethodTracker; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-api-method-tracker-derived-result |
| Navigation::Result Navigation::apiMethodTrackerDerivedResult(const NavigationAPIMethodTracker& apiMethodTracker) |
| { |
| return { |
| createDOMPromise(apiMethodTracker.committedPromise), |
| createDOMPromise(apiMethodTracker.finishedPromise), |
| }; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-reload |
| Navigation::Result Navigation::reload(ReloadOptions&& options, Ref<DeferredPromise>&& committed, Ref<DeferredPromise>&& finished) |
| { |
| auto serializedState = serializeState(options.state); |
| if (serializedState.hasException()) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), serializedState.releaseException()); |
| auto state = serializedState.releaseReturnValue(); |
| if (!state && currentEntry()) |
| state = currentEntry()->associatedHistoryItem().navigationAPIStateObject(); |
| |
| RefPtr window = this->window(); |
| if (!window->protectedDocument()->isFullyActive() || window->document()->unloadCounter()) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), ExceptionCode::InvalidStateError, "Invalid state"_s); |
| |
| RefPtr apiMethodTracker = maybeSetUpcomingNonTraversalTracker(WTFMove(committed), WTFMove(finished), WTFMove(options.info), WTFMove(state)); |
| |
| RefPtr lexicalFrame = lexicalFrameFromCommonVM(); |
| auto initiatedByMainFrame = lexicalFrame && lexicalFrame->isMainFrame() ? InitiatedByMainFrame::Yes : InitiatedByMainFrame::Unknown; |
| RefPtr frame = this->frame(); |
| RefPtr document = frame->document(); |
| ResourceRequest resourceRequest { URL { document->url() }, frame->loader().outgoingReferrer(), ResourceRequestCachePolicy::ReloadIgnoringCacheData }; |
| FrameLoadRequest frameLoadRequest { *document, document->securityOrigin(), WTFMove(resourceRequest), selfTargetFrameName(), initiatedByMainFrame }; |
| frameLoadRequest.setLockHistory(LockHistory::Yes); |
| frameLoadRequest.setLockBackForwardList(LockBackForwardList::Yes); |
| frameLoadRequest.setShouldOpenExternalURLsPolicy(document->shouldOpenExternalURLsPolicyToPropagate()); |
| |
| frame->loader().changeLocation(WTFMove(frameLoadRequest)); |
| |
| return apiMethodTrackerDerivedResult(*apiMethodTracker); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-navigate |
| Navigation::Result Navigation::navigate(const String& url, NavigateOptions&& options, Ref<DeferredPromise>&& committed, Ref<DeferredPromise>&& finished) |
| { |
| RefPtr window = this->window(); |
| auto newURL = window->protectedDocument()->completeURL(url, ScriptExecutionContext::ForceUTF8::Yes); |
| const URL& currentURL = protectedScriptExecutionContext()->url(); |
| |
| if (!newURL.isValid()) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), ExceptionCode::SyntaxError, "Invalid URL"_s); |
| |
| if (options.history == HistoryBehavior::Push && newURL.protocolIsJavaScript()) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), ExceptionCode::NotSupportedError, "A \"push\" navigation was explicitly requested, but only a \"replace\" navigation is possible when navigating to a javascript: URL."_s); |
| |
| if (options.history == HistoryBehavior::Push && currentURL.isAboutBlank()) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), ExceptionCode::NotSupportedError, "A \"push\" navigation was explicitly requested, but only a \"replace\" navigation is possible while on an about:blank document."_s); |
| |
| auto serializedState = serializeState(options.state); |
| if (serializedState.hasException()) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), serializedState.releaseException()); |
| |
| if (!window->protectedDocument()->isFullyActive() || window->document()->unloadCounter()) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), ExceptionCode::InvalidStateError, "Invalid state"_s); |
| |
| RefPtr apiMethodTracker = maybeSetUpcomingNonTraversalTracker(WTFMove(committed), WTFMove(finished), WTFMove(options.info), serializedState.releaseReturnValue()); |
| |
| auto request = FrameLoadRequest(*frame(), WTFMove(newURL)); |
| request.setNavigationHistoryBehavior(options.history); |
| request.setIsFromNavigationAPI(true); |
| frame()->loader().loadFrameRequest(WTFMove(request), nullptr, { }); |
| |
| // If the load() call never made it to the point that NavigateEvent was emitted, thus promoteUpcomingAPIMethodTracker() called, this will be true. |
| { |
| Locker locker { m_apiMethodTrackersLock }; |
| if (m_upcomingNonTraverseMethodTracker == apiMethodTracker) { |
| m_upcomingNonTraverseMethodTracker = nullptr; |
| return createErrorResult(WTFMove(apiMethodTracker->committedPromise), WTFMove(apiMethodTracker->finishedPromise), ExceptionCode::AbortError, "Navigation aborted"_s); |
| } |
| } |
| |
| return apiMethodTrackerDerivedResult(*apiMethodTracker); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#performing-a-navigation-api-traversal |
| Navigation::Result Navigation::performTraversal(const String& key, Navigation::Options options, Ref<DeferredPromise>&& committed, Ref<DeferredPromise>&& finished) |
| { |
| RefPtr window = this->window(); |
| if (!window->protectedDocument()->isFullyActive() || window->document()->unloadCounter()) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), ExceptionCode::InvalidStateError, "Invalid state"_s); |
| |
| auto entry = findEntryByKey(key); |
| if (!entry) |
| createErrorResult(WTFMove(committed), WTFMove(finished), ExceptionCode::AbortError, "Navigation aborted"_s); |
| |
| RefPtr frame = this->frame(); |
| if (!frame->isMainFrame() && window->protectedDocument()->canNavigate(&frame->protectedPage()->protectedMainFrame().get()) != CanNavigateState::Able) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), ExceptionCode::SecurityError, "Invalid state"_s); |
| |
| RefPtr current = currentEntry(); |
| if (current->key() == key) { |
| committed->resolve<IDLInterface<NavigationHistoryEntry>>(*current.get()); |
| finished->resolve<IDLInterface<NavigationHistoryEntry>>(*current.get()); |
| return { createDOMPromise(committed), createDOMPromise(finished) }; |
| } |
| |
| { |
| Locker locker { m_apiMethodTrackersLock }; |
| if (auto existingMethodTracker = m_upcomingTraverseMethodTrackers.getOptional(key)) |
| return apiMethodTrackerDerivedResult(*existingMethodTracker); |
| } |
| |
| RefPtr apiMethodTracker = addUpcomingTraverseAPIMethodTracker(WTFMove(committed), WTFMove(finished), key, options.info); |
| |
| // FIXME: 11. Let sourceSnapshotParams be the result of snapshotting source snapshot params given document. |
| frame->protectedNavigationScheduler()->scheduleHistoryNavigationByKey(key, [apiMethodTracker] (ScheduleHistoryNavigationResult result) { |
| if (result == ScheduleHistoryNavigationResult::Aborted) |
| createErrorResult(WTFMove(apiMethodTracker->committedPromise), WTFMove(apiMethodTracker->finishedPromise), ExceptionCode::AbortError, "Navigation aborted"_s); |
| }); |
| |
| return apiMethodTrackerDerivedResult(*apiMethodTracker); |
| } |
| |
| std::optional<Ref<NavigationHistoryEntry>> Navigation::findEntryByKey(const String& key) |
| { |
| auto entryIndex = m_entries.findIf([&key](const Ref<NavigationHistoryEntry> entry) { |
| return entry->key() == key; |
| }); |
| |
| if (entryIndex == notFound) |
| return std::nullopt; |
| |
| return m_entries[entryIndex]; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-traverseto |
| Navigation::Result Navigation::traverseTo(const String& key, Options&& options, Ref<DeferredPromise>&& committed, Ref<DeferredPromise>&& finished) |
| { |
| auto entry = findEntryByKey(key); |
| if (!entry) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), ExceptionCode::InvalidStateError, "Invalid key"_s); |
| |
| return performTraversal(key, options, WTFMove(committed), WTFMove(finished)); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-back |
| Navigation::Result Navigation::back(Options&& options, Ref<DeferredPromise>&& committed, Ref<DeferredPromise>&& finished) |
| { |
| if (!canGoBack()) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), ExceptionCode::InvalidStateError, "Cannot go back"_s); |
| |
| Ref previousEntry = m_entries[m_currentEntryIndex.value() - 1]; |
| |
| return performTraversal(previousEntry->key(), options, WTFMove(committed), WTFMove(finished)); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-forward |
| Navigation::Result Navigation::forward(Options&& options, Ref<DeferredPromise>&& committed, Ref<DeferredPromise>&& finished) |
| { |
| if (!canGoForward()) |
| return createErrorResult(WTFMove(committed), WTFMove(finished), ExceptionCode::InvalidStateError, "Cannot go forward"_s); |
| |
| Ref nextEntry = m_entries[m_currentEntryIndex.value() + 1]; |
| |
| return performTraversal(nextEntry->key(), options, WTFMove(committed), WTFMove(finished)); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-updatecurrententry |
| ExceptionOr<void> Navigation::updateCurrentEntry(UpdateCurrentEntryOptions&& options) |
| { |
| RefPtr current = currentEntry(); |
| if (!current) |
| return Exception { ExceptionCode::InvalidStateError }; |
| |
| auto serializedState = SerializedScriptValue::create(*protectedScriptExecutionContext()->globalObject(), options.state, SerializationForStorage::Yes, SerializationErrorMode::Throwing); |
| if (!serializedState) |
| return { }; |
| |
| current->setState(WTFMove(serializedState)); |
| |
| auto currentEntryChangeEvent = NavigationCurrentEntryChangeEvent::create(eventNames().currententrychangeEvent, { |
| { false, false, false }, |
| std::nullopt, |
| current |
| }); |
| dispatchEvent(currentEntryChangeEvent); |
| |
| return { }; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#has-entries-and-events-disabled |
| bool Navigation::hasEntriesAndEventsDisabled() const |
| { |
| RefPtr window = this->window(); |
| RefPtr document = window->document(); |
| if (!document || !document->isFullyActive()) |
| return true; |
| if (document->loader() && document->loader()->isInitialAboutBlank()) |
| return true; |
| if (window->securityOrigin() && window->securityOrigin()->isOpaque()) |
| return true; |
| return false; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#resolve-the-finished-promise |
| void Navigation::resolveFinishedPromise(NavigationAPIMethodTracker* apiMethodTracker) |
| { |
| RefPtr committedToEntry = apiMethodTracker->committedToEntry; |
| if (!committedToEntry) { |
| apiMethodTracker->finishedBeforeCommit = true; |
| return; |
| } |
| |
| Ref { apiMethodTracker->committedPromise }->resolve<IDLInterface<NavigationHistoryEntry>>(*committedToEntry); |
| Ref { apiMethodTracker->finishedPromise }->resolve<IDLInterface<NavigationHistoryEntry>>(*committedToEntry); |
| cleanupAPIMethodTracker(apiMethodTracker); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#reject-the-finished-promise |
| void Navigation::rejectFinishedPromise(NavigationAPIMethodTracker* apiMethodTracker, const Exception& exception, JSC::JSValue exceptionObject) |
| { |
| // finished is already marked as handled at this point so don't overwrite that. |
| Ref { apiMethodTracker->finishedPromise }->reject(exception, RejectAsHandled::Yes, exceptionObject); |
| Ref { apiMethodTracker->committedPromise }->reject(exception, RejectAsHandled::No, exceptionObject); |
| cleanupAPIMethodTracker(apiMethodTracker); |
| } |
| |
| void Navigation::rejectFinishedPromise(NavigationAPIMethodTracker* apiMethodTracker) |
| { |
| if (!apiMethodTracker) |
| return; |
| |
| auto* globalObject = protectedScriptExecutionContext()->globalObject(); |
| if (!globalObject && apiMethodTracker) |
| globalObject = apiMethodTracker->committedPromise->globalObject(); |
| if (!globalObject) |
| return; |
| |
| JSC::JSLockHolder locker(globalObject->vm()); |
| auto exception = Exception(ExceptionCode::AbortError, "Navigation aborted"_s); |
| auto domException = createDOMException(*globalObject, exception.isolatedCopy()); |
| rejectFinishedPromise(apiMethodTracker, exception, domException); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#notify-about-the-committed-to-entry |
| void Navigation::notifyCommittedToEntry(NavigationAPIMethodTracker* apiMethodTracker, NavigationHistoryEntry* entry, NavigationNavigationType navigationType) |
| { |
| ASSERT(entry); |
| apiMethodTracker->committedToEntry = entry; |
| if (navigationType != NavigationNavigationType::Traverse) { |
| if (apiMethodTracker->serializedState) |
| RefPtr { apiMethodTracker->committedToEntry }->setState(WTFMove(apiMethodTracker->serializedState)); |
| } |
| |
| if (apiMethodTracker->finishedBeforeCommit) |
| resolveFinishedPromise(apiMethodTracker); |
| else |
| Ref { apiMethodTracker->committedPromise }->resolve<IDLInterface<NavigationHistoryEntry>>(*entry); |
| } |
| |
| void Navigation::updateNavigationEntry(Ref<HistoryItem>&& item, ShouldCopyStateObjectFromCurrentEntry shouldCopyStateObjectFromCurrentEntry) |
| { |
| if (!m_currentEntryIndex) |
| return; |
| |
| m_entries[*m_currentEntryIndex] = NavigationHistoryEntry::create(*this, item.copyRef()); |
| |
| if (!frame()) |
| return; |
| |
| RefPtr firstChild = frame()->tree().firstChild(); |
| if (!firstChild) |
| return; |
| |
| for (RefPtr child = firstChild.get(); child; child = child->tree().nextSibling()) { |
| RefPtr localChild = dynamicDowncast<LocalFrame>(child.get()); |
| if (!localChild) |
| continue; |
| |
| if (RefPtr childItem = item->childItemWithFrameID(localChild->frameID())) { |
| RefPtr window = localChild->window(); |
| if (!window) |
| continue; |
| |
| window->protectedNavigation()->updateNavigationEntry(childItem.releaseNonNull(), shouldCopyStateObjectFromCurrentEntry); |
| } |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#update-the-navigation-api-entries-for-a-same-document-navigation |
| void Navigation::updateForNavigation(Ref<HistoryItem>&& item, NavigationNavigationType navigationType, ShouldCopyStateObjectFromCurrentEntry shouldCopyStateObjectFromCurrentEntry) |
| { |
| if (hasEntriesAndEventsDisabled()) |
| return; |
| |
| RefPtr oldCurrentEntry = currentEntry(); |
| if (!oldCurrentEntry) |
| return; |
| |
| Vector<Ref<NavigationHistoryEntry>> disposedEntries; |
| |
| switch (navigationType) { |
| case NavigationNavigationType::Traverse: |
| m_currentEntryIndex = getEntryIndexOfHistoryItem(m_entries, item); |
| if (!m_currentEntryIndex) |
| return; |
| break; |
| case NavigationNavigationType::Push: |
| m_currentEntryIndex = *m_currentEntryIndex + 1; |
| for (size_t i = *m_currentEntryIndex; i < m_entries.size(); i++) |
| disposedEntries.append(m_entries[i]); |
| m_entries.resize(*m_currentEntryIndex + 1); |
| break; |
| case NavigationNavigationType::Replace: |
| disposedEntries.append(*oldCurrentEntry); |
| break; |
| default: |
| break; |
| } |
| |
| if (navigationType == NavigationNavigationType::Push || navigationType == NavigationNavigationType::Replace) { |
| updateNavigationEntry(WTFMove(item), shouldCopyStateObjectFromCurrentEntry); |
| if (shouldCopyStateObjectFromCurrentEntry == ShouldCopyStateObjectFromCurrentEntry::Yes) |
| Ref { m_entries[*m_currentEntryIndex] }->setState(oldCurrentEntry->state()); |
| } |
| |
| RefPtr<NavigationAPIMethodTracker> ongoingAPIMethodTracker; |
| { |
| Locker locker { m_apiMethodTrackersLock }; |
| ongoingAPIMethodTracker = m_ongoingAPIMethodTracker; |
| } |
| if (ongoingAPIMethodTracker) |
| notifyCommittedToEntry(ongoingAPIMethodTracker.get(), protectedCurrentEntry().get(), navigationType); |
| |
| auto currentEntryChangeEvent = NavigationCurrentEntryChangeEvent::create(eventNames().currententrychangeEvent, { |
| { false, false, false }, navigationType, oldCurrentEntry |
| }); |
| dispatchEvent(currentEntryChangeEvent); |
| |
| for (auto& disposedEntry : disposedEntries) |
| disposedEntry->dispatchDisposeEvent(); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#update-the-navigation-api-entries-for-reactivation |
| void Navigation::updateForReactivation(Vector<Ref<HistoryItem>>& newHistoryItems, HistoryItem& reactivatedItem) |
| { |
| if (hasEntriesAndEventsDisabled()) |
| return; |
| |
| Vector<Ref<NavigationHistoryEntry>> newEntries; |
| Vector<Ref<NavigationHistoryEntry>> oldEntries = std::exchange(m_entries, { }); |
| |
| for (Ref item : newHistoryItems) { |
| RefPtr<NavigationHistoryEntry> newEntry; |
| |
| for (size_t entryIndex = 0; entryIndex < oldEntries.size(); entryIndex++) { |
| auto& entry = oldEntries.at(entryIndex); |
| if (entry->associatedHistoryItem() == item) { |
| newEntry = entry.ptr(); |
| oldEntries.removeAt(entryIndex); |
| break; |
| } |
| } |
| |
| if (!newEntry) |
| newEntry = NavigationHistoryEntry::create(*this, WTFMove(item)); |
| |
| newEntries.append(newEntry.releaseNonNull()); |
| } |
| |
| m_entries = WTFMove(newEntries); |
| m_currentEntryIndex = getEntryIndexOfHistoryItem(m_entries, reactivatedItem); |
| |
| for (auto& disposedEntry : oldEntries) |
| disposedEntry->dispatchDisposeEvent(); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#can-have-its-url-rewritten |
| static bool documentCanHaveURLRewritten(const Document& document, const URL& targetURL) |
| { |
| const URL& documentURL = document.url(); |
| Ref documentOrigin = document.securityOrigin(); |
| auto targetOrigin = SecurityOrigin::create(targetURL); |
| |
| if (!documentOrigin->isSameSiteAs(targetOrigin)) |
| return false; |
| |
| if (targetURL.protocolIsInHTTPFamily()) |
| return true; |
| |
| if (targetURL.protocolIsFile() && !isEqualIgnoringQueryAndFragments(documentURL, targetURL)) |
| return false; |
| |
| return equalIgnoringFragmentIdentifier(documentURL, targetURL); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#promote-an-upcoming-api-method-tracker-to-ongoing |
| void Navigation::promoteUpcomingAPIMethodTracker(const String& destinationKey) |
| { |
| // FIXME: We should be able to assert m_ongoingAPIMethodTracker is unset. |
| |
| Locker locker { m_apiMethodTrackersLock }; |
| if (!destinationKey.isEmpty()) |
| m_ongoingAPIMethodTracker = m_upcomingTraverseMethodTrackers.take(destinationKey); |
| else if (destinationKey.isNull()) { |
| m_ongoingAPIMethodTracker = WTFMove(m_upcomingNonTraverseMethodTracker); |
| m_upcomingNonTraverseMethodTracker = nullptr; |
| } else if (destinationKey.isEmpty() && !m_upcomingTraverseMethodTrackers.isEmpty()) { |
| // For traverse navigation where destination key is empty, try to use any available traverse method tracker. |
| // (e.g., cross-document navigation where NavigationHistoryEntry is not found). |
| auto firstTracker = m_upcomingTraverseMethodTrackers.begin(); |
| if (firstTracker != m_upcomingTraverseMethodTrackers.end()) { |
| String trackerKey = firstTracker->key; |
| m_ongoingAPIMethodTracker = m_upcomingTraverseMethodTrackers.take(trackerKey); |
| } |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-api-method-tracker-clean-up |
| void Navigation::cleanupAPIMethodTracker(NavigationAPIMethodTracker* apiMethodTracker) |
| { |
| Locker locker { m_apiMethodTrackersLock }; |
| if (m_ongoingAPIMethodTracker == apiMethodTracker) |
| m_ongoingAPIMethodTracker = nullptr; |
| else { |
| auto& key = apiMethodTracker->key; |
| // FIXME: We should be able to assert key isn't null and m_upcomingTraverseMethodTrackers contains it. |
| if (!key.isNull()) |
| m_upcomingTraverseMethodTrackers.remove(key); |
| } |
| } |
| |
| NavigationAPIMethodTracker* Navigation::upcomingTraverseMethodTracker(const String& key) const |
| { |
| Locker locker { m_apiMethodTrackersLock }; |
| return m_upcomingTraverseMethodTrackers.get(key); |
| } |
| |
| auto Navigation::registerAbortHandler() -> Ref<AbortHandler> |
| { |
| Ref abortHandler = AbortHandler::create(); |
| m_abortHandlers.add(abortHandler.get()); |
| return abortHandler; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#abort-the-ongoing-navigation |
| void Navigation::abortOngoingNavigation(NavigateEvent& event) |
| { |
| m_abortHandlers.forEach([](auto& abortHandler) { |
| abortHandler.markAsAborted(); |
| }); |
| |
| RefPtr scriptExecutionContext = this->scriptExecutionContext(); |
| auto* globalObject = scriptExecutionContext->globalObject(); |
| if (!globalObject) { |
| RefPtr<NavigationAPIMethodTracker> ongoingAPIMethodTracker; |
| { |
| Locker locker { m_apiMethodTrackersLock }; |
| ongoingAPIMethodTracker = m_ongoingAPIMethodTracker; |
| } |
| if (ongoingAPIMethodTracker) |
| globalObject = ongoingAPIMethodTracker->committedPromise->globalObject(); |
| } |
| if (!globalObject) |
| return; |
| |
| m_focusChangedDuringOngoingNavigation = FocusDidChange::No; |
| m_suppressNormalScrollRestorationDuringOngoingNavigation = false; |
| |
| if (event.isBeingDispatched()) |
| event.preventDefault(); |
| |
| JSC::JSLockHolder locker(globalObject->vm()); |
| auto exception = Exception(ExceptionCode::AbortError, "Navigation aborted"_s); |
| auto domException = createDOMException(*globalObject, exception.isolatedCopy()); |
| |
| auto error = JSC::createError(globalObject, "Navigation aborted"_s); |
| |
| ErrorInformation errorInformation; |
| if (auto* errorInstance = jsDynamicCast<JSC::ErrorInstance*>(error)) { |
| if (auto result = extractErrorInformationFromErrorInstance(globalObject, *errorInstance)) |
| errorInformation = WTFMove(*result); |
| // Default to document url if extractErrorInformationFromErrorInstance was not able to determine sourceURL. |
| if (errorInformation.sourceURL.isEmpty()) |
| errorInformation.sourceURL = scriptExecutionContext->url().string(); |
| } |
| |
| if (RefPtr signal = event.signal()) |
| signal->signalAbort(domException); |
| |
| m_ongoingNavigateEvent = nullptr; |
| |
| dispatchEvent(ErrorEvent::create(eventNames().navigateerrorEvent, exception.message(), errorInformation.sourceURL, errorInformation.line, errorInformation.column, { globalObject->vm(), domException })); |
| |
| RefPtr<NavigationAPIMethodTracker> ongoingAPIMethodTracker; |
| { |
| Locker locker { m_apiMethodTrackersLock }; |
| ongoingAPIMethodTracker = m_ongoingAPIMethodTracker; |
| } |
| if (ongoingAPIMethodTracker) |
| rejectFinishedPromise(ongoingAPIMethodTracker.get(), exception, domException); |
| |
| if (RefPtr transition = m_transition) { |
| transition->rejectPromise(exception, domException); |
| m_transition = nullptr; |
| } |
| } |
| |
| struct AwaitingPromiseData : public RefCounted<AwaitingPromiseData> { |
| WTF_DEPRECATED_MAKE_STRUCT_FAST_ALLOCATED(AwaitingPromiseData); |
| Function<void()> fulfilledCallback; |
| Function<void(JSC::JSValue)> rejectionCallback; |
| size_t remainingPromises = 0; |
| bool rejected = false; |
| |
| AwaitingPromiseData() = delete; |
| AwaitingPromiseData(Function<void()>&& fulfilledCallback, Function<void(JSC::JSValue)>&& rejectionCallback, size_t remainingPromises) |
| : fulfilledCallback(WTFMove(fulfilledCallback)) |
| , rejectionCallback(WTFMove(rejectionCallback)) |
| , remainingPromises(remainingPromises) |
| { |
| } |
| }; |
| |
| // https://webidl.spec.whatwg.org/#wait-for-all |
| static void waitForAllPromises(const Vector<RefPtr<DOMPromise>>& promises, Function<void()>&& fulfilledCallback, Function<void(JSC::JSValue)>&& rejectionCallback) |
| { |
| Ref awaitingData = adoptRef(*new AwaitingPromiseData(WTFMove(fulfilledCallback), WTFMove(rejectionCallback), promises.size())); |
| |
| for (const auto& promise : promises) { |
| // At any point between promises the frame could have been detached. |
| // FIXME: There is possibly a better way to handle this rather than just never complete. |
| if (promise->isSuspended()) |
| return; |
| |
| promise->whenSettled([awaitingData, promise] () mutable { |
| if (promise->isSuspended()) |
| return; |
| |
| switch (promise->status()) { |
| case DOMPromise::Status::Fulfilled: |
| if (--awaitingData->remainingPromises > 0) |
| break; |
| awaitingData->fulfilledCallback(); |
| break; |
| case DOMPromise::Status::Rejected: |
| if (awaitingData->rejected) |
| break; |
| awaitingData->rejected = true; |
| awaitingData->rejectionCallback(promise->result()); |
| break; |
| case DOMPromise::Status::Pending: |
| ASSERT_NOT_REACHED(); |
| break; |
| } |
| }); |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#inner-navigate-event-firing-algorithm |
| Navigation::DispatchResult Navigation::innerDispatchNavigateEvent(NavigationNavigationType navigationType, Ref<NavigationDestination>&& destination, const String& downloadRequestFilename, FormState* formState, SerializedScriptValue* classicHistoryAPIState, Element* sourceElement) |
| { |
| if (hasEntriesAndEventsDisabled()) { |
| #if ASSERT_ENABLED |
| Locker locker { m_apiMethodTrackersLock }; |
| #endif |
| ASSERT(!m_ongoingAPIMethodTracker); |
| ASSERT(!m_upcomingNonTraverseMethodTracker); |
| ASSERT(m_upcomingTraverseMethodTrackers.isEmpty()); |
| return DispatchResult::Completed; |
| } |
| |
| bool wasBeingDispatched = m_ongoingNavigateEvent ? m_ongoingNavigateEvent->isBeingDispatched() : false; |
| |
| abortOngoingNavigationIfNeeded(); |
| |
| // Prevent recursion on synchronous history navigation steps issued |
| // from the navigate event handler. |
| if (wasBeingDispatched && classicHistoryAPIState) |
| return DispatchResult::Completed; |
| |
| promoteUpcomingAPIMethodTracker(destination->key()); |
| |
| RefPtr document = protectedWindow()->document(); |
| |
| RefPtr<NavigationAPIMethodTracker> apiMethodTracker; |
| { |
| Locker locker { m_apiMethodTrackersLock }; |
| apiMethodTracker = m_ongoingAPIMethodTracker; |
| } |
| // FIXME: this should not be needed, we should pass it into FrameLoader. |
| if (apiMethodTracker && apiMethodTracker->serializedState) |
| destination->setStateObject(apiMethodTracker->serializedState.get()); |
| bool isSameDocument = destination->sameDocument(); |
| bool isTraversal = navigationType == NavigationNavigationType::Traverse; |
| bool canIntercept = documentCanHaveURLRewritten(*document, destination->url()) && (!isTraversal || isSameDocument); |
| bool canBeCanceled = !isTraversal || (document->isTopDocument() && isSameDocument); // FIXME: and user involvement is not browser-ui or navigation's relevant global object has transient activation. |
| bool hashChange = !classicHistoryAPIState && equalIgnoringFragmentIdentifier(document->url(), destination->url()) && !equalRespectingNullity(document->url().fragmentIdentifier(), destination->url().fragmentIdentifier()); |
| auto info = apiMethodTracker ? apiMethodTracker->info.getValue() : JSC::jsUndefined(); |
| |
| RefPtr scriptExecutionContext = this->scriptExecutionContext(); |
| RefPtr<DOMFormData> formData = nullptr; |
| if (formState) { |
| if (formState->form().isMethodPost() && (navigationType == NavigationNavigationType::Push || navigationType == NavigationNavigationType::Replace)) { |
| if (auto domFormData = DOMFormData::create(*scriptExecutionContext, Ref { formState->form() }.ptr(), RefPtr { formState->submitter() }.get()); !domFormData.hasException()) |
| formData = domFormData.releaseReturnValue(); |
| } |
| |
| if (!formState->form().target().isEmpty()) |
| sourceElement = nullptr; |
| else { |
| sourceElement = formState->submitter(); |
| if (!sourceElement) |
| sourceElement = &formState->form(); |
| } |
| } |
| |
| RefPtr abortController = AbortController::create(*scriptExecutionContext); |
| |
| auto init = NavigateEvent::Init { |
| { false, canBeCanceled, false }, |
| navigationType, |
| destination.ptr(), |
| Ref { abortController->signal() }, |
| formData, |
| downloadRequestFilename, |
| info, |
| sourceElement, |
| canIntercept, |
| UserGestureIndicator::processingUserGesture(document.get()), |
| hashChange, |
| document->page() && document->page()->isInSwipeAnimation(), |
| }; |
| |
| // Free up no longer needed info. |
| if (apiMethodTracker) |
| apiMethodTracker->info.clear(); |
| |
| Ref event = NavigateEvent::create(eventNames().navigateEvent, init, abortController.get()); |
| m_ongoingNavigateEvent = event.ptr(); |
| m_focusChangedDuringOngoingNavigation = FocusDidChange::No; |
| m_suppressNormalScrollRestorationDuringOngoingNavigation = false; |
| |
| dispatchEvent(event); |
| |
| // If the frame was detached in our event. |
| if (!frame()) { |
| abortOngoingNavigation(event); |
| return DispatchResult::Aborted; |
| } |
| |
| if (event->defaultPrevented()) { |
| // FIXME: If navigationType is "traverse", then consume history-action user activation. |
| if (!event->signal()->aborted()) |
| abortOngoingNavigation(event); |
| return DispatchResult::Aborted; |
| } |
| |
| bool endResultIsSameDocument = event->wasIntercepted() || destination->sameDocument(); |
| |
| // FIXME: Prepare to run script given navigation's relevant settings object. |
| |
| // Step 32: |
| if (event->wasIntercepted()) { |
| event->setInterceptionState(InterceptionState::Committed); |
| |
| RefPtr fromNavigationHistoryEntry = currentEntry(); |
| ASSERT(fromNavigationHistoryEntry); |
| |
| { |
| auto& domGlobalObject = *jsCast<JSDOMGlobalObject*>(scriptExecutionContext->globalObject()); |
| JSC::JSLockHolder locker(domGlobalObject.vm()); |
| m_transition = NavigationTransition::create(navigationType, *fromNavigationHistoryEntry, DeferredPromise::create(domGlobalObject, DeferredPromise::Mode::RetainPromiseOnResolve).releaseNonNull()); |
| } |
| |
| if (navigationType == NavigationNavigationType::Traverse) |
| m_suppressNormalScrollRestorationDuringOngoingNavigation = true; |
| |
| if (navigationType == NavigationNavigationType::Reload) { |
| // Not in specification but matches chromium implementation and tests. |
| updateForNavigation(currentEntry()->associatedHistoryItem(), navigationType); |
| } else if (navigationType == NavigationNavigationType::Push || navigationType == NavigationNavigationType::Replace) { |
| auto historyHandling = navigationType == NavigationNavigationType::Replace ? NavigationHistoryBehavior::Replace : NavigationHistoryBehavior::Push; |
| frame()->loader().updateURLAndHistory(destination->url(), classicHistoryAPIState, historyHandling); |
| } |
| } |
| |
| if (endResultIsSameDocument) { |
| Vector<RefPtr<DOMPromise>> promiseList; |
| |
| for (auto& handler : event->handlers()) { |
| auto callbackResult = handler->invoke(); |
| if (callbackResult.type() != CallbackResultType::UnableToExecute) |
| promiseList.append(callbackResult.releaseReturnValue()); |
| } |
| |
| if (promiseList.isEmpty()) { |
| auto promiseAndWrapper = createPromiseAndWrapper(*document); |
| Ref { promiseAndWrapper.second }->resolveWithCallback([](JSDOMGlobalObject&) { |
| return JSC::jsUndefined(); |
| }); |
| promiseList.append(WTFMove(promiseAndWrapper.first)); |
| } |
| |
| // FIXME: this emulates the behavior of a Promise wrapped around waitForAll, but we may want the real |
| // thing if the ordering-and-transition tests show timing related issues related to this. |
| scriptExecutionContext->checkedEventLoop()->queueTask(TaskSource::DOMManipulation, [weakThis = WeakPtr { this }, promiseList, abortController, document, apiMethodTracker]() { |
| waitForAllPromises(promiseList, [abortController, document, apiMethodTracker, weakThis]() mutable { |
| RefPtr protectedThis = weakThis.get(); |
| if (!protectedThis || abortController->signal().aborted() || !document->isFullyActive() || !protectedThis->m_ongoingNavigateEvent) |
| return; |
| |
| auto focusChanged = std::exchange(protectedThis->m_focusChangedDuringOngoingNavigation, FocusDidChange::No); |
| protectedThis->protectedOngoingNavigateEvent()->finish(*document, InterceptionHandlersDidFulfill::Yes, focusChanged); |
| protectedThis->m_ongoingNavigateEvent = nullptr; |
| |
| protectedThis->dispatchEvent(Event::create(eventNames().navigatesuccessEvent, { })); |
| |
| if (apiMethodTracker) |
| protectedThis->resolveFinishedPromise(apiMethodTracker.get()); |
| |
| if (RefPtr transition = std::exchange(protectedThis->m_transition, nullptr)) |
| transition->resolvePromise(); |
| |
| protectedThis->m_ongoingNavigateEvent = nullptr; |
| |
| }, [abortController, document, apiMethodTracker, weakThis](JSC::JSValue result) mutable { |
| RefPtr protectedThis = weakThis.get(); |
| if (!protectedThis || abortController->signal().aborted() || !document->isFullyActive() || !protectedThis->m_ongoingNavigateEvent) |
| return; |
| |
| auto focusChanged = std::exchange(protectedThis->m_focusChangedDuringOngoingNavigation, FocusDidChange::No); |
| protectedThis->protectedOngoingNavigateEvent()->finish(*document, InterceptionHandlersDidFulfill::No, focusChanged); |
| protectedThis->m_ongoingNavigateEvent = nullptr; |
| |
| ErrorInformation errorInformation; |
| String errorMessage; |
| if (auto* errorInstance = jsDynamicCast<JSC::ErrorInstance*>(result)) { |
| if (auto result = extractErrorInformationFromErrorInstance(protectedThis->protectedScriptExecutionContext()->globalObject(), *errorInstance)) { |
| errorInformation = WTFMove(*result); |
| errorMessage = makeString("Uncaught "_s, errorInformation.errorTypeString, ": "_s, errorInformation.message); |
| } |
| } |
| auto exception = Exception(ExceptionCode::UnknownError, errorMessage); |
| auto domException = createDOMException(*protectedThis->protectedScriptExecutionContext()->globalObject(), exception.isolatedCopy()); |
| |
| protectedThis->dispatchEvent(ErrorEvent::create(eventNames().navigateerrorEvent, errorMessage, errorInformation.sourceURL, errorInformation.line, errorInformation.column, { protectedThis->protectedScriptExecutionContext()->globalObject()->vm(), result })); |
| |
| if (apiMethodTracker) |
| Ref { apiMethodTracker->finishedPromise }->reject<IDLAny>(result, RejectAsHandled::Yes); |
| |
| if (RefPtr transition = std::exchange(protectedThis->m_transition, nullptr)) |
| transition->rejectPromise(exception, domException); |
| }); |
| }); |
| |
| // If a new event has been dispatched in our event handler then we were aborted above. |
| if (m_ongoingNavigateEvent != event.ptr()) |
| return DispatchResult::Aborted; |
| } else if (apiMethodTracker) |
| cleanupAPIMethodTracker(apiMethodTracker.get()); |
| else { |
| // FIXME: This situation isn't clear, we've made it through the event doing nothing so |
| // to avoid incorrectly being aborted we clear this. |
| // To reproduce see `inspector/runtime/execution-context-in-scriptless-page.html`. |
| m_ongoingNavigateEvent = nullptr; |
| } |
| |
| // FIXME: Step 35 Clean up after running script |
| |
| return event->wasIntercepted() ? DispatchResult::Intercepted : DispatchResult::Completed; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#fire-a-traverse-navigate-event |
| Navigation::DispatchResult Navigation::dispatchTraversalNavigateEvent(HistoryItem& historyItem) |
| { |
| RefPtr currentItem = frame() ? frame()->loader().history().currentItem() : nullptr; |
| bool isSameDocument = currentItem && currentItem->documentSequenceNumber() == historyItem.documentSequenceNumber(); |
| |
| RefPtr<NavigationHistoryEntry> destinationEntry; |
| auto index = m_entries.findIf([&historyItem](const auto& entry) { |
| return entry->associatedHistoryItem().itemSequenceNumber() == historyItem.itemSequenceNumber(); |
| }); |
| if (index != notFound) |
| destinationEntry = m_entries[index].ptr(); |
| |
| // FIXME: Set destinations state |
| Ref destination = NavigationDestination::create(historyItem.url(), WTFMove(destinationEntry), isSameDocument); |
| |
| return innerDispatchNavigateEvent(NavigationNavigationType::Traverse, WTFMove(destination), { }); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#fire-a-push/replace/reload-navigate-event |
| bool Navigation::dispatchPushReplaceReloadNavigateEvent(const URL& url, NavigationNavigationType navigationType, bool isSameDocument, FormState* formState, SerializedScriptValue* classicHistoryAPIState, Element* sourceElement) |
| { |
| Ref destination = NavigationDestination::create(url, nullptr, isSameDocument); |
| if (classicHistoryAPIState) |
| destination->setStateObject(classicHistoryAPIState); |
| |
| if (navigationType == NavigationNavigationType::Reload) { |
| formState = nullptr; |
| sourceElement = nullptr; |
| } |
| |
| return innerDispatchNavigateEvent(navigationType, WTFMove(destination), { }, formState, classicHistoryAPIState, sourceElement) == DispatchResult::Completed; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#fire-a-download-request-navigate-event |
| bool Navigation::dispatchDownloadNavigateEvent(const URL& url, const String& downloadFilename, Element* sourceElement) |
| { |
| Ref destination = NavigationDestination::create(url, nullptr, false); |
| return innerDispatchNavigateEvent(NavigationNavigationType::Push, WTFMove(destination), downloadFilename, nullptr, nullptr, sourceElement) == DispatchResult::Completed; |
| } |
| |
| // https://html.spec.whatwg.org/multipage/nav-history-apis.html#inform-the-navigation-api-about-aborting-navigation |
| void Navigation::abortOngoingNavigationIfNeeded() |
| { |
| if (RefPtr ongoingNavigateEvent = m_ongoingNavigateEvent) |
| abortOngoingNavigation(*ongoingNavigateEvent); |
| } |
| |
| void Navigation::visitAdditionalChildren(JSC::AbstractSlotVisitor& visitor) |
| { |
| Locker locker { m_apiMethodTrackersLock }; |
| if (m_ongoingAPIMethodTracker) |
| m_ongoingAPIMethodTracker->info.visit(visitor); |
| if (m_upcomingNonTraverseMethodTracker) |
| m_upcomingNonTraverseMethodTracker->info.visit(visitor); |
| for (auto& tracker : m_upcomingTraverseMethodTrackers.values()) |
| tracker->info.visit(visitor); |
| } |
| |
| } // namespace WebCore |