| /* |
| * Copyright (C) 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights reserved. |
| * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) |
| * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved. |
| * (http://www.torchmobile.com/) |
| * Copyright (C) 2009 Adam Barth. 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. |
| * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "core/loader/NavigationScheduler.h" |
| |
| #include "bindings/core/v8/ScriptController.h" |
| #include "core/events/Event.h" |
| #include "core/fetch/ResourceLoaderOptions.h" |
| #include "core/frame/Deprecation.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/frame/csp/ContentSecurityPolicy.h" |
| #include "core/html/HTMLFormElement.h" |
| #include "core/inspector/InspectorInstrumentation.h" |
| #include "core/loader/DocumentLoadTiming.h" |
| #include "core/loader/DocumentLoader.h" |
| #include "core/loader/FormSubmission.h" |
| #include "core/loader/FrameLoadRequest.h" |
| #include "core/loader/FrameLoader.h" |
| #include "core/loader/FrameLoaderClient.h" |
| #include "core/loader/FrameLoaderStateMachine.h" |
| #include "core/page/Page.h" |
| #include "platform/Histogram.h" |
| #include "platform/SharedBuffer.h" |
| #include "platform/UserGestureIndicator.h" |
| #include "platform/scheduler/CancellableTaskFactory.h" |
| #include "public/platform/Platform.h" |
| #include "public/platform/WebCachePolicy.h" |
| #include "public/platform/WebScheduler.h" |
| #include "wtf/CurrentTime.h" |
| #include "wtf/PtrUtil.h" |
| #include <memory> |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Add new scheduled navigation types before ScheduledLastEntry |
| enum ScheduledNavigationType { |
| ScheduledReload, |
| ScheduledFormSubmission, |
| ScheduledURLNavigation, |
| ScheduledRedirect, |
| ScheduledLocationChange, |
| ScheduledPageBlock, |
| |
| ScheduledLastEntry |
| }; |
| |
| // If the current frame has a provisional document loader, a scheduled |
| // navigation might abort that load. Log those occurrences until |
| // crbug.com/557430 is resolved. |
| void maybeLogScheduledNavigationClobber( |
| ScheduledNavigationType type, |
| LocalFrame* frame, |
| const FrameLoadRequest& request, |
| UserGestureIndicator* gestureIndicator) { |
| if (!frame->loader().provisionalDocumentLoader()) |
| return; |
| // Include enumeration values userGesture variants. |
| DEFINE_STATIC_LOCAL(EnumerationHistogram, scheduledNavigationClobberHistogram, |
| ("Navigation.Scheduled.MaybeCausedAbort", |
| ScheduledNavigationType::ScheduledLastEntry * 2)); |
| |
| UserGestureToken* gestureToken = gestureIndicator->currentToken(); |
| int value = gestureToken->hasGestures() ? type + ScheduledLastEntry : type; |
| scheduledNavigationClobberHistogram.count(value); |
| |
| DEFINE_STATIC_LOCAL( |
| CustomCountHistogram, scheduledClobberAbortTimeHistogram, |
| ("Navigation.Scheduled.MaybeCausedAbort.Time", 1, 10000, 50)); |
| double navigationStart = |
| frame->loader().provisionalDocumentLoader()->timing().navigationStart(); |
| if (navigationStart) |
| scheduledClobberAbortTimeHistogram.count(monotonicallyIncreasingTime() - |
| navigationStart); |
| } |
| |
| } // namespace |
| |
| unsigned NavigationDisablerForUnload::s_navigationDisableCount = 0; |
| |
| class ScheduledNavigation |
| : public GarbageCollectedFinalized<ScheduledNavigation> { |
| WTF_MAKE_NONCOPYABLE(ScheduledNavigation); |
| |
| public: |
| ScheduledNavigation(double delay, |
| Document* originDocument, |
| bool replacesCurrentItem, |
| bool isLocationChange) |
| : m_delay(delay), |
| m_originDocument(originDocument), |
| m_replacesCurrentItem(replacesCurrentItem), |
| m_isLocationChange(isLocationChange), |
| m_wasUserGesture(UserGestureIndicator::processingUserGesture()) { |
| if (m_wasUserGesture) |
| m_userGestureToken = UserGestureIndicator::currentToken(); |
| } |
| virtual ~ScheduledNavigation() {} |
| |
| virtual void fire(LocalFrame*) = 0; |
| |
| virtual bool shouldStartTimer(LocalFrame*) { return true; } |
| |
| double delay() const { return m_delay; } |
| Document* originDocument() const { return m_originDocument.get(); } |
| bool replacesCurrentItem() const { return m_replacesCurrentItem; } |
| bool isLocationChange() const { return m_isLocationChange; } |
| std::unique_ptr<UserGestureIndicator> createUserGestureIndicator() { |
| if (m_wasUserGesture && m_userGestureToken) |
| return wrapUnique(new UserGestureIndicator(m_userGestureToken)); |
| return wrapUnique( |
| new UserGestureIndicator(DefinitelyNotProcessingUserGesture)); |
| } |
| |
| DEFINE_INLINE_VIRTUAL_TRACE() { visitor->trace(m_originDocument); } |
| |
| protected: |
| void clearUserGesture() { m_wasUserGesture = false; } |
| |
| private: |
| double m_delay; |
| Member<Document> m_originDocument; |
| bool m_replacesCurrentItem; |
| bool m_isLocationChange; |
| bool m_wasUserGesture; |
| RefPtr<UserGestureToken> m_userGestureToken; |
| }; |
| |
| class ScheduledURLNavigation : public ScheduledNavigation { |
| protected: |
| ScheduledURLNavigation(double delay, |
| Document* originDocument, |
| const String& url, |
| bool replacesCurrentItem, |
| bool isLocationChange) |
| : ScheduledNavigation(delay, |
| originDocument, |
| replacesCurrentItem, |
| isLocationChange), |
| m_url(url), |
| m_shouldCheckMainWorldContentSecurityPolicy( |
| CheckContentSecurityPolicy) { |
| if (ContentSecurityPolicy::shouldBypassMainWorld(originDocument)) |
| m_shouldCheckMainWorldContentSecurityPolicy = |
| DoNotCheckContentSecurityPolicy; |
| } |
| |
| void fire(LocalFrame* frame) override { |
| std::unique_ptr<UserGestureIndicator> gestureIndicator = |
| createUserGestureIndicator(); |
| FrameLoadRequest request(originDocument(), m_url, "_self", |
| m_shouldCheckMainWorldContentSecurityPolicy); |
| request.setReplacesCurrentItem(replacesCurrentItem()); |
| request.setClientRedirect(ClientRedirectPolicy::ClientRedirect); |
| |
| ScheduledNavigationType type = |
| isLocationChange() ? ScheduledNavigationType::ScheduledLocationChange |
| : ScheduledNavigationType::ScheduledURLNavigation; |
| maybeLogScheduledNavigationClobber(type, frame, request, |
| gestureIndicator.get()); |
| frame->loader().load(request); |
| } |
| |
| String url() const { return m_url; } |
| |
| private: |
| String m_url; |
| ContentSecurityPolicyDisposition m_shouldCheckMainWorldContentSecurityPolicy; |
| }; |
| |
| class ScheduledRedirect final : public ScheduledURLNavigation { |
| public: |
| static ScheduledRedirect* create(double delay, |
| Document* originDocument, |
| const String& url, |
| bool replacesCurrentItem) { |
| return new ScheduledRedirect(delay, originDocument, url, |
| replacesCurrentItem); |
| } |
| |
| bool shouldStartTimer(LocalFrame* frame) override { |
| return frame->document()->loadEventFinished(); |
| } |
| |
| void fire(LocalFrame* frame) override { |
| std::unique_ptr<UserGestureIndicator> gestureIndicator = |
| createUserGestureIndicator(); |
| FrameLoadRequest request(originDocument(), url(), "_self"); |
| request.setReplacesCurrentItem(replacesCurrentItem()); |
| if (equalIgnoringFragmentIdentifier(frame->document()->url(), |
| request.resourceRequest().url())) |
| request.resourceRequest().setCachePolicy( |
| WebCachePolicy::ValidatingCacheData); |
| request.setClientRedirect(ClientRedirectPolicy::ClientRedirect); |
| maybeLogScheduledNavigationClobber( |
| ScheduledNavigationType::ScheduledRedirect, frame, request, |
| gestureIndicator.get()); |
| frame->loader().load(request); |
| } |
| |
| private: |
| ScheduledRedirect(double delay, |
| Document* originDocument, |
| const String& url, |
| bool replacesCurrentItem) |
| : ScheduledURLNavigation(delay, |
| originDocument, |
| url, |
| replacesCurrentItem, |
| false) { |
| clearUserGesture(); |
| } |
| }; |
| |
| class ScheduledLocationChange final : public ScheduledURLNavigation { |
| public: |
| static ScheduledLocationChange* create(Document* originDocument, |
| const String& url, |
| bool replacesCurrentItem) { |
| return new ScheduledLocationChange(originDocument, url, |
| replacesCurrentItem); |
| } |
| |
| private: |
| ScheduledLocationChange(Document* originDocument, |
| const String& url, |
| bool replacesCurrentItem) |
| : ScheduledURLNavigation(0.0, |
| originDocument, |
| url, |
| replacesCurrentItem, |
| !protocolIsJavaScript(url)) {} |
| }; |
| |
| class ScheduledReload final : public ScheduledNavigation { |
| public: |
| static ScheduledReload* create() { return new ScheduledReload; } |
| |
| void fire(LocalFrame* frame) override { |
| std::unique_ptr<UserGestureIndicator> gestureIndicator = |
| createUserGestureIndicator(); |
| ResourceRequest resourceRequest = frame->loader().resourceRequestForReload( |
| FrameLoadTypeReload, KURL(), ClientRedirectPolicy::ClientRedirect); |
| if (resourceRequest.isNull()) |
| return; |
| FrameLoadRequest request = FrameLoadRequest(nullptr, resourceRequest); |
| request.setClientRedirect(ClientRedirectPolicy::ClientRedirect); |
| maybeLogScheduledNavigationClobber(ScheduledNavigationType::ScheduledReload, |
| frame, request, gestureIndicator.get()); |
| frame->loader().load(request, FrameLoadTypeReload); |
| } |
| |
| private: |
| ScheduledReload() : ScheduledNavigation(0.0, nullptr, true, true) {} |
| }; |
| |
| class ScheduledPageBlock final : public ScheduledURLNavigation { |
| public: |
| static ScheduledPageBlock* create(Document* originDocument, |
| const String& url) { |
| return new ScheduledPageBlock(originDocument, url); |
| } |
| |
| void fire(LocalFrame* frame) override { |
| std::unique_ptr<UserGestureIndicator> gestureIndicator = |
| createUserGestureIndicator(); |
| SubstituteData substituteData(SharedBuffer::create(), "text/plain", "UTF-8", |
| KURL(), ForceSynchronousLoad); |
| FrameLoadRequest request(originDocument(), url(), substituteData); |
| request.setReplacesCurrentItem(true); |
| request.setClientRedirect(ClientRedirectPolicy::ClientRedirect); |
| maybeLogScheduledNavigationClobber( |
| ScheduledNavigationType::ScheduledPageBlock, frame, request, |
| gestureIndicator.get()); |
| frame->loader().load(request); |
| } |
| |
| private: |
| ScheduledPageBlock(Document* originDocument, const String& url) |
| : ScheduledURLNavigation(0.0, originDocument, url, true, true) {} |
| }; |
| |
| class ScheduledFormSubmission final : public ScheduledNavigation { |
| public: |
| static ScheduledFormSubmission* create(Document* document, |
| FormSubmission* submission, |
| bool replacesCurrentItem) { |
| return new ScheduledFormSubmission(document, submission, |
| replacesCurrentItem); |
| } |
| |
| void fire(LocalFrame* frame) override { |
| std::unique_ptr<UserGestureIndicator> gestureIndicator = |
| createUserGestureIndicator(); |
| FrameLoadRequest frameRequest = |
| m_submission->createFrameLoadRequest(originDocument()); |
| frameRequest.setReplacesCurrentItem(replacesCurrentItem()); |
| maybeLogScheduledNavigationClobber( |
| ScheduledNavigationType::ScheduledFormSubmission, frame, frameRequest, |
| gestureIndicator.get()); |
| frame->loader().load(frameRequest); |
| } |
| |
| DEFINE_INLINE_VIRTUAL_TRACE() { |
| visitor->trace(m_submission); |
| ScheduledNavigation::trace(visitor); |
| } |
| |
| private: |
| ScheduledFormSubmission(Document* document, |
| FormSubmission* submission, |
| bool replacesCurrentItem) |
| : ScheduledNavigation(0, document, replacesCurrentItem, true), |
| m_submission(submission) { |
| DCHECK(m_submission->form()); |
| } |
| |
| Member<FormSubmission> m_submission; |
| }; |
| |
| NavigationScheduler::NavigationScheduler(LocalFrame* frame) |
| : m_frame(frame), |
| m_navigateTaskFactory( |
| CancellableTaskFactory::create(this, |
| &NavigationScheduler::navigateTask)), |
| m_frameType(m_frame->isMainFrame() |
| ? WebScheduler::NavigatingFrameType::kMainFrame |
| : WebScheduler::NavigatingFrameType::kChildFrame) {} |
| |
| NavigationScheduler::~NavigationScheduler() { |
| if (m_navigateTaskFactory->isPending()) |
| Platform::current()->currentThread()->scheduler()->removePendingNavigation( |
| m_frameType); |
| } |
| |
| bool NavigationScheduler::locationChangePending() { |
| return m_redirect && m_redirect->isLocationChange(); |
| } |
| |
| bool NavigationScheduler::isNavigationScheduledWithin(double interval) const { |
| return m_redirect && m_redirect->delay() <= interval; |
| } |
| |
| // TODO(dcheng): There are really two different load blocking concepts at work |
| // here and they have been incorrectly tangled together. |
| // |
| // 1. NavigationDisablerForUnload is for blocking navigation scheduling during |
| // a beforeunload or unload events. Scheduled navigations during |
| // beforeunload would make it possible to get trapped in an endless loop of |
| // beforeunload dialogs. Scheduled navigations during the unload handler |
| // makes is possible to cancel a navigation that was initiated right before |
| // it commits. |
| // |
| // Checking Frame::isNavigationAllowed() doesn't make sense in this context: |
| // NavigationScheduler is always cleared when a new load commits, so it's |
| // impossible for a scheduled navigation to clobber a navigation that just |
| // committed. |
| // |
| // 2. FrameNavigationDisabler / LocalFrame::isNavigationAllowed() are intended |
| // to prevent Documents from being reattached during destruction, since it |
| // can cause bugs with security origin confusion. This is primarily intended |
| // to block /synchronous/ navigations during things lke |
| // Document::detachLayoutTree(). |
| inline bool NavigationScheduler::shouldScheduleReload() const { |
| return m_frame->page() && m_frame->isNavigationAllowed() && |
| NavigationDisablerForUnload::isNavigationAllowed(); |
| } |
| |
| inline bool NavigationScheduler::shouldScheduleNavigation( |
| const String& url) const { |
| return m_frame->page() && m_frame->isNavigationAllowed() && |
| (protocolIsJavaScript(url) || |
| NavigationDisablerForUnload::isNavigationAllowed()); |
| } |
| |
| void NavigationScheduler::scheduleRedirect(double delay, const String& url) { |
| if (!shouldScheduleNavigation(url)) |
| return; |
| if (delay < 0 || delay > INT_MAX / 1000) |
| return; |
| if (url.isEmpty()) |
| return; |
| |
| // We want a new back/forward list item if the refresh timeout is > 1 second. |
| if (!m_redirect || delay <= m_redirect->delay()) |
| schedule( |
| ScheduledRedirect::create(delay, m_frame->document(), url, delay <= 1)); |
| } |
| |
| bool NavigationScheduler::mustReplaceCurrentItem(LocalFrame* targetFrame) { |
| // Non-user navigation before the page has finished firing onload should not |
| // create a new back/forward item. See https://webkit.org/b/42861 for the |
| // original motivation for this. |
| if (!targetFrame->document()->loadEventFinished() && |
| !UserGestureIndicator::utilizeUserGesture()) |
| return true; |
| |
| // Navigation of a subframe during loading of an ancestor frame does not |
| // create a new back/forward item. The definition of "during load" is any time |
| // before all handlers for the load event have been run. See |
| // https://bugs.webkit.org/show_bug.cgi?id=14957 for the original motivation |
| // for this. |
| Frame* parentFrame = targetFrame->tree().parent(); |
| return parentFrame && parentFrame->isLocalFrame() && |
| !toLocalFrame(parentFrame)->loader().allAncestorsAreComplete(); |
| } |
| |
| void NavigationScheduler::scheduleLocationChange(Document* originDocument, |
| const String& url, |
| bool replacesCurrentItem) { |
| if (!shouldScheduleNavigation(url)) |
| return; |
| |
| replacesCurrentItem = replacesCurrentItem || mustReplaceCurrentItem(m_frame); |
| |
| // If the URL we're going to navigate to is the same as the current one, |
| // except for the fragment part, we don't need to schedule the location |
| // change. We'll skip this optimization for cross-origin navigations to |
| // minimize the navigator's ability to execute timing attacks. |
| if (originDocument->getSecurityOrigin()->canAccess( |
| m_frame->document()->getSecurityOrigin())) { |
| KURL parsedURL(ParsedURLString, url); |
| if (parsedURL.hasFragmentIdentifier() && |
| equalIgnoringFragmentIdentifier(m_frame->document()->url(), |
| parsedURL)) { |
| FrameLoadRequest request(originDocument, |
| m_frame->document()->completeURL(url), "_self"); |
| request.setReplacesCurrentItem(replacesCurrentItem); |
| if (replacesCurrentItem) |
| request.setClientRedirect(ClientRedirectPolicy::ClientRedirect); |
| m_frame->loader().load(request); |
| return; |
| } |
| } |
| |
| schedule(ScheduledLocationChange::create(originDocument, url, |
| replacesCurrentItem)); |
| } |
| |
| void NavigationScheduler::schedulePageBlock(Document* originDocument) { |
| DCHECK(m_frame->page()); |
| const KURL& url = m_frame->document()->url(); |
| schedule(ScheduledPageBlock::create(originDocument, url)); |
| } |
| |
| void NavigationScheduler::scheduleFormSubmission(Document* document, |
| FormSubmission* submission) { |
| DCHECK(m_frame->page()); |
| schedule(ScheduledFormSubmission::create(document, submission, |
| mustReplaceCurrentItem(m_frame))); |
| } |
| |
| void NavigationScheduler::scheduleReload() { |
| if (!shouldScheduleReload()) |
| return; |
| if (m_frame->document()->url().isEmpty()) |
| return; |
| schedule(ScheduledReload::create()); |
| } |
| |
| void NavigationScheduler::navigateTask() { |
| Platform::current()->currentThread()->scheduler()->removePendingNavigation( |
| m_frameType); |
| |
| if (!m_frame->page()) |
| return; |
| if (m_frame->page()->defersLoading()) { |
| InspectorInstrumentation::frameClearedScheduledNavigation(m_frame); |
| return; |
| } |
| |
| ScheduledNavigation* redirect(m_redirect.release()); |
| redirect->fire(m_frame); |
| InspectorInstrumentation::frameClearedScheduledNavigation(m_frame); |
| } |
| |
| void NavigationScheduler::schedule(ScheduledNavigation* redirect) { |
| DCHECK(m_frame->page()); |
| |
| // In a back/forward navigation, we sometimes restore history state to |
| // iframes, even though the state was generated dynamically and JS will try to |
| // put something different in the iframe. In this case, we will load stale |
| // things and/or confuse the JS when it shortly thereafter tries to schedule a |
| // location change. Let the JS have its way. |
| // FIXME: This check seems out of place. |
| if (!m_frame->loader().stateMachine()->committedFirstRealDocumentLoad() && |
| m_frame->loader().provisionalDocumentLoader()) { |
| m_frame->loader().stopAllLoaders(); |
| if (!m_frame->host()) |
| return; |
| } |
| |
| cancel(); |
| m_redirect = redirect; |
| startTimer(); |
| } |
| |
| void NavigationScheduler::startTimer() { |
| if (!m_redirect) |
| return; |
| |
| DCHECK(m_frame->page()); |
| if (m_navigateTaskFactory->isPending()) |
| return; |
| if (!m_redirect->shouldStartTimer(m_frame)) |
| return; |
| |
| WebScheduler* scheduler = Platform::current()->currentThread()->scheduler(); |
| scheduler->addPendingNavigation(m_frameType); |
| scheduler->loadingTaskRunner()->postDelayedTask( |
| BLINK_FROM_HERE, m_navigateTaskFactory->cancelAndCreate(), |
| m_redirect->delay() * 1000.0); |
| |
| InspectorInstrumentation::frameScheduledNavigation(m_frame, |
| m_redirect->delay()); |
| } |
| |
| void NavigationScheduler::cancel() { |
| if (m_navigateTaskFactory->isPending()) { |
| Platform::current()->currentThread()->scheduler()->removePendingNavigation( |
| m_frameType); |
| InspectorInstrumentation::frameClearedScheduledNavigation(m_frame); |
| } |
| m_navigateTaskFactory->cancel(); |
| m_redirect.clear(); |
| } |
| |
| DEFINE_TRACE(NavigationScheduler) { |
| visitor->trace(m_frame); |
| visitor->trace(m_redirect); |
| } |
| |
| } // namespace blink |