// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/file_system_access/file_system_access_handle_base.h"

#include <memory>
#include <vector>

#include "base/barrier_callback.h"
#include "base/base_paths.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/safe_base_name.h"
#include "base/functional/bind.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/file_system_access/features.h"
#include "content/browser/file_system_access/file_system_access_directory_handle_impl.h"
#include "content/browser/file_system_access/file_system_access_error.h"
#include "content/browser/file_system_access/file_system_access_file_handle_impl.h"
#include "content/browser/file_system_access/file_system_access_manager_impl.h"
#include "content/browser/file_system_access/file_system_access_safe_move_helper.h"
#include "content/browser/file_system_access/file_system_access_transfer_token_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/file_system_access_permission_context.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "mojo/public/cpp/bindings/message.h"
#include "storage/browser/file_system/file_system_operation.h"
#include "storage/browser/file_system/file_system_url.h"
#include "storage/common/file_system/file_system_types.h"
#include "storage/common/file_system/file_system_util.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/mojom/file_system_access/file_system_access_error.mojom-forward.h"

namespace content {

using LockHandle = FileSystemAccessLockManager::LockHandle;

namespace {
std::string GetURLDisplayName(const storage::FileSystemURL& url) {
  return base::UTF16ToUTF8(url.path().BaseName().LossyDisplayName());
}

}  // namespace

FileSystemAccessHandleBase::FileSystemAccessHandleBase(
    FileSystemAccessManagerImpl* manager,
    const BindingContext& context,
    const storage::FileSystemURL& url,
    const SharedHandleState& handle_state)
    : manager_(manager),
      context_(context),
      url_(url),
      handle_state_(handle_state) {
  DCHECK(manager_);

  // We support sandboxed file system and local file systems on all platforms.
  DCHECK(url_.type() == storage::kFileSystemTypeLocal ||
         url_.type() == storage::kFileSystemTypeTemporary ||
         url_.mount_type() == storage::kFileSystemTypeExternal ||
         url_.type() == storage::kFileSystemTypeTest)
      << url_.type();

  if (ShouldTrackUsage(url_)) {
    DCHECK(url_.mount_type() == storage::kFileSystemTypeLocal ||
           url_.mount_type() == storage::kFileSystemTypeExternal)
        << url_.mount_type();

    WebContents* web_contents =
        WebContentsImpl::FromRenderFrameHostID(context_.frame_id);
    if (web_contents) {
      web_contents_ = web_contents->GetWeakPtr();
      static_cast<WebContentsImpl*>(web_contents_.get())
          ->IncrementFileSystemAccessHandleCount();
    }
  }
}

FileSystemAccessHandleBase::~FileSystemAccessHandleBase() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (ShouldTrackUsage(url_) && web_contents_) {
    static_cast<WebContentsImpl*>(web_contents_.get())
        ->DecrementFileSystemAccessHandleCount();
  }
}

FileSystemAccessHandleBase::PermissionStatus
FileSystemAccessHandleBase::GetReadPermissionStatus() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return handle_state_.read_grant->GetStatus();
}

FileSystemAccessHandleBase::PermissionStatus
FileSystemAccessHandleBase::GetWritePermissionStatus() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!base::FeatureList::IsEnabled(
          blink::features::kFileSystemAccessWriteMode)) {
    // It is not currently possible to have write only handles, so no one should
    // call this method. See also:
    // http://wicg.github.io/file-system-access/#api-filesystemhandle-querypermission
    mojo::ReportBadMessage("feature 'FileSystemAccessWriteMode' not enabled");
    NOTREACHED();
  }
  return handle_state_.write_grant->GetStatus();
}

FileSystemAccessHandleBase::PermissionStatus
FileSystemAccessHandleBase::GetReadWritePermissionStatus() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  PermissionStatus read_status = GetReadPermissionStatus();
  if (read_status != PermissionStatus::GRANTED) {
    return read_status;
  }

  return handle_state_.write_grant->GetStatus();
}

