chromium/chrome/browser/webshare/chromeos/sharesheet_client.cc

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

#include "chrome/browser/webshare/chromeos/sharesheet_client.h"

#include <memory>

#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/functional/callback_helpers.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/apps/app_service/intent_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sharesheet/sharesheet_metrics.h"
#include "chrome/browser/visibility_timer_tab_helper.h"
#include "chrome/browser/webshare/prepare_directory_task.h"
#include "chrome/browser/webshare/prepare_subdirectory_task.h"
#include "chrome/browser/webshare/share_service_impl.h"
#include "chrome/browser/webshare/store_files_task.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "net/base/filename_util.h"

#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chrome/browser/ui/lacros/window_utility.h"
#include "chrome/common/chrome_paths_lacros.h"
#include "chromeos/crosapi/mojom/app_service_types.mojom.h"
#include "chromeos/crosapi/mojom/sharesheet.mojom.h"
#include "chromeos/crosapi/mojom/sharesheet_mojom_traits.h"
#include "chromeos/lacros/lacros_service.h"
#else
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/sharesheet/sharesheet_service.h"
#include "chrome/browser/sharesheet/sharesheet_service_factory.h"
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

using content::BrowserThread;
using content::WebContents;

namespace {

constexpr base::FilePath::CharType kWebShareDirname[] =
#if BUILDFLAG(IS_CHROMEOS_LACROS)
    FILE_PATH_LITERAL(".web_share");
#else
    FILE_PATH_LITERAL(".WebShare");
#endif

constexpr char kDefaultShareName[] = "share";

// Note that the suffix of |suggested_name| has been checked by
// ShareServiceImpl::IsDangerousFilename().
base::FilePath GenerateFileName(content::WebContents* web_contents,
                                const base::FilePath& directory,
                                const base::SafeBaseName& suggested_name) {
  static unsigned counter = 0;

  ++counter;
  std::string dirname = base::StringPrintf("share%u", counter);

  Profile* profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());
  std::string referrer_charset =
      profile->GetPrefs()->GetString(prefs::kDefaultCharset);

  base::FilePath filename =
      net::GenerateFileName(web_contents->GetLastCommittedURL(),
                            /*content_disposition=*/std::string(),
                            referrer_charset, suggested_name.path().value(),
                            /*mime_type=*/std::string(), kDefaultShareName);

  return directory.Append(dirname).Append(filename);
}

blink::mojom::ShareError SharesheetResultToShareError(
    sharesheet::SharesheetResult result) {
  switch (result) {
    case sharesheet::SharesheetResult::kSuccess:
      return blink::mojom::ShareError::OK;
    case sharesheet::SharesheetResult::kCancel:
    case sharesheet::SharesheetResult::kErrorAlreadyOpen:
    case sharesheet::SharesheetResult::kErrorWindowClosed:
      return blink::mojom::ShareError::CANCELED;
  }
}

// Deletes immediate parent directories of specified |file_paths|, after waiting
// |delay|.
void ScheduleSharedFileDirectoryDeletion(std::vector<base::FilePath> file_paths,
                                         base::TimeDelta delay) {
  for (size_t i = 0; i < file_paths.size(); ++i)
    file_paths[i] = file_paths[i].DirName();

  webshare::PrepareDirectoryTask::ScheduleSharedFileDeletion(
      std::move(file_paths), delay);
}

#if BUILDFLAG(IS_CHROMEOS_LACROS)
crosapi::mojom::IntentPtr CreateCrosapiShareIntentFromFiles(
    const std::vector<base::FilePath>& file_paths,
    const std::vector<std::string>& mime_types,
    const std::string& text,
    const std::string& title) {
  DCHECK_EQ(file_paths.size(), mime_types.size());

  std::vector<crosapi::mojom::IntentFilePtr> files;
  files.reserve(file_paths.size());
  for (size_t index = 0; index < file_paths.size(); ++index) {
    files.push_back(
        crosapi::mojom::IntentFile::New(file_paths[index], mime_types[index]));
  }

  // Always share text and/or files.
  std::optional<std::string> share_text;
  if (!text.empty() || file_paths.empty())
    share_text = text;

  std::optional<std::string> share_title;
  if (!title.empty())
    share_title = title;

  const char* action = file_paths.size() <= 1
                           ? apps_util::kIntentActionSend
                           : apps_util::kIntentActionSendMultiple;
  std::string mime_type = file_paths.empty()
                              ? "text/plain"
                              : apps_util::CalculateCommonMimeType(mime_types);
  return crosapi::mojom::Intent::New(action,
                                     /*url=*/std::nullopt, mime_type,
                                     share_text, share_title, std::move(files));
}
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)
}  // namespace

