| // 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/ui/ash/thumbnail_loader.h" |
| |
| #include <algorithm> |
| #include <optional> |
| #include <utility> |
| |
| #include "ash/public/cpp/image_downloader.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/json/values_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/values.h" |
| #include "chrome/browser/ash/file_manager/app_id.h" |
| #include "chrome/browser/ash/file_manager/fileapi_util.h" |
| #include "chrome/browser/extensions/api/messaging/native_message_port.h" |
| #include "chrome/browser/image_decoder/image_decoder.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "extensions/browser/api/messaging/channel_endpoint.h" |
| #include "extensions/browser/api/messaging/message_service.h" |
| #include "extensions/browser/api/messaging/native_message_host.h" |
| #include "extensions/common/api/messaging/messaging_endpoint.h" |
| #include "extensions/common/api/messaging/port_id.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/mojom/message_port.mojom-shared.h" |
| #include "net/base/data_url.h" |
| #include "net/base/mime_util.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/data_decoder/public/cpp/data_decoder.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "storage/browser/file_system/file_system_context.h" |
| #include "third_party/re2/src/re2/re2.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/geometry/skia_conversions.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "url/gurl.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // The native host name that will identify the thumbnail loader to the image |
| // loader extension. |
| constexpr char kNativeMessageHostName[] = "com.google.ash_thumbnail_loader"; |
| |
| // Returns whether the given `file_path` is supported by the `ThumbnailLoader`. |
| bool IsSupported(const base::FilePath& file_path) { |
| constexpr std::array<std::pair<const char*, const char*>, 25> |
| kFileMatchPatterns = {{ |
| // Document types ---------------------------------------------------- |
| { |
| /*extension=*/"(?i)\\.pdf$", |
| /*mime_type=*/"(?i)application\\/pdf", |
| }, |
| // Image types ------------------------------------------------------- |
| { |
| /*extension=*/"(?i)\\.jpe?g$", |
| /*mime_type=*/"(?i)image\\/jpeg", |
| }, |
| { |
| /*extension=*/"(?i)\\.bmp$", |
| /*mime_type=*/"(?i)image\\/bmp", |
| }, |
| { |
| /*extension=*/"(?i)\\.gif$", |
| /*mime_type=*/"(?i)image\\/gif", |
| }, |
| { |
| /*extension=*/"(?i)\\.ico$", |
| /*mime_type=*/"(?i)image\\/x\\-icon", |
| }, |
| { |
| /*extension=*/"(?i)\\.png$", |
| /*mime_type=*/"(?i)image\\/png", |
| }, |
| { |
| /*extension=*/"(?i)\\.webp$", |
| /*mime_type=*/"(?i)image\\/webp", |
| }, |
| { |
| /*extension=*/"(?i)\\.tiff?$", |
| /*mime_type=*/"(?i)image\\/tiff", |
| }, |
| { |
| /*extension=*/"(?i)\\.svg$", |
| /*mime_type=*/"(?i)image\\/svg\\+xml", |
| }, |
| { |
| /*extension=*/"(?i)\\.avif$", |
| /*mime_type=*/"(?i)image\\/avif", |
| }, |
| // Raw types --------------------------------------------------------- |
| { |
| /*extension=*/"(?i)\\.arw$", |
| /*mime_type=*/nullptr, |
| }, |
| { |
| /*extension=*/"(?i)\\.cr2$", |
| /*mime_type=*/nullptr, |
| }, |
| { |
| /*extension=*/"(?i)\\.dng$", |
| /*mime_type=*/nullptr, |
| }, |
| { |
| /*extension=*/"(?i)\\.nef$", |
| /*mime_type=*/nullptr, |
| }, |
| { |
| /*extension=*/"(?i)\\.nrw$", |
| /*mime_type=*/nullptr, |
| }, |
| { |
| /*extension=*/"(?i)\\.orf$", |
| /*mime_type=*/nullptr, |
| }, |
| { |
| /*extension=*/"(?i)\\.raf$", |
| /*mime_type=*/nullptr, |
| }, |
| { |
| /*extension=*/"(?i)\\.rw2$", |
| /*mime_type=*/nullptr, |
| }, |
| // Video types ------------------------------------------------------- |
| { |
| /*extension=*/"(?i)\\.3gpp?$", |
| /*mime_type=*/"(?i)video\\/3gpp", |
| }, |
| { |
| /*extension=*/"(?i)\\.avi$", |
| /*mime_type=*/"(?i)video\\/x\\-msvideo", |
| }, |
| { |
| /*extension=*/"(?i)\\.mov$", |
| /*mime_type=*/"(?i)video\\/quicktime", |
| }, |
| { |
| /*extension=*/"\\.mkv$", |
| /*mime_type=*/"video\\/x\\-matroska", |
| }, |
| { |
| /*extension=*/"(?i)\\.m(p4|4v|pg|peg|pg4|peg4)$", |
| /*mime_type=*/"(?i)video\\/mp(4|eg)", |
| }, |
| { |
| /*extension=*/"(?i)\\.og(m|v|x)$", |
| /*mime_type=*/"(?i)(application|video)\\/ogg", |
| }, |
| { |
| /*extension=*/"(?i)\\.webm$", |
| /*mime_type=*/"(?i)video\\/webm", |
| }, |
| }}; |
| |
| // First attempt to match based on `mime_type`. |
| std::string ext = file_path.Extension(); |
| std::string mime_type; |
| if (!ext.empty() && |
| net::GetWellKnownMimeTypeFromExtension(ext.substr(1), &mime_type)) { |
| for (const auto& file_match_pattern : kFileMatchPatterns) { |
| if (file_match_pattern.second && |
| re2::RE2::FullMatch(mime_type, file_match_pattern.second)) { |
| return true; |
| } |
| } |
| } |
| |
| // Then attempt to match based on `file_path` extension. |
| for (const auto& file_match_pattern : kFileMatchPatterns) { |
| if (re2::RE2::FullMatch(file_path.Extension(), file_match_pattern.first)) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| using ThumbnailDataCallback = base::OnceCallback<void(const std::string& data)>; |
| |
| // Handles a parsed message sent from image loader extension in response to a |
| // thumbnail request. |
| void HandleParsedThumbnailResponse( |
| const std::string& request_id, |
| ThumbnailDataCallback callback, |
| data_decoder::DataDecoder::ValueOrError result) { |
| if (!result.has_value()) { |
| VLOG(2) << "Failed to parse request response " << result.error(); |
| std::move(callback).Run(""); |
| return; |
| } |
| |
| if (!result->is_dict()) { |
| VLOG(2) << "Invalid response format"; |
| std::move(callback).Run(""); |
| return; |
| } |
| |
| const std::string* received_request_id = |
| result->GetDict().FindString("taskId"); |
| const std::string* data = result->GetDict().FindString("data"); |
| |
| if (!data || !received_request_id || *received_request_id != request_id) { |
| std::move(callback).Run(""); |
| return; |
| } |
| |
| std::move(callback).Run(*data); |
| } |
| |
| // Native message host for communication to the image loader extension. |
| // It handles a single image request - when the connection to the extension is |
| // established, it send a message containing an image request to the image |
| // loader. It closes the connection once it receives a response from the image |
| // loader. |
| class ThumbnailLoaderNativeMessageHost : public extensions::NativeMessageHost { |
| public: |
| ThumbnailLoaderNativeMessageHost(const std::string& request_id, |
| const std::string& message, |
| ThumbnailDataCallback callback) |
| : request_id_(request_id), |
| message_(message), |
| callback_(std::move(callback)) {} |
| |
| ~ThumbnailLoaderNativeMessageHost() override { |
| if (callback_) |
| std::move(callback_).Run(""); |
| } |
| |
| void OnMessage(const std::string& message) override { |
| if (response_received_) |
| return; |
| response_received_ = true; |
| |
| // Detach the callback from the message host in case the extension closes |
| // connection by the time the response is parsed. |
| data_decoder::DataDecoder::ParseJsonIsolated( |
| message, base::BindOnce(&HandleParsedThumbnailResponse, request_id_, |
| std::move(callback_))); |
| |
| client_->CloseChannel(""); |
| client_ = nullptr; |
| } |
| |
| void Start(Client* client) override { |
| client_ = client; |
| client_->PostMessageFromNativeHost(message_); |
| } |
| |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner() const override { |
| return task_runner_; |
| } |
| |
| private: |
| const std::string request_id_; |
| const std::string message_; |
| ThumbnailDataCallback callback_; |
| |
| raw_ptr<Client> client_ = nullptr; |
| |
| bool response_received_ = false; |
| |
| const scoped_refptr<base::SingleThreadTaskRunner> task_runner_ = |
| base::SingleThreadTaskRunner::GetCurrentDefault(); |
| }; |
| |
| } // namespace |
| |
| // Converts a data URL to bitmap. |
| class ThumbnailLoader::ThumbnailDecoder : public ImageDecoder::ImageRequest { |
| public: |
| ThumbnailDecoder() = default; |
| |
| ThumbnailDecoder(const ThumbnailDecoder&) = delete; |
| ThumbnailDecoder& operator=(const ThumbnailDecoder&) = delete; |
| ~ThumbnailDecoder() override = default; |
| |
| // ImageDecoder::ImageRequest: |
| void OnImageDecoded(const SkBitmap& bitmap) override { |
| std::move(callback_).Run(&bitmap, base::File::FILE_OK); |
| } |
| |
| // ImageDecoder::ImageRequest: |
| void OnDecodeImageFailed() override { |
| std::move(callback_).Run(/*bitmap=*/nullptr, base::File::FILE_ERROR_FAILED); |
| } |
| |
| void Start(const std::string& data, ThumbnailLoader::ImageCallback callback) { |
| DCHECK(!callback_); |
| |
| // The data sent from the image loader extension should be in form of a data |
| // URL. |
| GURL data_url(data); |
| if (!data_url.is_valid() || !data_url.SchemeIs(url::kDataScheme)) { |
| std::move(callback).Run(/*bitmap=*/nullptr, |
| base::File::FILE_ERROR_FAILED); |
| return; |
| } |
| |
| std::string mime_type, charset, image_data; |
| if (!net::DataURL::Parse(data_url, &mime_type, &charset, &image_data)) { |
| std::move(callback).Run(/*bitmap=*/nullptr, |
| base::File::FILE_ERROR_FAILED); |
| return; |
| } |
| |
| callback_ = std::move(callback); |
| ImageDecoder::Start(this, std::move(image_data)); |
| } |
| |
| private: |
| ThumbnailLoader::ImageCallback callback_; |
| }; |
| |
| ThumbnailLoader::ThumbnailLoader(Profile* profile) : profile_(profile) {} |
| |
| ThumbnailLoader::~ThumbnailLoader() { |
| // Run any pending callbacks to clean them up. |
| for (auto it = requests_.begin(); it != requests_.end();) { |
| std::move(it->second).Run(nullptr, base::File::Error::FILE_ERROR_ABORT); |
| it = requests_.erase(it); |
| } |
| } |
| |
| ThumbnailLoader::ThumbnailRequest::ThumbnailRequest( |
| const base::FilePath& file_path, |
| const gfx::Size& size) |
| : file_path(file_path), size(size) {} |
| |
| ThumbnailLoader::ThumbnailRequest::~ThumbnailRequest() = default; |
| |
| base::WeakPtr<ThumbnailLoader> ThumbnailLoader::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void ThumbnailLoader::Load(const ThumbnailRequest& request, |
| ImageCallback callback) { |
| // Get the file's last modified time - this will be used for cache lookup in |
| // the image loader extension. |
| GURL source_url = extensions::Extension::GetBaseURLFromExtensionId( |
| file_manager::kImageLoaderExtensionId); |
| file_manager::util::GetMetadataForPath( |
| file_manager::util::GetFileSystemContextForSourceURL(profile_, |
| source_url), |
| request.file_path, |
| {storage::FileSystemOperation::GetMetadataField::kIsDirectory, |
| storage::FileSystemOperation::GetMetadataField::kLastModified}, |
| base::BindOnce(&ThumbnailLoader::LoadForFileWithMetadata, |
| weak_factory_.GetWeakPtr(), request, std::move(callback))); |
| } |
| |
| void ThumbnailLoader::LoadForFileWithMetadata( |
| const ThumbnailRequest& request, |
| ImageCallback callback, |
| base::File::Error result, |
| const base::File::Info& file_info) { |
| if (result != base::File::FILE_OK) { |
| std::move(callback).Run(/*bitmap=*/nullptr, result); |
| return; |
| } |
| |
| // Short-circuit icons for folders. |
| if (file_info.is_directory) { |
| // `FILE_ERROR_NOT_A_FILE` is a special value used to signify that the |
| // file for which the thumbnail was requested is actually a folder. |
| std::move(callback).Run(/*bitmap=*/nullptr, |
| base::File::FILE_ERROR_NOT_A_FILE); |
| return; |
| } |
| |
| // Short-circuit if unsupported. |
| if (!IsSupported(request.file_path)) { |
| std::move(callback).Run(/*bitmap=*/nullptr, base::File::FILE_ERROR_ABORT); |
| return; |
| } |
| |
| GURL thumbnail_url; |
| if (!file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl( |
| profile_, request.file_path, |
| extensions::Extension::GetBaseURLFromExtensionId( |
| file_manager::kImageLoaderExtensionId), |
| &thumbnail_url)) { |
| std::move(callback).Run(/*bitmap=*/nullptr, base::File::FILE_ERROR_FAILED); |
| return; |
| } |
| |
| extensions::MessageService* const message_service = |
| extensions::MessageService::Get(profile_); |
| if (!message_service) { // May be `nullptr` in tests. |
| std::move(callback).Run(/*bitmap=*/nullptr, base::File::FILE_ERROR_FAILED); |
| return; |
| } |
| |
| base::UnguessableToken request_id = base::UnguessableToken::Create(); |
| requests_[request_id] = std::move(callback); |
| |
| // Unfortunately the image loader only supports cropping to square dimensions |
| // but a request for a non-cropped, non-square image would result in image |
| // distortion. To work around this we always request square images and then |
| // crop to requested dimensions on our end if necessary after bitmap decoding. |
| const int size = std::max(request.size.width(), request.size.height()); |
| |
| // Generate an image loader request. The request type is defined in |
| // ui/file_manager/image_loader/load_image_request.js. |
| base::Value::Dict request_dict; |
| request_dict.Set("taskId", base::Value(request_id.ToString())); |
| request_dict.Set("url", base::Value(thumbnail_url.spec())); |
| request_dict.Set("timestamp", base::TimeToValue(file_info.last_modified)); |
| // TODO(crbug.com/2650014) : Add an arg to set this to false for sharesheet. |
| request_dict.Set("cache", true); |
| request_dict.Set("crop", true); |
| request_dict.Set("priority", base::Value(1)); |
| request_dict.Set("width", base::Value(size)); |
| request_dict.Set("height", base::Value(size)); |
| |
| std::string request_message; |
| base::JSONWriter::Write(request_dict, &request_message); |
| |
| // Open a channel to the image loader extension using a message host that send |
| // the image loader request. |
| auto native_message_host = std::make_unique<ThumbnailLoaderNativeMessageHost>( |
| request_id.ToString(), request_message, |
| base::BindOnce(&ThumbnailLoader::OnThumbnailLoaded, |
| weak_factory_.GetWeakPtr(), request_id, request.size)); |
| const extensions::PortId port_id( |
| base::UnguessableToken::Create(), 1 /* port_number */, |
| true /* is_opener */, extensions::mojom::SerializationFormat::kJson); |
| auto native_message_port = std::make_unique<extensions::NativeMessagePort>( |
| message_service->GetChannelDelegate(), port_id, |
| std::move(native_message_host)); |
| message_service->OpenChannelToExtension( |
| extensions::ChannelEndpoint(profile_), port_id, |
| extensions::MessagingEndpoint::ForNativeApp(kNativeMessageHostName), |
| std::move(native_message_port), file_manager::kImageLoaderExtensionId, |
| GURL(), extensions::mojom::ChannelType::kNative, |
| std::string() /* channel_name */); |
| } |
| |
| void ThumbnailLoader::OnThumbnailLoaded( |
| const base::UnguessableToken& request_id, |
| const gfx::Size& requested_size, |
| const std::string& data) { |
| if (!requests_.count(request_id)) |
| return; |
| |
| if (data.empty()) { |
| RespondToRequest(request_id, requested_size, /*bitmap=*/nullptr, |
| base::File::FILE_ERROR_FAILED); |
| return; |
| } |
| |
| auto thumbnail_decoder = std::make_unique<ThumbnailDecoder>(); |
| ThumbnailDecoder* thumbnail_decoder_ptr = thumbnail_decoder.get(); |
| thumbnail_decoders_.emplace(request_id, std::move(thumbnail_decoder)); |
| thumbnail_decoder_ptr->Start( |
| data, |
| base::BindOnce(&ThumbnailLoader::RespondToRequest, |
| weak_factory_.GetWeakPtr(), request_id, requested_size)); |
| } |
| |
| void ThumbnailLoader::RespondToRequest(const base::UnguessableToken& request_id, |
| const gfx::Size& requested_size, |
| const SkBitmap* bitmap, |
| base::File::Error error) { |
| thumbnail_decoders_.erase(request_id); |
| auto request_it = requests_.find(request_id); |
| if (request_it == requests_.end()) |
| return; |
| |
| // To work around cropping limitations of the image loader, we requested a |
| // square image. If requested dimensions were non-square, we need to perform |
| // additional cropping on our end. |
| SkBitmap cropped_bitmap; |
| if (bitmap) { |
| gfx::Rect cropped_rect(0, 0, bitmap->width(), bitmap->height()); |
| if (cropped_rect.size() != requested_size) { |
| cropped_bitmap = *bitmap; |
| cropped_rect.ClampToCenteredSize(requested_size); |
| bitmap->extractSubset(&cropped_bitmap, gfx::RectToSkIRect(cropped_rect)); |
| } |
| } |
| |
| ImageCallback callback = std::move(request_it->second); |
| requests_.erase(request_it); |
| std::move(callback).Run(cropped_bitmap.isNull() ? bitmap : &cropped_bitmap, |
| error); |
| } |
| |
| } // namespace ash |