| // 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 <algorithm> |
| #include <utility> |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/queue.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/task/single_thread_task_runner.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" |
| #include "content/public/common/content_features.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // The following functions allow traversal over all RenderFrameHosts, including |
| // those across WebContentses. |
| // |
| // An inner WebContents may be embedded in an outer WebContents via an inner |
| // WebContentsTreeNode of the outer WebContents's WebContentsTreeNode. |
| std::vector<RenderFrameHostImpl*> GetChildren(RenderFrameHostImpl* rfh) { |
| std::vector<RenderFrameHostImpl*> children; |
| children.reserve(rfh->child_count()); |
| for (size_t i = 0; i != rfh->child_count(); ++i) { |
| if (auto* contents = static_cast<WebContentsImpl*>( |
| WebContentsImpl::FromOuterFrameTreeNode(rfh->child_at(i)))) { |
| // 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 = std::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 = std::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) { |
| if (base::FeatureList::IsEnabled(features::kGuestViewMPArch)) { |
| NOTIMPLEMENTED(); |
| return false; |
| } else { |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(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<FindRequestManager> 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<gfx::RectF>& 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_); |
| |
| 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::SingleThreadTaskRunner::GetCurrentDefault()->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<FindRequest>(); |
| |
| find_request_queue_.emplace(request_id, search_text, std::move(options)); |
| if (find_request_queue_.size() == 1) |
| FindInternal(find_request_queue_.front()); |
| if (request_id == current_session_id_) |
| find_request_queue_.pop(); |
| } |
| |
| void FindRequestManager::ForEachAddedFindInPageRenderFrameHost( |
| base::FunctionRef<void(RenderFrameHostImpl*)> func_ref) { |
| contents_->GetPrimaryMainFrame()->ForEachRenderFrameHostImpl( |
| [this, func_ref](RenderFrameHostImpl* rfh) { |
| if (!CheckFrame(rfh)) |
| return; |
| 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<blink::mojom::StopFindAction>(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<WebContentsImpl*>(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 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); |
| } else { |
| // If there's no FindInPageClient for `rfh`, the state related to it must |
| // have been cleared already. |
| return; |
| } |
| |
| // 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<FindRequest>(); |
| |
| // 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 (current_session_id_ == kInvalidId) { |
| // Just remove `rfh` from things that might point to it, but don't trigger |
| // any extra processing as there is no current find session ongoing. |
| pending_initial_replies_.erase(rfh); |
| if (pending_find_next_reply_ == rfh) { |
| pending_find_next_reply_ = nullptr; |
| } |
| 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<RenderFrameHostImpl*>(rfh)->GetFindInPage()->StopFinding( |
| blink::mojom::StopFindAction::kStopFindActionClearSelection); |
| |
| // 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<gfx::RectF>& 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 |
| // ForEachRenderFrameHostImpl instead of ForEachAddedFindInPageRenderFrameHost |
| // because that calls CheckFrame() which will only be true if we've called |
| // AddFrame() for the frame. |
| contents_->GetPrimaryMainFrame()->ForEachRenderFrameHostImpl( |
| [this](RenderFrameHostImpl* rfh) { |
| auto* wc = WebContents::FromRenderFrameHost(rfh); |
| // Make sure each WebContents is only added once. |
| if (rfh->IsInPrimaryMainFrame()) { |
| frame_observers_.push_back(std::make_unique<FrameObserver>(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<RenderFrameHostImpl*>(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(); |
| } |
| |
| // 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<RenderFrameHostImpl*>(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<RenderFrameHostImpl*>(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<RenderFrameHostImpl*>(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/40196212): 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<FindInPageClient> 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<FindInPageClient>(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<gfx::RectF> 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<gfx::RectF>& 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 |