1 // Copyright 2015 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/webui/history/browsing_history_handler.h"
6 
7 #include <stddef.h>
8 
9 #include <set>
10 
11 #include "base/bind.h"
12 #include "base/callback_helpers.h"
13 #include "base/check_op.h"
14 #include "base/i18n/rtl.h"
15 #include "base/i18n/time_formatting.h"
16 #include "base/notreached.h"
17 #include "base/strings/string_number_conversions.h"
18 #include "base/strings/utf_string_conversions.h"
19 #include "base/time/default_clock.h"
20 #include "base/time/time.h"
21 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
22 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
23 #include "chrome/browser/favicon/large_icon_service_factory.h"
24 #include "chrome/browser/history/history_service_factory.h"
25 #include "chrome/browser/history/history_utils.h"
26 #include "chrome/browser/profiles/profile.h"
27 #include "chrome/browser/sync/device_info_sync_service_factory.h"
28 #include "chrome/browser/sync/profile_sync_service_factory.h"
29 #include "chrome/browser/ui/browser_finder.h"
30 #include "chrome/browser/ui/chrome_pages.h"
31 #include "chrome/browser/ui/webui/favicon_source.h"
32 #include "chrome/common/buildflags.h"
33 #include "chrome/common/pref_names.h"
34 #include "components/bookmarks/browser/bookmark_model.h"
35 #include "components/bookmarks/browser/bookmark_utils.h"
36 #include "components/favicon/core/fallback_url_util.h"
37 #include "components/favicon/core/large_icon_service.h"
38 #include "components/favicon_base/favicon_url_parser.h"
39 #include "components/keyed_service/core/service_access_type.h"
40 #include "components/prefs/pref_service.h"
41 #include "components/query_parser/snippet.h"
42 #include "components/strings/grit/components_strings.h"
43 #include "components/sync/driver/sync_service.h"
44 #include "components/sync_device_info/device_info.h"
45 #include "components/sync_device_info/device_info_sync_service.h"
46 #include "components/sync_device_info/device_info_tracker.h"
47 #include "components/url_formatter/url_formatter.h"
48 #include "content/public/browser/url_data_source.h"
49 #include "content/public/browser/web_ui.h"
50 #include "ui/base/l10n/l10n_util.h"
51 #include "ui/base/l10n/time_format.h"
52 
53 #if BUILDFLAG(ENABLE_SUPERVISED_USERS)
54 #include "chrome/browser/supervised_user/supervised_user_navigation_observer.h"
55 #include "chrome/browser/supervised_user/supervised_user_service.h"
56 #include "chrome/browser/supervised_user/supervised_user_service_factory.h"
57 #include "chrome/browser/supervised_user/supervised_user_url_filter.h"
58 #endif
59 
60 using bookmarks::BookmarkModel;
61 using history::BrowsingHistoryService;
62 using history::HistoryService;
63 using history::WebHistoryService;
64 
65 namespace {
66 
67 // Identifiers for the type of device from which a history entry originated.
68 static const char kDeviceTypeLaptop[] = "laptop";
69 static const char kDeviceTypePhone[] = "phone";
70 static const char kDeviceTypeTablet[] = "tablet";
71 
72 // Gets the name and type of a device for the given sync client ID.
73 // |name| and |type| are out parameters.
GetDeviceNameAndType(const syncer::DeviceInfoTracker * tracker,const std::string & client_id,std::string * name,std::string * type)74 void GetDeviceNameAndType(const syncer::DeviceInfoTracker* tracker,
75                           const std::string& client_id,
76                           std::string* name,
77                           std::string* type) {
78   // DeviceInfoTracker must be syncing in order for remote history entries to
79   // be available.
80   DCHECK(tracker);
81   DCHECK(tracker->IsSyncing());
82 
83   std::unique_ptr<syncer::DeviceInfo> device_info =
84       tracker->GetDeviceInfo(client_id);
85   if (device_info.get()) {
86     *name = device_info->client_name();
87     switch (device_info->device_type()) {
88       case sync_pb::SyncEnums::TYPE_PHONE:
89         *type = kDeviceTypePhone;
90         break;
91       case sync_pb::SyncEnums::TYPE_TABLET:
92         *type = kDeviceTypeTablet;
93         break;
94       default:
95         *type = kDeviceTypeLaptop;
96     }
97     return;
98   }
99 
100   *name = l10n_util::GetStringUTF8(IDS_HISTORY_UNKNOWN_DEVICE);
101   *type = kDeviceTypeLaptop;
102 }
103 
104 // Formats |entry|'s URL and title and adds them to |result|.
SetHistoryEntryUrlAndTitle(const BrowsingHistoryService::HistoryEntry & entry,base::Value * result)105 void SetHistoryEntryUrlAndTitle(
106     const BrowsingHistoryService::HistoryEntry& entry,
107     base::Value* result) {
108   result->SetStringKey("url", entry.url.spec());
109 
110   bool using_url_as_the_title = false;
111   base::string16 title_to_set(entry.title);
112   if (entry.title.empty()) {
113     using_url_as_the_title = true;
114     title_to_set = base::UTF8ToUTF16(entry.url.spec());
115   }
116 
117   // Since the title can contain BiDi text, we need to mark the text as either
118   // RTL or LTR, depending on the characters in the string. If we use the URL
119   // as the title, we mark the title as LTR since URLs are always treated as
120   // left to right strings.
121   if (base::i18n::IsRTL()) {
122     if (using_url_as_the_title)
123       base::i18n::WrapStringWithLTRFormatting(&title_to_set);
124     else
125       base::i18n::AdjustStringForLocaleDirection(&title_to_set);
126   }
127 
128   // Number of chars to truncate titles when making them "short".
129   static const size_t kShortTitleLength = 300;
130   if (title_to_set.size() > kShortTitleLength)
131     title_to_set.resize(kShortTitleLength);
132 
133   result->SetStringKey("title", title_to_set);
134 }
135 
136 // Helper function to check if entry is present in user remote data (server-side
137 // history).
IsEntryInRemoteUserData(const BrowsingHistoryService::HistoryEntry & entry)138 bool IsEntryInRemoteUserData(
139     const BrowsingHistoryService::HistoryEntry& entry) {
140   switch (entry.entry_type) {
141     case BrowsingHistoryService::HistoryEntry::EntryType::EMPTY_ENTRY:
142     case BrowsingHistoryService::HistoryEntry::EntryType::LOCAL_ENTRY:
143       return false;
144     case BrowsingHistoryService::HistoryEntry::EntryType::REMOTE_ENTRY:
145     case BrowsingHistoryService::HistoryEntry::EntryType::COMBINED_ENTRY:
146       return true;
147   }
148   NOTREACHED();
149   return false;
150 }
151 
152 // Converts |entry| to a base::Value to be owned by the caller.
HistoryEntryToValue(const BrowsingHistoryService::HistoryEntry & entry,BookmarkModel * bookmark_model,Profile * profile,const syncer::DeviceInfoTracker * tracker,base::Clock * clock)153 base::Value HistoryEntryToValue(
154     const BrowsingHistoryService::HistoryEntry& entry,
155     BookmarkModel* bookmark_model,
156     Profile* profile,
157     const syncer::DeviceInfoTracker* tracker,
158     base::Clock* clock) {
159   base::Value result(base::Value::Type::DICTIONARY);
160   SetHistoryEntryUrlAndTitle(entry, &result);
161 
162   base::string16 domain = url_formatter::IDNToUnicode(entry.url.host());
163   // When the domain is empty, use the scheme instead. This allows for a
164   // sensible treatment of e.g. file: URLs when group by domain is on.
165   if (domain.empty())
166     domain = base::UTF8ToUTF16(entry.url.scheme() + ":");
167 
168   // The items which are to be written into result are also described in
169   // chrome/browser/resources/history/history.js in @typedef for
170   // HistoryEntry. Please update it whenever you add or remove
171   // any keys in result.
172   result.SetStringKey("domain", domain);
173 
174   result.SetStringKey(
175       "fallbackFaviconText",
176       base::UTF16ToASCII(favicon::GetFallbackIconText(entry.url)));
177 
178   result.SetDoubleKey("time", entry.time.ToJsTime());
179 
180   // Pass the timestamps in a list.
181   base::Value timestamps(base::Value::Type::LIST);
182   for (int64_t timestamp : entry.all_timestamps) {
183     timestamps.Append(base::Time::FromInternalValue(timestamp).ToJsTime());
184   }
185   result.SetKey("allTimestamps", std::move(timestamps));
186 
187   // Always pass the short date since it is needed both in the search and in
188   // the monthly view.
189   result.SetStringKey("dateShort", base::TimeFormatShortDate(entry.time));
190 
191   base::string16 snippet_string;
192   base::string16 date_relative_day;
193   base::string16 date_time_of_day;
194   bool is_blocked_visit = false;
195   int host_filtering_behavior = -1;
196 
197   // Only pass in the strings we need (search results need a shortdate
198   // and snippet, browse results need day and time information). Makes sure that
199   // values of result are never undefined
200   if (entry.is_search_result) {
201     snippet_string = entry.snippet;
202   } else {
203     base::Time midnight = clock->Now().LocalMidnight();
204     base::string16 date_str =
205         ui::TimeFormat::RelativeDate(entry.time, &midnight);
206     if (date_str.empty()) {
207       date_str = base::TimeFormatFriendlyDate(entry.time);
208     } else {
209       date_str = l10n_util::GetStringFUTF16(
210           IDS_HISTORY_DATE_WITH_RELATIVE_TIME, date_str,
211           base::TimeFormatFriendlyDate(entry.time));
212     }
213     date_relative_day = date_str;
214     date_time_of_day = base::TimeFormatTimeOfDay(entry.time);
215   }
216 
217   std::string device_name;
218   std::string device_type;
219   if (!entry.client_id.empty())
220     GetDeviceNameAndType(tracker, entry.client_id, &device_name, &device_type);
221   result.SetStringKey("deviceName", device_name);
222   result.SetStringKey("deviceType", device_type);
223 
224 #if BUILDFLAG(ENABLE_SUPERVISED_USERS)
225   SupervisedUserService* supervised_user_service = nullptr;
226   if (profile->IsSupervised()) {
227     supervised_user_service =
228         SupervisedUserServiceFactory::GetForProfile(profile);
229   }
230   if (supervised_user_service) {
231     const SupervisedUserURLFilter* url_filter =
232         supervised_user_service->GetURLFilter();
233     int filtering_behavior =
234         url_filter->GetFilteringBehaviorForURL(entry.url.GetWithEmptyPath());
235     is_blocked_visit = entry.blocked_visit;
236     host_filtering_behavior = filtering_behavior;
237   }
238 #endif
239 
240   result.SetStringKey("dateTimeOfDay", date_time_of_day);
241   result.SetStringKey("dateRelativeDay", date_relative_day);
242   result.SetStringKey("snippet", snippet_string);
243   result.SetBoolKey("starred", bookmark_model->IsBookmarked(entry.url));
244   result.SetIntKey("hostFilteringBehavior", host_filtering_behavior);
245   result.SetBoolKey("blockedVisit", is_blocked_visit);
246   result.SetBoolKey("isUrlInRemoteUserData", IsEntryInRemoteUserData(entry));
247   result.SetStringKey("remoteIconUrlForUma",
248                       entry.remote_icon_url_for_uma.spec());
249 
250   return result;
251 }
252 
253 }  // namespace
254 
BrowsingHistoryHandler()255 BrowsingHistoryHandler::BrowsingHistoryHandler()
256     : clock_(base::DefaultClock::GetInstance()),
257       browsing_history_service_(nullptr) {}
258 
~BrowsingHistoryHandler()259 BrowsingHistoryHandler::~BrowsingHistoryHandler() {}
260 
OnJavascriptAllowed()261 void BrowsingHistoryHandler::OnJavascriptAllowed() {
262   if (!browsing_history_service_ && initial_results_.is_none()) {
263     // Page was refreshed, so need to call StartQueryHistory here
264     StartQueryHistory();
265   }
266 
267   for (auto& callback : deferred_callbacks_) {
268     std::move(callback).Run();
269   }
270   deferred_callbacks_.clear();
271 }
272 
OnJavascriptDisallowed()273 void BrowsingHistoryHandler::OnJavascriptDisallowed() {
274   weak_factory_.InvalidateWeakPtrs();
275   browsing_history_service_ = nullptr;
276   initial_results_ = base::Value();
277   deferred_callbacks_.clear();
278   query_history_callback_id_.clear();
279   remove_visits_callback_.clear();
280 }
281 
RegisterMessages()282 void BrowsingHistoryHandler::RegisterMessages() {
283   // Create our favicon data source.
284   Profile* profile = GetProfile();
285   content::URLDataSource::Add(
286       profile, std::make_unique<FaviconSource>(
287                    profile, chrome::FaviconUrlFormat::kFavicon2));
288 
289   web_ui()->RegisterMessageCallback(
290       "queryHistory",
291       base::BindRepeating(&BrowsingHistoryHandler::HandleQueryHistory,
292                           base::Unretained(this)));
293   web_ui()->RegisterMessageCallback(
294       "queryHistoryContinuation",
295       base::BindRepeating(
296           &BrowsingHistoryHandler::HandleQueryHistoryContinuation,
297           base::Unretained(this)));
298   web_ui()->RegisterMessageCallback(
299       "removeVisits",
300       base::BindRepeating(&BrowsingHistoryHandler::HandleRemoveVisits,
301                           base::Unretained(this)));
302   web_ui()->RegisterMessageCallback(
303       "clearBrowsingData",
304       base::BindRepeating(&BrowsingHistoryHandler::HandleClearBrowsingData,
305                           base::Unretained(this)));
306   web_ui()->RegisterMessageCallback(
307       "removeBookmark",
308       base::BindRepeating(&BrowsingHistoryHandler::HandleRemoveBookmark,
309                           base::Unretained(this)));
310 }
311 
StartQueryHistory()312 void BrowsingHistoryHandler::StartQueryHistory() {
313   Profile* profile = GetProfile();
314   HistoryService* local_history = HistoryServiceFactory::GetForProfile(
315       profile, ServiceAccessType::EXPLICIT_ACCESS);
316   syncer::SyncService* sync_service =
317       ProfileSyncServiceFactory::GetForProfile(profile);
318   browsing_history_service_ = std::make_unique<BrowsingHistoryService>(
319       this, local_history, sync_service);
320 
321   // 150 = RESULTS_PER_PAGE from chrome/browser/resources/history/constants.js
322   SendHistoryQuery(150, base::string16());
323 }
324 
HandleQueryHistory(const base::ListValue * args)325 void BrowsingHistoryHandler::HandleQueryHistory(const base::ListValue* args) {
326   AllowJavascript();
327   const base::Value& callback_id = args->GetList()[0];
328   if (!initial_results_.is_none()) {
329     ResolveJavascriptCallback(callback_id, std::move(initial_results_));
330     initial_results_ = base::Value();
331     return;
332   }
333 
334   // Reset the query history continuation callback. Since it is repopulated in
335   // OnQueryComplete(), it cannot be reset earlier, as the early return above
336   // prevents the QueryHistory() call to the browsing history service.
337   query_history_continuation_.Reset();
338 
339   // Cancel the previous query if it is still in flight.
340   if (!query_history_callback_id_.empty()) {
341     RejectJavascriptCallback(base::Value(query_history_callback_id_),
342                              base::Value());
343   }
344   query_history_callback_id_ = callback_id.GetString();
345 
346   // Parse the arguments from JavaScript. There are two required arguments:
347   // - the text to search for (may be empty)
348   // - the maximum number of results to return (may be 0, meaning that there
349   //   is no maximum).
350   const base::Value& search_text = args->GetList()[1];
351 
352   const base::Value& count = args->GetList()[2];
353   if (!count.is_int()) {
354     NOTREACHED() << "Failed to convert argument 2.";
355     return;
356   }
357 
358   SendHistoryQuery(count.GetInt(), base::UTF8ToUTF16(search_text.GetString()));
359 }
360 
SendHistoryQuery(int max_count,const base::string16 & query)361 void BrowsingHistoryHandler::SendHistoryQuery(int max_count,
362                                               const base::string16& query) {
363   history::QueryOptions options;
364   options.max_count = max_count;
365   options.duplicate_policy = history::QueryOptions::REMOVE_DUPLICATES_PER_DAY;
366   browsing_history_service_->QueryHistory(query, options);
367 }
368 
HandleQueryHistoryContinuation(const base::ListValue * args)369 void BrowsingHistoryHandler::HandleQueryHistoryContinuation(
370     const base::ListValue* args) {
371   CHECK(args->GetList().size() == 1);
372   const base::Value& callback_id = args->GetList()[0];
373   // Cancel the previous query if it is still in flight.
374   if (!query_history_callback_id_.empty()) {
375     RejectJavascriptCallback(base::Value(query_history_callback_id_),
376                              base::Value());
377   }
378   query_history_callback_id_ = callback_id.GetString();
379 
380   DCHECK(query_history_continuation_);
381   std::move(query_history_continuation_).Run();
382 }
383 
HandleRemoveVisits(const base::ListValue * args)384 void BrowsingHistoryHandler::HandleRemoveVisits(const base::ListValue* args) {
385   CHECK(args->GetList().size() == 2);
386   const base::Value& callback_id = args->GetList()[0];
387   CHECK(remove_visits_callback_.empty());
388   remove_visits_callback_ = callback_id.GetString();
389 
390   std::vector<BrowsingHistoryService::HistoryEntry> items_to_remove;
391   const base::Value& items = args->GetList()[1];
392   base::Value::ConstListView list = items.GetList();
393   items_to_remove.reserve(list.size());
394   for (size_t i = 0; i < list.size(); ++i) {
395     // Each argument is a dictionary with properties "url" and "timestamps".
396     if (!list[i].is_dict()) {
397       NOTREACHED() << "Unable to extract arguments";
398       return;
399     }
400 
401     const std::string* url_ptr = list[i].FindStringKey("url");
402     const base::Value* timestamps_ptr = list[i].FindListKey("timestamps");
403     if (!url_ptr || !timestamps_ptr) {
404       NOTREACHED() << "Unable to extract arguments";
405       return;
406     }
407 
408     base::Value::ConstListView timestamps = timestamps_ptr->GetList();
409     DCHECK_GT(timestamps.size(), 0U);
410     BrowsingHistoryService::HistoryEntry entry;
411     entry.url = GURL(*url_ptr);
412 
413     for (size_t ts_index = 0; ts_index < timestamps.size(); ++ts_index) {
414       if (!timestamps[ts_index].is_double() && !timestamps[ts_index].is_int()) {
415         NOTREACHED() << "Unable to extract visit timestamp.";
416         continue;
417       }
418 
419       base::Time visit_time =
420           base::Time::FromJsTime(timestamps[ts_index].GetDouble());
421       entry.all_timestamps.insert(visit_time.ToInternalValue());
422     }
423 
424     items_to_remove.push_back(entry);
425   }
426 
427   browsing_history_service_->RemoveVisits(items_to_remove);
428 }
429 
HandleClearBrowsingData(const base::ListValue * args)430 void BrowsingHistoryHandler::HandleClearBrowsingData(
431     const base::ListValue* args) {
432   // TODO(beng): This is an improper direct dependency on Browser. Route this
433   // through some sort of delegate.
434   Browser* browser =
435       chrome::FindBrowserWithWebContents(web_ui()->GetWebContents());
436   chrome::ShowClearBrowsingDataDialog(browser);
437 }
438 
HandleRemoveBookmark(const base::ListValue * args)439 void BrowsingHistoryHandler::HandleRemoveBookmark(const base::ListValue* args) {
440   base::string16 url = ExtractStringValue(args);
441   Profile* profile = GetProfile();
442   BookmarkModel* model = BookmarkModelFactory::GetForBrowserContext(profile);
443   bookmarks::RemoveAllBookmarks(model, GURL(url));
444 }
445 
OnQueryComplete(const std::vector<BrowsingHistoryService::HistoryEntry> & results,const BrowsingHistoryService::QueryResultsInfo & query_results_info,base::OnceClosure continuation_closure)446 void BrowsingHistoryHandler::OnQueryComplete(
447     const std::vector<BrowsingHistoryService::HistoryEntry>& results,
448     const BrowsingHistoryService::QueryResultsInfo& query_results_info,
449     base::OnceClosure continuation_closure) {
450   query_history_continuation_ = std::move(continuation_closure);
451   Profile* profile = Profile::FromWebUI(web_ui());
452   BookmarkModel* bookmark_model =
453       BookmarkModelFactory::GetForBrowserContext(profile);
454 
455   const syncer::DeviceInfoTracker* tracker =
456       DeviceInfoSyncServiceFactory::GetForProfile(profile)
457           ->GetDeviceInfoTracker();
458 
459   // Convert the result vector into a ListValue.
460   DCHECK(tracker);
461   base::Value results_value(base::Value::Type::LIST);
462   for (const BrowsingHistoryService::HistoryEntry& entry : results) {
463     results_value.Append(
464         HistoryEntryToValue(entry, bookmark_model, profile, tracker, clock_));
465   }
466 
467   base::Value results_info(base::Value::Type::DICTIONARY);
468   // The items which are to be written into results_info_value_ are also
469   // described in chrome/browser/resources/history/history.js in @typedef for
470   // HistoryQuery. Please update it whenever you add or remove any keys in
471   // results_info_value_.
472   results_info.SetStringKey("term", query_results_info.search_text);
473   results_info.SetBoolKey("finished", query_results_info.reached_beginning);
474 
475   base::Value final_results(base::Value::Type::DICTIONARY);
476   final_results.SetKey("info", std::move(results_info));
477   final_results.SetKey("value", std::move(results_value));
478 
479   if (query_history_callback_id_.empty()) {
480     // This can happen if JS isn't ready yet when the first query comes back.
481     initial_results_ = std::move(final_results);
482     return;
483   }
484 
485   ResolveJavascriptCallback(base::Value(query_history_callback_id_),
486                             std::move(final_results));
487   query_history_callback_id_.clear();
488 }
489 
OnRemoveVisitsComplete()490 void BrowsingHistoryHandler::OnRemoveVisitsComplete() {
491   CHECK(!remove_visits_callback_.empty());
492   ResolveJavascriptCallback(base::Value(remove_visits_callback_),
493                             base::Value());
494   remove_visits_callback_.clear();
495 }
496 
OnRemoveVisitsFailed()497 void BrowsingHistoryHandler::OnRemoveVisitsFailed() {
498   CHECK(!remove_visits_callback_.empty());
499   RejectJavascriptCallback(base::Value(remove_visits_callback_), base::Value());
500   remove_visits_callback_.clear();
501 }
502 
HistoryDeleted()503 void BrowsingHistoryHandler::HistoryDeleted() {
504   if (IsJavascriptAllowed()) {
505     FireWebUIListener("history-deleted", base::Value());
506   } else {
507     deferred_callbacks_.push_back(base::BindOnce(
508         &BrowsingHistoryHandler::HistoryDeleted, weak_factory_.GetWeakPtr()));
509   }
510 }
511 
HasOtherFormsOfBrowsingHistory(bool has_other_forms,bool has_synced_results)512 void BrowsingHistoryHandler::HasOtherFormsOfBrowsingHistory(
513     bool has_other_forms,
514     bool has_synced_results) {
515   if (IsJavascriptAllowed()) {
516     FireWebUIListener("has-other-forms-changed", base::Value(has_other_forms));
517   } else {
518     deferred_callbacks_.push_back(base::BindOnce(
519         &BrowsingHistoryHandler::HasOtherFormsOfBrowsingHistory,
520         weak_factory_.GetWeakPtr(), has_other_forms, has_synced_results));
521   }
522 }
523 
GetProfile()524 Profile* BrowsingHistoryHandler::GetProfile() {
525   return Profile::FromWebUI(web_ui());
526 }
527