chromium/ash/picker/picker_insert_media.cc

// 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 "ash/picker/picker_insert_media.h"

#include <optional>
#include <string>
#include <utility>
#include <variant>

#include "ash/picker/picker_clipboard_insertion.h"
#include "ash/picker/picker_copy_media.h"
#include "ash/picker/picker_rich_media.h"
#include "ash/public/cpp/picker/picker_web_paste_target.h"
#include "base/base64.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/overloaded.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "net/base/mime_util.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/ime/text_input_type.h"
#include "url/gurl.h"

namespace ash {
namespace {

std::optional<std::string> GetMediaTypeFromFilePath(
    const base::FilePath& path) {
  std::string mime_type;
  if (!net::GetMimeTypeFromFile(path, &mime_type)) {
    return std::nullopt;
  }
  return mime_type;
}

std::optional<std::string> ReadFileToString(const base::FilePath& path) {
  std::string result;
  if (!base::ReadFileToString(path, &result)) {
    LOG(WARNING) << "Failed reading file";
    return std::nullopt;
  }
  return result;
}

void ReadFileAsync(
    base::FilePath path,
    base::OnceCallback<void(std::optional<std::string>)> callback) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
      base::BindOnce(&ReadFileToString, std::move(path)), std::move(callback));
}

std::optional<GURL> ConvertToDataUrl(std::string_view media_type,
                                     std::optional<std::string> data) {
  if (!data.has_value()) {
    return std::nullopt;
  }
  return GURL(base::StrCat(
      {"data:", media_type, ";base64,", base::Base64Encode(*data)}));
}

bool ShouldSkipLinkClipboardInsertion(const GURL& url_of_target) {
  // Google Slides does not correctly handle pasting of links.
  return url_of_target.DomainIs("docs.google.com") &&
         url_of_target.path_piece().starts_with("/presentation/");
}

// Some websites such as https://x.com use a `contenteditable` text field, but
// `<a>` elements are stripped. Inserting
//     <a href="https://example.com">Example</a>
// into these text fields will result in a _plain text_ "Example", without a
// link. As a result, we only insert link titles on a set of allowlisted
// websites. If this returns false, we insert
//     <a title="Example" href="https://example.com">https://example.com</a>
// instead, which, if the `<a>` element is stripped, still inserts the link
// "https://example.com".
//
// TODO: b/337064111 - Determine allowlist for inserting link title.
bool ShouldUseLinkTitle(const GURL& url_of_target) {
  if (url_of_target.DomainIs("google.com")) {
    return !url_of_target.DomainIs("docs.google.com");
  }

  if (url_of_target.DomainIs("onedrive.live.com") ||
      url_of_target.DomainIs("sharepoint.com")) {
    return true;
  }

  return false;
}

void InsertMediaToInputFieldNoClipboard(
    PickerRichMedia media,
    ui::TextInputClient& client,
    OnInsertMediaCompleteCallback callback) {
  std::visit(
      base::Overloaded{
          [&client, &callback](PickerTextMedia media) mutable {
            client.InsertText(media.text,
                              ui::TextInputClient::InsertTextCursorBehavior::
                                  kMoveCursorAfterText);
            std::move(callback).Run(InsertMediaResult::kSuccess);
          },
          [&client, &callback](PickerLinkMedia media) mutable {
            client.InsertText(base::UTF8ToUTF16(media.url.spec()),
                              ui::TextInputClient::InsertTextCursorBehavior::
                                  kMoveCursorAfterText);
            std::move(callback).Run(InsertMediaResult::kSuccess);
          },
          [&client, &callback](PickerLocalFileMedia media) mutable {
            if (!client.CanInsertImage()) {
              std::move(callback).Run(InsertMediaResult::kUnsupported);
              return;
            }

            std::optional<std::string> media_type =
                GetMediaTypeFromFilePath(media.path);
            if (!media_type.has_value()) {
              std::move(callback).Run(InsertMediaResult::kUnsupported);
              return;
            }

            ReadFileAsync(
                media.path,
                base::BindOnce(ConvertToDataUrl, std::move(*media_type))
                    .Then(base::BindOnce(
                        [](base::WeakPtr<ui::TextInputClient> client,
                           OnInsertMediaCompleteCallback callback,
                           std::optional<GURL> url) {
                          if (!url.has_value()) {
                            std::move(callback).Run(
                                InsertMediaResult::kNotFound);
                            return;
                          }
                          client->InsertImage(*url);
                          std::move(callback).Run(InsertMediaResult::kSuccess);
                        },
                        client.AsWeakPtr(), std::move(callback))));
          },
      },
      std::move(media));
}

}  // namespace

bool InputFieldSupportsInsertingMedia(const PickerRichMedia& media,
                                      ui::TextInputClient& client) {
  return std::visit(base::Overloaded{
                        [](const PickerTextMedia& media) { return true; },
                        [](const PickerLinkMedia& media) { return true; },
                        [&client](const PickerLocalFileMedia& media) {
                          return client.CanInsertImage();
                        },
                    },
                    media);
}

void InsertMediaToInputField(PickerRichMedia media,
                             ui::TextInputClient& client,
                             WebPasteTargetCallback get_web_paste_target,
                             OnInsertMediaCompleteCallback callback) {
  if (std::holds_alternative<PickerLinkMedia>(media) &&
      client.GetTextInputType() == ui::TEXT_INPUT_TYPE_CONTENT_EDITABLE) {
    std::optional<PickerWebPasteTarget> web_paste_target =
        get_web_paste_target.is_null() ? std::nullopt
                                       : std::move(get_web_paste_target).Run();
    base::OnceClosure do_paste;
    PickerClipboardDataOptions clipboard_data_options;
    bool skip_clipboard_insertion = false;
    if (web_paste_target.has_value()) {
      do_paste = std::move(web_paste_target->do_paste);
      clipboard_data_options.links_should_use_title =
          ShouldUseLinkTitle(web_paste_target->url);
      skip_clipboard_insertion =
          ShouldSkipLinkClipboardInsertion(web_paste_target->url);
    }

    if (!skip_clipboard_insertion) {
      InsertClipboardData(
          ClipboardDataFromMedia(media, clipboard_data_options),
          std::move(do_paste),
          base::BindOnce(
              [](PickerRichMedia media,
                 base::WeakPtr<ui::TextInputClient> client,
                 OnInsertMediaCompleteCallback callback, bool success) {
                if (success) {
                  std::move(callback).Run(InsertMediaResult::kSuccess);
                  return;
                }
                if (client == nullptr) {
                  std::move(callback).Run(InsertMediaResult::kUnsupported);
                  return;
                }
                InsertMediaToInputFieldNoClipboard(std::move(media), *client,
                                                   std::move(callback));
              },
              std::move(media), client.AsWeakPtr(), std::move(callback)));
      return;
    }
  }

  InsertMediaToInputFieldNoClipboard(std::move(media), client,
                                     std::move(callback));
}

}  // namespace ash