1 // Copyright (c) 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/browser/ui/toolbar/back_forward_menu_model.h"
6 
7 #include <stddef.h>
8 
9 #include "base/bind.h"
10 #include "base/callback_helpers.h"
11 #include "base/metrics/histogram_macros.h"
12 #include "base/metrics/user_metrics.h"
13 #include "base/numerics/ranges.h"
14 #include "base/stl_util.h"
15 #include "base/strings/string_number_conversions.h"
16 #include "build/build_config.h"
17 #include "chrome/browser/favicon/favicon_service_factory.h"
18 #include "chrome/browser/ui/browser.h"
19 #include "chrome/browser/ui/browser_commands.h"
20 #include "chrome/browser/ui/browser_navigator_params.h"
21 #include "chrome/browser/ui/singleton_tabs.h"
22 #include "chrome/browser/ui/tabs/tab_strip_model.h"
23 #include "chrome/common/url_constants.h"
24 #include "components/favicon_base/favicon_types.h"
25 #include "components/grit/components_scaled_resources.h"
26 #include "components/strings/grit/components_strings.h"
27 #include "content/public/browser/favicon_status.h"
28 #include "content/public/browser/navigation_controller.h"
29 #include "content/public/browser/navigation_entry.h"
30 #include "content/public/browser/web_contents.h"
31 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
32 #include "ui/base/accelerators/menu_label_accelerator_util.h"
33 #include "ui/base/l10n/l10n_util.h"
34 #include "ui/base/models/image_model.h"
35 #include "ui/base/resource/resource_bundle.h"
36 #include "ui/base/window_open_disposition.h"
37 #include "ui/gfx/text_elider.h"
38 
39 using base::UserMetricsAction;
40 using content::NavigationController;
41 using content::NavigationEntry;
42 using content::WebContents;
43 
44 const int BackForwardMenuModel::kMaxHistoryItems = 12;
45 const int BackForwardMenuModel::kMaxChapterStops = 5;
46 static const int kMaxBackForwardMenuWidth = 700;
47 
BackForwardMenuModel(Browser * browser,ModelType model_type)48 BackForwardMenuModel::BackForwardMenuModel(Browser* browser,
49                                            ModelType model_type)
50     : browser_(browser), model_type_(model_type) {}
51 
~BackForwardMenuModel()52 BackForwardMenuModel::~BackForwardMenuModel() {}
53 
HasIcons() const54 bool BackForwardMenuModel::HasIcons() const {
55   return true;
56 }
57 
GetItemCount() const58 int BackForwardMenuModel::GetItemCount() const {
59   int items = GetHistoryItemCount();
60   if (items <= 0)
61     return items;
62 
63   int chapter_stops = 0;
64 
65   // Next, we count ChapterStops, if any.
66   if (items == kMaxHistoryItems)
67     chapter_stops = GetChapterStopCount(items);
68 
69   if (chapter_stops)
70     items += chapter_stops + 1;  // Chapter stops also need a separator.
71 
72   // If the menu is not empty, add two positions in the end
73   // for a separator and a "Show Full History" item.
74   items += 2;
75   return items;
76 }
77 
GetTypeAt(int index) const78 ui::MenuModel::ItemType BackForwardMenuModel::GetTypeAt(int index) const {
79   return IsSeparator(index) ? TYPE_SEPARATOR : TYPE_COMMAND;
80 }
81 
GetSeparatorTypeAt(int index) const82 ui::MenuSeparatorType BackForwardMenuModel::GetSeparatorTypeAt(
83     int index) const {
84   return ui::NORMAL_SEPARATOR;
85 }
86 
GetCommandIdAt(int index) const87 int BackForwardMenuModel::GetCommandIdAt(int index) const {
88   return index;
89 }
90 
GetLabelAt(int index) const91 base::string16 BackForwardMenuModel::GetLabelAt(int index) const {
92   // Return label "Show Full History" for the last item of the menu.
93   if (index == GetItemCount() - 1)
94     return l10n_util::GetStringUTF16(IDS_HISTORY_SHOWFULLHISTORY_LINK);
95 
96   // Return an empty string for a separator.
97   if (IsSeparator(index))
98     return base::string16();
99 
100   // Return the entry title, escaping any '&' characters and eliding it if it's
101   // super long.
102   NavigationEntry* entry = GetNavigationEntry(index);
103   base::string16 menu_text(entry->GetTitleForDisplay());
104   menu_text = ui::EscapeMenuLabelAmpersands(menu_text);
105   menu_text = gfx::ElideText(menu_text, gfx::FontList(),
106                              kMaxBackForwardMenuWidth, gfx::ELIDE_TAIL);
107 
108   return menu_text;
109 }
110 
IsItemDynamicAt(int index) const111 bool BackForwardMenuModel::IsItemDynamicAt(int index) const {
112   // This object is only used for a single showing of a menu.
113   return false;
114 }
115 
GetAcceleratorAt(int index,ui::Accelerator * accelerator) const116 bool BackForwardMenuModel::GetAcceleratorAt(
117     int index,
118     ui::Accelerator* accelerator) const {
119   return false;
120 }
121 
IsItemCheckedAt(int index) const122 bool BackForwardMenuModel::IsItemCheckedAt(int index) const {
123   return false;
124 }
125 
GetGroupIdAt(int index) const126 int BackForwardMenuModel::GetGroupIdAt(int index) const {
127   return false;
128 }
129 
GetIconAt(int index) const130 ui::ImageModel BackForwardMenuModel::GetIconAt(int index) const {
131   if (!ItemHasIcon(index))
132     return ui::ImageModel();
133 
134   if (index == GetItemCount() - 1) {
135     return ui::ImageModel::FromImage(
136         ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
137             IDR_HISTORY_FAVICON));
138   } else {
139     NavigationEntry* entry = GetNavigationEntry(index);
140     content::FaviconStatus fav_icon = entry->GetFavicon();
141     if (!fav_icon.valid && menu_model_delegate()) {
142       // FetchFavicon is not const because it caches the result, but GetIconAt
143       // is const because it is not be apparent to outside observers that an
144       // internal change is taking place. Compared to spreading const in
145       // unintuitive places (e.g. making menu_model_delegate() const but
146       // returning a non-const while sprinkling virtual on member variables),
147       // this const_cast is the lesser evil.
148       const_cast<BackForwardMenuModel*>(this)->FetchFavicon(entry);
149     }
150     return ui::ImageModel::FromImage(fav_icon.image);
151   }
152 }
153 
GetButtonMenuItemAt(int index) const154 ui::ButtonMenuItemModel* BackForwardMenuModel::GetButtonMenuItemAt(
155     int index) const {
156   return nullptr;
157 }
158 
IsEnabledAt(int index) const159 bool BackForwardMenuModel::IsEnabledAt(int index) const {
160   return index < GetItemCount() && !IsSeparator(index);
161 }
162 
GetSubmenuModelAt(int index) const163 ui::MenuModel* BackForwardMenuModel::GetSubmenuModelAt(int index) const {
164   return nullptr;
165 }
166 
ActivatedAt(int index)167 void BackForwardMenuModel::ActivatedAt(int index) {
168   ActivatedAt(index, 0);
169 }
170 
ActivatedAt(int index,int event_flags)171 void BackForwardMenuModel::ActivatedAt(int index, int event_flags) {
172   DCHECK(!IsSeparator(index));
173 
174   // Execute the command for the last item: "Show Full History".
175   if (index == GetItemCount() - 1) {
176     base::RecordComputedAction(BuildActionName("ShowFullHistory", -1));
177     ShowSingletonTabOverwritingNTP(
178         browser_, GetSingletonTabNavigateParams(
179                       browser_, GURL(chrome::kChromeUIHistoryURL)));
180     return;
181   }
182 
183   // Log whether it was a history or chapter click.
184   int items = GetHistoryItemCount();
185   if (index < items) {
186     base::RecordComputedAction(BuildActionName("HistoryClick", index));
187   } else {
188     const int chapter_index = index - items - 1;
189     base::RecordComputedAction(BuildActionName("ChapterClick", chapter_index));
190   }
191 
192   int controller_index = MenuIndexToNavEntryIndex(index);
193 
194   UMA_HISTOGRAM_BOOLEAN(
195       "Navigation.BackForward.NavigatingToEntryMarkedToBeSkipped",
196       GetWebContents()->GetController().IsEntryMarkedToBeSkipped(
197           controller_index));
198 
199   WindowOpenDisposition disposition =
200       ui::DispositionFromEventFlags(event_flags);
201   chrome::NavigateToIndexWithDisposition(browser_, controller_index,
202                                          disposition);
203 }
204 
MenuWillShow()205 void BackForwardMenuModel::MenuWillShow() {
206   base::RecordComputedAction(BuildActionName("Popup", -1));
207   requested_favicons_.clear();
208   cancelable_task_tracker_.TryCancelAll();
209 }
210 
IsSeparator(int index) const211 bool BackForwardMenuModel::IsSeparator(int index) const {
212   int history_items = GetHistoryItemCount();
213   // If the index is past the number of history items + separator,
214   // we then consider if it is a chapter-stop entry.
215   if (index > history_items) {
216     // We either are in ChapterStop area, or at the end of the list (the "Show
217     // Full History" link).
218     int chapter_stops = GetChapterStopCount(history_items);
219     if (chapter_stops == 0)
220       return false;  // We must have reached the "Show Full History" link.
221     // Otherwise, look to see if we have reached the separator for the
222     // chapter-stops. If not, this is a chapter stop.
223     return (index == history_items + 1 + chapter_stops);
224   }
225 
226   // Look to see if we have reached the separator for the history items.
227   return index == history_items;
228 }
229 
FetchFavicon(NavigationEntry * entry)230 void BackForwardMenuModel::FetchFavicon(NavigationEntry* entry) {
231   // If the favicon has already been requested for this menu, don't do
232   // anything.
233   if (base::Contains(requested_favicons_, entry->GetUniqueID()))
234     return;
235 
236   requested_favicons_.insert(entry->GetUniqueID());
237   favicon::FaviconService* favicon_service =
238       FaviconServiceFactory::GetForProfile(browser_->profile(),
239                                            ServiceAccessType::EXPLICIT_ACCESS);
240   if (!favicon_service)
241     return;
242 
243   favicon_service->GetFaviconImageForPageURL(
244       entry->GetURL(),
245       base::BindOnce(&BackForwardMenuModel::OnFavIconDataAvailable,
246                      base::Unretained(this), entry->GetUniqueID()),
247       &cancelable_task_tracker_);
248 }
249 
OnFavIconDataAvailable(int navigation_entry_unique_id,const favicon_base::FaviconImageResult & image_result)250 void BackForwardMenuModel::OnFavIconDataAvailable(
251     int navigation_entry_unique_id,
252     const favicon_base::FaviconImageResult& image_result) {
253   if (image_result.image.IsEmpty())
254     return;
255 
256   // Find the current model_index for the unique id.
257   NavigationEntry* entry = nullptr;
258   int model_index = -1;
259   for (int i = 0; i < GetItemCount() - 1; i++) {
260     if (IsSeparator(i))
261       continue;
262     if (GetNavigationEntry(i)->GetUniqueID() == navigation_entry_unique_id) {
263       model_index = i;
264       entry = GetNavigationEntry(i);
265       break;
266     }
267   }
268 
269   if (!entry) {
270     // The NavigationEntry wasn't found, this can happen if the user
271     // navigates to another page and a NavigatationEntry falls out of the
272     // range of kMaxHistoryItems.
273     return;
274   }
275 
276   // Now that we have a valid NavigationEntry, decode the favicon and assign
277   // it to the NavigationEntry.
278   entry->GetFavicon().valid = true;
279   entry->GetFavicon().url = image_result.icon_url;
280   entry->GetFavicon().image = image_result.image;
281   if (menu_model_delegate()) {
282     menu_model_delegate()->OnIconChanged(model_index);
283   }
284 }
285 
GetHistoryItemCount() const286 int BackForwardMenuModel::GetHistoryItemCount() const {
287   WebContents* contents = GetWebContents();
288 
289   int items = contents->GetController().GetCurrentEntryIndex();
290   if (model_type_ == ModelType::kForward) {
291     // Only count items from n+1 to end (if n is current entry)
292     items = contents->GetController().GetEntryCount() - items - 1;
293   }
294 
295   return base::ClampToRange(items, 0, kMaxHistoryItems);
296 }
297 
GetChapterStopCount(int history_items) const298 int BackForwardMenuModel::GetChapterStopCount(int history_items) const {
299   if (history_items != kMaxHistoryItems)
300     return 0;
301 
302   WebContents* contents = GetWebContents();
303   int current_entry = contents->GetController().GetCurrentEntryIndex();
304 
305   const bool forward = model_type_ == ModelType::kForward;
306   int chapter_id = current_entry;
307   if (forward)
308     chapter_id += history_items;
309   else
310     chapter_id -= history_items;
311 
312   int chapter_stops = 0;
313   do {
314     chapter_id = GetIndexOfNextChapterStop(chapter_id, forward);
315     if (chapter_id == -1)
316       break;
317     ++chapter_stops;
318   } while (chapter_stops < kMaxChapterStops);
319 
320   return chapter_stops;
321 }
322 
GetIndexOfNextChapterStop(int start_from,bool forward) const323 int BackForwardMenuModel::GetIndexOfNextChapterStop(int start_from,
324                                                     bool forward) const {
325   if (start_from < 0)
326     return -1;  // Out of bounds.
327 
328   // We want to advance over the current chapter stop, so we add one.
329   // We don't need to do this when direction is backwards.
330   if (forward)
331     start_from++;
332 
333   NavigationController& controller = GetWebContents()->GetController();
334   const int max_count = controller.GetEntryCount();
335   if (start_from >= max_count)
336     return -1;  // Out of bounds.
337 
338   NavigationEntry* start_entry = controller.GetEntryAtIndex(start_from);
339   const GURL& url = start_entry->GetURL();
340 
341   auto same_domain_func = [&controller, &url](int i) {
342     return net::registry_controlled_domains::SameDomainOrHost(
343         url, controller.GetEntryAtIndex(i)->GetURL(),
344         net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES);
345   };
346 
347   if (forward) {
348     // When going forwards we return the entry before the entry that has a
349     // different domain.
350     for (int i = start_from + 1; i < max_count; ++i) {
351       if (!same_domain_func(i))
352         return i - 1;
353     }
354     // Last entry is always considered a chapter stop.
355     return max_count - 1;
356   }
357 
358   // When going backwards we return the first entry we find that has a
359   // different domain.
360   for (int i = start_from - 1; i >= 0; --i) {
361     if (!same_domain_func(i))
362       return i;
363   }
364   // We have reached the beginning without finding a chapter stop.
365   return -1;
366 }
367 
FindChapterStop(int offset,bool forward,int skip) const368 int BackForwardMenuModel::FindChapterStop(int offset,
369                                           bool forward,
370                                           int skip) const {
371   if (offset < 0 || skip < 0)
372     return -1;
373 
374   if (!forward)
375     offset *= -1;
376 
377   WebContents* contents = GetWebContents();
378   int entry = contents->GetController().GetCurrentEntryIndex() + offset;
379   for (int i = 0; i < skip + 1; i++)
380     entry = GetIndexOfNextChapterStop(entry, forward);
381 
382   return entry;
383 }
384 
ItemHasCommand(int index) const385 bool BackForwardMenuModel::ItemHasCommand(int index) const {
386   return index < GetItemCount() && !IsSeparator(index);
387 }
388 
ItemHasIcon(int index) const389 bool BackForwardMenuModel::ItemHasIcon(int index) const {
390   return index < GetItemCount() && !IsSeparator(index);
391 }
392 
GetShowFullHistoryLabel() const393 base::string16 BackForwardMenuModel::GetShowFullHistoryLabel() const {
394   return l10n_util::GetStringUTF16(IDS_HISTORY_SHOWFULLHISTORY_LINK);
395 }
396 
GetWebContents() const397 WebContents* BackForwardMenuModel::GetWebContents() const {
398   // We use the test web contents if the unit test has specified it.
399   return test_web_contents_ ?
400       test_web_contents_ :
401       browser_->tab_strip_model()->GetActiveWebContents();
402 }
403 
MenuIndexToNavEntryIndex(int index) const404 int BackForwardMenuModel::MenuIndexToNavEntryIndex(int index) const {
405   WebContents* contents = GetWebContents();
406   int history_items = GetHistoryItemCount();
407 
408   DCHECK_GE(index, 0);
409 
410   // Convert anything above the History items separator.
411   if (index < history_items) {
412     if (model_type_ == ModelType::kForward) {
413       index += contents->GetController().GetCurrentEntryIndex() + 1;
414     } else {
415       // Back menu is reverse.
416       index = contents->GetController().GetCurrentEntryIndex() - (index + 1);
417     }
418     return index;
419   }
420   if (index == history_items)
421     return -1;  // Don't translate the separator for history items.
422 
423   if (index >= history_items + 1 + GetChapterStopCount(history_items))
424     return -1;  // This is beyond the last chapter stop so we abort.
425 
426   // This menu item is a chapter stop located between the two separators.
427   return FindChapterStop(history_items, model_type_ == ModelType::kForward,
428                          index - history_items - 1);
429 }
430 
GetNavigationEntry(int index) const431 NavigationEntry* BackForwardMenuModel::GetNavigationEntry(int index) const {
432   int controller_index = MenuIndexToNavEntryIndex(index);
433   NavigationController& controller = GetWebContents()->GetController();
434 
435   DCHECK_GE(controller_index, 0);
436   DCHECK_LT(controller_index, controller.GetEntryCount());
437 
438   return controller.GetEntryAtIndex(controller_index);
439 }
440 
BuildActionName(const std::string & action,int index) const441 std::string BackForwardMenuModel::BuildActionName(
442     const std::string& action, int index) const {
443   DCHECK(!action.empty());
444   DCHECK_GE(index, -1);
445   std::string metric_string;
446   if (model_type_ == ModelType::kForward)
447     metric_string += "ForwardMenu_";
448   else
449     metric_string += "BackMenu_";
450   metric_string += action;
451   if (index != -1) {
452     // +1 is for historical reasons (indices used to start at 1).
453     metric_string += base::NumberToString(index + 1);
454   }
455   return metric_string;
456 }
457