blob: 5bf8cc33204b5a7c04e4683a09a83dc17afa5f30 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/web/image_fetch_tab_helper.h"
#include "base/base64.h"
#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/values.h"
#include "components/image_fetcher/ios/ios_image_data_fetcher_wrapper.h"
#include "ios/web/public/browser_state.h"
#include "ios/web/public/referrer_util.h"
#import "ios/web/public/web_state/navigation_context.h"
#include "ios/web/public/web_task_traits.h"
#include "ios/web/public/web_thread.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
const char kUmaGetImageDataByJsResult[] =
"ContextMenu.iOS.GetImageDataByJsResult";
namespace {
// Command prefix for injected JavaScript.
const char kCommandPrefix[] = "imageFetch";
// Key for image_fetcher
const char kImageFetcherKeyName[] = "0";
// Timeout for GetImageDataByJs in milliseconds.
const int kGetImageDataByJsTimeout = 300;
// Wrapper class for image_fetcher::IOSImageDataFetcherWrapper. ImageFetcher is
// attached to web::BrowserState instead of web::WebState, because if a user
// closes the tab immediately after Copy/Save image, the web::WebState will be
// destroyed thus fail the download.
class ImageFetcher : public image_fetcher::IOSImageDataFetcherWrapper,
public base::SupportsUserData::Data {
public:
~ImageFetcher() override = default;
ImageFetcher(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: image_fetcher::IOSImageDataFetcherWrapper(url_loader_factory) {}
static ImageFetcher* FromBrowserState(web::BrowserState* browser_state) {
if (!browser_state->GetUserData(&kImageFetcherKeyName)) {
browser_state->SetUserData(
&kImageFetcherKeyName,
std::make_unique<ImageFetcher>(
browser_state->GetSharedURLLoaderFactory()));
}
return static_cast<ImageFetcher*>(
browser_state->GetUserData(&kImageFetcherKeyName));
}
DISALLOW_COPY_AND_ASSIGN(ImageFetcher);
};
}
ImageFetchTabHelper::ImageFetchTabHelper(web::WebState* web_state)
: web_state_(web_state), weak_ptr_factory_(this) {
web_state->AddObserver(this);
// BindRepeating cannot work on WeakPtr and function with return value, use
// lambda as mediator.
web_state->AddScriptCommandCallback(
base::BindRepeating(
[](base::WeakPtr<ImageFetchTabHelper> ptr,
const base::DictionaryValue& message, const GURL& page_url,
bool has_user_gesture, bool form_in_main_frame,
web::WebFrame* sender_frame) {
return ptr ? ptr->OnJsMessage(message) : true;
},
weak_ptr_factory_.GetWeakPtr()),
kCommandPrefix);
}
ImageFetchTabHelper::~ImageFetchTabHelper() = default;
void ImageFetchTabHelper::DidStartNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
if (navigation_context->IsSameDocument()) {
return;
}
for (auto&& pair : js_callbacks_)
std::move(pair.second).Run(nullptr);
js_callbacks_.clear();
}
void ImageFetchTabHelper::WebStateDestroyed(web::WebState* web_state) {
web_state->RemoveScriptCommandCallback(kCommandPrefix);
for (auto&& pair : js_callbacks_)
std::move(pair.second).Run(nullptr);
web_state->RemoveObserver(this);
web_state_ = nullptr;
}
void ImageFetchTabHelper::GetImageData(const GURL& url,
const web::Referrer& referrer,
ImageDataCallback callback) {
// |this| is captured into the callback of GetImageDataByJs, which will always
// be invoked before the |this| is destroyed, so it's safe.
GetImageDataByJs(
url, base::TimeDelta::FromMilliseconds(kGetImageDataByJsTimeout),
base::BindOnce(&ImageFetchTabHelper::JsCallbackOfGetImageData,
base::Unretained(this), url, referrer, callback));
}
void ImageFetchTabHelper::JsCallbackOfGetImageData(
const GURL& url,
const web::Referrer& referrer,
ImageDataCallback callback,
const std::string* data) {
if (data) {
callback([NSData dataWithBytes:data->c_str() length:data->size()]);
return;
}
ImageFetcher::FromBrowserState(web_state_->GetBrowserState())
->FetchImageDataWebpDecoded(
url,
^(NSData* data, const image_fetcher::RequestMetadata& metadata) {
callback(data);
},
web::ReferrerHeaderValueForNavigation(url, referrer),
web::PolicyForNavigation(url, referrer), /*send_cookies=*/true);
}
void ImageFetchTabHelper::GetImageDataByJs(const GURL& url,
base::TimeDelta timeout,
JsCallback&& callback) {
++call_id_;
DCHECK_EQ(js_callbacks_.count(call_id_), 0UL);
js_callbacks_.insert({call_id_, std::move(callback)});
base::PostDelayedTaskWithTraits(
FROM_HERE, {web::WebThread::UI},
base::BindRepeating(&ImageFetchTabHelper::OnJsTimeout,
weak_ptr_factory_.GetWeakPtr(), call_id_),
timeout);
std::string js =
base::StringPrintf("__gCrWeb.imageFetch.getImageData(%d, '%s')", call_id_,
url.spec().c_str());
web_state_->ExecuteJavaScript(base::UTF8ToUTF16(js));
}
void ImageFetchTabHelper::RecordGetImageDataByJsResult(
ContextMenuGetImageDataByJsResult result) {
UMA_HISTOGRAM_ENUMERATION(kUmaGetImageDataByJsResult, result);
}
// The expected message from JavaScript has format:
//
// For success:
// {'command': 'image.getImageData',
// 'id': id_sent_to_gCrWeb_image_getImageData,
// 'data': image_data_in_base64,
// 'from': 'canvas' or 'xhr'}
//
// For failure:
// {'command': 'image.getImageData',
// 'id': id_sent_to_gCrWeb_image_getImageData}
bool ImageFetchTabHelper::OnJsMessage(const base::DictionaryValue& message) {
const base::Value* id_key = message.FindKey("id");
if (!id_key || !id_key->is_double()) {
return false;
}
int id_value = static_cast<int>(id_key->GetDouble());
if (!js_callbacks_.count(id_value)) {
return true;
}
JsCallback callback = std::move(js_callbacks_[id_value]);
js_callbacks_.erase(id_value);
const base::Value* data = message.FindKey("data");
std::string decoded_data;
if (data && data->is_string() && !data->GetString().empty() &&
base::Base64Decode(data->GetString(), &decoded_data)) {
std::move(callback).Run(&decoded_data);
const base::Value* from = message.FindKey("from");
if (from && from->is_string() &&
(from->GetString() == "canvas" || from->GetString() == "xhr")) {
RecordGetImageDataByJsResult(
(from->GetString() == "canvas")
? ContextMenuGetImageDataByJsResult::kCanvasSucceed
: ContextMenuGetImageDataByJsResult::kXMLHttpRequestSucceed);
} else {
return false;
}
} else {
std::move(callback).Run(nullptr);
RecordGetImageDataByJsResult(ContextMenuGetImageDataByJsResult::kFail);
}
return true;
}
void ImageFetchTabHelper::OnJsTimeout(int call_id) {
if (js_callbacks_.count(call_id)) {
JsCallback callback = std::move(js_callbacks_[call_id]);
js_callbacks_.erase(call_id);
std::move(callback).Run(nullptr);
RecordGetImageDataByJsResult(ContextMenuGetImageDataByJsResult::kTimeout);
}
}