1/*
2 * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
3 * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
4 * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/)
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 * 3. Neither the name of Apple Inc. ("Apple") nor the names of
16 * its contributors may be used to endorse or promote products derived
17 * from this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31#include "config.h"
32#include "HistoryController.h"
33
34#include "BackForwardController.h"
35#include "CachedPage.h"
36#include "Document.h"
37#include "DocumentLoader.h"
38#include "Frame.h"
39#include "FrameLoader.h"
40#include "FrameLoaderClient.h"
41#include "FrameLoaderStateMachine.h"
42#include "FrameTree.h"
43#include "FrameView.h"
44#include "HistoryItem.h"
45#include "Logging.h"
46#include "Page.h"
47#include "PageCache.h"
48#include "ScrollingCoordinator.h"
49#include "SerializedScriptValue.h"
50#include "SharedStringHash.h"
51#include "ShouldTreatAsContinuingLoad.h"
52#include "VisitedLinkStore.h"
53#include <wtf/text/CString.h>
54
55namespace WebCore {
56
57static inline void addVisitedLink(Page& page, const URL& url)
58{
59 page.visitedLinkStore().addVisitedLink(page, computeSharedStringHash(url.string()));
60}
61
62HistoryController::HistoryController(Frame& frame)
63 : m_frame(frame)
64 , m_frameLoadComplete(true)
65 , m_defersLoading(false)
66{
67}
68
69HistoryController::~HistoryController() = default;
70
71void HistoryController::saveScrollPositionAndViewStateToItem(HistoryItem* item)
72{
73 FrameView* frameView = m_frame.view();
74 if (!item || !frameView)
75 return;
76
77 if (m_frame.document()->pageCacheState() != Document::NotInPageCache)
78 item->setScrollPosition(frameView->cachedScrollPosition());
79 else
80 item->setScrollPosition(frameView->scrollPosition());
81
82#if PLATFORM(IOS_FAMILY)
83 item->setExposedContentRect(frameView->exposedContentRect());
84 item->setUnobscuredContentRect(frameView->unobscuredContentRect());
85#endif
86
87 Page* page = m_frame.page();
88 if (page && m_frame.isMainFrame()) {
89 item->setPageScaleFactor(page->pageScaleFactor() / page->viewScaleFactor());
90#if PLATFORM(IOS_FAMILY)
91 item->setObscuredInsets(page->obscuredInsets());
92#endif
93 }
94
95 // FIXME: It would be great to work out a way to put this code in WebCore instead of calling through to the client.
96 m_frame.loader().client().saveViewStateToItem(*item);
97
98 // Notify clients that the HistoryItem has changed.
99 item->notifyChanged();
100}
101
102void HistoryController::clearScrollPositionAndViewState()
103{
104 if (!m_currentItem)
105 return;
106
107 m_currentItem->clearScrollPosition();
108 m_currentItem->setPageScaleFactor(0);
109}
110
111/*
112 There is a race condition between the layout and load completion that affects restoring the scroll position.
113 We try to restore the scroll position at both the first layout and upon load completion.
114
115 1) If first layout happens before the load completes, we want to restore the scroll position then so that the
116 first time we draw the page is already scrolled to the right place, instead of starting at the top and later
117 jumping down. It is possible that the old scroll position is past the part of the doc laid out so far, in
118 which case the restore silent fails and we will fix it in when we try to restore on doc completion.
119 2) If the layout happens after the load completes, the attempt to restore at load completion time silently
120 fails. We then successfully restore it when the layout happens.
121*/
122void HistoryController::restoreScrollPositionAndViewState()
123{
124 if (!m_frame.loader().stateMachine().committedFirstRealDocumentLoad())
125 return;
126
127 ASSERT(m_currentItem);
128
129 // FIXME: As the ASSERT attests, it seems we should always have a currentItem here.
130 // One counterexample is <rdar://problem/4917290>
131 // For now, to cover this issue in release builds, there is no technical harm to returning
132 // early and from a user standpoint - as in the above radar - the previous page load failed
133 // so there *is* no scroll or view state to restore!
134 if (!m_currentItem)
135 return;
136
137 auto view = makeRefPtr(m_frame.view());
138
139 // FIXME: There is some scrolling related work that needs to happen whenever a page goes into the
140 // page cache and similar work that needs to occur when it comes out. This is where we do the work
141 // that needs to happen when we exit, and the work that needs to happen when we enter is in
142 // Document::setIsInPageCache(bool). It would be nice if there was more symmetry in these spots.
143 // https://bugs.webkit.org/show_bug.cgi?id=98698
144 if (view) {
145 Page* page = m_frame.page();
146 if (page && m_frame.isMainFrame()) {
147 if (ScrollingCoordinator* scrollingCoordinator = page->scrollingCoordinator())
148 scrollingCoordinator->frameViewRootLayerDidChange(*view);
149 }
150 }
151
152 // FIXME: It would be great to work out a way to put this code in WebCore instead of calling
153 // through to the client.
154 m_frame.loader().client().restoreViewState();
155
156#if !PLATFORM(IOS_FAMILY)
157 // Don't restore scroll point on iOS as FrameLoaderClient::restoreViewState() does that.
158 if (view && !view->wasScrolledByUser()) {
159 view->scrollToFocusedElementImmediatelyIfNeeded();
160
161 Page* page = m_frame.page();
162 auto desiredScrollPosition = m_currentItem->shouldRestoreScrollPosition() ? m_currentItem->scrollPosition() : view->scrollPosition();
163 LOG(Scrolling, "HistoryController::restoreScrollPositionAndViewState scrolling to %d,%d", desiredScrollPosition.x(), desiredScrollPosition.y());
164 if (page && m_frame.isMainFrame() && m_currentItem->pageScaleFactor())
165 page->setPageScaleFactor(m_currentItem->pageScaleFactor() * page->viewScaleFactor(), desiredScrollPosition);
166 else
167 view->setScrollPosition(desiredScrollPosition);
168
169 // If the scroll position doesn't have to be clamped, consider it successfully restored.
170 if (m_frame.isMainFrame()) {
171 auto adjustedDesiredScrollPosition = view->adjustScrollPositionWithinRange(desiredScrollPosition);
172 if (desiredScrollPosition == adjustedDesiredScrollPosition)
173 m_frame.loader().client().didRestoreScrollPosition();
174 }
175
176 }
177#endif
178}
179
180void HistoryController::updateBackForwardListForFragmentScroll()
181{
182 updateBackForwardListClippedAtTarget(false);
183}
184
185void HistoryController::saveDocumentState()
186{
187 // FIXME: Reading this bit of FrameLoader state here is unfortunate. I need to study
188 // this more to see if we can remove this dependency.
189 if (m_frame.loader().stateMachine().creatingInitialEmptyDocument())
190 return;
191
192 // For a standard page load, we will have a previous item set, which will be used to
193 // store the form state. However, in some cases we will have no previous item, and
194 // the current item is the right place to save the state. One example is when we
195 // detach a bunch of frames because we are navigating from a site with frames to
196 // another site. Another is when saving the frame state of a frame that is not the
197 // target of the current navigation (if we even decide to save with that granularity).
198
199 // Because of previousItem's "masking" of currentItem for this purpose, it's important
200 // that we keep track of the end of a page transition with m_frameLoadComplete. We
201 // leverage the checkLoadComplete recursion to achieve this goal.
202
203 HistoryItem* item = m_frameLoadComplete ? m_currentItem.get() : m_previousItem.get();
204 if (!item)
205 return;
206
207 ASSERT(m_frame.document());
208 Document& document = *m_frame.document();
209 if (item->isCurrentDocument(document) && document.hasLivingRenderTree()) {
210 if (DocumentLoader* documentLoader = document.loader())
211 item->setShouldOpenExternalURLsPolicy(documentLoader->shouldOpenExternalURLsPolicyToPropagate());
212
213 LOG(Loading, "WebCoreLoading %s: saving form state to %p", m_frame.tree().uniqueName().string().utf8().data(), item);
214 item->setDocumentState(document.formElementsState());
215 }
216}
217
218// Walk the frame tree, telling all frames to save their form state into their current
219// history item.
220void HistoryController::saveDocumentAndScrollState()
221{
222 for (Frame* frame = &m_frame; frame; frame = frame->tree().traverseNext(&m_frame)) {
223 frame->loader().history().saveDocumentState();
224 frame->loader().history().saveScrollPositionAndViewStateToItem(frame->loader().history().currentItem());
225 }
226}
227
228void HistoryController::restoreDocumentState()
229{
230 switch (m_frame.loader().loadType()) {
231 case FrameLoadType::Reload:
232 case FrameLoadType::ReloadFromOrigin:
233 case FrameLoadType::ReloadExpiredOnly:
234 case FrameLoadType::Same:
235 case FrameLoadType::Replace:
236 // Not restoring the document state.
237 return;
238 case FrameLoadType::Back:
239 case FrameLoadType::Forward:
240 case FrameLoadType::IndexedBackForward:
241 case FrameLoadType::RedirectWithLockedBackForwardList:
242 case FrameLoadType::Standard:
243 break;
244 }
245
246 if (!m_currentItem)
247 return;
248 if (m_frame.loader().requestedHistoryItem() != m_currentItem.get())
249 return;
250 if (m_frame.loader().documentLoader()->isClientRedirect())
251 return;
252
253 m_frame.loader().documentLoader()->setShouldOpenExternalURLsPolicy(m_currentItem->shouldOpenExternalURLsPolicy());
254
255 LOG(Loading, "WebCoreLoading %s: restoring form state from %p", m_frame.tree().uniqueName().string().utf8().data(), m_currentItem.get());
256 m_frame.document()->setStateForNewFormElements(m_currentItem->documentState());
257}
258
259void HistoryController::invalidateCurrentItemCachedPage()
260{
261 if (!currentItem())
262 return;
263
264 // When we are pre-commit, the currentItem is where any page cache data resides.
265 std::unique_ptr<CachedPage> cachedPage = PageCache::singleton().take(*currentItem(), m_frame.page());
266 if (!cachedPage)
267 return;
268
269 // FIXME: This is a grotesque hack to fix <rdar://problem/4059059> Crash in RenderFlow::detach
270 // Somehow the PageState object is not properly updated, and is holding onto a stale document.
271 // Both Xcode and FileMaker see this crash, Safari does not.
272
273 ASSERT(cachedPage->document() == m_frame.document());
274 if (cachedPage->document() == m_frame.document()) {
275 cachedPage->document()->setPageCacheState(Document::NotInPageCache);
276 cachedPage->clear();
277 }
278}
279
280bool HistoryController::shouldStopLoadingForHistoryItem(HistoryItem& targetItem) const
281{
282 if (!m_currentItem)
283 return false;
284
285 // Don't abort the current load if we're navigating within the current document.
286 if (m_currentItem->shouldDoSameDocumentNavigationTo(targetItem))
287 return false;
288
289 return true;
290}
291
292// Main funnel for navigating to a previous location (back/forward, non-search snap-back)
293// This includes recursion to handle loading into framesets properly
294void HistoryController::goToItem(HistoryItem& targetItem, FrameLoadType type, ShouldTreatAsContinuingLoad shouldTreatAsContinuingLoad)
295{
296 LOG(History, "HistoryController %p goToItem %p type=%d", this, &targetItem, static_cast<int>(type));
297
298 ASSERT(!m_frame.tree().parent());
299
300 // shouldGoToHistoryItem is a private delegate method. This is needed to fix:
301 // <rdar://problem/3951283> can view pages from the back/forward cache that should be disallowed by Parental Controls
302 // Ultimately, history item navigations should go through the policy delegate. That's covered in:
303 // <rdar://problem/3979539> back/forward cache navigations should consult policy delegate
304 Page* page = m_frame.page();
305 if (!page)
306 return;
307 if (!m_frame.loader().client().shouldGoToHistoryItem(targetItem))
308 return;
309 if (m_defersLoading) {
310 m_deferredItem = &targetItem;
311 m_deferredFrameLoadType = type;
312 return;
313 }
314
315 // Set the BF cursor before commit, which lets the user quickly click back/forward again.
316 // - plus, it only makes sense for the top level of the operation through the frame tree,
317 // as opposed to happening for some/one of the page commits that might happen soon
318 RefPtr<HistoryItem> currentItem = page->backForward().currentItem();
319 page->backForward().setCurrentItem(targetItem);
320
321 // First set the provisional item of any frames that are not actually navigating.
322 // This must be done before trying to navigate the desired frame, because some
323 // navigations can commit immediately (such as about:blank). We must be sure that
324 // all frames have provisional items set before the commit.
325 recursiveSetProvisionalItem(targetItem, currentItem.get());
326
327 // Now that all other frames have provisional items, do the actual navigation.
328 recursiveGoToItem(targetItem, currentItem.get(), type, shouldTreatAsContinuingLoad);
329}
330
331void HistoryController::setDefersLoading(bool defer)
332{
333 m_defersLoading = defer;
334 if (!defer && m_deferredItem) {
335 goToItem(*m_deferredItem, m_deferredFrameLoadType, ShouldTreatAsContinuingLoad::No);
336 m_deferredItem = nullptr;
337 }
338}
339
340void HistoryController::updateForBackForwardNavigation()
341{
342 LOG(History, "HistoryController %p updateForBackForwardNavigation: Updating History for back/forward navigation in frame %p (main frame %d) %s", this, &m_frame, m_frame.isMainFrame(), m_frame.loader().documentLoader() ? m_frame.loader().documentLoader()->url().string().utf8().data() : "");
343
344 // Must grab the current scroll position before disturbing it
345 if (!m_frameLoadComplete)
346 saveScrollPositionAndViewStateToItem(m_previousItem.get());
347
348 // When traversing history, we may end up redirecting to a different URL
349 // this time (e.g., due to cookies). See http://webkit.org/b/49654.
350 updateCurrentItem();
351}
352
353void HistoryController::updateForReload()
354{
355 LOG(History, "HistoryController %p updateForReload: Updating History for reload in frame %p (main frame %d) %s", this, &m_frame, m_frame.isMainFrame(), m_frame.loader().documentLoader() ? m_frame.loader().documentLoader()->url().string().utf8().data() : "");
356
357 if (m_currentItem) {
358 PageCache::singleton().remove(*m_currentItem);
359
360 if (m_frame.loader().loadType() == FrameLoadType::Reload || m_frame.loader().loadType() == FrameLoadType::ReloadFromOrigin)
361 saveScrollPositionAndViewStateToItem(m_currentItem.get());
362
363 // Rebuild the history item tree when reloading as trying to re-associate everything is too error-prone.
364 m_currentItem->clearChildren();
365 }
366
367 // When reloading the page, we may end up redirecting to a different URL
368 // this time (e.g., due to cookies). See http://webkit.org/b/4072.
369 updateCurrentItem();
370}
371
372// There are 3 things you might think of as "history", all of which are handled by these functions.
373//
374// 1) Back/forward: The m_currentItem is part of this mechanism.
375// 2) Global history: Handled by the client.
376// 3) Visited links: Handled by the PageGroup.
377
378void HistoryController::updateForStandardLoad(HistoryUpdateType updateType)
379{
380 LOG(History, "HistoryController %p updateForStandardLoad: Updating History for standard load in frame %p (main frame %d) %s", this, &m_frame, m_frame.isMainFrame(), m_frame.loader().documentLoader()->url().string().ascii().data());
381
382 FrameLoader& frameLoader = m_frame.loader();
383
384 bool needPrivacy = m_frame.page() ? m_frame.page()->usesEphemeralSession() : true;
385 const URL& historyURL = frameLoader.documentLoader()->urlForHistory();
386
387 if (!frameLoader.documentLoader()->isClientRedirect()) {
388 if (!historyURL.isEmpty()) {
389 if (updateType != UpdateAllExceptBackForwardList)
390 updateBackForwardListClippedAtTarget(true);
391 if (!needPrivacy) {
392 frameLoader.client().updateGlobalHistory();
393 frameLoader.documentLoader()->setDidCreateGlobalHistoryEntry(true);
394 if (frameLoader.documentLoader()->unreachableURL().isEmpty())
395 frameLoader.client().updateGlobalHistoryRedirectLinks();
396 }
397 }
398 } else {
399 // The client redirect replaces the current history item.
400 updateCurrentItem();
401 }
402
403 if (!historyURL.isEmpty() && !needPrivacy) {
404 if (Page* page = m_frame.page())
405 addVisitedLink(*page, historyURL);
406
407 if (!frameLoader.documentLoader()->didCreateGlobalHistoryEntry() && frameLoader.documentLoader()->unreachableURL().isEmpty() && !m_frame.document()->url().isEmpty())
408 frameLoader.client().updateGlobalHistoryRedirectLinks();
409 }
410}
411
412void HistoryController::updateForRedirectWithLockedBackForwardList()
413{
414 LOG(History, "HistoryController %p updateForRedirectWithLockedBackForwardList: Updating History for redirect load in frame %p (main frame %d) %s", this, &m_frame, m_frame.isMainFrame(), m_frame.loader().documentLoader() ? m_frame.loader().documentLoader()->url().string().utf8().data() : "");
415
416 bool needPrivacy = m_frame.page() ? m_frame.page()->usesEphemeralSession() : true;
417 const URL& historyURL = m_frame.loader().documentLoader()->urlForHistory();
418
419 if (m_frame.loader().documentLoader()->isClientRedirect()) {
420 if (!m_currentItem && !m_frame.tree().parent()) {
421 if (!historyURL.isEmpty()) {
422 updateBackForwardListClippedAtTarget(true);
423 if (!needPrivacy) {
424 m_frame.loader().client().updateGlobalHistory();
425 m_frame.loader().documentLoader()->setDidCreateGlobalHistoryEntry(true);
426 if (m_frame.loader().documentLoader()->unreachableURL().isEmpty())
427 m_frame.loader().client().updateGlobalHistoryRedirectLinks();
428 }
429 }
430 }
431 // The client redirect replaces the current history item.
432 updateCurrentItem();
433 } else {
434 Frame* parentFrame = m_frame.tree().parent();
435 if (parentFrame && parentFrame->loader().history().currentItem())
436 parentFrame->loader().history().currentItem()->setChildItem(createItem());
437 }
438
439 if (!historyURL.isEmpty() && !needPrivacy) {
440 if (Page* page = m_frame.page())
441 addVisitedLink(*page, historyURL);
442
443 if (!m_frame.loader().documentLoader()->didCreateGlobalHistoryEntry() && m_frame.loader().documentLoader()->unreachableURL().isEmpty())
444 m_frame.loader().client().updateGlobalHistoryRedirectLinks();
445 }
446}
447
448void HistoryController::updateForClientRedirect()
449{
450 LOG(History, "HistoryController %p updateForClientRedirect: Updating History for client redirect in frame %p (main frame %d) %s", this, &m_frame, m_frame.isMainFrame(), m_frame.loader().documentLoader() ? m_frame.loader().documentLoader()->url().string().utf8().data() : "");
451
452 // Clear out form data so we don't try to restore it into the incoming page. Must happen after
453 // webcore has closed the URL and saved away the form state.
454 if (m_currentItem) {
455 m_currentItem->clearDocumentState();
456 m_currentItem->clearScrollPosition();
457 }
458
459 bool needPrivacy = m_frame.page() ? m_frame.page()->usesEphemeralSession() : true;
460 const URL& historyURL = m_frame.loader().documentLoader()->urlForHistory();
461
462 if (!historyURL.isEmpty() && !needPrivacy) {
463 if (Page* page = m_frame.page())
464 addVisitedLink(*page, historyURL);
465 }
466}
467
468void HistoryController::updateForCommit()
469{
470 FrameLoader& frameLoader = m_frame.loader();
471 LOG(History, "HistoryController %p updateForCommit: Updating History for commit in frame %p (main frame %d) %s", this, &m_frame, m_frame.isMainFrame(), m_frame.loader().documentLoader() ? m_frame.loader().documentLoader()->url().string().utf8().data() : "");
472
473 FrameLoadType type = frameLoader.loadType();
474 if (isBackForwardLoadType(type)
475 || isReplaceLoadTypeWithProvisionalItem(type)
476 || (isReloadTypeWithProvisionalItem(type) && !frameLoader.provisionalDocumentLoader()->unreachableURL().isEmpty())) {
477 // Once committed, we want to use current item for saving DocState, and
478 // the provisional item for restoring state.
479 // Note previousItem must be set before we close the URL, which will
480 // happen when the data source is made non-provisional below
481
482 // FIXME: https://bugs.webkit.org/show_bug.cgi?id=146842
483 // We should always have a provisional item when committing, but we sometimes don't.
484 // Not having one leads to us not having a m_currentItem later, which is also a terrible known issue.
485 // We should get to the bottom of this.
486 ASSERT(m_provisionalItem);
487 if (m_provisionalItem)
488 setCurrentItem(*m_provisionalItem.get());
489 m_provisionalItem = nullptr;
490
491 // Tell all other frames in the tree to commit their provisional items and
492 // restore their scroll position. We'll avoid this frame (which has already
493 // committed) and its children (which will be replaced).
494 m_frame.mainFrame().loader().history().recursiveUpdateForCommit();
495 }
496}
497
498bool HistoryController::isReplaceLoadTypeWithProvisionalItem(FrameLoadType type)
499{
500 // Going back to an error page in a subframe can trigger a FrameLoadType::Replace
501 // while m_provisionalItem is set, so we need to commit it.
502 return type == FrameLoadType::Replace && m_provisionalItem;
503}
504
505bool HistoryController::isReloadTypeWithProvisionalItem(FrameLoadType type)
506{
507 return (type == FrameLoadType::Reload || type == FrameLoadType::ReloadFromOrigin) && m_provisionalItem;
508}
509
510void HistoryController::recursiveUpdateForCommit()
511{
512 // The frame that navigated will now have a null provisional item.
513 // Ignore it and its children.
514 if (!m_provisionalItem)
515 return;
516
517 // For each frame that already had the content the item requested (based on
518 // (a matching URL and frame tree snapshot), just restore the scroll position.
519 // Save form state (works from currentItem, since m_frameLoadComplete is true)
520 if (m_currentItem && itemsAreClones(*m_currentItem, m_provisionalItem.get())) {
521 ASSERT(m_frameLoadComplete);
522 saveDocumentState();
523 saveScrollPositionAndViewStateToItem(m_currentItem.get());
524
525 if (FrameView* view = m_frame.view())
526 view->setWasScrolledByUser(false);
527
528 // Now commit the provisional item
529 if (m_provisionalItem)
530 setCurrentItem(*m_provisionalItem.get());
531 m_provisionalItem = nullptr;
532
533 // Restore form state (works from currentItem)
534 restoreDocumentState();
535
536 // Restore the scroll position (we choose to do this rather than going back to the anchor point)
537 restoreScrollPositionAndViewState();
538 }
539
540 // Iterate over the rest of the tree
541 for (Frame* child = m_frame.tree().firstChild(); child; child = child->tree().nextSibling())
542 child->loader().history().recursiveUpdateForCommit();
543}
544
545void HistoryController::updateForSameDocumentNavigation()
546{
547 if (m_frame.document()->url().isEmpty())
548 return;
549
550 Page* page = m_frame.page();
551 if (!page)
552 return;
553
554 if (page->usesEphemeralSession())
555 return;
556
557 addVisitedLink(*page, m_frame.document()->url());
558 m_frame.mainFrame().loader().history().recursiveUpdateForSameDocumentNavigation();
559
560 if (m_currentItem) {
561 m_currentItem->setURL(m_frame.document()->url());
562 m_frame.loader().client().updateGlobalHistory();
563 }
564}
565
566void HistoryController::recursiveUpdateForSameDocumentNavigation()
567{
568 // The frame that navigated will now have a null provisional item.
569 // Ignore it and its children.
570 if (!m_provisionalItem)
571 return;
572
573 // The provisional item may represent a different pending navigation.
574 // Don't commit it if it isn't a same document navigation.
575 if (m_currentItem && !m_currentItem->shouldDoSameDocumentNavigationTo(*m_provisionalItem))
576 return;
577
578 // Commit the provisional item.
579 if (m_provisionalItem)
580 setCurrentItem(*m_provisionalItem.get());
581 m_provisionalItem = nullptr;
582
583 // Iterate over the rest of the tree.
584 for (Frame* child = m_frame.tree().firstChild(); child; child = child->tree().nextSibling())
585 child->loader().history().recursiveUpdateForSameDocumentNavigation();
586}
587
588void HistoryController::updateForFrameLoadCompleted()
589{
590 // Even if already complete, we might have set a previous item on a frame that
591 // didn't do any data loading on the past transaction. Make sure to track that
592 // the load is complete so that we use the current item instead.
593 m_frameLoadComplete = true;
594}
595
596void HistoryController::setCurrentItem(HistoryItem& item)
597{
598 m_frameLoadComplete = false;
599 m_previousItem = m_currentItem;
600 m_currentItem = &item;
601}
602
603void HistoryController::setCurrentItemTitle(const StringWithDirection& title)
604{
605 // FIXME: This ignores the title's direction.
606 if (m_currentItem)
607 m_currentItem->setTitle(title.string);
608}
609
610bool HistoryController::currentItemShouldBeReplaced() const
611{
612 // From the HTML5 spec for location.assign():
613 // "If the browsing context's session history contains only one Document,
614 // and that was the about:blank Document created when the browsing context
615 // was created, then the navigation must be done with replacement enabled."
616 return m_currentItem && !m_previousItem && equalIgnoringASCIICase(m_currentItem->urlString(), WTF::blankURL());
617}
618
619void HistoryController::clearPreviousItem()
620{
621 m_previousItem = nullptr;
622 for (Frame* child = m_frame.tree().firstChild(); child; child = child->tree().nextSibling())
623 child->loader().history().clearPreviousItem();
624}
625
626void HistoryController::setProvisionalItem(HistoryItem* item)
627{
628 m_provisionalItem = item;
629}
630
631void HistoryController::initializeItem(HistoryItem& item)
632{
633 DocumentLoader* documentLoader = m_frame.loader().documentLoader();
634 ASSERT(documentLoader);
635
636 URL unreachableURL = documentLoader->unreachableURL();
637
638 URL url;
639 URL originalURL;
640
641 if (!unreachableURL.isEmpty()) {
642 url = unreachableURL;
643 originalURL = unreachableURL;
644 } else {
645 url = documentLoader->url();
646 originalURL = documentLoader->originalURL();
647 }
648
649 // Frames that have never successfully loaded any content
650 // may have no URL at all. Currently our history code can't
651 // deal with such things, so we nip that in the bud here.
652 // Later we may want to learn to live with nil for URL.
653 // See bug 3368236 and related bugs for more information.
654 if (url.isEmpty())
655 url = WTF::blankURL();
656 if (originalURL.isEmpty())
657 originalURL = WTF::blankURL();
658
659 StringWithDirection title = documentLoader->title();
660
661 item.setURL(url);
662 item.setTarget(m_frame.tree().uniqueName());
663 // FIXME: Should store the title direction as well.
664 item.setTitle(title.string);
665 item.setOriginalURLString(originalURL.string());
666
667 if (!unreachableURL.isEmpty() || documentLoader->response().httpStatusCode() >= 400)
668 item.setLastVisitWasFailure(true);
669
670 item.setShouldOpenExternalURLsPolicy(documentLoader->shouldOpenExternalURLsPolicyToPropagate());
671
672 // Save form state if this is a POST
673 item.setFormInfoFromRequest(documentLoader->request());
674}
675
676Ref<HistoryItem> HistoryController::createItem()
677{
678 Ref<HistoryItem> item = HistoryItem::create();
679 initializeItem(item);
680
681 // Set the item for which we will save document state
682 setCurrentItem(item);
683
684 return item;
685}
686
687Ref<HistoryItem> HistoryController::createItemTree(Frame& targetFrame, bool clipAtTarget)
688{
689 Ref<HistoryItem> bfItem = createItem();
690 if (!m_frameLoadComplete)
691 saveScrollPositionAndViewStateToItem(m_previousItem.get());
692
693 if (!clipAtTarget || &m_frame != &targetFrame) {
694 // save frame state for items that aren't loading (khtml doesn't save those)
695 saveDocumentState();
696
697 // clipAtTarget is false for navigations within the same document, so
698 // we should copy the documentSequenceNumber over to the newly create
699 // item. Non-target items are just clones, and they should therefore
700 // preserve the same itemSequenceNumber.
701 if (m_previousItem) {
702 if (&m_frame != &targetFrame)
703 bfItem->setItemSequenceNumber(m_previousItem->itemSequenceNumber());
704 bfItem->setDocumentSequenceNumber(m_previousItem->documentSequenceNumber());
705 }
706
707 for (Frame* child = m_frame.tree().firstChild(); child; child = child->tree().nextSibling()) {
708 FrameLoader& childLoader = child->loader();
709 bool hasChildLoaded = childLoader.frameHasLoaded();
710
711 // If the child is a frame corresponding to an <object> element that never loaded,
712 // we don't want to create a history item, because that causes fallback content
713 // to be ignored on reload.
714
715 if (!(!hasChildLoaded && childLoader.isHostedByObjectElement()))
716 bfItem->addChildItem(childLoader.history().createItemTree(targetFrame, clipAtTarget));
717 }
718 }
719 // FIXME: Eliminate the isTargetItem flag in favor of itemSequenceNumber.
720 if (&m_frame == &targetFrame)
721 bfItem->setIsTargetItem(true);
722 return bfItem;
723}
724
725// The general idea here is to traverse the frame tree and the item tree in parallel,
726// tracking whether each frame already has the content the item requests. If there is
727// a match, we set the provisional item and recurse. Otherwise we will reload that
728// frame and all its kids in recursiveGoToItem.
729void HistoryController::recursiveSetProvisionalItem(HistoryItem& item, HistoryItem* fromItem)
730{
731 if (!itemsAreClones(item, fromItem))
732 return;
733
734 // Set provisional item, which will be committed in recursiveUpdateForCommit.
735 m_provisionalItem = &item;
736
737 for (auto& childItem : item.children()) {
738 const String& childFrameName = childItem->target();
739
740 HistoryItem* fromChildItem = fromItem->childItemWithTarget(childFrameName);
741 ASSERT(fromChildItem);
742 Frame* childFrame = m_frame.tree().child(childFrameName);
743 ASSERT(childFrame);
744
745 childFrame->loader().history().recursiveSetProvisionalItem(childItem, fromChildItem);
746 }
747}
748
749// We now traverse the frame tree and item tree a second time, loading frames that
750// do have the content the item requests.
751void HistoryController::recursiveGoToItem(HistoryItem& item, HistoryItem* fromItem, FrameLoadType type, ShouldTreatAsContinuingLoad shouldTreatAsContinuingLoad)
752{
753 if (!itemsAreClones(item, fromItem)) {
754 m_frame.loader().loadItem(item, fromItem, type, shouldTreatAsContinuingLoad);
755 return;
756 }
757
758 // Just iterate over the rest, looking for frames to navigate.
759 for (auto& childItem : item.children()) {
760 const String& childFrameName = childItem->target();
761
762 HistoryItem* fromChildItem = fromItem->childItemWithTarget(childFrameName);
763 ASSERT(fromChildItem);
764 if (Frame* childFrame = m_frame.tree().child(childFrameName))
765 childFrame->loader().history().recursiveGoToItem(childItem, fromChildItem, type, shouldTreatAsContinuingLoad);
766 }
767}
768
769// The following logic must be kept in sync with WebKit::WebBackForwardListItem::itemIsClone().
770bool HistoryController::itemsAreClones(HistoryItem& item1, HistoryItem* item2) const
771{
772 // If the item we're going to is a clone of the item we're at, then we do
773 // not need to load it again. The current frame tree and the frame tree
774 // snapshot in the item have to match.
775 // Note: Some clients treat a navigation to the current history item as
776 // a reload. Thus, if item1 and item2 are the same, we need to create a
777 // new document and should not consider them clones.
778 // (See http://webkit.org/b/35532 for details.)
779 return item2
780 && &item1 != item2
781 && item1.itemSequenceNumber() == item2->itemSequenceNumber()
782 && currentFramesMatchItem(item1)
783 && item2->hasSameFrames(item1);
784}
785
786// Helper method that determines whether the current frame tree matches given history item's.
787bool HistoryController::currentFramesMatchItem(HistoryItem& item) const
788{
789 if ((!m_frame.tree().uniqueName().isEmpty() || !item.target().isEmpty()) && m_frame.tree().uniqueName() != item.target())
790 return false;
791
792 const auto& childItems = item.children();
793 if (childItems.size() != m_frame.tree().childCount())
794 return false;
795
796 for (auto& item : childItems) {
797 if (!m_frame.tree().child(item->target()))
798 return false;
799 }
800
801 return true;
802}
803
804void HistoryController::updateBackForwardListClippedAtTarget(bool doClip)
805{
806 // In the case of saving state about a page with frames, we store a tree of items that mirrors the frame tree.
807 // The item that was the target of the user's navigation is designated as the "targetItem".
808 // When this function is called with doClip=true we're able to create the whole tree except for the target's children,
809 // which will be loaded in the future. That part of the tree will be filled out as the child loads are committed.
810
811 Page* page = m_frame.page();
812 if (!page)
813 return;
814
815 if (m_frame.loader().documentLoader()->urlForHistory().isEmpty())
816 return;
817
818 FrameLoader& frameLoader = m_frame.mainFrame().loader();
819
820 Ref<HistoryItem> topItem = frameLoader.history().createItemTree(m_frame, doClip);
821 LOG(History, "HistoryController %p updateBackForwardListClippedAtTarget: Adding backforward item %p in frame %p (main frame %d) %s", this, topItem.ptr(), &m_frame, m_frame.isMainFrame(), m_frame.loader().documentLoader()->url().string().utf8().data());
822
823 page->backForward().addItem(WTFMove(topItem));
824}
825
826void HistoryController::updateCurrentItem()
827{
828 if (!m_currentItem)
829 return;
830
831 DocumentLoader* documentLoader = m_frame.loader().documentLoader();
832
833 if (!documentLoader->unreachableURL().isEmpty())
834 return;
835
836 if (m_currentItem->url() != documentLoader->url()) {
837 // We ended up on a completely different URL this time, so the HistoryItem
838 // needs to be re-initialized. Preserve the isTargetItem flag as it is a
839 // property of how this HistoryItem was originally created and is not
840 // dependent on the document.
841 bool isTargetItem = m_currentItem->isTargetItem();
842 m_currentItem->reset();
843 initializeItem(*m_currentItem);
844 m_currentItem->setIsTargetItem(isTargetItem);
845 } else {
846 // Even if the final URL didn't change, the form data may have changed.
847 m_currentItem->setFormInfoFromRequest(documentLoader->request());
848 }
849}
850
851void HistoryController::pushState(RefPtr<SerializedScriptValue>&& stateObject, const String& title, const String& urlString)
852{
853 if (!m_currentItem)
854 return;
855
856 Page* page = m_frame.page();
857 ASSERT(page);
858
859 bool shouldRestoreScrollPosition = m_currentItem->shouldRestoreScrollPosition();
860
861 // Get a HistoryItem tree for the current frame tree.
862 Ref<HistoryItem> topItem = m_frame.mainFrame().loader().history().createItemTree(m_frame, false);
863
864 // Override data in the current item (created by createItemTree) to reflect
865 // the pushState() arguments.
866 m_currentItem->setTitle(title);
867 m_currentItem->setStateObject(WTFMove(stateObject));
868 m_currentItem->setURLString(urlString);
869 m_currentItem->setShouldRestoreScrollPosition(shouldRestoreScrollPosition);
870
871 LOG(History, "HistoryController %p pushState: Adding top item %p, setting url of current item %p to %s, scrollRestoration is %s", this, topItem.ptr(), m_currentItem.get(), urlString.ascii().data(), topItem->shouldRestoreScrollPosition() ? "auto" : "manual");
872
873 page->backForward().addItem(WTFMove(topItem));
874
875 if (m_frame.page()->usesEphemeralSession())
876 return;
877
878 addVisitedLink(*page, URL({ }, urlString));
879 m_frame.loader().client().updateGlobalHistory();
880}
881
882void HistoryController::replaceState(RefPtr<SerializedScriptValue>&& stateObject, const String& title, const String& urlString)
883{
884 if (!m_currentItem)
885 return;
886
887 LOG(History, "HistoryController %p replaceState: Setting url of current item %p to %s scrollRestoration %s", this, m_currentItem.get(), urlString.ascii().data(), m_currentItem->shouldRestoreScrollPosition() ? "auto" : "manual");
888
889 if (!urlString.isEmpty())
890 m_currentItem->setURLString(urlString);
891 m_currentItem->setTitle(title);
892 m_currentItem->setStateObject(WTFMove(stateObject));
893 m_currentItem->setFormData(nullptr);
894 m_currentItem->setFormContentType(String());
895
896 ASSERT(m_frame.page());
897 if (m_frame.page()->usesEphemeralSession())
898 return;
899
900 addVisitedLink(*m_frame.page(), URL({ }, urlString));
901 m_frame.loader().client().updateGlobalHistory();
902}
903
904void HistoryController::replaceCurrentItem(HistoryItem* item)
905{
906 if (!item)
907 return;
908
909 m_previousItem = nullptr;
910 if (m_provisionalItem)
911 m_provisionalItem = item;
912 else
913 m_currentItem = item;
914}
915
916} // namespace WebCore
917