blob: bdbcbc5947bcde437c2bd9a4b6ae12ae611fd60d [file] [log] [blame]
// Copyright 2017 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.
#import "ios/web/navigation/navigation_manager_impl.h"
#include <algorithm>
#include "base/metrics/histogram_macros.h"
#import "ios/web/navigation/navigation_manager_delegate.h"
#import "ios/web/navigation/wk_navigation_util.h"
#import "ios/web/public/web_client.h"
#include "ui/base/page_transition_types.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace web {
const char kRestoreNavigationItemCount[] = "IOS.RestoreNavigationItemCount";
NavigationManager::WebLoadParams::WebLoadParams(const GURL& url)
: url(url),
transition_type(ui::PAGE_TRANSITION_LINK),
user_agent_override_option(UserAgentOverrideOption::INHERIT),
is_renderer_initiated(false),
post_data(nil) {}
NavigationManager::WebLoadParams::~WebLoadParams() {}
NavigationManager::WebLoadParams::WebLoadParams(const WebLoadParams& other)
: url(other.url),
referrer(other.referrer),
transition_type(other.transition_type),
user_agent_override_option(other.user_agent_override_option),
is_renderer_initiated(other.is_renderer_initiated),
extra_headers([other.extra_headers copy]),
post_data([other.post_data copy]) {}
NavigationManager::WebLoadParams& NavigationManager::WebLoadParams::operator=(
const WebLoadParams& other) {
url = other.url;
referrer = other.referrer;
is_renderer_initiated = other.is_renderer_initiated;
transition_type = other.transition_type;
user_agent_override_option = other.user_agent_override_option;
extra_headers = [other.extra_headers copy];
post_data = [other.post_data copy];
return *this;
}
/* static */
NavigationItem* NavigationManagerImpl::GetLastCommittedNonRedirectedItem(
const NavigationManager* nav_manager) {
if (!nav_manager || !nav_manager->GetItemCount())
return nullptr;
int index = nav_manager->GetLastCommittedItemIndex();
while (index >= 0) {
NavigationItem* item = nav_manager->GetItemAtIndex(index);
// Returns the first non-Redirect item found.
if (!ui::PageTransitionIsRedirect(item->GetTransitionType()))
return item;
--index;
}
return nullptr;
}
/* static */
bool NavigationManagerImpl::IsFragmentChangeNavigationBetweenUrls(
const GURL& existing_url,
const GURL& new_url) {
// TODO(crbug.com/749542): Current implementation incorrectly returns false
// if URL changes from http://google.com#foo to http://google.com.
if (existing_url == new_url || !new_url.has_ref())
return false;
return existing_url.EqualsIgnoringRef(new_url);
}
/* static */
void NavigationManagerImpl::UpdatePendingItemUserAgentType(
UserAgentOverrideOption user_agent_override_option,
const NavigationItem* inherit_from_item,
NavigationItem* pending_item) {
DCHECK(pending_item);
// |user_agent_override_option| must be INHERIT if |pending_item|'s
// UserAgentType is NONE, as requesting a desktop or mobile user agent should
// be disabled for app-specific URLs.
DCHECK(pending_item->GetUserAgentType() != UserAgentType::NONE ||
user_agent_override_option == UserAgentOverrideOption::INHERIT);
// Newly created pending items are created with UserAgentType::NONE for native
// pages or UserAgentType::MOBILE for non-native pages. If the pending item's
// URL is non-native, check which user agent type it should be created with
// based on |user_agent_override_option|.
DCHECK_NE(UserAgentType::DESKTOP, pending_item->GetUserAgentType());
if (pending_item->GetUserAgentType() == UserAgentType::NONE)
return;
switch (user_agent_override_option) {
case UserAgentOverrideOption::DESKTOP:
pending_item->SetUserAgentType(UserAgentType::DESKTOP);
break;
case UserAgentOverrideOption::MOBILE:
pending_item->SetUserAgentType(UserAgentType::MOBILE);
break;
case UserAgentOverrideOption::INHERIT: {
// Propagate the last committed non-native item's UserAgentType if there
// is one, otherwise keep the default value, which is mobile.
DCHECK(!inherit_from_item ||
inherit_from_item->GetUserAgentType() != UserAgentType::NONE);
if (inherit_from_item) {
pending_item->SetUserAgentType(inherit_from_item->GetUserAgentType());
}
break;
}
}
}
NavigationManagerImpl::NavigationManagerImpl()
: delegate_(nullptr), browser_state_(nullptr) {}
NavigationManagerImpl::~NavigationManagerImpl() = default;
void NavigationManagerImpl::SetDelegate(NavigationManagerDelegate* delegate) {
delegate_ = delegate;
}
void NavigationManagerImpl::SetBrowserState(BrowserState* browser_state) {
browser_state_ = browser_state;
}
void NavigationManagerImpl::DetachFromWebView() {}
void NavigationManagerImpl::RemoveTransientURLRewriters() {
transient_url_rewriters_.clear();
}
std::unique_ptr<NavigationItemImpl> NavigationManagerImpl::CreateNavigationItem(
const GURL& url,
const Referrer& referrer,
ui::PageTransition transition,
NavigationInitiationType initiation_type) {
NavigationItem* last_committed_item = GetLastCommittedItem();
auto item = CreateNavigationItemWithRewriters(
url, referrer, transition, initiation_type,
last_committed_item ? last_committed_item->GetURL() : GURL::EmptyGURL(),
&transient_url_rewriters_);
RemoveTransientURLRewriters();
return item;
}
void NavigationManagerImpl::UpdatePendingItemUrl(const GURL& url) const {
// If there is no pending item, navigation is probably happening within the
// back forward history. Don't modify the item list.
NavigationItemImpl* pending_item = GetPendingItemInCurrentOrRestoredSession();
if (!pending_item || url == pending_item->GetURL())
return;
// UpdatePendingItemUrl is used to handle redirects after loading starts for
// the currenting pending item. No transient item should exists at this point.
DCHECK(!GetTransientItem());
pending_item->SetURL(url);
pending_item->SetVirtualURL(url);
// Redirects (3xx response code), or client side navigation must change POST
// requests to GETs.
pending_item->SetPostData(nil);
pending_item->ResetHttpRequestHeaders();
}
NavigationItemImpl* NavigationManagerImpl::GetCurrentItemImpl() const {
NavigationItemImpl* transient_item = GetTransientItemImpl();
if (transient_item)
return transient_item;
NavigationItemImpl* pending_item = GetPendingItemInCurrentOrRestoredSession();
if (pending_item)
return pending_item;
return GetLastCommittedItemInCurrentOrRestoredSession();
}
void NavigationManagerImpl::UpdateCurrentItemForReplaceState(
const GURL& url,
NSString* state_object) {
DCHECK(!GetTransientItem());
NavigationItemImpl* current_item = GetCurrentItemImpl();
current_item->SetURL(url);
current_item->SetSerializedStateObject(state_object);
current_item->SetHasStateBeenReplaced(true);
current_item->SetPostData(nil);
}
void NavigationManagerImpl::GoToIndex(int index,
NavigationInitiationType initiation_type,
bool has_user_gesture) {
if (index < 0 || index >= GetItemCount()) {
NOTREACHED();
return;
}
if (!GetTransientItem()) {
delegate_->RecordPageStateInNavigationItem();
}
delegate_->ClearTransientContent();
// Notify delegate if the new navigation will use a different user agent.
UserAgentType to_item_user_agent_type =
GetItemAtIndex(index)->GetUserAgentType();
NavigationItem* pending_item = GetPendingItem();
NavigationItem* previous_item =
pending_item ? pending_item : GetLastCommittedItem();
UserAgentType previous_item_user_agent_type =
previous_item ? previous_item->GetUserAgentType() : UserAgentType::NONE;
if (to_item_user_agent_type != UserAgentType::NONE &&
to_item_user_agent_type != previous_item_user_agent_type) {
delegate_->WillChangeUserAgentType();
}
FinishGoToIndex(index, initiation_type, has_user_gesture);
}
void NavigationManagerImpl::GoToIndex(int index) {
GoToIndex(index, NavigationInitiationType::BROWSER_INITIATED,
/*has_user_gesture=*/true);
}
NavigationItem* NavigationManagerImpl::GetLastCommittedItem() const {
// GetLastCommittedItem() should return null while session restoration is in
// progress and real item after the first post-restore navigation is
// finished. IsRestoreSessionInProgress(), will return true until the first
// post-restore is finished.
if (IsRestoreSessionInProgress())
return nullptr;
return GetLastCommittedItemInCurrentOrRestoredSession();
}
int NavigationManagerImpl::GetLastCommittedItemIndex() const {
// GetLastCommittedItemIndex() should return -1 while session restoration is
// in progress and real item after the first post-restore navigation is
// finished. IsRestoreSessionInProgress(), will return true until the first
// post-restore is finished.
if (IsRestoreSessionInProgress())
return -1;
return GetLastCommittedItemIndexInCurrentOrRestoredSession();
}
NavigationItem* NavigationManagerImpl::GetPendingItem() const {
NavigationItem* item = GetPendingItemInCurrentOrRestoredSession();
// GetPendingItem() should return null while session restoration is in
// progress and real item when the first post-restore navigation has started.
// It's not possible to rely on IsRestoreSessionInProgress(), because this
// method may return true until the first post-restore is finished, hence
// this code relies on actual navigation URL to determine if restoration is
// complete.
if (item && wk_navigation_util::IsRestoreSessionUrl(item->GetURL()))
return nullptr;
return item;
}
NavigationItem* NavigationManagerImpl::GetTransientItem() const {
return GetTransientItemImpl();
}
void NavigationManagerImpl::LoadURLWithParams(
const NavigationManager::WebLoadParams& params) {
DCHECK(!(params.transition_type & ui::PAGE_TRANSITION_FORWARD_BACK));
delegate_->ClearTransientContent();
delegate_->RecordPageStateInNavigationItem();
NavigationInitiationType initiation_type =
params.is_renderer_initiated
? NavigationInitiationType::RENDERER_INITIATED
: NavigationInitiationType::BROWSER_INITIATED;
AddPendingItem(params.url, params.referrer, params.transition_type,
initiation_type, params.user_agent_override_option);
// Mark pending item as created from hash change if necessary. This is needed
// because window.hashchange message may not arrive on time.
NavigationItemImpl* pending_item = GetPendingItemInCurrentOrRestoredSession();
if (pending_item) {
NavigationItem* last_committed_item =
GetLastCommittedItemInCurrentOrRestoredSession();
GURL last_committed_url = last_committed_item
? last_committed_item->GetVirtualURL()
: GURL::EmptyGURL();
GURL pending_url = pending_item->GetURL();
if (last_committed_url != pending_url &&
last_committed_url.EqualsIgnoringRef(pending_url)) {
pending_item->SetIsCreatedFromHashChange(true);
}
if (params.virtual_url.is_valid())
pending_item->SetVirtualURL(params.virtual_url);
}
// Add additional headers to the NavigationItem before loading it in the web
// view. This implementation must match CRWWebController's |currentNavItem|.
// However, to avoid introducing a GetCurrentItem() that is only used here,
// the logic in |currentNavItem| is inlined here with the small simplification
// since AddPendingItem() implies that any transient item would have been
// cleared.
DCHECK(!GetTransientItem());
NavigationItemImpl* added_item =
pending_item ? pending_item
: GetLastCommittedItemInCurrentOrRestoredSession();
DCHECK(added_item);
if (params.extra_headers)
added_item->AddHttpRequestHeaders(params.extra_headers);
if (params.post_data) {
DCHECK([added_item->GetHttpRequestHeaders() objectForKey:@"Content-Type"])
<< "Post data should have an associated content type";
added_item->SetPostData(params.post_data);
added_item->SetShouldSkipRepostFormConfirmation(true);
}
FinishLoadURLWithParams();
}
void NavigationManagerImpl::AddTransientURLRewriter(
BrowserURLRewriter::URLRewriter rewriter) {
DCHECK(rewriter);
transient_url_rewriters_.push_back(rewriter);
}
void NavigationManagerImpl::Reload(ReloadType reload_type,
bool check_for_reposts) {
if (IsRestoreSessionInProgress()) {
// Do not interrupt session restoration process. Last committed item will
// eventually reload once the session is restored.
return;
}
if (!GetTransientItem() && !GetPendingItem() && !GetLastCommittedItem())
return;
// Reload with ORIGINAL_REQUEST_URL type should reload with the original
// request url of the transient item, or pending item if transient doesn't
// exist, or last committed item if both of them don't exist. The reason is
// that a server side redirect may change the item's url.
// For example, the user visits www.chromium.org and is then redirected
// to m.chromium.org, when the user wants to refresh the page with a different
// configuration (e.g. user agent), the user would be expecting to visit
// www.chromium.org instead of m.chromium.org.
if (reload_type == web::ReloadType::ORIGINAL_REQUEST_URL) {
NavigationItem* reload_item = nullptr;
if (GetTransientItem())
reload_item = GetTransientItem();
else if (GetPendingItem())
reload_item = GetPendingItem();
else
reload_item = GetLastCommittedItem();
DCHECK(reload_item);
reload_item->SetURL(reload_item->GetOriginalRequestURL());
}
FinishReload();
}
void NavigationManagerImpl::ReloadWithUserAgentType(
UserAgentType user_agent_type) {
DCHECK_NE(user_agent_type, UserAgentType::NONE);
// This removes the web view, which will be recreated at the end of this.
delegate_->WillChangeUserAgentType();
NavigationItem* last_non_redirect_item = GetTransientItem();
if (!last_non_redirect_item ||
ui::PageTransitionIsRedirect(last_non_redirect_item->GetTransitionType()))
last_non_redirect_item = GetVisibleItem();
if (!last_non_redirect_item ||
ui::PageTransitionIsRedirect(last_non_redirect_item->GetTransitionType()))
last_non_redirect_item = GetLastCommittedNonRedirectedItem(this);
if (!last_non_redirect_item)
return;
// |reloadURL| will be empty if a page was open by DOM.
GURL reload_url(last_non_redirect_item->GetOriginalRequestURL());
if (reload_url.is_empty()) {
reload_url = last_non_redirect_item->GetVirtualURL();
}
// Reload using a client-side redirect URL to create a new entry in
// WKBackForwardList for the new user agent type. This hack is not needed for
// LegacyNavigationManagerImpl which manages its own history entries.
if (web::GetWebClient()->IsSlimNavigationManagerEnabled()) {
GURL target_url;
// If current entry is a redirect URL, reload the original target URL. This
// can happen on a slow connection when user taps on Request Desktop Site
// before the previous redirect has finished (https://crbug.com/833958).
if (wk_navigation_util::IsRestoreSessionUrl(reload_url) &&
wk_navigation_util::ExtractTargetURL(reload_url, &target_url)) {
reload_url = target_url;
}
reload_url = wk_navigation_util::CreateRedirectUrl(reload_url);
}
WebLoadParams params(reload_url);
if (last_non_redirect_item->GetVirtualURL() != reload_url)
params.virtual_url = last_non_redirect_item->GetVirtualURL();
params.referrer = last_non_redirect_item->GetReferrer();
params.transition_type = ui::PAGE_TRANSITION_RELOAD;
switch (user_agent_type) {
case UserAgentType::DESKTOP:
params.user_agent_override_option = UserAgentOverrideOption::DESKTOP;
break;
case UserAgentType::MOBILE:
params.user_agent_override_option = UserAgentOverrideOption::MOBILE;
break;
case UserAgentType::NONE:
NOTREACHED();
}
LoadURLWithParams(params);
}
void NavigationManagerImpl::LoadIfNecessary() {
delegate_->LoadIfNecessary();
}
void NavigationManagerImpl::AddRestoreCompletionCallback(
base::OnceClosure callback) {
std::move(callback).Run();
}
void NavigationManagerImpl::WillRestore(size_t item_count) {
// It should be uncommon for the user to have more than 100 items in their
// session, so bucketing 100+ logs together is fine.
UMA_HISTOGRAM_COUNTS_100(kRestoreNavigationItemCount, item_count);
}
std::unique_ptr<NavigationItemImpl>
NavigationManagerImpl::CreateNavigationItemWithRewriters(
const GURL& url,
const Referrer& referrer,
ui::PageTransition transition,
NavigationInitiationType initiation_type,
const GURL& previous_url,
const std::vector<BrowserURLRewriter::URLRewriter>* additional_rewriters)
const {
GURL loaded_url(url);
// Do not rewrite placeholder URL. Navigation code relies on this special URL
// to implement native view and WebUI, and rewriter code should not be exposed
// to this special type of about:blank URL.
if (!IsPlaceholderUrl(url)) {
bool url_was_rewritten = false;
if (additional_rewriters && !additional_rewriters->empty()) {
url_was_rewritten = web::BrowserURLRewriter::RewriteURLWithWriters(
&loaded_url, browser_state_, *additional_rewriters);
}
if (!url_was_rewritten) {
web::BrowserURLRewriter::GetInstance()->RewriteURLIfNecessary(
&loaded_url, browser_state_);
}
}
// The URL should not be changed to app-specific URL if the load is
// renderer-initiated requested by non-app-specific URL. Pages with
// app-specific urls have elevated previledges and should not be allowed to
// open app-specific URLs.
if (initiation_type == web::NavigationInitiationType::RENDERER_INITIATED &&
loaded_url != url && web::GetWebClient()->IsAppSpecificURL(loaded_url) &&
!web::GetWebClient()->IsAppSpecificURL(previous_url)) {
loaded_url = url;
}
auto item = std::make_unique<NavigationItemImpl>();
item->SetOriginalRequestURL(loaded_url);
item->SetURL(loaded_url);
item->SetReferrer(referrer);
item->SetTransitionType(transition);
item->SetNavigationInitiationType(initiation_type);
if (!wk_navigation_util::URLNeedsUserAgentType(loaded_url)) {
item->SetUserAgentType(web::UserAgentType::NONE);
}
return item;
}
NavigationItem* NavigationManagerImpl::GetLastCommittedItemWithUserAgentType()
const {
for (int index = GetLastCommittedItemIndexInCurrentOrRestoredSession();
index >= 0; index--) {
NavigationItem* item = GetItemAtIndex(index);
if (wk_navigation_util::URLNeedsUserAgentType(item->GetURL()))
return item;
}
return nullptr;
}
void NavigationManagerImpl::FinishReload() {
delegate_->Reload();
}
void NavigationManagerImpl::FinishLoadURLWithParams() {
delegate_->LoadCurrentItem();
}
bool NavigationManagerImpl::IsPlaceholderUrl(const GURL& url) const {
return false; // Default implementation doesn't use placeholder URLs
}
} // namespace web