blob: 9cabe26205096ee5e21b144c267c1295f4d3e1a6 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/web/js_messaging/web_frame_impl.h"
#import <Foundation/Foundation.h>
#import "base/check.h"
#import "base/debug/crash_logging.h"
#import "base/debug/dump_without_crashing.h"
#import "base/feature_list.h"
#import "base/functional/bind.h"
#import "base/ios/ios_util.h"
#import "base/json/json_writer.h"
#import "base/logging.h"
#import "base/metrics/histogram_macros.h"
#import "base/strings/string_split.h"
#import "base/strings/string_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/values.h"
#import "ios/web/common/features.h"
#import "ios/web/js_messaging/java_script_content_world.h"
#import "ios/web/js_messaging/java_script_feature_manager.h"
#import "ios/web/js_messaging/web_view_js_utils.h"
#import "ios/web/public/js_messaging/web_view_js_utils.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "net/base/apple/url_conversions.h"
#import "url/gurl.h"
namespace {
// Creates a JavaScript string for executing the function
// __gCrWeb.callFunctionInGcrWeb with parameters api_name, func_or_prop_name and
// args.
NSString* CreateFunctionCallWithParameters(
const std::string& name,
const base::Value::List& parameters) {
NSMutableArray* parameter_strings = [[NSMutableArray alloc] init];
for (const auto& value : parameters) {
std::string string_value;
base::JSONWriter::Write(value, &string_value);
[parameter_strings addObject:base::SysUTF8ToNSString(string_value)];
}
// Assuming 'name' comes in the format "api.functionName" or
// "api.propertyName"
std::optional<std::pair<std::string_view, std::string_view>> name_parts =
base::SplitStringOnce(name, ".");
std::string_view api_name;
std::string_view function_name;
if (name_parts) {
api_name = name_parts->first;
function_name = name_parts->second;
} else {
api_name = "";
function_name = name;
}
return [NSString
stringWithFormat:@"__gCrWeb.callFunctionInGcrWeb(\"%s\", \"%s\", [%@])",
std::string(api_name).c_str(),
std::string(function_name).c_str(),
[parameter_strings componentsJoinedByString:@","]];
}
// The NSError message returned for frames which can not execute JavaScript.
// This string is used to filter these errors because they share a more general
// error code `WKErrorJavaScriptExceptionOccurred`.
const NSString* kCannotExecuteJSInDocumentErrorMessage =
@"Cannot execute JavaScript in this document";
} // namespace
namespace web {
const double kJavaScriptFunctionCallDefaultTimeout = 100.0;
WebFrameImpl::WebFrameImpl(WKFrameInfo* frame_info,
const std::string& frame_id,
bool is_main_frame,
url::Origin security_origin,
web::WebState* web_state,
ContentWorld content_world)
: frame_info_(frame_info),
frame_id_(base::ToLowerASCII(frame_id)),
is_main_frame_(is_main_frame),
security_origin_(security_origin),
web_state_(web_state),
content_world_(content_world) {
DCHECK(frame_info_);
DCHECK(web_state_);
web_state->AddObserver(this);
}
WebFrameImpl::~WebFrameImpl() {
CancelPendingRequests();
DetachFromWebState();
}
WebFrameInternal* WebFrameImpl::GetWebFrameInternal() {
return this;
}
WebState* WebFrameImpl::GetWebState() {
return web_state_;
}
std::string WebFrameImpl::GetFrameId() const {
return frame_id_;
}
bool WebFrameImpl::IsMainFrame() const {
return is_main_frame_;
}
url::Origin WebFrameImpl::GetSecurityOrigin() const {
return security_origin_;
}
GURL WebFrameImpl::GetUrl() const {
return net::GURLWithNSURL(frame_info_.request.URL);
}
BrowserState* WebFrameImpl::GetBrowserState() {
return GetWebState()->GetBrowserState();
}
bool WebFrameImpl::CallJavaScriptFunctionInContentWorld(
const std::string& name,
const base::Value::List& parameters,
JavaScriptContentWorld* content_world,
bool reply_with_result) {
int message_id = next_message_id_;
next_message_id_++;
if (content_world && content_world->GetWKContentWorld()) {
return ExecuteJavaScriptFunction(content_world, name, parameters,
message_id, reply_with_result);
}
return false;
}
bool WebFrameImpl::CallJavaScriptFunction(const std::string& name,
const base::Value::List& parameters) {
JavaScriptContentWorld* content_world =
JavaScriptFeatureManager::GetContentWorldForBrowserState(
content_world_, GetBrowserState());
return CallJavaScriptFunctionInContentWorld(name, parameters, content_world,
/*reply_with_result=*/false);
}
bool WebFrameImpl::CallJavaScriptFunctionInContentWorld(
const std::string& name,
const base::Value::List& parameters,
JavaScriptContentWorld* content_world) {
return CallJavaScriptFunctionInContentWorld(name, parameters, content_world,
/*reply_with_result=*/false);
}
bool WebFrameImpl::CallJavaScriptFunction(
const std::string& name,
const base::Value::List& parameters,
base::OnceCallback<void(const base::Value*)> callback,
base::TimeDelta timeout) {
JavaScriptContentWorld* content_world =
JavaScriptFeatureManager::GetContentWorldForBrowserState(
content_world_, GetBrowserState());
return CallJavaScriptFunctionInContentWorld(name, parameters, content_world,
std::move(callback), timeout);
}
bool WebFrameImpl::CallJavaScriptFunctionInContentWorld(
const std::string& name,
const base::Value::List& parameters,
JavaScriptContentWorld* content_world,
base::OnceCallback<void(const base::Value*)> callback,
base::TimeDelta timeout) {
int message_id = next_message_id_;
auto timeout_callback = std::make_unique<TimeoutCallback>(base::BindOnce(
&WebFrameImpl::CancelRequest, base::Unretained(this), message_id));
auto callbacks = std::make_unique<struct RequestCallbacks>(
std::move(callback), std::move(timeout_callback));
pending_requests_[message_id] = std::move(callbacks);
web::GetUIThreadTaskRunner({})->PostDelayedTask(
FROM_HERE, pending_requests_[message_id]->timeout_callback->callback(),
timeout);
bool called =
CallJavaScriptFunctionInContentWorld(name, parameters, content_world,
/*reply_with_result=*/true);
if (!called) {
// Remove callbacks if the call failed.
auto request = pending_requests_.find(message_id);
if (request != pending_requests_.end()) {
pending_requests_.erase(request);
}
}
return called;
}
bool WebFrameImpl::ExecuteJavaScript(const std::u16string& script) {
return ExecuteJavaScript(script,
base::DoNothingAs<void(const base::Value*)>());
}
bool WebFrameImpl::ExecuteJavaScript(
const std::u16string& script,
base::OnceCallback<void(const base::Value*)> callback) {
ExecuteJavaScriptCallbackWithError callback_with_error =
ExecuteJavaScriptCallbackAdapter(std::move(callback));
return ExecuteJavaScript(script, std::move(callback_with_error));
}
bool WebFrameImpl::ExecuteJavaScript(
const std::u16string& script,
ExecuteJavaScriptCallbackWithError callback) {
JavaScriptContentWorld* content_world =
JavaScriptFeatureManager::GetContentWorldForBrowserState(
content_world_, GetBrowserState());
return ExecuteJavaScriptInContentWorld(script, content_world,
std::move(callback));
}
base::WeakPtr<WebFrame> WebFrameImpl::AsWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
bool WebFrameImpl::ExecuteJavaScriptInContentWorld(
const std::u16string& script,
JavaScriptContentWorld* content_world,
ExecuteJavaScriptCallbackWithError callback) {
DCHECK(frame_info_);
NSString* ns_script = base::SysUTF16ToNSString(script);
__block auto internal_callback = std::move(callback);
void (^completion_handler)(id, NSError*) = ^void(id value, NSError* error) {
if (error) {
LogScriptWarning(ns_script, error);
std::move(internal_callback).Run(nullptr, error);
} else {
std::move(internal_callback)
.Run(ValueResultFromWKResult(value).get(), nil);
}
};
web::ExecuteJavaScript(frame_info_.webView,
content_world->GetWKContentWorld(), frame_info_,
ns_script, completion_handler);
return true;
}
ExecuteJavaScriptCallbackWithError
WebFrameImpl::ExecuteJavaScriptCallbackAdapter(
base::OnceCallback<void(const base::Value*)> callback) {
// Because blocks treat scoped-variables
// as const, we have to redefine the callback with the
// __block keyword to be able to run the callback inside
// the completion handler.
__block auto internal_callback = std::move(callback);
return base::BindOnce(^(const base::Value* value, NSError* error) {
if (!error) {
std::move(internal_callback).Run(value);
}
});
}
void WebFrameImpl::LogScriptWarning(NSString* script, NSError* error) {
std::u16string executed_script = base::SysNSStringToUTF16(script);
std::u16string error_string =
base::SysNSStringToUTF16(error.userInfo[NSLocalizedDescriptionKey]);
NSString* ns_exception = error.userInfo[@"WKJavaScriptExceptionMessage"];
std::u16string exception = base::SysNSStringToUTF16(ns_exception);
DLOG(WARNING) << "Script execution of:" << executed_script
<< "\nfailed with error: " << error_string
<< "\nand exception: " << exception;
if (base::FeatureList::IsEnabled(features::kAssertOnJavaScriptErrors)) {
CHECK(false)
<< "JavaScript error occurred with kAssertOnJavaScriptErrors enabled.";
}
UMA_HISTOGRAM_BOOLEAN("IOS.JavaScript.ScriptExecutionFailed", true);
if (!base::FeatureList::IsEnabled(features::kLogJavaScriptErrors)) {
return;
}
// Do not log invalid target frame errors. This error means that the frame is
// no longer valid. This is an expected failure state as native code only has
// an outdated view of the web frames (updated asyncronously via JS messages
// or navigation callbacks).
if (error.domain == WKErrorDomain &&
error.code == WKErrorJavaScriptInvalidFrameTarget) {
return;
}
// Some frames do not allow JavaScript execution, there is no need to report
// this as an error as it is an expected case.
if (error.domain == WKErrorDomain &&
[kCannotExecuteJSInDocumentErrorMessage isEqualToString:ns_exception]) {
return;
}
SCOPED_CRASH_KEY_STRING256("JavaScript", "script",
base::UTF16ToUTF8(executed_script));
SCOPED_CRASH_KEY_STRING256("JavaScript", "error",
base::UTF16ToUTF8(error_string));
SCOPED_CRASH_KEY_STRING256("JavaScript", "exception",
base::UTF16ToUTF8(exception));
base::debug::DumpWithoutCrashing();
}
bool WebFrameImpl::ExecuteJavaScriptFunction(
JavaScriptContentWorld* content_world,
const std::string& name,
const base::Value::List& parameters,
int message_id,
bool reply_with_result) {
DCHECK(content_world);
DCHECK(frame_info_);
NSString* script = CreateFunctionCallWithParameters(name, parameters);
void (^completion_handler)(id, NSError*) = nil;
if (reply_with_result) {
base::WeakPtr<WebFrameImpl> weak_frame = weak_ptr_factory_.GetWeakPtr();
completion_handler = ^void(id value, NSError* error) {
if (error) {
LogScriptWarning(script, error);
}
if (weak_frame) {
weak_frame->CompleteRequest(message_id,
ValueResultFromWKResult(value).get());
}
};
}
WKContentWorld* world = content_world->GetWKContentWorld();
DCHECK(world);
web::ExecuteJavaScript(frame_info_.webView, world, frame_info_, script,
completion_handler);
return true;
}
void WebFrameImpl::CompleteRequest(int message_id, const base::Value* result) {
auto request = pending_requests_.find(message_id);
if (request == pending_requests_.end()) {
return;
}
CompleteRequest(std::move(request->second), result);
pending_requests_.erase(request);
}
void WebFrameImpl::CompleteRequest(
std::unique_ptr<RequestCallbacks> request_callbacks,
const base::Value* result) {
request_callbacks->timeout_callback->Cancel();
std::move(request_callbacks->completion).Run(result);
}
void WebFrameImpl::CancelRequest(int message_id) {
CompleteRequest(message_id, /*result=*/nullptr);
}
void WebFrameImpl::CancelPendingRequests() {
for (auto& it : pending_requests_) {
CompleteRequest(std::move(it.second), /*result=*/nullptr);
}
pending_requests_.clear();
}
void WebFrameImpl::DetachFromWebState() {
if (web_state_) {
web_state_->RemoveObserver(this);
web_state_ = nullptr;
}
}
void WebFrameImpl::WebStateDestroyed(web::WebState* web_state) {
CancelPendingRequests();
DetachFromWebState();
}
WebFrameImpl::RequestCallbacks::RequestCallbacks(
base::OnceCallback<void(const base::Value*)> completion,
std::unique_ptr<TimeoutCallback> timeout)
: completion(std::move(completion)), timeout_callback(std::move(timeout)) {}
WebFrameImpl::RequestCallbacks::~RequestCallbacks() {}
} // namespace web