blob: 748a23eb982b910f038707bceade21034e4cf1c3 [file] [log] [blame]
// Copyright (c) 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/external_files/external_file_remover_impl.h"
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/logging.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/url_and_title.h"
#include "components/sessions/core/tab_restore_service.h"
#include "ios/chrome/browser/bookmarks/bookmark_model_factory.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/chrome_url_util.h"
#import "ios/chrome/browser/main/browser.h"
#import "ios/chrome/browser/main/browser_list.h"
#import "ios/chrome/browser/main/browser_list_factory.h"
#include "ios/chrome/browser/sessions/ios_chrome_tab_restore_service_factory.h"
#import "ios/chrome/browser/web_state_list/web_state_list.h"
#include "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#include "ios/web/public/thread/web_thread.h"
#import "ios/web/public/web_state.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// The path relative to the <Application_Home>/Documents/ directory where the
// files received from other applications are saved.
NSString* const kInboxPath = @"Inbox";
// Conversion factor to turn number of days to number of seconds.
const CFTimeInterval kSecondsPerDay = 60 * 60 * 24;
// Empty callback. The closure owned by |closure_runner| will be invoked as
// part of the destructor of base::ScopedClosureRunner (which takes care of
// checking for null closure).
void RunCallback(base::ScopedClosureRunner closure_runner) {}
NSSet* ComputeReferencedExternalFiles(Browser* browser) {
NSMutableSet* referenced_files = [NSMutableSet set];
if (!browser)
return referenced_files;
WebStateList* web_state_list = browser->GetWebStateList();
// Check the currently open tabs for external files.
for (int index = 0; index < web_state_list->count(); ++index) {
web::WebState* web_state = web_state_list->GetWebStateAt(index);
const GURL& last_committed_url = web_state->GetLastCommittedURL();
if (UrlIsExternalFileReference(last_committed_url)) {
[referenced_files addObject:base::SysUTF8ToNSString(
last_committed_url.ExtractFileName())];
}
web::NavigationItem* pending_item =
web_state->GetNavigationManager()->GetPendingItem();
if (pending_item && UrlIsExternalFileReference(pending_item->GetURL())) {
[referenced_files
addObject:base::SysUTF8ToNSString(
pending_item->GetURL().ExtractFileName())];
}
}
// Do the same for the recently closed tabs.
sessions::TabRestoreService* restore_service =
IOSChromeTabRestoreServiceFactory::GetForBrowserState(
browser->GetBrowserState());
DCHECK(restore_service);
for (const auto& entry : restore_service->entries()) {
sessions::TabRestoreService::Tab* tab =
static_cast<sessions::TabRestoreService::Tab*>(entry.get());
int navigation_index = tab->current_navigation_index;
sessions::SerializedNavigationEntry navigation =
tab->navigations[navigation_index];
GURL url = navigation.virtual_url();
if (UrlIsExternalFileReference(url)) {
NSString* file_name = base::SysUTF8ToNSString(url.ExtractFileName());
[referenced_files addObject:file_name];
}
}
return referenced_files;
}
// Returns the path in the application sandbox of an external file from the
// URL received for that file.
NSString* GetInboxDirectoryPath() {
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
if ([paths count] < 1)
return nil;
NSString* documents_directory_path = [paths objectAtIndex:0];
return [documents_directory_path stringByAppendingPathComponent:kInboxPath];
}
// Removes all the files in the Inbox directory that are not in
// |files_to_keep| and that are older than |age_in_days| days.
// |files_to_keep| may be nil if all files should be removed.
void RemoveFilesWithOptions(NSSet* files_to_keep, NSInteger age_in_days) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
NSFileManager* file_manager = [NSFileManager defaultManager];
NSString* inbox_directory = GetInboxDirectoryPath();
NSArray* external_files =
[file_manager contentsOfDirectoryAtPath:inbox_directory error:nil];
for (NSString* filename in external_files) {
NSString* file_path =
[inbox_directory stringByAppendingPathComponent:filename];
if ([files_to_keep containsObject:filename])
continue;
// Checks the age of the file and do not remove files that are too recent.
// Under normal circumstances, e.g. when file purge is not initiated by
// user action, leave recently downloaded files around to avoid users
// using history back or recent tabs to reach an external file that was
// pre-maturely purged.
NSError* error = nil;
NSDictionary* attributesDictionary =
[file_manager attributesOfItemAtPath:file_path error:&error];
if (error) {
DLOG(ERROR) << "Failed to retrieve attributes for " << file_path << ": "
<< base::SysNSStringToUTF8([error description]);
continue;
}
NSDate* date = [attributesDictionary objectForKey:NSFileCreationDate];
if (-[date timeIntervalSinceNow] <= (age_in_days * kSecondsPerDay))
continue;
// Removes the file.
[file_manager removeItemAtPath:file_path error:&error];
if (error) {
DLOG(ERROR) << "Failed to remove file " << file_path << ": "
<< base::SysNSStringToUTF8([error description]);
continue;
}
}
}
} // namespace
ExternalFileRemoverImpl::ExternalFileRemoverImpl(
ChromeBrowserState* browser_state,
sessions::TabRestoreService* tab_restore_service)
: tab_restore_service_(tab_restore_service),
browser_state_(browser_state),
weak_ptr_factory_(this) {
DCHECK(tab_restore_service_);
tab_restore_service_->AddObserver(this);
}
ExternalFileRemoverImpl::~ExternalFileRemoverImpl() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void ExternalFileRemoverImpl::RemoveAfterDelay(base::TimeDelta delay,
base::OnceClosure callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::ScopedClosureRunner closure_runner =
base::ScopedClosureRunner(std::move(callback));
bool remove_all_files = delay == base::Seconds(0);
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ExternalFileRemoverImpl::RemoveFiles,
weak_ptr_factory_.GetWeakPtr(), remove_all_files,
std::move(closure_runner)),
delay);
}
void ExternalFileRemoverImpl::Shutdown() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (tab_restore_service_) {
tab_restore_service_->RemoveObserver(this);
tab_restore_service_ = nullptr;
}
delayed_file_remove_requests_.clear();
}
void ExternalFileRemoverImpl::TabRestoreServiceChanged(
sessions::TabRestoreService* service) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (service->IsLoaded())
return;
tab_restore_service_->RemoveObserver(this);
tab_restore_service_ = nullptr;
std::vector<DelayedFileRemoveRequest> delayed_file_remove_requests;
delayed_file_remove_requests = std::move(delayed_file_remove_requests_);
for (DelayedFileRemoveRequest& request : delayed_file_remove_requests) {
RemoveFiles(request.remove_all_files, std::move(request.closure_runner));
}
}
void ExternalFileRemoverImpl::TabRestoreServiceDestroyed(
sessions::TabRestoreService* service) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
NOTREACHED() << "Should never happen as unregistration happen in Shutdown";
}
void ExternalFileRemoverImpl::Remove(bool all_files,
base::ScopedClosureRunner closure_runner) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!tab_restore_service_) {
RemoveFiles(all_files, std::move(closure_runner));
return;
}
// Removal is delayed until tab restore loading completes.
DCHECK(!tab_restore_service_->IsLoaded());
DelayedFileRemoveRequest request = {all_files, std::move(closure_runner)};
delayed_file_remove_requests_.push_back(std::move(request));
if (delayed_file_remove_requests_.size() == 1)
tab_restore_service_->LoadTabsFromLastSession();
}
void ExternalFileRemoverImpl::RemoveFiles(
bool all_files,
base::ScopedClosureRunner closure_runner) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
NSSet* referenced_files = all_files ? GetReferencedExternalFiles() : nil;
const NSInteger kMinimumAgeInDays = 30;
NSInteger age_in_days = all_files ? 0 : kMinimumAgeInDays;
base::ThreadPool::PostTaskAndReply(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&RemoveFilesWithOptions, referenced_files, age_in_days),
base::BindOnce(&RunCallback, std::move(closure_runner)));
}
NSSet* ExternalFileRemoverImpl::GetReferencedExternalFiles() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Add files from all Browsers.
NSMutableSet* referenced_external_files = [NSMutableSet set];
BrowserList* browser_list =
BrowserListFactory::GetForBrowserState(browser_state_);
std::set<Browser*> browsers = browser_state_->IsOffTheRecord()
? browser_list->AllIncognitoBrowsers()
: browser_list->AllRegularBrowsers();
for (Browser* browser : browsers) {
NSSet* files = ComputeReferencedExternalFiles(browser);
if (files) {
[referenced_external_files unionSet:files];
}
}
bookmarks::BookmarkModel* bookmark_model =
ios::BookmarkModelFactory::GetForBrowserState(browser_state_);
// Check if the bookmark model is loaded.
if (!bookmark_model || !bookmark_model->loaded())
return referenced_external_files;
// Add files from Bookmarks.
std::vector<bookmarks::UrlAndTitle> bookmarks;
bookmark_model->GetBookmarks(&bookmarks);
for (const auto& bookmark : bookmarks) {
GURL bookmark_url = bookmark.url;
if (UrlIsExternalFileReference(bookmark_url)) {
[referenced_external_files
addObject:base::SysUTF8ToNSString(bookmark_url.ExtractFileName())];
}
}
return referenced_external_files;
}