// Copyright 2016 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "content/browser/find_request_manager.h" #include #include "base/bind.h" #include "base/containers/contains.h" #include "base/containers/queue.h" #include "base/memory/raw_ptr.h" #include "base/metrics/histogram_macros.h" #include "base/ranges/algorithm.h" #include "base/threading/thread_task_runner_handle.h" #include "build/build_config.h" #include "content/browser/find_in_page_client.h" #include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/public/browser/content_browser_client.h" #include "content/public/browser/render_frame_host.h" #include "content/public/common/content_client.h" namespace content { namespace { // The following functions allow traversal over all RenderFrameHosts, including // those across WebContentses. Excludes portals as they are not relevant for // find-in-page. // // An inner WebContents may be embedded in an outer WebContents via an inner // WebContentsTreeNode of the outer WebContents's WebContentsTreeNode. // Returns all child RenderFrameHosts of |rfh| except those in portals. std::vector GetChildren(RenderFrameHostImpl* rfh) { std::vector children; children.reserve(rfh->child_count()); for (size_t i = 0; i != rfh->child_count(); ++i) { if (auto* contents = static_cast( WebContentsImpl::FromOuterFrameTreeNode(rfh->child_at(i)))) { // Portals can't receive keyboard events or be focused, so we don't return // find results inside a portal. if (!contents->IsPortal()) { // If the child is used for an inner WebContents then add the inner // WebContents. children.push_back( contents->GetPrimaryFrameTree().root()->current_frame_host()); } } else { children.push_back(rfh->child_at(i)->current_frame_host()); } } return children; } // Returns the first child RenderFrameHostImpl under |rfh|, if |rfh| has a // child, or nullptr otherwise. RenderFrameHostImpl* GetFirstChild(RenderFrameHostImpl* rfh) { auto children = GetChildren(rfh); if (!children.empty()) return children.front(); return nullptr; } // Returns the last child RenderFrameHostImpl under |rfh|, if |rfh| has a // child, or nullptr otherwise. RenderFrameHostImpl* GetLastChild(RenderFrameHostImpl* rfh) { auto children = GetChildren(rfh); if (!children.empty()) return children.back(); return nullptr; } // Returns the deepest last child frame under |rfh| in the frame tree. RenderFrameHostImpl* GetDeepestLastChild(RenderFrameHostImpl* rfh) { while (RenderFrameHostImpl* last_child = GetLastChild(rfh)) rfh = last_child; return rfh; } // Returns the parent RenderFrameHost of |rfh|, if |rfh| has a parent, or // nullptr otherwise. RenderFrameHostImpl* GetAncestor(RenderFrameHostImpl* rfh) { if (!rfh) return nullptr; return rfh->GetParentOrOuterDocumentOrEmbedder(); } // Returns the previous sibling RenderFrameHostImpl of |rfh|, if one exists, // or nullptr otherwise. RenderFrameHostImpl* GetPreviousSibling(RenderFrameHostImpl* rfh) { if (rfh->PreviousSibling()) { return rfh->PreviousSibling()->current_frame_host(); } // The previous sibling may be in another WebContents. if (RenderFrameHostImpl* parent = GetAncestor(rfh)) { auto children = GetChildren(parent); auto it = base::ranges::find(children, rfh); // It is odd that this rfh may not be a child of its parent, but this is // actually possible during teardown, hence the need for the check for // "it != children.end()". if (it != children.end() && it != children.begin()) return *--it; } return nullptr; } // Returns the next sibling RenderFrameHostImpl of |rfh|, if one exists, or // nullptr otherwise. RenderFrameHostImpl* GetNextSibling(RenderFrameHostImpl* rfh) { if (rfh->NextSibling()) return rfh->NextSibling()->current_frame_host(); // The next sibling may be in another WebContents. if (RenderFrameHostImpl* parent = GetAncestor(rfh)) { auto children = GetChildren(parent); auto it = base::ranges::find(children, rfh); // It is odd that this RenderFrameHost may not be a child of its parent, but // this is actually possible during teardown, hence the need for the check // for "it != children.end()". if (it != children.end() && ++it != children.end()) return *it; } return nullptr; } // Returns the RenderFrameHostImpl directly after |rfh| in the rfh tree in // search order, or nullptr if one does not exist. If |wrap| is set, then // wrapping between the first and last frames is permitted. Note that this // traversal follows the same ordering as in // blink::FrameTree::traverseNextWithWrap(). RenderFrameHostImpl* TraverseNext(RenderFrameHostImpl* rfh, bool wrap) { if (RenderFrameHostImpl* first_child = GetFirstChild(rfh)) return first_child; RenderFrameHostImpl* sibling = GetNextSibling(rfh); while (!sibling) { RenderFrameHostImpl* parent = GetAncestor(rfh); if (!parent) return wrap ? rfh : nullptr; rfh = parent; sibling = GetNextSibling(rfh); } return sibling; } // Returns the RenderFrameHostImpl directly before |rfh| in the frame tree in // search order, or nullptr if one does not exist. If |wrap| is set, then // wrapping between the first and last frames is permitted. Note that this // traversal follows the same ordering as in // blink::FrameTree::traversePreviousWithWrap(). RenderFrameHostImpl* TraversePrevious(RenderFrameHostImpl* rfh, bool wrap) { if (RenderFrameHostImpl* previous_sibling = GetPreviousSibling(rfh)) return GetDeepestLastChild(previous_sibling); if (RenderFrameHostImpl* parent = GetAncestor(rfh)) return parent; return wrap ? GetDeepestLastChild(rfh) : nullptr; } // The same as either TraverseNext() or TraversePrevious(), depending on // |forward|. RenderFrameHostImpl* TraverseFrame(RenderFrameHostImpl* rfh, bool forward, bool wrap) { return forward ? TraverseNext(rfh, wrap) : TraversePrevious(rfh, wrap); } bool IsFindInPageDisabled(RenderFrameHost* rfh) { return rfh && GetContentClient()->browser()->IsFindInPageDisabledForOrigin( rfh->GetLastCommittedOrigin()); } bool IsUnattachedGuestView(RenderFrameHost* rfh) { WebContentsImpl* web_contents = static_cast(WebContents::FromRenderFrameHost(rfh)); if (!web_contents->IsGuest()) return false; return !web_contents->GetOuterWebContents(); } // kMinKeystrokesWithoutDelay should be high enough that script in the page // can't provide every possible search result at the same time. constexpr int kMinKeystrokesWithoutDelay = 4; // The delay for very short queries, before sending find requests. This should // be higher than the duration in between two keystrokes. This is based on // WebCore.FindInPage.DurationBetweenKeystrokes metrics, this is higher than // 90% of them. constexpr int kDelayMs = 400; } // namespace // Observes searched WebContentses for RenderFrameHost state updates, including // deletion and loads. class FindRequestManager::FrameObserver : public WebContentsObserver { public: FrameObserver(WebContents* web_contents, FindRequestManager* manager) : WebContentsObserver(web_contents), manager_(manager) {} FrameObserver(const FrameObserver&) = delete; FrameObserver& operator=(const FrameObserver&) = delete; ~FrameObserver() override = default; void RenderFrameDeleted(RenderFrameHost* rfh) override { manager_->RemoveFrame(rfh); } void RenderFrameHostStateChanged( RenderFrameHost* rfh, RenderFrameHost::LifecycleState old_state, RenderFrameHost::LifecycleState new_state) override { if (manager_->current_session_id_ == kInvalidId || IsFindInPageDisabled(rfh)) { return; } if (new_state == RenderFrameHost::LifecycleState::kActive) { // Add the RFH to the current find-in-page session when its status // changes to active since this is when the document becomes part of the // primary page (i.e prerendered pages getting activated, or pages in // BackForwardCache getting restored), so that we can get the results from // all frames in the primary page. manager_->AddFrame(rfh, false /* force */); } else if (old_state == RenderFrameHost::LifecycleState::kActive) { // Remove the RFH from the current find-in-page session if it stops being // part of the primary page. manager_->RemoveFrame(rfh); } } void DidFinishLoad(RenderFrameHost* rfh, const GURL& validated_url) override { if (manager_->current_session_id_ == kInvalidId) return; manager_->RemoveFrame(rfh); // Make sure RenderFrameDeleted will be called to clean up DCHECK(rfh->IsRenderFrameLive()); if (IsFindInPageDisabled(rfh)) return; manager_->AddFrame(rfh, /*force=*/true); } private: // The FindRequestManager that owns this FrameObserver. const raw_ptr manager_; }; bool FindRequestManager::RunDelayedFindTaskForTesting() { if (!delayed_find_task_.IsCancelled()) { delayed_find_task_.callback().Run(); return true; } return false; } FindRequestManager::FindRequest::FindRequest() = default; FindRequestManager::FindRequest::FindRequest( int id, const std::u16string& search_text, blink::mojom::FindOptionsPtr options) : id(id), search_text(search_text), options(std::move(options)) {} FindRequestManager::FindRequest::FindRequest(const FindRequest& request) : id(request.id), search_text(request.search_text), options(request.options.Clone()) {} FindRequestManager::FindRequest::~FindRequest() = default; FindRequestManager::FindRequest& FindRequestManager::FindRequest::operator=( const FindRequest& request) { id = request.id; search_text = request.search_text; options = request.options.Clone(); return *this; } #if BUILDFLAG(IS_ANDROID) FindRequestManager::ActivateNearestFindResultState:: ActivateNearestFindResultState() = default; FindRequestManager::ActivateNearestFindResultState:: ActivateNearestFindResultState(float x, float y) : current_request_id(GetNextID()), point(x, y) {} FindRequestManager::ActivateNearestFindResultState:: ~ActivateNearestFindResultState() = default; FindRequestManager::FrameRects::FrameRects() = default; FindRequestManager::FrameRects::FrameRects(const std::vector& rects, int version) : rects(rects), version(version) {} FindRequestManager::FrameRects::~FrameRects() = default; FindRequestManager::FindMatchRectsState::FindMatchRectsState() = default; FindRequestManager::FindMatchRectsState::~FindMatchRectsState() = default; #endif // static const int FindRequestManager::kInvalidId = -1; FindRequestManager::FindRequestManager(WebContentsImpl* web_contents) : contents_(web_contents) {} FindRequestManager::~FindRequestManager() = default; void FindRequestManager::Find(int request_id, const std::u16string& search_text, blink::mojom::FindOptionsPtr options, bool skip_delay) { // Every find request must have a unique ID, and these IDs must strictly // increase so that newer requests always have greater IDs than older // requests. DCHECK_GT(request_id, current_request_.id); DCHECK_GT(request_id, current_session_id_); // TODO(crbug.com/1250158): Remove this when we decide how long the // find-in-page delay should be. if (options->new_session) { base::TimeTicks now = base::TimeTicks::Now(); if (!last_time_typed_.is_null() && base::StartsWith(search_text, last_searched_text_)) { base::TimeDelta elapsed = now - last_time_typed_; // If we waited more than 5 seconds, the user probably is searching for // something else now. if (elapsed.InSecondsF() <= 5.f) { UMA_HISTOGRAM_TIMES("WebCore.FindInPage.DurationBetweenKeystrokes", elapsed); } } last_time_typed_ = now; last_searched_text_ = search_text; } if (skip_delay) { delayed_find_task_.Cancel(); EmitFindRequest(request_id, search_text, std::move(options)); return; } if (!options->new_session) { // If the user presses enter while we are waiting for a delayed find, then // run the find now to improve responsiveness. if (!delayed_find_task_.IsCancelled()) { delayed_find_task_.callback().Run(); } else { EmitFindRequest(request_id, search_text, std::move(options)); } return; } if (search_text.length() < kMinKeystrokesWithoutDelay) { delayed_find_task_.Reset(base::BindOnce( &FindRequestManager::EmitFindRequest, weak_factory_.GetWeakPtr(), request_id, search_text, std::move(options))); base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, delayed_find_task_.callback(), base::Milliseconds(kDelayMs)); return; } // If we aren't going to delay, then clear any previous attempts to delay. delayed_find_task_.Cancel(); EmitFindRequest(request_id, search_text, std::move(options)); } void FindRequestManager::EmitFindRequest(int request_id, const std::u16string& search_text, blink::mojom::FindOptionsPtr options) { // If this is a new find session, clear any queued requests from last session. if (options->new_session) find_request_queue_ = base::queue(); find_request_queue_.emplace(request_id, search_text, std::move(options)); if (find_request_queue_.size() == 1) FindInternal(find_request_queue_.front()); } void FindRequestManager::ForEachAddedFindInPageRenderFrameHost( base::FunctionRef func_ref) { contents_->GetPrimaryMainFrame()->ForEachRenderFrameHost( [this, func_ref](RenderFrameHostImpl* rfh) { if (!CheckFrame(rfh)) return; // A Portal's RenderFrameHost can't reach here because we don't observe // Portals WebContents (see FindRequestManager::FindInternal()). DCHECK(!WebContents::FromRenderFrameHost(rfh)->IsPortal()); DCHECK(rfh->IsRenderFrameLive()); DCHECK(rfh->IsActive()); func_ref(rfh); }); } void FindRequestManager::StopFinding(StopFindAction action) { // Cancel any delayed find-in-page requests delayed_find_task_.Cancel(); ForEachAddedFindInPageRenderFrameHost([action](RenderFrameHostImpl* rfh) { rfh->GetFindInPage()->StopFinding( // TODO(dcheng): Use typemapping or use the Mojo enum directly. static_cast(action)); }); current_session_id_ = kInvalidId; #if BUILDFLAG(IS_ANDROID) // It is important that these pending replies are cleared whenever a find // session ends, so that subsequent replies for the old session are ignored. activate_.pending_replies.clear(); match_rects_.pending_replies.clear(); #endif } bool FindRequestManager::ShouldIgnoreReply(RenderFrameHostImpl* rfh, int request_id) { // Ignore stale replies from abandoned find sessions or dead frames. return current_session_id_ == kInvalidId || request_id < current_session_id_ || !CheckFrame(rfh); } void FindRequestManager::HandleFinalUpdateForFrame(RenderFrameHostImpl* rfh, int request_id) { // This is the final update for this frame for the current find operation. pending_initial_replies_.erase(rfh); if (request_id == current_session_id_ && !pending_initial_replies_.empty()) { NotifyFindReply(request_id, false /* final_update */); return; } // This is the final update for all frames for the current find operation. if (request_id == current_request_.id && request_id != current_session_id_) { DCHECK(!current_request_.options->new_session); DCHECK_EQ(pending_find_next_reply_, rfh); pending_find_next_reply_ = nullptr; } FinalUpdateReceived(request_id, rfh); } void FindRequestManager::UpdatedFrameNumberOfMatches(RenderFrameHostImpl* rfh, unsigned int old_count, unsigned int new_count) { if (old_count == new_count) return; // Change the number of matches for this frame in the global count. number_of_matches_ -= old_count; number_of_matches_ += new_count; // All matches may have been removed since the last find reply. if (rfh == active_frame_ && !new_count) relative_active_match_ordinal_ = 0; // The active match ordinal may need updating since the number of matches // before the active match may have changed. UpdateActiveMatchOrdinal(); } void FindRequestManager::SetActiveMatchRect( const gfx::Rect& active_match_rect) { selection_rect_ = active_match_rect; } void FindRequestManager::SetActiveMatchOrdinal(RenderFrameHostImpl* rfh, int request_id, int active_match_ordinal) { if (active_match_ordinal > 0) { // Call SetFocusedFrame on the WebContents associated with |rfh| (which // might not be the same as |contents_|, as a WebContents might have // inner WebContents). We need to focus on the frame where the active // match is in, which should be in the |rfh|'s associated WebContents. WebContentsImpl* web_contents = static_cast(WebContents::FromRenderFrameHost(rfh)); // Do not focus inactive RenderFrameHost. if (!rfh->IsActive()) return; web_contents->SetFocusedFrame(rfh->frame_tree_node(), rfh->GetSiteInstance()->group()); } if (rfh == active_frame_) { active_match_ordinal_ += active_match_ordinal - relative_active_match_ordinal_; relative_active_match_ordinal_ = active_match_ordinal; } else { if (active_frame_) { // The new active match is in a different frame than the previous, so // the previous active frame needs to be informed (to clear its active // match highlighting). ClearActiveFindMatch(); } active_frame_ = rfh; relative_active_match_ordinal_ = active_match_ordinal; UpdateActiveMatchOrdinal(); } if (pending_active_match_ordinal_ && request_id == current_request_.id) pending_active_match_ordinal_ = false; AdvanceQueue(request_id); } void FindRequestManager::RemoveFrame(RenderFrameHost* rfh) { if (current_session_id_ == kInvalidId || !base::Contains(find_in_page_clients_, rfh)) { return; } // Make sure to always clear the highlighted selection. It is useful in case // the user goes back to the same page using the BackForwardCache. static_cast(rfh)->GetFindInPage()->StopFinding( blink::mojom::StopFindAction::kStopFindActionClearSelection); // If matches are counted for the frame that is being removed, decrement the // match total before erasing that entry. auto it = find_in_page_clients_.find(rfh); if (it != find_in_page_clients_.end()) { number_of_matches_ -= it->second->number_of_matches(); find_in_page_clients_.erase(it); } // If this is a primary main frame, then clear the search queue as well, since // we shouldn't be dispatching any more requests. Note that if any other frame // is removed, we can target any queued requests to the focused frame or // primary main frame. However, if the primary main frame is removed we will // not have a valid RenderFrameHost to target for the request queue. if (rfh->IsInPrimaryMainFrame()) find_request_queue_ = base::queue(); // Update the active match ordinal, since it may have changed. if (active_frame_ == rfh) { active_frame_ = nullptr; relative_active_match_ordinal_ = 0; selection_rect_ = gfx::Rect(); } UpdateActiveMatchOrdinal(); #if BUILDFLAG(IS_ANDROID) // The removed frame may contain the nearest find result known so far. Note // that once all queried frames have responded, if this result was the overall // nearest, then no activation will occur. if (rfh == activate_.nearest_frame) activate_.nearest_frame = nullptr; // Match rects in the removed frame are no longer relevant. if (match_rects_.frame_rects.erase(rfh) != 0) ++match_rects_.known_version; // A reply should not be expected from the removed frame. RemoveNearestFindResultPendingReply(rfh); RemoveFindMatchRectsPendingReply(rfh); #endif // If no pending find replies are expected for the removed frame, then just // report the updated results. if (!base::Contains(pending_initial_replies_, rfh) && pending_find_next_reply_ != rfh) { bool final_update = pending_initial_replies_.empty() && !pending_find_next_reply_; NotifyFindReply(current_session_id_, final_update); return; } if (pending_initial_replies_.erase(rfh) != 0) { // A reply should not be expected from the removed frame. if (pending_initial_replies_.empty()) { FinalUpdateReceived(current_session_id_, rfh); } } if (pending_find_next_reply_ == rfh) { // A reply should not be expected from the removed frame. pending_find_next_reply_ = nullptr; FinalUpdateReceived(current_request_.id, rfh); } } void FindRequestManager::ClearActiveFindMatch() { active_frame_->GetFindInPage()->ClearActiveFindMatch(); } #if BUILDFLAG(IS_ANDROID) void FindRequestManager::ActivateNearestFindResult(float x, float y) { if (current_session_id_ == kInvalidId) return; activate_ = ActivateNearestFindResultState(x, y); // Request from each frame the distance to the nearest find result (in that // frame) from the point (x, y), defined in find-in-page coordinates. ForEachAddedFindInPageRenderFrameHost([this](RenderFrameHostImpl* rfh) { activate_.pending_replies.insert(rfh); // Lifetime of FindRequestManager > RenderFrameHost > Mojo // connection, so it's safe to bind |this| and |rfh|. rfh->GetFindInPage()->GetNearestFindResult( activate_.point, base::BindOnce(&FindRequestManager::OnGetNearestFindResultReply, base::Unretained(this), rfh, activate_.current_request_id)); }); } void FindRequestManager::OnGetNearestFindResultReply(RenderFrameHostImpl* rfh, int request_id, float distance) { if (request_id != activate_.current_request_id || !base::Contains(activate_.pending_replies, rfh)) { return; } // Check if this frame has a nearer find result than the current nearest. if (distance < activate_.nearest_distance) { activate_.nearest_frame = rfh; activate_.nearest_distance = distance; } RemoveNearestFindResultPendingReply(rfh); } void FindRequestManager::RequestFindMatchRects(int current_version) { match_rects_.pending_replies.clear(); match_rects_.request_version = current_version; match_rects_.active_rect = gfx::RectF(); // Request the latest find match rects from each frame. ForEachAddedFindInPageRenderFrameHost([this](RenderFrameHostImpl* rfh) { match_rects_.pending_replies.insert(rfh); auto it = match_rects_.frame_rects.find(rfh); int version = (it != match_rects_.frame_rects.end()) ? it->second.version : kInvalidId; // Lifetime of FindRequestManager > RenderFrameHost > Mojo // connection, so it's safe to bind |this| and |rfh|. rfh->GetFindInPage()->FindMatchRects( version, base::BindOnce(&FindRequestManager::OnFindMatchRectsReply, base::Unretained(this), rfh)); }); } void FindRequestManager::OnFindMatchRectsReply( RenderFrameHost* rfh, int version, const std::vector& rects, const gfx::RectF& active_rect) { auto it = match_rects_.frame_rects.find(rfh); if (it == match_rects_.frame_rects.end() || it->second.version != version) { // New version of rects has been received, so update the data. match_rects_.frame_rects[rfh] = FrameRects(rects, version); ++match_rects_.known_version; } if (!active_rect.IsEmpty()) match_rects_.active_rect = active_rect; RemoveFindMatchRectsPendingReply(rfh); } #endif void FindRequestManager::Reset(const FindRequest& initial_request) { current_session_id_ = initial_request.id; current_request_ = initial_request; pending_initial_replies_.clear(); pending_find_next_reply_ = nullptr; pending_active_match_ordinal_ = true; find_in_page_clients_.clear(); number_of_matches_ = 0; active_frame_ = nullptr; relative_active_match_ordinal_ = 0; active_match_ordinal_ = 0; selection_rect_ = gfx::Rect(); last_reported_id_ = kInvalidId; frame_observers_.clear(); #if BUILDFLAG(IS_ANDROID) activate_ = ActivateNearestFindResultState(); match_rects_.pending_replies.clear(); #endif } void FindRequestManager::FindInternal(const FindRequest& request) { DCHECK_GT(request.id, current_request_.id); DCHECK_GT(request.id, current_session_id_); if (!request.options->new_session) { // This is a find next operation. // This implies that there is an ongoing find session with the same search // text. DCHECK_GE(current_session_id_, 0); DCHECK_EQ(request.search_text, current_request_.search_text); // The find next request will be directed at the focused frame if there is // one, or the first frame with matches otherwise. RenderFrameHost* target_rfh = contents_->GetFocusedWebContents()->GetFocusedFrame(); if (!target_rfh || !CheckFrame(target_rfh)) target_rfh = GetInitialFrame(request.options->forward); SendFindRequest(request, target_rfh); current_request_ = request; pending_active_match_ordinal_ = true; return; } // This is an initial find operation. Reset(request); // Add and observe eligible RFHs in the WebContents. And, use // ForEachRenderFrameHost instead of ForEachAddedFindInPageRenderFrameHost // because that calls CheckFrame() which will only be true if we've called // AddFrame() for the frame. contents_->GetPrimaryMainFrame()->ForEachRenderFrameHost( [this](RenderFrameHostImpl* rfh) { // Portals can't receive keyboard events or be focused, so we don't // return find results inside a portal. auto* wc = WebContents::FromRenderFrameHost(rfh); if (wc->IsPortal()) return; // Make sure each WebContents is only added once. if (rfh->IsInPrimaryMainFrame()) { frame_observers_.push_back(std::make_unique(wc, this)); } if (IsFindInPageDisabled(rfh)) return; AddFrame(rfh, false /* force */); }); } void FindRequestManager::AdvanceQueue(int request_id) { if (find_request_queue_.empty() || request_id != find_request_queue_.front().id) { return; } find_request_queue_.pop(); if (!find_request_queue_.empty()) FindInternal(find_request_queue_.front()); } void FindRequestManager::SendFindRequest(const FindRequest& request, RenderFrameHost* rfh) { DCHECK(CheckFrame(rfh)); DCHECK(rfh->IsRenderFrameLive()); DCHECK(rfh->IsActive()); if (request.options->new_session) pending_initial_replies_.insert(rfh); else pending_find_next_reply_ = rfh; static_cast(rfh)->GetFindInPage()->Find( request.id, base::UTF16ToUTF8(request.search_text), request.options.Clone()); } void FindRequestManager::NotifyFindReply(int request_id, bool final_update) { if (request_id == kInvalidId) { NOTREACHED(); return; } // Ensure that replies are not reported with IDs lower than the ID of the // latest request we have results from. if (request_id < last_reported_id_) request_id = last_reported_id_; else last_reported_id_ = request_id; contents_->NotifyFindReply(request_id, number_of_matches_, selection_rect_, active_match_ordinal_, final_update); } RenderFrameHost* FindRequestManager::GetInitialFrame(bool forward) const { RenderFrameHost* rfh = contents_->GetPrimaryFrameTree().root()->current_frame_host(); if (!forward) rfh = GetDeepestLastChild(static_cast(rfh)); return rfh; } RenderFrameHost* FindRequestManager::Traverse(RenderFrameHost* from_rfh, bool forward, bool matches_only, bool wrap) const { DCHECK(from_rfh); // If |from_rfh| is being detached, it might already be removed from // its parent's list of children, meaning we can't traverse it correctly. // We also don't traverse when |from_rfh| is in back-forward cache or is being // prerendered, as we don't allow any updates in these states. auto* from_rfh_impl = static_cast(from_rfh); if (from_rfh_impl->IsPendingDeletion() || from_rfh_impl->IsInBackForwardCache() || from_rfh_impl->lifecycle_state() == RenderFrameHostImpl::LifecycleStateImpl::kPrerendering) { return nullptr; } RenderFrameHostImpl* rfh = from_rfh_impl; RenderFrameHostImpl* last_frame = rfh; while ((rfh = TraverseFrame(rfh, forward, wrap)) != nullptr) { if (!CheckFrame(rfh)) { // If we're in the same frame as before, we might got into an infinite // loop. if (last_frame == rfh) break; last_frame = rfh; continue; } RenderFrameHost* current_rfh = rfh; if (!matches_only || find_in_page_clients_.find(current_rfh)->second->number_of_matches() || base::Contains(pending_initial_replies_, current_rfh)) { // Note that if there is still a pending reply expected for this frame, // then it may have unaccounted matches and will not be skipped via // |matches_only|. return rfh; } if (wrap && rfh == from_rfh) return nullptr; } return nullptr; } void FindRequestManager::AddFrame(RenderFrameHost* rfh, bool force) { if (!rfh || !rfh->IsRenderFrameLive() || !rfh->IsActive() || IsUnattachedGuestView(rfh)) { return; } // A frame that is already being searched should not normally be added again. DCHECK(force || !CheckFrame(rfh)); DCHECK(!IsFindInPageDisabled(rfh)); find_in_page_clients_[rfh] = CreateFindInPageClient(static_cast(rfh)); FindRequest request = current_request_; request.id = current_session_id_; request.options->new_session = true; request.options->force = force; SendFindRequest(request, rfh); } bool FindRequestManager::CheckFrame(RenderFrameHost* rfh) const { // TODO(crbug.com/1245613): Convert IsFindInPageDisabled to a DCHECK when we // replace DidFinishLoad with DidFinishNavigation in FrameObserver. if (!rfh || !base::Contains(find_in_page_clients_, rfh) || IsFindInPageDisabled(rfh)) { return false; } DCHECK(rfh->IsActive()); return true; } void FindRequestManager::UpdateActiveMatchOrdinal() { active_match_ordinal_ = 0; if (!active_frame_ || !relative_active_match_ordinal_) { DCHECK(!active_frame_ && !relative_active_match_ordinal_); return; } // Traverse the frame tree backwards (in search order) and count all of the // matches in frames before the frame with the active match, in order to // determine the overall active match ordinal. RenderFrameHost* frame = active_frame_; while ((frame = Traverse(frame, false /* forward */, true /* matches_only */, false /* wrap */)) != nullptr) { active_match_ordinal_ += find_in_page_clients_[frame]->number_of_matches(); } active_match_ordinal_ += relative_active_match_ordinal_; } void FindRequestManager::FinalUpdateReceived(int request_id, RenderFrameHost* rfh) { if (!number_of_matches_ || !current_request_.options->find_match || (active_match_ordinal_ && !pending_active_match_ordinal_) || pending_find_next_reply_) { // All the find results for |request_id| are in and ready to report. Note // that |final_update| will be set to false if there are still pending // replies expected from the initial find request. NotifyFindReply(request_id, pending_initial_replies_.empty() /* final_update */); AdvanceQueue(request_id); return; } // There are matches, but no active match was returned, so another find next // request must be sent. RenderFrameHost* target_rfh; if (request_id == current_request_.id && !current_request_.options->new_session) { // If this was a find next operation, then the active match will be in the // next frame with matches after this one. target_rfh = Traverse(rfh, current_request_.options->forward, true /* matches_only */, true /* wrap */); } else if ((target_rfh = contents_->GetFocusedWebContents()->GetFocusedFrame()) != nullptr) { // Otherwise, if there is a focused frame, then the active match will be in // the next frame with matches after that one. target_rfh = Traverse(target_rfh, current_request_.options->forward, true /* matches_only */, true /* wrap */); } else { // Otherwise, the first frame with matches will have the active match. target_rfh = GetInitialFrame(current_request_.options->forward); if (!CheckFrame(target_rfh) || !find_in_page_clients_[target_rfh]->number_of_matches()) { target_rfh = Traverse(target_rfh, current_request_.options->forward, true /* matches_only */, false /* wrap */); } } if (!target_rfh) { // Sometimes when the WebContents is deleted/navigated, we got into this // situation. We don't care about this WebContents anyways so it's ok to // just not ask for the active match and return immediately. // TODO(rakina): Understand what leads to this situation. // See: https://crbug.com/884679. return; } // Forward the find reply without |final_update| set because the active match // has not yet been found. NotifyFindReply(request_id, false /* final_update */); current_request_.options->new_session = false; SendFindRequest(current_request_, target_rfh); } std::unique_ptr FindRequestManager::CreateFindInPageClient( RenderFrameHostImpl* rfh) { if (create_find_in_page_client_for_testing_) return create_find_in_page_client_for_testing_(this, rfh); return std::make_unique(this, rfh); } #if BUILDFLAG(IS_ANDROID) void FindRequestManager::RemoveNearestFindResultPendingReply( RenderFrameHost* rfh) { auto it = activate_.pending_replies.find(rfh); if (it == activate_.pending_replies.end()) return; activate_.pending_replies.erase(it); if (activate_.pending_replies.empty() && CheckFrame(activate_.nearest_frame)) { const auto client_it = find_in_page_clients_.find(activate_.nearest_frame); if (client_it != find_in_page_clients_.end()) client_it->second->ActivateNearestFindResult(current_session_id_, activate_.point); } } void FindRequestManager::RemoveFindMatchRectsPendingReply( RenderFrameHost* rfh) { auto it = match_rects_.pending_replies.find(rfh); if (it == match_rects_.pending_replies.end()) return; match_rects_.pending_replies.erase(it); if (!match_rects_.pending_replies.empty()) return; // All replies are in. std::vector aggregate_rects; if (match_rects_.request_version != match_rects_.known_version) { // Request version is stale, so aggregate and report the newer find // match rects. The rects should be aggregated in search order. for (RenderFrameHost* frame = GetInitialFrame(true /* forward */); frame; frame = Traverse(frame, true /* forward */, true /* matches_only */, false /* wrap */)) { auto frame_it = match_rects_.frame_rects.find(frame); if (frame_it == match_rects_.frame_rects.end()) continue; std::vector& frame_rects = frame_it->second.rects; aggregate_rects.insert(aggregate_rects.end(), frame_rects.begin(), frame_rects.end()); } } contents_->NotifyFindMatchRectsReply( match_rects_.known_version, aggregate_rects, match_rects_.active_rect); } #endif // BUILDFLAG(IS_ANDROID) } // namespace content