chromium/ios/web/find_in_page/java_script_find_in_page_manager_impl.mm

// 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