FileSystemAccessHandleBase::PermissionStatus
FileSystemAccessHandleBase::GetEffectiveWritePermissionStatus() {
  return GetPermissionStatusForMode(
      FileSystemAccessManagerImpl::GetEffectiveWritePermissionMode());
}

// TODO(crbug.com/40276567): Update callers if they only need write permission.
void FileSystemAccessHandleBase::DoGetPermissionStatus(
    blink::mojom::FileSystemAccessPermissionMode mode,
    base::OnceCallback<void(PermissionStatus)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  std::move(callback).Run(GetPermissionStatusForMode(mode));
}

// TODO(crbug.com/40276567): Update callers if they only need write permission.
void FileSystemAccessHandleBase::DoRequestPermission(
    blink::mojom::FileSystemAccessPermissionMode mode,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr,
                            PermissionStatus)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!base::FeatureList::IsEnabled(
          blink::features::kFileSystemAccessWriteMode) &&
      mode == blink::mojom::FileSystemAccessPermissionMode::kWrite) {
    // Rejects invalid mode at the very beginning.
    mojo::ReportBadMessage("feature 'FileSystemAccessWriteMode' not enabled");
    NOTREACHED();
  }

  PermissionStatus current_status = GetPermissionStatusForMode(mode);
  // If we already have a valid permission status, just return that. Also just
  // return the current permission status if this is called from a worker, as we
  // don't support prompting for increased permissions from workers.
  //
  // Currently the worker check here is redundant because there is no way for
  // workers to get File System Access handles. While workers will never be able
  // to call chooseEntries(), they will be able to receive existing handles from
  // windows via postMessage() and IndexedDB.
  if (current_status != PermissionStatus::ASK || context_.is_worker) {
    std::move(callback).Run(file_system_access_error::Ok(), current_status);
    return;
  }

  // 1. Request "read"-only permission.
  if (mode == blink::mojom::FileSystemAccessPermissionMode::kRead) {
    handle_state_.read_grant->RequestPermission(
        context().frame_id,
        FileSystemAccessPermissionGrant::UserActivationState::kRequired,
        base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
                       AsWeakPtr(), mode, std::move(callback)));
    return;
  }

  // 2. Request "write"-only permission.
  if (mode == blink::mojom::FileSystemAccessPermissionMode::kWrite) {
    handle_state_.write_grant->RequestPermission(
        context().frame_id,
        FileSystemAccessPermissionGrant::UserActivationState::kRequired,
        base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
                       AsWeakPtr(), mode, std::move(callback)));
    return;
  }

  // 3. Ask for both read and write permission at the same time, the permission
  // context should coalesce these into one prompt.

  if (base::FeatureList::IsEnabled(
          blink::features::kFileSystemAccessWriteMode)) {
    // When the write mode feature is enabled, we need to check both read and
    // write permission status.
    if (GetReadPermissionStatus() == PermissionStatus::ASK &&
        GetWritePermissionStatus() == PermissionStatus::ASK) {
      handle_state_.read_grant->RequestPermission(
          context().frame_id,
          FileSystemAccessPermissionGrant::UserActivationState::kRequired,
          base::DoNothing());
      handle_state_.write_grant->RequestPermission(
          context().frame_id,
          FileSystemAccessPermissionGrant::UserActivationState::kRequired,
          base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
                         AsWeakPtr(), mode, std::move(callback)));
      return;
    }
    if (GetReadPermissionStatus() == PermissionStatus::ASK) {
      handle_state_.read_grant->RequestPermission(
          context().frame_id,
          FileSystemAccessPermissionGrant::UserActivationState::kRequired,
          base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
                         AsWeakPtr(), mode, std::move(callback)));
      return;
    }
    if (GetWritePermissionStatus() == PermissionStatus::ASK) {
      handle_state_.write_grant->RequestPermission(
          context().frame_id,
          FileSystemAccessPermissionGrant::UserActivationState::kRequired,
          base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
                         AsWeakPtr(), mode, std::move(callback)));
      return;
    }
    NOTREACHED();
  }

  if (GetReadPermissionStatus() == PermissionStatus::ASK) {
    // Ignore callback for the read permission request; if the request fails,
    // the write permission request probably fails the same way. And we check
    // the final permission status after the permission request completes
    // anyway.
    handle_state_.read_grant->RequestPermission(
        context().frame_id,
        FileSystemAccessPermissionGrant::UserActivationState::kRequired,
        base::DoNothing());
  }

  // When the standalone write mode feature is disabled, we don't need to
  // check the write permission status as it's always requested with read
  // permission, i.e. only `GetReadWritePermissionStatus()` is valid here, and
  // it must be ASK.
  handle_state_.write_grant->RequestPermission(
      context().frame_id,
      FileSystemAccessPermissionGrant::UserActivationState::kRequired,
      base::BindOnce(&FileSystemAccessHandleBase::DidRequestPermission,
                     AsWeakPtr(), mode, std::move(callback)));
  // Both read & write permission is either granted or denied at this point.
}