namespace webshare {

SharesheetClient::CurrentShare::CurrentShare() = default;
SharesheetClient::CurrentShare::CurrentShare(CurrentShare&&) = default;
SharesheetClient::CurrentShare& SharesheetClient::CurrentShare::operator=(
    SharesheetClient::CurrentShare&&) = default;
SharesheetClient::CurrentShare::~CurrentShare() = default;

SharesheetClient::SharesheetClient(content::WebContents* web_contents)
    : content::WebContentsObserver(web_contents) {}

SharesheetClient::~SharesheetClient() = default;

void SharesheetClient::Share(
    const std::string& title,
    const std::string& text,
    const GURL& share_url,
    std::vector<blink::mojom::SharedFilePtr> files,
    blink::mojom::ShareService::ShareCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // The SharesheetClient only shows one share sheet at a time.
  if (current_share_.has_value() || !web_contents()) {
    VLOG(1) << "Cannot share when an existing share is in progress, or after "
               "navigating away";
    std::move(callback).Run(blink::mojom::ShareError::PERMISSION_DENIED);
    return;
  }

  Profile* const profile =
      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
  DCHECK(profile);

  // File sharing is denied in incognito, as files are written to disk.
  // To prevent sites from using that to detect whether incognito mode is
  // active, we deny after a random time delay, to simulate a user cancelling
  // the share.
  if (profile->IsIncognitoProfile() && !files.empty()) {
    // Random number of seconds in the range [1.0, 2.0).
    double delay_seconds = 1.0 + 1.0 * base::RandDouble();
    VisibilityTimerTabHelper::CreateForWebContents(web_contents());
    VisibilityTimerTabHelper::FromWebContents(web_contents())
        ->PostTaskAfterVisibleDelay(
            FROM_HERE,
            base::BindOnce(std::move(callback),
                           blink::mojom::ShareError::CANCELED),
            base::Seconds(delay_seconds));
    return;
  }

  current_share_ = CurrentShare();
  current_share_->files = std::move(files);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  current_share_->directory =
      file_manager::util::GetShareCacheFilePath(profile).Append(
          kWebShareDirname);
#else
  base::FilePath share_cache_dir;
  if (chrome::GetShareCachePath(&share_cache_dir)) {
    current_share_->directory = share_cache_dir.Append(kWebShareDirname);
  } else {
    LOG(ERROR) << "Share cache path not set";  // DO NOT LAND
    VLOG(1) << "Share cache path not set";
    current_share_->files.clear();
  }
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
  if (share_url.is_valid()) {
    if (text.empty())
      current_share_->text = share_url.spec();
    else
      current_share_->text = text + " " + share_url.spec();
  } else {
    current_share_->text = text;
  }
  current_share_->title = title;
  current_share_->callback = std::move(callback);

  if (current_share_->files.empty()) {
    GetSharesheetCallback().Run(
        web_contents(), current_share_->file_paths,
        current_share_->content_types, current_share_->file_sizes,
        current_share_->text, current_share_->title,
        base::BindOnce(&SharesheetClient::OnShowSharesheet,
                       weak_ptr_factory_.GetWeakPtr()));
    return;
  }

#if BUILDFLAG(IS_CHROMEOS_ASH)
  // Previously, shared files were stored in MyFiles/.WebShare. We remove this
  // obsolete directory.
  PrepareDirectoryTask::ScheduleSharedFileDeletion(
      {file_manager::util::GetMyFilesFolderForProfile(profile).Append(
          kWebShareDirname)},
      /*delay=*/base::TimeDelta());
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

  current_share_->prepare_directory_task =
      std::make_unique<PrepareDirectoryTask>(
          current_share_->directory, kMaxSharedFileBytes,
          base::BindOnce(&SharesheetClient::OnPrepareDirectory,
                         weak_ptr_factory_.GetWeakPtr()));
  current_share_->prepare_directory_task->Start();
}

// static
void SharesheetClient::SetSharesheetCallbackForTesting(
    SharesheetCallback callback) {
  GetSharesheetCallback() = std::move(callback);
}

void SharesheetClient::OnPrepareDirectory(blink::mojom::ShareError error) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!current_share_.has_value())
    return;

  if (!web_contents() || error != blink::mojom::ShareError::OK) {
    std::move(current_share_->callback).Run(error);
    current_share_ = std::nullopt;
    return;
  }

  for (const auto& file : current_share_->files) {
    current_share_->content_types.push_back(file->blob->content_type);
    current_share_->file_paths.push_back(GenerateFileName(
        web_contents(), current_share_->directory, file->name));
    current_share_->file_sizes.push_back(file->blob->size);
  }

  current_share_->prepare_subdirectory_task =
      std::make_unique<PrepareSubDirectoryTask>(
          current_share_->file_paths,
          base::BindOnce(&SharesheetClient::OnPrepareSubdirectory,
                         weak_ptr_factory_.GetWeakPtr()));
  current_share_->prepare_subdirectory_task->Start();
}

