1 /*
2  * Copyright (C) 2009 Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google Inc. nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30 
31 #include "third_party/blink/renderer/core/frame/find_in_page.h"
32 
33 #include <utility>
34 
35 #include "third_party/blink/public/web/web_document.h"
36 #include "third_party/blink/public/web/web_local_frame_client.h"
37 #include "third_party/blink/public/web/web_plugin.h"
38 #include "third_party/blink/public/web/web_plugin_document.h"
39 #include "third_party/blink/public/web/web_widget_client.h"
40 #include "third_party/blink/renderer/core/display_lock/display_lock_document_state.h"
41 #include "third_party/blink/renderer/core/editing/finder/text_finder.h"
42 #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
43 #include "third_party/blink/renderer/core/layout/layout_view.h"
44 #include "third_party/blink/renderer/core/page/focus_controller.h"
45 #include "third_party/blink/renderer/core/page/page.h"
46 
47 namespace blink {
48 
FindInPage(WebLocalFrameImpl & frame,InterfaceRegistry * interface_registry)49 FindInPage::FindInPage(WebLocalFrameImpl& frame,
50                        InterfaceRegistry* interface_registry)
51     : frame_(&frame) {
52   // TODO(rakina): Use InterfaceRegistry of |frame| directly rather than passing
53   // both of them.
54   if (!interface_registry)
55     return;
56   // TODO(crbug.com/800641): Use InterfaceValidator when it works for associated
57   // interfaces.
58   interface_registry->AddAssociatedInterface(WTF::BindRepeating(
59       &FindInPage::BindToReceiver, WrapWeakPersistent(this)));
60 }
61 
Find(int request_id,const String & search_text,mojom::blink::FindOptionsPtr options)62 void FindInPage::Find(int request_id,
63                       const String& search_text,
64                       mojom::blink::FindOptionsPtr options) {
65   DCHECK(!search_text.IsEmpty());
66 
67   // Record the fact that we have a find-in-page request.
68   frame_->GetFrame()->GetDocument()->MarkHasFindInPageRequest();
69 
70   blink::WebPlugin* plugin = GetWebPluginForFind();
71   // Check if the plugin still exists in the document.
72   if (plugin) {
73     if (!options->new_session) {
74       // Just navigate back/forward.
75       plugin->SelectFindResult(options->forward, request_id);
76       LocalFrame* core_frame = frame_->GetFrame();
77       core_frame->GetPage()->GetFocusController().SetFocusedFrame(core_frame);
78     } else if (!plugin->StartFind(search_text, options->match_case,
79                                   request_id)) {
80       // Send "no results"
81       ReportFindInPageMatchCount(request_id, 0 /* count */,
82                                  true /* final_update */);
83     }
84     return;
85   }
86 
87   // Send "no results" if this frame has no visible content.
88   if (!frame_->HasVisibleContent() && !options->force) {
89     ReportFindInPageMatchCount(request_id, 0 /* count */,
90                                true /* final_update */);
91     return;
92   }
93 
94   WebRange current_selection = frame_->SelectionRange();
95   bool result = false;
96   bool active_now = false;
97 
98   if (options->new_session)
99     EnsureTextFinder().InitNewSession(*options);
100 
101   // Search for an active match only if this frame is focused or if this is an
102   // existing session.
103   if (options->find_match &&
104       (frame_->IsFocused() || !options->new_session)) {
105     result = FindInternal(request_id, search_text, *options,
106                           false /* wrap_within_frame */, &active_now);
107   }
108 
109   if (result && options->new_session) {
110     // Indicate that at least one match has been found. 1 here means
111     // possibly more matches could be coming.
112     ReportFindInPageMatchCount(request_id, 1 /* count */,
113                                false /* final_update */);
114   }
115 
116   // There are three cases in which scoping is needed:
117   //
118   // (1) This is a new find session. This will be its first scoping effort.
119   //
120   // (2) Something has been selected since the last search. This means that we
121   // cannot just increment the current match ordinal; we need to re-generate
122   // it.
123   //
124   // (3) TextFinder::Find() found what should be the next match (|result| is
125   // true), but was unable to activate it (|activeNow| is false). This means
126   // that the text containing this match was dynamically added since the last
127   // scope of the frame. The frame needs to be re-scoped so that any matches
128   // in the new text can be highlighted and included in the reported number of
129   // matches.
130   //
131   // If none of these cases are true, then we just report the current match
132   // count without scoping.
133   if (/* (1) */ !options->new_session && /* (2) */ current_selection.IsNull() &&
134       /* (3) */ !(result && !active_now)) {
135     // Force report of the actual count.
136     EnsureTextFinder().IncreaseMatchCount(request_id, 0);
137     return;
138   }
139 
140   // Start a new scoping  If the scoping function determines that it
141   // needs to scope, it will defer until later.
142   EnsureTextFinder().StartScopingStringMatches(request_id, search_text,
143                                                *options);
144 }
145 
FindForTesting(int identifier,const WebString & search_text,bool match_case,bool forward,bool new_session,bool force,bool wrap_within_frame,bool async)146 bool WebLocalFrameImpl::FindForTesting(int identifier,
147                                        const WebString& search_text,
148                                        bool match_case,
149                                        bool forward,
150                                        bool new_session,
151                                        bool force,
152                                        bool wrap_within_frame,
153                                        bool async) {
154   auto options = mojom::blink::FindOptions::New();
155   options->match_case = match_case;
156   options->forward = forward;
157   options->new_session = new_session;
158   options->force = force;
159   options->run_synchronously_for_testing = !async;
160   bool result = find_in_page_->FindInternal(identifier, search_text, *options,
161                                             wrap_within_frame, nullptr);
162   find_in_page_->StopFinding(
163       mojom::blink::StopFindAction::kStopFindActionKeepSelection);
164   return result;
165 }
166 
FindInternal(int identifier,const WebString & search_text,const mojom::blink::FindOptions & options,bool wrap_within_frame,bool * active_now)167 bool FindInPage::FindInternal(int identifier,
168                               const WebString& search_text,
169                               const mojom::blink::FindOptions& options,
170                               bool wrap_within_frame,
171                               bool* active_now) {
172   if (!frame_->GetFrame())
173     return false;
174 
175   // Unlikely, but just in case we try to find-in-page on a detached frame.
176   DCHECK(frame_->GetFrame()->GetPage());
177 
178   auto forced_activatable_locks = frame_->GetFrame()
179                                       ->GetDocument()
180                                       ->GetDisplayLockDocumentState()
181                                       .GetScopedForceActivatableLocks();
182 
183   // Up-to-date, clean tree is required for finding text in page, since it
184   // relies on TextIterator to look over the text.
185   frame_->GetFrame()->GetDocument()->UpdateStyleAndLayout(
186       DocumentUpdateReason::kFindInPage);
187 
188   return EnsureTextFinder().Find(identifier, search_text, options,
189                                  wrap_within_frame, active_now);
190 }
191 
StopFinding(mojom::StopFindAction action)192 void FindInPage::StopFinding(mojom::StopFindAction action) {
193   WebPlugin* const plugin = GetWebPluginForFind();
194   if (plugin) {
195     plugin->StopFind();
196     return;
197   }
198 
199   const bool clear_selection =
200       action == mojom::StopFindAction::kStopFindActionClearSelection;
201   if (clear_selection)
202     frame_->ExecuteCommand(WebString::FromUTF8("Unselect"));
203 
204   if (GetTextFinder()) {
205     if (!clear_selection)
206       GetTextFinder()->SetFindEndstateFocusAndSelection();
207     GetTextFinder()->StopFindingAndClearSelection();
208   }
209 
210   if (action == mojom::StopFindAction::kStopFindActionActivateSelection &&
211       frame_->IsFocused()) {
212     WebDocument doc = frame_->GetDocument();
213     if (!doc.IsNull()) {
214       WebElement element = doc.FocusedElement();
215       if (!element.IsNull())
216         element.SimulateClick();
217     }
218   }
219 }
220 
FindMatchMarkersVersion() const221 int FindInPage::FindMatchMarkersVersion() const {
222   if (GetTextFinder())
223     return GetTextFinder()->FindMatchMarkersVersion();
224   return 0;
225 }
226 
ActiveFindMatchRect()227 gfx::RectF FindInPage::ActiveFindMatchRect() {
228   if (GetTextFinder())
229     return GetTextFinder()->ActiveFindMatchRect();
230   return gfx::RectF();
231 }
232 
ActivateNearestFindResult(int request_id,const gfx::PointF & point)233 void FindInPage::ActivateNearestFindResult(int request_id,
234                                            const gfx::PointF& point) {
235   gfx::Rect active_match_rect;
236   const int ordinal =
237       EnsureTextFinder().SelectNearestFindMatch(point, &active_match_rect);
238   if (ordinal == -1) {
239     // Something went wrong, so send a no-op reply (force the frame to report
240     // the current match count) in case the host is waiting for a response due
241     // to rate-limiting.
242     EnsureTextFinder().IncreaseMatchCount(request_id, 0);
243     return;
244   }
245   ReportFindInPageSelection(request_id, ordinal, active_match_rect,
246                             true /* final_update */);
247 }
248 
SetClient(mojo::PendingRemote<mojom::blink::FindInPageClient> remote)249 void FindInPage::SetClient(
250     mojo::PendingRemote<mojom::blink::FindInPageClient> remote) {
251   // TODO(crbug.com/984878): Having to call reset() to try to bind a remote that
252   // might be bound is questionable behavior and suggests code may be buggy.
253   client_.reset();
254   client_.Bind(std::move(remote));
255 }
256 
GetNearestFindResult(const gfx::PointF & point,GetNearestFindResultCallback callback)257 void FindInPage::GetNearestFindResult(const gfx::PointF& point,
258                                       GetNearestFindResultCallback callback) {
259   float distance;
260   EnsureTextFinder().NearestFindMatch(FloatPoint(point), &distance);
261   std::move(callback).Run(distance);
262 }
263 
FindMatchRects(int current_version,FindMatchRectsCallback callback)264 void FindInPage::FindMatchRects(int current_version,
265                                 FindMatchRectsCallback callback) {
266   int rects_version = FindMatchMarkersVersion();
267   Vector<gfx::RectF> rects;
268   if (current_version != rects_version)
269     rects = EnsureTextFinder().FindMatchRects();
270   std::move(callback).Run(rects_version, rects, ActiveFindMatchRect());
271 }
272 
ClearActiveFindMatch()273 void FindInPage::ClearActiveFindMatch() {
274   // TODO(rakina): Do collapse selection as this currently does nothing.
275   frame_->ExecuteCommand(WebString::FromUTF8("CollapseSelection"));
276   EnsureTextFinder().ClearActiveFindMatch();
277 }
278 
SetTickmarks(const WebElement & target,const WebVector<WebRect> & tickmarks)279 void WebLocalFrameImpl::SetTickmarks(const WebElement& target,
280                                      const WebVector<WebRect>& tickmarks) {
281   find_in_page_->SetTickmarks(target, tickmarks);
282 }
283 
SetTickmarks(const WebElement & target,const WebVector<WebRect> & tickmarks)284 void FindInPage::SetTickmarks(const WebElement& target,
285                               const WebVector<WebRect>& tickmarks) {
286   Vector<IntRect> tickmarks_converted(SafeCast<wtf_size_t>(tickmarks.size()));
287   for (wtf_size_t i = 0; i < tickmarks.size(); ++i)
288     tickmarks_converted[i] = tickmarks[i];
289 
290   LayoutBox* box;
291   if (target.IsNull())
292     box = frame_->GetFrame()->ContentLayoutObject();
293   else
294     box = target.ConstUnwrap<Element>()->GetLayoutBoxForScrolling();
295   if (box)
296     box->OverrideTickmarks(std::move(tickmarks_converted));
297 }
298 
GetTextFinder() const299 TextFinder* WebLocalFrameImpl::GetTextFinder() const {
300   return find_in_page_->GetTextFinder();
301 }
302 
GetTextFinder() const303 TextFinder* FindInPage::GetTextFinder() const {
304   return text_finder_;
305 }
306 
EnsureTextFinder()307 TextFinder& WebLocalFrameImpl::EnsureTextFinder() {
308   return find_in_page_->EnsureTextFinder();
309 }
310 
EnsureTextFinder()311 TextFinder& FindInPage::EnsureTextFinder() {
312   if (!text_finder_)
313     text_finder_ = MakeGarbageCollected<TextFinder>(*frame_);
314 
315   return *text_finder_;
316 }
317 
SetPluginFindHandler(WebPluginContainer * plugin)318 void FindInPage::SetPluginFindHandler(WebPluginContainer* plugin) {
319   plugin_find_handler_ = plugin;
320 }
321 
PluginFindHandler() const322 WebPluginContainer* FindInPage::PluginFindHandler() const {
323   return plugin_find_handler_;
324 }
325 
GetWebPluginForFind()326 WebPlugin* FindInPage::GetWebPluginForFind() {
327   if (frame_->GetDocument().IsPluginDocument())
328     return frame_->GetDocument().To<WebPluginDocument>().Plugin();
329   if (plugin_find_handler_)
330     return plugin_find_handler_->Plugin();
331   return nullptr;
332 }
333 
BindToReceiver(mojo::PendingAssociatedReceiver<mojom::blink::FindInPage> receiver)334 void FindInPage::BindToReceiver(
335     mojo::PendingAssociatedReceiver<mojom::blink::FindInPage> receiver) {
336   receiver_.Bind(std::move(receiver),
337                  frame_->GetTaskRunner(blink::TaskType::kInternalDefault));
338 }
339 
Dispose()340 void FindInPage::Dispose() {
341   receiver_.reset();
342 }
343 
ReportFindInPageMatchCount(int request_id,int count,bool final_update)344 void FindInPage::ReportFindInPageMatchCount(int request_id,
345                                             int count,
346                                             bool final_update) {
347   // In tests, |client_| might not be set.
348   if (!client_)
349     return;
350   client_->SetNumberOfMatches(
351       request_id, count,
352       final_update ? mojom::blink::FindMatchUpdateType::kFinalUpdate
353                    : mojom::blink::FindMatchUpdateType::kMoreUpdatesComing);
354 }
355 
ReportFindInPageSelection(int request_id,int active_match_ordinal,const gfx::Rect & selection_rect,bool final_update)356 void FindInPage::ReportFindInPageSelection(int request_id,
357                                            int active_match_ordinal,
358                                            const gfx::Rect& selection_rect,
359                                            bool final_update) {
360   // In tests, |client_| might not be set.
361   if (!client_)
362     return;
363   client_->SetActiveMatch(
364       request_id, selection_rect, active_match_ordinal,
365       final_update ? mojom::blink::FindMatchUpdateType::kFinalUpdate
366                    : mojom::blink::FindMatchUpdateType::kMoreUpdatesComing);
367 }
368 
369 }  // namespace blink
370