void FileSystemAccessHandleBase::DidRequestPermission(
    blink::mojom::FileSystemAccessPermissionMode mode,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr,
                            PermissionStatus)> callback,
    FileSystemAccessPermissionGrant::PermissionRequestOutcome outcome) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  using Outcome = FileSystemAccessPermissionGrant::PermissionRequestOutcome;
  switch (outcome) {
    case Outcome::kInvalidFrame:
    case Outcome::kThirdPartyContext:
      std::move(callback).Run(
          file_system_access_error::FromStatus(
              blink::mojom::FileSystemAccessStatus::kSecurityError,
              "Not allowed to request permissions in this context."),
          GetPermissionStatusForMode(mode));
      return;
    case Outcome::kNoUserActivation:
      std::move(callback).Run(
          file_system_access_error::FromStatus(
              blink::mojom::FileSystemAccessStatus::kSecurityError,
              "User activation is required to request permissions."),
          GetPermissionStatusForMode(mode));
      return;
    case Outcome::kBlockedByContentSetting:
    case Outcome::kUserGranted:
    case Outcome::kUserDenied:
    case Outcome::kUserDismissed:
    case Outcome::kRequestAborted:
    case Outcome::kGrantedByContentSetting:
    case Outcome::kGrantedByPersistentPermission:
    case Outcome::kGrantedByAncestorPersistentPermission:
    case Outcome::kGrantedByRestorePrompt:
      std::move(callback).Run(file_system_access_error::Ok(),
                              GetPermissionStatusForMode(mode));
      return;
  }
  NOTREACHED();
}

void FileSystemAccessHandleBase::DoMove(
    mojo::PendingRemote<blink::mojom::FileSystemAccessTransferToken>
        destination_directory,
    const std::string& new_entry_name,
    bool has_transient_user_activation,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK_EQ(GetEffectiveWritePermissionStatus(),
            blink::mojom::PermissionStatus::GRANTED);

  manager()->ResolveTransferToken(
      std::move(destination_directory),
      base::BindOnce(&FileSystemAccessHandleBase::DidResolveTokenToMove,
                     AsWeakPtr(), new_entry_name, has_transient_user_activation,
                     std::move(callback)));
}

