// 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/functional/callback_helpers.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_features/window_error/web_js_error_report_processor.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 "ios/web/public/web_client.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::ListValue& parameters) {
  NSMutableArray* parameter_strings = [[NSMutableArray alloc] init];

  for (const auto& value : parameters) {
    std::string string_value = base::WriteJson(value).value_or("");
    [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:@","]];
}

void LogInterestingScriptResultError(const web::ScriptContext& context) {
  UMA_HISTOGRAM_BOOLEAN("IOS.JavaScript.InterestingScriptError", true);

  if (!base::FeatureList::IsEnabled(web::features::kLogCrWebJavaScriptErrors)) {
    return;
  }

  if (!context.web_state) {
    web::WebJsErrorReportProcessor::LogProcessorUnavailable();
    return;
  }

  web::WebJsErrorReportProcessor* report_processor =
      web::WebJsErrorReportProcessor::FromBrowserState(
          context.web_state->GetBrowserState());
  if (!report_processor) {
    web::WebJsErrorReportProcessor::LogProcessorUnavailable();
    return;
  }

  report_processor->ReportJavaScriptExecutionFailed(
      context.api.value_or(""), context.security_origin, context.error,
      context.is_main_frame);
}

// 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";

void LogScriptResultError(const web::ScriptContext& context) {
  NSString* ns_exception =
      context.error.userInfo[@"WKJavaScriptExceptionMessage"];

  if (DLOG_IS_ON(WARNING)) {
    std::string executed_script = base::SysNSStringToUTF8(context.script);
    std::string error_string = base::SysNSStringToUTF8(
        context.error.userInfo[NSLocalizedDescriptionKey]);
    std::string exception = base::SysNSStringToUTF8(ns_exception);

    DLOG(WARNING) << "Script execution of:" << executed_script
                  << "\nfailed with error: " << error_string
                  << "\nand exception: " << exception;
  }

  if (base::FeatureList::IsEnabled(web::features::kAssertOnJavaScriptErrors)) {
    CHECK(false)
        << "JavaScript error occurred with kAssertOnJavaScriptErrors enabled.";
  }

  // 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 (context.error.domain == WKErrorDomain &&
      context.error.code == WKErrorJavaScriptInvalidFrameTarget) {
    UMA_HISTOGRAM_BOOLEAN("IOS.JavaScript.InterestingScriptError", false);
    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 (context.error.domain == WKErrorDomain &&
      [kCannotExecuteJSInDocumentErrorMessage isEqualToString:ns_exception]) {
    UMA_HISTOGRAM_BOOLEAN("IOS.JavaScript.InterestingScriptError", false);
    return;
  }

  // Ignore WKErrorJavaScriptResultTypeIsUnsupported error due to the WebView
  // or WebFrame being released or navigated while a JavaScript function is
  // executing. In case of a webstate navigation, the old web_frames are
  // destroyed, and checking `!web_frame` catches this case.
  bool isTypeUnsupportedResultError =
      [context.error.domain isEqualToString:WKErrorDomain] &&
      context.error.code == WKErrorJavaScriptResultTypeIsUnsupported;

  if (isTypeUnsupportedResultError) {
    if (!context.web_state || !context.web_frame) {
      UMA_HISTOGRAM_BOOLEAN("IOS.JavaScript.InterestingScriptError", false);
    } else if (context.web_frame) {
      context.web_frame->CacheError(std::move(context));
    }

    return;
  }

  LogInterestingScriptResultError(context);
}

void OnJavaScriptExecutedInContentWorld(
    web::ScriptContext context,
    web::ExecuteJavaScriptCallbackWithError callback,
    id value,
    NSError* error) {
  if (error) {
    context.error = error;
    LogScriptResultError(context);

    std::move(callback).Run(nullptr, error);
  } else {
    if (context.web_frame) {
      context.web_frame->ProcessCachedErrors();
    }
    std::move(callback).Run(web::ValueResultFromWKResult(value).get(), nil);
  }
}

void JSExecutionCompleteReplyWithResultForMessageId(web::ScriptContext context,
                                                    int message_id,
                                                    id value,
                                                    NSError* error) {
  if (context.web_frame) {
    if (error) {
      context.error = error;
      LogScriptResultError(context);
    } else {
      context.web_frame->ProcessCachedErrors();
    }
    context.web_frame->OnJSResultReceivedForMessageWithId(message_id, value);
  }
}

void JSExecutionComplete(web::ScriptContext context, id value, NSError* error) {
  if (error) {
    context.error = error;
    LogScriptResultError(context);
  } else if (context.web_frame) {
    context.web_frame->ProcessCachedErrors();
  }
}

}  // 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::ListValue& 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::ListValue& 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::ListValue& parameters,
    JavaScriptContentWorld* content_world) {
  return CallJavaScriptFunctionInContentWorld(name, parameters, content_world,
                                              /*reply_with_result=*/false);
}

bool WebFrameImpl::CallJavaScriptFunction(
    const std::string& name,
    const base::ListValue& 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::ListValue& 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);
  ScriptContext context(web_state_->GetWeakPtr(),
                        weak_ptr_factory_.GetWeakPtr(), security_origin_,
                        is_main_frame_, ns_script);

  auto completion = base::BindOnce(&OnJavaScriptExecutedInContentWorld,
                                   std::move(context), std::move(callback));

  web::ExecuteJavaScript(
      frame_info_.webView, content_world->GetWKContentWorld(), frame_info_,
      ns_script, base::CallbackToBlock(std::move(completion)));
  return true;
}

