chromium/chrome/browser/webshare/mac/sharing_service_operation.mm

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

#import "chrome/browser/webshare/mac/sharing_service_operation.h"

#include <AppKit/AppKit.h>

#include <utility>

#include "base/functional/bind.h"
#include "base/i18n/file_util_icu.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/uuid.h"
#include "chrome/browser/profiles/profile.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 "content/public/browser/browser_context.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "net/base/filename_util.h"
#include "storage/browser/blob/blob_storage_context.h"
#include "url/gurl.h"

using content::BrowserContext;
using content::StoragePartition;

namespace {

constexpr base::FilePath::CharType kWebShareDirname[] =
    FILE_PATH_LITERAL("WebShare");

base::FilePath GenerateUniqueSubDirectory(const base::FilePath& directory) {
  std::string unique_subdirectory = base::StringPrintf(
      "share-%s", base::Uuid::GenerateRandomV4().AsLowercaseString().c_str());
  return directory.Append(unique_subdirectory);
}

}  // namespace

namespace webshare {

SharingServiceOperation::SharingServiceOperation(
    const std::string& title,
    const std::string& text,
    const GURL& url,
    std::vector<blink::mojom::SharedFilePtr> files,
    content::WebContents* web_contents)
    : web_contents_(web_contents->GetWeakPtr()),
      title_(title),
      text_(text),
      url_(url),
      shared_files_(std::move(files)) {}

SharingServiceOperation::~SharingServiceOperation() = default;

void SharingServiceOperation::Share(
    blink::mojom::ShareService::ShareCallback callback) {
  callback_ = std::move(callback);

  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() && !shared_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_.get());
    VisibilityTimerTabHelper::FromWebContents(web_contents_.get())
        ->PostTaskAfterVisibleDelay(
            FROM_HERE,
            base::BindOnce(std::move(callback_),
                           blink::mojom::ShareError::CANCELED),
            base::Seconds(delay_seconds));
    return;
  }

  if (shared_files_.size() == 0) {
    GetSharePickerCallback().Run(
        web_contents_.get(), file_paths_, text_, title_, url_,
        base::BindOnce(&SharingServiceOperation::OnShowSharePicker,
                       weak_factory_.GetWeakPtr()));
    return;
  }

  BrowserContext* browser_context = web_contents_->GetBrowserContext();
  StoragePartition* const partition =
      browser_context->GetDefaultStoragePartition();
  directory_ = partition->GetPath().Append(kWebShareDirname);

  prepare_directory_task_ = std::make_unique<PrepareDirectoryTask>(
      directory_, kMaxSharedFileBytes,
      base::BindOnce(&SharingServiceOperation::OnPrepareDirectory,
                     weak_factory_.GetWeakPtr()));

  prepare_directory_task_->Start();
}

// static
void SharingServiceOperation::SetSharePickerCallbackForTesting(
    SharePickerCallback callback) {
  GetSharePickerCallback() = std::move(callback);
}

void SharingServiceOperation::OnPrepareDirectory(
    blink::mojom::ShareError error) {
  if (!web_contents_ || error != blink::mojom::ShareError::OK) {
    std::move(callback_).Run(error);
    return;
  }

  for (const auto& file : shared_files_) {
    // SafeBaseName protects against including paths in a file name.
    std::string file_name = file->name.path().value();
    DCHECK_EQ(file_name.find('/'), std::string::npos);
    base::i18n::ReplaceIllegalCharactersInPath(&file_name, '_');
    file_paths_.push_back(
        GenerateUniqueSubDirectory(directory_).Append(file_name));
  }

  prepare_subdirectory_task_ = std::make_unique<PrepareSubDirectoryTask>(
      file_paths_,
      base::BindOnce(&SharingServiceOperation::OnPrepareSubDirectory,
                     weak_factory_.GetWeakPtr()));

  prepare_subdirectory_task_->Start();
}

void SharingServiceOperation::OnPrepareSubDirectory(
    blink::mojom::ShareError error) {
  if (!web_contents_ || error != blink::mojom::ShareError::OK) {
    std::move(callback_).Run(error);
    return;
  }

  auto store_files_task = std::make_unique<StoreFilesTask>(
      file_paths_, std::move(shared_files_), kMaxSharedFileBytes,
      base::BindOnce(&SharingServiceOperation::OnStoreFiles,
                     weak_factory_.GetWeakPtr()));

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

void SharingServiceOperation::OnStoreFiles(blink::mojom::ShareError error) {
  if (!web_contents_ || error != blink::mojom::ShareError::OK) {
    PrepareDirectoryTask::ScheduleSharedFileDeletion(std::move(file_paths_),
                                                     base::Minutes(0));
    std::move(callback_).Run(error);
    return;
  }

  GetSharePickerCallback().Run(
      web_contents_.get(), file_paths_, text_, title_, url_,
      base::BindOnce(&SharingServiceOperation::OnShowSharePicker,
                     weak_factory_.GetWeakPtr()));
}

void SharingServiceOperation::OnShowSharePicker(
    blink::mojom::ShareError error) {
  if (file_paths_.size() > 0) {
    PrepareDirectoryTask::ScheduleSharedFileDeletion(std::move(file_paths_),
                                                     base::Minutes(0));
  }
  std::move(callback_).Run(error);
}

// static
void SharingServiceOperation::ShowSharePicker(
    content::WebContents* web_contents,
    const std::vector<base::FilePath>& file_paths,
    const std::string& text,
    const std::string& title,
    const GURL& url,
    blink::mojom::ShareService::ShareCallback callback) {
  std::vector<std::string> file_paths_as_utf8;
  for (const auto& file_path : file_paths) {
    file_paths_as_utf8.emplace_back(file_path.AsUTF8Unsafe());
  }

  web_contents->GetRenderWidgetHostView()->ShowSharePicker(
      title, text, url.spec(), file_paths_as_utf8, std::move(callback));
}

// static
SharingServiceOperation::SharePickerCallback&
SharingServiceOperation::GetSharePickerCallback() {
  static base::NoDestructor<SharePickerCallback> callback(
      base::BindRepeating(&SharingServiceOperation::ShowSharePicker));
  return *callback;
}

}  // namespace webshare