FileSystemAccessHandleBase::RenamePermission
FileSystemAccessHandleBase::GetRenamePermission(
    const std::string& new_entry_name,
    const bool has_transient_user_activation,
    const storage::FileSystemURL destination_url) {
  if (destination_url.type() ==
      storage::FileSystemType::kFileSystemTypeTemporary) {
    // For OPFS files, rename with overwrite permission is always allowed.
    return RenamePermission::kAllowOverwrite;
  }

  // 1. Check write permission to the target file.
  CHECK_NE(url().type(), storage::FileSystemType::kFileSystemTypeTemporary);
  SharedHandleState destination_shared_handle_state =
      manager()->GetSharedHandleStateForNonSandboxedPath(
          content::PathInfo(destination_url.virtual_path(), new_entry_name),
          context().storage_key,
          // TODO(crbug.com/40198034): Support directory moves.
          FileSystemAccessPermissionContext::HandleType::kFile,
          FileSystemAccessPermissionContext::UserAction::kNone);

  if (destination_shared_handle_state.write_grant->GetStatus() ==
      PermissionStatus::GRANTED) {
    // When write access to the target file is granted, rename with overwrite
    // permission is allowed, regardless of the flag.
    return RenamePermission::kAllowOverwrite;
  }

  // 2. Determine if we need to check parent directory write permission.
  // Legacy Behavior (flag off): allow gesture bypass.
  if (!base::FeatureList::IsEnabled(
          features::kFileSystemAccessRenameRequiresParentWritePermission)) {
    return has_transient_user_activation
               ? RenamePermission::kAllowRenameWithoutOverwrite
               : RenamePermission::kDeny;
  }
  // New behavior (flag on): since we don't have write permission to the target
  // file, write permission to the parent directory is required to rename the
  // file. We no longer need to check user activation, because user activation
  // was introduced as a fallback security mitigation instead of requiring
  // parent directory write permission, in order to avoid silent renaming in the
  // background.
  bool should_check_parent_directory_permission = true;
  // If `kOnlyInHomedir` is true, the parent directory write permission check is
  // restricted to the home directory.
  if (features::kOnlyInHomedir.Get()) {
    // When the param is set to true, check the parent directory access only
    // when the destination is in home directory.
    base::FilePath home_dir;
    if (base::PathService::Get(base::DIR_HOME, &home_dir)) {
      base::FilePath parent_dir = destination_url.path().DirName();
      should_check_parent_directory_permission =
          home_dir.IsParent(parent_dir) || (home_dir == parent_dir);
    }
  }

  // 3. Perform the appropriate permission check.
  if (should_check_parent_directory_permission) {
    SharedHandleState parent_shared_handle_state =
        manager()->GetSharedHandleStateForNonSandboxedPath(
            content::PathInfo(
                destination_url.type() == storage::kFileSystemTypeExternal
                    ? PathType::kExternal
                    : PathType::kLocal,
                destination_url.path().DirName()),
            context().storage_key,
            FileSystemAccessPermissionContext::HandleType::kDirectory,
            FileSystemAccessPermissionContext::UserAction::kNone);

    if (parent_shared_handle_state.write_grant->GetStatus() ==
        PermissionStatus::GRANTED) {
      // Parent directory permission is granted.
      return RenamePermission::kAllowOverwrite;
    } else {
      return RenamePermission::kDeny;
    }
  } else {
    // Flag on with `kOnlyHomedir` true, but the destination is not in the
    // homedir. Allow gesture bypass.
    CHECK(features::kOnlyInHomedir.Get());
    if (!has_transient_user_activation) {
      return RenamePermission::kDeny;
    } else {
      return RenamePermission::kAllowRenameWithoutOverwrite;
    }
  }
}

void FileSystemAccessHandleBase::DoRename(
    const std::string& new_entry_name,
    bool has_transient_user_activation,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // To get this far, we must have write access to the entry being moved.
  DCHECK_EQ(GetEffectiveWritePermissionStatus(),
            blink::mojom::PermissionStatus::GRANTED);

  if (!manager()->IsSafePathComponent(url().type(), new_entry_name)) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kInvalidArgument));
    return;
  }

  auto sibling_safe_name =
      base::SafeBaseName::Create(storage::StringToFilePath(new_entry_name));
  CHECK(sibling_safe_name.has_value());
  storage::FileSystemURL destination_url =
      url().CreateSibling(*sibling_safe_name);
#if BUILDFLAG(IS_ANDROID)
  // Android Content-URIs do not support CreateSibling().
  if (!destination_url.is_valid()) {
    CHECK(url().path().IsContentUri());
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kInvalidModificationError));
    return;
  }
