// Copyright 2023 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/content/js_messaging/content_web_frames_manager.h"

#import <set>

#import "base/feature_list.h"
#import "base/no_destructor.h"
#import "base/strings/string_util.h"
#import "base/strings/utf_string_conversions.h"
#import "base/unguessable_token.h"
#import "components/js_injection/browser/js_communication_host.h"
#import "content/public/browser/navigation_handle.h"
#import "content/public/browser/page.h"
#import "content/public/browser/web_contents.h"
#import "ios/web/content/js_messaging/content_java_script_feature_manager.h"
#import "ios/web/content/js_messaging/content_java_script_feature_util.h"
#import "ios/web/content/js_messaging/content_web_frame.h"
#import "ios/web/content/js_messaging/ios_web_message_host_factory.h"
#import "ios/web/content/web_state/content_web_state.h"
#import "ios/web/public/js_messaging/java_script_feature_util.h"
#import "ios/web/public/js_messaging/script_message.h"
#import "ios/web/public/web_client.h"

namespace web {

namespace {

// Names of JSON dictionary properties that JavaScript populates when sending
// messages to the browser.
const char kHandlerNamePropertyName[] = "handler_name";
const char kMessagePropertyName[] = "message";

const char kSendWebKitMessageScriptName[] = "send_webkit_message";

// This feature intercepts calls to window.webkit.messageHandlers and reroutes
// them to window.webkitMessageHandler.
JavaScriptFeature* GetSendWebKitMessageJavaScriptFeature() {
  // Static storage is ok for `send_webkit_message_feature` as it holds no
  // state.
  static base::NoDestructor<JavaScriptFeature> send_webkit_message_feature(
      ContentWorld::kPageContentWorld,
      std::vector<JavaScriptFeature::FeatureScript>(
          {JavaScriptFeature::FeatureScript::CreateWithFilename(
              kSendWebKitMessageScriptName,
              JavaScriptFeature::FeatureScript::InjectionTime::kDocumentStart,
              JavaScriptFeature::FeatureScript::TargetFrames::kAllFrames)}));
  return send_webkit_message_feature.get();
}

}  // namespace

ContentWebFramesManager::ContentWebFramesManager(
    ContentWebState* content_web_state)
    : content::WebContentsObserver(content_web_state->GetWebContents()),
      content_web_state_(content_web_state),
      js_communication_host_(
          std::make_unique<js_injection::JsCommunicationHost>(
              content_web_state->GetWebContents())) {
  auto web_message_callback =
      base::BindRepeating(&ContentWebFramesManager::ScriptMessageReceived,
                          weak_factory_.GetWeakPtr());
  auto message_host_factory =
      std::make_unique<IOSWebMessageHostFactory>(web_message_callback);
  js_communication_host_->AddWebMessageHostFactory(
      std::move(message_host_factory), u"webkitMessageHandler", {"*"},
      /* world_identifier= */ 0);

  std::vector<JavaScriptFeature*> java_script_features;
  java_script_features.push_back(GetSendWebKitMessageJavaScriptFeature());
  for (JavaScriptFeature* feature :
       java_script_features::GetBuiltInJavaScriptFeaturesForContent(
           content_web_state->GetBrowserState())) {
    java_script_features.push_back(feature);
  }
  for (JavaScriptFeature* feature : GetWebClient()->GetJavaScriptFeatures(
           content_web_state->GetBrowserState())) {
    java_script_features.push_back(feature);
  }

  js_feature_manager_ = std::make_unique<ContentJavaScriptFeatureManager>(
      std::move(java_script_features));
  js_feature_manager_->AddDocumentStartScripts(js_communication_host_.get());

  // Handle frames that may already exist if WebContents was created before
  // this observer was attached (e.g., when created with an opener).
  content_web_state->GetWebContents()->ForEachRenderFrameHost(
      [this](content::RenderFrameHost* render_frame_host) {
        if (render_frame_host->IsRenderFrameLive()) {
          RenderFrameCreated(render_frame_host);
        }
      });
}

ContentWebFramesManager::~ContentWebFramesManager() = default;

void ContentWebFramesManager::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void ContentWebFramesManager::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

std::set<WebFrame*> ContentWebFramesManager::GetAllWebFrames() {
  std::set<WebFrame*> frames;
  for (const auto& it : available_frame_hosts_) {
    frames.insert(WebFrameForContentId(it));
  }
  return frames;
}

WebFrame* ContentWebFramesManager::GetMainWebFrame() {
  auto web_id_it = content_to_web_id_map_.find(main_frame_content_id_);
  if (web_id_it == content_to_web_id_map_.end()) {
    return nullptr;
  }

  return GetFrameWithId(web_id_it->second);
}

WebFrame* ContentWebFramesManager::GetFrameWithId(const std::string& frame_id) {
  if (frame_id.empty()) {
    return nullptr;
  }
  auto web_frames_it = web_frames_.find(frame_id);
  return web_frames_it == web_frames_.end() ? nullptr
                                            : web_frames_it->second.get();
}

void ContentWebFramesManager::RenderFrameCreated(
    content::RenderFrameHost* render_frame_host) {
  // Guard against duplicate registration (e.g., if ForEachRenderFrameHost in
  // constructor already registered this frame).
  if (content_to_web_id_map_.contains(render_frame_host->GetGlobalId())) {
    return;
  }

  std::string web_frame_id = base::UnguessableToken::Create().ToString();
  auto web_frame = std::make_unique<ContentWebFrame>(
      web_frame_id, render_frame_host, content_web_state_);
  web_frames_[web_frame_id] = std::move(web_frame);
  content_to_web_id_map_[render_frame_host->GetGlobalId()] = web_frame_id;
}

void ContentWebFramesManager::RenderFrameDeleted(
    content::RenderFrameHost* render_frame_host) {
  content::GlobalRenderFrameHostId content_id =
      render_frame_host->GetGlobalId();
  auto web_id_it = content_to_web_id_map_.find(content_id);
  DCHECK(web_id_it != content_to_web_id_map_.end());

  if (available_frame_hosts_.count(content_id)) {
    for (auto& observer : observers_) {
      observer.WebFrameBecameUnavailable(this, web_id_it->second);
    }
    available_frame_hosts_.erase(content_id);
  }

  if (main_frame_content_id_ == content_id) {
    main_frame_content_id_ = content::GlobalRenderFrameHostId();
  }

  web_frames_.erase(web_id_it->second);
  content_to_web_id_map_.erase(web_id_it);
}

void ContentWebFramesManager::PrimaryPageChanged(content::Page& page) {
  main_frame_content_id_ = page.GetMainDocument().GetGlobalId();
}

void ContentWebFramesManager::DOMContentLoaded(
    content::RenderFrameHost* render_frame_host) {
  content::GlobalRenderFrameHostId content_id =
      render_frame_host->GetGlobalId();
  WebFrame* web_frame = WebFrameForContentId(content_id);

  if (base::FeatureList::IsEnabled(kContentEnableInjectedFeatureScripts)) {
    // Inject JavaScript to override `getFrameId` to return the WebFrame id
    // chosen in `RenderFrameCreated`. This must happen even if the frame has
    // already been added to `available_frame_hosts_`, since navigation to a new
    // document will result in a fresh JavaScript execution context.
    std::u16string format_string = u"__gCrWeb.frameId = '$1';";
    std::u16string script_to_inject = base::ReplaceStringPlaceholders(
        format_string, base::UTF8ToUTF16(web_frame->GetFrameId()),
        /*offset=*/nullptr);
    web_frame->ExecuteJavaScript(script_to_inject);
  }

  js_feature_manager_->InjectDocumentEndScripts(render_frame_host);

  if (available_frame_hosts_.count(content_id)) {
    return;
  }

  available_frame_hosts_.insert(content_id);

  // Notify observers here rather than in `RenderFrameCreated`, to
  // ensure that frames are no longer in a speculative lifecycle
  // phase where JavaScript injection is not yet allowed. crbug.com/1183639
  // tracks delaying `RenderFrameCreated` until frames are past the
  // speculative state, which is not intended to be exposed to embedders.
  for (auto& observer : observers_) {
    observer.WebFrameBecameAvailable(this, web_frame);
  }
}

WebFrame* ContentWebFramesManager::WebFrameForContentId(
    content::GlobalRenderFrameHostId content_id) {
  auto web_id_it = content_to_web_id_map_.find(content_id);
  DCHECK(web_id_it != content_to_web_id_map_.end());
  auto web_frame_it = web_frames_.find(web_id_it->second);
  DCHECK(web_frame_it != web_frames_.end());

  return web_frame_it->second.get();
}

void ContentWebFramesManager::ScriptMessageReceived(
    const ScriptMessage& script_message) {
  // In ios/web/content, only a single script message handler is exposed to
  // JavaScript. To simulate having multiple handlers (as in WKWebView-based
  // ios/web), messages sent to this handler have the form of a dictionary with
  // two fields, specifying the destination handler name along with the actual
  // message for that handler. Once these two parts are extracted from
  // `script_message`, a new ScriptMessage is constructed with only the actual
  // message intended for the handler.

  base::DictValue* dict = script_message.legacy_body()->GetIfDict();
  if (!dict) {
    return;
  }

  const std::string* handler_name = dict->FindString(kHandlerNamePropertyName);
  base::Value* message_content = dict->Find(kMessagePropertyName);
  if (!handler_name || !message_content) {
    return;
  }

  ScriptMessage message_for_handler(
      std::make_unique<base::Value>(std::move(*message_content)),
      script_message.is_user_interacting(), script_message.is_main_frame(),
      script_message.request_url(), script_message.security_origin());

  js_feature_manager_->ScriptMessageReceived(message_for_handler, *handler_name,
                                             content_web_state_);
}

}  // namespace web
