blob: 704682ac04592187f8a7640792e4c108941dafac [file] [log] [blame]
// Copyright 2012 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/crw_session_controller.h"
#include <stddef.h>
#include <algorithm>
#include <utility>
#include "base/format_macros.h"
#include "base/logging.h"
#import "base/mac/foundation_util.h"
#import "base/mac/scoped_nsobject.h"
#include "base/memory/ptr_util.h"
#include "base/strings/sys_string_conversions.h"
#include "ios/web/history_state_util.h"
#import "ios/web/navigation/crw_session_controller+private_constructors.h"
#import "ios/web/navigation/navigation_item_impl.h"
#import "ios/web/navigation/navigation_manager_impl.h"
#include "ios/web/navigation/time_smoother.h"
#include "ios/web/public/browser_state.h"
#include "ios/web/public/browser_url_rewriter.h"
#include "ios/web/public/referrer.h"
#include "ios/web/public/ssl_status.h"
#import "ios/web/public/web_client.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface CRWSessionController () {
// Weak pointer back to the owning NavigationManager. This is to facilitate
// the incremental merging of the two classes.
web::NavigationManagerImpl* _navigationManager;
// Identifies the index of the last committed item in the items array.
NSInteger _lastCommittedItemIndex;
// Identifies the index of the previous item in the items array.
NSInteger _previousItemIndex;
// The browser state associated with this CRWSessionController;
web::BrowserState* _browserState; // weak
// Time smoother for navigation item timestamps; see comment in
// navigation_controller_impl.h
web::TimeSmoother _timeSmoother;
// Backing objects for properties of the same name.
web::ScopedNavigationItemImplList _items;
// |_pendingItem| only contains a NavigationItem for non-history navigations.
// For back/forward navigations within session history, _pendingItemIndex will
// be an index within |_items|, and self.pendingItem will return the item at
// that index.
std::unique_ptr<web::NavigationItemImpl> _pendingItem;
std::unique_ptr<web::NavigationItemImpl> _transientItem;
}
// Redefine as readwrite.
@property(nonatomic, readwrite, assign) NSInteger lastCommittedItemIndex;
// Expose setters for serialization properties. These are exposed in a category
// in SessionStorageBuilder, and will be removed as ownership of
// their backing ivars moves to NavigationManagerImpl.
@property(nonatomic, readwrite, assign) NSInteger previousItemIndex;
// Removes all items after lastCommittedItemIndex.
- (void)clearForwardItems;
// Discards the transient item, if any.
- (void)discardTransientItem;
// Creates a NavigationItemImpl with the specified properties.
- (std::unique_ptr<web::NavigationItemImpl>)
itemWithURL:(const GURL&)url
referrer:(const web::Referrer&)referrer
transition:(ui::PageTransition)transition
initiationType:(web::NavigationInitiationType)initiationType;
// Returns YES if the PageTransition for the underlying navigationItem at
// |index| in |items| has ui::PAGE_TRANSITION_IS_REDIRECT_MASK.
- (BOOL)isRedirectTransitionForItemAtIndex:(size_t)index;
// Should create a new pending item if the new pending item is not a duplicate
// of the last added or committed item. Returns YES if one of the following
// rules apply:
// 1. There is no last added or committed item.
// 2. The new item has different url from the last added or committed item.
// 3. Url is the same, but the new item is a form submission resulted from the
// last added or committed item.
// 4. Url is the same, but new item is a reload with different user agent type
// resulted from last added or committed item.
- (BOOL)shouldCreatePendingItemWithURL:(const GURL&)URL
transition:(ui::PageTransition)transition
userAgentOverrideOption:
(web::NavigationManager::UserAgentOverrideOption)
userAgentOverrideOption;
@end
@implementation CRWSessionController
@synthesize lastCommittedItemIndex = _lastCommittedItemIndex;
@synthesize previousItemIndex = _previousItemIndex;
@synthesize pendingItemIndex = _pendingItemIndex;
- (instancetype)initWithBrowserState:(web::BrowserState*)browserState {
self = [super init];
if (self) {
_browserState = browserState;
_lastCommittedItemIndex = -1;
_previousItemIndex = -1;
_pendingItemIndex = -1;
}
return self;
}
- (instancetype)initWithBrowserState:(web::BrowserState*)browserState
navigationItems:(web::ScopedNavigationItemList)items
lastCommittedItemIndex:(NSUInteger)lastCommittedItemIndex {
self = [super init];
if (self) {
_browserState = browserState;
_items = web::CreateScopedNavigationItemImplList(std::move(items));
_lastCommittedItemIndex =
std::min(static_cast<NSInteger>(lastCommittedItemIndex),
static_cast<NSInteger>(_items.size()) - 1);
_previousItemIndex = -1;
_pendingItemIndex = -1;
}
return self;
}
#pragma mark - Accessors
- (void)setLastCommittedItemIndex:(NSInteger)lastCommittedItemIndex {
if (_lastCommittedItemIndex != lastCommittedItemIndex) {
_lastCommittedItemIndex = lastCommittedItemIndex;
if (_navigationManager)
_navigationManager->RemoveTransientURLRewriters();
}
}
- (void)setPendingItemIndex:(NSInteger)pendingItemIndex {
DCHECK_GE(pendingItemIndex, -1);
DCHECK_LT(pendingItemIndex, static_cast<NSInteger>(self.items.size()));
_pendingItemIndex = pendingItemIndex;
DCHECK(_pendingItemIndex == -1 || self.pendingItem);
}
- (BOOL)canPruneAllButLastCommittedItem {
return self.lastCommittedItemIndex != -1 && self.pendingItemIndex == -1 &&
!self.transientItem;
}
- (const web::ScopedNavigationItemImplList&)items {
return _items;
}
- (web::NavigationItemImpl*)currentItem {
if (self.transientItem)
return self.transientItem;
if (self.pendingItem)
return self.pendingItem;
return self.lastCommittedItem;
}
- (web::NavigationItemImpl*)visibleItem {
if (self.transientItem)
return self.transientItem;
// Only return the |pendingItem| for new (non-history), browser-initiated
// navigations in order to prevent URL spoof attacks.
web::NavigationItemImpl* pendingItem = self.pendingItem;
if (pendingItem) {
bool isUserInitiated = pendingItem->NavigationInitiationType() ==
web::NavigationInitiationType::USER_INITIATED;
bool safeToShowPending = isUserInitiated && _pendingItemIndex == -1;
if (safeToShowPending)
return pendingItem;
}
return self.lastCommittedItem;
}
- (web::NavigationItemImpl*)pendingItem {
if (self.pendingItemIndex == -1)
return _pendingItem.get();
return self.items[self.pendingItemIndex].get();
}
- (web::NavigationItemImpl*)transientItem {
return _transientItem.get();
}
- (web::NavigationItemImpl*)lastCommittedItem {
NSInteger index = self.lastCommittedItemIndex;
return index == -1 ? nullptr : self.items[index].get();
}
- (web::NavigationItemImpl*)previousItem {
NSInteger index = self.previousItemIndex;
return index == -1 || self.items.empty() ? nullptr : self.items[index].get();
}
- (web::NavigationItemList)backwardItems {
web::NavigationItemList items;
// This explicit check is necessary to protect the loop below which uses an
// unsafe signed (NSInteger) to unsigned (size_t) conversion.
if (_lastCommittedItemIndex > -1) {
// If the current navigation item is a transient item (e.g. SSL
// interstitial), the last committed item should also be considered part of
// the backward history.
DCHECK(self.lastCommittedItem);
if (self.transientItem) {
items.push_back(self.lastCommittedItem);
}
for (size_t index = _lastCommittedItemIndex; index > 0; --index) {
if (![self isRedirectTransitionForItemAtIndex:index])
items.push_back(self.items[index - 1].get());
}
}
return items;
}
- (web::NavigationItemList)forwardItems {
web::NavigationItemList items;
NSUInteger lastNonRedirectedIndex = _lastCommittedItemIndex + 1;
while (lastNonRedirectedIndex < self.items.size()) {
web::NavigationItem* item = self.items[lastNonRedirectedIndex].get();
if (!ui::PageTransitionIsRedirect(item->GetTransitionType()))
items.push_back(item);
++lastNonRedirectedIndex;
}
return items;
}
#pragma mark - NSObject
- (NSString*)description {
// Create description for |items|.
NSMutableString* itemsDescription = [NSMutableString stringWithString:@"[\n"];
#ifndef NDEBUG
for (const auto& item : self.items)
[itemsDescription appendFormat:@"%@\n", item->GetDescription()];
#endif
[itemsDescription appendString:@"]"];
// Create description for |pendingItem| and |transientItem|.
NSString* pendingItemDescription = @"(null)";
NSString* transientItemDescription = @"(null)";
#ifndef NDEBUG
if (self.pendingItem)
pendingItemDescription = self.pendingItem->GetDescription();
if (self.transientItem)
transientItemDescription = self.transientItem->GetDescription();
#else
if (self.pendingItem) {
pendingItemDescription =
[NSString stringWithFormat:@"%p", self.pendingItem];
}
if (self.transientItem) {
transientItemDescription =
[NSString stringWithFormat:@"%p", self.transientItem];
}
#endif
return [NSString stringWithFormat:@"last committed item index: %" PRIdNS
@"\nprevious item index: %" PRIdNS
@"\npending item index: %" PRIdNS
@"\nall items: %@ \npending item: %@"
@"\ntransient item: %@\n",
_lastCommittedItemIndex, _previousItemIndex,
_pendingItemIndex, itemsDescription,
pendingItemDescription,
transientItemDescription];
}
#pragma mark - Public
- (void)setNavigationManager:(web::NavigationManagerImpl*)navigationManager {
_navigationManager = navigationManager;
if (_navigationManager) {
// _browserState will be nullptr if CRWSessionController has been
// initialized with -initWithCoder: method. Take _browserState from
// NavigationManagerImpl if that's the case.
if (!_browserState) {
_browserState = _navigationManager->GetBrowserState();
}
DCHECK_EQ(_browserState, _navigationManager->GetBrowserState());
}
}
- (void)setBrowserState:(web::BrowserState*)browserState {
_browserState = browserState;
DCHECK(!_navigationManager ||
_navigationManager->GetBrowserState() == _browserState);
}
- (void)addPendingItem:(const GURL&)url
referrer:(const web::Referrer&)ref
transition:(ui::PageTransition)trans
initiationType:(web::NavigationInitiationType)initiationType
userAgentOverrideOption:(web::NavigationManager::UserAgentOverrideOption)
userAgentOverrideOption {
[self discardTransientItem];
self.pendingItemIndex = -1;
if (![self shouldCreatePendingItemWithURL:url
transition:trans
userAgentOverrideOption:userAgentOverrideOption]) {
return;
}
_pendingItem = [self itemWithURL:url
referrer:ref
transition:trans
initiationType:initiationType];
DCHECK_EQ(-1, self.pendingItemIndex);
}
- (BOOL)shouldCreatePendingItemWithURL:(const GURL&)URL
transition:(ui::PageTransition)transition
userAgentOverrideOption:
(web::NavigationManager::UserAgentOverrideOption)
userAgentOverrideOption {
// Note: CRWSessionController currently has the responsibility to distinguish
// between new navigations and history stack navigation, hence the inclusion
// of specific transiton type logic here, in order to make it reliable with
// real-world observed behavior.
// TODO(crbug.com/676129): Fix the way changes are detected/reported elsewhere
// in the web layer so that this hack can be removed.
// Remove the workaround code from -presentSafeBrowsingWarningForResource:.
web::NavigationItemImpl* currentItem = self.currentItem;
if (!currentItem)
return YES;
// User agent override option should always be different from the user agent
// type of the pending item, or the last committed item if pending doesn't
// exist.
DCHECK(userAgentOverrideOption !=
web::NavigationManager::UserAgentOverrideOption::DESKTOP ||
currentItem->GetUserAgentType() != web::UserAgentType::DESKTOP);
DCHECK(userAgentOverrideOption !=
web::NavigationManager::UserAgentOverrideOption::MOBILE ||
currentItem->GetUserAgentType() != web::UserAgentType::MOBILE);
BOOL hasSameURL = self.currentItem->GetURL() == URL;
if (!hasSameURL) {
// Different url indicates that it's not a duplicate item.
return YES;
}
BOOL isPendingTransitionFormSubmit =
PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_FORM_SUBMIT);
BOOL isCurrentTransitionFormSubmit = PageTransitionCoreTypeIs(
currentItem->GetTransitionType(), ui::PAGE_TRANSITION_FORM_SUBMIT);
if (isPendingTransitionFormSubmit && !isCurrentTransitionFormSubmit) {
// |isPendingTransitionFormSubmit| indicates that the new item is a form
// submission resulted from the last added or committed item, and
// |!isCurrentTransitionFormSubmit| shows that the form submission is not
// counted multiple times.
return YES;
}
BOOL isPendingTransitionReload =
PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_RELOAD);
BOOL isInheritingUserAgentType =
userAgentOverrideOption ==
web::NavigationManager::UserAgentOverrideOption::INHERIT;
if (isPendingTransitionReload && !isInheritingUserAgentType) {
// Overriding user agent type to MOBILE or DESKTOP indicates that the new
// new item is a reload with different user agent type.
return YES;
}
return NO;
}
- (void)updatePendingItem:(const GURL&)url {
// If there is no pending item, navigation is probably happening within the
// session history. Don't modify the item list.
web::NavigationItemImpl* item = self.pendingItem;
if (!item)
return;
if (url != item->GetURL()) {
// Assume a redirection, and discard any transient item.
// TODO(stuartmorgan): Once the current safe browsing code is gone,
// consider making this a DCHECK that there's no transient item.
[self discardTransientItem];
item->SetURL(url);
item->SetVirtualURL(url);
// Redirects (3xx response code), or client side navigation must change
// POST requests to GETs.
item->SetPostData(nil);
item->ResetHttpRequestHeaders();
}
}
- (void)clearForwardItems {
DCHECK_EQ(self.pendingItemIndex, -1);
[self discardTransientItem];
NSInteger forwardItemStartIndex = _lastCommittedItemIndex + 1;
DCHECK(forwardItemStartIndex >= 0);
size_t itemCount = self.items.size();
if (forwardItemStartIndex >= static_cast<NSInteger>(itemCount))
return;
if (_previousItemIndex >= forwardItemStartIndex)
_previousItemIndex = -1;
// Remove the NavigationItems and notify the NavigationManager.
_items.erase(_items.begin() + forwardItemStartIndex, _items.end());
if (_navigationManager) {
_navigationManager->OnNavigationItemsPruned(itemCount -
forwardItemStartIndex);
}
}
- (void)commitPendingItem {
if (self.pendingItem) {
// Once an item is committed it's not renderer-initiated any more. (Matches
// the implementation in NavigationController.)
self.pendingItem->ResetForCommit();
NSInteger newItemIndex = self.pendingItemIndex;
if (newItemIndex == -1) {
[self clearForwardItems];
// Add the new item at the end.
_items.push_back(std::move(_pendingItem));
newItemIndex = self.items.size() - 1;
}
_previousItemIndex = _lastCommittedItemIndex;
self.lastCommittedItemIndex = newItemIndex;
self.pendingItemIndex = -1;
DCHECK(!_pendingItem);
}
web::NavigationItem* item = self.currentItem;
// Update the navigation timestamp now that it's actually happened.
if (item)
item->SetTimestamp(_timeSmoother.GetSmoothedTime(base::Time::Now()));
if (_navigationManager && item)
_navigationManager->OnNavigationItemCommitted();
DCHECK_EQ(self.pendingItemIndex, -1);
}
- (void)addTransientItemWithURL:(const GURL&)URL {
_transientItem =
[self itemWithURL:URL
referrer:web::Referrer()
transition:ui::PAGE_TRANSITION_CLIENT_REDIRECT
initiationType:web::NavigationInitiationType::USER_INITIATED];
_transientItem->SetTimestamp(
_timeSmoother.GetSmoothedTime(base::Time::Now()));
}
- (void)pushNewItemWithURL:(const GURL&)URL
stateObject:(NSString*)stateObject
transition:(ui::PageTransition)transition {
DCHECK(!self.pendingItem);
DCHECK(self.currentItem);
web::NavigationItem* lastCommittedItem = self.lastCommittedItem;
CHECK(web::history_state_util::IsHistoryStateChangeValid(
lastCommittedItem->GetURL(), URL));
web::Referrer referrer(lastCommittedItem->GetURL(),
web::ReferrerPolicyDefault);
std::unique_ptr<web::NavigationItemImpl> pushedItem =
[self itemWithURL:URL
referrer:referrer
transition:transition
initiationType:web::NavigationInitiationType::USER_INITIATED];
pushedItem->SetUserAgentType(lastCommittedItem->GetUserAgentType());
pushedItem->SetSerializedStateObject(stateObject);
pushedItem->SetIsCreatedFromPushState(true);
pushedItem->GetSSL() = lastCommittedItem->GetSSL();
pushedItem->SetTimestamp(_timeSmoother.GetSmoothedTime(base::Time::Now()));
[self clearForwardItems];
// Add the new item at the end.
_items.push_back(std::move(pushedItem));
_previousItemIndex = _lastCommittedItemIndex;
self.lastCommittedItemIndex = self.items.size() - 1;
if (_navigationManager)
_navigationManager->OnNavigationItemCommitted();
}
- (void)updateCurrentItemWithURL:(const GURL&)url
stateObject:(NSString*)stateObject {
DCHECK(!self.transientItem);
web::NavigationItemImpl* currentItem = self.currentItem;
currentItem->SetURL(url);
currentItem->SetSerializedStateObject(stateObject);
currentItem->SetHasStateBeenReplaced(true);
currentItem->SetPostData(nil);
// If the change is to a committed item, notify interested parties.
if (currentItem != self.pendingItem && _navigationManager)
_navigationManager->OnNavigationItemChanged();
}
- (void)discardNonCommittedItems {
[self discardTransientItem];
_pendingItem.reset();
self.pendingItemIndex = -1;
}
- (void)discardTransientItem {
_transientItem.reset();
}
- (void)copyStateFromSessionControllerAndPrune:(CRWSessionController*)source {
DCHECK(source);
if (!self.canPruneAllButLastCommittedItem)
return;
// The other session may not have any items, in which case there is nothing
// to insert.
const web::ScopedNavigationItemImplList& sourceItems = source->_items;
if (sourceItems.empty())
return;
// Early return if there's no committed source item.
if (!source.lastCommittedItem)
return;
// Copy |sourceItems| into a new NavigationItemList. |mergedItems| needs to
// be large enough for all items in |source| preceding
// |sourceLastCommittedItemIndex|, the |source|'s current item, and |self|'s
// current item, which comes out to |sourceCurrentIndex| + 2.
DCHECK_GT(source.lastCommittedItemIndex, -1);
size_t sourceLastCommittedItemIndex =
static_cast<size_t>(source.lastCommittedItemIndex);
web::ScopedNavigationItemImplList mergedItems(sourceLastCommittedItemIndex +
2);
for (size_t index = 0; index <= sourceLastCommittedItemIndex; ++index) {
mergedItems[index] =
base::MakeUnique<web::NavigationItemImpl>(*sourceItems[index]);
}
mergedItems.back() = std::move(_items[self.lastCommittedItemIndex]);
// Use |mergedItems| as the session history.
std::swap(mergedItems, _items);
// Update state to reflect inserted NavigationItems.
_previousItemIndex = -1;
_lastCommittedItemIndex = self.items.size() - 1;
DCHECK_LT(static_cast<NSUInteger>(_lastCommittedItemIndex),
self.items.size());
}
- (void)goToItemAtIndex:(NSInteger)index
discardNonCommittedItems:(BOOL)discard {
if (index < 0 || static_cast<NSUInteger>(index) >= self.items.size())
return;
if (index == _lastCommittedItemIndex) {
// |delta| is 0, no need to change current navigation index.
return;
}
if (discard) {
if (index < _lastCommittedItemIndex) {
// Going back.
[self discardNonCommittedItems];
} else if (_lastCommittedItemIndex < index) {
// Going forward.
[self discardTransientItem];
}
}
_previousItemIndex = _lastCommittedItemIndex;
_lastCommittedItemIndex = index;
}
- (void)removeItemAtIndex:(NSInteger)index {
DCHECK(index < static_cast<NSInteger>(self.items.size()));
DCHECK(index != _lastCommittedItemIndex);
DCHECK(index >= 0);
[self discardNonCommittedItems];
_items.erase(_items.begin() + index);
if (_lastCommittedItemIndex > index)
_lastCommittedItemIndex--;
if (_previousItemIndex >= index)
_previousItemIndex--;
if (_navigationManager)
_navigationManager->OnNavigationItemsPruned(1U);
}
- (BOOL)isSameDocumentNavigationBetweenItem:(web::NavigationItem*)firstItem
andItem:(web::NavigationItem*)secondItem {
if (!firstItem || !secondItem || firstItem == secondItem)
return NO;
int firstIndex = [self indexOfItem:firstItem];
int secondIndex = [self indexOfItem:secondItem];
if (firstIndex == -1 || secondIndex == -1)
return NO;
int startIndex = firstIndex < secondIndex ? firstIndex : secondIndex;
int endIndex = firstIndex < secondIndex ? secondIndex : firstIndex;
for (int i = startIndex + 1; i <= endIndex; i++) {
web::NavigationItemImpl* item = self.items[i].get();
// Every item in the sequence has to be created from a hash change or
// pushState() call.
if (!item->IsCreatedFromPushState() && !item->IsCreatedFromHashChange())
return NO;
// Every item in the sequence has to have a URL that could have been
// created from a pushState() call.
if (!web::history_state_util::IsHistoryStateChangeValid(firstItem->GetURL(),
item->GetURL()))
return NO;
}
return YES;
}
- (int)indexOfItem:(const web::NavigationItem*)item {
DCHECK(item);
for (size_t index = 0; index < self.items.size(); ++index) {
if (self.items[index].get() == item)
return index;
}
return -1;
}
- (web::NavigationItemImpl*)itemAtIndex:(NSInteger)index {
if (index < 0 || self.items.size() <= static_cast<NSUInteger>(index))
return nullptr;
return self.items[index].get();
}
#pragma mark -
#pragma mark Private methods
- (std::unique_ptr<web::NavigationItemImpl>)
itemWithURL:(const GURL&)url
referrer:(const web::Referrer&)referrer
transition:(ui::PageTransition)transition
initiationType:(web::NavigationInitiationType)initiationType {
DCHECK(_navigationManager);
return _navigationManager->CreateNavigationItem(url, referrer, transition,
initiationType);
}
- (BOOL)isRedirectTransitionForItemAtIndex:(size_t)index {
DCHECK_LT(index, self.items.size());
ui::PageTransition transition = self.items[index]->GetTransitionType();
return (transition & ui::PAGE_TRANSITION_IS_REDIRECT_MASK) ? YES : NO;
}
@end