#endif
  CHECK(destination_url.is_valid());

  RenamePermission permission = GetRenamePermission(
      new_entry_name, has_transient_user_activation, destination_url);
  if (permission == RenamePermission::kDeny) {
    CHECK_NE(destination_url.type(), storage::kFileSystemTypeTemporary);
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kPermissionDenied));
    return;
  }

  const bool has_overwrite_permission =
      permission == RenamePermission::kAllowOverwrite;
  PrepareForMove(std::move(destination_url), has_overwrite_permission,
                 has_transient_user_activation, std::move(callback));
}

void FileSystemAccessHandleBase::DidResolveTokenToMove(
    const std::string& new_entry_name,
    bool has_transient_user_activation,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback,
    FileSystemAccessTransferTokenImpl* resolved_destination_directory) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!resolved_destination_directory ||
      resolved_destination_directory->origin() !=
          context_.storage_key.origin()) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kInvalidArgument));
    return;
  }

  if (resolved_destination_directory->type() !=
      FileSystemAccessPermissionContext::HandleType::kDirectory) {
    mojo::ReportBadMessage(
        "FileSystemHandle::move() was passed a token which is not a directory");
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kInvalidArgument));
    return;
  }

  if (!manager()->IsSafePathComponent(
          resolved_destination_directory->url().type(), new_entry_name)) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kInvalidArgument));
    return;
  }

  std::unique_ptr<FileSystemAccessDirectoryHandleImpl> dir_handle =
      resolved_destination_directory->CreateDirectoryHandle(context_);

  // Must have write access to the target directory for cross-directory moves.
  bool has_write_access_to_parent =
      dir_handle->GetEffectiveWritePermissionStatus() ==
      blink::mojom::PermissionStatus::GRANTED;
  if (!has_write_access_to_parent) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kPermissionDenied));
    return;
  }

  // Moving into a readable directory would make the entry readable through that
  // directory's read grant. Require read access to the source so an entry whose
  // read grant was revoked (e.g. by remove()) cannot regain read access this
  // way. See crbug.com/523741272.
  if (dir_handle->GetReadPermissionStatus() ==
          blink::mojom::PermissionStatus::GRANTED &&
      GetReadPermissionStatus() != blink::mojom::PermissionStatus::GRANTED) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kPermissionDenied));
    return;
  }

  storage::FileSystemURL dest_url;
  blink::mojom::FileSystemAccessErrorPtr error =
      dir_handle->GetChildURL(new_entry_name, &dest_url);
  if (error != file_system_access_error::Ok()) {
    std::move(callback).Run(std::move(error));
    return;
  }

  // Disallow moving a directory into itself or one of its descendants.
  if (url().path().IsParent(dest_url.path())) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kInvalidModificationError));
    return;
  }

  // Disallow moves either to or from a sandboxed file system.
  if (url().type() != dest_url.type() &&
      (url().type() == storage::FileSystemType::kFileSystemTypeTemporary ||
       dest_url.type() == storage::FileSystemType::kFileSystemTypeTemporary)) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kInvalidModificationError));
    return;
  }

  PrepareForMove(std::move(dest_url), has_write_access_to_parent,
                 has_transient_user_activation, std::move(callback));
}

void FileSystemAccessHandleBase::PrepareForMove(
    storage::FileSystemURL destination_url,
    bool has_overwrite_permission,
    bool has_transient_user_activation,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // The file can only be moved if we can acquire exclusive locks to both the
  // source and destination URLs.
  const bool take_destination_lock = destination_url != url();
  auto barrier_callback = base::BarrierCallback<scoped_refptr<LockHandle>>(
      take_destination_lock ? 2 : 1,
      base::BindOnce(&FileSystemAccessHandleBase::DidTakeMoveLocks, AsWeakPtr(),
                     destination_url, has_transient_user_activation,
                     has_overwrite_permission, std::move(callback)));

  manager()->TakeLock(context(), url(), manager()->GetExclusiveLockType(),
                      barrier_callback);
  if (take_destination_lock) {
    manager()->TakeLock(context(), destination_url,
                        manager()->GetExclusiveLockType(), barrier_callback);
  }
}

