chromium/components/embedder_support/ios/delegate/file_chooser/file_select_helper_ios.mm

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

#include "components/embedder_support/ios/delegate/file_chooser/file_select_helper_ios.h"

#include "base/memory/scoped_refptr.h"
#include "base/strings/utf_string_conversions.h"
#include "content/public/browser/file_select_listener.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "net/base/filename_util.h"
#include "net/base/mime_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/shell_dialogs/select_file_policy.h"
#include "ui/shell_dialogs/selected_file_info.h"

namespace web_contents_delegate_ios {

namespace {

// Helper function to get allowed extensions for select file dialog from
// the specified accept types as defined in the spec:
//   http://whatwg.org/html/number-state.html#attr-input-accept
// |accept_types| contains only valid lowercased MIME types or file extensions
// beginning with a period (.).
std::unique_ptr<ui::SelectFileDialog::FileTypeInfo> GetFileTypesFromAcceptType(
    const std::vector<std::u16string>& accept_types) {
  auto base_file_type = std::make_unique<ui::SelectFileDialog::FileTypeInfo>();
  if (accept_types.empty()) {
    return base_file_type;
  }

  // Creates FileTypeInfo and pre-allocate for the first extension list.
  auto file_type =
      std::make_unique<ui::SelectFileDialog::FileTypeInfo>(*base_file_type);
  file_type->extensions.resize(1);
  std::vector<base::FilePath::StringType>* extensions =
      &file_type->extensions.back();

  // Finds the corresponding extensions.
  size_t valid_type_count = 0;
  for (const auto& accept_type : accept_types) {
    size_t old_extension_size = extensions->size();
    if (accept_type[0] == '.') {
      // If the type starts with a period it is assumed to be a file extension
      // so we just have to add it to the list.
      base::FilePath::StringType ext =
          base::FilePath::FromUTF16Unsafe(accept_type).value();
      extensions->push_back(ext.substr(1));
    } else {
      if (!base::IsStringASCII(accept_type)) {
        continue;
      }
      std::string ascii_type = base::UTF16ToASCII(accept_type);
      net::GetExtensionsForMimeType(ascii_type, extensions);
    }

    if (extensions->size() > old_extension_size) {
      valid_type_count++;
    }
  }

  // If no valid extension is added, bail out.
  if (valid_type_count == 0) {
    return base_file_type;
  }

  return file_type;
}

}  // namespace

struct FileSelectHelperIOS::ActiveDirectoryEnumeration {
  explicit ActiveDirectoryEnumeration(const base::FilePath& path)
      : path_(path) {}

