// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/sessions/model/web_session_state_cache.h"
#import <UIKit/UIKit.h>
#import "base/apple/foundation_util.h"
#import "base/base_paths.h"
#import "base/containers/contains.h"
#import "base/files/file_enumerator.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/functional/bind.h"
#import "base/logging.h"
#import "base/observer_list.h"
#import "base/path_service.h"
#import "base/sequence_checker.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/thread_pool.h"
#import "base/threading/scoped_blocking_call.h"
#import "ios/chrome/browser/sessions/model/session_constants.h"
#import "ios/chrome/browser/sessions/model/web_session_state_tab_helper.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/web/public/web_state_id.h"
namespace {
// The delay, in seconds, for cleaning up any unassociated session state files
// when -removeSessionStateDataForWebState is called while `_delayRemove` is
// true.
const int kRemoveSessionStateDataDelay = 10;
// Returns the session identifier for `web_state_id` as a string.
std::string SessionIdentifierForWebStateID(web::WebStateID web_state_id) {
DCHECK(web_state_id.valid());
DCHECK_GT(web_state_id.identifier(), 0);
static_assert(sizeof(decltype(web_state_id.identifier())) == sizeof(int32_t));
const uint32_t identifier = static_cast<uint32_t>(web_state_id.identifier());
return base::StringPrintf("%08u", identifier);
}
// Writes `session_data` to `file_path`. If -writeToFile fails, deletes
// the old (now stale) data.
void WriteSessionData(NSData* session_data, base::FilePath file_path) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
const base::FilePath directory = file_path.DirName();
if (!base::DirectoryExists(directory)) {
bool success = base::CreateDirectory(directory);
if (!success) {
DLOG(ERROR) << "Error creating session cache directory "
<< directory.AsUTF8Unsafe();
return;
}
}
NSDataWritingOptions options =
NSDataWritingAtomic |
NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication;
NSString* file_path_string = base::apple::FilePathToNSString(file_path);
NSError* error = nil;
if (![session_data writeToFile:file_path_string
options:options
error:&error]) {
DLOG(WARNING) << "Error writing session data: "
<< base::SysNSStringToUTF8(file_path_string) << ": "
<< base::SysNSStringToUTF8([error description]);
// If -writeToFile failed, this session data is now stale. Delete it and
// revert to legacy session restore.
base::DeleteFile(file_path);
return;
}
}
// Helper function to implement -purgeCacheExcept: on a background sequence.
void PurgeCacheOnBackgroundSequenceExcept(
base::FilePath cache_directory,
std::set<base::FilePath> files_to_keep) {
if (!base::DirectoryExists(cache_directory))
return;
base::FileEnumerator enumerator(cache_directory, false,
base::FileEnumerator::FILES);
for (base::FilePath current_file = enumerator.Next(); !current_file.empty();
current_file = enumerator.Next()) {
if (base::Contains(files_to_keep, current_file))
continue;
base::DeleteFile(current_file);
}
}
} // anonymous namespace
@interface WebSessionStateCache ()
// The ChromeBrowserState passed on initialization.
@property(nonatomic) ChromeBrowserState* browserState;
@end
@implementation WebSessionStateCache {
// Task runner used to run tasks in the background. Will be invalidated when
// -shutdown is invoked. Code should support this value to be null (generally
// by not posting the task).
scoped_refptr<base::SequencedTaskRunner> _taskRunner;
// Directory where the thumbnails are saved.
base::FilePath _cacheDirectory;
// When set, delay calls to -removeSessionStateDataForWebState, replaced with
// a single -purgeCache call.
BOOL _delayRemove;
// Check that public API is called from the correct sequence.
SEQUENCE_CHECKER(_sequenceChecker);
}
- (instancetype)initWithBrowserState:(ChromeBrowserState*)browserState {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if ((self = [super init])) {
_browserState = browserState;
_cacheDirectory =
browserState->GetStatePath().Append(kLegacyWebSessionsDirname);
_taskRunner = base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN});
}
return self;
}
- (void)dealloc {
DCHECK(!_taskRunner) << "-shutdown must be called before -dealloc";
}
- (void)persistSessionStateData:(NSData*)data
forWebStateID:(web::WebStateID)webStateID {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (!data || !_taskRunner) {
return;
}
_taskRunner->PostTask(
FROM_HERE,
base::BindOnce(
&WriteSessionData, data,
_cacheDirectory.Append(SessionIdentifierForWebStateID(webStateID))));
}
- (NSData*)sessionStateDataForWebStateID:(web::WebStateID)webStateID {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
return [NSData dataWithContentsOfFile:base::apple::FilePathToNSString(
_cacheDirectory.Append(
SessionIdentifierForWebStateID(
webStateID)))];
}
- (void)purgeUnassociatedDataWithCompletion:(base::OnceClosure)closure {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
[self purgeCacheExcept:[self liveSessionIDs] closure:std::move(closure)];
}
- (void)removeSessionStateDataForWebStateID:(web::WebStateID)webStateID
incognito:(BOOL)incognito {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (!_taskRunner)
return;
if (_delayRemove && !incognito) {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self performSelector:@selector(purgeUnassociatedData)
withObject:nil
afterDelay:kRemoveSessionStateDataDelay];
return;
}
_taskRunner->PostTask(
FROM_HERE,
base::BindOnce(
base::IgnoreResult(&base::DeleteFile),
_cacheDirectory.Append(SessionIdentifierForWebStateID(webStateID))));
}
- (void)setDelayRemove:(BOOL)delayRemove {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
_delayRemove = delayRemove;
}
- (void)shutdown {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
[NSObject cancelPreviousPerformRequestsWithTarget:self];
_taskRunner = nullptr;
_browserState = nullptr;
}
#pragma mark - Private
- (void)purgeUnassociatedData {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
[self purgeUnassociatedDataWithCompletion:base::DoNothing()];
}
// Returns a set of all known tab ids.
- (std::set<std::string>)liveSessionIDs {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
DCHECK(_browserState) << "-liveSessionIDs called after -shutdown";
std::set<std::string> liveSessionIDs;
BrowserList* browserList =
BrowserListFactory::GetForBrowserState(self.browserState);
for (Browser* browser :
browserList->BrowsersOfType(BrowserList::BrowserType::kAll)) {
WebStateList* webStateList = browser->GetWebStateList();
for (int index = 0; index < webStateList->count(); ++index) {
web::WebState* webState = webStateList->GetWebStateAt(index);
liveSessionIDs.insert(
SessionIdentifierForWebStateID(webState->GetUniqueIdentifier()));
}
}
return liveSessionIDs;
}
// Deletes any files from the session cache directory that don't exist in
// `liveSessionIDs`.
- (void)purgeCacheExcept:(std::set<std::string>)liveSessionIDs
closure:(base::OnceClosure)closure {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
if (!_taskRunner) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(closure));
return;
}
std::set<base::FilePath> filesToKeep;
for (const std::string& sessionID : liveSessionIDs) {
filesToKeep.insert(_cacheDirectory.Append(sessionID));
}
_taskRunner->PostTaskAndReply(
FROM_HERE,
base::BindOnce(&PurgeCacheOnBackgroundSequenceExcept, _cacheDirectory,
filesToKeep),
std::move(closure));
}
@end