void FileSystemAccessHandleBase::DidTakeMoveLocks(
    storage::FileSystemURL destination_url,
    bool has_transient_user_activation,
    bool has_overwrite_permission,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback,
    std::vector<scoped_refptr<LockHandle>> locks) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  const auto& source_lock = locks[0];
  if (!source_lock) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kNoModificationAllowedError,
        base::StrCat(
            {"Failed to move ", GetURLDisplayName(url()),
             ". A FileSystemHandle cannot be moved while it is locked."})));
    return;
  }
  if (locks.size() > 1 && !locks[1]) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kNoModificationAllowedError,
        base::StrCat({"Failed to move ", GetURLDisplayName(url()), " to ",
                      GetURLDisplayName(destination_url),
                      ". A FileSystemHandle cannot be moved to a destination "
                      "which is locked."})));
    return;
  }

  if (manager()->permission_context() &&
      destination_url.type() !=
          storage::FileSystemType::kFileSystemTypeTemporary) {
    // So far the destination path is considered safe with write access.
    // However, it might still point to a blocklisted file type. Request a
    // sensitive entry access check which will decide if the destination path
    // should be allowed or blocked, and may spawn a confirmation UI.
    content::PathInfo path_info{
        destination_url.type() == storage::FileSystemType::kFileSystemTypeLocal
            ? PathType::kLocal
            : PathType::kExternal,
        destination_url.path()};
    manager()->permission_context()->ConfirmSensitiveEntryAccess(
        context().storage_key.origin(), path_info,
        // TODO(crbug.com/40198034): Update once moving directory is supported.
        FileSystemAccessPermissionContext::HandleType::kFile,
        // This must be kSave as the move operation will save the file.
        FileSystemAccessPermissionContext::UserAction::kSave,
        context().frame_id,
        base::BindOnce(
            &FileSystemAccessHandleBase::DidVerifySensitiveEntryAccessForMove,
            AsWeakPtr(), std::move(destination_url), has_overwrite_permission,
            has_transient_user_activation, std::move(callback),
            std::move(locks)));
  } else {
    // Skipping ConfirmSensitiveEntryAccess() as either of the following holds:
    // (1) no permission context. Possibly because manager() is being destroyed
    // or in a test.
    // (2) destination file is in Bucket File System, i.e. not a real file.
    DidVerifySensitiveEntryAccessForMove(
        std::move(destination_url), has_overwrite_permission,
        has_transient_user_activation, std::move(callback), std::move(locks),
        FileSystemAccessPermissionContext::SensitiveEntryResult::kAllowed);
  }
}

void FileSystemAccessHandleBase::DidVerifySensitiveEntryAccessForMove(
    storage::FileSystemURL destination_url,
    bool has_overwrite_permission,
    bool has_transient_user_activation,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback,
    std::vector<scoped_refptr<LockHandle>> locks,
    FileSystemAccessPermissionContext::SensitiveEntryResult
        sensitive_entry_result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (sensitive_entry_result !=
      FileSystemAccessPermissionContext::SensitiveEntryResult::kAllowed) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kInvalidArgument));
    return;
  }

  // Only allow overwriting moves if we have write access to the destination or
  // the parent directory write permission.
  if (has_overwrite_permission) {
    DoPerformMoveOperation(destination_url, std::move(locks),
                           has_transient_user_activation, std::move(callback));
    return;
  }

  // TODO(crbug.com/40198034): Use DirectoryExists() for directory moves.
  manager()->DoFileSystemOperation(
      FROM_HERE, &storage::FileSystemOperationRunner::FileExists,
      base::BindOnce(
          &FileSystemAccessHandleBase::ConfirmMoveWillNotOverwriteDestination,
          AsWeakPtr(), destination_url, std::move(locks),
          has_transient_user_activation, std::move(callback)),
      destination_url);
}

