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 | |
55 | namespace WebCore { |
56 | |
57 | static inline void addVisitedLink(Page& page, const URL& url) |
58 | { |
59 | page.visitedLinkStore().addVisitedLink(page, computeSharedStringHash(url.string())); |
60 | } |
61 | |
62 | HistoryController::HistoryController(Frame& frame) |
63 | : m_frame(frame) |
64 | , m_frameLoadComplete(true) |
65 | , m_defersLoading(false) |
66 | { |
67 | } |
68 | |
69 | HistoryController::~HistoryController() = default; |
70 | |
71 | void 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 | |
102 | void 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 | */ |
122 | void 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 | |
180 | void HistoryController::updateBackForwardListForFragmentScroll() |
181 | { |
182 | updateBackForwardListClippedAtTarget(false); |
183 | } |
184 | |
185 | void 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. |
220 | void 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 | |
228 | void 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 | |
259 | void 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 | |
280 | bool 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 |
294 | void 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 | |
331 | void 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 | |
340 | void 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 | |
353 | void 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 | |
378 | void 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 | |
412 | void 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 | |
448 | void 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 | |
468 | void 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 | |
498 | bool 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 | |
505 | bool HistoryController::isReloadTypeWithProvisionalItem(FrameLoadType type) |
506 | { |
507 | return (type == FrameLoadType::Reload || type == FrameLoadType::ReloadFromOrigin) && m_provisionalItem; |
508 | } |
509 | |
510 | void 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 | |
545 | void 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 | |
566 | void 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 | |
588 | void 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 | |
596 | void HistoryController::setCurrentItem(HistoryItem& item) |
597 | { |
598 | m_frameLoadComplete = false; |
599 | m_previousItem = m_currentItem; |
600 | m_currentItem = &item; |
601 | } |
602 | |
603 | void 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 | |
610 | bool 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 | |
619 | void 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 | |
626 | void HistoryController::setProvisionalItem(HistoryItem* item) |
627 | { |
628 | m_provisionalItem = item; |
629 | } |
630 | |
631 | void 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 | |
676 | Ref<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 | |
687 | Ref<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. |
729 | void 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. |
751 | void 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(). |
770 | bool 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. |
787 | bool 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 | |
804 | void 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 | |
826 | void 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 | |
851 | void 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 | |
882 | void 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 | |
904 | void 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 | |