  std::unique_ptr<net::DirectoryLister> lister_;
  const base::FilePath path_;
  std::vector<base::FilePath> results_;
};

// static
void FileSelectHelperIOS::RunFileChooser(
    content::RenderFrameHost* render_frame_host,
    scoped_refptr<content::FileSelectListener> listener,
    const blink::mojom::FileChooserParams& params) {
  // FileSelectHelperIOS will keep itself alive until it sends the result
  // message.
  scoped_refptr<FileSelectHelperIOS> file_select_helper(
      new FileSelectHelperIOS());
  file_select_helper->RunFileChooser(render_frame_host, std::move(listener),
                                     params.Clone());
}

FileSelectHelperIOS::FileSelectHelperIOS() = default;

FileSelectHelperIOS::~FileSelectHelperIOS() {
  // There may be pending file dialogs, we need to tell them that we've gone
  // away so they don't try and call back to us.
  if (select_file_dialog_) {
    select_file_dialog_->ListenerDestroyed();
  }
}

void FileSelectHelperIOS::RunFileChooser(
    content::RenderFrameHost* render_frame_host,
    scoped_refptr<content::FileSelectListener> listener,
    blink::mojom::FileChooserParamsPtr params) {
  DCHECK(!web_contents_);
  DCHECK(listener);
  DCHECK(!listener_);
  DCHECK(!select_file_dialog_);

  listener_ = std::move(listener);
  web_contents_ = content::WebContents::FromRenderFrameHost(render_frame_host)
                      ->GetWeakPtr();

  select_file_dialog_ = ui::SelectFileDialog::Create(this, nullptr);

  select_file_types_ = GetFileTypesFromAcceptType(params->accept_types);
  select_file_types_->allowed_paths =
      params->need_local_path ? ui::SelectFileDialog::FileTypeInfo::NATIVE_PATH
                              : ui::SelectFileDialog::FileTypeInfo::ANY_PATH;
  // 1-based index of default extension to show.
  int file_type_index =
      select_file_types_ && !select_file_types_->extensions.empty() ? 1 : 0;

  dialog_mode_ = params->mode;
  switch (params->mode) {
    case blink::mojom::FileChooserParams::Mode::kOpen:
      dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE;
      break;
    case blink::mojom::FileChooserParams::Mode::kOpenMultiple:
      dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE;
      break;
    case blink::mojom::FileChooserParams::Mode::kUploadFolder:
      dialog_type_ = ui::SelectFileDialog::SELECT_UPLOAD_FOLDER;
      break;
    case blink::mojom::FileChooserParams::Mode::kSave:
      dialog_type_ = ui::SelectFileDialog::SELECT_SAVEAS_FILE;
      break;
    default:
      // Prevent warning.
      dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE;
      NOTREACHED_IN_MIGRATION();
  }

  gfx::NativeWindow owning_window = web_contents_->GetTopLevelNativeWindow();
  select_file_dialog_->SelectFile(dialog_type_, std::u16string(),
                                  base::FilePath(), select_file_types_.get(),
                                  file_type_index, base::FilePath::StringType(),
                                  owning_window, nullptr);

  // Because this class returns notifications to the RenderViewHost, it is
  // difficult for callers to know how long to keep a reference to this
  // instance. We AddRef() here to keep the instance alive after we return
  // to the caller, until the last callback is received from the file dialog.
  // At that point, we must call RunFileChooserEnd().
  AddRef();
}

void FileSelectHelperIOS::RunFileChooserEnd() {
  if (listener_) {
    listener_->FileSelectionCanceled();
  }

  select_file_dialog_->ListenerDestroyed();
  select_file_dialog_.reset();
  Release();
}

void FileSelectHelperIOS::FileSelected(const ui::SelectedFileInfo& file,
                                       int index) {
  if (dialog_type_ == ui::SelectFileDialog::SELECT_UPLOAD_FOLDER) {
    StartNewEnumeration(file.local_path);
    return;
  }
  ConvertToFileChooserFileInfoList({file});
}

void FileSelectHelperIOS::MultiFilesSelected(
    const std::vector<ui::SelectedFileInfo>& files) {
  ConvertToFileChooserFileInfoList(files);
}

void FileSelectHelperIOS::FileSelectionCanceled() {
  RunFileChooserEnd();
}

void FileSelectHelperIOS::StartNewEnumeration(const base::FilePath& path) {
  base_dir_ = path;
  auto entry = std::make_unique<ActiveDirectoryEnumeration>(path);
  entry->lister_ = base::WrapUnique(new net::DirectoryLister(
      path, net::DirectoryLister::NO_SORT_RECURSIVE, this));
  entry->lister_->Start();
  directory_enumeration_ = std::move(entry);
}

void FileSelectHelperIOS::OnListFile(
    const net::DirectoryLister::DirectoryListerData& data) {
  // Directory upload only cares about files.
  if (data.info.IsDirectory()) {
    return;
  }

  directory_enumeration_->results_.push_back(data.path);
}

void FileSelectHelperIOS::OnListDone(int error) {
  if (!web_contents_) {
    // Web contents was destroyed under us (probably by closing the tab). We
    // must notify |listener_| and release our reference to
    // ourself. RunFileChooserEnd() performs this.
    RunFileChooserEnd();
    return;
  }

  // This entry needs to be cleaned up when this function is done.
  std::unique_ptr<ActiveDirectoryEnumeration> entry =
      std::move(directory_enumeration_);
  if (error) {
    FileSelectionCanceled();
    return;
  }

  std::vector<ui::SelectedFileInfo> selected_files =
      ui::FilePathListToSelectedFileInfoList(entry->results_);

  std::vector<blink::mojom::FileChooserFileInfoPtr> chooser_files;
  for (const auto& file_path : entry->results_) {
    chooser_files.push_back(blink::mojom::FileChooserFileInfo::NewNativeFile(
        blink::mojom::NativeFileInfo::New(file_path, std::u16string())));
  }

  listener_->FileSelected(std::move(chooser_files), base_dir_,
                          blink::mojom::FileChooserParams::Mode::kUploadFolder);
  listener_.reset();
  // No members should be accessed from here on.
  RunFileChooserEnd();
}

void FileSelectHelperIOS::ConvertToFileChooserFileInfoList(
    const std::vector<ui::SelectedFileInfo>& files) {
  if (!web_contents_) {
    RunFileChooserEnd();
    return;
  }

  std::vector<blink::mojom::FileChooserFileInfoPtr> chooser_files;
  for (const auto& file : files) {
    chooser_files.push_back(blink::mojom::FileChooserFileInfo::NewNativeFile(
        blink::mojom::NativeFileInfo::New(
            file.local_path,
            base::FilePath(file.display_name).AsUTF16Unsafe())));
  }

  listener_->FileSelected(std::move(chooser_files), base::FilePath(),
                          dialog_mode_);
  listener_ = nullptr;

  // No members should be accessed from here on.
  RunFileChooserEnd();
}

}  // namespace web_contents_delegate_ios