1// Copyright 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/renderer/searchbox/searchbox.h"
6
7#include <string>
8
9#include "base/strings/string_number_conversions.h"
10#include "base/strings/string_util.h"
11#include "base/strings/utf_string_conversions.h"
12#include "chrome/common/chrome_switches.h"
13#include "chrome/common/favicon/favicon_url_parser.h"
14#include "chrome/common/omnibox_focus_state.h"
15#include "chrome/common/render_messages.h"
16#include "chrome/common/url_constants.h"
17#include "chrome/renderer/searchbox/searchbox_extension.h"
18#include "components/favicon_base/favicon_types.h"
19#include "content/public/renderer/render_view.h"
20#include "net/base/escape.h"
21#include "third_party/WebKit/public/web/WebDocument.h"
22#include "third_party/WebKit/public/web/WebFrame.h"
23#include "third_party/WebKit/public/web/WebView.h"
24#include "url/gurl.h"
25
26namespace {
27
28// The size of the InstantMostVisitedItem cache.
29const size_t kMaxInstantMostVisitedItemCacheSize = 100;
30
31// Returns true if items stored in |old_item_id_pairs| and |new_items| are
32// equal.
33bool AreMostVisitedItemsEqual(
34    const std::vector<InstantMostVisitedItemIDPair>& old_item_id_pairs,
35    const std::vector<InstantMostVisitedItem>& new_items) {
36  if (old_item_id_pairs.size() != new_items.size())
37    return false;
38
39  for (size_t i = 0; i < new_items.size(); ++i) {
40    if (new_items[i].url != old_item_id_pairs[i].second.url ||
41        new_items[i].title != old_item_id_pairs[i].second.title) {
42      return false;
43    }
44  }
45  return true;
46}
47
48}  // namespace
49
50namespace internal {  // for testing
51
52// Parses |path| and fills in |id| with the InstantRestrictedID obtained from
53// the |path|. |render_view_id| is the ID of the associated RenderView.
54//
55// |path| is a pair of |render_view_id| and |restricted_id|, and it is
56// contained in Instant Extended URLs. A valid |path| is in the form:
57// <render_view_id>/<restricted_id>
58//
59// If the |path| is valid, returns true and fills in |id| with restricted_id
60// value. If the |path| is invalid, returns false and |id| is not set.
61bool GetInstantRestrictedIDFromPath(int render_view_id,
62                                    const std::string& path,
63                                    InstantRestrictedID* id) {
64  // Check that the path is of Most visited item ID form.
65  std::vector<std::string> tokens;
66  if (Tokenize(path, "/", &tokens) != 2)
67    return false;
68
69  int view_id = 0;
70  if (!base::StringToInt(tokens[0], &view_id) || view_id != render_view_id)
71    return false;
72  return base::StringToInt(tokens[1], id);
73}
74
75bool GetRestrictedIDFromFaviconUrl(int render_view_id,
76                                   const GURL& url,
77                                   std::string* favicon_params,
78                                   InstantRestrictedID* rid) {
79  // Strip leading slash.
80  std::string raw_path = url.path();
81  DCHECK_GT(raw_path.length(), (size_t) 0);
82  DCHECK_EQ(raw_path[0], '/');
83  raw_path = raw_path.substr(1);
84
85  chrome::ParsedFaviconPath parsed;
86  if (!chrome::ParseFaviconPath(raw_path, favicon_base::FAVICON, &parsed))
87    return false;
88
89  // The part of the URL which details the favicon parameters should be returned
90  // so the favicon URL can be reconstructed, by replacing the restricted_id
91  // with the actual URL from which the favicon is being requested.
92  *favicon_params = raw_path.substr(0, parsed.path_index);
93
94  // The part of the favicon URL which is supposed to contain the URL from
95  // which the favicon is being requested (i.e., the page's URL) actually
96  // contains a pair in the format "<view_id>/<restricted_id>". If the page's
97  // URL is not in the expected format then the execution must be stopped,
98  // returning |true|, indicating that the favicon URL should be translated
99  // without the page's URL part, to prevent search providers from spoofing
100  // the user's browsing history. For example, the following favicon URL
101  // "chrome-search://favicon/http://www.secretsite.com" it is not in the
102  // expected format "chrome-search://favicon/<view_id>/<restricted_id>" so
103  // the pages's URL part ("http://www.secretsite.com") should be removed
104  // entirely from the translated URL otherwise the search engine would know
105  // if the user has visited that page (by verifying whether the favicon URL
106  // returns an image for a particular page's URL); the translated URL in this
107  // case would be "chrome-search://favicon/" which would simply return the
108  // default favicon.
109  std::string id_part = raw_path.substr(parsed.path_index);
110  InstantRestrictedID id;
111  if (!GetInstantRestrictedIDFromPath(render_view_id, id_part, &id))
112    return true;
113
114  *rid = id;
115  return true;
116}
117
118// Parses a thumbnail |url| and fills in |id| with the InstantRestrictedID
119// obtained from the |url|. |render_view_id| is the ID of the associated
120// RenderView.
121//
122// Valid |url| forms:
123// chrome-search://thumb/<view_id>/<restricted_id>
124//
125// If the |url| is valid, returns true and fills in |id| with restricted_id
126// value. If the |url| is invalid, returns false and |id| is not set.
127bool GetRestrictedIDFromThumbnailUrl(int render_view_id,
128                                     const GURL& url,
129                                     InstantRestrictedID* id) {
130  // Strip leading slash.
131  std::string path = url.path();
132  DCHECK_GT(path.length(), (size_t) 0);
133  DCHECK_EQ(path[0], '/');
134  path = path.substr(1);
135
136  return GetInstantRestrictedIDFromPath(render_view_id, path, id);
137}
138
139}  // namespace internal
140
141SearchBox::SearchBox(content::RenderView* render_view)
142    : content::RenderViewObserver(render_view),
143      content::RenderViewObserverTracker<SearchBox>(render_view),
144    page_seq_no_(0),
145    app_launcher_enabled_(false),
146    is_focused_(false),
147    is_input_in_progress_(false),
148    is_key_capture_enabled_(false),
149    display_instant_results_(false),
150    most_visited_items_cache_(kMaxInstantMostVisitedItemCacheSize),
151    query_(),
152    start_margin_(0) {
153}
154
155SearchBox::~SearchBox() {
156}
157
158void SearchBox::LogEvent(NTPLoggingEventType event) {
159  render_view()->Send(new ChromeViewHostMsg_LogEvent(
160      render_view()->GetRoutingID(), page_seq_no_, event));
161}
162
163void SearchBox::LogMostVisitedImpression(int position,
164                                         const base::string16& provider) {
165  render_view()->Send(new ChromeViewHostMsg_LogMostVisitedImpression(
166      render_view()->GetRoutingID(), page_seq_no_, position, provider));
167}
168
169void SearchBox::LogMostVisitedNavigation(int position,
170                                         const base::string16& provider) {
171  render_view()->Send(new ChromeViewHostMsg_LogMostVisitedNavigation(
172      render_view()->GetRoutingID(), page_seq_no_, position, provider));
173}
174
175void SearchBox::CheckIsUserSignedInToChromeAs(const base::string16& identity) {
176  render_view()->Send(new ChromeViewHostMsg_ChromeIdentityCheck(
177      render_view()->GetRoutingID(), page_seq_no_, identity));
178}
179
180void SearchBox::DeleteMostVisitedItem(
181    InstantRestrictedID most_visited_item_id) {
182  render_view()->Send(new ChromeViewHostMsg_SearchBoxDeleteMostVisitedItem(
183      render_view()->GetRoutingID(),
184      page_seq_no_,
185      GetURLForMostVisitedItem(most_visited_item_id)));
186}
187
188bool SearchBox::GenerateFaviconURLFromTransientURL(const GURL& transient_url,
189                                                   GURL* url) const {
190  std::string favicon_params;
191  InstantRestrictedID rid = -1;
192  bool success = internal::GetRestrictedIDFromFaviconUrl(
193      render_view()->GetRoutingID(), transient_url, &favicon_params, &rid);
194  if (!success)
195    return false;
196
197  InstantMostVisitedItem item;
198  std::string item_url;
199  if (rid != -1 && GetMostVisitedItemWithID(rid, &item))
200    item_url = item.url.spec();
201
202  *url = GURL(base::StringPrintf("chrome-search://favicon/%s%s",
203                                 favicon_params.c_str(),
204                                 item_url.c_str()));
205  return true;
206}
207
208bool SearchBox::GenerateThumbnailURLFromTransientURL(const GURL& transient_url,
209                                                     GURL* url) const {
210  InstantRestrictedID rid = 0;
211  if (!internal::GetRestrictedIDFromThumbnailUrl(render_view()->GetRoutingID(),
212                                                 transient_url, &rid)) {
213    return false;
214  }
215
216  GURL most_visited_item_url(GetURLForMostVisitedItem(rid));
217  if (most_visited_item_url.is_empty())
218    return false;
219  *url = GURL(base::StringPrintf("chrome-search://thumb/%s",
220                                 most_visited_item_url.spec().c_str()));
221  return true;
222}
223
224void SearchBox::GetMostVisitedItems(
225    std::vector<InstantMostVisitedItemIDPair>* items) const {
226  return most_visited_items_cache_.GetCurrentItems(items);
227}
228
229bool SearchBox::GetMostVisitedItemWithID(
230    InstantRestrictedID most_visited_item_id,
231    InstantMostVisitedItem* item) const {
232  return most_visited_items_cache_.GetItemWithRestrictedID(most_visited_item_id,
233                                                           item);
234}
235
236const ThemeBackgroundInfo& SearchBox::GetThemeBackgroundInfo() {
237  return theme_info_;
238}
239
240void SearchBox::Focus() {
241  render_view()->Send(new ChromeViewHostMsg_FocusOmnibox(
242      render_view()->GetRoutingID(), page_seq_no_, OMNIBOX_FOCUS_VISIBLE));
243}
244
245void SearchBox::NavigateToURL(const GURL& url,
246                              WindowOpenDisposition disposition,
247                              bool is_most_visited_item_url) {
248  render_view()->Send(new ChromeViewHostMsg_SearchBoxNavigate(
249      render_view()->GetRoutingID(), page_seq_no_, url,
250      disposition, is_most_visited_item_url));
251}
252
253void SearchBox::Paste(const base::string16& text) {
254  render_view()->Send(new ChromeViewHostMsg_PasteAndOpenDropdown(
255      render_view()->GetRoutingID(), page_seq_no_, text));
256}
257
258void SearchBox::SetVoiceSearchSupported(bool supported) {
259  render_view()->Send(new ChromeViewHostMsg_SetVoiceSearchSupported(
260      render_view()->GetRoutingID(), page_seq_no_, supported));
261}
262
263void SearchBox::StartCapturingKeyStrokes() {
264  render_view()->Send(new ChromeViewHostMsg_FocusOmnibox(
265      render_view()->GetRoutingID(), page_seq_no_, OMNIBOX_FOCUS_INVISIBLE));
266}
267
268void SearchBox::StopCapturingKeyStrokes() {
269  render_view()->Send(new ChromeViewHostMsg_FocusOmnibox(
270      render_view()->GetRoutingID(), page_seq_no_, OMNIBOX_FOCUS_NONE));
271}
272
273void SearchBox::UndoAllMostVisitedDeletions() {
274  render_view()->Send(
275      new ChromeViewHostMsg_SearchBoxUndoAllMostVisitedDeletions(
276          render_view()->GetRoutingID(), page_seq_no_));
277}
278
279void SearchBox::UndoMostVisitedDeletion(
280    InstantRestrictedID most_visited_item_id) {
281  render_view()->Send(new ChromeViewHostMsg_SearchBoxUndoMostVisitedDeletion(
282      render_view()->GetRoutingID(), page_seq_no_,
283      GetURLForMostVisitedItem(most_visited_item_id)));
284}
285
286bool SearchBox::OnMessageReceived(const IPC::Message& message) {
287  bool handled = true;
288  IPC_BEGIN_MESSAGE_MAP(SearchBox, message)
289    IPC_MESSAGE_HANDLER(ChromeViewMsg_SetPageSequenceNumber,
290                        OnSetPageSequenceNumber)
291    IPC_MESSAGE_HANDLER(ChromeViewMsg_ChromeIdentityCheckResult,
292                        OnChromeIdentityCheckResult)
293    IPC_MESSAGE_HANDLER(ChromeViewMsg_DetermineIfPageSupportsInstant,
294                        OnDetermineIfPageSupportsInstant)
295    IPC_MESSAGE_HANDLER(ChromeViewMsg_SearchBoxFocusChanged, OnFocusChanged)
296    IPC_MESSAGE_HANDLER(ChromeViewMsg_SearchBoxMarginChange, OnMarginChange)
297    IPC_MESSAGE_HANDLER(ChromeViewMsg_SearchBoxMostVisitedItemsChanged,
298                        OnMostVisitedChanged)
299    IPC_MESSAGE_HANDLER(ChromeViewMsg_SearchBoxPromoInformation,
300                        OnPromoInformationReceived)
301    IPC_MESSAGE_HANDLER(ChromeViewMsg_SearchBoxSetDisplayInstantResults,
302                        OnSetDisplayInstantResults)
303    IPC_MESSAGE_HANDLER(ChromeViewMsg_SearchBoxSetInputInProgress,
304                        OnSetInputInProgress)
305    IPC_MESSAGE_HANDLER(ChromeViewMsg_SearchBoxSetSuggestionToPrefetch,
306                        OnSetSuggestionToPrefetch)
307    IPC_MESSAGE_HANDLER(ChromeViewMsg_SearchBoxSubmit, OnSubmit)
308    IPC_MESSAGE_HANDLER(ChromeViewMsg_SearchBoxThemeChanged,
309                        OnThemeChanged)
310    IPC_MESSAGE_HANDLER(ChromeViewMsg_SearchBoxToggleVoiceSearch,
311                        OnToggleVoiceSearch)
312    IPC_MESSAGE_UNHANDLED(handled = false)
313  IPC_END_MESSAGE_MAP()
314  return handled;
315}
316
317void SearchBox::OnSetPageSequenceNumber(int page_seq_no) {
318  page_seq_no_ = page_seq_no;
319}
320
321void SearchBox::OnChromeIdentityCheckResult(const base::string16& identity,
322                                            bool identity_match) {
323  if (render_view()->GetWebView() && render_view()->GetWebView()->mainFrame()) {
324    extensions_v8::SearchBoxExtension::DispatchChromeIdentityCheckResult(
325        render_view()->GetWebView()->mainFrame(), identity, identity_match);
326  }
327}
328
329void SearchBox::OnDetermineIfPageSupportsInstant() {
330  if (render_view()->GetWebView() && render_view()->GetWebView()->mainFrame()) {
331    bool result = extensions_v8::SearchBoxExtension::PageSupportsInstant(
332        render_view()->GetWebView()->mainFrame());
333    DVLOG(1) << render_view() << " PageSupportsInstant: " << result;
334    render_view()->Send(new ChromeViewHostMsg_InstantSupportDetermined(
335        render_view()->GetRoutingID(), page_seq_no_, result));
336  }
337}
338
339void SearchBox::OnFocusChanged(OmniboxFocusState new_focus_state,
340                               OmniboxFocusChangeReason reason) {
341  bool key_capture_enabled = new_focus_state == OMNIBOX_FOCUS_INVISIBLE;
342  if (key_capture_enabled != is_key_capture_enabled_) {
343    // Tell the page if the key capture mode changed unless the focus state
344    // changed because of TYPING. This is because in that case, the browser
345    // hasn't really stopped capturing key strokes.
346    //
347    // (More practically, if we don't do this check, the page would receive
348    // onkeycapturechange before the corresponding onchange, and the page would
349    // have no way of telling whether the keycapturechange happened because of
350    // some actual user action or just because they started typing.)
351    if (reason != OMNIBOX_FOCUS_CHANGE_TYPING &&
352        render_view()->GetWebView() &&
353        render_view()->GetWebView()->mainFrame()) {
354      is_key_capture_enabled_ = key_capture_enabled;
355      DVLOG(1) << render_view() << " OnKeyCaptureChange";
356      extensions_v8::SearchBoxExtension::DispatchKeyCaptureChange(
357          render_view()->GetWebView()->mainFrame());
358    }
359  }
360  bool is_focused = new_focus_state == OMNIBOX_FOCUS_VISIBLE;
361  if (is_focused != is_focused_) {
362    is_focused_ = is_focused;
363    DVLOG(1) << render_view() << " OnFocusChange";
364    if (render_view()->GetWebView() &&
365        render_view()->GetWebView()->mainFrame()) {
366      extensions_v8::SearchBoxExtension::DispatchFocusChange(
367          render_view()->GetWebView()->mainFrame());
368    }
369  }
370}
371
372void SearchBox::OnMarginChange(int margin) {
373  start_margin_ = margin;
374  if (render_view()->GetWebView() && render_view()->GetWebView()->mainFrame()) {
375    extensions_v8::SearchBoxExtension::DispatchMarginChange(
376        render_view()->GetWebView()->mainFrame());
377  }
378}
379
380void SearchBox::OnMostVisitedChanged(
381    const std::vector<InstantMostVisitedItem>& items) {
382  std::vector<InstantMostVisitedItemIDPair> last_known_items;
383  GetMostVisitedItems(&last_known_items);
384
385  if (AreMostVisitedItemsEqual(last_known_items, items))
386    return;  // Do not send duplicate onmostvisitedchange events.
387
388  most_visited_items_cache_.AddItems(items);
389  if (render_view()->GetWebView() && render_view()->GetWebView()->mainFrame()) {
390    extensions_v8::SearchBoxExtension::DispatchMostVisitedChanged(
391        render_view()->GetWebView()->mainFrame());
392  }
393}
394
395void SearchBox::OnPromoInformationReceived(bool is_app_launcher_enabled) {
396  app_launcher_enabled_ = is_app_launcher_enabled;
397}
398
399void SearchBox::OnSetDisplayInstantResults(bool display_instant_results) {
400  display_instant_results_ = display_instant_results;
401}
402
403void SearchBox::OnSetInputInProgress(bool is_input_in_progress) {
404  if (is_input_in_progress_ != is_input_in_progress) {
405    is_input_in_progress_ = is_input_in_progress;
406    DVLOG(1) << render_view() << " OnSetInputInProgress";
407    if (render_view()->GetWebView() &&
408        render_view()->GetWebView()->mainFrame()) {
409      if (is_input_in_progress_) {
410        extensions_v8::SearchBoxExtension::DispatchInputStart(
411            render_view()->GetWebView()->mainFrame());
412      } else {
413        extensions_v8::SearchBoxExtension::DispatchInputCancel(
414            render_view()->GetWebView()->mainFrame());
415      }
416    }
417  }
418}
419
420void SearchBox::OnSetSuggestionToPrefetch(const InstantSuggestion& suggestion) {
421  suggestion_ = suggestion;
422  if (render_view()->GetWebView() && render_view()->GetWebView()->mainFrame()) {
423    DVLOG(1) << render_view() << " OnSetSuggestionToPrefetch";
424    extensions_v8::SearchBoxExtension::DispatchSuggestionChange(
425        render_view()->GetWebView()->mainFrame());
426  }
427}
428
429void SearchBox::OnSubmit(const base::string16& query) {
430  query_ = query;
431  if (render_view()->GetWebView() && render_view()->GetWebView()->mainFrame()) {
432    DVLOG(1) << render_view() << " OnSubmit";
433    extensions_v8::SearchBoxExtension::DispatchSubmit(
434        render_view()->GetWebView()->mainFrame());
435  }
436  if (!query.empty())
437    Reset();
438}
439
440void SearchBox::OnThemeChanged(const ThemeBackgroundInfo& theme_info) {
441  // Do not send duplicate notifications.
442  if (theme_info_ == theme_info)
443    return;
444
445  theme_info_ = theme_info;
446  if (render_view()->GetWebView() && render_view()->GetWebView()->mainFrame()) {
447    extensions_v8::SearchBoxExtension::DispatchThemeChange(
448        render_view()->GetWebView()->mainFrame());
449  }
450}
451
452void SearchBox::OnToggleVoiceSearch() {
453  if (render_view()->GetWebView() && render_view()->GetWebView()->mainFrame()) {
454    extensions_v8::SearchBoxExtension::DispatchToggleVoiceSearch(
455        render_view()->GetWebView()->mainFrame());
456  }
457}
458
459GURL SearchBox::GetURLForMostVisitedItem(InstantRestrictedID item_id) const {
460  InstantMostVisitedItem item;
461  return GetMostVisitedItemWithID(item_id, &item) ? item.url : GURL();
462}
463
464void SearchBox::Reset() {
465  query_.clear();
466  suggestion_ = InstantSuggestion();
467  start_margin_ = 0;
468  is_focused_ = false;
469  is_key_capture_enabled_ = false;
470  theme_info_ = ThemeBackgroundInfo();
471}
472