void FileSystemAccessHandleBase::ConfirmMoveWillNotOverwriteDestination(
    const storage::FileSystemURL& destination_url,
    std::vector<scoped_refptr<LockHandle>> locks,
    bool has_transient_user_activation,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback,
    base::File::Error result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (result == base::File::FILE_OK ||
      result == base::File::FILE_ERROR_EXISTS ||
      result == base::File::FILE_ERROR_NOT_EMPTY ||
      result == base::File::FILE_ERROR_NOT_A_FILE ||
      result == base::File::FILE_ERROR_INVALID_OPERATION) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kInvalidModificationError));
    return;
  }

  DoPerformMoveOperation(destination_url, std::move(locks),
                         has_transient_user_activation, std::move(callback));
}

void FileSystemAccessHandleBase::DoPerformMoveOperation(
    const storage::FileSystemURL& destination_url,
    std::vector<scoped_refptr<LockHandle>> locks,
    bool has_transient_user_activation,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  auto file_system_access_safe_move_helper =
      std::make_unique<FileSystemAccessSafeMoveHelper>(
          manager()->AsWeakPtr(), context(), url(), destination_url,
          storage::FileSystemOperation::CopyOrMoveOptionSet(),
          GetContentClient()->browser()->GetQuarantineConnectionCallback(),
          has_transient_user_activation);
  // Allows the unique pointer to be bound to the callback so the helper stays
  // alive until the operation completes.
  FileSystemAccessSafeMoveHelper* raw_helper =
      file_system_access_safe_move_helper.get();
  raw_helper->Start(base::BindOnce(
      &FileSystemAccessHandleBase::DidMove, AsWeakPtr(), destination_url,
      std::move(locks), std::move(file_system_access_safe_move_helper),
      std::move(callback)));
}

void FileSystemAccessHandleBase::DidMove(
    storage::FileSystemURL destination_url,
    std::vector<scoped_refptr<LockHandle>> locks,
    std::unique_ptr<FileSystemAccessSafeMoveHelper> /*move_helper*/,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback,
    blink::mojom::FileSystemAccessErrorPtr result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (result->status == blink::mojom::FileSystemAccessStatus::kOk) {
    // TODO(crbug.com/40790187): Update permission grants appropriately when
    // moving into/out of the OPFS. Current state:
    // - Moving out of the OPFS: the destination directory is guaranteed to have
    //   write permission. We _should_ update `handle_state_` to point to this
    //   directory, but it technically works as-is.
    // - Moving into the OPFS: we leave a dangling permission grant at the old
    //   path. We should clean this up, but since it's effectively a remove()
    //   this isn't the worst behavior for now.
    if (ShouldTrackUsage(url_) && ShouldTrackUsage(destination_url) &&
        manager()->permission_context()) {
      manager()->permission_context()->NotifyEntryMoved(
          context_.storage_key.origin(), content::PathInfo(url_.path()),
          content::PathInfo(destination_url.path()));
    }

    url_ = std::move(destination_url);
  }

  // Destroy locks so they are released by the time the callback runs.
  locks.clear();

  std::move(callback).Run(std::move(result));
}

void FileSystemAccessHandleBase::DoRemove(
    const storage::FileSystemURL& url,
    bool recurse,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK_EQ(GetEffectiveWritePermissionStatus(),
            blink::mojom::PermissionStatus::GRANTED);

  // A locked file cannot be removed. Acquire a lock and release it after the
  // remove operation completes.
  manager()->TakeLock(
      context(), url, manager()->GetExclusiveLockType(),
      base::BindOnce(&FileSystemAccessHandleBase::DidTakeRemoveLock,
                     AsWeakPtr(), url, recurse, std::move(callback)));
}