void SharesheetClient::OnPrepareSubdirectory(blink::mojom::ShareError error) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!current_share_.has_value())
    return;

  if (!web_contents() || error != blink::mojom::ShareError::OK) {
    std::move(current_share_->callback).Run(error);
    current_share_ = std::nullopt;
    return;
  }

  std::unique_ptr<StoreFilesTask> store_files_task =
      std::make_unique<StoreFilesTask>(
          current_share_->file_paths, std::move(current_share_->files),
          kMaxSharedFileBytes,
          base::BindOnce(&SharesheetClient::OnStoreFiles,
                         weak_ptr_factory_.GetWeakPtr()));

  // The StoreFilesTask is self-owned.
  store_files_task.release()->Start();
}

void SharesheetClient::OnStoreFiles(blink::mojom::ShareError error) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!current_share_.has_value())
    return;

  if (!web_contents() || error != blink::mojom::ShareError::OK) {
    std::move(current_share_->callback).Run(error);
    ScheduleSharedFileDirectoryDeletion(std::move(current_share_->file_paths),
                                        base::Minutes(0));
    current_share_ = std::nullopt;
    return;
  }

  GetSharesheetCallback().Run(
      web_contents(), current_share_->file_paths, current_share_->content_types,
      current_share_->file_sizes, current_share_->text, current_share_->title,
      base::BindOnce(&SharesheetClient::OnShowSharesheet,
                     weak_ptr_factory_.GetWeakPtr()));
}

void SharesheetClient::OnShowSharesheet(sharesheet::SharesheetResult result) {
  if (!current_share_.has_value())
    return;

  std::move(current_share_->callback).Run(SharesheetResultToShareError(result));
  ScheduleSharedFileDirectoryDeletion(
      std::move(current_share_->file_paths),
      PrepareDirectoryTask::kSharedFileLifetime);
  current_share_ = std::nullopt;
}

// static
void SharesheetClient::ShowSharesheet(
    content::WebContents* web_contents,
    const std::vector<base::FilePath>& file_paths,
    const std::vector<std::string>& content_types,
    const std::vector<uint64_t>& file_sizes,
    const std::string& text,
    const std::string& title,
    DeliveredCallback delivered_callback) {
  DCHECK_EQ(file_paths.size(), content_types.size());
  DCHECK_EQ(file_paths.size(), file_sizes.size());

  Profile* const profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());
  DCHECK(profile);

#if BUILDFLAG(IS_CHROMEOS_LACROS)
  auto* const service = chromeos::LacrosService::Get();
  if (!service || !service->IsAvailable<crosapi::mojom::Sharesheet>()) {
    std::move(delivered_callback).Run(sharesheet::SharesheetResult::kCancel);
    return;
  }
  crosapi::mojom::IntentPtr intent =
      CreateCrosapiShareIntentFromFiles(file_paths, content_types, text, title);
  DCHECK(intent->share_text.has_value() || !intent->files->empty());

  service->GetRemote<crosapi::mojom::Sharesheet>()->ShowBubble(
      lacros_window_utility::GetRootWindowUniqueId(
          web_contents->GetTopLevelNativeWindow()),
      sharesheet::LaunchSource::kWebShare, std::move(intent),
      std::move(delivered_callback));
#else
  apps::IntentPtr intent =
      file_paths.empty() ? apps_util::MakeShareIntent(text, title)
                         : apps_util::CreateShareIntentFromFiles(
                               profile, file_paths, content_types, text, title);
  if (!intent->files.empty() && intent->files.size() == file_paths.size()) {
    for (size_t index = 0; index < file_paths.size(); ++index) {
      (intent->files)[index]->mime_type = content_types[index];
      (intent->files)[index]->file_size = file_sizes[index];
    }
  }
  DCHECK(intent->share_text.has_value() || !intent->files.empty());

  sharesheet::SharesheetService* const sharesheet_service =
      sharesheet::SharesheetServiceFactory::GetForProfile(profile);
  sharesheet_service->ShowBubble(web_contents, std::move(intent),
                                 sharesheet::LaunchSource::kWebShare,
                                 std::move(delivered_callback));
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)
}

SharesheetClient::SharesheetCallback&
SharesheetClient::GetSharesheetCallback() {
  static base::NoDestructor<SharesheetCallback> callback(
      base::BindRepeating(&SharesheetClient::ShowSharesheet));

  return *callback;
}

void SharesheetClient::WebContentsDestroyed() {
  current_share_ = std::nullopt;
}

}  // namespace webshare