diff options
author | droger <droger@chromium.org> | 2015-03-18 08:29:53 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-03-18 15:30:49 +0000 |
commit | 3e8d98b6e21fa696a4cb85a59cb0023a83f1e991 (patch) | |
tree | 3130004069c1ff9f489a901397c26a69431a8f94 /ios | |
parent | 23445b31b84396a79d77c16fe700aedaf393d9f6 (diff) | |
download | chromium_src-3e8d98b6e21fa696a4cb85a59cb0023a83f1e991.zip chromium_src-3e8d98b6e21fa696a4cb85a59cb0023a83f1e991.tar.gz chromium_src-3e8d98b6e21fa696a4cb85a59cb0023a83f1e991.tar.bz2 |
[iOS] Upstream CookieStoreIOS
Review URL: https://codereview.chromium.org/1021463002
Cr-Commit-Position: refs/heads/master@{#321138}
Diffstat (limited to 'ios')
-rw-r--r-- | ios/net/cookies/cookie_store_ios.h | 298 | ||||
-rw-r--r-- | ios/net/cookies/cookie_store_ios.mm | 961 | ||||
-rw-r--r-- | ios/net/cookies/cookie_store_ios_unittest.mm | 912 | ||||
-rw-r--r-- | ios/net/ios_net.gyp | 2 | ||||
-rw-r--r-- | ios/net/ios_net_unittests.gyp | 1 |
5 files changed, 2174 insertions, 0 deletions
diff --git a/ios/net/cookies/cookie_store_ios.h b/ios/net/cookies/cookie_store_ios.h new file mode 100644 index 0000000..d44123d --- /dev/null +++ b/ios/net/cookies/cookie_store_ios.h @@ -0,0 +1,298 @@ +// 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_NET_COOKIES_COOKIE_STORE_IOS_H_ +#define IOS_NET_COOKIES_COOKIE_STORE_IOS_H_ + +#include <map> +#include <string> +#include <utility> +#include <vector> + +#include "base/callback.h" +#include "base/cancelable_callback.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/thread_checker.h" +#include "base/time/time.h" +#include "ios/net/cookies/cookie_cache.h" +#include "net/cookies/cookie_monster.h" +#include "net/cookies/cookie_store.h" +#include "url/gurl.h" + +#if defined(__OBJC__) +@class NSHTTPCookie; +@class NSArray; +#else +class NSHTTPCookie; +class NSArray; +#endif + +namespace net { + +class CookieCreationTimeManager; + +// Observer for system cookie notifications. +class CookieNotificationObserver { + public: + // Called when any cookie is added, deleted or changed in the system store. + virtual void OnSystemCookiesChanged() = 0; + // Called when the cookie policy changes. + virtual void OnSystemCookiePolicyChanged() = 0; +}; + +// The CookieStoreIOS is an implementation of CookieStore relying on +// NSHTTPCookieStorage, ensuring that the cookies are consistent between the +// network stack and the UIWebViews. +// On iOS, the Chrome CookieMonster is not used in conjunction with UIWebView, +// because UIWebView expects the cookies to be in the shared +// NSHTTPCookieStorage. In particular, javascript may read and write cookies +// there. +// CookieStoreIOS is not thread safe. +// +// At any given time, a CookieStoreIOS can either be synchronized with the +// system cookie store or not. If a CookieStoreIOS is not synchronized with the +// system store, changes are written back to the backing CookieStore. If a +// CookieStoreIOS is synchronized with the system store, changes are written +// directly to the system cookie store, then propagated to the backing store by +// OnSystemCookiesChanged, which is called by the system store once the change +// to the system store is written back. +// +// To unsynchronize, CookieStoreIOS copies the system cookie store into its +// backing CookieStore. To synchronize, CookieStoreIOS clears the system cookie +// store, copies its backing CookieStore into the system cookie store. +class CookieStoreIOS : public net::CookieStore, + public CookieNotificationObserver { + public: + explicit CookieStoreIOS( + net::CookieMonster::PersistentCookieStore* persistent_store); + + enum CookiePolicy { ALLOW, BLOCK }; + + // Must be called on the thread where CookieStoreIOS instances live. + static void SetCookiePolicy(CookiePolicy setting); + + // Create an instance of CookieStoreIOS that is generated from the cookies + // stored in the system NSHTTPCookieStorage. The caller is responsible for + // deleting the cookie store. + // Apple does not persist the cookies' creation dates in the system store, + // so callers should not expect these values to be populated. + static CookieStoreIOS* CreateCookieStoreFromNSHTTPCookieStorage(); + + // As there is only one system store, only one CookieStoreIOS at a time may + // be synchronized with it. + static void SwitchSynchronizedStore(CookieStoreIOS* old_store, + CookieStoreIOS* new_store); + + // Must be called when the state of the system cookie store changes. + static void NotifySystemCookiesChanged(); + + // Saves the cookies to the cookie monster. + // Note: ignores the write cookie operation if |write_on_flush_| is false. + void Flush(const base::Closure& callback); + + // Unsynchronizes the cookie store if it is currently synchronized. + void UnSynchronize(); + + // Only one cookie store may enable metrics. + void SetMetricsEnabled(); + + // Sets the delay between flushes. Only used in tests. + void set_flush_delay_for_testing(base::TimeDelta delay) { + flush_delay_ = delay; + } + + // Inherited CookieStore methods. + void SetCookieWithOptionsAsync(const GURL& url, + const std::string& cookie_line, + const net::CookieOptions& options, + const SetCookiesCallback& callback) override; + void GetCookiesWithOptionsAsync(const GURL& url, + const net::CookieOptions& options, + const GetCookiesCallback& callback) override; + void GetAllCookiesForURLAsync(const GURL& url, + const GetCookieListCallback& callback) override; + void DeleteCookieAsync(const GURL& url, + const std::string& cookie_name, + const base::Closure& callback) override; + net::CookieMonster* GetCookieMonster() override; + void DeleteAllCreatedBetweenAsync(const base::Time& delete_begin, + const base::Time& delete_end, + const DeleteCallback& callback) override; + void DeleteAllCreatedBetweenForHostAsync( + const base::Time delete_begin, + const base::Time delete_end, + const GURL& url, + const DeleteCallback& callback) override; + void DeleteSessionCookiesAsync(const DeleteCallback& callback) override; + + scoped_ptr<CookieChangedSubscription> AddCallbackForCookie( + const GURL& url, + const std::string& name, + const CookieChangedCallback& callback) override; + + protected: + ~CookieStoreIOS() override; + + private: + // For tests. + friend struct CookieStoreIOSTestTraits; + + enum SynchronizationState { + NOT_SYNCHRONIZED, // Uses CookieMonster as backend. + SYNCHRONIZING, // Moves from NSHTTPCookieStorage to CookieMonster. + SYNCHRONIZED // Uses NSHTTPCookieStorage as backend. + }; + + // Cookie fliter for DeleteCookiesWithFilter(). + // Takes a cookie and a creation time and returns true if the cookie must be + // deleted. + typedef base::Callback<bool(NSHTTPCookie*, base::Time)> CookieFilterFunction; + + // Clears the system cookie store. + void ClearSystemStore(); + // Changes the synchronization of the store. + // If |synchronized| is true, then the system cookie store is used as a + // backend, else |cookie_monster_| is used. Cookies are moved from one to + // the other accordingly. + void SetSynchronizedWithSystemStore(bool synchronized); + // Returns true if the system cookie store policy is + // |NSHTTPCookieAcceptPolicyAlways|. + bool SystemCookiesAllowed(); + // Converts |cookies| to NSHTTPCookie and add them to the system store. + void AddCookiesToSystemStore(const net::CookieList& cookies); + // Copies the cookies to the backing CookieMonster. If the cookie store is not + // synchronized with the system store, this is a no-op. + void WriteToCookieMonster(NSArray* system_cookies); + + // Inherited CookieNotificationObserver methods. + void OnSystemCookiesChanged() override; + void OnSystemCookiePolicyChanged() override; + + void DeleteCookiesWithFilter(const CookieFilterFunction& filter, + const DeleteCallback& callback); + + scoped_refptr<net::CookieMonster> cookie_monster_; + scoped_ptr<CookieCreationTimeManager> creation_time_manager_; + bool metrics_enabled_; + base::TimeDelta flush_delay_; + base::CancelableClosure flush_closure_; + + SynchronizationState synchronization_state_; + // Tasks received when SYNCHRONIZING are queued and run when SYNCHRONIZED. + std::vector<base::Closure> tasks_pending_synchronization_; + + base::ThreadChecker thread_checker_; + + // Cookie notification methods. + // The cookie cache is updated from both the system store and the + // CookieStoreIOS' own mutators. Changes when the CookieStoreIOS is + // synchronized are signalled by the system store; changes when the + // CookieStoreIOS is not synchronized are signalled by the appropriate + // mutators on CookieStoreIOS. The cookie cache tracks the system store when + // the CookieStoreIOS is synchronized and the CookieStore when the + // CookieStoreIOS is not synchronized. + + // Fetches any cookies named |name| that would be sent with a request for + // |url| from the system cookie store and pushes them onto the back of the + // vector pointed to by |cookies|. Returns true if any cookies were pushed + // onto the vector, and false otherwise. + bool GetSystemCookies(const GURL& url, + const std::string& name, + std::vector<net::CanonicalCookie>* cookies); + + // Updates the cookie cache with the current set of system cookies named + // |name| that would be sent with a request for |url|. Returns whether the + // cache changed. + // |out_removed_cookies|, if not null, will be populated with the cookies that + // were removed. + // |out_changed_cookies|, if not null, will be populated with the cookies that + // were added. + bool UpdateCacheForCookieFromSystem( + const GURL& gurl, + const std::string& name, + std::vector<net::CanonicalCookie>* out_removed_cookies, + std::vector<net::CanonicalCookie>* out_added_cookies); + + // Runs all callbacks registered for cookies named |name| that would be sent + // with a request for |url|. + // All cookies in |cookies| must have the name equal to |name|. + void RunCallbacksForCookies(const GURL& url, + const std::string& name, + const std::vector<net::CanonicalCookie>& cookies, + bool removed); + + // Called by this CookieStoreIOS' internal CookieMonster instance when + // GetAllCookiesForURLAsync() completes. Updates the cookie cache and runs + // callbacks if the cache changed. + void GotCookieListFor(const std::pair<GURL, std::string> key, + const net::CookieList& cookies); + + // Fetches new values for all (url, name) pairs that have hooks registered, + // asynchronously invoking callbacks if necessary. + void UpdateCachesFromCookieMonster(); + + // Called after cookies are cleared from NSHTTPCookieStorage so that cookies + // can be cleared from .binarycookies file. |callback| is called after all the + // cookies are deleted (with the total number of cookies deleted). + // |num_deleted| contains the number of cookies deleted from + // NSHTTPCookieStorage. + void DidClearNSHTTPCookieStorageCookies(const DeleteCallback& callback, + int num_deleted); + // Called after cookies are cleared from .binarycookies files. |callback| is + // called after all the cookies are deleted with the total number of cookies + // deleted. + // |num_deleted_from_nshttp_cookie_storage| contains the number of cookies + // deleted from NSHTTPCookieStorage. + void DidClearBinaryCookiesFileCookies( + const DeleteCallback& callback, + int num_deleted_from_nshttp_cookie_storage); + + // Callback-wrapping: + // When this CookieStoreIOS object is synchronized with the system store, + // OnSystemCookiesChanged is responsible for updating the cookie cache (and + // hence running callbacks). + // + // When this CookieStoreIOS object is not synchronized (or is synchronizing), + // the various mutator methods (SetCookieWithOptionsAsync &c) instead store + // their state in a CookieMonster object to be written back when the system + // store synchronizes. To deliver notifications in a timely manner, the + // mutators have to ensure that hooks get run, but only after the changes have + // been written back to CookieMonster. To do this, the mutators wrap the + // user-supplied callback in a callback which schedules an asynchronous task + // to synchronize the cache and run callbacks, then calls through to the + // user-specified callback. + // + // These three UpdateCachesAfter functions are responsible for scheduling an + // asynchronous cache update (using UpdateCachesFromCookieMonster()) and + // calling the provided callback. + + void UpdateCachesAfterSet(const SetCookiesCallback& callback, bool success); + void UpdateCachesAfterDelete(const DeleteCallback& callback, int num_deleted); + void UpdateCachesAfterClosure(const base::Closure& callback); + + // These three functions are used for wrapping user-supplied callbacks given + // to CookieStoreIOS mutator methods. Given a callback, they return a new + // callback that invokes UpdateCachesFromCookieMonster() to schedule an + // asynchronous synchronization of the cookie cache and then calls the + // original callback. + + SetCookiesCallback WrapSetCallback(const SetCookiesCallback& callback); + DeleteCallback WrapDeleteCallback(const DeleteCallback& callback); + base::Closure WrapClosure(const base::Closure& callback); + + // Cached values of system cookies. Only cookies which have an observer added + // with AddCallbackForCookie are kept in this cache. + scoped_ptr<CookieCache> cookie_cache_; + + // Callbacks for cookie changes installed by AddCallbackForCookie. + typedef std::map<std::pair<GURL, std::string>, CookieChangedCallbackList*> + CookieChangedHookMap; + CookieChangedHookMap hook_map_; + + DISALLOW_COPY_AND_ASSIGN(CookieStoreIOS); +}; + +} // namespace net + +#endif // IOS_NET_COOKIES_COOKIE_STORE_IOS_H_ diff --git a/ios/net/cookies/cookie_store_ios.mm b/ios/net/cookies/cookie_store_ios.mm new file mode 100644 index 0000000..4ccf8b7 --- /dev/null +++ b/ios/net/cookies/cookie_store_ios.mm @@ -0,0 +1,961 @@ +// 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. + +#include "ios/net/cookies/cookie_store_ios.h" + +#import <Foundation/Foundation.h> + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/ios/ios_util.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/mac/foundation_util.h" +#include "base/mac/scoped_nsobject.h" +#include "base/memory/weak_ptr.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/metrics/histogram.h" +#include "base/observer_list.h" +#include "base/sequenced_task_runner.h" +#include "base/stl_util.h" +#include "base/strings/sys_string_conversions.h" +#include "base/task_runner_util.h" +#include "base/threading/thread_restrictions.h" +#include "ios/net/cookies/cookie_creation_time_manager.h" +#include "ios/net/cookies/cookie_store_ios_client.h" +#include "ios/net/cookies/system_cookie_util.h" +#import "net/base/mac/url_conversions.h" +#include "net/cookies/cookie_util.h" +#include "url/gurl.h" + +namespace net { + +namespace { + +#if !defined(NDEBUG) +// The current cookie store. This weak pointer must not be used to do actual +// work. Its only purpose is to check that there is only one synchronized +// cookie store. +CookieStoreIOS* g_current_synchronized_store = nullptr; +#endif + +#pragma mark NotificationTrampoline + +// NotificationTrampoline dispatches cookie notifications to all the existing +// CookieStoreIOS. +class NotificationTrampoline { + public: + static NotificationTrampoline* GetInstance(); + + void AddObserver(CookieNotificationObserver* obs); + void RemoveObserver(CookieNotificationObserver* obs); + + // Notify the observers. + void NotifyCookiesChanged(); + void NotifyCookiePolicyChanged(); + + private: + NotificationTrampoline(); + ~NotificationTrampoline(); + + ObserverList<CookieNotificationObserver> observer_list_; + + DISALLOW_COPY_AND_ASSIGN(NotificationTrampoline); + + static NotificationTrampoline* g_notification_trampoline; +}; + +#pragma mark NotificationTrampoline implementation + +NotificationTrampoline* NotificationTrampoline::GetInstance() { + if (!g_notification_trampoline) + g_notification_trampoline = new NotificationTrampoline; + return g_notification_trampoline; +} + +void NotificationTrampoline::AddObserver(CookieNotificationObserver* obs) { + observer_list_.AddObserver(obs); +} + +void NotificationTrampoline::RemoveObserver(CookieNotificationObserver* obs) { + observer_list_.RemoveObserver(obs); +} + +void NotificationTrampoline::NotifyCookiesChanged() { + FOR_EACH_OBSERVER(CookieNotificationObserver, observer_list_, + OnSystemCookiesChanged()); +} + +void NotificationTrampoline::NotifyCookiePolicyChanged() { + FOR_EACH_OBSERVER(CookieNotificationObserver, observer_list_, + OnSystemCookiePolicyChanged()); +} + +NotificationTrampoline::NotificationTrampoline() { +} + +NotificationTrampoline::~NotificationTrampoline() { +} + +// Global instance of NotificationTrampoline. +NotificationTrampoline* NotificationTrampoline::g_notification_trampoline = + nullptr; + +#pragma mark Utility functions + +// Returns the path to Cookie.binarycookies file on the file system where +// WKWebView flushes its cookies. +base::FilePath GetBinaryCookiesFilePath() { + base::FilePath path = base::mac::GetUserLibraryPath(); + // The relative path of the file (from the user library folder) where + // WKWebView stores its cookies. + const std::string kCookiesFilePath = "Cookies/Cookies.binarycookies"; + return path.Append(kCookiesFilePath); +} + +// Clears all cookies from the .binarycookies file. +// Must be called from a thread where IO operations are allowed. +// Preconditions: There must be no active WKWebViews present in the app. +void ClearAllCookiesFromBinaryCookiesFile() { + // The .binarycookies file is present only on iOS8+. + if (!base::ios::IsRunningOnIOS8OrLater()) { + return; + } + base::FilePath path = GetBinaryCookiesFilePath(); + if (base::PathExists(path)) { + bool success = base::DeleteFile(path, false); + if (!success) { + DLOG(WARNING) << "Failed to remove binarycookies file."; + } + } + // TODO(shreyasv): Should .binarycookies be parsed to find out how many + // more cookies are deleted? Investigate further if the accuracy of this + // actually matters to the callback. +} + +// Builds a NSHTTPCookie from a header cookie line ("Set-Cookie: xxx") and a +// URL. +NSHTTPCookie* GetNSHTTPCookieFromCookieLine(const std::string& cookie_line, + const GURL& url, + base::Time server_time) { + NSURL* nsurl = net::NSURLWithGURL(url); + NSString* ns_cookie_line = base::SysUTF8ToNSString(cookie_line); + if (!ns_cookie_line) { + DLOG(ERROR) << "Cookie line is not UTF8: " << cookie_line; + return nil; + } + NSArray* cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:@{ + @"Set-Cookie" : ns_cookie_line + } forURL:nsurl]; + if ([cookies count] != 1) + return nil; + + NSHTTPCookie* cookie = [cookies objectAtIndex:0]; + if (![cookie expiresDate] || server_time.is_null()) + return cookie; + + // Perform clock skew correction. + base::TimeDelta clock_skew = base::Time::Now() - server_time; + NSDate* corrected_expire_date = + [[cookie expiresDate] dateByAddingTimeInterval:clock_skew.InSecondsF()]; + NSMutableDictionary* properties = + [NSMutableDictionary dictionaryWithDictionary:[cookie properties]]; + [properties setObject:corrected_expire_date forKey:NSHTTPCookieExpires]; + NSHTTPCookie* corrected_cookie = + [NSHTTPCookie cookieWithProperties:properties]; + DCHECK(corrected_cookie); + return corrected_cookie; +} + +// Compares cookies based on the path lengths and the creation times, as per +// RFC6265. +NSInteger CompareCookies(id a, id b, void* context) { + NSHTTPCookie* cookie_a = (NSHTTPCookie*)a; + NSHTTPCookie* cookie_b = (NSHTTPCookie*)b; + // Compare path lengths first. + NSUInteger path_length_a = [[cookie_a path] length]; + NSUInteger path_length_b = [[cookie_b path] length]; + if (path_length_a < path_length_b) + return NSOrderedDescending; + if (path_length_b < path_length_a) + return NSOrderedAscending; + + // Compare creation times. + CookieCreationTimeManager* manager = (CookieCreationTimeManager*)context; + DCHECK(manager); + base::Time created_a = manager->GetCreationTime(cookie_a); + base::Time created_b = manager->GetCreationTime(cookie_b); +#if !defined(CRNET) + // CookieCreationTimeManager is returning creation times that are null. + // Since in CrNet, the cookie store is recreated on startup, let's suppress + // this warning for now. + // TODO(huey): Instead of suppressing the warning, assign a creation time + // to cookies if one doesn't already exist. + DLOG_IF(ERROR, created_a.is_null() || created_b.is_null()) + << "Cookie without creation date"; +#endif + if (created_a < created_b) + return NSOrderedAscending; + return (created_a > created_b) ? NSOrderedDescending : NSOrderedSame; +} + +// Gets the cookies for |url| from the system cookie store. +NSArray* GetCookiesForURL(const GURL& url, CookieCreationTimeManager* manager) { + NSArray* cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] + cookiesForURL:net::NSURLWithGURL(url)]; + + // Sort cookies by decreasing path length, then creation time, as per RFC6265. + return [cookies sortedArrayUsingFunction:CompareCookies context:manager]; +} + +// Builds a cookie line (such as "key1=value1; key2=value2") from an array of +// cookies. +std::string BuildCookieLine(NSArray* cookies, + const net::CookieOptions& options) { + // The exclude_httponly() option would only be used by a javascript engine. + DCHECK(!options.exclude_httponly()); + + // This utility function returns all the cookies, including the httponly ones. + // This is fine because we don't support the exclude_httponly option. + NSDictionary* header = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + return base::SysNSStringToUTF8([header valueForKey:@"Cookie"]); +} + +// Tests whether the |cookie| is a session cookie. +bool IsCookieSessionCookie(NSHTTPCookie* cookie, base::Time time) { + return [cookie isSessionOnly]; +} + +// Tests whether the |creation_time| of |cookie| is in the time range defined +// by |time_begin| and |time_end|. A null |time_end| means end-of-time. +bool IsCookieCreatedBetween(base::Time time_begin, + base::Time time_end, + NSHTTPCookie* cookie, + base::Time creation_time) { + return time_begin <= creation_time && + (time_end.is_null() || creation_time <= time_end); +} + +// Tests whether the |creation_time| of |cookie| is in the time range defined +// by |time_begin| and |time_end| and the cookie host match |host|. A null +// |time_end| means end-of-time. +bool IsCookieCreatedBetweenForHost(base::Time time_begin, + base::Time time_end, + NSString* host, + NSHTTPCookie* cookie, + base::Time creation_time) { + NSString* domain = [cookie domain]; + return [domain characterAtIndex:0] != '.' && + [domain caseInsensitiveCompare:host] == NSOrderedSame && + IsCookieCreatedBetween(time_begin, time_end, cookie, creation_time); +} + +// Adds cookies in |cookies| with name |name| to |filtered|. +void OnlyCookiesWithName(const net::CookieList& cookies, + const std::string& name, + net::CookieList* filtered) { + for (const auto& cookie : cookies) { + if (cookie.Name() == name) + filtered->push_back(cookie); + } +} + +} // namespace + +#pragma mark - +#pragma mark CookieStoreIOS + +CookieStoreIOS::CookieStoreIOS( + net::CookieMonster::PersistentCookieStore* persistent_store) + : creation_time_manager_(new CookieCreationTimeManager), + metrics_enabled_(false), + flush_delay_(base::TimeDelta::FromSeconds(10)), + synchronization_state_(NOT_SYNCHRONIZED), + cookie_cache_(new CookieCache()) { + NotificationTrampoline::GetInstance()->AddObserver(this); + cookie_monster_ = new net::CookieMonster(persistent_store, nullptr); + cookie_monster_->SetPersistSessionCookies(true); + cookie_monster_->SetForceKeepSessionState(); +} + +// static +void CookieStoreIOS::SetCookiePolicy(CookiePolicy setting) { + NSHTTPCookieAcceptPolicy policy = (setting == ALLOW) + ? NSHTTPCookieAcceptPolicyAlways + : NSHTTPCookieAcceptPolicyNever; + NSHTTPCookieStorage* store = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + NSHTTPCookieAcceptPolicy current_policy = [store cookieAcceptPolicy]; + if (current_policy == policy) + return; + [store setCookieAcceptPolicy:policy]; + NotificationTrampoline::GetInstance()->NotifyCookiePolicyChanged(); +} + +CookieStoreIOS* CookieStoreIOS::CreateCookieStoreFromNSHTTPCookieStorage() { + // TODO(huey): Update this when CrNet supports multiple cookie jars. + [[NSHTTPCookieStorage sharedHTTPCookieStorage] + setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways]; + + // Create a cookie store with no persistent store backing. Then, populate + // it from the system's cookie jar. + CookieStoreIOS* cookie_store = new CookieStoreIOS(nullptr); + cookie_store->synchronization_state_ = SYNCHRONIZED; + cookie_store->Flush(base::Closure()); + return cookie_store; +} + +// static +void CookieStoreIOS::SwitchSynchronizedStore(CookieStoreIOS* old_store, + CookieStoreIOS* new_store) { + DCHECK(new_store); + DCHECK_NE(new_store, old_store); + if (old_store) + old_store->SetSynchronizedWithSystemStore(false); + new_store->SetSynchronizedWithSystemStore(true); +} + +// static +void CookieStoreIOS::NotifySystemCookiesChanged() { + NotificationTrampoline::GetInstance()->NotifyCookiesChanged(); +} + +void CookieStoreIOS::Flush(const base::Closure& closure) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (SystemCookiesAllowed()) { + // If cookies are disabled, the system store is empty, and the cookies are + // stashed on disk. Do not delete the cookies on the disk in this case. + WriteToCookieMonster( + [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]); + } + cookie_monster_->FlushStore(closure); + flush_closure_.Cancel(); +} + +void CookieStoreIOS::UnSynchronize() { + SetSynchronizedWithSystemStore(false); +} + +void CookieStoreIOS::SetMetricsEnabled() { + static CookieStoreIOS* g_cookie_store_with_metrics = nullptr; + DCHECK(!g_cookie_store_with_metrics || g_cookie_store_with_metrics == this) + << "Only one cookie store may use metrics."; + g_cookie_store_with_metrics = this; + metrics_enabled_ = true; +} + +#pragma mark - +#pragma mark CookieStore methods + +void CookieStoreIOS::SetCookieWithOptionsAsync( + const GURL& url, + const std::string& cookie_line, + const net::CookieOptions& options, + const SetCookiesCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + switch (synchronization_state_) { + case NOT_SYNCHRONIZED: + cookie_monster_->SetCookieWithOptionsAsync(url, cookie_line, options, + WrapSetCallback(callback)); + break; + case SYNCHRONIZING: + tasks_pending_synchronization_.push_back( + base::Bind(&CookieStoreIOS::SetCookieWithOptionsAsync, this, url, + cookie_line, options, WrapSetCallback(callback))); + break; + case SYNCHRONIZED: + // The exclude_httponly() option would only be used by a javascript + // engine. + DCHECK(!options.exclude_httponly()); + // If cookies are not allowed, they are stashed in the CookieMonster, and + // should be written there instead. + DCHECK(SystemCookiesAllowed()); + + base::Time server_time = + options.has_server_time() ? options.server_time() : base::Time(); + NSHTTPCookie* cookie = + GetNSHTTPCookieFromCookieLine(cookie_line, url, server_time); + DLOG_IF(WARNING, !cookie) + << "Could not create cookie for line: " << cookie_line; + + // On iOS, [cookie domain] is not empty when the cookie domain is not + // specified: it is inferred from the URL instead. + // That is why two specific cases are tested here: + // - url_host == domain_string, happens when the cookie has no domain + // - domain_string.empty(), happens when the domain is not formatted + // correctly + std::string domain_string(base::SysNSStringToUTF8([cookie domain])); + const std::string url_host(url.host()); + std::string dummy; + bool success = (cookie != nil) && !domain_string.empty() && + (url_host == domain_string || + net::cookie_util::GetCookieDomainWithString( + url, domain_string, &dummy)); + + if (success) { + [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie]; + creation_time_manager_->SetCreationTime( + cookie, + creation_time_manager_->MakeUniqueCreationTime(base::Time::Now())); + } + + if (!callback.is_null()) + callback.Run(success); + break; + } +} + +void CookieStoreIOS::GetCookiesWithOptionsAsync( + const GURL& url, + const net::CookieOptions& options, + const GetCookiesCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + switch (synchronization_state_) { + case NOT_SYNCHRONIZED: + cookie_monster_->GetCookiesWithOptionsAsync(url, options, callback); + break; + case SYNCHRONIZING: + tasks_pending_synchronization_.push_back( + base::Bind(&CookieStoreIOS::GetCookiesWithOptionsAsync, this, url, + options, callback)); + break; + case SYNCHRONIZED: + // If cookies are not allowed, they are stashed in the CookieMonster, and + // should be read from there instead. + DCHECK(SystemCookiesAllowed()); + // The exclude_httponly() option would only be used by a javascript + // engine. + DCHECK(!options.exclude_httponly()); + + NSArray* cookies = GetCookiesForURL(url, creation_time_manager_.get()); + if (!callback.is_null()) + callback.Run(BuildCookieLine(cookies, options)); + break; + } +} + +void CookieStoreIOS::GetAllCookiesForURLAsync( + const GURL& url, + const GetCookieListCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + switch (synchronization_state_) { + case NOT_SYNCHRONIZED: + cookie_monster_->GetAllCookiesForURLAsync(url, callback); + break; + case SYNCHRONIZING: + tasks_pending_synchronization_.push_back(base::Bind( + &CookieStoreIOS::GetAllCookiesForURLAsync, this, url, callback)); + break; + case SYNCHRONIZED: + if (!SystemCookiesAllowed()) { + // If cookies are not allowed, the cookies are stashed in the + // CookieMonster, so get them from there. + cookie_monster_->GetAllCookiesForURLAsync(url, callback); + return; + } + + NSArray* cookies = GetCookiesForURL(url, creation_time_manager_.get()); + net::CookieList cookie_list; + cookie_list.reserve([cookies count]); + for (NSHTTPCookie* cookie in cookies) { + base::Time created = creation_time_manager_->GetCreationTime(cookie); + cookie_list.push_back(CanonicalCookieFromSystemCookie(cookie, created)); + } + if (!callback.is_null()) + callback.Run(cookie_list); + break; + } +} + +void CookieStoreIOS::DeleteCookieAsync(const GURL& url, + const std::string& cookie_name, + const base::Closure& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + switch (synchronization_state_) { + case NOT_SYNCHRONIZED: + cookie_monster_->DeleteCookieAsync(url, cookie_name, + WrapClosure(callback)); + break; + case SYNCHRONIZING: + tasks_pending_synchronization_.push_back( + base::Bind(&CookieStoreIOS::DeleteCookieAsync, this, url, cookie_name, + WrapClosure(callback))); + break; + case SYNCHRONIZED: + NSArray* cookies = GetCookiesForURL(url, creation_time_manager_.get()); + for (NSHTTPCookie* cookie in cookies) { + if ([[cookie name] + isEqualToString:base::SysUTF8ToNSString(cookie_name)]) { + [[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie]; + creation_time_manager_->DeleteCreationTime(cookie); + } + } + if (!callback.is_null()) + callback.Run(); + break; + } +} + +// CookieStoreIOS is an implementation of CookieStore which is not a +// CookieMonster. As CookieStore is the main cookie API, a caller of +// GetCookieMonster must handle the case where this returns null. +net::CookieMonster* CookieStoreIOS::GetCookieMonster() { + DCHECK(thread_checker_.CalledOnValidThread()); + return nullptr; +} + +void CookieStoreIOS::DeleteAllCreatedBetweenAsync( + const base::Time& delete_begin, + const base::Time& delete_end, + const DeleteCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (metrics_enabled_) + ResetCookieCountMetrics(); + + switch (synchronization_state_) { + case NOT_SYNCHRONIZED: + cookie_monster_->DeleteAllCreatedBetweenAsync( + delete_begin, delete_end, WrapDeleteCallback(callback)); + break; + case SYNCHRONIZING: + tasks_pending_synchronization_.push_back( + base::Bind(&CookieStoreIOS::DeleteAllCreatedBetweenAsync, this, + delete_begin, delete_end, WrapDeleteCallback(callback))); + break; + case SYNCHRONIZED: + CookieFilterFunction filter = + base::Bind(&IsCookieCreatedBetween, delete_begin, delete_end); + DeleteCookiesWithFilter(filter, callback); + break; + } +} + +void CookieStoreIOS::DeleteAllCreatedBetweenForHostAsync( + const base::Time delete_begin, + const base::Time delete_end, + const GURL& url, + const DeleteCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (metrics_enabled_) + ResetCookieCountMetrics(); + + switch (synchronization_state_) { + case NOT_SYNCHRONIZED: + cookie_monster_->DeleteAllCreatedBetweenForHostAsync( + delete_begin, delete_end, url, WrapDeleteCallback(callback)); + break; + case SYNCHRONIZING: + tasks_pending_synchronization_.push_back(base::Bind( + &CookieStoreIOS::DeleteAllCreatedBetweenForHostAsync, this, + delete_begin, delete_end, url, WrapDeleteCallback(callback))); + break; + case SYNCHRONIZED: + NSString* host = base::SysUTF8ToNSString(url.host()); + CookieFilterFunction filter = base::Bind(IsCookieCreatedBetweenForHost, + delete_begin, delete_end, host); + DeleteCookiesWithFilter(filter, callback); + break; + } +} + +void CookieStoreIOS::DeleteSessionCookiesAsync(const DeleteCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (metrics_enabled_) + ResetCookieCountMetrics(); + + switch (synchronization_state_) { + case NOT_SYNCHRONIZED: + cookie_monster_->DeleteSessionCookiesAsync(WrapDeleteCallback(callback)); + break; + case SYNCHRONIZING: + tasks_pending_synchronization_.push_back( + base::Bind(&CookieStoreIOS::DeleteSessionCookiesAsync, this, + WrapDeleteCallback(callback))); + break; + case SYNCHRONIZED: + CookieFilterFunction filter = base::Bind(&IsCookieSessionCookie); + DeleteCookiesWithFilter(filter, callback); + break; + } +} + +#pragma mark - +#pragma mark Protected methods + +CookieStoreIOS::~CookieStoreIOS() { + NotificationTrampoline::GetInstance()->RemoveObserver(this); + STLDeleteContainerPairSecondPointers(hook_map_.begin(), hook_map_.end()); +} + +#pragma mark - +#pragma mark Private methods + +void CookieStoreIOS::ClearSystemStore() { + DCHECK(thread_checker_.CalledOnValidThread()); + NSHTTPCookieStorage* cookie_storage = + [NSHTTPCookieStorage sharedHTTPCookieStorage]; + base::scoped_nsobject<NSArray> copy( + [[NSArray alloc] initWithArray:[cookie_storage cookies]]); + for (NSHTTPCookie* cookie in copy.get()) + [cookie_storage deleteCookie:cookie]; + DCHECK_EQ(0u, [[cookie_storage cookies] count]); + creation_time_manager_->Clear(); +} + +void CookieStoreIOS::OnSystemCookiePolicyChanged() { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (synchronization_state_ == NOT_SYNCHRONIZED) + return; + + // If the state is SYNCHRONIZING, this function will be a no-op because + // AddCookiesToSystemStore() will return early. + + NSHTTPCookieAcceptPolicy policy = + [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookieAcceptPolicy]; + if (policy == NSHTTPCookieAcceptPolicyAlways) { + // If cookies are disabled, the system cookie store should be empty. + DCHECK(![[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies] count]); + synchronization_state_ = SYNCHRONIZING; + cookie_monster_->GetAllCookiesAsync( + base::Bind(&CookieStoreIOS::AddCookiesToSystemStore, this)); + } else { + DCHECK_EQ(NSHTTPCookieAcceptPolicyNever, policy); + // Flush() does not write the cookies to disk when they are disabled. + // Explicitly copy them. + WriteToCookieMonster( + [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]); + Flush(base::Closure()); + ClearSystemStore(); + } +} + +void CookieStoreIOS::SetSynchronizedWithSystemStore(bool synchronized) { + DCHECK(thread_checker_.CalledOnValidThread()); + + if (synchronized == (synchronization_state_ != NOT_SYNCHRONIZED)) + return; // The cookie store is already in the desired state. + +#if !defined(NDEBUG) + if (!synchronized) { + DCHECK_EQ(this, g_current_synchronized_store) + << "This cookie store was not synchronized"; + g_current_synchronized_store = nullptr; + } else { + DCHECK_EQ((CookieStoreIOS*)nullptr, g_current_synchronized_store) + << "Un-synchronize the current cookie store first."; + g_current_synchronized_store = this; + } +#endif + + NSHTTPCookieAcceptPolicy policy = + [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookieAcceptPolicy]; + DCHECK(policy == NSHTTPCookieAcceptPolicyAlways || + policy == NSHTTPCookieAcceptPolicyNever); + + // If cookies are disabled, the system cookie store should be empty. + DCHECK(policy == NSHTTPCookieAcceptPolicyAlways || + ![[[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies] count]); + + // If cookies are disabled, nothing is done now, the work will be done when + // cookies are re-enabled. + if (policy == NSHTTPCookieAcceptPolicyAlways) { + if (synchronized) { + synchronization_state_ = SYNCHRONIZING; + ClearSystemStore(); + cookie_monster_->GetAllCookiesAsync( + base::Bind(&CookieStoreIOS::AddCookiesToSystemStore, this)); + return; + } else { + // Copy the cookies from the global store to |cookie_monster_|. + Flush(base::Closure()); + } + } + synchronization_state_ = synchronized ? SYNCHRONIZED : NOT_SYNCHRONIZED; +} + +bool CookieStoreIOS::SystemCookiesAllowed() { + DCHECK(thread_checker_.CalledOnValidThread()); + return [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookieAcceptPolicy] == + NSHTTPCookieAcceptPolicyAlways; +} + +void CookieStoreIOS::AddCookiesToSystemStore(const net::CookieList& cookies) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (!SystemCookiesAllowed() || synchronization_state_ != SYNCHRONIZING) { + // The case where synchronization aborts while there are pending tasks is + // not supported. + // Note: it should be ok to drop the tasks here (at least if cookies are not + // allowed). + DCHECK(tasks_pending_synchronization_.empty()); + return; + } + + // Report metrics. + if (metrics_enabled_) { + size_t cookie_count = cookies.size(); + UMA_HISTOGRAM_COUNTS_10000("CookieIOS.CookieReadCount", cookie_count); + CheckForCookieLoss(cookie_count, COOKIES_READ); + } + + net::CookieList::const_iterator it; + for (it = cookies.begin(); it != cookies.end(); ++it) { + const net::CanonicalCookie& net_cookie = *it; + NSHTTPCookie* system_cookie = SystemCookieFromCanonicalCookie(net_cookie); + // Canonical cookie may not be convertable into system cookie if it contains + // invalid characters. + if (!system_cookie) + continue; + [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:system_cookie]; + creation_time_manager_->SetCreationTime(system_cookie, + net_cookie.CreationDate()); + } + + synchronization_state_ = SYNCHRONIZED; + // Run all the pending tasks. + for (const auto& task : tasks_pending_synchronization_) { + task.Run(); + } + tasks_pending_synchronization_.clear(); +} + +void CookieStoreIOS::WriteToCookieMonster(NSArray* system_cookies) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (synchronization_state_ != SYNCHRONIZED) + return; + + // Copy the cookies from the global cookie store to |cookie_monster_|. + // Unlike the system store, CookieMonster requires unique creation times. + net::CookieList cookie_list; + NSUInteger cookie_count = [system_cookies count]; + cookie_list.reserve(cookie_count); + for (NSHTTPCookie* cookie in system_cookies) { + cookie_list.push_back(CanonicalCookieFromSystemCookie( + cookie, creation_time_manager_->GetCreationTime(cookie))); + } + cookie_monster_->SetAllCookiesAsync(cookie_list, SetCookiesCallback()); + + // Update metrics. + if (metrics_enabled_) + UMA_HISTOGRAM_COUNTS_10000("CookieIOS.CookieWrittenCount", cookie_count); +} + +void CookieStoreIOS::DeleteCookiesWithFilter(const CookieFilterFunction& filter, + const DeleteCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK_EQ(SYNCHRONIZED, synchronization_state_); + NSArray* cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; + + // Collect the cookies to delete. + base::scoped_nsobject<NSMutableArray> to_delete( + [[NSMutableArray alloc] init]); + for (NSHTTPCookie* cookie in cookies) { + base::Time creation_time = creation_time_manager_->GetCreationTime(cookie); + if (filter.Run(cookie, creation_time)) + [to_delete addObject:cookie]; + } + + // Delete them. + for (NSHTTPCookie* cookie in to_delete.get()) { + [[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie]; + creation_time_manager_->DeleteCreationTime(cookie); + } + + if (!callback.is_null()) + callback.Run([to_delete count]); +} + +void CookieStoreIOS::OnSystemCookiesChanged() { + DCHECK(thread_checker_.CalledOnValidThread()); + + // If the CookieStoreIOS is not synchronized, system cookies are irrelevant. + if (synchronization_state_ != SYNCHRONIZED) + return; + + for (const auto& hook_map_entry : hook_map_) { + std::pair<GURL, std::string> key = hook_map_entry.first; + std::vector<net::CanonicalCookie> removed_cookies; + std::vector<net::CanonicalCookie> added_cookies; + if (UpdateCacheForCookieFromSystem(key.first, key.second, &removed_cookies, + &added_cookies)) { + RunCallbacksForCookies(key.first, key.second, removed_cookies, true); + RunCallbacksForCookies(key.first, key.second, added_cookies, false); + } + } + + // Do not schedule a flush if one is already scheduled. + if (!flush_closure_.IsCancelled()) + return; + + flush_closure_.Reset(base::Bind(&CookieStoreIOS::Flush, + base::Unretained(this), base::Closure())); + base::MessageLoopProxy::current()->PostDelayedTask( + FROM_HERE, flush_closure_.callback(), flush_delay_); +} + +scoped_ptr<net::CookieStore::CookieChangedSubscription> +CookieStoreIOS::AddCallbackForCookie(const GURL& gurl, + const std::string& name, + const CookieChangedCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + // Prefill cookie cache with all pertinent cookies for |url| if needed. + std::pair<GURL, std::string> key(gurl, name); + if (hook_map_.count(key) == 0) { + UpdateCacheForCookieFromSystem(gurl, name, nullptr, nullptr); + if (hook_map_.count(key) == 0) + hook_map_[key] = new CookieChangedCallbackList; + } + + DCHECK(hook_map_.find(key) != hook_map_.end()); + return hook_map_[key]->Add(callback); +} + +bool CookieStoreIOS::UpdateCacheForCookieFromSystem( + const GURL& gurl, + const std::string& name, + std::vector<net::CanonicalCookie>* out_removed_cookies, + std::vector<net::CanonicalCookie>* out_added_cookies) { + DCHECK(thread_checker_.CalledOnValidThread()); + std::vector<net::CanonicalCookie> system_cookies; + GetSystemCookies(gurl, name, &system_cookies); + return cookie_cache_->Update(gurl, name, system_cookies, out_removed_cookies, + out_added_cookies); +} + +void CookieStoreIOS::RunCallbacksForCookies( + const GURL& url, + const std::string& name, + const std::vector<net::CanonicalCookie>& cookies, + bool removed) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (cookies.empty()) + return; + + std::pair<GURL, std::string> key(url, name); + CookieChangedCallbackList* callbacks = hook_map_[key]; + for (const auto& cookie : cookies) { + DCHECK_EQ(name, cookie.Name()); + callbacks->Notify(cookie, removed); + } +} + +bool CookieStoreIOS::GetSystemCookies( + const GURL& gurl, + const std::string& name, + std::vector<net::CanonicalCookie>* cookies) { + DCHECK(thread_checker_.CalledOnValidThread()); + NSURL* url = net::NSURLWithGURL(gurl); + NSHTTPCookieStorage* storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + NSArray* nscookies = [storage cookiesForURL:url]; + bool found_cookies = false; + for (NSHTTPCookie* nscookie in nscookies) { + if (nscookie.name.UTF8String == name) { + net::CanonicalCookie canonical_cookie = CanonicalCookieFromSystemCookie( + nscookie, creation_time_manager_->GetCreationTime(nscookie)); + cookies->push_back(canonical_cookie); + found_cookies = true; + } + } + return found_cookies; +} + +void CookieStoreIOS::GotCookieListFor(const std::pair<GURL, std::string> key, + const net::CookieList& cookies) { + DCHECK(thread_checker_.CalledOnValidThread()); + + net::CookieList filtered; + OnlyCookiesWithName(cookies, key.second, &filtered); + std::vector<net::CanonicalCookie> removed_cookies; + std::vector<net::CanonicalCookie> added_cookies; + if (cookie_cache_->Update(key.first, key.second, filtered, &removed_cookies, + &added_cookies)) { + RunCallbacksForCookies(key.first, key.second, removed_cookies, true); + RunCallbacksForCookies(key.first, key.second, added_cookies, false); + } +} + +void CookieStoreIOS::DidClearNSHTTPCookieStorageCookies( + const DeleteCallback& delete_callback, + int num_deleted) { + DCHECK(thread_checker_.CalledOnValidThread()); + + CookieStoreIOSClient* client = net::GetCookieStoreIOSClient(); + DCHECK(client); + auto sequenced_task_runner = client->GetTaskRunner(); + DCHECK(sequenced_task_runner); + auto callback = base::Bind(&CookieStoreIOS::DidClearBinaryCookiesFileCookies, + this, delete_callback, num_deleted); + sequenced_task_runner.get()->PostTaskAndReply( + FROM_HERE, base::Bind(&ClearAllCookiesFromBinaryCookiesFile), callback); +} + +void CookieStoreIOS::DidClearBinaryCookiesFileCookies( + const DeleteCallback& callback, + int num_deleted_from_nshttp_cookie_storage) { + DCHECK(thread_checker_.CalledOnValidThread()); + + CookieStoreIOSClient* client = net::GetCookieStoreIOSClient(); + DCHECK(client); + client->DidChangeCookieStorage(); + callback.Run(num_deleted_from_nshttp_cookie_storage); +} + +void CookieStoreIOS::UpdateCachesFromCookieMonster() { + DCHECK(thread_checker_.CalledOnValidThread()); + for (const auto& hook_map_entry : hook_map_) { + std::pair<GURL, std::string> key = hook_map_entry.first; + GetCookieListCallback callback = + base::Bind(&CookieStoreIOS::GotCookieListFor, this, key); + cookie_monster_->GetAllCookiesForURLAsync(key.first, callback); + } +} + +void CookieStoreIOS::UpdateCachesAfterSet(const SetCookiesCallback& callback, + bool success) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (success) + UpdateCachesFromCookieMonster(); + callback.Run(success); +} + +void CookieStoreIOS::UpdateCachesAfterDelete(const DeleteCallback& callback, + int num_deleted) { + DCHECK(thread_checker_.CalledOnValidThread()); + UpdateCachesFromCookieMonster(); + callback.Run(num_deleted); +} + +void CookieStoreIOS::UpdateCachesAfterClosure(const base::Closure& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + UpdateCachesFromCookieMonster(); + callback.Run(); +} + +CookieStoreIOS::SetCookiesCallback CookieStoreIOS::WrapSetCallback( + const SetCookiesCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + return base::Bind(&CookieStoreIOS::UpdateCachesAfterSet, this, callback); +} + +CookieStoreIOS::DeleteCallback CookieStoreIOS::WrapDeleteCallback( + const DeleteCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + return base::Bind(&CookieStoreIOS::UpdateCachesAfterDelete, this, callback); +} + +base::Closure CookieStoreIOS::WrapClosure(const base::Closure& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + return base::Bind(&CookieStoreIOS::UpdateCachesAfterClosure, this, callback); +} + +} // namespace net diff --git a/ios/net/cookies/cookie_store_ios_unittest.mm b/ios/net/cookies/cookie_store_ios_unittest.mm new file mode 100644 index 0000000..311587a --- /dev/null +++ b/ios/net/cookies/cookie_store_ios_unittest.mm @@ -0,0 +1,912 @@ +// 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. + +#include "ios/net/cookies/cookie_store_ios.h" + +#import <Foundation/Foundation.h> + +#include "base/bind_helpers.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/sys_string_conversions.h" +#import "net/base/mac/url_conversions.h" +#include "net/cookies/cookie_store_unittest.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { +// Clears the underlying NSHTTPCookieStorage. +void ClearCookies() { + NSHTTPCookieStorage* store = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + [store setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways]; + NSArray* cookies = [store cookies]; + for (NSHTTPCookie* cookie in cookies) + [store deleteCookie:cookie]; + EXPECT_EQ(0u, [[store cookies] count]); +} +} // namespace + +namespace net { + +struct CookieStoreIOSTestTraits { + static net::CookieStore* Create() { + ClearCookies(); + CookieStoreIOS* store = new CookieStoreIOS(nullptr); + store->synchronization_state_ = CookieStoreIOS::SYNCHRONIZED; + return store; + } + + static const bool is_cookie_monster = false; + static const bool supports_http_only = false; + static const bool supports_non_dotted_domains = false; + static const bool supports_trailing_dots = false; + static const bool filters_schemes = false; + static const bool has_path_prefix_bug = true; + static const int creation_time_granularity_in_ms = 1000; + + base::MessageLoop loop_; +}; + +struct InactiveCookieStoreIOSTestTraits { + static scoped_refptr<net::CookieStore> Create() { + return new CookieStoreIOS(nullptr); + } + + static const bool is_cookie_monster = false; + static const bool supports_http_only = false; + static const bool supports_non_dotted_domains = true; + static const bool supports_trailing_dots = true; + static const bool filters_schemes = false; + static const bool has_path_prefix_bug = false; + static const int creation_time_granularity_in_ms = 0; + + base::MessageLoop loop_; +}; + +// RoundTripTestCookieStore is un-synchronized and re-synchronized after all +// cookie operations. This means all system cookies are converted to Chrome +// cookies and converted back. +// The purpose of this class is to test that cookie conversions do not break the +// cookie store. +class RoundTripTestCookieStore : public net::CookieStore { + public: + RoundTripTestCookieStore() + : store_(new CookieStoreIOS(nullptr)), + dummy_store_(new CookieStoreIOS(nullptr)) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + } + + // Inherited CookieStore methods. + void SetCookieWithOptionsAsync(const GURL& url, + const std::string& cookie_line, + const net::CookieOptions& options, + const SetCookiesCallback& callback) override { + RoundTrip(); + store_->SetCookieWithOptionsAsync(url, cookie_line, options, callback); + } + + void GetCookiesWithOptionsAsync(const GURL& url, + const net::CookieOptions& options, + const GetCookiesCallback& callback) override { + RoundTrip(); + store_->GetCookiesWithOptionsAsync(url, options, callback); + } + + void GetAllCookiesForURLAsync( + const GURL& url, + const GetCookieListCallback& callback) override { + RoundTrip(); + store_->GetAllCookiesForURLAsync(url, callback); + } + + void DeleteCookieAsync(const GURL& url, + const std::string& cookie_name, + const base::Closure& callback) override { + RoundTrip(); + store_->DeleteCookieAsync(url, cookie_name, callback); + } + + net::CookieMonster* GetCookieMonster() override { return nullptr; } + + void DeleteAllCreatedBetweenAsync(const base::Time& delete_begin, + const base::Time& delete_end, + const DeleteCallback& callback) override { + RoundTrip(); + store_->DeleteAllCreatedBetweenAsync(delete_begin, delete_end, callback); + } + + void DeleteAllCreatedBetweenForHostAsync( + const base::Time delete_begin, + const base::Time delete_end, + const GURL& url, + const DeleteCallback& callback) override { + RoundTrip(); + store_->DeleteAllCreatedBetweenForHostAsync(delete_begin, delete_end, url, + callback); + } + + void DeleteSessionCookiesAsync(const DeleteCallback& callback) override { + RoundTrip(); + store_->DeleteSessionCookiesAsync(callback); + } + + scoped_ptr<CookieStore::CookieChangedSubscription> AddCallbackForCookie( + const GURL& url, + const std::string& name, + const CookieChangedCallback& callback) override { + return scoped_ptr<CookieStore::CookieChangedSubscription>(); + } + + protected: + ~RoundTripTestCookieStore() override { store_->UnSynchronize(); } + + private: + void RoundTrip() { + CookieStoreIOS::SwitchSynchronizedStore(store_.get(), dummy_store_.get()); + // Check that the system store is empty, because it is synchronized with + // |dummy_store_| which is empty. + NSHTTPCookieStorage* store = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + EXPECT_EQ(0u, [[store cookies] count]); + CookieStoreIOS::SwitchSynchronizedStore(dummy_store_.get(), store_.get()); + } + + scoped_refptr<CookieStoreIOS> store_; + // |dummy_store_| is not directly used, but is needed to make |store_| + // inactive. + scoped_refptr<CookieStoreIOS> dummy_store_; +}; + +struct RoundTripTestCookieStoreTraits { + static scoped_refptr<net::CookieStore> Create() { + ClearCookies(); + return new RoundTripTestCookieStore(); + } + + static const bool is_cookie_monster = false; + static const bool supports_http_only = false; + static const bool supports_non_dotted_domains = false; + static const bool supports_trailing_dots = false; + static const bool filters_schemes = false; + static const bool has_path_prefix_bug = true; + static const int creation_time_granularity_in_ms = 1000; +}; + +} // namespace net + +namespace net { + +INSTANTIATE_TYPED_TEST_CASE_P(CookieStoreIOS, + CookieStoreTest, + CookieStoreIOSTestTraits); + +INSTANTIATE_TYPED_TEST_CASE_P(InactiveCookieStoreIOS, + CookieStoreTest, + InactiveCookieStoreIOSTestTraits); + +INSTANTIATE_TYPED_TEST_CASE_P(RoundTripTestCookieStore, + CookieStoreTest, + RoundTripTestCookieStoreTraits); + +} // namespace net + +namespace { + +// Test net::CookieMonster::PersistentCookieStore allowing to control when the +// initialization completes. +class TestPersistentCookieStore + : public net::CookieMonster::PersistentCookieStore { + public: + TestPersistentCookieStore() + : kTestCookieURL("http://foo.google.com/bar"), flushed_(false) {} + + // Runs the completion callback with a "a=b" cookie. + void RunLoadedCallback() { + std::vector<net::CanonicalCookie*> cookies; + net::CookieOptions options; + options.set_include_httponly(); + cookies.push_back(net::CanonicalCookie::Create(kTestCookieURL, "a=b", + base::Time::Now(), options)); + // Some canonical cookies cannot be converted into System cookies, for + // example if value is not valid utf8. Such cookies are ignored. + net::CanonicalCookie* bad_canonical_cookie = new net::CanonicalCookie( + kTestCookieURL, "name", "\x81r\xe4\xbd\xa0\xe5\xa5\xbd", "domain", + "path/", + base::Time(), // creation + base::Time(), // expires + base::Time(), // last_access + false, // secure + false, // httponly + false, // first_party_only + net::COOKIE_PRIORITY_DEFAULT); + cookies.push_back(bad_canonical_cookie); + loaded_callback_.Run(cookies); + } + + bool flushed() { return flushed_; } + + private: + // net::CookieMonster::PersistentCookieStore implementation: + void Load(const LoadedCallback& loaded_callback) override { + loaded_callback_ = loaded_callback; + } + + void LoadCookiesForKey(const std::string& key, + const LoadedCallback& loaded_callback) override { + loaded_callback_ = loaded_callback; + } + + void AddCookie(const net::CanonicalCookie& cc) override {} + void UpdateCookieAccessTime(const net::CanonicalCookie& cc) override {} + void DeleteCookie(const net::CanonicalCookie& cc) override {} + void SetForceKeepSessionState() override {} + void Flush(const base::Closure& callback) override { flushed_ = true; } + + private: + ~TestPersistentCookieStore() override {} + + const GURL kTestCookieURL; + LoadedCallback loaded_callback_; + bool flushed_; +}; + +// Helper callback to be passed to CookieStore::GetCookiesWithOptionsAsync(). +class GetCookieCallback { + public: + GetCookieCallback() : did_run_(false) {} + + // Returns true if the callback has been run. + bool did_run() { return did_run_; } + + // Returns the parameter of the callback. + const std::string& cookie_line() { return cookie_line_; } + + void Run(const std::string& cookie_line) { + ASSERT_FALSE(did_run_); + did_run_ = true; + cookie_line_ = cookie_line; + } + + private: + bool did_run_; + std::string cookie_line_; +}; + +// Helper callback to be passed to CookieStore::GetAllCookiesForURLAsync(). +class GetAllCookiesCallback { + public: + GetAllCookiesCallback() : did_run_(false) {} + + // Returns true if the callback has been run. + bool did_run() { return did_run_; } + + // Returns the parameter of the callback. + const net::CookieList& cookie_list() { return cookie_list_; } + + void Run(const net::CookieList& cookie_list) { + ASSERT_FALSE(did_run_); + did_run_ = true; + cookie_list_ = cookie_list; + } + + private: + bool did_run_; + net::CookieList cookie_list_; +}; + +namespace { + +void RecordCookieChanges(std::vector<net::CanonicalCookie>* out_cookies, + std::vector<bool>* out_removes, + const net::CanonicalCookie& cookie, + bool removed) { + DCHECK(out_cookies); + out_cookies->push_back(cookie); + if (out_removes) + out_removes->push_back(removed); +} + +void IgnoreBoolean(bool ignored) { +} + +void IgnoreString(const std::string& ignored) { +} + +} // namespace + +class CookieStoreIOSWithBackend : public testing::Test { + public: + CookieStoreIOSWithBackend() + : kTestCookieURL("http://foo.google.com/bar"), + kTestCookieURL2("http://foo.google.com/baz"), + kTestCookieURL3("http://foo.google.com"), + kTestCookieURL4("http://bar.google.com/bar") { + backend_ = new TestPersistentCookieStore; + store_ = new net::CookieStoreIOS(backend_.get()); + net::CookieStoreIOS::SetCookiePolicy(net::CookieStoreIOS::ALLOW); + cookie_changed_callback_ = store_->AddCallbackForCookie( + kTestCookieURL, "abc", + base::Bind(&RecordCookieChanges, &cookies_changed_, &cookies_removed_)); + } + + ~CookieStoreIOSWithBackend() override {} + + // Gets the cookies. |callback| will be called on completion. + void GetCookies(const net::CookieStore::GetCookiesCallback& callback) { + net::CookieOptions options; + options.set_include_httponly(); + store_->GetCookiesWithOptionsAsync(kTestCookieURL, options, callback); + } + + // Sets a cookie. + void SetCookie(const std::string& cookie_line) { + net::CookieOptions options; + options.set_include_httponly(); + store_->SetCookieWithOptionsAsync(kTestCookieURL, cookie_line, options, + base::Bind(&IgnoreBoolean)); + net::CookieStoreIOS::NotifySystemCookiesChanged(); + // Wait until the flush is posted. + base::MessageLoop::current()->RunUntilIdle(); + } + + void SetSystemCookie(const GURL& url, + const std::string& name, + const std::string& value) { + NSHTTPCookieStorage* storage = + [NSHTTPCookieStorage sharedHTTPCookieStorage]; + [storage setCookie:[NSHTTPCookie cookieWithProperties:@{ + NSHTTPCookiePath : base::SysUTF8ToNSString(url.path()), + NSHTTPCookieName : base::SysUTF8ToNSString(name), + NSHTTPCookieValue : base::SysUTF8ToNSString(value), + NSHTTPCookieDomain : base::SysUTF8ToNSString(url.host()), + }]]; + net::CookieStoreIOS::NotifySystemCookiesChanged(); + base::MessageLoop::current()->RunUntilIdle(); + } + + void DeleteSystemCookie(const GURL& gurl, const std::string& name) { + NSHTTPCookieStorage* storage = + [NSHTTPCookieStorage sharedHTTPCookieStorage]; + NSURL* nsurl = net::NSURLWithGURL(gurl); + NSArray* cookies = [storage cookiesForURL:nsurl]; + for (NSHTTPCookie* cookie in cookies) { + if (cookie.name.UTF8String == name) { + [storage deleteCookie:cookie]; + break; + } + } + net::CookieStoreIOS::NotifySystemCookiesChanged(); + base::MessageLoop::current()->RunUntilIdle(); + } + + protected: + const GURL kTestCookieURL; + const GURL kTestCookieURL2; + const GURL kTestCookieURL3; + const GURL kTestCookieURL4; + + base::MessageLoop loop_; + scoped_refptr<TestPersistentCookieStore> backend_; + scoped_refptr<net::CookieStoreIOS> store_; + scoped_ptr<net::CookieStore::CookieChangedSubscription> + cookie_changed_callback_; + std::vector<net::CanonicalCookie> cookies_changed_; + std::vector<bool> cookies_removed_; +}; + +} // namespace + +namespace net { + +TEST_F(CookieStoreIOSWithBackend, SetCookieCallsHookWhenNotSynchronized) { + ClearCookies(); + SetCookie("abc=def"); + EXPECT_EQ(0U, cookies_changed_.size()); + EXPECT_EQ(0U, cookies_removed_.size()); + backend_->RunLoadedCallback(); + base::MessageLoop::current()->RunUntilIdle(); + EXPECT_EQ(1U, cookies_changed_.size()); + EXPECT_EQ(1U, cookies_removed_.size()); + EXPECT_EQ("abc", cookies_changed_[0].Name()); + EXPECT_EQ("def", cookies_changed_[0].Value()); + EXPECT_FALSE(cookies_removed_[0]); + + // Replacing an existing cookie is actually a two-phase delete + set + // operation, so we get an extra notification. + SetCookie("abc=ghi"); + EXPECT_EQ(3U, cookies_changed_.size()); + EXPECT_EQ(3U, cookies_removed_.size()); + EXPECT_EQ("abc", cookies_changed_[1].Name()); + EXPECT_EQ("def", cookies_changed_[1].Value()); + EXPECT_TRUE(cookies_removed_[1]); + EXPECT_EQ("abc", cookies_changed_[2].Name()); + EXPECT_EQ("ghi", cookies_changed_[2].Value()); + EXPECT_FALSE(cookies_removed_[2]); + + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, SetCookieCallsHookWhenSynchronized) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + GetCookies(base::Bind(&IgnoreString)); + backend_->RunLoadedCallback(); + base::MessageLoop::current()->RunUntilIdle(); + ClearCookies(); + SetCookie("abc=def"); + EXPECT_EQ(1U, cookies_changed_.size()); + EXPECT_EQ(1U, cookies_removed_.size()); + EXPECT_EQ("abc", cookies_changed_[0].Name()); + EXPECT_EQ("def", cookies_changed_[0].Value()); + EXPECT_FALSE(cookies_removed_[0]); + + SetCookie("abc=ghi"); + EXPECT_EQ(3U, cookies_changed_.size()); + EXPECT_EQ(3U, cookies_removed_.size()); + EXPECT_EQ("abc", cookies_changed_[1].Name()); + EXPECT_EQ("def", cookies_changed_[1].Value()); + EXPECT_TRUE(cookies_removed_[1]); + EXPECT_EQ("abc", cookies_changed_[2].Name()); + EXPECT_EQ("ghi", cookies_changed_[2].Value()); + EXPECT_FALSE(cookies_removed_[2]); + DeleteSystemCookie(kTestCookieURL, "abc"); + + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, DeleteCallsHook) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + GetCookies(base::Bind(&IgnoreString)); + backend_->RunLoadedCallback(); + base::MessageLoop::current()->RunUntilIdle(); + ClearCookies(); + SetCookie("abc=def"); + EXPECT_EQ(1U, cookies_changed_.size()); + EXPECT_EQ(1U, cookies_removed_.size()); + store_->DeleteCookieAsync(kTestCookieURL, "abc", + base::Bind(&IgnoreBoolean, false)); + CookieStoreIOS::NotifySystemCookiesChanged(); + base::MessageLoop::current()->RunUntilIdle(); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, SameValueDoesNotCallHook) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + GetCookieCallback callback; + GetCookies(base::Bind(&IgnoreString)); + backend_->RunLoadedCallback(); + base::MessageLoop::current()->RunUntilIdle(); + ClearCookies(); + SetCookie("abc=def"); + EXPECT_EQ(1U, cookies_changed_.size()); + SetCookie("abc=def"); + EXPECT_EQ(1U, cookies_changed_.size()); + store_->UnSynchronize(); +} + +TEST(CookieStoreIOS, GetAllCookiesForURLAsync) { + base::MessageLoop loop; + const GURL kTestCookieURL("http://foo.google.com/bar"); + ClearCookies(); + scoped_refptr<CookieStoreIOS> cookie_store(new CookieStoreIOS(nullptr)); + CookieStoreIOS::SwitchSynchronizedStore(nullptr, cookie_store.get()); + // Add a cookie. + net::CookieOptions options; + options.set_include_httponly(); + cookie_store->SetCookieWithOptionsAsync( + kTestCookieURL, "a=b", options, net::CookieStore::SetCookiesCallback()); + // Disallow cookies. + CookieStoreIOS::SetCookiePolicy(CookieStoreIOS::BLOCK); + // No cookie in the system store. + NSHTTPCookieStorage* system_store = + [NSHTTPCookieStorage sharedHTTPCookieStorage]; + EXPECT_EQ(0u, [[system_store cookies] count]); + // Flushing should not have any effect. + cookie_store->Flush(base::Closure()); + // Check we can get the cookie even though cookies are disabled. + GetAllCookiesCallback callback; + cookie_store->GetAllCookiesForURLAsync( + kTestCookieURL, + base::Bind(&GetAllCookiesCallback::Run, base::Unretained(&callback))); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ(1u, callback.cookie_list().size()); + net::CanonicalCookie cookie = callback.cookie_list()[0]; + EXPECT_EQ("a", cookie.Name()); + EXPECT_EQ("b", cookie.Value()); + // Re-enable cookies. + CookieStoreIOS::SetCookiePolicy(CookieStoreIOS::ALLOW); + // Cookie is back in the system store. + EXPECT_EQ(1u, [[system_store cookies] count]); + cookie_store->UnSynchronize(); +} + +// Tests that cookies can be read before the backend is loaded. +TEST_F(CookieStoreIOSWithBackend, NotSynchronized) { + // Start fetching the cookie. + GetCookieCallback callback; + GetCookies(base::Bind(&GetCookieCallback::Run, base::Unretained(&callback))); + // Backend loading completes. + backend_->RunLoadedCallback(); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ("a=b", callback.cookie_line()); +} + +// Tests that cookies can be read before synchronization is complete. +TEST_F(CookieStoreIOSWithBackend, Synchronizing) { + // Start synchronization. + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + GetCookieCallback callback; + GetCookies(base::Bind(&GetCookieCallback::Run, base::Unretained(&callback))); + // Backend loading completes (end of synchronization). + backend_->RunLoadedCallback(); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ("a=b", callback.cookie_line()); + store_->UnSynchronize(); +} + +// Tests that cookies can be read before synchronization is complete, when +// triggered by a change in cookie policy. +TEST_F(CookieStoreIOSWithBackend, SynchronizingAfterPolicyChange) { + ClearCookies(); + CookieStoreIOS::SetCookiePolicy(CookieStoreIOS::BLOCK); + // SwitchSynchronizedStore() does nothing when cookies are blocked. + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + // Start synchronization by allowing cookies. + CookieStoreIOS::SetCookiePolicy(CookieStoreIOS::ALLOW); + GetCookieCallback callback; + GetCookies(base::Bind(&GetCookieCallback::Run, base::Unretained(&callback))); + // Backend loading completes (end of synchronization). + backend_->RunLoadedCallback(); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ("a=b", callback.cookie_line()); + store_->UnSynchronize(); +} + +// Tests that Synchronization can be "aborted" (i.e. the cookie store is +// unsynchronized while synchronization is in progress). +TEST_F(CookieStoreIOSWithBackend, SyncThenUnsync) { + ClearCookies(); + scoped_refptr<CookieStoreIOS> dummy_store = new CookieStoreIOS(nullptr); + // Switch back and forth before synchronization can complete. + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + CookieStoreIOS::SwitchSynchronizedStore(store_.get(), dummy_store.get()); + backend_->RunLoadedCallback(); + // No cookie leak in the system store. + NSHTTPCookieStorage* store = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + EXPECT_EQ(0u, [[store cookies] count]); + // No cookie lost. + GetCookieCallback callback; + GetCookies(base::Bind(&GetCookieCallback::Run, base::Unretained(&callback))); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ("a=b", callback.cookie_line()); + dummy_store->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, ChangePolicyOnceDuringSynchronization) { + // Start synchronization. + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + // Toggle cookie policy to trigger another synchronization while the first one + // is still in progress. + CookieStoreIOS::SetCookiePolicy(CookieStoreIOS::BLOCK); + // Backend loading completes (end of synchronization). + backend_->RunLoadedCallback(); + CookieStoreIOS::SetCookiePolicy(CookieStoreIOS::ALLOW); + GetCookieCallback callback; + GetCookies(base::Bind(&GetCookieCallback::Run, base::Unretained(&callback))); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ("a=b", callback.cookie_line()); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, ChangePolicyTwiceDuringSynchronization) { + // Start synchronization. + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + // Toggle cookie policy to trigger another synchronization while the first one + // is still in progress. + CookieStoreIOS::SetCookiePolicy(CookieStoreIOS::BLOCK); + CookieStoreIOS::SetCookiePolicy(CookieStoreIOS::ALLOW); + // Backend loading completes (end of synchronization). + backend_->RunLoadedCallback(); + GetCookieCallback callback; + GetCookies(base::Bind(&GetCookieCallback::Run, base::Unretained(&callback))); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ("a=b", callback.cookie_line()); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, UnSynchronizeBeforeLoadComplete) { + ClearCookies(); + // Switch back and forth before synchronization can complete. + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + store_->UnSynchronize(); + backend_->RunLoadedCallback(); + // No cookie lost. + GetCookieCallback callback; + GetCookies(base::Bind(&GetCookieCallback::Run, base::Unretained(&callback))); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ("a=b", callback.cookie_line()); +} + +TEST_F(CookieStoreIOSWithBackend, UnSynchronize) { + ClearCookies(); + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + backend_->RunLoadedCallback(); + store_->UnSynchronize(); + // No cookie lost. + GetCookieCallback callback; + GetCookies(base::Bind(&GetCookieCallback::Run, base::Unretained(&callback))); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ("a=b", callback.cookie_line()); +} + +TEST_F(CookieStoreIOSWithBackend, FlushOnUnSynchronize) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + EXPECT_FALSE(backend_->flushed()); + store_->UnSynchronize(); + EXPECT_TRUE(backend_->flushed()); +} + +TEST_F(CookieStoreIOSWithBackend, FlushOnSwitch) { + scoped_refptr<CookieStoreIOS> dummy_store = new CookieStoreIOS(nullptr); + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + EXPECT_FALSE(backend_->flushed()); + CookieStoreIOS::SwitchSynchronizedStore(store_.get(), dummy_store.get()); + EXPECT_TRUE(backend_->flushed()); + dummy_store->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, FlushOnCookieChanged) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + store_->set_flush_delay_for_testing(base::TimeDelta()); + backend_->RunLoadedCallback(); + EXPECT_FALSE(backend_->flushed()); + + // Set a cookie an check that it triggers a flush. + SetCookie("x=y"); + EXPECT_TRUE(backend_->flushed()); + + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, ManualFlush) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + backend_->RunLoadedCallback(); + EXPECT_FALSE(backend_->flushed()); + + // The store should be flushed even if it is not dirty. + store_->Flush(base::Closure()); + EXPECT_TRUE(backend_->flushed()); + + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, FlushOnPolicyChange) { + // Start synchronization. + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + // Toggle cookie policy to trigger a flush. + EXPECT_FALSE(backend_->flushed()); + CookieStoreIOS::SetCookiePolicy(CookieStoreIOS::BLOCK); + EXPECT_TRUE(backend_->flushed()); + store_->UnSynchronize(); + CookieStoreIOS::SetCookiePolicy(CookieStoreIOS::ALLOW); +} + +TEST_F(CookieStoreIOSWithBackend, NoInitialNotifyWithNoCookie) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + std::vector<net::CanonicalCookie> cookies; + store_->AddCallbackForCookie( + kTestCookieURL, "abc", + base::Bind(&RecordCookieChanges, &cookies, nullptr)); + EXPECT_EQ(0U, cookies.size()); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, NoInitialNotifyWithSystemCookie) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + SetSystemCookie(kTestCookieURL, "abc", "def"); + std::vector<net::CanonicalCookie> cookies; + store_->AddCallbackForCookie( + kTestCookieURL, "abc", + base::Bind(&RecordCookieChanges, &cookies, nullptr)); + EXPECT_EQ(0U, cookies.size()); + DeleteSystemCookie(kTestCookieURL, "abc"); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, NotifyOnAdd) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + backend_->RunLoadedCallback(); + std::vector<net::CanonicalCookie> cookies; + std::vector<bool> removes; + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle = + store_->AddCallbackForCookie( + kTestCookieURL, "abc", + base::Bind(&RecordCookieChanges, &cookies, &removes)); + EXPECT_EQ(0U, cookies.size()); + EXPECT_EQ(0U, removes.size()); + SetSystemCookie(kTestCookieURL, "abc", "def"); + EXPECT_EQ(1U, cookies.size()); + EXPECT_EQ(1U, removes.size()); + EXPECT_EQ("abc", cookies[0].Name()); + EXPECT_EQ("def", cookies[0].Value()); + EXPECT_FALSE(removes[0]); + + SetSystemCookie(kTestCookieURL, "ghi", "jkl"); + EXPECT_EQ(1U, cookies.size()); + EXPECT_EQ(1U, removes.size()); + + DeleteSystemCookie(kTestCookieURL, "abc"); + DeleteSystemCookie(kTestCookieURL, "ghi"); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, NotifyOnChange) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + backend_->RunLoadedCallback(); + std::vector<net::CanonicalCookie> cookies; + std::vector<bool> removes; + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle = + store_->AddCallbackForCookie( + kTestCookieURL, "abc", + base::Bind(&RecordCookieChanges, &cookies, &removes)); + EXPECT_EQ(0U, cookies.size()); + SetSystemCookie(kTestCookieURL, "abc", "def"); + EXPECT_EQ(1U, cookies.size()); + SetSystemCookie(kTestCookieURL, "abc", "ghi"); + EXPECT_EQ(3U, cookies.size()); + EXPECT_EQ(3U, removes.size()); + EXPECT_EQ("abc", cookies[1].Name()); + EXPECT_EQ("def", cookies[1].Value()); + EXPECT_TRUE(removes[1]); + EXPECT_EQ("abc", cookies[2].Name()); + EXPECT_EQ("ghi", cookies[2].Value()); + EXPECT_FALSE(removes[2]); + + DeleteSystemCookie(kTestCookieURL, "abc"); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, NotifyOnDelete) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + backend_->RunLoadedCallback(); + std::vector<net::CanonicalCookie> cookies; + std::vector<bool> removes; + SetSystemCookie(kTestCookieURL, "abc", "def"); + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle = + store_->AddCallbackForCookie( + kTestCookieURL, "abc", + base::Bind(&RecordCookieChanges, &cookies, &removes)); + EXPECT_EQ(0U, cookies.size()); + DeleteSystemCookie(kTestCookieURL, "abc"); + EXPECT_EQ(1U, cookies.size()); + EXPECT_EQ(1U, removes.size()); + EXPECT_TRUE(removes[0]); + SetSystemCookie(kTestCookieURL, "abc", "def"); + EXPECT_EQ(2U, cookies.size()); + EXPECT_EQ(2U, removes.size()); + EXPECT_FALSE(removes[1]); + DeleteSystemCookie(kTestCookieURL, "abc"); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, NoNotifyOnNoChange) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + backend_->RunLoadedCallback(); + std::vector<net::CanonicalCookie> cookies; + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle = + store_->AddCallbackForCookie( + kTestCookieURL, "abc", + base::Bind(&RecordCookieChanges, &cookies, nullptr)); + EXPECT_EQ(0U, cookies.size()); + SetSystemCookie(kTestCookieURL, "abc", "def"); + EXPECT_EQ(1U, cookies.size()); + SetSystemCookie(kTestCookieURL, "abc", "def"); + EXPECT_EQ(1U, cookies.size()); + DeleteSystemCookie(kTestCookieURL, "abc"); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, MultipleNotifies) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + backend_->RunLoadedCallback(); + std::vector<net::CanonicalCookie> cookies; + std::vector<net::CanonicalCookie> cookies2; + std::vector<net::CanonicalCookie> cookies3; + std::vector<net::CanonicalCookie> cookies4; + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle = + store_->AddCallbackForCookie( + kTestCookieURL, "abc", + base::Bind(&RecordCookieChanges, &cookies, nullptr)); + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle2 = + store_->AddCallbackForCookie( + kTestCookieURL2, "abc", + base::Bind(&RecordCookieChanges, &cookies2, nullptr)); + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle3 = + store_->AddCallbackForCookie( + kTestCookieURL3, "abc", + base::Bind(&RecordCookieChanges, &cookies3, nullptr)); + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle4 = + store_->AddCallbackForCookie( + kTestCookieURL4, "abc", + base::Bind(&RecordCookieChanges, &cookies4, nullptr)); + SetSystemCookie(kTestCookieURL, "abc", "def"); + SetSystemCookie(kTestCookieURL2, "abc", "def"); + SetSystemCookie(kTestCookieURL3, "abc", "def"); + SetSystemCookie(kTestCookieURL4, "abc", "def"); + EXPECT_EQ(2U, cookies.size()); + EXPECT_EQ(2U, cookies2.size()); + EXPECT_EQ(1U, cookies3.size()); + EXPECT_EQ(1U, cookies4.size()); + DeleteSystemCookie(kTestCookieURL, "abc"); + DeleteSystemCookie(kTestCookieURL2, "abc"); + DeleteSystemCookie(kTestCookieURL3, "abc"); + DeleteSystemCookie(kTestCookieURL4, "abc"); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, LessSpecificNestedCookie) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + backend_->RunLoadedCallback(); + std::vector<net::CanonicalCookie> cookies; + SetSystemCookie(kTestCookieURL2, "abc", "def"); + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle = + store_->AddCallbackForCookie( + kTestCookieURL2, "abc", + base::Bind(&RecordCookieChanges, &cookies, nullptr)); + EXPECT_EQ(0U, cookies.size()); + SetSystemCookie(kTestCookieURL3, "abc", "ghi"); + EXPECT_EQ(1U, cookies.size()); + DeleteSystemCookie(kTestCookieURL, "abc"); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, MoreSpecificNestedCookie) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + backend_->RunLoadedCallback(); + std::vector<net::CanonicalCookie> cookies; + SetSystemCookie(kTestCookieURL3, "abc", "def"); + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle = + store_->AddCallbackForCookie( + kTestCookieURL2, "abc", + base::Bind(&RecordCookieChanges, &cookies, nullptr)); + EXPECT_EQ(0U, cookies.size()); + SetSystemCookie(kTestCookieURL2, "abc", "ghi"); + EXPECT_EQ(1U, cookies.size()); + DeleteSystemCookie(kTestCookieURL, "abc"); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, MoreSpecificNestedCookieWithSameValue) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + backend_->RunLoadedCallback(); + std::vector<net::CanonicalCookie> cookies; + SetSystemCookie(kTestCookieURL3, "abc", "def"); + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle = + store_->AddCallbackForCookie( + kTestCookieURL2, "abc", + base::Bind(&RecordCookieChanges, &cookies, nullptr)); + EXPECT_EQ(0U, cookies.size()); + SetSystemCookie(kTestCookieURL2, "abc", "def"); + EXPECT_EQ(1U, cookies.size()); + DeleteSystemCookie(kTestCookieURL, "abc"); + store_->UnSynchronize(); +} + +TEST_F(CookieStoreIOSWithBackend, RemoveCallback) { + CookieStoreIOS::SwitchSynchronizedStore(nullptr, store_.get()); + backend_->RunLoadedCallback(); + std::vector<net::CanonicalCookie> cookies; + SetSystemCookie(kTestCookieURL, "abc", "def"); + scoped_ptr<net::CookieStore::CookieChangedSubscription> handle = + store_->AddCallbackForCookie( + kTestCookieURL, "abc", + base::Bind(&RecordCookieChanges, &cookies, nullptr)); + EXPECT_EQ(0U, cookies.size()); + SetSystemCookie(kTestCookieURL, "abc", "ghi"); + EXPECT_EQ(2U, cookies.size()); + // this deletes the callback + handle.reset(); + SetSystemCookie(kTestCookieURL, "abc", "jkl"); + EXPECT_EQ(2U, cookies.size()); + DeleteSystemCookie(kTestCookieURL, "abc"); + store_->UnSynchronize(); +} + +} // namespace net diff --git a/ios/net/ios_net.gyp b/ios/net/ios_net.gyp index ed1a582..6208ce9 100644 --- a/ios/net/ios_net.gyp +++ b/ios/net/ios_net.gyp @@ -29,6 +29,8 @@ 'cookies/cookie_cache.h', 'cookies/cookie_creation_time_manager.h', 'cookies/cookie_creation_time_manager.mm', + 'cookies/cookie_store_ios.h', + 'cookies/cookie_store_ios.mm', 'cookies/cookie_store_ios_client.h', 'cookies/cookie_store_ios_client.mm', 'cookies/system_cookie_util.h', diff --git a/ios/net/ios_net_unittests.gyp b/ios/net/ios_net_unittests.gyp index 42a19ec..8818b33 100644 --- a/ios/net/ios_net_unittests.gyp +++ b/ios/net/ios_net_unittests.gyp @@ -24,6 +24,7 @@ 'clients/crn_forwarding_network_client_factory_unittest.mm', 'cookies/cookie_cache_unittest.cc', 'cookies/cookie_creation_time_manager_unittest.mm', + 'cookies/cookie_store_ios_unittest.mm', 'cookies/system_cookie_util_unittest.mm', 'nsurlrequest_util_unittest.mm', 'protocol_handler_util_unittest.mm', |