void FileSystemAccessHandleBase::DidTakeRemoveLock(
    const storage::FileSystemURL& url,
    bool recurse,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback,
    scoped_refptr<LockHandle> lock) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!lock) {
    std::move(callback).Run(file_system_access_error::FromStatus(
        blink::mojom::FileSystemAccessStatus::kNoModificationAllowedError));
    return;
  }

  // Bind the `lock` to the Remove callback to guarantee the lock is held until
  // the operation completes.
  auto wrapped_callback =
      base::BindOnce(&FileSystemAccessHandleBase::DidRemove, AsWeakPtr(), url,
                     std::move(lock), std::move(callback));

  manager()->DoFileSystemOperation(FROM_HERE,
                                   &storage::FileSystemOperationRunner::Remove,
                                   std::move(wrapped_callback), url, recurse);
}

void FileSystemAccessHandleBase::DidRemove(
    const storage::FileSystemURL& url,
    scoped_refptr<FileSystemAccessLockManager::LockHandle> lock,
    base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)> callback,
    base::File::Error result) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Update permission grants appropriately after removing the entry.
  // See crbug.com/421690393.
  if (base::FeatureList::IsEnabled(
          blink::features::kFileSystemAccessRevokeReadOnRemove) &&
      result == base::File::FILE_OK && manager()->permission_context() &&
      ShouldTrackUsage(url_)) {
    manager()->permission_context()->NotifyEntryRemoved(
        context_.storage_key.origin(), PathInfo(url.path()));
  }

  // Destroy lock so it is released by the time the callback runs.
  lock.reset();
  std::move(callback).Run(file_system_access_error::FromFileError(result));
}

void FileSystemAccessHandleBase::DoGetCloudIdentifiers(
    FileSystemAccessPermissionContext::HandleType handle_type,
    ContentBrowserClient::GetCloudIdentifiersCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!base::FeatureList::IsEnabled(
          blink::features::kFileSystemAccessGetCloudIdentifiers)) {
    mojo::ReportBadMessage(
        "feature 'FileSystemAccessGetCloudIdentifiers' not enabled");
    std::move(callback).Run(
        file_system_access_error::FromStatus(
            blink::mojom::FileSystemAccessStatus::kSecurityError),
        {});
    return;
  }

  if (GetReadPermissionStatus() != PermissionStatus::GRANTED) {
    std::move(callback).Run(
        file_system_access_error::FromStatus(
            blink::mojom::FileSystemAccessStatus::kPermissionDenied),
        {});
    return;
  }

  GetContentClient()->browser()->GetCloudIdentifiers(url_, handle_type,
                                                     std::move(callback));
}

// Calculates the parent URL fom current context, propagating any
// storage bucket overrides from the child.
storage::FileSystemURL FileSystemAccessHandleBase::GetParentURL() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  const storage::FileSystemURL child = url();
  storage::FileSystemURL parent =
      file_system_context()->CreateCrackedFileSystemURL(
          child.storage_key(), child.mount_type(),
          storage::VirtualPath::DirName(child.virtual_path()));
  if (child.bucket()) {
    parent.SetBucket(child.bucket().value());
  }
  return parent;
}

void FileSystemAccessHandleBase::MaybeNotifyEntryModified(
    const storage::FileSystemURL& url) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (base::FeatureList::IsEnabled(
          blink::features::kFileSystemAccessRevokeReadOnRemove) &&
      manager()->permission_context() && ShouldTrackUsage(url)) {
    manager()->permission_context()->NotifyEntryModified(
        context().storage_key.origin(), PathInfo(url.path()));
  }
}

FileSystemAccessHandleBase::PermissionStatus
FileSystemAccessHandleBase::GetPermissionStatusForMode(
    blink::mojom::FileSystemAccessPermissionMode mode) {
  switch (mode) {
    case blink::mojom::FileSystemAccessPermissionMode::kRead:
      return GetReadPermissionStatus();
    case blink::mojom::FileSystemAccessPermissionMode::kReadWrite:
      return GetReadWritePermissionStatus();
    case blink::mojom::FileSystemAccessPermissionMode::kWrite:
      return GetWritePermissionStatus();
  }
}

}  // namespace content