bool WebFrameImpl::ExecuteAsyncJavaScript(
    const std::u16string& script,
    const base::DictValue& parameters,
    ExecuteJavaScriptCallbackWithError callback) {
  JavaScriptContentWorld* content_world =
      JavaScriptFeatureManager::GetContentWorldForBrowserState(
          content_world_, GetBrowserState());

  return ExecuteAsyncJavaScriptInContentWorld(script, parameters, content_world,
                                              std::move(callback));
}

bool WebFrameImpl::ExecuteAsyncJavaScriptInContentWorld(
    const std::u16string& script,
    const base::DictValue& parameters,
    JavaScriptContentWorld* content_world,
    ExecuteJavaScriptCallbackWithError callback) {
  DCHECK(frame_info_);

  NSString* ns_script = base::SysUTF16ToNSString(script);

  id ns_dict = web::NSDictionaryFromValue(parameters);

  ScriptContext context(web_state_->GetWeakPtr(),
                        weak_ptr_factory_.GetWeakPtr(), security_origin_,
                        is_main_frame_, ns_script);

  auto completion = base::BindOnce(&OnJavaScriptExecutedInContentWorld,
                                   std::move(context), std::move(callback));

  web::ExecuteAsyncJavaScript(
      frame_info_.webView, content_world->GetWKContentWorld(), frame_info_,
      ns_script, ns_dict, base::CallbackToBlock(std::move(completion)));
  return true;
}

bool WebFrameImpl::CallAsyncJavaScriptFunction(
    const std::string& name,
    const base::DictValue& parameters,
    ExecuteJavaScriptCallbackWithError callback) {
  JavaScriptContentWorld* content_world =
      JavaScriptFeatureManager::GetContentWorldForBrowserState(
          content_world_, GetBrowserState());
  return CallAsyncJavaScriptFunctionInContentWorld(
      name, parameters, content_world, std::move(callback));
}

bool WebFrameImpl::CallAsyncJavaScriptFunctionInContentWorld(
    const std::string& name,
    const base::DictValue& parameters,
    JavaScriptContentWorld* content_world,
    ExecuteJavaScriptCallbackWithError callback) {
  base::DictValue arguments;
  arguments.Set("crw_args", parameters.Clone());

  std::optional<std::pair<std::string_view, std::string_view>> name_parts =
      base::SplitStringOnce(name, ".");

  std::string api_name;
  std::string function_name;

  if (name_parts) {
    api_name = name_parts->first;
    function_name = name_parts->second;
  } else {
    api_name = "";
    function_name = name;
  }

  std::string script = "return __gCrWeb.callFunctionInGcrWeb('" + api_name +
                       "', '" + function_name + "', [crw_args]);";

  return ExecuteAsyncJavaScriptInContentWorld(
      base::UTF8ToUTF16(script), arguments, content_world, std::move(callback));
}

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);
    }
  });
}



bool WebFrameImpl::ExecuteJavaScriptFunction(
    JavaScriptContentWorld* content_world,
    const std::string& name,
    const base::ListValue& parameters,
    int message_id,
    bool reply_with_result) {
  DCHECK(content_world);
  DCHECK(frame_info_);

  WKContentWorld* world = content_world->GetWKContentWorld();
  DCHECK(world);

  NSString* script = CreateFunctionCallWithParameters(name, parameters);
  ScriptContext context(web_state_->GetWeakPtr(),
                        weak_ptr_factory_.GetWeakPtr(), security_origin_,
                        is_main_frame_, script, name);

  if (reply_with_result) {
    auto callback =
        base::BindOnce(&JSExecutionCompleteReplyWithResultForMessageId,
                       std::move(context), message_id);
    web::ExecuteJavaScript(frame_info_.webView, world, frame_info_, script,
                           base::CallbackToBlock(std::move(callback)));
  } else {
    auto callback = base::BindOnce(&JSExecutionComplete, std::move(context));
    web::ExecuteJavaScript(frame_info_.webView, world, frame_info_, script,
                           base::CallbackToBlock(std::move(callback)));
  }

  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();
}

void WebFrameImpl::OnJSResultReceivedForMessageWithId(int message_id,
                                                      id value) {
  CompleteRequest(message_id, web::ValueResultFromWKResult(value).get());
}

void WebFrameImpl::CacheError(ScriptContext error) {
  cached_errors_.push_back(std::move(error));
}

void WebFrameImpl::ProcessCachedErrors() {
  for (const ScriptContext& error : cached_errors_) {
    LogInterestingScriptResultError(error);
  }
  cached_errors_.clear();
}

ScriptContext::ScriptContext(base::WeakPtr<web::WebState> web_state,
                             base::WeakPtr<web::WebFrameImpl> web_frame,
                             url::Origin security_origin,
                             bool is_main_frame,
                             NSString* script,
                             std::optional<std::string> api)
    : web_state(web_state),
      web_frame(web_frame),
      security_origin(security_origin),
      is_main_frame(is_main_frame),
      script(script),
      api(std::move(api)) {}

ScriptContext::ScriptContext(const ScriptContext&) = default;
ScriptContext::ScriptContext(ScriptContext&&) = default;
ScriptContext& ScriptContext::operator=(const ScriptContext&) = default;
ScriptContext& ScriptContext::operator=(ScriptContext&&) = default;
ScriptContext::~ScriptContext() = default;

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
