// 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. #import "ios/web/public/crw_browsing_data_store.h" #import #include "base/ios/ios_util.h" #import "base/ios/weak_nsobject.h" #include "base/logging.h" #import "base/mac/scoped_nsobject.h" #import "ios/web/browsing_data_managers/crw_cookie_browsing_data_manager.h" #include "ios/web/public/active_state_manager.h" #include "ios/web/public/browser_state.h" namespace { // Represents the type of operations that a CRWBrowsingDataStore can perform. enum OperationType { // Represents NOP. NONE = 1, // Stash operation involves stashing browsing data that web views create to // the associated BrowserState's state path. STASH, // Restore operation involves restoring browsing data from the // associated BrowserState's state path so that web views can read from them. RESTORE, // Remove operation involves removing the data that web views create. REMOVE, }; // The name of the NSOperation that performs a |RESTORE| operation. NSString* const kRestoreOperationName = @"CRWBrowsingDataStore.RESTORE"; // The name of the NSOperation that performs a |STASH| operation. NSString* const kStashOperationName = @"CRWBrowsingDataStore.STASH"; } // namespace // CRWBrowsingDataStore is implemented using 2 queues. // 1) operationQueueForStashAndRestoreOperations: // - This queue is used to perform |STASH| and |RESTORE| operations. // - |STASH|, |RESTORE| operations are operations that have been explicitly // requested by the user. And the performance of these operations block // further user interaction. Hence this queue has a QoS of // NSQualityOfServiceUserInitiated. // - |STASH|, |RESTORE| operations from 2 different CRWBrowsingDataStores are // not re-entrant. Hence this is a serial queue. // 2) operationQueueForRemoveOperations: // - This queue is used to perform |REMOVE| operations. // - |REMOVE| operations is an operation that has been explicitly requested by // the user. And the performance of this operation blocks further user // interaction. Hence this queue has a QoS of NSQualityOfServiceUserInitiated. // - |REMOVE| operations from 2 different CRWBrowingDataStores can be run in // parallel. Hence this is a concurrent queue. // // |STASH|, |RESTORE|, |REMOVE| operations of a particular CRWBrowsingDataStore // are not re-entrant. Hence these operations are serialized. @interface CRWBrowsingDataStore () // Redefined to be read-write. Must be called from the main thread. @property(nonatomic, assign) web::BrowsingDataStoreMode mode; // The array of all browsing data managers. Must be called from the main // thread. @property(nonatomic, readonly) NSArray* allBrowsingDataManagers; // The number of |STASH| or |RESTORE| operations that are still pending. Must be // called from the main thread. @property(nonatomic, assign) NSUInteger numberOfPendingStashOrRestoreOperations; // Returns a serial queue on which |STASH| and |RESTORE| operations can be // scheduled to be run. All |STASH|/|RESTORE| operations need to be run on the // same queue hence it is shared with all CRWBrowsingDataStores. + (NSOperationQueue*)operationQueueForStashAndRestoreOperations; // Returns a concurrent queue on which remove operations can be scheduled to be // run. All |REMOVE| operations need to be run on the same queue hence it is // shared with all CRWBrowsingDataStores. + (NSOperationQueue*)operationQueueForRemoveOperations; // Returns an array of CRWBrowsingDataManagers for the given // |browsingDataTypes|. - (NSArray*)browsingDataManagersForBrowsingDataTypes: (web::BrowsingDataTypes)browsingDataTypes; // Returns the selector that needs to be performed on the // CRWBrowsingDataManagers for the |operationType|. |operationType| cannot be // |NONE|. - (SEL)browsingDataManagerSelectorForOperationType:(OperationType)operationType; // Returns the selector that needs to be performed on the // CRWBrowsingDataManagers for a |REMOVE| operation. - (SEL)browsingDataManagerSelectorForRemoveOperationType; // Sets the mode iff there are no more |STASH| or |RESTORE| operations that are // still pending. |mode| can only be either |ACTIVE| or |INACTIVE|. // Returns YES if the mode was successfully changed to |mode|. - (BOOL)finalizeChangeToMode:(web::BrowsingDataStoreMode)mode; // Changes the mode of the CRWBrowsingDataStore to |mode|. This is an // asynchronous operation and the mode is not changed immediately. // |completionHandler| can be nil. // |completionHandler| is called on the main thread. This block has no return // value and takes a single BOOL argument that indicates whether or not the // mode change was successfully changed to |mode|. - (void)changeMode:(web::BrowsingDataStoreMode)mode completionHandler:(void (^)(BOOL modeChangeWasSuccessful))completionHandler; // Returns the OperationType (that needs to be performed) in order to change the // mode to |mode|. Consults the delegate if one is present. |mode| cannot be // |CHANGING|. - (OperationType)operationTypeToChangeMode:(web::BrowsingDataStoreMode)mode; // Performs an operation of type |operationType| on each of the // |browsingDataManagers|. |operationType| cannot be |NONE|. // Precondition: There must be no web views associated with the BrowserState. // |completionHandler| is called on the main thread and cannot be nil. - (void)performOperationWithType:(OperationType)operationType browsingDataManagers:(NSArray*)browsingDataManagers completionHandler:(ProceduralBlock)completionHandler; // Returns an NSOperation that calls |selector| on all the // |browsingDataManagers|. |selector| needs to be one of the methods in // CRWBrowsingDataManager. - (NSOperation*)operationWithBrowsingDataManagers:(NSArray*)browsingDataManagers selector:(SEL)selector; // Enqueues |operation| to be run on |queue|. All operations are serialized to // be run one after another. - (void)addOperation:(NSOperation*)operation toQueue:(NSOperationQueue*)queue; @end @implementation CRWBrowsingDataStore { web::BrowserState* _browserState; // Weak, owns this object. // The delegate. base::WeakNSProtocol> _delegate; // The mode of the CRWBrowsingDataStore. web::BrowsingDataStoreMode _mode; // The dictionary that maps a browsing data type to its // CRWBrowsingDataManager. base::scoped_nsobject _browsingDataTypeMap; // The last operation that was enqueued to be run. Can be a |STASH|, |RESTORE| // or a |REMOVE| operation. base::scoped_nsobject _lastDispatchedOperation; // The last dispatched |STASH| or |RESTORE| operation that was enqueued to be // run. base::scoped_nsobject _lastDispatchedStashOrRestoreOperation; // The number of |STASH| or |RESTORE| operations that are still pending. If // the number of |STASH| or |RESTORE| operations is greater than 0U, the mode // of the CRWBrowsingDataStore is |CHANGING|. The mode can be made ACTIVE or // INACTIVE only be set when this value is 0U. NSUInteger _numberOfPendingStashOrRestoreOperations; } #pragma mark - #pragma mark NSObject Methods - (instancetype)initWithBrowserState:(web::BrowserState*)browserState { self = [super init]; if (self) { DCHECK([NSThread isMainThread]); DCHECK(browserState); _browserState = browserState; web::ActiveStateManager* activeStateManager = web::BrowserState::GetActiveStateManager(browserState); DCHECK(activeStateManager); _mode = activeStateManager->IsActive() ? web::ACTIVE : web::INACTIVE; base::scoped_nsobject cookieBrowsingDataManager([[CRWCookieBrowsingDataManager alloc] initWithBrowserState:browserState]); _browsingDataTypeMap.reset([@{ @(web::BROWSING_DATA_TYPE_COOKIES) : cookieBrowsingDataManager, } retain]); } return self; } - (instancetype)init { NOTREACHED(); return nil; } - (NSString*)description { NSString* format = @"<%@: %p; hasPendingOperations = { %@ }; mode = { %@ }>"; NSString* hasPendingOperationsString = [self hasPendingOperations] ? @"YES" : @"NO"; NSString* modeString = nil; switch (self.mode) { case web::ACTIVE: modeString = @"ACTIVE"; break; case web::CHANGING: modeString = @"CHANGING"; break; case web::INACTIVE: modeString = @"INACTIVE"; break; } NSString* result = [NSString stringWithFormat:format, NSStringFromClass(self.class), self, hasPendingOperationsString, modeString]; return result; } + (BOOL)automaticallyNotifiesObserversForKey:(NSString*)key { // It is necessary to override this for |mode| because the default KVO // behavior in NSObject is to fire a notification irrespective of if an actual // change was made to the ivar or not. The |mode| property needs fine grained // control over the actual notifications being fired since observers need to // be notified iff the |mode| actually changed. if ([key isEqual:@"mode"]) { return NO; } return [super automaticallyNotifiesObserversForKey:key]; } #pragma mark - #pragma mark Public Properties - (id)delegate { return _delegate; } - (void)setDelegate:(id)delegate { _delegate.reset(delegate); } - (web::BrowsingDataStoreMode)mode { DCHECK([NSThread isMainThread]); return _mode; } - (BOOL)hasPendingOperations { if (!_lastDispatchedOperation) { return NO; } return ![_lastDispatchedOperation isFinished]; } #pragma mark - #pragma mark Public Methods - (void)makeActiveWithCompletionHandler: (void (^)(BOOL success))completionHandler { DCHECK([NSThread isMainThread]); [self changeMode:web::ACTIVE completionHandler:completionHandler]; } - (void)makeInactiveWithCompletionHandler: (void (^)(BOOL success))completionHandler { DCHECK([NSThread isMainThread]); [self changeMode:web::INACTIVE completionHandler:completionHandler]; } - (void)removeDataOfTypes:(web::BrowsingDataTypes)browsingDataTypes completionHandler:(ProceduralBlock)completionHandler { DCHECK([NSThread isMainThread]); NSArray* browsingDataManagers = [self browsingDataManagersForBrowsingDataTypes:browsingDataTypes]; [self performOperationWithType:REMOVE browsingDataManagers:browsingDataManagers completionHandler:completionHandler]; } #pragma mark - #pragma mark Private Properties - (void)setMode:(web::BrowsingDataStoreMode)mode { DCHECK([NSThread isMainThread]); if (_mode == mode) { return; } if (mode == web::ACTIVE || mode == web::INACTIVE) { DCHECK(!self.numberOfPendingStashOrRestoreOperations); } [self willChangeValueForKey:@"mode"]; _mode = mode; [self didChangeValueForKey:@"mode"]; } - (NSArray*)allBrowsingDataManagers { DCHECK([NSThread isMainThread]); return [_browsingDataTypeMap allValues]; } - (NSUInteger)numberOfPendingStashOrRestoreOperations { DCHECK([NSThread isMainThread]); return _numberOfPendingStashOrRestoreOperations; } - (void)setNumberOfPendingStashOrRestoreOperations: (NSUInteger)numberOfPendingStashOrRestoreOperations { DCHECK([NSThread isMainThread]); _numberOfPendingStashOrRestoreOperations = numberOfPendingStashOrRestoreOperations; } #pragma mark - #pragma mark Private Class Methods + (NSOperationQueue*)operationQueueForStashAndRestoreOperations { static dispatch_once_t onceToken = 0; static NSOperationQueue* operationQueueForStashAndRestoreOperations = nil; dispatch_once(&onceToken, ^{ operationQueueForStashAndRestoreOperations = [[NSOperationQueue alloc] init]; [operationQueueForStashAndRestoreOperations setMaxConcurrentOperationCount:1U]; if (base::ios::IsRunningOnIOS8OrLater()) { [operationQueueForStashAndRestoreOperations setQualityOfService:NSQualityOfServiceUserInitiated]; } }); return operationQueueForStashAndRestoreOperations; } + (NSOperationQueue*)operationQueueForRemoveOperations { static dispatch_once_t onceToken = 0; static NSOperationQueue* operationQueueForRemoveOperations = nil; dispatch_once(&onceToken, ^{ operationQueueForRemoveOperations = [[NSOperationQueue alloc] init]; [operationQueueForRemoveOperations setMaxConcurrentOperationCount:NSUIntegerMax]; if (base::ios::IsRunningOnIOS8OrLater()) { [operationQueueForRemoveOperations setQualityOfService:NSQualityOfServiceUserInitiated]; } }); return operationQueueForRemoveOperations; } #pragma mark - #pragma mark Private Methods - (NSArray*)browsingDataManagersForBrowsingDataTypes: (web::BrowsingDataTypes)browsingDataTypes { __block NSMutableArray* result = [NSMutableArray array]; [_browsingDataTypeMap enumerateKeysAndObjectsUsingBlock:^(NSNumber* dataType, id manager, BOOL*) { if ([dataType unsignedIntegerValue] & browsingDataTypes) { [result addObject:manager]; } }]; return result; } - (SEL)browsingDataManagerSelectorForOperationType: (OperationType)operationType { switch (operationType) { case NONE: NOTREACHED(); return nullptr; case STASH: return @selector(stashData); case RESTORE: return @selector(restoreData); case REMOVE: return [self browsingDataManagerSelectorForRemoveOperationType]; }; } - (SEL)browsingDataManagerSelectorForRemoveOperationType { if (self.mode == web::ACTIVE) { return @selector(removeDataAtCanonicalPath); } if (self.mode == web::INACTIVE) { return @selector(removeDataAtStashPath); } // Since the mode is |CHANGING|, find the last |STASH| or |RESTORE| operation // that was enqueued in order to find out the eventual mode that the // CRWBrowsingDataStore will be in when this |REMOVE| operation is run. DCHECK(_lastDispatchedStashOrRestoreOperation); NSString* lastDispatchedStashOrRestoreOperationName = [_lastDispatchedStashOrRestoreOperation name]; if ([lastDispatchedStashOrRestoreOperationName isEqual:kRestoreOperationName]) { return @selector(removeDataAtCanonicalPath); } if ([lastDispatchedStashOrRestoreOperationName isEqual:kStashOperationName]) { return @selector(removeDataAtStashPath); } NOTREACHED(); return nullptr; } - (BOOL)finalizeChangeToMode:(web::BrowsingDataStoreMode)mode { DCHECK([NSThread isMainThread]); DCHECK_NE(web::CHANGING, mode); BOOL modeChangeWasSuccessful = NO; if (!self.numberOfPendingStashOrRestoreOperations) { [self setMode:mode]; modeChangeWasSuccessful = YES; } return modeChangeWasSuccessful; } - (void)changeMode:(web::BrowsingDataStoreMode)mode completionHandler: (void (^)(BOOL modeChangeWasSuccessful))completionHandler { DCHECK([NSThread isMainThread]); ProceduralBlock completionHandlerAfterPerformingOperation = ^{ BOOL modeChangeWasSuccessful = [self finalizeChangeToMode:mode]; if (completionHandler) { completionHandler(modeChangeWasSuccessful); } }; OperationType operationType = [self operationTypeToChangeMode:mode]; if (operationType == NONE) { // As a caller of this API, it is awkward to get the callback before the // method call has completed, hence defer it. dispatch_async(dispatch_get_main_queue(), completionHandlerAfterPerformingOperation); } else { [self performOperationWithType:operationType browsingDataManagers:[self allBrowsingDataManagers] completionHandler:completionHandlerAfterPerformingOperation]; } } - (OperationType)operationTypeToChangeMode:(web::BrowsingDataStoreMode)mode { DCHECK_NE(web::CHANGING, mode); OperationType operationType = NONE; if (mode == self.mode) { // Already in the desired mode. operationType = NONE; } else if (mode == web::ACTIVE) { // By default a |RESTORE| operation is performed when the mode is changed // to |ACTIVE|. operationType = RESTORE; web::BrowsingDataStoreMakeActivePolicy makeActivePolicy = [_delegate decideMakeActiveOperationPolicyForBrowsingDataStore:self]; operationType = (makeActivePolicy == web::ADOPT) ? REMOVE : RESTORE; } else { // By default a |STASH| operation is performed when the mode is changed to // |INACTIVE|. operationType = STASH; web::BrowsingDataStoreMakeInactivePolicy makeInactivePolicy = [_delegate decideMakeInactiveOperationPolicyForBrowsingDataStore:self]; operationType = (makeInactivePolicy == web::DELETE) ? REMOVE : STASH; } return operationType; } - (void)performOperationWithType:(OperationType)operationType browsingDataManagers:(NSArray*)browsingDataManagers completionHandler:(ProceduralBlock)completionHandler { DCHECK([NSThread isMainThread]); DCHECK(completionHandler); DCHECK_NE(NONE, operationType); SEL selector = [self browsingDataManagerSelectorForOperationType:operationType]; DCHECK(selector); if (operationType == RESTORE || operationType == STASH) { [self setMode:web::CHANGING]; ++self.numberOfPendingStashOrRestoreOperations; completionHandler = ^{ --self.numberOfPendingStashOrRestoreOperations; // It is safe to this and does not lead to the block (|completionHandler|) // retaining itself. completionHandler(); }; } ProceduralBlock callCompletionHandlerOnMainThread = ^{ // This is called on a background thread, hence the need to bounce to the // main thread. dispatch_async(dispatch_get_main_queue(), completionHandler); }; NSOperation* operation = [self operationWithBrowsingDataManagers:browsingDataManagers selector:selector]; if (operationType == RESTORE || operationType == STASH) { [operation setName:(RESTORE ? kRestoreOperationName : kStashOperationName)]; _lastDispatchedStashOrRestoreOperation.reset([operation retain]); } NSOperationQueue* queue = nil; switch (operationType) { case NONE: NOTREACHED(); break; case STASH: case RESTORE: queue = [CRWBrowsingDataStore operationQueueForStashAndRestoreOperations]; break; case REMOVE: queue = [CRWBrowsingDataStore operationQueueForRemoveOperations]; break; } NSOperation* completionHandlerOperation = [NSBlockOperation blockOperationWithBlock:callCompletionHandlerOnMainThread]; [self addOperation:operation toQueue:queue]; [self addOperation:completionHandlerOperation toQueue:queue]; } - (NSOperation*)operationWithBrowsingDataManagers:(NSArray*)browsingDataManagers selector:(SEL)selector { NSBlockOperation* operation = [[[NSBlockOperation alloc] init] autorelease]; for (id manager : browsingDataManagers) { // |addExecutionBlock| farms out the different blocks added to it. hence the // operations are implicitly parallelized. [operation addExecutionBlock:^{ [manager performSelector:selector]; }]; } return operation; } - (void)addOperation:(NSOperation*)operation toQueue:(NSOperationQueue*)queue { DCHECK([NSThread isMainThread]); DCHECK(operation); DCHECK(queue); if (_lastDispatchedOperation) { [operation addDependency:_lastDispatchedOperation]; } _lastDispatchedOperation.reset([operation retain]); [queue addOperation:operation]; } @end