| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /* |
| * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved. |
| * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies) |
| * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved. |
| * (http://www.torchmobile.com/) |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "content/renderer/history_controller.h" |
| |
| #include <utility> |
| |
| #include "base/memory/ptr_util.h" |
| #include "content/common/navigation_params.h" |
| #include "content/common/site_isolation_policy.h" |
| #include "content/renderer/render_frame_impl.h" |
| #include "content/renderer/render_view_impl.h" |
| #include "third_party/WebKit/public/web/WebFrameLoadType.h" |
| #include "third_party/WebKit/public/web/WebLocalFrame.h" |
| |
| using blink::WebCachePolicy; |
| using blink::WebFrame; |
| using blink::WebHistoryCommitType; |
| using blink::WebHistoryItem; |
| using blink::WebURLRequest; |
| |
| namespace content { |
| |
| HistoryController::HistoryController(RenderViewImpl* render_view) |
| : render_view_(render_view) { |
| // We don't use HistoryController in OOPIF enabled modes. |
| DCHECK(!SiteIsolationPolicy::UseSubframeNavigationEntries()); |
| } |
| |
| HistoryController::~HistoryController() { |
| } |
| |
| bool HistoryController::GoToEntry( |
| blink::WebLocalFrame* main_frame, |
| std::unique_ptr<HistoryEntry> target_entry, |
| std::unique_ptr<NavigationParams> navigation_params, |
| WebCachePolicy cache_policy) { |
| DCHECK(!main_frame->parent()); |
| HistoryFrameLoadVector same_document_loads; |
| HistoryFrameLoadVector different_document_loads; |
| |
| set_provisional_entry(std::move(target_entry)); |
| navigation_params_ = std::move(navigation_params); |
| |
| if (current_entry_) { |
| RecursiveGoToEntry( |
| main_frame, same_document_loads, different_document_loads); |
| } |
| |
| if (same_document_loads.empty() && different_document_loads.empty()) { |
| // If we don't have any frames to navigate at this point, either |
| // (1) there is no previous history entry to compare against, or |
| // (2) we were unable to match any frames by name. In the first case, |
| // doing a different document navigation to the root item is the only valid |
| // thing to do. In the second case, we should have been able to find a |
| // frame to navigate based on names if this were a same document |
| // navigation, so we can safely assume this is the different document case. |
| different_document_loads.push_back( |
| std::make_pair(main_frame, provisional_entry_->root())); |
| } |
| |
| bool has_main_frame_request = false; |
| for (const auto& item : same_document_loads) { |
| WebFrame* frame = item.first; |
| RenderFrameImpl* render_frame = RenderFrameImpl::FromWebFrame(frame); |
| if (!render_frame) |
| continue; |
| render_frame->SetPendingNavigationParams( |
| base::MakeUnique<NavigationParams>(*navigation_params_.get())); |
| WebURLRequest request = frame->toWebLocalFrame()->requestFromHistoryItem( |
| item.second, cache_policy); |
| frame->toWebLocalFrame()->load( |
| request, blink::WebFrameLoadType::BackForward, item.second, |
| blink::WebHistorySameDocumentLoad); |
| if (frame == main_frame) |
| has_main_frame_request = true; |
| } |
| for (const auto& item : different_document_loads) { |
| WebFrame* frame = item.first; |
| RenderFrameImpl* render_frame = RenderFrameImpl::FromWebFrame(frame); |
| if (!render_frame) |
| continue; |
| render_frame->SetPendingNavigationParams( |
| base::MakeUnique<NavigationParams>(*navigation_params_.get())); |
| WebURLRequest request = frame->toWebLocalFrame()->requestFromHistoryItem( |
| item.second, cache_policy); |
| frame->toWebLocalFrame()->load( |
| request, blink::WebFrameLoadType::BackForward, item.second, |
| blink::WebHistoryDifferentDocumentLoad); |
| if (frame == main_frame) |
| has_main_frame_request = true; |
| } |
| |
| return has_main_frame_request; |
| } |
| |
| void HistoryController::RecursiveGoToEntry( |
| WebFrame* frame, |
| HistoryFrameLoadVector& same_document_loads, |
| HistoryFrameLoadVector& different_document_loads) { |
| DCHECK(provisional_entry_); |
| DCHECK(current_entry_); |
| RenderFrameImpl* render_frame = RenderFrameImpl::FromWebFrame(frame); |
| const WebHistoryItem& new_item = |
| provisional_entry_->GetItemForFrame(render_frame); |
| |
| // Use the last committed history item for the frame rather than |
| // current_entry_, since the latter may not accurately reflect which URL is |
| // currently committed in the frame. See https://crbug.com/612713#c12. |
| const WebHistoryItem& old_item = render_frame->current_history_item(); |
| |
| if (new_item.isNull()) |
| return; |
| |
| if (old_item.isNull() || |
| new_item.itemSequenceNumber() != old_item.itemSequenceNumber()) { |
| if (!old_item.isNull() && |
| new_item.documentSequenceNumber() == |
| old_item.documentSequenceNumber()) { |
| same_document_loads.push_back(std::make_pair(frame, new_item)); |
| |
| // Returning here (and omitting child frames which have also changed) is |
| // wrong, but not returning here is worse. See the discussion in |
| // NavigationControllerImpl::FindFramesToNavigate for more information. |
| return; |
| } else { |
| different_document_loads.push_back(std::make_pair(frame, new_item)); |
| // For a different document, the subframes will be destroyed, so there's |
| // no need to consider them. |
| return; |
| } |
| } |
| |
| for (WebFrame* child = frame->firstChild(); child; |
| child = child->nextSibling()) { |
| RecursiveGoToEntry(child, same_document_loads, different_document_loads); |
| } |
| } |
| |
| void HistoryController::UpdateForInitialLoadInChildFrame( |
| RenderFrameImpl* frame, |
| const WebHistoryItem& item) { |
| DCHECK_NE(frame->GetWebFrame()->top(), frame->GetWebFrame()); |
| if (!current_entry_) |
| return; |
| if (HistoryEntry::HistoryNode* existing_node = |
| current_entry_->GetHistoryNodeForFrame(frame)) { |
| // Clear the children and any NavigationParams if this commit isn't for |
| // the same item. Otherwise we might have stale data after a redirect. |
| if (existing_node->item().itemSequenceNumber() != |
| item.itemSequenceNumber()) { |
| existing_node->RemoveChildren(); |
| navigation_params_.reset(); |
| } |
| existing_node->set_item(item); |
| return; |
| } |
| RenderFrameImpl* parent = |
| RenderFrameImpl::FromWebFrame(frame->GetWebFrame()->parent()); |
| if (!parent) |
| return; |
| if (HistoryEntry::HistoryNode* parent_history_node = |
| current_entry_->GetHistoryNodeForFrame(parent)) { |
| parent_history_node->AddChild(item); |
| } |
| } |
| |
| void HistoryController::UpdateForCommit(RenderFrameImpl* frame, |
| const WebHistoryItem& item, |
| WebHistoryCommitType commit_type, |
| bool navigation_within_page) { |
| switch (commit_type) { |
| case blink::WebBackForwardCommit: |
| if (!provisional_entry_) { |
| // The provisional entry may have been discarded due to a navigation in |
| // a different frame. For main frames, it is not safe to leave the |
| // current_entry_ in place, which may have a cross-site page and will be |
| // included in the PageState for this commit. Replace it with a new |
| // HistoryEntry corresponding to the commit, and clear any stale |
| // NavigationParams which might point to the wrong entry. |
| // |
| // This will lack any subframe history items that were in the original |
| // provisional entry, but we don't know what those were after discarding |
| // it. We'll load the default URL in those subframes instead. |
| // |
| // TODO(creis): It's also possible to get here for subframe commits. |
| // We'll leave a stale current_entry_ in that case, but that only causes |
| // an earlier URL to load in the subframe when leaving and coming back, |
| // and only in rare cases. It does not risk a URL spoof, unlike the |
| // main frame case. Since this bug is not present in the new |
| // FrameNavigationEntry-based navigation path (https://crbug.com/236848) |
| // we'll wait for that to fix the subframe case. |
| if (frame->IsMainFrame()) { |
| current_entry_.reset(new HistoryEntry(item)); |
| navigation_params_.reset(); |
| } |
| |
| return; |
| } |
| |
| // If the current entry is null, this must be a main frame commit. |
| DCHECK(current_entry_ || frame->IsMainFrame()); |
| |
| // Commit the provisional entry, but only if it is a plausible transition. |
| // Do not commit it if the navigation is in a subframe and the provisional |
| // entry's main frame item does not match the current entry's main frame, |
| // which can happen if multiple forward navigations occur. In that case, |
| // committing the provisional entry would corrupt it, leading to a URL |
| // spoof. See https://crbug.com/597322. (Note that the race in this bug |
| // does not affect main frame navigations, only navigations in subframes.) |
| // |
| // Note that we cannot compare the provisional entry against |item|, since |
| // |item| may have redirected to a different URL and ISN. We also cannot |
| // compare against the main frame's URL, since that may have changed due |
| // to a replaceState. (Even origin can change on replaceState in certain |
| // modes.) |
| // |
| // It would be safe to additionally check the ISNs of all parent frames |
| // (and not just the root), but that is less critical because it won't |
| // lead to a URL spoof. |
| if (frame->IsMainFrame() || |
| current_entry_->root().itemSequenceNumber() == |
| provisional_entry_->root().itemSequenceNumber()) { |
| current_entry_.reset(provisional_entry_.release()); |
| } |
| |
| // We're guaranteed to have a current entry now. |
| DCHECK(current_entry_); |
| |
| if (HistoryEntry::HistoryNode* node = |
| current_entry_->GetHistoryNodeForFrame(frame)) { |
| // Clear the children and any NavigationParams if this commit isn't for |
| // the same item. Otherwise we might have stale data from a race. |
| if (node->item().itemSequenceNumber() != item.itemSequenceNumber()) { |
| node->RemoveChildren(); |
| navigation_params_.reset(); |
| } |
| |
| node->set_item(item); |
| } |
| break; |
| case blink::WebStandardCommit: |
| CreateNewBackForwardItem(frame, item, navigation_within_page); |
| break; |
| case blink::WebInitialCommitInChildFrame: |
| UpdateForInitialLoadInChildFrame(frame, item); |
| break; |
| case blink::WebHistoryInertCommit: |
| // Even for inert commits (e.g., location.replace, client redirects), make |
| // sure the current entry gets updated, if there is one. |
| if (current_entry_) { |
| if (HistoryEntry::HistoryNode* node = |
| current_entry_->GetHistoryNodeForFrame(frame)) { |
| // Inert commits that reset the page without changing the item (e.g., |
| // reloads, location.replace) shouldn't keep the old subtree. |
| if (!navigation_within_page) |
| node->RemoveChildren(); |
| node->set_item(item); |
| } |
| } |
| break; |
| default: |
| NOTREACHED() << "Invalid commit type: " << commit_type; |
| } |
| } |
| |
| HistoryEntry* HistoryController::GetCurrentEntry() { |
| return current_entry_.get(); |
| } |
| |
| WebHistoryItem HistoryController::GetItemForNewChildFrame( |
| RenderFrameImpl* frame) const { |
| if (navigation_params_.get()) { |
| frame->SetPendingNavigationParams( |
| base::MakeUnique<NavigationParams>(*navigation_params_.get())); |
| } |
| |
| if (!current_entry_) |
| return WebHistoryItem(); |
| return current_entry_->GetItemForFrame(frame); |
| } |
| |
| void HistoryController::RemoveChildrenForRedirect(RenderFrameImpl* frame) { |
| if (!provisional_entry_) |
| return; |
| if (HistoryEntry::HistoryNode* node = |
| provisional_entry_->GetHistoryNodeForFrame(frame)) |
| node->RemoveChildren(); |
| } |
| |
| void HistoryController::CreateNewBackForwardItem( |
| RenderFrameImpl* target_frame, |
| const WebHistoryItem& new_item, |
| bool clone_children_of_target) { |
| if (!current_entry_) { |
| current_entry_.reset(new HistoryEntry(new_item)); |
| } else { |
| current_entry_.reset(current_entry_->CloneAndReplace( |
| new_item, clone_children_of_target, target_frame, render_view_)); |
| } |
| } |
| |
| } // namespace content |