blob: 30f9955b5fd7083ad45ea3d8d131799c9dac3a1b [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <iomanip>
#import "ios/web/text_fragments/text_fragments_manager_impl.h"
#import "base/json/json_writer.h"
#import "base/strings/string_util.h"
#import "base/strings/utf_string_conversions.h"
#import "components/shared_highlighting/core/common/shared_highlighting_metrics.h"
#import "components/shared_highlighting/core/common/text_fragments_constants.h"
#import "components/shared_highlighting/core/common/text_fragments_utils.h"
#import "ios/web/common/features.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/navigation/referrer.h"
#import "ios/web/public/web_state.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
const char kScriptCommandPrefix[] = "textFragments";
const char kScriptResponseCommand[] = "textFragments.response";
const double kMinSelectorCount = 0.0;
const double kMaxSelectorCount = 200.0;
// Returns a rgb hexadecimal color, suitable for processing in JavaScript
std::string ToHexStringRGB(int color) {
std::stringstream sstream;
sstream << "'" << std::setfill('0') << std::setw(6) << std::hex
<< (color & 0x00FFFFFF) << "'";
return sstream.str();
}
} // namespace
namespace web {
TextFragmentsManagerImpl::TextFragmentsManagerImpl(WebState* web_state)
: web_state_(web_state) {
DCHECK(web_state_);
web_state_->AddObserver(this);
if (base::FeatureList::IsEnabled(web::features::kScrollToTextIOS)) {
const web::WebState::ScriptCommandCallback callback = base::BindRepeating(
^(const base::DictionaryValue& message, const GURL& page_url,
bool interacted, web::WebFrame* sender_frame) {
if (web_state_ && sender_frame && sender_frame->IsMainFrame()) {
DidReceiveJavaScriptResponse(message);
}
});
subscription_ =
web_state_->AddScriptCommandCallback(callback, kScriptCommandPrefix);
}
}
TextFragmentsManagerImpl::~TextFragmentsManagerImpl() {
if (web_state_) {
web_state_->RemoveObserver(this);
web_state_ = nullptr;
}
}
// static
void TextFragmentsManagerImpl::CreateForWebState(WebState* web_state) {
DCHECK(web_state);
if (!FromWebState(web_state)) {
web_state->SetUserData(
UserDataKey(), std::make_unique<TextFragmentsManagerImpl>(web_state));
}
}
// static
TextFragmentsManagerImpl* TextFragmentsManagerImpl::FromWebState(
WebState* web_state) {
return static_cast<TextFragmentsManagerImpl*>(
TextFragmentsManager::FromWebState(web_state));
}
void TextFragmentsManagerImpl::DidFinishNavigation(
WebState* web_state,
NavigationContext* navigation_context) {
DCHECK(web_state_ == web_state);
web::NavigationItem* item =
web_state->GetNavigationManager()->GetLastCommittedItem();
if (!item)
return;
ProcessTextFragments(navigation_context, item->GetReferrer());
}
void TextFragmentsManagerImpl::WebStateDestroyed(WebState* web_state) {
web_state_->RemoveObserver(this);
web_state_ = nullptr;
}
void TextFragmentsManagerImpl::ProcessTextFragments(
const web::NavigationContext* context,
const web::Referrer& referrer) {
DCHECK(web_state_);
if (!context || !AreTextFragmentsAllowed(context)) {
return;
}
base::Value parsed_fragments = shared_highlighting::ParseTextFragments(
web_state_->GetLastCommittedURL());
if (parsed_fragments.type() == base::Value::Type::NONE) {
return;
}
// Log metrics and cache Referrer for UKM logging.
shared_highlighting::LogTextFragmentSelectorCount(
parsed_fragments.GetList().size());
shared_highlighting::LogTextFragmentLinkOpenSource(referrer.url);
latest_source_id_ = ukm::ConvertToSourceId(context->GetNavigationId(),
ukm::SourceIdType::NAVIGATION_ID);
latest_referrer_url_ = referrer.url;
std::string fragment_param;
base::JSONWriter::Write(parsed_fragments, &fragment_param);
std::string script;
if (base::FeatureList::IsEnabled(
web::features::kIOSSharedHighlightingColorChange)) {
script = base::ReplaceStringPlaceholders(
"__gCrWeb.textFragments.handleTextFragments($1, $2, $3, $4)",
{fragment_param,
/* scroll = */ "true",
/* backgroundColor = */
ToHexStringRGB(shared_highlighting::kFragmentTextBackgroundColorARGB),
/* foregroundColor = */
ToHexStringRGB(shared_highlighting::kFragmentTextForegroundColorARGB)},
/* offsets= */ nil);
} else {
script = base::ReplaceStringPlaceholders(
"__gCrWeb.textFragments.handleTextFragments($1, $2, $3, $4)",
{fragment_param, /* scroll = */ "true", "null", "null"},
/* offsets= */ nil);
}
web_state_->ExecuteJavaScript(base::UTF8ToUTF16(script));
}
#pragma mark - Private Methods
// Returns false if fragments highlighting is not allowed in the current
// |context|.
bool TextFragmentsManagerImpl::AreTextFragmentsAllowed(
const web::NavigationContext* context) {
if (!base::FeatureList::IsEnabled(web::features::kScrollToTextIOS)) {
return false;
}
if (!web_state_ || web_state_->HasOpener()) {
// TODO(crbug.com/1099268): Loosen this restriction if the opener has the
// same domain.
return false;
}
return context->HasUserGesture() && !context->IsSameDocument();
}
void TextFragmentsManagerImpl::DidReceiveJavaScriptResponse(
const base::DictionaryValue& response) {
const std::string* command = response.FindStringKey("command");
if (!command || *command != kScriptResponseCommand) {
return;
}
// Log success metrics.
base::Optional<double> optional_fragment_count =
response.FindDoublePath("result.fragmentsCount");
base::Optional<double> optional_success_count =
response.FindDoublePath("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());
shared_highlighting::LogTextFragmentMatchRate(success_count, fragment_count);
shared_highlighting::LogTextFragmentAmbiguousMatch(
/*ambiguous_match=*/success_count != fragment_count);
shared_highlighting::LogLinkOpenedUkmEvent(
latest_source_id_, latest_referrer_url_,
/*success=*/success_count == fragment_count);
}
WEB_STATE_USER_DATA_KEY_IMPL(TextFragmentsManager)
} // namespace web