diff options
author | stuartmorgan <stuartmorgan@chromium.org> | 2015-03-24 09:58:09 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-03-24 16:58:50 +0000 |
commit | 271aab95144f3cb8dbf41e7a3bba829018a6dafc (patch) | |
tree | ce6fba11ad0dc4d0ae0a68c506d898acd20a4b1d | |
parent | c90b59db808926b62c1dcc42845af79fc5a82926 (diff) | |
download | chromium_src-271aab95144f3cb8dbf41e7a3bba829018a6dafc.zip chromium_src-271aab95144f3cb8dbf41e7a3bba829018a6dafc.tar.gz chromium_src-271aab95144f3cb8dbf41e7a3bba829018a6dafc.tar.bz2 |
Upstream ios/web/navigation
This directly upstreams the remainder of the ios/web/navigation folder,
and a few supporting pieces.
BUG=464810
Review URL: https://codereview.chromium.org/1028603004
Cr-Commit-Position: refs/heads/master@{#322014}
24 files changed, 4004 insertions, 1 deletions
diff --git a/ios/web/DEPS b/ios/web/DEPS index 2f189bf..abbd333 100644 --- a/ios/web/DEPS +++ b/ios/web/DEPS @@ -1,8 +1,13 @@ include_rules = [ + "+ios/public/provider/web", + "+ios/net", "+ios/web", "+net", "+third_party/libwebp/webp", "+ui", + + # For tests. + "+ios/testing", ] specific_include_rules = { diff --git a/ios/web/ios_web.gyp b/ios/web/ios_web.gyp index 5621e41..9f1845a 100644 --- a/ios/web/ios_web.gyp +++ b/ios/web/ios_web.gyp @@ -73,6 +73,8 @@ 'public/web_state/js/crw_js_injection_manager.h', 'public/web_state/js/crw_js_injection_receiver.h', 'public/web_state/js/crw_js_message_manager.h', + 'public/web_state/page_scroll_state.h', + 'public/web_state/page_scroll_state.mm', 'public/web_state/url_verification_constants.h', 'public/web_state/web_state_observer.h', 'public/web_state/web_state_observer_bridge.h', diff --git a/ios/web/navigation/crw_session_certificate_policy_manager.h b/ios/web/navigation/crw_session_certificate_policy_manager.h new file mode 100644 index 0000000..a97e913 --- /dev/null +++ b/ios/web/navigation/crw_session_certificate_policy_manager.h @@ -0,0 +1,43 @@ +// 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. + +#ifndef IOS_WEB_NAVIGATION_CRW_SESSION_CERTIFICATE_POLICY_MANAGER_H_ +#define IOS_WEB_NAVIGATION_CRW_SESSION_CERTIFICATE_POLICY_MANAGER_H_ + +#import <Foundation/Foundation.h> +#include <string> + +#include "base/memory/ref_counted.h" +#include "net/cert/cert_status_flags.h" + +@class CRWSessionEntry; + +namespace net { +class X509Certificate; +} + +namespace web { +class CertificatePolicyCache; +} + +// The CRWSessionCertificatePolicyManager keeps track of the certificates that +// have been manually allowed by the user despite the errors. +// The CRWSessionCertificatePolicyManager lives on the main thread. +@interface CRWSessionCertificatePolicyManager : NSObject <NSCoding, NSCopying> + +- (void)registerAllowedCertificate:(net::X509Certificate*)certificate + forHost:(const std::string&)host + status:(net::CertStatus)status; + +// Removes all the certificates associated with this session. Note that this has +// no effect on the policy cache service. +- (void)clearCertificates; + +// Copies the certificate polices for the session into |cache|. +- (void)updateCertificatePolicyCache: + (const scoped_refptr<web::CertificatePolicyCache>&)cache; + +@end + +#endif // IOS_WEB_NAVIGATION_CRW_SESSION_CERTIFICATE_POLICY_MANAGER_H_ diff --git a/ios/web/navigation/crw_session_certificate_policy_manager.mm b/ios/web/navigation/crw_session_certificate_policy_manager.mm new file mode 100644 index 0000000..3547536 --- /dev/null +++ b/ios/web/navigation/crw_session_certificate_policy_manager.mm @@ -0,0 +1,182 @@ +// 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_certificate_policy_manager.h" + +#include <map> +#include <set> + +#include "base/bind.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/strings/sys_string_conversions.h" +#include "ios/web/public/certificate_policy_cache.h" +#include "ios/web/public/web_thread.h" +#include "net/cert/x509_certificate.h" + +// Break if we detect that CertStatus values changed, because we persist them on +// disk and thus require them to be consistent. +COMPILE_ASSERT(net::CERT_STATUS_ALL_ERRORS == 0xFFFF, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_COMMON_NAME_INVALID == 1 << 0, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_DATE_INVALID == 1 << 1, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_AUTHORITY_INVALID == 1 << 2, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_NO_REVOCATION_MECHANISM == 1 << 4, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_UNABLE_TO_CHECK_REVOCATION == 1 << 5, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_REVOKED == 1 << 6, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_INVALID == 1 << 7, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_WEAK_SIGNATURE_ALGORITHM == 1 << 8, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_NON_UNIQUE_NAME == 1 << 10, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_WEAK_KEY == 1 << 11, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_IS_EV == 1 << 16, + cert_status_value_changed); +COMPILE_ASSERT(net::CERT_STATUS_REV_CHECKING_ENABLED == 1 << 17, + cert_status_value_changed); + +namespace { + +NSString* const kAllowedCertificatesKey = @"allowedCertificates"; + +struct AllowedCertificate { + scoped_refptr<net::X509Certificate> certificate; + std::string host; +}; + +class LessThan { + public: + bool operator() (const AllowedCertificate& lhs, + const AllowedCertificate& rhs) const { + if (lhs.host != rhs.host) + return lhs.host < rhs.host; + return certificateCompare_(lhs.certificate, rhs.certificate); + } + private: + net::X509Certificate::LessThan certificateCompare_; +}; + +typedef std::map<AllowedCertificate, net::CertStatus, LessThan> + AllowedCertificates; + +NSData* CertificateToNSData(net::X509Certificate* certificate) { + std::string s; + bool success = + net::X509Certificate::GetDEREncoded(certificate->os_cert_handle(), &s); + DCHECK(success); + return [NSData dataWithBytes:s.c_str() length:s.length()]; +} + +net::X509Certificate* NSDataToCertificate(NSData* data) { + return net::X509Certificate::CreateFromBytes((const char *)[data bytes], + [data length]); +} + +void AddToCertificatePolicyCache( + scoped_refptr<web::CertificatePolicyCache> policy_cache, + AllowedCertificates certs) { + DCHECK(policy_cache); + AllowedCertificates::iterator it; + for (it = certs.begin(); it != certs.end(); ++it) { + policy_cache->AllowCertForHost( + it->first.certificate.get(), it->first.host, it->second); + } +} + +} // namespace + +@implementation CRWSessionCertificatePolicyManager { + @private + AllowedCertificates allowed_; +} + +- (void)registerAllowedCertificate:(net::X509Certificate*)certificate + forHost:(const std::string&)host + status:(net::CertStatus)status { + DCHECK([NSThread isMainThread]); + DCHECK(certificate); + AllowedCertificate allowedCertificate = {certificate, host}; + allowed_[allowedCertificate] = status; +} + +- (void)clearCertificates { + DCHECK([NSThread isMainThread]); + allowed_.clear(); +} + +- (void)updateCertificatePolicyCache: + (const scoped_refptr<web::CertificatePolicyCache>&)cache { + DCHECK([NSThread isMainThread]); + DCHECK(cache); + // Make a copy of allowed_ and access the policy cache from the IOThread. + web::WebThread::PostTask( + web::WebThread::IO, FROM_HERE, + base::Bind(&AddToCertificatePolicyCache, cache, allowed_)); +} + +#pragma mark - +#pragma mark NSCoding and NSCopying methods + +- (id)initWithCoder:(NSCoder*)aDecoder { + DCHECK([NSThread isMainThread]); + self = [super init]; + if (self) { + NSMutableSet* allowed = [aDecoder + decodeObjectForKey:kAllowedCertificatesKey]; + for (NSArray* fields in allowed) { + if ([fields count] == 2) { + DVLOG(2) << "Dropping cached certificate policy (old format)."; + continue; + } else if ([fields count] != 3) { + NOTREACHED(); + continue; + } + net::X509Certificate* c = NSDataToCertificate([fields objectAtIndex:0]); + std::string host = base::SysNSStringToUTF8([fields objectAtIndex:1]); + net::CertStatus status = (net::CertStatus)[[fields objectAtIndex:2] + unsignedIntegerValue]; + [self registerAllowedCertificate:c forHost:host status:status]; + } + } + return self; +} + +- (void)encodeWithCoder:(NSCoder*)aCoder { + if (allowed_.size() == 0) + return; + + // Simple serialization of the set. If a same certificate is duplicated in the + // set (for a different host), the serialization will be duplicated as well. + NSMutableSet* allowedToEncode = [NSMutableSet set]; + AllowedCertificates::iterator it; + for (it = allowed_.begin(); it != allowed_.end(); ++it) { + NSData* c = CertificateToNSData(it->first.certificate.get()); + NSString* h = base::SysUTF8ToNSString(it->first.host); + DCHECK(c); + DCHECK(h); + if (!c || !h) + continue; + const NSUInteger status = (NSUInteger)it->second; + NSArray* fields = [NSArray arrayWithObjects:c, h, @(status), nil]; + [allowedToEncode addObject:fields]; + } + [aCoder encodeObject:allowedToEncode forKey:kAllowedCertificatesKey]; +} + +- (id)copyWithZone:(NSZone*)zone { + DCHECK([NSThread isMainThread]); + CRWSessionCertificatePolicyManager* copy = [[[self class] alloc] init]; + copy->allowed_ = allowed_; + return copy; +} + +@end diff --git a/ios/web/navigation/crw_session_controller+private_constructors.h b/ios/web/navigation/crw_session_controller+private_constructors.h new file mode 100644 index 0000000..dd7a119 --- /dev/null +++ b/ios/web/navigation/crw_session_controller+private_constructors.h @@ -0,0 +1,37 @@ +// 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. + +#ifndef IOS_WEB_NAVIGATION_CRW_SESSION_CONTROLLER_PRIVATE_CONSTRUCTORS_H_ +#define IOS_WEB_NAVIGATION_CRW_SESSION_CONTROLLER_PRIVATE_CONSTRUCTORS_H_ + +#import "ios/web/navigation/crw_session_controller.h" + +#include "base/memory/scoped_vector.h" + +namespace web { +class BrowserState; +class NavigationItem; +} + +// Temporary interface for NavigationManager and tests to create +// CRWSessionControllers. Once CRWSessionController has no users outside of +// web/, these methods can go back into session_controller.h. crbug.com/318974 +@interface CRWSessionController (PrivateConstructors) +// Initializes a session controller, supplying a unique textual identifier for +// the window, or nil. |opener| is the tab id of the parent tab. It may be +// nil or empty if there is no parent. +- (id)initWithWindowName:(NSString*)windowName + openerId:(NSString*)opener + openedByDOM:(BOOL)openedByDOM + openerNavigationIndex:(NSInteger)openerIndex + browserState:(web::BrowserState*)browserState; + +// Initializes a session controller, supplying a list of NavigationItem objects +// and the current index in the navigation history. +- (id)initWithNavigationItems:(ScopedVector<web::NavigationItem>)scoped_items + currentIndex:(NSUInteger)currentIndex + browserState:(web::BrowserState*)browserState; +@end + +#endif // IOS_WEB_NAVIGATION_CRW_SESSION_CONTROLLER_PRIVATE_CONSTRUCTORS_H_ diff --git a/ios/web/navigation/crw_session_controller.h b/ios/web/navigation/crw_session_controller.h new file mode 100644 index 0000000..b6eb972 --- /dev/null +++ b/ios/web/navigation/crw_session_controller.h @@ -0,0 +1,159 @@ +// 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. + +#ifndef IOS_WEB_NAVIGATION_CRW_SESSION_CONTROLLER_H_ +#define IOS_WEB_NAVIGATION_CRW_SESSION_CONTROLLER_H_ + +#import <Foundation/Foundation.h> +#include <vector> + +#include "ui/base/page_transition_types.h" +#include "url/gurl.h" + +@class CRWSessionEntry; +@class CRWSessionCertificatePolicyManager; +@class XCallbackParameters; + +namespace web { +class NavigationManagerImpl; +struct Referrer; +struct SSLStatus; +} + +// A CRWSessionController is similar to a NavigationController object in desktop +// Chrome. It maintains information needed to save/restore a tab with its +// complete session history. There is one of these for each tab. +// TODO(stuartmorgan): Move under NavigationManager, and consider merging into +// NavigationManager. +@interface CRWSessionController : NSObject<NSCoding, NSCopying> + +@property(nonatomic, readonly, retain) NSString* tabId; +@property(nonatomic, readonly, assign) NSInteger currentNavigationIndex; +@property(nonatomic, readonly, assign) NSInteger previousNavigationIndex; +@property(nonatomic, readonly, retain) NSArray* entries; +@property(nonatomic, copy) NSString* windowName; +// Indicates whether the page was opened by DOM (e.g. with |window.open| +// JavaScript call or by clicking a link with |_blank| target). +@property(nonatomic, readonly, getter=isOpenedByDOM) BOOL openedByDOM; +@property(nonatomic, readonly, retain) + CRWSessionCertificatePolicyManager* sessionCertificatePolicyManager; +// Returns the current entry in the session list, or the pending entry if there +// is a navigation in progress. +@property(nonatomic, readonly) CRWSessionEntry* currentEntry; +// Returns the entry that should be displayed to users (e.g., in the omnibox). +@property(nonatomic, readonly) CRWSessionEntry* visibleEntry; +// Returns the pending entry, if any. +@property(nonatomic, readonly) CRWSessionEntry* pendingEntry; +// Returns the transient entry, if any. +@property(nonatomic, readonly) CRWSessionEntry* transientEntry; +// Returns the last committed entry. +@property(nonatomic, readonly) CRWSessionEntry* lastCommittedEntry; +// Returns the previous entry in the session list, or nil if there isn't any. +@property(nonatomic, readonly) CRWSessionEntry* previousEntry; +@property(nonatomic, assign) NSTimeInterval lastVisitedTimestamp; +@property(nonatomic, readonly, copy) NSString* openerId; +@property(nonatomic, readonly, assign) NSInteger openerNavigationIndex; +@property(nonatomic, retain) XCallbackParameters* xCallbackParameters; + +// CRWSessionController doesn't have public constructors. New +// CRWSessionControllers are created by deserialization, or via a +// NavigationManager. + +// Sets the corresponding NavigationManager. +- (void)setNavigationManager:(web::NavigationManagerImpl*)navigationManager; + +// Add a new entry with the given url, referrer, and navigation type, making it +// the current entry. If |url| is the same as the current entry's url, this +// does nothing. |referrer| may be nil if there isn't one. The entry starts +// out as pending, and will be lost unless |-commitPendingEntry| is called. +- (void)addPendingEntry:(const GURL&)url + referrer:(const web::Referrer&)referrer + transition:(ui::PageTransition)type + rendererInitiated:(BOOL)rendererInitiated; + +// Updates the URL of the yet to be committed pending entry. Useful for page +// redirects. Does nothing if there is no pending entry. +- (void)updatePendingEntry:(const GURL&)url; + +// Commits the current pending entry. No changes are made to the entry during +// this process, it is just moved from pending to committed. +// TODO(pinkerton): Desktop Chrome broadcasts a notification here, should we? +- (void)commitPendingEntry; + +// Adds a transient entry with the given information. A transient entry will be +// discarded on any navigation. +// TODO(stuartmorgan): Make this work more like upstream, where the entry can +// be made from outside and then handed in, instead of having to pass a bunch +// of construction params here. +- (void)addTransientEntry:(const GURL&)url + title:(const base::string16&)title + sslStatus:(const web::SSLStatus*)status; + +// Creates a new CRWSessionEntry with the given URL and state object. A state +// object is a serialized generic JavaScript object that contains details of the +// UI's state for a given CRWSessionEntry/URL. The current entry's URL is the +// new entry's referrer. +- (void)pushNewEntryWithURL:(const GURL&)url stateObject:(NSString*)stateObject; +// Updates the URL and state object for the current entry. +- (void)updateCurrentEntryWithURL:(const GURL&)url + stateObject:(NSString*)stateObject; + +- (void)discardNonCommittedEntries; + +// Returns YES if there is a pending entry. +- (BOOL)hasPendingEntry; + +// Copies history state from the given CRWSessionController and adds it to this +// controller. If |replaceState|, replaces the state of this controller with +// the state of |otherSession|, instead of appending. +- (void)copyStateFromAndPrune:(CRWSessionController*)otherSession + replaceState:(BOOL)replaceState; + +// Returns YES if there are entries to go back or forward to, given the +// current entry. +- (BOOL)canGoBack; +- (BOOL)canGoForward; +// Adjusts the current entry to reflect the navigation in the corresponding +// direction in history. +- (void)goBack; +- (void)goForward; +// Calls goBack or goForward the appropriate number of times to adjust +// currentNavigationIndex_ by delta. +- (void)goDelta:(int)delta; +// Sets |currentNavigationIndex_| to the index of |entry| if |entries_| contains +// |entry|. +- (void)goToEntry:(CRWSessionEntry*)entry; + +// Removes the entry at |index| after discarding any noncomitted entries. +// |index| must not be the index of the last committed entry, or a noncomitted +// entry. +- (void)removeEntryAtIndex:(NSInteger)index; + +// Returns an array containing all of the non-redirected CRWSessionEntry objects +// whose index in |entries_| is less than |currentNavigationIndex_|. +- (NSArray*)backwardEntries; + +// Returns an array containing all of the non-redirected CRWSessionEntry objects +// whose index in |entries_| is greater than |currentNavigationIndex_|. +- (NSArray*)forwardEntries; + +// Returns the URLs in the entries that are redirected to the current entry. +- (std::vector<GURL>)currentRedirectedUrls; + +// Determines if navigation between the two given entries is a push state +// navigation. Entries can be passed in in any order. +- (BOOL)isPushStateNavigationBetweenEntry:(CRWSessionEntry*)firstEntry + andEntry:(CRWSessionEntry*)secondEntry; + +// Find the most recent session entry that is not a redirect. Returns nil if +// |entries_| is empty. +- (CRWSessionEntry*)lastUserEntry; + +// Set |useDesktopUserAgentForNextPendingEntry_| to YES if there is no pending +// entry, otherwise set |useDesktopUserAgent| in the pending entry. +- (void)useDesktopUserAgentForNextPendingEntry; + +@end + +#endif // IOS_WEB_NAVIGATION_CRW_SESSION_CONTROLLER_H_ diff --git a/ios/web/navigation/crw_session_controller.mm b/ios/web/navigation/crw_session_controller.mm new file mode 100644 index 0000000..5407ec3 --- /dev/null +++ b/ios/web/navigation/crw_session_controller.mm @@ -0,0 +1,874 @@ +// 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 <algorithm> +#include <vector> + +#include "base/format_macros.h" +#include "base/logging.h" +#include "base/mac/objc_property_releaser.h" +#import "base/mac/scoped_nsobject.h" +#include "base/metrics/user_metrics_action.h" +#include "base/strings/sys_string_conversions.h" +#import "ios/web/history_state_util.h" +#import "ios/web/navigation/crw_session_certificate_policy_manager.h" +#import "ios/web/navigation/crw_session_controller+private_constructors.h" +#import "ios/web/navigation/crw_session_entry.h" +#include "ios/web/navigation/navigation_item_impl.h" +#import "ios/web/navigation/navigation_manager_facade_delegate.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" +#include "ios/web/public/user_metrics.h" + +using base::UserMetricsAction; + +namespace { +NSString* const kCertificatePolicyManagerKey = @"certificatePolicyManager"; +NSString* const kCurrentNavigationIndexKey = @"currentNavigationIndex"; +NSString* const kEntriesKey = @"entries"; +NSString* const kLastVisitedTimestampKey = @"lastVisitedTimestamp"; +NSString* const kOpenerIdKey = @"openerId"; +NSString* const kOpenedByDOMKey = @"openedByDOM"; +NSString* const kOpenerNavigationIndexKey = @"openerNavigationIndex"; +NSString* const kPreviousNavigationIndexKey = @"previousNavigationIndex"; +NSString* const kTabIdKey = @"tabId"; +NSString* const kWindowNameKey = @"windowName"; +NSString* const kXCallbackParametersKey = @"xCallbackParameters"; +} // anonymous namespace + +@interface CRWSessionController () { + // Weak pointer back to the owning NavigationManager. This is to facilitate + // the incremental merging of the two classes. + web::NavigationManagerImpl* _navigationManager; + + NSString* _tabId; // Unique id of the tab. + NSString* _openerId; // Id of tab who opened this tab, empty/nil if none. + // Navigation index of the tab which opened this tab. Do not rely on the + // value of this member variable to indicate whether or not this tab has + // an opener, as both 0 and -1 are used as navigationIndex values. + NSInteger _openerNavigationIndex; + // Identifies the index of the current navigation in the CRWSessionEntry + // array. + NSInteger _currentNavigationIndex; + // Identifies the index of the previous navigation in the CRWSessionEntry + // array. + NSInteger _previousNavigationIndex; + // Ordered array of |CRWSessionEntry| objects, one for each site in session + // history. End of the list is the most recent load. + NSMutableArray* _entries; + + // An entry we haven't gotten a response for yet. This will be discarded + // when we navigate again. It's used only so we know what the currently + // displayed tab is. It backs the property of the same name and should only + // be set through its setter. + base::scoped_nsobject<CRWSessionEntry> _pendingEntry; + + // The transient entry, if any. A transient entry is discarded on any + // navigation, and is used for representing interstitials that need to be + // represented in the session. It backs the property of the same name and + // should only be set through its setter. + base::scoped_nsobject<CRWSessionEntry> _transientEntry; + + // The window name associated with the session. + NSString* _windowName; + + // Stores the certificate policies decided by the user. + CRWSessionCertificatePolicyManager* _sessionCertificatePolicyManager; + + // The timestamp of the last time this tab is visited, represented in time + // interval since 1970. + NSTimeInterval _lastVisitedTimestamp; + + // If |YES|, override |currentEntry.useDesktopUserAgent| and create the + // pending entry using the desktop user agent. + BOOL _useDesktopUserAgentForNextPendingEntry; + + // The browser state associated with this CRWSessionController; + __weak web::BrowserState* _browserState; + + // Time smoother for navigation entry timestamps; see comment in + // navigation_controller_impl.h + web::TimeSmoother _timeSmoother; + + // XCallback parameters used to create (or clobber) the tab. Can be nil. + XCallbackParameters* _xCallbackParameters; + + base::mac::ObjCPropertyReleaser _propertyReleaser_CRWSessionController; +} + +// TODO(rohitrao): These properties must be redefined readwrite to work around a +// clang bug. crbug.com/228650 +@property(nonatomic, readwrite, retain) NSString* tabId; +@property(nonatomic, readwrite, retain) NSArray* entries; +@property(nonatomic, readwrite, retain) + CRWSessionCertificatePolicyManager* sessionCertificatePolicyManager; + +- (NSString*)uniqueID; +// Removes all entries after currentNavigationIndex_. +- (void)clearForwardEntries; +// Discards the transient entry, if any. +- (void)discardTransientEntry; +// Create a new autoreleased session entry. +- (CRWSessionEntry*)sessionEntryWithURL:(const GURL&)url + referrer:(const web::Referrer&)referrer + transition:(ui::PageTransition)transition + useDesktopUserAgent:(BOOL)useDesktopUserAgent + rendererInitiated:(BOOL)rendererInitiated; +// Return the PageTransition for the underlying navigationItem at |index| in +// |entries_| +- (ui::PageTransition)transitionForIndex:(NSUInteger)index; +@end + +@implementation CRWSessionController + +@synthesize tabId = _tabId; +@synthesize currentNavigationIndex = _currentNavigationIndex; +@synthesize previousNavigationIndex = _previousNavigationIndex; +@synthesize entries = _entries; +@synthesize windowName = _windowName; +@synthesize lastVisitedTimestamp = _lastVisitedTimestamp; +@synthesize openerId = _openerId; +@synthesize openedByDOM = _openedByDOM; +@synthesize openerNavigationIndex = _openerNavigationIndex; +@synthesize sessionCertificatePolicyManager = _sessionCertificatePolicyManager; +@synthesize xCallbackParameters = _xCallbackParameters; + +- (id)initWithWindowName:(NSString*)windowName + openerId:(NSString*)openerId + openedByDOM:(BOOL)openedByDOM + openerNavigationIndex:(NSInteger)openerIndex + browserState:(web::BrowserState*)browserState { + self = [super init]; + if (self) { + _propertyReleaser_CRWSessionController.Init(self, + [CRWSessionController class]); + self.windowName = windowName; + _tabId = [[self uniqueID] retain]; + _openerId = [openerId copy]; + _openedByDOM = openedByDOM; + _openerNavigationIndex = openerIndex; + _browserState = browserState; + _entries = [[NSMutableArray array] retain]; + _lastVisitedTimestamp = [[NSDate date] timeIntervalSince1970]; + _currentNavigationIndex = -1; + _previousNavigationIndex = -1; + _sessionCertificatePolicyManager = + [[CRWSessionCertificatePolicyManager alloc] init]; + } + return self; +} + +- (id)initWithNavigationItems:(ScopedVector<web::NavigationItem>)scoped_items + currentIndex:(NSUInteger)currentIndex + browserState:(web::BrowserState*)browserState { + self = [super init]; + if (self) { + _propertyReleaser_CRWSessionController.Init(self, + [CRWSessionController class]); + _tabId = [[self uniqueID] retain]; + _openerId = nil; + _browserState = browserState; + + // Create entries array from list of navigations. + _entries = [[NSMutableArray alloc] initWithCapacity:scoped_items.size()]; + std::vector<web::NavigationItem*> items; + scoped_items.release(&items); + + for (size_t i = 0; i < items.size(); ++i) { + scoped_ptr<web::NavigationItem> item(items[i]); + base::scoped_nsobject<CRWSessionEntry> entry( + [[CRWSessionEntry alloc] initWithNavigationItem:item.Pass() index:i]); + [_entries addObject:entry]; + } + _currentNavigationIndex = currentIndex; + // Prior to M34, 0 was used as "no index" instead of -1; adjust for that. + if (![_entries count]) + _currentNavigationIndex = -1; + if (_currentNavigationIndex >= static_cast<NSInteger>(items.size())) { + _currentNavigationIndex = static_cast<NSInteger>(items.size()) - 1; + } + _previousNavigationIndex = -1; + _lastVisitedTimestamp = [[NSDate date] timeIntervalSince1970]; + _sessionCertificatePolicyManager = + [[CRWSessionCertificatePolicyManager alloc] init]; + } + return self; +} + +- (id)initWithCoder:(NSCoder*)aDecoder { + self = [super init]; + if (self) { + _propertyReleaser_CRWSessionController.Init(self, + [CRWSessionController class]); + NSString* uuid = [aDecoder decodeObjectForKey:kTabIdKey]; + if (!uuid) + uuid = [self uniqueID]; + + self.windowName = [aDecoder decodeObjectForKey:kWindowNameKey]; + _tabId = [uuid retain]; + _openerId = [[aDecoder decodeObjectForKey:kOpenerIdKey] copy]; + _openedByDOM = [aDecoder decodeBoolForKey:kOpenedByDOMKey]; + _openerNavigationIndex = + [aDecoder decodeIntForKey:kOpenerNavigationIndexKey]; + _currentNavigationIndex = + [aDecoder decodeIntForKey:kCurrentNavigationIndexKey]; + _previousNavigationIndex = + [aDecoder decodeIntForKey:kPreviousNavigationIndexKey]; + _lastVisitedTimestamp = + [aDecoder decodeDoubleForKey:kLastVisitedTimestampKey]; + NSMutableArray* temp = + [NSMutableArray arrayWithArray: + [aDecoder decodeObjectForKey:kEntriesKey]]; + _entries = [temp retain]; + // Prior to M34, 0 was used as "no index" instead of -1; adjust for that. + if (![_entries count]) + _currentNavigationIndex = -1; + _sessionCertificatePolicyManager = + [[aDecoder decodeObjectForKey:kCertificatePolicyManagerKey] retain]; + if (!_sessionCertificatePolicyManager) { + _sessionCertificatePolicyManager = + [[CRWSessionCertificatePolicyManager alloc] init]; + } + + _xCallbackParameters = + [[aDecoder decodeObjectForKey:kXCallbackParametersKey] retain]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder*)aCoder { + [aCoder encodeObject:_tabId forKey:kTabIdKey]; + [aCoder encodeObject:_openerId forKey:kOpenerIdKey]; + [aCoder encodeBool:_openedByDOM forKey:kOpenedByDOMKey]; + [aCoder encodeInt:_openerNavigationIndex forKey:kOpenerNavigationIndexKey]; + [aCoder encodeObject:_windowName forKey:kWindowNameKey]; + [aCoder encodeInt:_currentNavigationIndex forKey:kCurrentNavigationIndexKey]; + [aCoder encodeInt:_previousNavigationIndex + forKey:kPreviousNavigationIndexKey]; + [aCoder encodeDouble:_lastVisitedTimestamp forKey:kLastVisitedTimestampKey]; + [aCoder encodeObject:_entries forKey:kEntriesKey]; + [aCoder encodeObject:_sessionCertificatePolicyManager + forKey:kCertificatePolicyManagerKey]; + [aCoder encodeObject:_xCallbackParameters forKey:kXCallbackParametersKey]; + // rendererInitiated is deliberately not preserved, as upstream. +} + +- (id)copyWithZone:(NSZone*)zone { + CRWSessionController* copy = [[[self class] alloc] init]; + copy->_propertyReleaser_CRWSessionController.Init( + copy, [CRWSessionController class]); + copy->_tabId = [_tabId copy]; + copy->_openerId = [_openerId copy]; + copy->_openedByDOM = _openedByDOM; + copy->_openerNavigationIndex = _openerNavigationIndex; + copy.windowName = self.windowName; + copy->_currentNavigationIndex = _currentNavigationIndex; + copy->_previousNavigationIndex = _previousNavigationIndex; + copy->_lastVisitedTimestamp = _lastVisitedTimestamp; + copy->_entries = [_entries copy]; + copy->_sessionCertificatePolicyManager = + [_sessionCertificatePolicyManager copy]; + copy->_xCallbackParameters = [_xCallbackParameters copy]; + return copy; +} + +- (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()); + } +} + +- (NSString*)description { + return [NSString + stringWithFormat: + @"id: %@\nname: %@\nlast visit: %f\ncurrent index: %" PRIdNS + @"\nprevious index: %" PRIdNS "\n%@\npending: %@\nxCallback:\n%@\n", + _tabId, + self.windowName, + _lastVisitedTimestamp, + _currentNavigationIndex, + _previousNavigationIndex, + _entries, + _pendingEntry.get(), + _xCallbackParameters]; +} + +// Returns the current entry in the session list, or the pending entry if there +// is a navigation in progress. +- (CRWSessionEntry*)currentEntry { + if (_transientEntry) + return _transientEntry.get(); + if (_pendingEntry) + return _pendingEntry.get(); + return [self lastCommittedEntry]; +} + +// See NavigationController::GetVisibleEntry for the motivation for this +// distinction. +- (CRWSessionEntry*)visibleEntry { + if (_transientEntry) + return _transientEntry.get(); + // Only return the pending_entry for: + // (a) new (non-history), browser-initiated navigations, and + // (b) pending unsafe navigations (while showing the interstitial) + // in order to prevent URL spoof attacks. + web::NavigationItemImpl* pendingItemImpl = + static_cast<web::NavigationItemImpl*>([_pendingEntry navigationItem]); + if (_pendingEntry && + (!pendingItemImpl->is_renderer_initiated() || + pendingItemImpl->IsUnsafe())) { + return _pendingEntry.get(); + } + return [self lastCommittedEntry]; +} + +- (CRWSessionEntry*)pendingEntry { + return _pendingEntry.get(); +} + +- (CRWSessionEntry*)transientEntry { + return _transientEntry.get(); +} + +- (CRWSessionEntry*)lastCommittedEntry { + if (_currentNavigationIndex == -1) + return nil; + return [_entries objectAtIndex:_currentNavigationIndex]; +} + +// Returns the previous entry in the session list, or nil if there isn't any. +- (CRWSessionEntry*)previousEntry { + if ((_previousNavigationIndex < 0) || (![_entries count])) + return nil; + return [_entries objectAtIndex:_previousNavigationIndex]; +} + +- (void)addPendingEntry:(const GURL&)url + referrer:(const web::Referrer&)ref + transition:(ui::PageTransition)trans + rendererInitiated:(BOOL)rendererInitiated { + [self discardTransientEntry]; + + // Don't create a new entry if it's already the same as the current entry, + // allowing this routine to be called multiple times in a row without issue. + // 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(stuartmorgan): 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:. + CRWSessionEntry* currentEntry = self.currentEntry; + if (currentEntry) { + // If the current entry is known-unsafe (and thus not visible and likely to + // be removed), ignore any renderer-initated updates and don't worry about + // sending a notification. + web::NavigationItem* item = [currentEntry navigationItem]; + if (item->IsUnsafe() && rendererInitiated) { + return; + } + if (item->GetURL() == url && + (!PageTransitionCoreTypeIs(trans, ui::PAGE_TRANSITION_FORM_SUBMIT) || + PageTransitionCoreTypeIs(item->GetTransitionType(), + ui::PAGE_TRANSITION_FORM_SUBMIT) || + item->IsUnsafe())) { + // Send the notification anyway, to preserve old behavior. It's unknown + // whether anything currently relies on this, but since both this whole + // hack and the content facade will both be going away, it's not worth + // trying to unwind. + if (_navigationManager && _navigationManager->GetFacadeDelegate()) { + _navigationManager->GetFacadeDelegate()->OnNavigationItemPending(); + } + return; + } + } + + BOOL useDesktopUserAgent = _useDesktopUserAgentForNextPendingEntry || + self.currentEntry.useDesktopUserAgent; + _useDesktopUserAgentForNextPendingEntry = NO; + _pendingEntry.reset([[self sessionEntryWithURL:url + referrer:ref + transition:trans + useDesktopUserAgent:useDesktopUserAgent + rendererInitiated:rendererInitiated] retain]); + + if (_navigationManager && _navigationManager->GetFacadeDelegate()) { + _navigationManager->GetFacadeDelegate()->OnNavigationItemPending(); + } +} + +- (void)updatePendingEntry:(const GURL&)url { + [self discardTransientEntry]; + + // If there is no pending entry, navigation is probably happening within the + // session history. Don't modify the entry list. + if (!_pendingEntry) + return; + web::NavigationItem* item = [_pendingEntry navigationItem]; + if (url != item->GetURL()) { + item->SetURL(url); + item->SetVirtualURL(url); + // Since updates are caused by page redirects, they are renderer-initiated. + web::NavigationItemImpl* pendingItemImpl = + static_cast<web::NavigationItemImpl*>([_pendingEntry navigationItem]); + pendingItemImpl->set_is_renderer_initiated(true); + // Redirects (3xx response code), or client side navigation must change + // POST requests to GETs. + [_pendingEntry setPOSTData:nil]; + [_pendingEntry resetHTTPHeaders]; + } + + // This should probably not be sent if the URLs matched, but that's what was + // done before, so preserve behavior in case something relies on it. + if (_navigationManager && _navigationManager->GetFacadeDelegate()) { + _navigationManager->GetFacadeDelegate()->OnNavigationItemPending(); + } +} + +- (void)clearForwardEntries { + [self discardTransientEntry]; + + NSInteger forwardEntryStartIndex = _currentNavigationIndex + 1; + DCHECK(forwardEntryStartIndex >= 0); + + if (forwardEntryStartIndex >= static_cast<NSInteger>([_entries count])) + return; + + NSRange remove = NSMakeRange(forwardEntryStartIndex, + [_entries count] - forwardEntryStartIndex); + // Store removed items in temporary NSArray so they can be deallocated after + // their facades. + base::scoped_nsobject<NSArray> removedItems( + [[_entries subarrayWithRange:remove] retain]); + [_entries removeObjectsInRange:remove]; + if (_previousNavigationIndex >= forwardEntryStartIndex) + _previousNavigationIndex = -1; + if (_navigationManager && _navigationManager->GetFacadeDelegate()) { + _navigationManager->GetFacadeDelegate()->OnNavigationItemsPruned( + remove.length); + } +} + +- (void)commitPendingEntry { + if (_pendingEntry) { + [self clearForwardEntries]; + // Add the new entry at the end. + [_entries addObject:_pendingEntry]; + _previousNavigationIndex = _currentNavigationIndex; + _currentNavigationIndex = [_entries count] - 1; + // Once an entry is committed it's not renderer-initiated any more. (Matches + // the implementation in NavigationController.) + web::NavigationItemImpl* pendingItemImpl = + static_cast<web::NavigationItemImpl*>([_pendingEntry navigationItem]); + pendingItemImpl->ResetForCommit(); + _pendingEntry.reset(); + } + + CRWSessionEntry* currentEntry = self.currentEntry; + web::NavigationItem* item = currentEntry.navigationItem; + // Update the navigation timestamp now that it's actually happened. + if (item) + item->SetTimestamp(_timeSmoother.GetSmoothedTime(base::Time::Now())); + + if (_navigationManager && item) + _navigationManager->OnNavigationItemCommitted(); +} + +- (void)addTransientEntry:(const GURL&)url + title:(const base::string16&)title + sslStatus:(const web::SSLStatus*)status { + // TODO(stuartmorgan): Don't do this; this is here only to preserve the old + // behavior from when transient entries were faked with pending entries, so + // any actual pending entry had to be committed. This shouldn't be necessary + // now, but things may rely on the old behavior and need to be fixed. + [self commitPendingEntry]; + + _transientEntry.reset( + [[self sessionEntryWithURL:url + referrer:web::Referrer() + transition:ui::PAGE_TRANSITION_CLIENT_REDIRECT + useDesktopUserAgent:NO + rendererInitiated:NO] retain]); + + web::NavigationItem* navigationItem = [_transientEntry navigationItem]; + DCHECK(navigationItem); + if (status) + navigationItem->GetSSL() = *status; + navigationItem->SetTitle(title); + navigationItem->SetTimestamp( + _timeSmoother.GetSmoothedTime(base::Time::Now())); + + // This doesn't match upstream, but matches what we've traditionally done and + // will hopefully continue to be good enough for as long as we need the + // facade. + if (_navigationManager) + _navigationManager->OnNavigationItemChanged(); +} + +- (void)pushNewEntryWithURL:(const GURL&)url + stateObject:(NSString*)stateObject { + DCHECK([self currentEntry]); + web::NavigationItem* item = [self currentEntry].navigationItem; + CHECK( + web::history_state_util::IsHistoryStateChangeValid(item->GetURL(), url)); + web::Referrer referrer(item->GetURL(), web::ReferrerPolicyDefault); + base::scoped_nsobject<CRWSessionEntry> pushedEntry( + [[self sessionEntryWithURL:url + referrer:referrer + transition:ui::PAGE_TRANSITION_LINK + useDesktopUserAgent:self.currentEntry.useDesktopUserAgent + rendererInitiated:NO] retain]); + pushedEntry.get().serializedStateObject = stateObject; + pushedEntry.get().createdFromPushState = YES; + web::SSLStatus& sslStatus = [self currentEntry].navigationItem->GetSSL(); + pushedEntry.get().navigationItem->GetSSL() = sslStatus; + + [self clearForwardEntries]; + // Add the new entry at the end. + [_entries addObject:pushedEntry]; + _previousNavigationIndex = _currentNavigationIndex; + _currentNavigationIndex = [_entries count] - 1; + + if (_navigationManager) + _navigationManager->OnNavigationItemCommitted(); +} + +- (void)updateCurrentEntryWithURL:(const GURL&)url + stateObject:(NSString*)stateObject { + DCHECK(!_transientEntry); + CRWSessionEntry* currentEntry = self.currentEntry; + currentEntry.navigationItem->SetURL(url); + currentEntry.serializedStateObject = stateObject; + // If the change is to a committed entry, notify interested parties. + if (currentEntry != self.pendingEntry && _navigationManager) + _navigationManager->OnNavigationItemChanged(); +} + +- (void)discardNonCommittedEntries { + [self discardTransientEntry]; + _pendingEntry.reset(); +} + +- (void)discardTransientEntry { + // Keep the entry alive temporarily. There are flows that get the current + // entry, do some navigation operation, and then try to use that old current + // entry; since navigations clear the transient entry, these flows might + // crash. (This should be removable once more session management is handled + // within this class and/or NavigationManager). + [[_transientEntry retain] autorelease]; + _transientEntry.reset(); +} + +- (BOOL)hasPendingEntry { + return _pendingEntry != nil; +} + +- (void)copyStateFromAndPrune:(CRWSessionController*)otherSession + replaceState:(BOOL)replaceState { + DCHECK(otherSession); + if (replaceState) { + [_entries removeAllObjects]; + _currentNavigationIndex = -1; + _previousNavigationIndex = -1; + } + self.xCallbackParameters = + [[otherSession.xCallbackParameters copy] autorelease]; + self.windowName = otherSession.windowName; + NSInteger numInitialEntries = [_entries count]; + + // Cycle through the entries from the other session and insert them before any + // entries from this session. Do not copy anything that comes after the other + // session's current entry unless replaceState is true. + NSArray* otherEntries = [otherSession entries]; + + // The other session may not have any entries, in which case there is nothing + // to copy or prune. The other session's currentNavigationEntry will be bogus + // in such cases, so ignore it and return early. + // TODO(rohitrao): Do we need to copy over any pending entries? We might not + // add the prerendered page into the back/forward history if we don't copy + // pending entries. + if (![otherEntries count]) + return; + + NSInteger maxCopyIndex = replaceState ? [otherEntries count] - 1 : + [otherSession currentNavigationIndex]; + for (NSInteger i = 0; i <= maxCopyIndex; ++i) { + [_entries insertObject:[otherEntries objectAtIndex:i] atIndex:i]; + ++_currentNavigationIndex; + _previousNavigationIndex = -1; + } + + // If this CRWSessionController has no entries initially, reset + // |currentNavigationIndex_| to be in bounds. + if (!numInitialEntries) { + if (replaceState) { + _currentNavigationIndex = [otherSession currentNavigationIndex]; + _previousNavigationIndex = [otherSession previousNavigationIndex]; + } else { + _currentNavigationIndex = maxCopyIndex; + } + } + DCHECK_LT((NSUInteger)_currentNavigationIndex, [_entries count]); +} + +- (ui::PageTransition)transitionForIndex:(NSUInteger)index { + return [[_entries objectAtIndex:index] navigationItem]->GetTransitionType(); +} + +- (BOOL)canGoBack { + if ([_entries count] == 0) + return NO; + + NSInteger lastNonRedirectedIndex = _currentNavigationIndex; + while (lastNonRedirectedIndex >= 0 && + ui::PageTransitionIsRedirect( + [self transitionForIndex:lastNonRedirectedIndex])) { + --lastNonRedirectedIndex; + } + + return lastNonRedirectedIndex > 0; +} + +- (BOOL)canGoForward { + // In case there are pending entries return no since when the entry will be + // committed the history will be cleared from that point forward. + if (_pendingEntry) + return NO; + // If the current index is less than the last element, there are entries to + // go forward to. + const NSInteger count = [_entries count]; + return count && _currentNavigationIndex < (count - 1); +} + +- (void)goBack { + if (![self canGoBack]) + return; + + [self discardTransientEntry]; + + web::RecordAction(UserMetricsAction("Back")); + _previousNavigationIndex = _currentNavigationIndex; + // To stop the user getting 'stuck' on redirecting pages they weren't even + // aware existed, it is necessary to pass over pages that would immediately + // result in a redirect (the entry *before* the redirected page). + while (_currentNavigationIndex && + [self transitionForIndex:_currentNavigationIndex] & + ui::PAGE_TRANSITION_IS_REDIRECT_MASK) { + --_currentNavigationIndex; + } + + if (_currentNavigationIndex) + --_currentNavigationIndex; +} + +- (void)goForward { + [self discardTransientEntry]; + + web::RecordAction(UserMetricsAction("Forward")); + if (_currentNavigationIndex + 1 < static_cast<NSInteger>([_entries count])) { + _previousNavigationIndex = _currentNavigationIndex; + ++_currentNavigationIndex; + } + // To reduce the chance of a redirect kicking in (truncating the history + // stack) we skip over any pages that might do this; we detect this by + // looking for when the *next* page had rediection transition type (was + // auto redirected to). + while (_currentNavigationIndex + 1 < + (static_cast<NSInteger>([_entries count])) && + ([self transitionForIndex:_currentNavigationIndex + 1] & + ui::PAGE_TRANSITION_IS_REDIRECT_MASK)) { + ++_currentNavigationIndex; + } +} + +- (void)goDelta:(int)delta { + if (delta < 0) { + while ([self canGoBack] && delta < 0) { + [self goBack]; + ++delta; + } + } else { + while ([self canGoForward] && delta > 0) { + [self goForward]; + --delta; + } + } +} + +- (void)goToEntry:(CRWSessionEntry*)entry { + DCHECK(entry); + + [self discardTransientEntry]; + + // Check that |entries_| still contains |entry|. |entry| could have been + // removed by -clearForwardEntries. + if ([_entries containsObject:entry]) + _currentNavigationIndex = [_entries indexOfObject:entry]; +} + +- (void)removeEntryAtIndex:(NSInteger)index { + DCHECK(index < static_cast<NSInteger>([_entries count])); + DCHECK(index != _currentNavigationIndex); + DCHECK(index >= 0); + + [self discardNonCommittedEntries]; + + [_entries removeObjectAtIndex:index]; + if (_currentNavigationIndex > index) + _currentNavigationIndex--; + if (_previousNavigationIndex >= index) + _previousNavigationIndex--; +} + +- (NSArray*)backwardEntries { + NSMutableArray* entries = [NSMutableArray array]; + NSInteger lastNonRedirectedIndex = _currentNavigationIndex; + while (lastNonRedirectedIndex >= 0) { + CRWSessionEntry* entry = [_entries objectAtIndex:lastNonRedirectedIndex]; + if (!ui::PageTransitionIsRedirect( + entry.navigationItem->GetTransitionType())) { + [entries addObject:entry]; + } + --lastNonRedirectedIndex; + } + // Remove the currently displayed entry. + [entries removeObjectAtIndex:0]; + return entries; +} + +- (NSArray*)forwardEntries { + NSMutableArray* entries = [NSMutableArray array]; + NSUInteger lastNonRedirectedIndex = _currentNavigationIndex + 1; + while (lastNonRedirectedIndex < [_entries count]) { + CRWSessionEntry* entry = [_entries objectAtIndex:lastNonRedirectedIndex]; + if (!ui::PageTransitionIsRedirect( + entry.navigationItem->GetTransitionType())) { + [entries addObject:entry]; + } + ++lastNonRedirectedIndex; + } + return entries; +} + +- (std::vector<GURL>)currentRedirectedUrls { + std::vector<GURL> results; + if (_pendingEntry) { + web::NavigationItem* item = [_pendingEntry navigationItem]; + results.push_back(item->GetURL()); + + if (!ui::PageTransitionIsRedirect(item->GetTransitionType())) + return results; + } + + if (![_entries count]) + return results; + + NSInteger index = _currentNavigationIndex; + // Add urls in the redirected entries. + while (index >= 0) { + web::NavigationItem* item = [[_entries objectAtIndex:index] navigationItem]; + if (!ui::PageTransitionIsRedirect(item->GetTransitionType())) + break; + results.push_back(item->GetURL()); + --index; + } + // Add the last non-redirected entry. + if (index >= 0) { + web::NavigationItem* item = [[_entries objectAtIndex:index] navigationItem]; + results.push_back(item->GetURL()); + } + return results; +} + +- (BOOL)isPushStateNavigationBetweenEntry:(CRWSessionEntry*)firstEntry + andEntry:(CRWSessionEntry*)secondEntry { + DCHECK(firstEntry); + DCHECK(secondEntry); + if (firstEntry == secondEntry) + return NO; + NSUInteger firstIndex = [_entries indexOfObject:firstEntry]; + NSUInteger secondIndex = [_entries indexOfObject:secondEntry]; + if (firstIndex == NSNotFound || secondIndex == NSNotFound) + return NO; + NSUInteger startIndex = firstIndex < secondIndex ? firstIndex : secondIndex; + NSUInteger endIndex = firstIndex < secondIndex ? secondIndex : firstIndex; + + for (NSUInteger i = startIndex + 1; i <= endIndex; i++) { + CRWSessionEntry* entry = [_entries objectAtIndex:i]; + // Every entry in the sequence has to be created from a pushState() call. + if (!entry.createdFromPushState) + return NO; + // Every entry in the sequence has to have a URL that could have been + // created from a pushState() call. + if (!web::history_state_util::IsHistoryStateChangeValid( + firstEntry.navigationItem->GetURL(), + entry.navigationItem->GetURL())) + return NO; + } + return YES; +} + +- (CRWSessionEntry*)lastUserEntry { + if (![_entries count]) + return nil; + + NSInteger index = _currentNavigationIndex; + // This will return the first session entry if all other entries are + // redirects, regardless of the transition state of the first entry. + while (index > 0 && + [self transitionForIndex:index] & + ui::PAGE_TRANSITION_IS_REDIRECT_MASK) { + --index; + } + return [_entries objectAtIndex:index]; +} + +- (void)useDesktopUserAgentForNextPendingEntry { + if (_pendingEntry) + [_pendingEntry setUseDesktopUserAgent:YES]; + else + _useDesktopUserAgentForNextPendingEntry = YES; +} + +#pragma mark - +#pragma mark Private methods + +- (NSString*)uniqueID { + CFUUIDRef uuidRef = CFUUIDCreate(NULL); + CFStringRef uuidStringRef = CFUUIDCreateString(NULL, uuidRef); + CFRelease(uuidRef); + NSString* uuid = [NSString stringWithString:(NSString*)uuidStringRef]; + CFRelease(uuidStringRef); + return uuid; +} + +- (CRWSessionEntry*)sessionEntryWithURL:(const GURL&)url + referrer:(const web::Referrer&)referrer + transition:(ui::PageTransition)transition + useDesktopUserAgent:(BOOL)useDesktopUserAgent + rendererInitiated:(BOOL)rendererInitiated { + GURL loaded_url(url); + web::BrowserURLRewriter::GetInstance()->RewriteURLIfNecessary(&loaded_url, + _browserState); + return [[[CRWSessionEntry alloc] initWithUrl:loaded_url + referrer:referrer + transition:transition + useDesktopUserAgent:useDesktopUserAgent + rendererInitiated:rendererInitiated] autorelease]; +} + +@end diff --git a/ios/web/navigation/crw_session_controller_unittest.mm b/ios/web/navigation/crw_session_controller_unittest.mm new file mode 100644 index 0000000..79a6dff --- /dev/null +++ b/ios/web/navigation/crw_session_controller_unittest.mm @@ -0,0 +1,1022 @@ +// 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 <Foundation/Foundation.h> + +#include "base/logging.h" +#import "base/mac/scoped_nsobject.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/sys_string_conversions.h" +#import "ios/web/navigation/crw_session_controller+private_constructors.h" +#import "ios/web/navigation/crw_session_controller.h" +#include "ios/web/navigation/crw_session_entry.h" +#include "ios/web/navigation/navigation_item_impl.h" +#include "ios/web/public/referrer.h" +#include "ios/web/public/test/test_browser_state.h" +#import "net/base/mac/url_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gtest_mac.h" +#include "testing/platform_test.h" + +@interface CRWSessionController (Testing) +- (const GURL&)URLForSessionAtIndex:(NSUInteger)index; +- (const GURL&)currentURL; +@end + +@implementation CRWSessionController (Testing) +- (const GURL&)URLForSessionAtIndex:(NSUInteger)index { + CRWSessionEntry* entry = + static_cast<CRWSessionEntry*>([self.entries objectAtIndex:index]); + return entry.navigationItem->GetURL(); +} + +- (const GURL&)currentURL { + DCHECK([self currentEntry]); + return [self currentEntry].navigationItem->GetURL(); +} +@end + +namespace { + +class CRWSessionControllerTest : public PlatformTest { + protected: + void SetUp() override { + session_controller_.reset( + [[CRWSessionController alloc] initWithWindowName:@"test window" + openerId:@"opener" + openedByDOM:NO + openerNavigationIndex:0 + browserState:&browser_state_]); + } + + web::Referrer MakeReferrer(std::string url) { + return web::Referrer(GURL(url), web::ReferrerPolicyDefault); + } + + web::TestBrowserState browser_state_; + base::scoped_nsobject<CRWSessionController> session_controller_; +}; + +TEST_F(CRWSessionControllerTest, InitWithWindowName) { + EXPECT_NSEQ(@"test window", [session_controller_ windowName]); + EXPECT_NSEQ(@"opener", [session_controller_ openerId]); + EXPECT_FALSE([session_controller_ isOpenedByDOM]); + EXPECT_EQ(0U, [[session_controller_ entries] count]); + EXPECT_EQ(nil, [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, AddPendingEntry) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + + EXPECT_EQ(0U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ currentURL]); +} + +TEST_F(CRWSessionControllerTest, AddPendingEntryWithCommittedEntries) { + [session_controller_ + addPendingEntry:GURL("http://www.committed.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + + EXPECT_EQ(1U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.committed.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ currentURL]); +} + +TEST_F(CRWSessionControllerTest, AddPendingEntryOverriding) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ + addPendingEntry:GURL("http://www.another.url.com") + referrer:MakeReferrer("http://www.another.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + + EXPECT_EQ(0U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.another.url.com/"), + [session_controller_ currentURL]); +} + +TEST_F(CRWSessionControllerTest, AddPendingEntryAndCommit) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + EXPECT_EQ(1U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, AddPendingEntryOverridingAndCommit) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ + addPendingEntry:GURL("http://www.another.url.com") + referrer:MakeReferrer("http://www.another.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + EXPECT_EQ(1U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.another.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, AddPendingEntryAndCommitMultiple) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ + addPendingEntry:GURL("http://www.another.url.com") + referrer:MakeReferrer("http://www.another.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + EXPECT_EQ(2U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + GURL("http://www.another.url.com/"), + [session_controller_ URLForSessionAtIndex:1U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:1U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, AddPendingEntryAndDiscard) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ discardNonCommittedEntries]; + + EXPECT_EQ(0U, [[session_controller_ entries] count]); + EXPECT_EQ(nil, [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, AddPendingEntryAndDiscardAndAddAndCommit) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ discardNonCommittedEntries]; + + [session_controller_ + addPendingEntry:GURL("http://www.another.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + EXPECT_EQ(1U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.another.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, AddPendingEntryAndCommitAndAddAndDiscard) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ + addPendingEntry:GURL("http://www.another.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ discardNonCommittedEntries]; + + EXPECT_EQ(1U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, + CommitPendingEntryWithoutPendingOrCommittedEntry) { + [session_controller_ commitPendingEntry]; + + EXPECT_EQ(0U, [[session_controller_ entries] count]); + EXPECT_EQ(nil, [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, + CommitPendingEntryWithoutPendingEntryWithCommittedEntry) { + // Setup committed entry + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + // Commit pending entry when there is no such one + [session_controller_ commitPendingEntry]; + + EXPECT_EQ(1U, [[session_controller_ entries] count]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, + DiscardPendingEntryWithoutPendingOrCommittedEntry) { + [session_controller_ discardNonCommittedEntries]; + + EXPECT_EQ(0U, [[session_controller_ entries] count]); + EXPECT_EQ(nil, [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, + DiscardPendingEntryWithoutPendingEntryWithCommittedEntry) { + // Setup committed entry + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + // Discard noncommitted entries when there is no such one + [session_controller_ discardNonCommittedEntries]; + + EXPECT_EQ(1U, [[session_controller_ entries] count]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, UpdatePendingEntryWithoutPendingEntry) { + [session_controller_ + updatePendingEntry:GURL("http://www.another.url.com")]; + [session_controller_ commitPendingEntry]; + + EXPECT_EQ(0U, [[session_controller_ entries] count]); + EXPECT_EQ(nil, [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, UpdatePendingEntryWithPendingEntry) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ + updatePendingEntry:GURL("http://www.another.url.com")]; + + EXPECT_EQ( + GURL("http://www.another.url.com/"), + [session_controller_ currentURL]); +} + +TEST_F(CRWSessionControllerTest, + UpdatePendingEntryWithPendingEntryAlreadyCommited) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + updatePendingEntry:GURL("http://www.another.url.com")]; + [session_controller_ commitPendingEntry]; + + EXPECT_EQ(1U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, GoBackWithoutCommitedEntry) { + [session_controller_ goBack]; + + EXPECT_EQ(0U, [[session_controller_ entries] count]); + EXPECT_EQ(nil, [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, GoBackWithSingleCommitedEntry) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ goBack]; + + EXPECT_EQ(1U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, GoBackFromTheEnd) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url2.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ goBack]; + + EXPECT_EQ(2U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + GURL("http://www.url2.com/"), + [session_controller_ URLForSessionAtIndex:1U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, GoBackFromTheBeginning) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url2.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ goBack]; + [session_controller_ goBack]; + + EXPECT_EQ(2U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + GURL("http://www.url2.com/"), + [session_controller_ URLForSessionAtIndex:1U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, GoBackFromTheMiddle) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url2.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url3.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url4.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ goBack]; + [session_controller_ goBack]; + + EXPECT_EQ(4U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + GURL("http://www.url2.com/"), + [session_controller_ URLForSessionAtIndex:1U]); + EXPECT_EQ( + GURL("http://www.url3.com/"), + [session_controller_ URLForSessionAtIndex:2U]); + EXPECT_EQ( + GURL("http://www.url4.com/"), + [session_controller_ URLForSessionAtIndex:3U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:1U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, GoBackAndRemove) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url2.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ goBack]; + [session_controller_ removeEntryAtIndex:1]; + + EXPECT_EQ(1U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); + EXPECT_EQ([session_controller_ currentEntry], + [session_controller_ previousEntry]); +} + +TEST_F(CRWSessionControllerTest, GoForwardWithoutCommitedEntry) { + [session_controller_ goForward]; + + EXPECT_EQ(0U, [[session_controller_ entries] count]); + EXPECT_EQ(nil, [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, GoForwardWithSingleCommitedEntry) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ goForward]; + + EXPECT_EQ(1U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:0U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, GoForewardFromTheEnd) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url2.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ goForward]; + + EXPECT_EQ(2U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + GURL("http://www.url2.com/"), + [session_controller_ URLForSessionAtIndex:1U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:1U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, GoForewardFromTheBeginning) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url2.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ goBack]; + [session_controller_ goForward]; + + EXPECT_EQ(2U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + GURL("http://www.url2.com/"), + [session_controller_ URLForSessionAtIndex:1U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:1U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, GoForwardFromTheMiddle) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url2.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url3.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url4.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + [session_controller_ goBack]; + [session_controller_ goBack]; + [session_controller_ goForward]; + + EXPECT_EQ(4U, [[session_controller_ entries] count]); + EXPECT_EQ( + GURL("http://www.url.com/"), + [session_controller_ URLForSessionAtIndex:0U]); + EXPECT_EQ( + GURL("http://www.url2.com/"), + [session_controller_ URLForSessionAtIndex:1U]); + EXPECT_EQ( + GURL("http://www.url3.com/"), + [session_controller_ URLForSessionAtIndex:2U]); + EXPECT_EQ( + GURL("http://www.url4.com/"), + [session_controller_ URLForSessionAtIndex:3U]); + EXPECT_EQ( + [[session_controller_ entries] objectAtIndex:2U], + [session_controller_ currentEntry]); +} + +TEST_F(CRWSessionControllerTest, CanGoBackWithoutCommitedEntry) { + EXPECT_FALSE([session_controller_ canGoBack]); +} + +TEST_F(CRWSessionControllerTest, CanGoBackWithSingleCommitedEntry) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + EXPECT_FALSE([session_controller_ canGoBack]); +} + +TEST_F(CRWSessionControllerTest, CanGoBackWithMultipleCommitedEntries) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url1.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url2.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + EXPECT_TRUE([session_controller_ canGoBack]); + + [session_controller_ goBack]; + EXPECT_TRUE([session_controller_ canGoBack]); + + [session_controller_ goBack]; + EXPECT_FALSE([session_controller_ canGoBack]); + + [session_controller_ goBack]; + EXPECT_FALSE([session_controller_ canGoBack]); + + [session_controller_ goForward]; + EXPECT_TRUE([session_controller_ canGoBack]); +} + +TEST_F(CRWSessionControllerTest, CanGoForwardWithoutCommitedEntry) { + EXPECT_FALSE([session_controller_ canGoBack]); +} + +TEST_F(CRWSessionControllerTest, CanGoForwardWithSingleCommitedEntry) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + EXPECT_FALSE([session_controller_ canGoBack]); +} + +TEST_F(CRWSessionControllerTest, CanGoForwardWithMultipleCommitedEntries) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url1.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url2.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + EXPECT_FALSE([session_controller_ canGoForward]); + + [session_controller_ goBack]; + EXPECT_TRUE([session_controller_ canGoForward]); + + [session_controller_ goBack]; + EXPECT_TRUE([session_controller_ canGoForward]); + + [session_controller_ goForward]; + EXPECT_TRUE([session_controller_ canGoForward]); + + [session_controller_ goForward]; + EXPECT_FALSE([session_controller_ canGoForward]); +} + +// Helper to create a NavigationItem. Caller is responsible for freeing +// the memory. +web::NavigationItem* CreateNavigationItem(const std::string& url, + const std::string& referrer, + NSString* title) { + web::Referrer referrer_object(GURL(referrer), + web::ReferrerPolicyDefault); + web::NavigationItemImpl* navigation_item = new web::NavigationItemImpl(); + navigation_item->SetURL(GURL(url)); + navigation_item->SetReferrer(referrer_object); + navigation_item->SetTitle(base::SysNSStringToUTF16(title)); + navigation_item->SetTransitionType(ui::PAGE_TRANSITION_TYPED); + + return navigation_item; +} + +TEST_F(CRWSessionControllerTest, CreateWithEmptyNavigations) { + ScopedVector<web::NavigationItem> items; + base::scoped_nsobject<CRWSessionController> controller( + [[CRWSessionController alloc] initWithNavigationItems:items.Pass() + currentIndex:0 + browserState:&browser_state_]); + EXPECT_EQ(controller.get().entries.count, 0U); + EXPECT_EQ(controller.get().currentNavigationIndex, -1); + EXPECT_EQ(controller.get().previousNavigationIndex, -1); + EXPECT_FALSE(controller.get().currentEntry); +} + +TEST_F(CRWSessionControllerTest, CreateWithNavList) { + ScopedVector<web::NavigationItem> items; + items.push_back(CreateNavigationItem("http://www.google.com", + "http://www.referrer.com", @"Google")); + items.push_back(CreateNavigationItem("http://www.yahoo.com", + "http://www.google.com", @"Yahoo")); + items.push_back(CreateNavigationItem("http://www.espn.com", + "http://www.nothing.com", @"ESPN")); + base::scoped_nsobject<CRWSessionController> controller( + [[CRWSessionController alloc] initWithNavigationItems:items.Pass() + currentIndex:1 + browserState:&browser_state_]); + + EXPECT_EQ(controller.get().entries.count, 3U); + EXPECT_EQ(controller.get().currentNavigationIndex, 1); + EXPECT_EQ(controller.get().previousNavigationIndex, -1); + // Sanity check the current entry, the CRWSessionEntry unit test will ensure + // the entire object is created properly. + CRWSessionEntry* current_entry = controller.get().currentEntry; + EXPECT_EQ(current_entry.navigationItem->GetURL(), + GURL("http://www.yahoo.com")); + EXPECT_EQ([[controller openerId] length], 0UL); +} + +TEST_F(CRWSessionControllerTest, PreviousNavigationEntry) { + [session_controller_ + addPendingEntry:GURL("http://www.url.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url1.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ + addPendingEntry:GURL("http://www.url2.com") + referrer:MakeReferrer("http://www.referer.com") + transition:ui::PAGE_TRANSITION_TYPED + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + EXPECT_EQ(session_controller_.get().previousNavigationIndex, 1); + + [session_controller_ goBack]; + EXPECT_EQ(session_controller_.get().previousNavigationIndex, 2); + + [session_controller_ goBack]; + EXPECT_EQ(session_controller_.get().previousNavigationIndex, 1); + + [session_controller_ goForward]; + EXPECT_EQ(session_controller_.get().previousNavigationIndex, 0); + + [session_controller_ goForward]; + EXPECT_EQ(session_controller_.get().previousNavigationIndex, 1); +} + +TEST_F(CRWSessionControllerTest, PushNewEntry) { + ScopedVector<web::NavigationItem> items; + items.push_back(CreateNavigationItem("http://www.firstpage.com", + "http://www.starturl.com", @"First")); + items.push_back(CreateNavigationItem("http://www.secondpage.com", + "http://www.firstpage.com", @"Second")); + items.push_back(CreateNavigationItem("http://www.thirdpage.com", + "http://www.secondpage.com", @"Third")); + base::scoped_nsobject<CRWSessionController> controller( + [[CRWSessionController alloc] initWithNavigationItems:items.Pass() + currentIndex:0 + browserState:&browser_state_]); + + GURL pushPageGurl1("http://www.firstpage.com/#push1"); + NSString* stateObject1 = @"{'foo': 1}"; + [controller pushNewEntryWithURL:pushPageGurl1 stateObject:stateObject1]; + CRWSessionEntry* pushedEntry = [controller currentEntry]; + NSUInteger expectedCount = 2; + EXPECT_EQ(expectedCount, controller.get().entries.count); + EXPECT_EQ(pushPageGurl1, pushedEntry.navigationItem->GetURL()); + EXPECT_TRUE(pushedEntry.createdFromPushState); + EXPECT_NSEQ(stateObject1, pushedEntry.serializedStateObject); + EXPECT_EQ(GURL("http://www.firstpage.com/"), + pushedEntry.navigationItem->GetReferrer().url); + + // Add another new entry and check size and fields again. + GURL pushPageGurl2("http://www.firstpage.com/push2"); + [controller pushNewEntryWithURL:pushPageGurl2 stateObject:nil]; + pushedEntry = [controller currentEntry]; + expectedCount = 3; + EXPECT_EQ(expectedCount, controller.get().entries.count); + EXPECT_EQ(pushPageGurl2, pushedEntry.navigationItem->GetURL()); + EXPECT_TRUE(pushedEntry.createdFromPushState); + EXPECT_EQ(nil, pushedEntry.serializedStateObject); + EXPECT_EQ(pushPageGurl1, pushedEntry.navigationItem->GetReferrer().url); +} + +TEST_F(CRWSessionControllerTest, IsPushStateNavigation) { + ScopedVector<web::NavigationItem> items; + items.push_back( + CreateNavigationItem("http://foo.com", "http://google.com", @"First")); + // Push state navigation. + items.push_back( + CreateNavigationItem("http://foo.com#bar", "http://foo.com", @"Second")); + items.push_back(CreateNavigationItem("http://google.com", + "http://foo.com#bar", @"Third")); + items.push_back( + CreateNavigationItem("http://foo.com", "http://google.com", @"Fourth")); + // Push state navigation. + items.push_back( + CreateNavigationItem("http://foo.com/bar", "http://foo.com", @"Fifth")); + // Push state navigation. + items.push_back(CreateNavigationItem("http://foo.com/bar#bar", + "http://foo.com/bar", @"Sixth")); + base::scoped_nsobject<CRWSessionController> controller( + [[CRWSessionController alloc] initWithNavigationItems:items.Pass() + currentIndex:0 + browserState:&browser_state_]); + CRWSessionEntry* entry0 = [controller.get().entries objectAtIndex:0]; + CRWSessionEntry* entry1 = [controller.get().entries objectAtIndex:1]; + CRWSessionEntry* entry2 = [controller.get().entries objectAtIndex:2]; + CRWSessionEntry* entry3 = [controller.get().entries objectAtIndex:3]; + CRWSessionEntry* entry4 = [controller.get().entries objectAtIndex:4]; + CRWSessionEntry* entry5 = [controller.get().entries objectAtIndex:5]; + entry1.createdFromPushState = YES; + entry4.createdFromPushState = YES; + entry5.createdFromPushState = YES; + + EXPECT_TRUE( + [controller isPushStateNavigationBetweenEntry:entry0 andEntry:entry1]); + EXPECT_TRUE( + [controller isPushStateNavigationBetweenEntry:entry5 andEntry:entry3]); + EXPECT_TRUE( + [controller isPushStateNavigationBetweenEntry:entry4 andEntry:entry3]); + EXPECT_FALSE( + [controller isPushStateNavigationBetweenEntry:entry1 andEntry:entry2]); + EXPECT_FALSE( + [controller isPushStateNavigationBetweenEntry:entry0 andEntry:entry5]); + EXPECT_FALSE( + [controller isPushStateNavigationBetweenEntry:entry2 andEntry:entry4]); +} + +TEST_F(CRWSessionControllerTest, UpdateCurrentEntry) { + ScopedVector<web::NavigationItem> items; + items.push_back(CreateNavigationItem("http://www.firstpage.com", + "http://www.starturl.com", @"First")); + items.push_back(CreateNavigationItem("http://www.secondpage.com", + "http://www.firstpage.com", @"Second")); + items.push_back(CreateNavigationItem("http://www.thirdpage.com", + "http://www.secondpage.com", @"Third")); + base::scoped_nsobject<CRWSessionController> controller( + [[CRWSessionController alloc] initWithNavigationItems:items.Pass() + currentIndex:0 + browserState:&browser_state_]); + + GURL replacePageGurl1("http://www.firstpage.com/#replace1"); + NSString* stateObject1 = @"{'foo': 1}"; + + // Replace current entry and check the size of history and fields of the + // modified entry. + [controller updateCurrentEntryWithURL:replacePageGurl1 + stateObject:stateObject1]; + CRWSessionEntry* replacedEntry = [controller currentEntry]; + NSUInteger expectedCount = 3; + EXPECT_EQ(expectedCount, controller.get().entries.count); + EXPECT_EQ(replacePageGurl1, replacedEntry.navigationItem->GetURL()); + EXPECT_FALSE(replacedEntry.createdFromPushState); + EXPECT_NSEQ(stateObject1, replacedEntry.serializedStateObject); + EXPECT_EQ(GURL("http://www.starturl.com/"), + replacedEntry.navigationItem->GetReferrer().url); + + // Replace current entry and check size and fields again. + GURL replacePageGurl2("http://www.firstpage.com/#replace2"); + [controller.get() updateCurrentEntryWithURL:replacePageGurl2 stateObject:nil]; + replacedEntry = [controller currentEntry]; + EXPECT_EQ(expectedCount, controller.get().entries.count); + EXPECT_EQ(replacePageGurl2, replacedEntry.navigationItem->GetURL()); + EXPECT_FALSE(replacedEntry.createdFromPushState); + EXPECT_NSEQ(nil, replacedEntry.serializedStateObject); + EXPECT_EQ(GURL("http://www.starturl.com/"), + replacedEntry.navigationItem->GetReferrer().url); +} + +TEST_F(CRWSessionControllerTest, TestBackwardForwardEntries) { + [session_controller_ addPendingEntry:GURL("http://www.example.com/0") + referrer:MakeReferrer("http://www.example.com/a") + transition:ui::PAGE_TRANSITION_LINK + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ addPendingEntry:GURL("http://www.example.com/1") + referrer:MakeReferrer("http://www.example.com/b") + transition:ui::PAGE_TRANSITION_LINK + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ addPendingEntry:GURL("http://www.example.com/redirect") + referrer:MakeReferrer("http://www.example.com/r") + transition:ui::PAGE_TRANSITION_IS_REDIRECT_MASK + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ addPendingEntry:GURL("http://www.example.com/2") + referrer:MakeReferrer("http://www.example.com/c") + transition:ui::PAGE_TRANSITION_LINK + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + + EXPECT_EQ(3, session_controller_.get().currentNavigationIndex); + NSArray* backEntries = [session_controller_ backwardEntries]; + EXPECT_EQ(2U, [backEntries count]); + EXPECT_EQ(0U, [[session_controller_ forwardEntries] count]); + EXPECT_EQ("http://www.example.com/1", + [[backEntries objectAtIndex:0] navigationItem]->GetURL().spec()); + + [session_controller_ goBack]; + EXPECT_EQ(1U, [[session_controller_ backwardEntries] count]); + EXPECT_EQ(1U, [[session_controller_ forwardEntries] count]); + + [session_controller_ goBack]; + NSArray* forwardEntries = [session_controller_ forwardEntries]; + EXPECT_EQ(0U, [[session_controller_ backwardEntries] count]); + EXPECT_EQ(2U, [forwardEntries count]); + EXPECT_EQ("http://www.example.com/2", + [[forwardEntries objectAtIndex:1] navigationItem]->GetURL().spec()); +} + +TEST_F(CRWSessionControllerTest, GoToEntry) { + [session_controller_ addPendingEntry:GURL("http://www.example.com/0") + referrer:MakeReferrer("http://www.example.com/a") + transition:ui::PAGE_TRANSITION_LINK + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ addPendingEntry:GURL("http://www.example.com/1") + referrer:MakeReferrer("http://www.example.com/b") + transition:ui::PAGE_TRANSITION_LINK + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ addPendingEntry:GURL("http://www.example.com/redirect") + referrer:MakeReferrer("http://www.example.com/r") + transition:ui::PAGE_TRANSITION_IS_REDIRECT_MASK + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + [session_controller_ addPendingEntry:GURL("http://www.example.com/2") + referrer:MakeReferrer("http://www.example.com/c") + transition:ui::PAGE_TRANSITION_LINK + rendererInitiated:NO]; + [session_controller_ commitPendingEntry]; + EXPECT_EQ(3, session_controller_.get().currentNavigationIndex); + + CRWSessionEntry* entry1 = [session_controller_.get().entries objectAtIndex:1]; + [session_controller_ goToEntry:entry1]; + EXPECT_EQ(1, session_controller_.get().currentNavigationIndex); + + // Remove an entry and attempt to go it. Ensure it outlives the removal. + base::scoped_nsobject<CRWSessionEntry> entry3( + [[session_controller_.get().entries objectAtIndex:3] retain]); + [session_controller_ removeEntryAtIndex:3]; + [session_controller_ goToEntry:entry3]; + EXPECT_EQ(1, session_controller_.get().currentNavigationIndex); +} + +} // anonymous namespace diff --git a/ios/web/navigation/crw_session_entry.h b/ios/web/navigation/crw_session_entry.h new file mode 100644 index 0000000..00d8ec8 --- /dev/null +++ b/ios/web/navigation/crw_session_entry.h @@ -0,0 +1,65 @@ +// 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. + +#ifndef IOS_WEB_NAVIGATION_CRW_SESSION_ENTRY_H_ +#define IOS_WEB_NAVIGATION_CRW_SESSION_ENTRY_H_ + +#import <Foundation/Foundation.h> + +#include "base/memory/scoped_ptr.h" +#include "base/strings/string16.h" +#include "base/time/time.h" +#include "ui/base/page_transition_types.h" +#include "url/gurl.h" + +namespace web { +class NavigationItem; +struct Referrer; +} + +// A CRWSessionEntry is similar to a NavigationEntry object in desktop Chrome. +// It maintains the information needed to save/restore a single entry in session +// history (i.e., one site) for a tab. A tab may have multiple of these objects +// comprising its entire session history. +// TODO(rohitrao): Fold CRWSessionEntry's logic into NavigationItem. +@interface CRWSessionEntry : NSObject<NSCoding, NSCopying> + +@property(nonatomic, assign) NSInteger index; +@property(nonatomic, readonly) const GURL& originalUrl; +@property(nonatomic, assign) BOOL useDesktopUserAgent; +@property(nonatomic, assign) BOOL usedDataReductionProxy; +@property(nonatomic, retain) NSString* serializedStateObject; +@property(nonatomic, assign) BOOL createdFromPushState; +@property(nonatomic, retain) NSData* POSTData; +@property(nonatomic, readonly) NSDictionary* httpHeaders; +@property(nonatomic, assign) BOOL skipResubmitDataConfirmation; + +// Initialize the session entry with the given url. +- (id)initWithUrl:(const GURL&)url + referrer:(const web::Referrer&)referrer + transition:(ui::PageTransition)transition + useDesktopUserAgent:(BOOL)useDesktopUserAgent + rendererInitiated:(BOOL)rendererInitiated; + +// Initialize the session entry with the given NavigationItem. +- (id)initWithNavigationItem:(scoped_ptr<web::NavigationItem>)item + index:(int)index; + +// Returns a pointer to the NavigationItem associated with this CRWSessionEntry. +// Eventually, this will replace CRWSessionEntry entirely. +- (web::NavigationItem*)navigationItem; + +// Adds headers from |moreHTTPHeaders| to |httpHeaders|; existing headers with +// the same key will be overridden. +- (void)addHTTPHeaders:(NSDictionary*)moreHTTPHeaders; + +// Removes the header for the given key from |httpHeaders|. +- (void)removeHTTPHeaderForKey:(NSString*)key; + +// Resets |httpHeaders| to nil. +- (void)resetHTTPHeaders; + +@end + +#endif // IOS_WEB_NAVIGATION_CRW_SESSION_ENTRY_H_ diff --git a/ios/web/navigation/crw_session_entry.mm b/ios/web/navigation/crw_session_entry.mm new file mode 100644 index 0000000..c786499 --- /dev/null +++ b/ios/web/navigation/crw_session_entry.mm @@ -0,0 +1,318 @@ +// 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_entry.h" + +#include "base/mac/objc_property_releaser.h" +#include "base/mac/scoped_nsobject.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/sys_string_conversions.h" +#include "ios/web/navigation/navigation_item_impl.h" +#include "ios/web/navigation/nscoder_util.h" +#include "ios/web/public/navigation_item.h" +#include "ios/web/public/web_state/page_scroll_state.h" +#import "net/base/mac/url_conversions.h" + +namespace { +// Keys used to serialize web::PageScrollState properties. +NSString* const kScrollOffsetXKey = @"scrollX"; +NSString* const kScrollOffsetYKey = @"scrollY"; +NSString* const kMinimumZoomScaleKey = @"minZoom"; +NSString* const kMaximumZoomScaleKey = @"maxZoom"; +NSString* const kZoomScaleKey = @"zoom"; +} + +@interface CRWSessionEntry () { + // The index in the CRWSessionController. + // + // This is used when determining the selected CRWSessionEntry and only useful + // to the SessionServiceIOS. + NSInteger _index; + + // The original URL of the page. In cases where a redirect occurred, |url_| + // will contain the final post-redirect URL, and |originalUrl_| will contain + // the pre-redirect URL. This field is not persisted to disk. + GURL _originalUrl; + + // Headers passed along with the request. For POST requests, these are + // persisted, to be able to resubmit them. Some specialized non-POST requests + // may also pass custom headers. + base::scoped_nsobject<NSMutableDictionary> _httpHeaders; + + // Data submitted with a POST request, persisted for resubmits. + NSData* _POSTData; + + // Serialized representation of the state object that was used in conjunction + // with a JavaScript window.history.pushState() or + // window.history.replaceState() call that created or modified this + // CRWSessionEntry. Intended to be used for JavaScript history operations and + // will be nil in most cases. + NSString* _serializedStateObject; + + // Whether or not this entry was created by calling history.pushState(). + BOOL _createdFromPushState; + + // If |YES| use a desktop user agent in HTTP requests and UIWebView. + BOOL _useDesktopUserAgent; + + // If |YES| the page was last fetched through the data reduction proxy. + BOOL _usedDataReductionProxy; + + // Whether or not to bypass showing the resubmit data confirmation when + // loading a POST request. Set to YES for browser-generated POST requests such + // as search-by-image requests. + BOOL _skipResubmitDataConfirmation; + + // The NavigationItemImpl corresponding to this CRWSessionEntry. + // TODO(stuartmorgan): Move ownership to NavigationManagerImpl. + scoped_ptr<web::NavigationItemImpl> _navigationItem; + + base::mac::ObjCPropertyReleaser _propertyReleaser_CRWSessionEntry; +} +// Redefine originalUrl to be read-write. +@property(nonatomic, readwrite) const GURL& originalUrl; + +// Converts a serialized NSDictionary to a web::PageScrollState. ++ (web::PageScrollState)scrollStateFromDictionary:(NSDictionary*)dictionary; +// Serializes a web::PageScrollState to an NSDictionary. ++ (NSDictionary*)dictionaryFromScrollState: + (const web::PageScrollState&)scrollState; +// Returns a readable description of |scrollState|. ++ (NSString*)scrollStateDescription:(const web::PageScrollState&)scrollState; +@end + +@implementation CRWSessionEntry + +@synthesize POSTData = _POSTData; +@synthesize originalUrl = _originalUrl; +@synthesize useDesktopUserAgent = _useDesktopUserAgent; +@synthesize usedDataReductionProxy = _usedDataReductionProxy; +@synthesize index = _index; +@synthesize serializedStateObject = _serializedStateObject; +@synthesize createdFromPushState = _createdFromPushState; +@synthesize skipResubmitDataConfirmation = _skipResubmitDataConfirmation; + +// Creates a new session entry. These may be nil. +- (instancetype)initWithUrl:(const GURL&)url + referrer:(const web::Referrer&)referrer + transition:(ui::PageTransition)transition + useDesktopUserAgent:(BOOL)useDesktopUserAgent + rendererInitiated:(BOOL)rendererInitiated { + self = [super init]; + if (self) { + _propertyReleaser_CRWSessionEntry.Init(self, [CRWSessionEntry class]); + _navigationItem.reset(new web::NavigationItemImpl()); + + _navigationItem->SetURL(url); + _navigationItem->SetReferrer(referrer); + _navigationItem->SetTransitionType(transition); + _navigationItem->set_is_renderer_initiated(rendererInitiated); + + self.originalUrl = url; + self.useDesktopUserAgent = useDesktopUserAgent; + } + return self; +} + +- (instancetype)initWithNavigationItem:(scoped_ptr<web::NavigationItem>)item + index:(int)index { + self = [super init]; + if (self) { + _propertyReleaser_CRWSessionEntry.Init(self, [CRWSessionEntry class]); + _navigationItem.reset( + static_cast<web::NavigationItemImpl*>(item.release())); + + self.index = index; + self.originalUrl = _navigationItem->GetURL(); + self.useDesktopUserAgent = NO; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder*)aDecoder { + self = [super init]; + if (self) { + _propertyReleaser_CRWSessionEntry.Init(self, [CRWSessionEntry class]); + _navigationItem.reset(new web::NavigationItemImpl()); + + // Desktop chrome only persists virtualUrl_ and uses it to feed the url + // when creating a NavigationEntry. + GURL url; + if ([aDecoder containsValueForKey:@"virtualUrlString"]) { + url = GURL( + web::nscoder_util::DecodeString(aDecoder, @"virtualUrlString")); + } else { + // Backward compatibility. + url = net::GURLWithNSURL([aDecoder decodeObjectForKey:@"virtualUrl"]); + } + _navigationItem->SetURL(url); + self.originalUrl = url; + + if ([aDecoder containsValueForKey:@"referrerUrlString"]) { + const std::string referrerString(web::nscoder_util::DecodeString( + aDecoder, @"referrerUrlString")); + web::ReferrerPolicy referrerPolicy = + static_cast<web::ReferrerPolicy>( + [aDecoder decodeIntForKey:@"referrerPolicy"]); + _navigationItem->SetReferrer( + web::Referrer(GURL(referrerString), referrerPolicy)); + } else { + // Backward compatibility. + NSURL* referrer = [aDecoder decodeObjectForKey:@"referrer"]; + _navigationItem->SetReferrer(web::Referrer( + net::GURLWithNSURL(referrer), web::ReferrerPolicyDefault)); + } + + if ([aDecoder containsValueForKey:@"timestamp"]) { + int64 us = [aDecoder decodeInt64ForKey:@"timestamp"]; + _navigationItem->SetTimestamp(base::Time::FromInternalValue(us)); + } + + NSString* title = [aDecoder decodeObjectForKey:@"title"]; + // Use a transition type of reload so that we don't incorrectly increase + // the typed count. This is what desktop chrome does. + _navigationItem->SetPageID(-1); + _navigationItem->SetTitle(base::SysNSStringToUTF16(title)); + _navigationItem->SetTransitionType(ui::PAGE_TRANSITION_RELOAD); + _navigationItem->SetPageScrollState([[self class] + scrollStateFromDictionary:[aDecoder decodeObjectForKey:@"state"]]); + self.index = [aDecoder decodeIntForKey:@"index"]; + self.useDesktopUserAgent = + [aDecoder decodeBoolForKey:@"useDesktopUserAgent"]; + self.usedDataReductionProxy = + [aDecoder decodeBoolForKey:@"usedDataReductionProxy"]; + [self addHTTPHeaders:[aDecoder decodeObjectForKey:@"httpHeaders"]]; + self.POSTData = [aDecoder decodeObjectForKey:@"POSTData"]; + self.skipResubmitDataConfirmation = + [aDecoder decodeBoolForKey:@"skipResubmitDataConfirmation"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder*)aCoder { + // Desktop Chrome doesn't persist |url_| or |originalUrl_|, only + // |virtualUrl_|. + [aCoder encodeInt:self.index forKey:@"index"]; + web::nscoder_util::EncodeString(aCoder, @"virtualUrlString", + _navigationItem->GetVirtualURL().spec()); + web::nscoder_util::EncodeString(aCoder, @"referrerUrlString", + _navigationItem->GetReferrer().url.spec()); + [aCoder encodeInt:_navigationItem->GetReferrer().policy + forKey:@"referrerPolicy"]; + [aCoder encodeInt64:_navigationItem->GetTimestamp().ToInternalValue() + forKey:@"timestamp"]; + + [aCoder encodeObject:base::SysUTF16ToNSString(_navigationItem->GetTitle()) + forKey:@"title"]; + [aCoder encodeObject:[[self class] dictionaryFromScrollState: + _navigationItem->GetPageScrollState()] + forKey:@"state"]; + [aCoder encodeBool:self.useDesktopUserAgent forKey:@"useDesktopUserAgent"]; + [aCoder encodeBool:self.usedDataReductionProxy + forKey:@"usedDataReductionProxy"]; + [aCoder encodeObject:self.httpHeaders forKey:@"httpHeaders"]; + [aCoder encodeObject:self.POSTData forKey:@"POSTData"]; + [aCoder encodeBool:self.skipResubmitDataConfirmation + forKey:@"skipResubmitDataConfirmation"]; +} + +// TODO(ios): Shall we overwrite EqualTo:? + +- (instancetype)copyWithZone:(NSZone*)zone { + CRWSessionEntry* copy = [[[self class] alloc] init]; + copy->_propertyReleaser_CRWSessionEntry.Init(copy, [CRWSessionEntry class]); + copy->_navigationItem.reset( + new web::NavigationItemImpl(*_navigationItem.get())); + copy->_index = _index; + copy->_originalUrl = _originalUrl; + copy->_useDesktopUserAgent = _useDesktopUserAgent; + copy->_usedDataReductionProxy = _usedDataReductionProxy; + copy->_POSTData = [_POSTData copy]; + copy->_httpHeaders.reset([_httpHeaders mutableCopy]); + copy->_skipResubmitDataConfirmation = _skipResubmitDataConfirmation; + return copy; +} + +- (NSString*)description { + return [NSString + stringWithFormat: + @"url:%@ originalurl:%@ title:%@ transition:%d scrollState:%@ " + @"desktopUA:%d " @"proxy:%d", + base::SysUTF8ToNSString(_navigationItem->GetURL().spec()), + base::SysUTF8ToNSString(self.originalUrl.spec()), + base::SysUTF16ToNSString(_navigationItem->GetTitle()), + _navigationItem->GetTransitionType(), + [[self class] + scrollStateDescription:_navigationItem->GetPageScrollState()], + _useDesktopUserAgent, _usedDataReductionProxy]; +} + +- (web::NavigationItem*)navigationItem { + return _navigationItem.get(); +} + +- (NSDictionary*)httpHeaders { + return _httpHeaders ? [NSDictionary dictionaryWithDictionary:_httpHeaders] + : nil; +} + +- (void)addHTTPHeaders:(NSDictionary*)moreHTTPHeaders { + if (_httpHeaders) + [_httpHeaders addEntriesFromDictionary:moreHTTPHeaders]; + else + _httpHeaders.reset([moreHTTPHeaders mutableCopy]); +} + +- (void)removeHTTPHeaderForKey:(NSString*)key { + [_httpHeaders removeObjectForKey:key]; + if (![_httpHeaders count]) + _httpHeaders.reset(); +} + +- (void)resetHTTPHeaders { + _httpHeaders.reset(); +} + +#pragma mark - Serialization helpers + ++ (web::PageScrollState)scrollStateFromDictionary:(NSDictionary*)dictionary { + web::PageScrollState scrollState; + NSNumber* serializedValue = nil; + if ((serializedValue = dictionary[kScrollOffsetXKey])) + scrollState.set_scroll_offset_x([serializedValue doubleValue]); + if ((serializedValue = dictionary[kScrollOffsetYKey])) + scrollState.set_scroll_offset_y([serializedValue doubleValue]); + if ((serializedValue = dictionary[kMinimumZoomScaleKey])) + scrollState.set_minimum_zoom_scale([serializedValue doubleValue]); + if ((serializedValue = dictionary[kMaximumZoomScaleKey])) + scrollState.set_maximum_zoom_scale([serializedValue doubleValue]); + if ((serializedValue = dictionary[kZoomScaleKey])) + scrollState.set_zoom_scale([serializedValue doubleValue]); + return scrollState; +} + ++ (NSDictionary*)dictionaryFromScrollState: + (const web::PageScrollState&)scrollState { + return @{ + kScrollOffsetXKey : @(scrollState.scroll_offset_x()), + kScrollOffsetYKey : @(scrollState.scroll_offset_y()), + kMinimumZoomScaleKey : @(scrollState.minimum_zoom_scale()), + kMaximumZoomScaleKey : @(scrollState.maximum_zoom_scale()), + kZoomScaleKey : @(scrollState.zoom_scale()) + }; +} + ++ (NSString*)scrollStateDescription:(const web::PageScrollState&)scrollState { + NSString* const kPageScrollStateDescriptionFormat = + @"{ scrollOffset:(%0.2f, %0.2f), zoomScaleRange:(%0.2f, %0.2f), " + @"zoomScale:%0.2f }"; + return [NSString stringWithFormat:kPageScrollStateDescriptionFormat, + scrollState.scroll_offset_x(), + scrollState.scroll_offset_y(), + scrollState.minimum_zoom_scale(), + scrollState.maximum_zoom_scale(), + scrollState.zoom_scale()]; +} + +@end diff --git a/ios/web/navigation/crw_session_entry_unittest.mm b/ios/web/navigation/crw_session_entry_unittest.mm new file mode 100644 index 0000000..210afc6 --- /dev/null +++ b/ios/web/navigation/crw_session_entry_unittest.mm @@ -0,0 +1,400 @@ +// 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 <Foundation/Foundation.h> + +#include "base/mac/scoped_nsobject.h" +#include "base/strings/sys_string_conversions.h" +#import "ios/testing/ocmock_complex_type_helper.h" +#import "ios/web/navigation/crw_session_entry.h" +#include "ios/web/navigation/navigation_item_impl.h" +#include "ios/web/public/referrer.h" +#import "net/base/mac/url_conversions.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gtest_mac.h" +#include "testing/platform_test.h" +#import "third_party/ocmock/OCMock/OCMock.h" +#include "third_party/ocmock/gtest_support.h" +#include "ui/base/page_transition_types.h" + +@interface CRWSessionEntry (ExposedForTesting) ++ (web::PageScrollState)scrollStateFromDictionary:(NSDictionary*)dictionary; ++ (NSDictionary*)dictionaryFromScrollState: + (const web::PageScrollState&)scrollState; +@end + +static NSString* const kHTTPHeaderKey1 = @"key1"; +static NSString* const kHTTPHeaderKey2 = @"key2"; +static NSString* const kHTTPHeaderValue1 = @"value1"; +static NSString* const kHTTPHeaderValue2 = @"value2"; + +class CRWSessionEntryTest : public PlatformTest { + public: + static void expectEqualSessionEntries(CRWSessionEntry* entry1, + CRWSessionEntry* entry2, + ui::PageTransition transition); + + protected: + void SetUp() override { + GURL url("http://init.test"); + ui::PageTransition transition = + ui::PAGE_TRANSITION_AUTO_BOOKMARK; + sessionEntry_.reset([[CRWSessionEntry alloc] initWithUrl:url + referrer:web::Referrer() + transition:transition + useDesktopUserAgent:NO + rendererInitiated:NO]); + [sessionEntry_ navigationItem]->SetTimestamp(base::Time::Now()); + [sessionEntry_ addHTTPHeaders:@{ kHTTPHeaderKey1 : kHTTPHeaderValue1 }]; + [sessionEntry_ + setPOSTData:[@"Test data" dataUsingEncoding:NSUTF8StringEncoding]]; + } + void TearDown() override { sessionEntry_.reset(); } + + protected: + base::scoped_nsobject<CRWSessionEntry> sessionEntry_; +}; + +@implementation OCMockComplexTypeHelper (CRWSessionEntryTest) +typedef void (^encodeBytes_length_forKey_block)( + const uint8_t*, NSUInteger, NSString*); +- (void)encodeBytes:(const uint8_t*)bytes + length:(NSUInteger)length + forKey:(NSString*)key { + static_cast<encodeBytes_length_forKey_block>([self blockForSelector:_cmd])( + bytes, length, key); +} + +typedef const uint8_t* (^decodeBytesForKeyBlock)(NSString*, NSUInteger*); +- (const uint8_t*)decodeBytesForKey:(NSString*)key + returnedLength:(NSUInteger*)lengthp { + return static_cast<decodeBytesForKeyBlock>([self blockForSelector:_cmd])( + key, lengthp); +} +@end + +void CRWSessionEntryTest::expectEqualSessionEntries( + CRWSessionEntry* entry1, + CRWSessionEntry* entry2, + ui::PageTransition transition) { + EXPECT_EQ(entry1.index, entry2.index); + web::NavigationItem* navItem1 = entry1.navigationItem; + web::NavigationItem* navItem2 = entry2.navigationItem; + // url is not compared because it could differ after copy or archive. + EXPECT_EQ(navItem1->GetVirtualURL(), navItem2->GetVirtualURL()); + EXPECT_EQ(navItem1->GetReferrer().url, navItem2->GetReferrer().url); + EXPECT_EQ(navItem1->GetTimestamp(), navItem2->GetTimestamp()); + EXPECT_EQ(navItem1->GetTitle(), navItem2->GetTitle()); + EXPECT_EQ(navItem1->GetPageScrollState(), navItem2->GetPageScrollState()); + EXPECT_EQ(entry1.useDesktopUserAgent, entry2.useDesktopUserAgent); + EXPECT_EQ(entry1.usedDataReductionProxy, entry2.usedDataReductionProxy); + EXPECT_EQ(navItem2->GetTransitionType(), transition); + EXPECT_NSEQ(entry1.httpHeaders, entry2.httpHeaders); + EXPECT_TRUE((!entry1.POSTData && !entry2.POSTData) || + [entry1.POSTData isEqualToData:entry2.POSTData]); + EXPECT_EQ(entry1.skipResubmitDataConfirmation, + entry2.skipResubmitDataConfirmation); +} + +TEST_F(CRWSessionEntryTest, Description) { + [sessionEntry_ navigationItem]->SetTitle(base::SysNSStringToUTF16(@"Title")); + EXPECT_NSEQ([sessionEntry_ description], @"url:http://init.test/ " + @"originalurl:http://init.test/ " @"title:Title " @"transition:2 " + @"scrollState:{ scrollOffset:(nan, nan), zoomScaleRange:(nan, " + @"nan), zoomScale:nan } " @"desktopUA:0 " @"proxy:0"); +} + +TEST_F(CRWSessionEntryTest, InitWithCoder) { + web::NavigationItem* item = [sessionEntry_ navigationItem]; + item->SetVirtualURL(GURL("http://user.friendly")); + item->SetTitle(base::SysNSStringToUTF16(@"Title")); + int index = sessionEntry_.get().index; + // Old serialized entries have no timestamp. + item->SetTimestamp(base::Time::FromInternalValue(0)); + + NSURL* virtualUrl = net::NSURLWithGURL(item->GetVirtualURL()); + NSURL* referrer = net::NSURLWithGURL(item->GetReferrer().url); + NSString* title = base::SysUTF16ToNSString(item->GetTitle()); + base::scoped_nsobject<id> decoder([[OCMockComplexTypeHelper alloc] + initWithRepresentedObject:[OCMockObject mockForClass:[NSCoder class]]]); + + decodeBytesForKeyBlock block = ^ const uint8_t* (NSString* key, + NSUInteger* length) { + *length = 0; + return NULL; + }; + + [[[decoder stub] andReturnValue:[NSNumber numberWithBool:NO]] + containsValueForKey:[OCMArg any]]; + + [decoder onSelector:@selector(decodeBytesForKey:returnedLength:) + callBlockExpectation:block]; + [[[decoder expect] andReturnValue:OCMOCK_VALUE(index)] + decodeIntForKey:@"index"]; + [[[decoder expect] andReturn:virtualUrl] + decodeObjectForKey:@"virtualUrl"]; + [[[decoder expect] andReturn:referrer] + decodeObjectForKey:@"referrer"]; + [[[decoder expect] andReturn:title] + decodeObjectForKey:@"title"]; + const web::PageScrollState& scrollState = + [sessionEntry_ navigationItem]->GetPageScrollState(); + NSDictionary* serializedScrollState = + [CRWSessionEntry dictionaryFromScrollState:scrollState]; + [[[decoder expect] andReturn:serializedScrollState] + decodeObjectForKey:@"state"]; + BOOL useDesktopUserAgent = sessionEntry_.get().useDesktopUserAgent; + [[[decoder expect] andReturnValue:OCMOCK_VALUE(useDesktopUserAgent)] + decodeBoolForKey:@"useDesktopUserAgent"]; + BOOL usedDataReductionProxy = sessionEntry_.get().usedDataReductionProxy; + [[[decoder expect] andReturnValue:OCMOCK_VALUE(usedDataReductionProxy)] + decodeBoolForKey:@"usedDataReductionProxy"]; + [[[decoder expect] andReturn:sessionEntry_.get().httpHeaders] + decodeObjectForKey:@"httpHeaders"]; + [[[decoder expect] andReturn:sessionEntry_.get().POSTData] + decodeObjectForKey:@"POSTData"]; + BOOL skipResubmitDataConfirmation = + sessionEntry_.get().skipResubmitDataConfirmation; + [[[decoder expect] andReturnValue:OCMOCK_VALUE(skipResubmitDataConfirmation)] + decodeBoolForKey:@"skipResubmitDataConfirmation"]; + + base::scoped_nsobject<CRWSessionEntry> newSessionEntry( + [[CRWSessionEntry alloc] initWithCoder:decoder]); + web::NavigationItem* newItem = [newSessionEntry navigationItem]; + + EXPECT_OCMOCK_VERIFY(decoder); + expectEqualSessionEntries(sessionEntry_, newSessionEntry, + ui::PAGE_TRANSITION_RELOAD); + EXPECT_NE(item->GetURL(), newItem->GetURL()); + EXPECT_EQ(item->GetVirtualURL(), newItem->GetURL()); +} + +TEST_F(CRWSessionEntryTest, InitWithCoderNewStyle) { + web::NavigationItem* item = [sessionEntry_ navigationItem]; + item->SetVirtualURL(GURL("http://user.friendly")); + item->SetTitle(base::SysNSStringToUTF16(@"Title")); + int index = sessionEntry_.get().index; + int64 timestamp = item->GetTimestamp().ToInternalValue(); + + std::string virtualUrl = item->GetVirtualURL().spec(); + std::string referrerUrl = item->GetReferrer().url.spec(); + NSString* title = base::SysUTF16ToNSString(item->GetTitle()); + base::scoped_nsobject<id> decoder([[OCMockComplexTypeHelper alloc] + initWithRepresentedObject:[OCMockObject mockForClass:[NSCoder class]]]); + + const std::string emptyString; + decodeBytesForKeyBlock block = ^ const uint8_t* (NSString* key, + NSUInteger* length) { + const std::string *value = &emptyString; + if ([key isEqualToString:@"virtualUrlString"]) + value = &virtualUrl; + else if ([key isEqualToString:@"referrerUrlString"]) + value = &referrerUrl; + else + EXPECT_TRUE(false); + + *length = value->size(); + return reinterpret_cast<const uint8_t*>(value->data()); + }; + + [decoder onSelector:@selector(decodeBytesForKey:returnedLength:) + callBlockExpectation:block]; + [[[decoder stub] andReturnValue:[NSNumber numberWithBool:YES]] + containsValueForKey:[OCMArg any]]; + [[[decoder expect] andReturnValue:OCMOCK_VALUE(index)] + decodeIntForKey:@"index"]; + web::ReferrerPolicy expectedPolicy = item->GetReferrer().policy; + [[[decoder expect] + andReturnValue:OCMOCK_VALUE(expectedPolicy)] + decodeIntForKey:@"referrerPolicy"]; + [[[decoder expect] andReturnValue:OCMOCK_VALUE(timestamp)] + decodeInt64ForKey:@"timestamp"]; + [[[decoder expect] andReturn:title] + decodeObjectForKey:@"title"]; + const web::PageScrollState& scrollState = + [sessionEntry_ navigationItem]->GetPageScrollState(); + NSDictionary* serializedScrollState = + [CRWSessionEntry dictionaryFromScrollState:scrollState]; + [[[decoder expect] andReturn:serializedScrollState] + decodeObjectForKey:@"state"]; + BOOL useDesktopUserAgent = sessionEntry_.get().useDesktopUserAgent; + [[[decoder expect] andReturnValue:OCMOCK_VALUE(useDesktopUserAgent)] + decodeBoolForKey:@"useDesktopUserAgent"]; + BOOL usedDataReductionProxy = sessionEntry_.get().usedDataReductionProxy; + [[[decoder expect] andReturnValue:OCMOCK_VALUE(usedDataReductionProxy)] + decodeBoolForKey:@"usedDataReductionProxy"]; + [[[decoder expect] andReturn:sessionEntry_.get().httpHeaders] + decodeObjectForKey:@"httpHeaders"]; + [[[decoder expect] andReturn:sessionEntry_.get().POSTData] + decodeObjectForKey:@"POSTData"]; + BOOL skipResubmitDataConfirmation = + sessionEntry_.get().skipResubmitDataConfirmation; + [[[decoder expect] andReturnValue:OCMOCK_VALUE(skipResubmitDataConfirmation)] + decodeBoolForKey:@"skipResubmitDataConfirmation"]; + + base::scoped_nsobject<CRWSessionEntry> newSessionEntry( + [[CRWSessionEntry alloc] initWithCoder:decoder]); + web::NavigationItem* newItem = [newSessionEntry navigationItem]; + + EXPECT_OCMOCK_VERIFY(decoder); + expectEqualSessionEntries(sessionEntry_, newSessionEntry, + ui::PAGE_TRANSITION_RELOAD); + EXPECT_NE(item->GetURL(), newItem->GetURL()); + EXPECT_EQ(item->GetVirtualURL(), newItem->GetVirtualURL()); +} + +TEST_F(CRWSessionEntryTest, EncodeDecode) { + NSData *data = + [NSKeyedArchiver archivedDataWithRootObject:sessionEntry_]; + id decoded = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + + expectEqualSessionEntries(sessionEntry_, decoded, + ui::PAGE_TRANSITION_RELOAD); +} + +TEST_F(CRWSessionEntryTest, EncodeWithCoder) { + web::NavigationItem* item = [sessionEntry_ navigationItem]; + NSString* title = base::SysUTF16ToNSString(item->GetTitle()); + + base::scoped_nsobject<id> coder([[OCMockComplexTypeHelper alloc] + initWithRepresentedObject:[OCMockObject mockForClass:[NSCoder class]]]); + + encodeBytes_length_forKey_block block = ^(const uint8_t* bytes, + NSUInteger length, + NSString* key) { + if ([key isEqualToString:@"virtualUrlString"]) { + ASSERT_EQ(item->GetVirtualURL().spec(), + std::string(reinterpret_cast<const char*>(bytes), length)); + return; + } else if ([key isEqualToString:@"referrerUrlString"]) { + ASSERT_EQ(item->GetReferrer().url.spec(), + std::string(reinterpret_cast<const char*>(bytes), length)); + return; + } + FAIL(); + }; + [coder onSelector:@selector(encodeBytes:length:forKey:) + callBlockExpectation:block]; + [[coder expect] encodeInt:[sessionEntry_ index] forKey:@"index"]; + [[coder expect] encodeInt:item->GetReferrer().policy + forKey:@"referrerPolicy"]; + [[coder expect] encodeInt64:item->GetTimestamp().ToInternalValue() + forKey:@"timestamp"]; + [[coder expect] encodeObject:title forKey:@"title"]; + const web::PageScrollState& scrollState = + [sessionEntry_ navigationItem]->GetPageScrollState(); + NSDictionary* serializedScrollState = + [CRWSessionEntry dictionaryFromScrollState:scrollState]; + [[coder expect] encodeObject:serializedScrollState forKey:@"state"]; + [[coder expect] encodeBool:[sessionEntry_ useDesktopUserAgent] + forKey:@"useDesktopUserAgent"]; + [[coder expect] encodeBool:[sessionEntry_ usedDataReductionProxy] + forKey:@"usedDataReductionProxy"]; + [[coder expect] encodeObject:[sessionEntry_ httpHeaders] + forKey:@"httpHeaders"]; + [[coder expect] encodeObject:[sessionEntry_ POSTData] forKey:@"POSTData"]; + [[coder expect] encodeBool:[sessionEntry_ skipResubmitDataConfirmation] + forKey:@"skipResubmitDataConfirmation"]; + [sessionEntry_ encodeWithCoder:coder]; + EXPECT_OCMOCK_VERIFY(coder); +} + +TEST_F(CRWSessionEntryTest, CodingEncoding) { + web::NavigationItem* item = [sessionEntry_ navigationItem]; + item->SetVirtualURL(GURL("http://user.friendly")); + NSData* data = [NSKeyedArchiver archivedDataWithRootObject:sessionEntry_]; + EXPECT_TRUE(data != nil); + CRWSessionEntry* unarchivedSessionEntry = + [NSKeyedUnarchiver unarchiveObjectWithData:data]; + ASSERT_TRUE(unarchivedSessionEntry != nil); + web::NavigationItem* unarchivedItem = [unarchivedSessionEntry navigationItem]; + expectEqualSessionEntries(sessionEntry_, unarchivedSessionEntry, + ui::PAGE_TRANSITION_RELOAD); + EXPECT_EQ(unarchivedItem->GetURL(), item->GetVirtualURL()); + EXPECT_NE(unarchivedItem->GetURL(), item->GetURL()); +} + +TEST_F(CRWSessionEntryTest, CopyWithZone) { + CRWSessionEntry* sessionEntry2 = [sessionEntry_ copy]; + EXPECT_NE(sessionEntry_, sessionEntry2); + expectEqualSessionEntries( + sessionEntry_, sessionEntry2, + [sessionEntry_ navigationItem]->GetTransitionType()); +} + +TEST_F(CRWSessionEntryTest, EmptyVirtualUrl) { + EXPECT_EQ(GURL("http://init.test/"), + [sessionEntry_ navigationItem]->GetURL()); +} + +TEST_F(CRWSessionEntryTest, NonEmptyVirtualUrl) { + web::NavigationItem* item = [sessionEntry_ navigationItem]; + item->SetVirtualURL(GURL("http://user.friendly")); + EXPECT_EQ(GURL("http://user.friendly/"), item->GetVirtualURL()); + EXPECT_EQ(GURL("http://init.test/"), item->GetURL()); +} + +TEST_F(CRWSessionEntryTest, EmptyDescription) { + EXPECT_GT([[sessionEntry_ description] length], 0U); +} + +TEST_F(CRWSessionEntryTest, CreateWithNavigationItem) { + int index = 5; // Just pick something non-zero. + GURL url("http://www.virtualurl.com"); + web::Referrer referrer(GURL("http://www.referrer.com"), + web::ReferrerPolicyDefault); + base::string16 title = base::SysNSStringToUTF16(@"Title"); + std::string state; + ui::PageTransition transition = ui::PAGE_TRANSITION_GENERATED; + + scoped_ptr<web::NavigationItem> navigation_item( + new web::NavigationItemImpl()); + navigation_item->SetURL(url); + navigation_item->SetReferrer(referrer); + navigation_item->SetTitle(title); + navigation_item->SetTransitionType(transition); + + base::scoped_nsobject<CRWSessionEntry> sessionEntry( + [[CRWSessionEntry alloc] initWithNavigationItem:navigation_item.Pass() + index:index]); + web::NavigationItem* item = [sessionEntry navigationItem]; + // Validate everything was set correctly. + EXPECT_EQ(sessionEntry.get().index, index); + // Desktop only persists the virtual url, all three fields are initialized + // by it. + EXPECT_EQ(item->GetURL(), url); + EXPECT_EQ(item->GetVirtualURL(), url); + EXPECT_EQ(sessionEntry.get().originalUrl, url); + EXPECT_EQ(item->GetReferrer().url, referrer.url); + EXPECT_EQ(item->GetTitle(), title); + EXPECT_EQ(item->GetTransitionType(), transition); +} + +TEST_F(CRWSessionEntryTest, AddHTTPHeaders) { + EXPECT_NSEQ(@{ kHTTPHeaderKey1 : kHTTPHeaderValue1 }, + [sessionEntry_ httpHeaders]); + + [sessionEntry_ addHTTPHeaders:@{ kHTTPHeaderKey1 : kHTTPHeaderValue2 }]; + EXPECT_NSEQ(@{ kHTTPHeaderKey1 : kHTTPHeaderValue2 }, + [sessionEntry_ httpHeaders]); + + [sessionEntry_ addHTTPHeaders:@{ kHTTPHeaderKey2 : kHTTPHeaderValue1 }]; + NSDictionary* expected = @{ kHTTPHeaderKey1 : kHTTPHeaderValue2, + kHTTPHeaderKey2 : kHTTPHeaderValue1 }; + EXPECT_NSEQ(expected, [sessionEntry_ httpHeaders]); +} + +TEST_F(CRWSessionEntryTest, RemoveHTTPHeaderForKey) { + NSDictionary* httpHeaders = @{ kHTTPHeaderKey1 : kHTTPHeaderValue1, + kHTTPHeaderKey2 : kHTTPHeaderValue2 }; + [sessionEntry_ addHTTPHeaders:httpHeaders]; + EXPECT_NSEQ(httpHeaders, [sessionEntry_ httpHeaders]); + + [sessionEntry_ removeHTTPHeaderForKey:kHTTPHeaderKey1]; + EXPECT_NSEQ(@{ kHTTPHeaderKey2 : kHTTPHeaderValue2 }, + [sessionEntry_ httpHeaders]); + + [sessionEntry_ removeHTTPHeaderForKey:kHTTPHeaderKey2]; + EXPECT_TRUE([sessionEntry_ httpHeaders] == nil); +} diff --git a/ios/web/navigation/navigation_item_facade_delegate.h b/ios/web/navigation/navigation_item_facade_delegate.h new file mode 100644 index 0000000..8ccd6c0 --- /dev/null +++ b/ios/web/navigation/navigation_item_facade_delegate.h @@ -0,0 +1,31 @@ +// 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. + +#ifndef IOS_WEB_NAVIGATION_NAVIGATION_ITEM_FACADE_DELEGATE_H_ +#define IOS_WEB_NAVIGATION_NAVIGATION_ITEM_FACADE_DELEGATE_H_ + +namespace content { +class NavigationEntry; +} + +namespace web { + +class NavigationItemImpl; + +// Interface used by NavigationItems to interact with their NavigationEntry +// facades. This pushes references to NavigationEntry out of the web-layer. +// Note that since communication is one-directional, this interface is primarily +// to add hooks for creation/deletion of facades. +class NavigationItemFacadeDelegate { + public: + NavigationItemFacadeDelegate() {} + virtual ~NavigationItemFacadeDelegate() {} + + // Returns the facade object being driven by this delegate. + virtual content::NavigationEntry* GetNavigationEntryFacade() = 0; +}; + +} // namespace web + +#endif // IOS_WEB_NAVIGATION_NAVIGATION_ITEM_FACADE_DELEGATE_H_ diff --git a/ios/web/navigation/navigation_item_impl.h b/ios/web/navigation/navigation_item_impl.h index 26cc2bc..9abd992 100644 --- a/ios/web/navigation/navigation_item_impl.h +++ b/ios/web/navigation/navigation_item_impl.h @@ -8,14 +8,18 @@ #include "base/basictypes.h" #include "base/memory/scoped_ptr.h" #include "base/strings/string16.h" +#include "ios/web/navigation/navigation_item_facade_delegate.h" #include "ios/web/public/favicon_status.h" #include "ios/web/public/navigation_item.h" #include "ios/web/public/referrer.h" #include "ios/web/public/ssl_status.h" #include "url/gurl.h" + namespace web { +class NavigationItemFacadeDelegate; + // Implementation of NavigationItem. class NavigationItemImpl : public web::NavigationItem { public: @@ -23,6 +27,17 @@ class NavigationItemImpl : public web::NavigationItem { NavigationItemImpl(); ~NavigationItemImpl() override; + // Since NavigationItemImpls own their facade delegates, there is no implicit + // copy constructor (scoped_ptrs can't be copied), so one is defined here. + NavigationItemImpl(const NavigationItemImpl& item); + + // Accessors for the delegate used to drive the navigation entry facade. + // NOTE: to minimize facade synchronization code, NavigationItems take + // ownership of their facade delegates. + void SetFacadeDelegate( + scoped_ptr<NavigationItemFacadeDelegate> facade_delegate); + NavigationItemFacadeDelegate* GetFacadeDelegate() const; + // NavigationItem implementation: int GetUniqueID() const override; void SetURL(const GURL& url) override; @@ -35,6 +50,8 @@ class NavigationItemImpl : public web::NavigationItem { const base::string16& GetTitle() const override; void SetPageID(int page_id) override; int32 GetPageID() const override; + void SetPageScrollState(const PageScrollState& scroll_state) override; + const PageScrollState& GetPageScrollState() const override; const base::string16& GetTitleForDisplay( const std::string& languages) const override; void SetTransitionType(ui::PageTransition transition_type) override; @@ -45,6 +62,19 @@ class NavigationItemImpl : public web::NavigationItem { SSLStatus& GetSSL() override; void SetTimestamp(base::Time timestamp) override; base::Time GetTimestamp() const override; + void SetUnsafe(bool is_unsafe) override; + bool IsUnsafe() const override; + + // Once a navigation item is committed, we should no longer track + // non-persisted state, as documented on the members below. + void ResetForCommit(); + + // Whether this (pending) navigation is renderer-initiated. Resets to false + // for all types of navigations after commit. + void set_is_renderer_initiated(bool is_renderer_initiated) { + is_renderer_initiated_ = is_renderer_initiated; + } + bool is_renderer_initiated() const { return is_renderer_initiated_; } private: int unique_id_; @@ -53,15 +83,27 @@ class NavigationItemImpl : public web::NavigationItem { GURL virtual_url_; base::string16 title_; int32 page_id_; + PageScrollState page_scroll_state_; ui::PageTransition transition_type_; FaviconStatus favicon_; SSLStatus ssl_; base::Time timestamp_; + // Whether the item, while loading, was created for a renderer-initiated + // navigation. This dictates whether the URL should be displayed before the + // navigation commits. It is cleared in |ResetForCommit| and not persisted. + bool is_renderer_initiated_; + + // Whether the navigation contains unsafe resources. + bool is_unsafe_; + // This is a cached version of the result of GetTitleForDisplay. When the URL, // virtual URL, or title is set, this should be cleared to force a refresh. mutable base::string16 cached_display_title_; + // Weak pointer to the facade delegate. + scoped_ptr<NavigationItemFacadeDelegate> facade_delegate_; + // Copy and assignment is explicitly allowed for this class. }; diff --git a/ios/web/navigation/navigation_item_impl.mm b/ios/web/navigation/navigation_item_impl.mm index 1f86d72..56edd85 100644 --- a/ios/web/navigation/navigation_item_impl.mm +++ b/ios/web/navigation/navigation_item_impl.mm @@ -31,12 +31,42 @@ scoped_ptr<NavigationItem> NavigationItem::Create() { NavigationItemImpl::NavigationItemImpl() : unique_id_(GetUniqueIDInConstructor()), page_id_(-1), - transition_type_(ui::PAGE_TRANSITION_LINK) { + transition_type_(ui::PAGE_TRANSITION_LINK), + is_renderer_initiated_(false), + is_unsafe_(false), + facade_delegate_(nullptr) { } NavigationItemImpl::~NavigationItemImpl() { } +NavigationItemImpl::NavigationItemImpl(const NavigationItemImpl& item) + : unique_id_(item.unique_id_), + url_(item.url_), + referrer_(item.referrer_), + virtual_url_(item.virtual_url_), + title_(item.title_), + page_id_(item.page_id_), + page_scroll_state_(item.page_scroll_state_), + transition_type_(item.transition_type_), + favicon_(item.favicon_), + ssl_(item.ssl_), + timestamp_(item.timestamp_), + is_renderer_initiated_(item.is_renderer_initiated_), + is_unsafe_(item.is_unsafe_), + cached_display_title_(item.cached_display_title_), + facade_delegate_(nullptr) { +} + +void NavigationItemImpl::SetFacadeDelegate( + scoped_ptr<NavigationItemFacadeDelegate> facade_delegate) { + facade_delegate_ = facade_delegate.Pass(); +} + +NavigationItemFacadeDelegate* NavigationItemImpl::GetFacadeDelegate() const { + return facade_delegate_.get(); +} + int NavigationItemImpl::GetUniqueID() const { return unique_id_; } @@ -84,6 +114,15 @@ int32 NavigationItemImpl::GetPageID() const { return page_id_; } +void NavigationItemImpl::SetPageScrollState( + const web::PageScrollState& scroll_state) { + page_scroll_state_ = scroll_state; +} + +const PageScrollState& NavigationItemImpl::GetPageScrollState() const { + return page_scroll_state_; +} + const base::string16& NavigationItemImpl::GetTitleForDisplay( const std::string& languages) const { // Most pages have real titles. Don't even bother caching anything if this is @@ -148,4 +187,18 @@ base::Time NavigationItemImpl::GetTimestamp() const { return timestamp_; } +void NavigationItemImpl::ResetForCommit() { + // Any state that only matters when a navigation item is pending should be + // cleared here. + set_is_renderer_initiated(false); +} + +void NavigationItemImpl::SetUnsafe(bool is_unsafe) { + is_unsafe_ = is_unsafe; +} + +bool NavigationItemImpl::IsUnsafe() const { + return is_unsafe_; +} + } // namespace web diff --git a/ios/web/navigation/navigation_manager_delegate.h b/ios/web/navigation/navigation_manager_delegate.h new file mode 100644 index 0000000..bc1e058 --- /dev/null +++ b/ios/web/navigation/navigation_manager_delegate.h @@ -0,0 +1,34 @@ +// Copyright 2015 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. + +#ifndef IOS_WEB_NAVIGATION_NAVIGATION_MANAGER_DELEGATE_H_ +#define IOS_WEB_NAVIGATION_NAVIGATION_MANAGER_DELEGATE_H_ + +namespace web { + +struct LoadCommittedDetails; +class WebState; + +// Delegate for NavigationManager to hand off parts of the navigation flow. +// TODO(stuartmorgan): See if this can be eliminated by moving more +class NavigationManagerDelegate { + public: + virtual ~NavigationManagerDelegate() {} + + // Instructs the delegate to begin navigating to the pending entry. + // TODO(stuartmorgan): Remove this once more navigation logic moves to + // NavigationManagerImpl. + virtual void NavigateToPendingEntry() = 0; + + // Informs the delegate that a navigation item has been commited. + virtual void OnNavigationItemCommitted( + const LoadCommittedDetails& load_details) = 0; + + // Returns the WebState associated with this delegate. + virtual WebState* GetWebState() = 0; +}; + +} // namespace web + +#endif // IOS_WEB_NAVIGATION_NAVIGATION_MANAGER_DELEGATE_H_ diff --git a/ios/web/navigation/navigation_manager_facade_delegate.h b/ios/web/navigation/navigation_manager_facade_delegate.h new file mode 100644 index 0000000..b405f04 --- /dev/null +++ b/ios/web/navigation/navigation_manager_facade_delegate.h @@ -0,0 +1,47 @@ +// 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. + +#ifndef IOS_WEB_NAVIGATION_NAVIGATION_MANAGER_FACADE_DELEGATE_H_ +#define IOS_WEB_NAVIGATION_NAVIGATION_MANAGER_FACADE_DELEGATE_H_ + +namespace content { +class NavigationController; +} + +namespace web { + +class NavigationItemImpl; +class NavigationManagerImpl; + +// Interface used by the NavigationManager to drive the NavigationController +// facade. This pushes the ownership of the facade out of the web-layer to +// simplify upstreaming efforts. Once upstream features are componentized and +// use NavigationManager, this class will no longer be necessary. +class NavigationManagerFacadeDelegate { + public: + NavigationManagerFacadeDelegate() {} + virtual ~NavigationManagerFacadeDelegate() {} + + // Sets the NavigationManagerImpl that backs the NavigationController facade. + virtual void SetNavigationManager( + NavigationManagerImpl* navigation_manager) = 0; + // Returns the facade object being driven by this delegate. + virtual content::NavigationController* GetNavigationControllerFacade() = 0; + + // Callbacks for triggering notifications: + + // Called when the NavigationManager has a pending NavigationItem. + virtual void OnNavigationItemPending() = 0; + // Called when the NavigationManager has updated a NavigationItem. + virtual void OnNavigationItemChanged() = 0; + // Called when the NavigationManager has committed a pending NavigationItem. + virtual void OnNavigationItemCommitted(int previous_item_index, + bool is_in_page) = 0; + // Called when the NavigationManager has pruned committed NavigationItems. + virtual void OnNavigationItemsPruned(size_t pruned_item_count) = 0; +}; + +} // namespace web + +#endif // IOS_WEB_NAVIGATION_NAVIGATION_MANAGER_FACADE_DELEGATE_H_ diff --git a/ios/web/navigation/navigation_manager_impl.h b/ios/web/navigation/navigation_manager_impl.h new file mode 100644 index 0000000..92f65d4 --- /dev/null +++ b/ios/web/navigation/navigation_manager_impl.h @@ -0,0 +1,147 @@ +// Copyright 2013 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. + +#ifndef IOS_WEB_NAVIGATION_NAVIGATION_MANAGER_IMPL_H_ +#define IOS_WEB_NAVIGATION_NAVIGATION_MANAGER_IMPL_H_ + +#include <vector> + +#include "base/basictypes.h" +#include "base/mac/scoped_nsobject.h" +#include "base/memory/scoped_vector.h" +#include "ios/web/public/navigation_manager.h" +#include "ui/base/page_transition_types.h" +#include "url/gurl.h" + +@class CRWSessionController; +@class CRWSessionEntry; + +namespace web { +class BrowserState; +class NavigationItem; +struct Referrer; +class NavigationManagerDelegate; +class NavigationManagerFacadeDelegate; + +// Implementation of NavigationManager. +// Generally mirrors upstream's NavigationController. +class NavigationManagerImpl : public web::NavigationManager { + public: + NavigationManagerImpl(NavigationManagerDelegate* delegate, + BrowserState* browser_state); + ~NavigationManagerImpl() override; + + // Sets the CRWSessionController that backs this object. + // Keeps a strong reference to |session_controller|. + // This method should only be called when deserializing |session_controller| + // and joining it with its NavigationManager. Other cases should call + // InitializeSession() or ReplaceSessionHistory(). + // TODO(stuartmorgan): Also move deserialization of CRWSessionControllers + // under the control of this class, and move the bulk of CRWSessionController + // logic into it. + void SetSessionController(CRWSessionController* session_controller); + + // Initializes a new session history, supplying a unique |window_name| for the + // window (or nil). |opener_id| is the id of opener, or nil if there is none. + // |opened_by_dom| is YES if the page was opened by DOM. + // |opener_index| is the navigation index of the opener, or -1 if there is + // none. + void InitializeSession(NSString* window_name, + NSString* opener_id, + BOOL opened_by_dom, + int opener_navigation_index); + + // Replace the session history with a new one, where |items| is the + // complete set of navigation items in the new history, and |current_index| + // is the index of the currently active item. + void ReplaceSessionHistory(ScopedVector<web::NavigationItem> items, + int current_index); + + // Sets the delegate used to drive the navigation controller facade. + void SetFacadeDelegate(NavigationManagerFacadeDelegate* facade_delegate); + NavigationManagerFacadeDelegate* GetFacadeDelegate() const; + + // Helper functions for communicating with the facade layer. + // TODO(stuartmorgan): Make these private once the logic triggering them moves + // into this layer. + void OnNavigationItemChanged(); + void OnNavigationItemCommitted(); + + // Returns the pending entry corresponding to the navigation that is + // currently in progress, or nullptr if there is none. + NavigationItem* GetPendingItem() const; + + // Returns the transient item if any. This is an item which is removed and + // discarded if any navigation occurs. Note that the returned item is owned + // by the navigation manager and may be deleted at any time. + NavigationItem* GetTransientItem() const; + + // Returns the last committed NavigationItem, which may be NULL if there + // are no committed entries. + NavigationItem* GetLastCommittedItem() const; + + // Returns the committed NavigationItem at |index|. + NavigationItem* GetItemAtIndex(size_t index) const; + + // Temporary accessors and content/ class pass-throughs. + // TODO(stuartmorgan): Re-evaluate this list once the refactorings have + // settled down. + CRWSessionController* GetSessionController(); + int GetCurrentEntryIndex() const; + int GetLastCommittedEntryIndex() const; + int GetEntryCount() const; + bool RemoveEntryAtIndex(int index); + void DiscardNonCommittedEntries(); + int GetPendingEntryIndex() const; + void LoadURL(const GURL& url, + const web::Referrer& referrer, + ui::PageTransition type); + bool CanGoBack() const; + bool CanGoForward() const; + void GoBack(); + void GoForward(); + + // Convenience accessors to get the underlying NavigationItems from the + // SessionEntries returned from |session_controller_|'s -lastUserEntry and + // -previousEntry methods. + // TODO(marq):Evaluate the long-term utility of these methods. + NavigationItem* GetLastUserItem() const; + NavigationItem* GetPreviousItem() const; + + // Temporary method. Returns a vector of NavigationItems corresponding to + // the SessionEntries of the uderlying CRWSessionController. + // TOOD(marq): Remove this method and unfork its caller, + // TabRestoreServiceHelper::PopulateTab + std::vector<NavigationItem*> GetItems(); + + // NavigationManager: + BrowserState* GetBrowserState() const override; + WebState* GetWebState() const override; + web::NavigationItem* GetVisibleItem() const override; + + // Copy state from |navigation_manager|, including a copy of that object's + // CRWSessionController. + // TODO(marq): This doesn't deep-copy the SessionEntries in the + // CRWSessionController. + void CopyState(NavigationManagerImpl* navigation_manager); + private: + // The primary delegate for this manager. + NavigationManagerDelegate* delegate_; + + // The BrowserState that is associated with this instance. + BrowserState* browser_state_; + + // CRWSessionController that backs this instance. + // TODO(stuartmorgan): Fold CRWSessionController into this class. + base::scoped_nsobject<CRWSessionController> session_controller_; + + // Weak pointer to the facade delegate. + NavigationManagerFacadeDelegate* facade_delegate_; + + DISALLOW_COPY_AND_ASSIGN(NavigationManagerImpl); +}; + +} // namespace web + +#endif // IOS_WEB_NAVIGATION_NAVIGATION_MANAGER_IMPL_H_ diff --git a/ios/web/navigation/navigation_manager_impl.mm b/ios/web/navigation/navigation_manager_impl.mm new file mode 100644 index 0000000..f445104 --- /dev/null +++ b/ios/web/navigation/navigation_manager_impl.mm @@ -0,0 +1,251 @@ +// Copyright 2013 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. + +#include "ios/web/navigation/navigation_manager_impl.h" + +#include "base/logging.h" +#import "ios/web/navigation/crw_session_controller+private_constructors.h" +#import "ios/web/navigation/crw_session_controller.h" +#import "ios/web/navigation/crw_session_entry.h" +#include "ios/web/navigation/navigation_item_impl.h" +#include "ios/web/navigation/navigation_manager_delegate.h" +#import "ios/web/navigation/navigation_manager_facade_delegate.h" +#include "ios/web/public/load_committed_details.h" +#include "ios/web/public/navigation_item.h" +#include "ios/web/public/web_state/web_state.h" +#include "ui/base/page_transition_types.h" + +namespace { + +// Checks whether or not two URL are an in-page navigation (differing only +// in the fragment). +bool AreURLsInPageNavigation(const GURL& existing_url, const GURL& new_url) { + if (existing_url == new_url || !new_url.has_ref()) + return false; + + url::Replacements<char> replacements; + replacements.ClearRef(); + return existing_url.ReplaceComponents(replacements) == + new_url.ReplaceComponents(replacements); +} + +} // anonymous namespace + +namespace web { + +NavigationManagerImpl::NavigationManagerImpl( + NavigationManagerDelegate* delegate, + BrowserState* browser_state) + : delegate_(delegate), + browser_state_(browser_state), + facade_delegate_(nullptr) { + DCHECK(browser_state_); +} + +NavigationManagerImpl::~NavigationManagerImpl() { + // The facade layer should be deleted before this object. + DCHECK(!facade_delegate_); + + [session_controller_ setNavigationManager:nullptr]; +} + +CRWSessionController* NavigationManagerImpl::GetSessionController() { + return session_controller_; +} + +void NavigationManagerImpl::SetSessionController( + CRWSessionController* session_controller) { + session_controller_.reset([session_controller retain]); + [session_controller_ setNavigationManager:this]; +} + +void NavigationManagerImpl::InitializeSession(NSString* window_name, + NSString* opener_id, + BOOL opened_by_dom, + int opener_navigation_index) { + SetSessionController([[[CRWSessionController alloc] + initWithWindowName:window_name + openerId:opener_id + openedByDOM:opened_by_dom + openerNavigationIndex:opener_navigation_index + browserState:browser_state_] autorelease]); +} + +void NavigationManagerImpl::ReplaceSessionHistory( + ScopedVector<web::NavigationItem> items, + int current_index) { + SetSessionController([[[CRWSessionController alloc] + initWithNavigationItems:items.Pass() + currentIndex:current_index + browserState:browser_state_] autorelease]); +} + +void NavigationManagerImpl::SetFacadeDelegate( + NavigationManagerFacadeDelegate* facade_delegate) { + facade_delegate_ = facade_delegate; +} + +NavigationManagerFacadeDelegate* NavigationManagerImpl::GetFacadeDelegate() + const { + return facade_delegate_; +} + + +void NavigationManagerImpl::OnNavigationItemChanged() { + if (facade_delegate_) + facade_delegate_->OnNavigationItemChanged(); +} + +void NavigationManagerImpl::OnNavigationItemCommitted() { + LoadCommittedDetails details; + details.item = GetLastCommittedItem(); + DCHECK(details.item); + details.previous_item_index = [session_controller_ previousNavigationIndex]; + if (details.previous_item_index >= 0) { + DCHECK([session_controller_ previousEntry]); + details.previous_url = + [session_controller_ previousEntry].navigationItem->GetURL(); + details.is_in_page = + AreURLsInPageNavigation(details.previous_url, details.item->GetURL()); + } else { + details.previous_url = GURL(); + details.is_in_page = NO; + } + + delegate_->OnNavigationItemCommitted(details); + + if (facade_delegate_) { + facade_delegate_->OnNavigationItemCommitted(details.previous_item_index, + details.is_in_page); + } +} + +NavigationItem* NavigationManagerImpl::GetVisibleItem() const { + CRWSessionEntry* entry = [session_controller_ visibleEntry]; + return [entry navigationItem]; +} + +NavigationItem* NavigationManagerImpl::GetPendingItem() const { + return [[session_controller_ pendingEntry] navigationItem]; +} + +NavigationItem* NavigationManagerImpl::GetTransientItem() const { + return [[session_controller_ transientEntry] navigationItem]; +} + +NavigationItem* NavigationManagerImpl::GetLastCommittedItem() const { + CRWSessionEntry* entry = [session_controller_ lastCommittedEntry]; + return [entry navigationItem]; +} + +NavigationItem* NavigationManagerImpl::GetItemAtIndex(size_t index) const { + NSArray* entries = [session_controller_ entries]; + return index < entries.count ? [entries[index] navigationItem] : nullptr; +} + +int NavigationManagerImpl::GetCurrentEntryIndex() const { + return [session_controller_ currentNavigationIndex]; +} + +int NavigationManagerImpl::GetLastCommittedEntryIndex() const { + if (![[session_controller_ entries] count]) + return -1; + return [session_controller_ currentNavigationIndex]; +} + +int NavigationManagerImpl::GetEntryCount() const { + return [[session_controller_ entries] count]; +} + +bool NavigationManagerImpl::RemoveEntryAtIndex(int index) { + if (index == GetLastCommittedEntryIndex() || + index == GetPendingEntryIndex()) + return false; + + NSUInteger idx = static_cast<NSUInteger>(index); + NSArray* entries = [session_controller_ entries]; + if (idx >= entries.count) + return false; + + [session_controller_ removeEntryAtIndex:index]; + return true; +} + +void NavigationManagerImpl::DiscardNonCommittedEntries() { + [session_controller_ discardNonCommittedEntries]; +} + +NavigationItem* NavigationManagerImpl::GetLastUserItem() const { + CRWSessionEntry* entry = [session_controller_ lastUserEntry]; + return [entry navigationItem]; +} + +NavigationItem* NavigationManagerImpl::GetPreviousItem() const { + CRWSessionEntry* entry = [session_controller_ previousEntry]; + return [entry navigationItem]; +} + +int NavigationManagerImpl::GetPendingEntryIndex() const { + if ([session_controller_ hasPendingEntry]) + return GetCurrentEntryIndex(); + return -1; +} + +void NavigationManagerImpl::LoadURL(const GURL& url, + const web::Referrer& referrer, + ui::PageTransition type) { + WebState::OpenURLParams params(url, referrer, CURRENT_TAB, type, NO); + delegate_->GetWebState()->OpenURL(params); +} + +bool NavigationManagerImpl::CanGoBack() const { + return [session_controller_ canGoBack]; +} + +bool NavigationManagerImpl::CanGoForward() const { + return [session_controller_ canGoForward]; +} + +void NavigationManagerImpl::GoBack() { + if (CanGoBack()) { + [session_controller_ goBack]; + // Signal the delegate to load the old page. + delegate_->NavigateToPendingEntry(); + } +} + +void NavigationManagerImpl::GoForward() { + if (CanGoForward()) { + [session_controller_ goForward]; + // Signal the delegate to load the new page. + delegate_->NavigateToPendingEntry(); + } +} + +std::vector<NavigationItem*> NavigationManagerImpl::GetItems() { + std::vector<NavigationItem*> items; + size_t i = 0; + items.resize([session_controller_ entries].count); + for (CRWSessionEntry* entry in [session_controller_ entries]) { + items[i++] = entry.navigationItem; + } + return items; +} + +BrowserState* NavigationManagerImpl::GetBrowserState() const { + return browser_state_; +} + +WebState* NavigationManagerImpl::GetWebState() const { + return delegate_->GetWebState(); +} + +void NavigationManagerImpl::CopyState( + NavigationManagerImpl* navigation_manager) { + SetSessionController( + [[navigation_manager->GetSessionController() copy] autorelease]); +} + +} // namespace web + diff --git a/ios/web/navigation/navigation_manager_impl_unittest.mm b/ios/web/navigation/navigation_manager_impl_unittest.mm new file mode 100644 index 0000000..9460fb0 --- /dev/null +++ b/ios/web/navigation/navigation_manager_impl_unittest.mm @@ -0,0 +1,32 @@ +// 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. + +#include "base/logging.h" +#include "ios/web/navigation/navigation_manager_impl.h" +#include "ios/web/public/test/test_browser_state.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gtest_mac.h" +#include "testing/platform_test.h" + +namespace web { +namespace { + +class NavigationManagerTest : public PlatformTest { + protected: + void SetUp() override { + manager_.reset(new NavigationManagerImpl(NULL, &browser_state_)); + } + scoped_ptr<NavigationManagerImpl> manager_; + TestBrowserState browser_state_; +}; + +// TODO(stuartmorgan): Remove this once NavigationManager has actual +// functionality to test, instead of just being pass-throughs. +TEST_F(NavigationManagerTest, Dummy) { + EXPECT_FALSE(manager_->CanGoBack()); + EXPECT_FALSE(manager_->CanGoForward()); +} + +} // namespace +} // namespace web diff --git a/ios/web/navigation/web_load_params.h b/ios/web/navigation/web_load_params.h new file mode 100644 index 0000000..0fea4df --- /dev/null +++ b/ios/web/navigation/web_load_params.h @@ -0,0 +1,60 @@ +// Copyright 2013 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. + +#ifndef IOS_WEB_NAVIGATION_WEB_LOAD_PARAMS_H_ +#define IOS_WEB_NAVIGATION_WEB_LOAD_PARAMS_H_ + +#import <Foundation/Foundation.h> + +#import "base/mac/scoped_nsobject.h" +#include "ios/net/request_tracker.h" +#include "ios/web/public/referrer.h" +#include "ui/base/page_transition_types.h" +#include "url/gurl.h" + +namespace web { + +// Parameters for creating a tab. Most paramaters are optional, and +// can be left at the default values set by the constructor. +// TODO(stuartmorgan): Remove the dependency on RequestTracker, then move +// this into NavigationManager to parallel NavigationController's +// LoadURLParams (which this is modeled after). +struct WebLoadParams { + public: + // The URL to load. Must be set. + GURL url; + + // The referrer for the load. May be empty. + Referrer referrer; + + // The transition type for the load. Defaults to PAGE_TRANSITION_LINK. + ui::PageTransition transition_type; + + // True for renderer-initiated navigations. This is + // important for tracking whether to display pending URLs. + bool is_renderer_initiated; + + // The cache mode to load with. Defaults to CACHE_NORMAL. + net::RequestTracker::CacheMode cache_mode; + + // Any extra HTTP headers to add to the load. + base::scoped_nsobject<NSDictionary> extra_headers; + + // Any post data to send with the load. When setting this, you should + // generally set a Content-Type header as well. + base::scoped_nsobject<NSData> post_data; + + // Create a new WebLoadParams with the given URL and defaults for all other + // parameters. + explicit WebLoadParams(const GURL& url); + ~WebLoadParams(); + + // Allow copying WebLoadParams. + WebLoadParams(const WebLoadParams& other); + WebLoadParams& operator=(const WebLoadParams& other); +}; + +} // namespace web + +#endif // IOS_WEB_NAVIGATION_WEB_LOAD_PARAMS_H_ diff --git a/ios/web/navigation/web_load_params.mm b/ios/web/navigation/web_load_params.mm new file mode 100644 index 0000000..2c5a82c --- /dev/null +++ b/ios/web/navigation/web_load_params.mm @@ -0,0 +1,42 @@ +// Copyright 2013 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. + +#include "ios/web/navigation/web_load_params.h" + +namespace web { + +WebLoadParams::WebLoadParams(const GURL& url) + : url(url), + transition_type(ui::PAGE_TRANSITION_LINK), + is_renderer_initiated(false), + cache_mode(net::RequestTracker::CACHE_NORMAL), + post_data(NULL) { +} + +WebLoadParams::~WebLoadParams() {} + +WebLoadParams::WebLoadParams(const WebLoadParams& other) + : url(other.url), + referrer(other.referrer), + transition_type(other.transition_type), + is_renderer_initiated(other.is_renderer_initiated), + cache_mode(other.cache_mode), + extra_headers([other.extra_headers copy]), + post_data([other.post_data copy]) { +} + +WebLoadParams& WebLoadParams::operator=( + const WebLoadParams& other) { + url = other.url; + referrer = other.referrer; + is_renderer_initiated = other.is_renderer_initiated; + transition_type = other.transition_type; + cache_mode = other.cache_mode; + extra_headers.reset([other.extra_headers copy]); + post_data.reset([other.post_data copy]); + + return *this; +} + +} // namespace web diff --git a/ios/web/public/navigation_item.h b/ios/web/public/navigation_item.h index a17a7e4..e067a82 100644 --- a/ios/web/public/navigation_item.h +++ b/ios/web/public/navigation_item.h @@ -8,6 +8,7 @@ #include "base/memory/scoped_ptr.h" #include "base/strings/string16.h" #include "base/time/time.h" +#include "ios/web/public/web_state/page_scroll_state.h" #include "ui/base/page_transition_types.h" class GURL; @@ -68,6 +69,10 @@ class NavigationItem { virtual void SetPageID(int page_id) = 0; virtual int32 GetPageID() const = 0; + // Stores the NavigationItem's last recorded scroll offset and zoom scale. + virtual void SetPageScrollState(const PageScrollState& scroll_state) = 0; + virtual const PageScrollState& GetPageScrollState() const = 0; + // Page-related helpers ------------------------------------------------------ // Returns the title to be displayed on the tab. This could be the title of @@ -104,6 +109,11 @@ class NavigationItem { // - or this navigation was copied from a foreign session. virtual void SetTimestamp(base::Time timestamp) = 0; virtual base::Time GetTimestamp() const = 0; + + // |true| if this item contains unsafe resources and will be removed. This + // property doesn't get serialized. + virtual void SetUnsafe(bool is_unsafe) = 0; + virtual bool IsUnsafe() const = 0; }; } // namespace web diff --git a/ios/web/public/web_state/page_scroll_state.h b/ios/web/public/web_state/page_scroll_state.h new file mode 100644 index 0000000..9984019 --- /dev/null +++ b/ios/web/public/web_state/page_scroll_state.h @@ -0,0 +1,75 @@ +// Copyright 2015 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. + +#ifndef IOS_WEB_PUBLIC_WEB_STATE_PAGE_SCROLL_STATE_H_ +#define IOS_WEB_PUBLIC_WEB_STATE_PAGE_SCROLL_STATE_H_ + +namespace web { + +// Class used to represent the scrolling offset and the zoom scale of a webview. +class PageScrollState { + public: + // Default constructor. Initializes scroll offsets and zoom scales to NAN. + PageScrollState(); + // Constructor with initial values. + PageScrollState(double scroll_offset_x, + double scroll_offset_y, + double minimum_zoom_scale, + double maximum_zoom_scale, + double zoom_scale); + ~PageScrollState(); + + // PageScrollStates cannot be applied until the scroll offset and zoom scale + // are both valid. + bool IsValid() const; + + // The scroll offset is valid if its x and y values are both non-NAN. + bool IsScrollOffsetValid() const; + + // Non-legacy zoom scales are valid if all three values are non-NAN and the + // zoom scale is within the minimum and maximum scales. Legacy-format + // PageScrollStates are considered valid if the minimum and maximum scales + // are NAN and the zoom scale is greater than zero. + bool IsZoomScaleValid() const; + + // PageScrollStates restored from the legacy serialization format make + // assumptions about the web view's implementation of zooming, and contain a + // non-NAN zoom scale and a NAN minimum and maximum scale. Legacy zoom scales + // can only be applied to CRWUIWebViewWebControllers. + bool IsZoomScaleLegacyFormat() const; + + // Accessors for scroll offsets and zoom scale. + double scroll_offset_x() const { return scroll_offset_x_; } + void set_scroll_offset_x(double scroll_offset_x) { + scroll_offset_x_ = scroll_offset_x; + } + double scroll_offset_y() const { return scroll_offset_y_; } + void set_scroll_offset_y(double scroll_offset_y) { + scroll_offset_y_ = scroll_offset_y; + } + double minimum_zoom_scale() const { return minimum_zoom_scale_; } + void set_minimum_zoom_scale(double minimum_zoom_scale) { + minimum_zoom_scale_ = minimum_zoom_scale; + } + double maximum_zoom_scale() const { return maximum_zoom_scale_; } + void set_maximum_zoom_scale(double maximum_zoom_scale) { + maximum_zoom_scale_ = maximum_zoom_scale; + } + double zoom_scale() const { return zoom_scale_; } + void set_zoom_scale(double zoom_scale) { zoom_scale_ = zoom_scale; } + + // Comparator operator. + bool operator==(const PageScrollState& other) const; + + private: + double scroll_offset_x_; + double scroll_offset_y_; + double minimum_zoom_scale_; + double maximum_zoom_scale_; + double zoom_scale_; +}; + +} // namespace web + +#endif // IOS_WEB_PUBLIC_WEB_STATE_PAGE_SCROLL_STATE_H_ diff --git a/ios/web/public/web_state/page_scroll_state.mm b/ios/web/public/web_state/page_scroll_state.mm new file mode 100644 index 0000000..b7479fd --- /dev/null +++ b/ios/web/public/web_state/page_scroll_state.mm @@ -0,0 +1,72 @@ +// Copyright 2015 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. + +#include "ios/web/public/web_state/page_scroll_state.h" + +#include <cmath> + +namespace web { + +namespace { +// Returns true if: +// - both |value1| and |value2| are NAN, or +// - |value1| and |value2| are equal non-NAN values. +inline bool StateValuesAreEqual(double value1, double value2) { + return std::isnan(value1) ? std::isnan(value2) : value1 == value2; +} +} // namespace + +PageScrollState::PageScrollState() + : scroll_offset_x_(NAN), + scroll_offset_y_(NAN), + minimum_zoom_scale_(NAN), + maximum_zoom_scale_(NAN), + zoom_scale_(NAN) { +} + +PageScrollState::PageScrollState(double scroll_offset_x, + double scroll_offset_y, + double minimum_zoom_scale, + double maximum_zoom_scale, + double zoom_scale) + : scroll_offset_x_(scroll_offset_x), + scroll_offset_y_(scroll_offset_y), + minimum_zoom_scale_(minimum_zoom_scale), + maximum_zoom_scale_(maximum_zoom_scale), + zoom_scale_(zoom_scale) { +} + +PageScrollState::~PageScrollState() { +} + +bool PageScrollState::IsValid() const { + return IsScrollOffsetValid() && IsZoomScaleValid(); +} + +bool PageScrollState::IsScrollOffsetValid() const { + return !std::isnan(scroll_offset_x_) && !std::isnan(scroll_offset_y_); +} + +bool PageScrollState::IsZoomScaleValid() const { + return IsZoomScaleLegacyFormat() || + (!std::isnan(minimum_zoom_scale_) && + !std::isnan(maximum_zoom_scale_) && !std::isnan(zoom_scale_) && + zoom_scale_ >= minimum_zoom_scale_ && + zoom_scale_ <= maximum_zoom_scale_); +} + +bool PageScrollState::IsZoomScaleLegacyFormat() const { + return std::isnan(minimum_zoom_scale_) && std::isnan(maximum_zoom_scale_) && + zoom_scale_ > 0.0; +} + +bool PageScrollState::operator==(const PageScrollState& other) const { + return StateValuesAreEqual(scroll_offset_x_, other.scroll_offset_x_) && + StateValuesAreEqual(scroll_offset_y_, other.scroll_offset_y_) && + StateValuesAreEqual(minimum_zoom_scale_, other.minimum_zoom_scale_) && + StateValuesAreEqual(maximum_zoom_scale_, other.maximum_zoom_scale_) && + StateValuesAreEqual(zoom_scale_, other.zoom_scale_); +} + +} // namespace web |