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