// Copyright 2021 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/text_fragments/text_fragments_java_script_feature.h"
#import <vector>
#import "base/no_destructor.h"
#import "base/strings/sys_string_conversions.h"
#import "components/shared_highlighting/ios/parsing_utils.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/text_fragments/text_fragments_manager_impl.h"
namespace {
const char kScriptName[] = "text_fragments";
const char kScriptHandlerName[] = "textFragments";
const char kHandleFragmentsScript[] = "textFragments.handleTextFragments";
const char kRemoveHighlightsScript[] = "textFragments.removeHighlights";
const double kMaxSelectorCount = 200.0;
const double kMinSelectorCount = 0.0;
}
namespace web {
TextFragmentsJavaScriptFeature::TextFragmentsJavaScriptFeature()
: JavaScriptFeature(
ContentWorld::kIsolatedWorld,
{FeatureScript::CreateWithFilename(
kScriptName,
FeatureScript::InjectionTime::kDocumentStart,
FeatureScript::TargetFrames::kMainFrame,
FeatureScript::ReinjectionBehavior::kInjectOncePerWindow)}) {}
TextFragmentsJavaScriptFeature::~TextFragmentsJavaScriptFeature() = default;
// static
TextFragmentsJavaScriptFeature* TextFragmentsJavaScriptFeature::GetInstance() {
static base::NoDestructor<TextFragmentsJavaScriptFeature> instance;
return instance.get();
}
void TextFragmentsJavaScriptFeature::ProcessTextFragments(
WebState* web_state,
base::Value parsed_fragments,
std::string background_color_hex_rgb,
std::string foreground_color_hex_rgb) {
DCHECK(web_state);
WebFrame* frame = GetWebFramesManager(web_state)->GetMainWebFrame();
if (!frame) {
return;
}
base::Value bg_color = background_color_hex_rgb.empty()
? base::Value()
: base::Value(background_color_hex_rgb);
base::Value fg_color = foreground_color_hex_rgb.empty()
? base::Value()
: base::Value(foreground_color_hex_rgb);
auto parameters = base::Value::List()
.Append(std::move(parsed_fragments))
.Append(/*scroll=*/true)
.Append(std::move(bg_color))
.Append(std::move(fg_color));
CallJavaScriptFunction(frame, kHandleFragmentsScript, parameters);
}
void TextFragmentsJavaScriptFeature::RemoveHighlights(WebState* web_state,
const GURL& new_url) {
DCHECK(web_state);
WebFrame* frame = GetWebFramesManager(web_state)->GetMainWebFrame();
if (!frame) {
return;
}
auto parameters =
base::Value::List().Append(new_url.is_valid() ? new_url.spec() : "");
CallJavaScriptFunction(frame, kRemoveHighlightsScript, parameters);
}
void TextFragmentsJavaScriptFeature::ScriptMessageReceived(
WebState* web_state,
const ScriptMessage& script_message) {
auto* manager = TextFragmentsManagerImpl::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 != current_url) {
return;
}
if (*command == "textFragments.processingComplete") {
// Extract success metrics.
std::optional<double> optional_fragment_count =
dict.FindDoubleByDottedPath("result.fragmentsCount");
std::optional<double> optional_success_count =
dict.FindDoubleByDottedPath("result.successCount");
// Since the response can't be trusted, don't log metrics if the results
// look invalid.
if (!optional_fragment_count ||
optional_fragment_count.value() > kMaxSelectorCount ||
optional_fragment_count.value() <= kMinSelectorCount) {
return;
}
if (!optional_success_count ||
optional_success_count.value() > kMaxSelectorCount ||
optional_success_count.value() < kMinSelectorCount) {
return;
}
if (optional_success_count.value() > optional_fragment_count.value()) {
return;
}
int fragment_count = static_cast<int>(optional_fragment_count.value());
int success_count = static_cast<int>(optional_success_count.value());
manager->OnProcessingComplete(success_count, fragment_count);
} else if (*command == "textFragments.onClick") {
manager->OnClick();
} else if (*command == "textFragments.onClickWithSender") {
std::optional<CGRect> rect =
shared_highlighting::ParseRect(dict.FindDict("rect"));
const std::string* text = dict.FindString("text");
const base::Value::List* fragment_values_list = dict.FindList("fragments");
std::vector<shared_highlighting::TextFragment> fragments;
if (fragment_values_list) {
for (const base::Value& val : *fragment_values_list) {
std::optional<shared_highlighting::TextFragment> fragment =
shared_highlighting::TextFragment::FromValue(&val);
if (fragment) {
fragments.push_back(*fragment);
}
}
}
if (!rect || !text || fragments.empty()) {
return;
}
manager->OnClickWithSender(
shared_highlighting::ConvertToBrowserRect(*rect, web_state),
base::SysUTF8ToNSString(*text), std::move(fragments));
}
}
std::optional<std::string>
TextFragmentsJavaScriptFeature::GetScriptMessageHandlerName() const {
return kScriptHandlerName;
}
} // namespace web