blob: 675ff9271971bb4423940beab7b5a6d48943d9ee [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/sessions/session_service_ios.h"
#import <UIKit/UIKit.h>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/format_macros.h"
#include "base/location.h"
#include "base/logging.h"
#import "base/mac/foundation_util.h"
#include "base/memory/ref_counted.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/post_task.h"
#include "base/threading/scoped_blocking_call.h"
#import "ios/chrome/browser/sessions/session_ios.h"
#import "ios/chrome/browser/sessions/session_window_ios.h"
#import "ios/web/public/crw_navigation_item_storage.h"
#import "ios/web/public/crw_session_certificate_policy_cache_storage.h"
#import "ios/web/public/crw_session_storage.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
// When C++ exceptions are disabled, the C++ library defines |try| and
// |catch| so as to allow exception-expecting C++ code to build properly when
// language support for exceptions is not present. These macros interfere
// with the use of |@try| and |@catch| in Objective-C files such as this one.
// Undefine these macros here, after everything has been #included, since
// there will be no C++ uses and only Objective-C uses from this point on.
#undef try
#undef catch
namespace {
const NSTimeInterval kSaveDelay = 2.5; // Value taken from Desktop Chrome.
NSString* const kRootObjectKey = @"root"; // Key for the root object.
}
@implementation NSKeyedUnarchiver (CrLegacySessionCompatibility)
// When adding a new compatibility alias here, create a new crbug to track its
// removal and mark it with a release at least one year after the introduction
// of the alias.
- (void)cr_registerCompatibilityAliases {
// TODO(crbug.com/661633): those aliases where introduced between M57 and
// M58, so remove them after M67 has shipped to stable.
[self setClass:[CRWSessionCertificatePolicyCacheStorage class]
forClassName:@"SessionCertificatePolicyManager"];
[self setClass:[CRWSessionStorage class] forClassName:@"SessionController"];
[self setClass:[CRWSessionStorage class]
forClassName:@"CRWSessionController"];
[self setClass:[CRWNavigationItemStorage class] forClassName:@"SessionEntry"];
[self setClass:[CRWNavigationItemStorage class]
forClassName:@"CRWSessionEntry"];
[self setClass:[SessionWindowIOS class] forClassName:@"SessionWindow"];
// TODO(crbug.com/661633): this alias was introduced between M58 and M59, so
// remove it after M68 has shipped to stable.
[self setClass:[CRWSessionStorage class]
forClassName:@"CRWNavigationManagerStorage"];
[self setClass:[CRWSessionCertificatePolicyCacheStorage class]
forClassName:@"CRWSessionCertificatePolicyManager"];
}
@end
@implementation SessionServiceIOS {
// The SequencedTaskRunner on which File IO operations are performed.
scoped_refptr<base::SequencedTaskRunner> _taskRunner;
// Maps session path to the pending session for the delayed save behaviour.
NSMutableDictionary<NSString*, SessionIOSFactory>* _pendingSessions;
}
#pragma mark - NSObject overrides
- (instancetype)init {
scoped_refptr<base::SequencedTaskRunner> taskRunner =
base::CreateSequencedTaskRunnerWithTraits(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN});
return [self initWithTaskRunner:taskRunner];
}
#pragma mark - Public interface
+ (SessionServiceIOS*)sharedService {
static SessionServiceIOS* singleton = nil;
if (!singleton) {
singleton = [[[self class] alloc] init];
}
return singleton;
}
- (instancetype)initWithTaskRunner:
(const scoped_refptr<base::SequencedTaskRunner>&)taskRunner {
DCHECK(taskRunner);
self = [super init];
if (self) {
_pendingSessions = [NSMutableDictionary dictionary];
_taskRunner = taskRunner;
}
return self;
}
- (void)saveSession:(SessionIOSFactory)factory
directory:(NSString*)directory
immediately:(BOOL)immediately {
NSString* sessionPath = [[self class] sessionPathForDirectory:directory];
BOOL hadPendingSession = [_pendingSessions objectForKey:sessionPath] != nil;
[_pendingSessions setObject:factory forKey:sessionPath];
if (immediately) {
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self performSaveToPathInBackground:sessionPath];
} else if (!hadPendingSession) {
// If there wasn't previously a delayed save pending for |sessionPath|,
// enqueue one now.
[self performSelector:@selector(performSaveToPathInBackground:)
withObject:sessionPath
afterDelay:kSaveDelay];
}
}
- (SessionIOS*)loadSessionFromDirectory:(NSString*)directory {
NSString* sessionPath = [[self class] sessionPathForDirectory:directory];
return [self loadSessionFromPath:sessionPath];
}
- (SessionIOS*)loadSessionFromPath:(NSString*)sessionPath {
NSObject<NSCoding>* rootObject = nil;
@try {
NSData* data = [NSData dataWithContentsOfFile:sessionPath];
if (!data)
return nil;
NSKeyedUnarchiver* unarchiver =
[[NSKeyedUnarchiver alloc] initForReadingWithData:data];
// Register compatibility aliases to support legacy saved sessions.
[unarchiver cr_registerCompatibilityAliases];
rootObject = [unarchiver decodeObjectForKey:kRootObjectKey];
} @catch (NSException* exception) {
NOTREACHED() << "Error loading session file: "
<< base::SysNSStringToUTF8(sessionPath) << ": "
<< base::SysNSStringToUTF8([exception reason]);
}
if (!rootObject)
return nil;
// Support for legacy saved session that contained a single SessionWindowIOS
// object as the root object (pre-M-59).
if ([rootObject isKindOfClass:[SessionWindowIOS class]]) {
return [[SessionIOS alloc] initWithWindows:@[
base::mac::ObjCCastStrict<SessionWindowIOS>(rootObject)
]];
}
return base::mac::ObjCCastStrict<SessionIOS>(rootObject);
}
- (void)deleteLastSessionFileInDirectory:(NSString*)directory
completion:(base::OnceClosure)callback {
NSString* sessionPath = [[self class] sessionPathForDirectory:directory];
_taskRunner->PostTaskAndReply(
FROM_HERE, base::BindOnce(^{
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::MAY_BLOCK);
NSFileManager* fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:sessionPath])
return;
NSError* error = nil;
if (![fileManager removeItemAtPath:sessionPath error:nil])
CHECK(false) << "Unable to delete session file: "
<< base::SysNSStringToUTF8(sessionPath) << ": "
<< base::SysNSStringToUTF8([error description]);
}),
std::move(callback));
}
+ (NSString*)sessionPathForDirectory:(NSString*)directory {
return [directory stringByAppendingPathComponent:@"session.plist"];
}
#pragma mark - Private methods
// Do the work of saving on a background thread.
- (void)performSaveToPathInBackground:(NSString*)sessionPath {
DCHECK(sessionPath);
DCHECK([_pendingSessions objectForKey:sessionPath] != nil);
// Serialize to NSData on the main thread to avoid accessing potentially
// non-threadsafe objects on a background thread.
SessionIOSFactory factory = [_pendingSessions objectForKey:sessionPath];
[_pendingSessions removeObjectForKey:sessionPath];
SessionIOS* session = factory();
@try {
NSData* sessionData = [NSKeyedArchiver archivedDataWithRootObject:session];
_taskRunner->PostTask(FROM_HERE, base::BindOnce(^{
[self performSaveSessionData:sessionData
sessionPath:sessionPath];
}));
} @catch (NSException* exception) {
NOTREACHED() << "Error serializing session for path: "
<< base::SysNSStringToUTF8(sessionPath) << ": "
<< base::SysNSStringToUTF8([exception description]);
return;
}
}
@end
@implementation SessionServiceIOS (SubClassing)
- (void)performSaveSessionData:(NSData*)sessionData
sessionPath:(NSString*)sessionPath {
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::MAY_BLOCK);
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* directory = [sessionPath stringByDeletingLastPathComponent];
NSError* error = nil;
BOOL isDirectory = NO;
if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory]) {
isDirectory = YES;
if (![fileManager createDirectoryAtPath:directory
withIntermediateDirectories:YES
attributes:nil
error:&error]) {
NOTREACHED() << "Error creating destination directory: "
<< base::SysNSStringToUTF8(directory) << ": "
<< base::SysNSStringToUTF8([error description]);
return;
}
}
if (!isDirectory) {
NOTREACHED() << "Error creating destination directory: "
<< base::SysNSStringToUTF8(directory) << ": "
<< "file exists and is not a directory.";
return;
}
NSDataWritingOptions options =
NSDataWritingAtomic | NSDataWritingFileProtectionComplete;
if (![sessionData writeToFile:sessionPath options:options error:&error]) {
NOTREACHED() << "Error writing session file: "
<< base::SysNSStringToUTF8(sessionPath) << ": "
<< base::SysNSStringToUTF8([error description]);
return;
}
}
@end