// Copyright 2019 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/find_in_page/java_script_find_in_page_manager_impl.h"
#import <optional>
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "base/values.h"
#import "ios/web/find_in_page/find_in_page_constants.h"
#import "ios/web/find_in_page/find_in_page_java_script_feature.h"
#import "ios/web/find_in_page/find_in_page_metrics.h"
#import "ios/web/public/find_in_page/find_in_page_manager_delegate.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "ios/web/web_state/web_state_impl.h"
namespace web {
// static
void JavaScriptFindInPageManager::CreateForWebState(WebState* web_state) {
DCHECK(web_state);
if (!FromWebState(web_state)) {
web_state->SetUserData(
UserDataKey(),
std::make_unique<JavaScriptFindInPageManagerImpl>(web_state));
}
}
JavaScriptFindInPageManagerImpl::JavaScriptFindInPageManagerImpl(
WebState* web_state)
: web_state_(web_state), weak_factory_(this) {
web_state_->AddObserver(this);
web::WebFramesManager* web_frames_manager =
FindInPageJavaScriptFeature::GetInstance()->GetWebFramesManager(
web_state);
web_frames_manager->AddObserver(this);
}
JavaScriptFindInPageManagerImpl::~JavaScriptFindInPageManagerImpl() {
if (web_state_) {
web_state_->RemoveObserver(this);
web_state_ = nullptr;
}
}
FindInPageManagerDelegate* JavaScriptFindInPageManagerImpl::GetDelegate() {
return delegate_;
}
void JavaScriptFindInPageManagerImpl::SetDelegate(
FindInPageManagerDelegate* delegate) {
delegate_ = delegate;
}
void JavaScriptFindInPageManagerImpl::WebFrameBecameAvailable(
WebFramesManager* web_frames_manager,
WebFrame* web_frame) {
const std::string frame_id = web_frame->GetFrameId();
last_find_request_.AddFrame(web_frame);
}
void JavaScriptFindInPageManagerImpl::WebFrameBecameUnavailable(
WebFramesManager* web_frames_manager,
const std::string& frame_id) {
int match_count = last_find_request_.GetMatchCountForFrame(frame_id);
last_find_request_.RemoveFrame(frame_id);
// Only notify the delegate if the match count has changed.
if (delegate_ && last_find_request_.GetRequestQuery() && match_count > 0) {
delegate_->DidHighlightMatches(this, web_state_,
last_find_request_.GetTotalMatchCount(),
last_find_request_.GetRequestQuery());
}
}
void JavaScriptFindInPageManagerImpl::WebStateDestroyed(WebState* web_state) {
web_state_->RemoveObserver(this);
web_state_ = nullptr;
}
void JavaScriptFindInPageManagerImpl::Find(NSString* query,
FindInPageOptions options) {
DCHECK(CanSearchContent());
switch (options) {
case FindInPageOptions::FindInPageSearch:
DCHECK(query);
StartSearch(query);
break;
case FindInPageOptions::FindInPageNext:
SelectNextMatch();
break;
case FindInPageOptions::FindInPagePrevious:
SelectPreviousMatch();
break;
}
}
void JavaScriptFindInPageManagerImpl::StartSearch(NSString* query) {
if (!web_state_) {
return;
}
RecordSearchStartedAction();
WebFramesManager* frames_manager =
FindInPageJavaScriptFeature::GetInstance()->GetWebFramesManager(
web_state_);
std::set<WebFrame*> all_frames = frames_manager->GetAllWebFrames();
last_find_request_.Reset(query, all_frames.size());
if (all_frames.size() == 0) {
// No frames to search in.
// Call asyncronously to match behavior if find was successful in frames.
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&JavaScriptFindInPageManagerImpl::LastFindRequestCompleted,
weak_factory_.GetWeakPtr()));
return;
}
for (WebFrame* frame : all_frames) {
bool result = FindInPageJavaScriptFeature::GetInstance()->Search(
frame, base::SysNSStringToUTF8(query),
base::BindOnce(
&JavaScriptFindInPageManagerImpl::ProcessFindInPageResult,
weak_factory_.GetWeakPtr(), frame->GetFrameId(),
last_find_request_.GetRequestId()));
if (!result) {
// Calling JavaScript function failed or the frame does not support
// messaging.
last_find_request_.DidReceiveFindResponseFromOneFrame();
if (last_find_request_.AreAllFindResponsesReturned()) {
// Call asyncronously to match behavior if find was done in frames.
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&JavaScriptFindInPageManagerImpl::LastFindRequestCompleted,
weak_factory_.GetWeakPtr()));
}
}
}
}
void JavaScriptFindInPageManagerImpl::StopFinding() {
if (!web_state_) {
return;
}
last_find_request_.Reset(/*new_query=*/nil,
/*new_pending_frame_call_count=*/0);
WebFramesManager* frames_manager =
FindInPageJavaScriptFeature::GetInstance()->GetWebFramesManager(
web_state_);
std::set<WebFrame*> all_frames = frames_manager->GetAllWebFrames();
for (WebFrame* frame : all_frames) {
FindInPageJavaScriptFeature::GetInstance()->Stop(frame);
}
if (delegate_) {
delegate_->DidHighlightMatches(this, web_state_,
last_find_request_.GetTotalMatchCount(),
last_find_request_.GetRequestQuery());
}
}
bool JavaScriptFindInPageManagerImpl::CanSearchContent() {
return web_state_->ContentIsHTML();
}
void JavaScriptFindInPageManagerImpl::ProcessFindInPageResult(
const std::string& frame_id,
const int unique_id,
std::optional<int> result_matches) {
if (unique_id != last_find_request_.GetRequestId()) {
// New find was started or current find was stopped.
return;
}
if (!web_state_) {
// WebState was destroyed before find finished.
return;
}
FindInPageJavaScriptFeature* feature =
FindInPageJavaScriptFeature::GetInstance();
WebFrame* frame =
feature->GetWebFramesManager(web_state_)->GetFrameWithId(frame_id);
if (!result_matches || !frame) {
// The frame no longer exists or the function call timed out. In both cases,
// result will be null.
// Zero out count to ensure every frame is updated for every find.
last_find_request_.SetMatchCountForFrame(0, frame_id);
} else {
// If response is equal to kFindInPagePending, find did not finish in the
// JavaScript. Call pumpSearch to continue find.
if (result_matches.value() == find_in_page::kFindInPagePending) {
feature->Pump(
frame, base::BindOnce(
&JavaScriptFindInPageManagerImpl::ProcessFindInPageResult,
weak_factory_.GetWeakPtr(), frame_id, unique_id));
return;
}
last_find_request_.SetMatchCountForFrame(result_matches.value(), frame_id);
}
last_find_request_.DidReceiveFindResponseFromOneFrame();
if (last_find_request_.AreAllFindResponsesReturned()) {
LastFindRequestCompleted();
}
}
void JavaScriptFindInPageManagerImpl::LastFindRequestCompleted() {
if (delegate_) {
delegate_->DidHighlightMatches(this, web_state_,
last_find_request_.GetTotalMatchCount(),
last_find_request_.GetRequestQuery());
}
int total_matches = last_find_request_.GetTotalMatchCount();
if (total_matches == 0) {
return;
}
if (last_find_request_.GoToFirstMatch()) {
SelectCurrentMatch();
}
}
void JavaScriptFindInPageManagerImpl::SelectDidFinish(
const base::Value* result) {
std::string match_context_string;
if (result && result->is_dict()) {
const base::Value::Dict& result_dict = result->GetDict();
// Get updated match count.
const std::optional<double> matches =
result_dict.FindDouble(kSelectAndScrollResultMatches);
if (matches) {
int match_count = static_cast<int>(matches.value());
if (match_count != last_find_request_.GetMatchCountForSelectedFrame()) {
last_find_request_.SetMatchCountForSelectedFrame(match_count);
if (delegate_) {
delegate_->DidHighlightMatches(
this, web_state_, last_find_request_.GetTotalMatchCount(),
last_find_request_.GetRequestQuery());
}
}
}
// Get updated currently selected index.
const std::optional<double> index =
result_dict.FindDouble(kSelectAndScrollResultIndex);
if (index) {
int current_index = static_cast<int>(index.value());
last_find_request_.SetCurrentSelectedMatchFrameIndex(current_index);
}
// Get context string.
const std::string* context_string =
result_dict.FindString(kSelectAndScrollResultContextString);
if (context_string) {
match_context_string = *context_string;
}
}
if (delegate_) {
delegate_->DidSelectMatch(
this, web_state_, last_find_request_.GetCurrentSelectedMatchPageIndex(),
base::SysUTF8ToNSString(match_context_string));
}
}
void JavaScriptFindInPageManagerImpl::SelectNextMatch() {
if (last_find_request_.GetTotalMatchCount() > 2) {
// Only record if number of matches is greater than 2 so
// JavaScriptFindInPageManagerImpl and FindInPageManagerImpl can be
// compared. The latter lacks the ability to differentiate between FindNext
// and FindPrevious metrics for 2 matches or less.
RecordFindNextAction();
}
if (last_find_request_.GoToNextMatch()) {
SelectCurrentMatch();
}
}
void JavaScriptFindInPageManagerImpl::SelectPreviousMatch() {
if (last_find_request_.GetTotalMatchCount() > 2) {
// Only record if number of matches is greater than 2 so
// JavaScriptFindInPageManagerImpl and FindInPageManagerImpl can be
// compared. The latter lacks the ability to differentiate between FindNext
// and FindPrevious metrics for 2 matches or less.
RecordFindPreviousAction();
}
if (last_find_request_.GoToPreviousMatch()) {
SelectCurrentMatch();
}
}
void JavaScriptFindInPageManagerImpl::SelectCurrentMatch() {
FindInPageJavaScriptFeature* feature =
FindInPageJavaScriptFeature::GetInstance();
WebFrame* frame =
feature->GetWebFramesManager(web_state_)
->GetFrameWithId(last_find_request_.GetSelectedFrameId());
if (frame) {
feature->SelectMatch(
frame, last_find_request_.GetCurrentSelectedMatchFrameIndex(),
base::BindOnce(&JavaScriptFindInPageManagerImpl::SelectDidFinish,
weak_factory_.GetWeakPtr()));
}
}
WEB_STATE_USER_DATA_KEY_IMPL(JavaScriptFindInPageManager)
} // namespace web