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