chromium/ios/chrome/browser/external_files/model/external_file_remover_impl.mm

// Copyright 2012 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/external_files/model/external_file_remover_impl.h"

#import <utility>

#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "base/logging.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/single_thread_task_runner.h"
#import "base/task/thread_pool.h"
#import "base/threading/scoped_blocking_call.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/browser/url_and_title.h"
#import "components/sessions/core/tab_restore_service.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/sessions/model/ios_chrome_tab_restore_service_factory.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/url/url_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/thread/web_thread.h"
#import "ios/web/public/web_state.h"

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())];
    }

    // An "unrealized" WebState has no pending load. Checking for realization
    // before accessing the NavigationManager prevents accidental realization
    // of the WebState.
    if (web_state->IsRealized()) {
      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::tab_restore::Tab* tab =
        static_cast<sessions::tab_restore::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::SingleThreadTaskRunner::GetCurrentDefault()->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_IN_MIGRATION()
      << "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_);
  const BrowserList::BrowserType browser_types =
      browser_state_->IsOffTheRecord()
          ? BrowserList::BrowserType::kIncognito
          : BrowserList::BrowserType::kRegularAndInactive;
  std::set<Browser*> browsers = browser_list->BrowsersOfType(browser_types);
  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.
  for (const auto& bookmark : bookmark_model->GetUniqueUrls()) {
    GURL bookmark_url = bookmark.url;
    if (UrlIsExternalFileReference(bookmark_url)) {
      [referenced_external_files
          addObject:base::SysUTF8ToNSString(bookmark_url.ExtractFileName())];
    }
  }
  return referenced_external_files;
}