chromium/ios/web/annotations/annotations_java_script_feature.mm

// Copyright 2022 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/annotations/annotations_java_script_feature.h"

#import <vector>

#import "base/logging.h"
#import "base/metrics/histogram_macros.h"
#import "base/no_destructor.h"
#import "base/strings/sys_string_conversions.h"
#import "components/shared_highlighting/ios/parsing_utils.h"
#import "ios/web/annotations/annotations_text_manager_impl.h"
#import "ios/web/common/features.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"

namespace {
const char kLegacyScriptName[] = "annotations";
const char kScriptName[] = "text_main";
const char kScriptHandlerName[] = "annotations";
}  // namespace

namespace web {

const char* GetScriptName() {
  return base::FeatureList::IsEnabled(features::kEnableViewportIntents)
             ? kScriptName
             : kLegacyScriptName;
}

AnnotationsJavaScriptFeature::AnnotationsJavaScriptFeature()
    : JavaScriptFeature(
          ContentWorld::kIsolatedWorld,
          {FeatureScript::CreateWithFilename(
              GetScriptName(),
              FeatureScript::InjectionTime::kDocumentStart,
              FeatureScript::TargetFrames::kMainFrame,
              FeatureScript::ReinjectionBehavior::kInjectOncePerWindow)}) {}

AnnotationsJavaScriptFeature::~AnnotationsJavaScriptFeature() = default;

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

void AnnotationsJavaScriptFeature::ExtractText(WebState* web_state,
                                               int maximum_text_length,
                                               int seq_id) {
  DCHECK(web_state);
  WebFrame* frame = GetWebFramesManager(web_state)->GetMainWebFrame();
  if (!frame) {
    return;
  }

  base::Value::List parameters;
  if (base::FeatureList::IsEnabled(features::kEnableViewportIntents)) {
    CallJavaScriptFunction(frame, "annotations.start", parameters);
  } else {
    parameters.Append(maximum_text_length);
    parameters.Append(seq_id);
    CallJavaScriptFunction(frame, "annotations.extractText", parameters);
  }
}

void AnnotationsJavaScriptFeature::DecorateAnnotations(WebState* web_state,
                                                       base::Value& annotations,
                                                       int seq_id) {
  DCHECK(web_state);
  WebFrame* frame = GetWebFramesManager(web_state)->GetMainWebFrame();
  if (!frame) {
    return;
  }

  base::Value::List parameters;
  parameters.Append(std::move(annotations));
  parameters.Append(seq_id);
  CallJavaScriptFunction(frame, "annotations.decorateAnnotations", parameters);
}

void AnnotationsJavaScriptFeature::RemoveDecorations(WebState* web_state) {
  DCHECK(web_state);
  WebFrame* frame = GetWebFramesManager(web_state)->GetMainWebFrame();
  if (!frame) {
    return;
  }

  CallJavaScriptFunction(frame, "annotations.removeDecorations", {});
}

void AnnotationsJavaScriptFeature::RemoveDecorationsWithType(
    WebState* web_state,
    const std::string& type) {
  DCHECK(web_state);
  WebFrame* frame = GetWebFramesManager(web_state)->GetMainWebFrame();
  if (!frame) {
    return;
  }

  base::Value::List parameters;
  parameters.Append(std::move(type));

  CallJavaScriptFunction(frame, "annotations.removeDecorationsWithType",
                         parameters);
}

void AnnotationsJavaScriptFeature::RemoveHighlight(WebState* web_state) {
  DCHECK(web_state);
  WebFrame* frame = GetWebFramesManager(web_state)->GetMainWebFrame();
  if (!frame) {
    return;
  }

  CallJavaScriptFunction(frame, "annotations.removeHighlight", {});
}

void AnnotationsJavaScriptFeature::ScriptMessageReceived(
    WebState* web_state,
    const ScriptMessage& script_message) {
  if (!script_message.is_main_frame()) {
    return;
  }

  AnnotationsTextManagerImpl* manager =
      static_cast<AnnotationsTextManagerImpl*>(
          AnnotationsTextManager::FromWebState(web_state));
  if (!manager) {
    return;
  }

  base::Value* response = script_message.body();
  if (!response || !response->is_dict()) {
    return;
  }

  const base::Value::Dict& dict = response->GetDict();

  const std::string* command = dict.FindString("command");
  if (!command) {
    return;
  }

  // Discard messages if we've navigated away.
  auto sender_url = script_message.request_url();
  GURL current_url = web_state->GetLastCommittedURL();
  if (!sender_url || !(*sender_url).EqualsIgnoringRef(current_url)) {
    return;
  }

  if (*command == "annotations.extractedText") {
    const std::string* text = dict.FindString("text");
    std::optional<double> seq_id = dict.FindDouble("seqId");
    const base::Value::Dict* metadata = dict.FindDict("metadata");
    if (!text || !seq_id || !metadata) {
      return;
    }
    manager->OnTextExtracted(web_state, *text, static_cast<int>(seq_id.value()),
                             *metadata);
  } else if (*command == "annotations.decoratingComplete") {
    std::optional<double> optional_annotations = dict.FindDouble("annotations");
    std::optional<double> optional_successes = dict.FindDouble("successes");
    std::optional<double> optional_failures = dict.FindDouble("failures");
    const base::Value::List* cancelled = dict.FindList("cancelled");
    if (!optional_annotations || !optional_successes || !optional_failures ||
        !cancelled) {
      return;
    }
    int annotations = static_cast<int>(optional_annotations.value());
    int successes = static_cast<int>(optional_successes.value());
    int failures = static_cast<int>(optional_failures.value());
    manager->OnDecorated(web_state, annotations, successes, failures,
                         *cancelled);
  } else if (*command == "annotations.onClick") {
    const std::string* data = dict.FindString("data");
    std::optional<CGRect> rect =
        shared_highlighting::ParseRect(dict.FindDict("rect"));
    const std::string* text = dict.FindString("text");
    std::optional<bool> cancel = dict.FindBool("cancel");
    if (!data || !rect || !text || !cancel) {
      return;
    }
    UMA_HISTOGRAM_BOOLEAN("IOS.Annotations.UserTap.Cancelled", *cancel);
    if (!*cancel) {
      manager->OnClick(
          web_state, *text,
          shared_highlighting::ConvertToBrowserRect(*rect, web_state), *data);
    }
  }
}

std::optional<std::string>
AnnotationsJavaScriptFeature::GetScriptMessageHandlerName() const {
  return kScriptHandlerName;
}

}  // namespace web