// Copyright 2025 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_features/clipboard/clipboard_java_script_feature.h"

#import "base/functional/bind.h"
#import "base/metrics/histogram_functions.h"
#import "base/values.h"
#import "ios/components/enterprise/data_controls/clipboard_enums.h"
#import "ios/components/enterprise/data_controls/metrics_utils.h"
#import "ios/web/js_features/clipboard/clipboard_constants.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/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_delegate.h"

namespace {
const char kClipboardScriptName[] = "clipboard";
const char kPasteHandlerScriptName[] = "paste_handler";

using data_controls::ClipboardAction;
using data_controls::ClipboardSource;
using data_controls::RecordClipboardOutcomeMetrics;
using data_controls::RecordClipboardSourceMetrics;

// Helper to convert a clipboard command string to a ClipboardAction.
std::optional<ClipboardAction> GetActionFromCommand(
    const std::string& command) {
  if (command == web::kReadCommand) {
    return ClipboardAction::kPaste;
  }
  if (command == web::kWriteCommand) {
    return ClipboardAction::kCopy;
  }
  return std::nullopt;
}

// Helper to record the source metric for clipboard commands.
void RecordClipboardSource(const std::string& command) {
  std::optional<ClipboardAction> action = GetActionFromCommand(command);
  if (!action) {
    return;
  }
  RecordClipboardSourceMetrics(*action, ClipboardSource::kClipboardAPI);
}

// Helper to record the outcome metric for clipboard commands.
void RecordClipboardOutcome(const std::string& command, bool allowed) {
  std::optional<ClipboardAction> action = GetActionFromCommand(command);
  if (!action) {
    return;
  }
  RecordClipboardOutcomeMetrics(*action, allowed);
}

}  // namespace

namespace web {

// static
ClipboardJavaScriptFeature* ClipboardJavaScriptFeature::GetInstance() {
  static base::NoDestructor<ClipboardJavaScriptFeature> instance;
  return instance.get();
}

ClipboardJavaScriptFeature::ClipboardJavaScriptFeature()
    : JavaScriptFeature(
          ContentWorld::kPageContentWorld,
          {FeatureScript::CreateWithFilename(
               kClipboardScriptName,
               FeatureScript::InjectionTime::kDocumentStart,
               FeatureScript::TargetFrames::kAllFrames,
               FeatureScript::ReinjectionBehavior::kInjectOncePerWindow),
           FeatureScript::CreateWithFilename(
               kPasteHandlerScriptName,
               FeatureScript::InjectionTime::kDocumentEnd,
               FeatureScript::TargetFrames::kAllFrames,
               FeatureScript::ReinjectionBehavior::
                   kReinjectOnDocumentRecreation)},
          {java_script_features::GetBaseJavaScriptFeature()}) {}

ClipboardJavaScriptFeature::~ClipboardJavaScriptFeature() = default;

std::optional<std::string>
ClipboardJavaScriptFeature::GetScriptMessageHandlerName() const {
  return kScriptMessageHandlerName;
}

void ClipboardJavaScriptFeature::ScriptMessageReceived(
    WebState* web_state,
    const ScriptMessage& message) {
  // Expected `message.legacy_body` format:
  // {
  //   "command": "read"|"write"|"didFinishClipboardRead",
  //   "requestId": <number>,  // Only for "read" and "write".
  //   "frameId": <string>,
  // }
  const base::DictValue* body = message.legacy_body()->GetIfDict();
  if (!body) {
    return;
  }

  const std::string* command = body->FindString(kCommandKey);
  if (!command) {
    return;
  }

  const std::string* frame_id = body->FindString(kFrameIdKey);
  if (!frame_id) {
    return;
  }

  WebFrame* web_frame =
      GetWebFramesManager(web_state)->GetFrameWithId(*frame_id);
  if (!web_frame) {
    return;
  }

  if (*command == kDidFinishClipboardReadCommand) {
    if (web_state->GetDelegate()) {
      web_state->GetDelegate()->DidFinishClipboardRead(web_state);
    }
  } else if (*command == kReadCommand || *command == kWriteCommand) {
    // In JavaScript, all numbers are doubles.
    std::optional<double> request_id_double = body->FindDouble(kRequestIdKey);
    if (!request_id_double) {
      return;
    }
    RecordClipboardSource(*command);
    int request_id = static_cast<int>(*request_id_double);
    HandleClipboardRequest(web_state, web_frame, request_id, *command);
  }
}

void ClipboardJavaScriptFeature::HandleClipboardRequest(
    WebState* web_state,
    WebFrame* web_frame,
    int request_id,
    const std::string& command) {
  // Requests are allowed by default.
  if (!web_state->GetDelegate()) {
    ResolveClipboardRequest(request_id, web_frame->AsWeakPtr(), command,
                            /* allowed= */ true);
    return;
  }

  // Request Clipboard access approval from the WebState's delegate.
  // It is safe to bind the callbacks to the singleton instance of
  // ClipboardJavaScriptFeature because it is never destroyed.
  base::OnceCallback<void(bool)> callback =
      base::BindOnce(&ClipboardJavaScriptFeature::ResolveClipboardRequest,
                     base::Unretained(GetInstance()), request_id,
                     web_frame->AsWeakPtr(), command);

  // Clipboard write operations from JavaScript are evaluated by the same
  // policy framework as native "copy" actions. Similarly, "read" operations
  // are evaluated as "paste" actions. This approach is consistent with the
  // desktop implementation in
  // content/browser/renderer_host/clipboard_host_impl.cc.
  if (command == kWriteCommand) {
    web_state->GetDelegate()->ShouldAllowCopy(web_state, std::move(callback));
  } else if (command == kReadCommand) {
    web_state->GetDelegate()->ShouldAllowPaste(web_state, std::move(callback));
  }
}

void ClipboardJavaScriptFeature::ResolveClipboardRequest(
    int request_id,
    base::WeakPtr<WebFrame> web_frame,
    const std::string& command,
    bool allowed) {
  RecordClipboardOutcome(command, allowed);

  if (!web_frame) {
    return;
  }

  base::ListValue parameters;
  parameters.Append(request_id);
  parameters.Append(allowed);
  CallJavaScriptFunction(web_frame.get(), "clipboard.resolveRequest",
                         parameters);
}

}  // namespace web
