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/webui/history_ui.h" 6 7 #include <set> 8 9 #include "base/bind.h" 10 #include "base/bind_helpers.h" 11 #include "base/command_line.h" 12 #include "base/i18n/rtl.h" 13 #include "base/i18n/time_formatting.h" 14 #include "base/memory/singleton.h" 15 #include "base/message_loop/message_loop.h" 16 #include "base/metrics/histogram.h" 17 #include "base/prefs/pref_service.h" 18 #include "base/strings/string16.h" 19 #include "base/strings/string_number_conversions.h" 20 #include "base/strings/utf_string_conversions.h" 21 #include "base/time/time.h" 22 #include "base/values.h" 23 #include "chrome/browser/bookmarks/bookmark_model_factory.h" 24 #include "chrome/browser/chrome_notification_types.h" 25 #include "chrome/browser/extensions/activity_log/activity_log.h" 26 #include "chrome/browser/history/history_notifications.h" 27 #include "chrome/browser/history/history_service_factory.h" 28 #include "chrome/browser/history/history_types.h" 29 #include "chrome/browser/history/web_history_service.h" 30 #include "chrome/browser/history/web_history_service_factory.h" 31 #include "chrome/browser/profiles/profile.h" 32 #include "chrome/browser/search/search.h" 33 #include "chrome/browser/sync/glue/device_info.h" 34 #include "chrome/browser/sync/profile_sync_service.h" 35 #include "chrome/browser/sync/profile_sync_service_factory.h" 36 #include "chrome/browser/ui/browser_finder.h" 37 #include "chrome/browser/ui/chrome_pages.h" 38 #include "chrome/browser/ui/webui/favicon_source.h" 39 #include "chrome/browser/ui/webui/metrics_handler.h" 40 #include "chrome/common/chrome_switches.h" 41 #include "chrome/common/pref_names.h" 42 #include "chrome/common/url_constants.h" 43 #include "components/bookmarks/browser/bookmark_model.h" 44 #include "components/bookmarks/browser/bookmark_utils.h" 45 #include "content/public/browser/notification_details.h" 46 #include "content/public/browser/notification_source.h" 47 #include "content/public/browser/url_data_source.h" 48 #include "content/public/browser/web_ui.h" 49 #include "content/public/browser/web_ui_data_source.h" 50 #include "grit/browser_resources.h" 51 #include "grit/generated_resources.h" 52 #include "grit/theme_resources.h" 53 #include "net/base/escape.h" 54 #include "net/base/net_util.h" 55 #include "sync/protocol/history_delete_directive_specifics.pb.h" 56 #include "ui/base/l10n/l10n_util.h" 57 #include "ui/base/l10n/time_format.h" 58 #include "ui/base/resource/resource_bundle.h" 59 60 #if defined(ENABLE_MANAGED_USERS) 61 #include "chrome/browser/supervised_user/supervised_user_navigation_observer.h" 62 #include "chrome/browser/supervised_user/supervised_user_service.h" 63 #include "chrome/browser/supervised_user/supervised_user_service_factory.h" 64 #include "chrome/browser/supervised_user/supervised_user_url_filter.h" 65 #endif 66 67 #if defined(OS_ANDROID) 68 #include "chrome/browser/android/chromium_application.h" 69 #endif 70 71 #if !defined(OS_ANDROID) && !defined(OS_IOS) 72 #include "chrome/browser/ui/webui/ntp/foreign_session_handler.h" 73 #include "chrome/browser/ui/webui/ntp/ntp_login_handler.h" 74 #endif 75 76 static const char kStringsJsFile[] = "strings.js"; 77 static const char kHistoryJsFile[] = "history.js"; 78 static const char kOtherDevicesJsFile[] = "other_devices.js"; 79 80 // The amount of time to wait for a response from the WebHistoryService. 81 static const int kWebHistoryTimeoutSeconds = 3; 82 83 namespace { 84 85 // Buckets for UMA histograms. 86 enum WebHistoryQueryBuckets { 87 WEB_HISTORY_QUERY_FAILED = 0, 88 WEB_HISTORY_QUERY_SUCCEEDED, 89 WEB_HISTORY_QUERY_TIMED_OUT, 90 NUM_WEB_HISTORY_QUERY_BUCKETS 91 }; 92 93 #if defined(OS_MACOSX) 94 const char kIncognitoModeShortcut[] = "(" 95 "\xE2\x87\xA7" // Shift symbol (U+21E7 'UPWARDS WHITE ARROW'). 96 "\xE2\x8C\x98" // Command symbol (U+2318 'PLACE OF INTEREST SIGN'). 97 "N)"; 98 #elif defined(OS_WIN) 99 const char kIncognitoModeShortcut[] = "(Ctrl+Shift+N)"; 100 #else 101 const char kIncognitoModeShortcut[] = "(Shift+Ctrl+N)"; 102 #endif 103 104 // Identifiers for the type of device from which a history entry originated. 105 static const char kDeviceTypeLaptop[] = "laptop"; 106 static const char kDeviceTypePhone[] = "phone"; 107 static const char kDeviceTypeTablet[] = "tablet"; 108 109 content::WebUIDataSource* CreateHistoryUIHTMLSource(Profile* profile) { 110 PrefService* prefs = profile->GetPrefs(); 111 112 content::WebUIDataSource* source = 113 content::WebUIDataSource::Create(chrome::kChromeUIHistoryFrameHost); 114 source->AddBoolean("isUserSignedIn", 115 !prefs->GetString(prefs::kGoogleServicesUsername).empty()); 116 source->AddLocalizedString("collapseSessionMenuItemText", 117 IDS_NEW_TAB_OTHER_SESSIONS_COLLAPSE_SESSION); 118 source->AddLocalizedString("expandSessionMenuItemText", 119 IDS_NEW_TAB_OTHER_SESSIONS_EXPAND_SESSION); 120 source->AddLocalizedString("restoreSessionMenuItemText", 121 IDS_NEW_TAB_OTHER_SESSIONS_OPEN_ALL); 122 source->AddLocalizedString("xMore", IDS_OTHER_DEVICES_X_MORE); 123 source->AddLocalizedString("loading", IDS_HISTORY_LOADING); 124 source->AddLocalizedString("title", IDS_HISTORY_TITLE); 125 source->AddLocalizedString("newest", IDS_HISTORY_NEWEST); 126 source->AddLocalizedString("newer", IDS_HISTORY_NEWER); 127 source->AddLocalizedString("older", IDS_HISTORY_OLDER); 128 source->AddLocalizedString("searchResultsFor", IDS_HISTORY_SEARCHRESULTSFOR); 129 source->AddLocalizedString("history", IDS_HISTORY_BROWSERESULTS); 130 source->AddLocalizedString("cont", IDS_HISTORY_CONTINUED); 131 source->AddLocalizedString("searchButton", IDS_HISTORY_SEARCH_BUTTON); 132 source->AddLocalizedString("noSearchResults", IDS_HISTORY_NO_SEARCH_RESULTS); 133 source->AddLocalizedString("noResults", IDS_HISTORY_NO_RESULTS); 134 source->AddLocalizedString("historyInterval", IDS_HISTORY_INTERVAL); 135 source->AddLocalizedString("removeSelected", 136 IDS_HISTORY_REMOVE_SELECTED_ITEMS); 137 source->AddLocalizedString("clearAllHistory", 138 IDS_HISTORY_OPEN_CLEAR_BROWSING_DATA_DIALOG); 139 source->AddString( 140 "deleteWarning", 141 l10n_util::GetStringFUTF16(IDS_HISTORY_DELETE_PRIOR_VISITS_WARNING, 142 base::UTF8ToUTF16(kIncognitoModeShortcut))); 143 source->AddLocalizedString("actionMenuDescription", 144 IDS_HISTORY_ACTION_MENU_DESCRIPTION); 145 source->AddLocalizedString("removeFromHistory", IDS_HISTORY_REMOVE_PAGE); 146 source->AddLocalizedString("moreFromSite", IDS_HISTORY_MORE_FROM_SITE); 147 source->AddLocalizedString("groupByDomainLabel", IDS_GROUP_BY_DOMAIN_LABEL); 148 source->AddLocalizedString("rangeLabel", IDS_HISTORY_RANGE_LABEL); 149 source->AddLocalizedString("rangeAllTime", IDS_HISTORY_RANGE_ALL_TIME); 150 source->AddLocalizedString("rangeWeek", IDS_HISTORY_RANGE_WEEK); 151 source->AddLocalizedString("rangeMonth", IDS_HISTORY_RANGE_MONTH); 152 source->AddLocalizedString("rangeToday", IDS_HISTORY_RANGE_TODAY); 153 source->AddLocalizedString("rangeNext", IDS_HISTORY_RANGE_NEXT); 154 source->AddLocalizedString("rangePrevious", IDS_HISTORY_RANGE_PREVIOUS); 155 source->AddLocalizedString("numberVisits", IDS_HISTORY_NUMBER_VISITS); 156 source->AddLocalizedString("filterAllowed", IDS_HISTORY_FILTER_ALLOWED); 157 source->AddLocalizedString("filterBlocked", IDS_HISTORY_FILTER_BLOCKED); 158 source->AddLocalizedString("inContentPack", IDS_HISTORY_IN_CONTENT_PACK); 159 source->AddLocalizedString("allowItems", IDS_HISTORY_FILTER_ALLOW_ITEMS); 160 source->AddLocalizedString("blockItems", IDS_HISTORY_FILTER_BLOCK_ITEMS); 161 source->AddLocalizedString("lockButton", IDS_HISTORY_LOCK_BUTTON); 162 source->AddLocalizedString("blockedVisitText", 163 IDS_HISTORY_BLOCKED_VISIT_TEXT); 164 source->AddLocalizedString("unlockButton", IDS_HISTORY_UNLOCK_BUTTON); 165 source->AddLocalizedString("hasSyncedResults", 166 IDS_HISTORY_HAS_SYNCED_RESULTS); 167 source->AddLocalizedString("noSyncedResults", IDS_HISTORY_NO_SYNCED_RESULTS); 168 source->AddLocalizedString("cancel", IDS_CANCEL); 169 source->AddLocalizedString("deleteConfirm", 170 IDS_HISTORY_DELETE_PRIOR_VISITS_CONFIRM_BUTTON); 171 source->AddBoolean("isFullHistorySyncEnabled", 172 WebHistoryServiceFactory::GetForProfile(profile) != NULL); 173 source->AddBoolean("groupByDomain", 174 CommandLine::ForCurrentProcess()->HasSwitch( 175 switches::kHistoryEnableGroupByDomain)); 176 source->AddBoolean("allowDeletingHistory", 177 prefs->GetBoolean(prefs::kAllowDeletingBrowserHistory)); 178 source->AddBoolean("isInstantExtendedApiEnabled", 179 chrome::IsInstantExtendedAPIEnabled()); 180 181 source->SetJsonPath(kStringsJsFile); 182 source->AddResourcePath(kHistoryJsFile, IDR_HISTORY_JS); 183 source->AddResourcePath(kOtherDevicesJsFile, IDR_OTHER_DEVICES_JS); 184 source->SetDefaultResource(IDR_HISTORY_HTML); 185 source->SetUseJsonJSFormatV2(); 186 source->DisableDenyXFrameOptions(); 187 source->AddBoolean("isManagedProfile", profile->IsSupervised()); 188 source->AddBoolean("showDeleteVisitUI", !profile->IsSupervised()); 189 190 return source; 191 } 192 193 // Returns a localized version of |visit_time| including a relative 194 // indicator (e.g. today, yesterday). 195 base::string16 getRelativeDateLocalized(const base::Time& visit_time) { 196 base::Time midnight = base::Time::Now().LocalMidnight(); 197 base::string16 date_str = ui::TimeFormat::RelativeDate(visit_time, &midnight); 198 if (date_str.empty()) { 199 date_str = base::TimeFormatFriendlyDate(visit_time); 200 } else { 201 date_str = l10n_util::GetStringFUTF16( 202 IDS_HISTORY_DATE_WITH_RELATIVE_TIME, 203 date_str, 204 base::TimeFormatFriendlyDate(visit_time)); 205 } 206 return date_str; 207 } 208 209 210 // Sets the correct year when substracting months from a date. 211 void normalizeMonths(base::Time::Exploded* exploded) { 212 // Decrease a year at a time until we have a proper date. 213 while (exploded->month < 1) { 214 exploded->month += 12; 215 exploded->year--; 216 } 217 } 218 219 // Returns true if |entry| represents a local visit that had no corresponding 220 // visit on the server. 221 bool IsLocalOnlyResult(const BrowsingHistoryHandler::HistoryEntry& entry) { 222 return entry.entry_type == BrowsingHistoryHandler::HistoryEntry::LOCAL_ENTRY; 223 } 224 225 // Gets the name and type of a device for the given sync client ID. 226 // |name| and |type| are out parameters. 227 void GetDeviceNameAndType(const ProfileSyncService* sync_service, 228 const std::string& client_id, 229 std::string* name, 230 std::string* type) { 231 if (sync_service && sync_service->sync_initialized()) { 232 scoped_ptr<browser_sync::DeviceInfo> device_info = 233 sync_service->GetDeviceInfo(client_id); 234 if (device_info.get()) { 235 *name = device_info->client_name(); 236 switch (device_info->device_type()) { 237 case sync_pb::SyncEnums::TYPE_PHONE: 238 *type = kDeviceTypePhone; 239 break; 240 case sync_pb::SyncEnums::TYPE_TABLET: 241 *type = kDeviceTypeTablet; 242 break; 243 default: 244 *type = kDeviceTypeLaptop; 245 } 246 return; 247 } 248 } else { 249 NOTREACHED() << "Got a remote history entry but no ProfileSyncService."; 250 } 251 *name = l10n_util::GetStringUTF8(IDS_HISTORY_UNKNOWN_DEVICE); 252 *type = kDeviceTypeLaptop; 253 } 254 255 } // namespace 256 257 //////////////////////////////////////////////////////////////////////////////// 258 // 259 // BrowsingHistoryHandler 260 // 261 //////////////////////////////////////////////////////////////////////////////// 262 263 BrowsingHistoryHandler::HistoryEntry::HistoryEntry( 264 BrowsingHistoryHandler::HistoryEntry::EntryType entry_type, 265 const GURL& url, const base::string16& title, base::Time time, 266 const std::string& client_id, bool is_search_result, 267 const base::string16& snippet, bool blocked_visit, 268 const std::string& accept_languages) { 269 this->entry_type = entry_type; 270 this->url = url; 271 this->title = title; 272 this->time = time; 273 this->client_id = client_id; 274 all_timestamps.insert(time.ToInternalValue()); 275 this->is_search_result = is_search_result; 276 this->snippet = snippet; 277 this->blocked_visit = blocked_visit; 278 this->accept_languages = accept_languages; 279 } 280 281 BrowsingHistoryHandler::HistoryEntry::HistoryEntry() 282 : entry_type(EMPTY_ENTRY), is_search_result(false), blocked_visit(false) { 283 } 284 285 BrowsingHistoryHandler::HistoryEntry::~HistoryEntry() { 286 } 287 288 void BrowsingHistoryHandler::HistoryEntry::SetUrlAndTitle( 289 base::DictionaryValue* result) const { 290 result->SetString("url", url.spec()); 291 292 bool using_url_as_the_title = false; 293 base::string16 title_to_set(title); 294 if (title.empty()) { 295 using_url_as_the_title = true; 296 title_to_set = base::UTF8ToUTF16(url.spec()); 297 } 298 299 // Since the title can contain BiDi text, we need to mark the text as either 300 // RTL or LTR, depending on the characters in the string. If we use the URL 301 // as the title, we mark the title as LTR since URLs are always treated as 302 // left to right strings. 303 if (base::i18n::IsRTL()) { 304 if (using_url_as_the_title) 305 base::i18n::WrapStringWithLTRFormatting(&title_to_set); 306 else 307 base::i18n::AdjustStringForLocaleDirection(&title_to_set); 308 } 309 result->SetString("title", title_to_set); 310 } 311 312 scoped_ptr<base::DictionaryValue> BrowsingHistoryHandler::HistoryEntry::ToValue( 313 BookmarkModel* bookmark_model, 314 SupervisedUserService* supervised_user_service, 315 const ProfileSyncService* sync_service) const { 316 scoped_ptr<base::DictionaryValue> result(new base::DictionaryValue()); 317 SetUrlAndTitle(result.get()); 318 319 base::string16 domain = net::IDNToUnicode(url.host(), accept_languages); 320 // When the domain is empty, use the scheme instead. This allows for a 321 // sensible treatment of e.g. file: URLs when group by domain is on. 322 if (domain.empty()) 323 domain = base::UTF8ToUTF16(url.scheme() + ":"); 324 325 result->SetString("domain", domain); 326 result->SetDouble("time", time.ToJsTime()); 327 328 // Pass the timestamps in a list. 329 scoped_ptr<base::ListValue> timestamps(new base::ListValue); 330 for (std::set<int64>::const_iterator it = all_timestamps.begin(); 331 it != all_timestamps.end(); ++it) { 332 timestamps->AppendDouble(base::Time::FromInternalValue(*it).ToJsTime()); 333 } 334 result->Set("allTimestamps", timestamps.release()); 335 336 // Always pass the short date since it is needed both in the search and in 337 // the monthly view. 338 result->SetString("dateShort", base::TimeFormatShortDate(time)); 339 340 // Only pass in the strings we need (search results need a shortdate 341 // and snippet, browse results need day and time information). 342 if (is_search_result) { 343 result->SetString("snippet", snippet); 344 } else { 345 base::Time midnight = base::Time::Now().LocalMidnight(); 346 base::string16 date_str = ui::TimeFormat::RelativeDate(time, &midnight); 347 if (date_str.empty()) { 348 date_str = base::TimeFormatFriendlyDate(time); 349 } else { 350 date_str = l10n_util::GetStringFUTF16( 351 IDS_HISTORY_DATE_WITH_RELATIVE_TIME, 352 date_str, 353 base::TimeFormatFriendlyDate(time)); 354 } 355 result->SetString("dateRelativeDay", date_str); 356 result->SetString("dateTimeOfDay", base::TimeFormatTimeOfDay(time)); 357 } 358 result->SetBoolean("starred", bookmark_model->IsBookmarked(url)); 359 360 std::string device_name; 361 std::string device_type; 362 if (!client_id.empty()) 363 GetDeviceNameAndType(sync_service, client_id, &device_name, &device_type); 364 result->SetString("deviceName", device_name); 365 result->SetString("deviceType", device_type); 366 367 #if defined(ENABLE_MANAGED_USERS) 368 if (supervised_user_service) { 369 const SupervisedUserURLFilter* url_filter = 370 supervised_user_service->GetURLFilterForUIThread(); 371 int filtering_behavior = 372 url_filter->GetFilteringBehaviorForURL(url.GetWithEmptyPath()); 373 result->SetInteger("hostFilteringBehavior", filtering_behavior); 374 375 result->SetBoolean("blockedVisit", blocked_visit); 376 } 377 #endif 378 379 return result.Pass(); 380 } 381 382 bool BrowsingHistoryHandler::HistoryEntry::SortByTimeDescending( 383 const BrowsingHistoryHandler::HistoryEntry& entry1, 384 const BrowsingHistoryHandler::HistoryEntry& entry2) { 385 return entry1.time > entry2.time; 386 } 387 388 BrowsingHistoryHandler::BrowsingHistoryHandler() 389 : has_pending_delete_request_(false), 390 weak_factory_(this) { 391 } 392 393 BrowsingHistoryHandler::~BrowsingHistoryHandler() { 394 history_request_consumer_.CancelAllRequests(); 395 web_history_request_.reset(); 396 } 397 398 void BrowsingHistoryHandler::RegisterMessages() { 399 // Create our favicon data source. 400 Profile* profile = Profile::FromWebUI(web_ui()); 401 content::URLDataSource::Add( 402 profile, new FaviconSource(profile, FaviconSource::ANY)); 403 404 // Get notifications when history is cleared. 405 registrar_.Add(this, chrome::NOTIFICATION_HISTORY_URLS_DELETED, 406 content::Source<Profile>(profile->GetOriginalProfile())); 407 408 web_ui()->RegisterMessageCallback("queryHistory", 409 base::Bind(&BrowsingHistoryHandler::HandleQueryHistory, 410 base::Unretained(this))); 411 web_ui()->RegisterMessageCallback("removeVisits", 412 base::Bind(&BrowsingHistoryHandler::HandleRemoveVisits, 413 base::Unretained(this))); 414 web_ui()->RegisterMessageCallback("clearBrowsingData", 415 base::Bind(&BrowsingHistoryHandler::HandleClearBrowsingData, 416 base::Unretained(this))); 417 web_ui()->RegisterMessageCallback("removeBookmark", 418 base::Bind(&BrowsingHistoryHandler::HandleRemoveBookmark, 419 base::Unretained(this))); 420 } 421 422 bool BrowsingHistoryHandler::ExtractIntegerValueAtIndex( 423 const base::ListValue* value, 424 int index, 425 int* out_int) { 426 double double_value; 427 if (value->GetDouble(index, &double_value)) { 428 *out_int = static_cast<int>(double_value); 429 return true; 430 } 431 NOTREACHED(); 432 return false; 433 } 434 435 void BrowsingHistoryHandler::WebHistoryTimeout() { 436 // TODO(dubroy): Communicate the failure to the front end. 437 if (!history_request_consumer_.HasPendingRequests()) 438 ReturnResultsToFrontEnd(); 439 440 UMA_HISTOGRAM_ENUMERATION( 441 "WebHistory.QueryCompletion", 442 WEB_HISTORY_QUERY_TIMED_OUT, NUM_WEB_HISTORY_QUERY_BUCKETS); 443 } 444 445 void BrowsingHistoryHandler::QueryHistory( 446 base::string16 search_text, const history::QueryOptions& options) { 447 Profile* profile = Profile::FromWebUI(web_ui()); 448 449 // Anything in-flight is invalid. 450 history_request_consumer_.CancelAllRequests(); 451 web_history_request_.reset(); 452 453 query_results_.clear(); 454 results_info_value_.Clear(); 455 456 HistoryService* hs = HistoryServiceFactory::GetForProfile( 457 profile, Profile::EXPLICIT_ACCESS); 458 hs->QueryHistory(search_text, 459 options, 460 &history_request_consumer_, 461 base::Bind(&BrowsingHistoryHandler::QueryComplete, 462 base::Unretained(this), search_text, options)); 463 464 history::WebHistoryService* web_history = 465 WebHistoryServiceFactory::GetForProfile(profile); 466 if (web_history) { 467 web_history_query_results_.clear(); 468 web_history_request_ = web_history->QueryHistory( 469 search_text, 470 options, 471 base::Bind(&BrowsingHistoryHandler::WebHistoryQueryComplete, 472 base::Unretained(this), 473 search_text, options, 474 base::TimeTicks::Now())); 475 // Start a timer so we know when to give up. 476 web_history_timer_.Start( 477 FROM_HERE, base::TimeDelta::FromSeconds(kWebHistoryTimeoutSeconds), 478 this, &BrowsingHistoryHandler::WebHistoryTimeout); 479 480 // Set this to false until the results actually arrive. 481 results_info_value_.SetBoolean("hasSyncedResults", false); 482 } 483 } 484 485 void BrowsingHistoryHandler::HandleQueryHistory(const base::ListValue* args) { 486 history::QueryOptions options; 487 488 // Parse the arguments from JavaScript. There are five required arguments: 489 // - the text to search for (may be empty) 490 // - the offset from which the search should start (in multiples of week or 491 // month, set by the next argument). 492 // - the range (BrowsingHistoryHandler::Range) Enum value that sets the range 493 // of the query. 494 // - the end time for the query. Only results older than this time will be 495 // returned. 496 // - the maximum number of results to return (may be 0, meaning that there 497 // is no maximum). 498 base::string16 search_text = ExtractStringValue(args); 499 int offset; 500 if (!args->GetInteger(1, &offset)) { 501 NOTREACHED() << "Failed to convert argument 1. "; 502 return; 503 } 504 int range; 505 if (!args->GetInteger(2, &range)) { 506 NOTREACHED() << "Failed to convert argument 2. "; 507 return; 508 } 509 510 if (range == BrowsingHistoryHandler::MONTH) 511 SetQueryTimeInMonths(offset, &options); 512 else if (range == BrowsingHistoryHandler::WEEK) 513 SetQueryTimeInWeeks(offset, &options); 514 515 double end_time; 516 if (!args->GetDouble(3, &end_time)) { 517 NOTREACHED() << "Failed to convert argument 3. "; 518 return; 519 } 520 if (end_time) 521 options.end_time = base::Time::FromJsTime(end_time); 522 523 if (!ExtractIntegerValueAtIndex(args, 4, &options.max_count)) { 524 NOTREACHED() << "Failed to convert argument 4."; 525 return; 526 } 527 528 options.duplicate_policy = history::QueryOptions::REMOVE_DUPLICATES_PER_DAY; 529 QueryHistory(search_text, options); 530 } 531 532 void BrowsingHistoryHandler::HandleRemoveVisits(const base::ListValue* args) { 533 Profile* profile = Profile::FromWebUI(web_ui()); 534 // TODO(davidben): history.js is not aware of this failure and will still 535 // override |deleteCompleteCallback_|. 536 if (delete_task_tracker_.HasTrackedTasks() || 537 has_pending_delete_request_ || 538 !profile->GetPrefs()->GetBoolean(prefs::kAllowDeletingBrowserHistory)) { 539 web_ui()->CallJavascriptFunction("deleteFailed"); 540 return; 541 } 542 543 HistoryService* history_service = 544 HistoryServiceFactory::GetForProfile(profile, Profile::EXPLICIT_ACCESS); 545 history::WebHistoryService* web_history = 546 WebHistoryServiceFactory::GetForProfile(profile); 547 548 base::Time now = base::Time::Now(); 549 std::vector<history::ExpireHistoryArgs> expire_list; 550 expire_list.reserve(args->GetSize()); 551 552 DCHECK(urls_to_be_deleted_.empty()); 553 for (base::ListValue::const_iterator it = args->begin(); 554 it != args->end(); ++it) { 555 base::DictionaryValue* deletion = NULL; 556 base::string16 url; 557 base::ListValue* timestamps = NULL; 558 559 // Each argument is a dictionary with properties "url" and "timestamps". 560 if (!((*it)->GetAsDictionary(&deletion) && 561 deletion->GetString("url", &url) && 562 deletion->GetList("timestamps", ×tamps))) { 563 NOTREACHED() << "Unable to extract arguments"; 564 return; 565 } 566 DCHECK(timestamps->GetSize() > 0); 567 568 // In order to ensure that visits will be deleted from the server and other 569 // clients (even if they are offline), create a sync delete directive for 570 // each visit to be deleted. 571 sync_pb::HistoryDeleteDirectiveSpecifics delete_directive; 572 sync_pb::GlobalIdDirective* global_id_directive = 573 delete_directive.mutable_global_id_directive(); 574 575 double timestamp; 576 history::ExpireHistoryArgs* expire_args = NULL; 577 for (base::ListValue::const_iterator ts_iterator = timestamps->begin(); 578 ts_iterator != timestamps->end(); ++ts_iterator) { 579 if (!(*ts_iterator)->GetAsDouble(×tamp)) { 580 NOTREACHED() << "Unable to extract visit timestamp."; 581 continue; 582 } 583 base::Time visit_time = base::Time::FromJsTime(timestamp); 584 if (!expire_args) { 585 GURL gurl(url); 586 expire_list.resize(expire_list.size() + 1); 587 expire_args = &expire_list.back(); 588 expire_args->SetTimeRangeForOneDay(visit_time); 589 expire_args->urls.insert(gurl); 590 urls_to_be_deleted_.insert(gurl); 591 } 592 // The local visit time is treated as a global ID for the visit. 593 global_id_directive->add_global_id(visit_time.ToInternalValue()); 594 } 595 596 // Set the start and end time in microseconds since the Unix epoch. 597 global_id_directive->set_start_time_usec( 598 (expire_args->begin_time - base::Time::UnixEpoch()).InMicroseconds()); 599 600 // Delete directives shouldn't have an end time in the future. 601 // TODO(dubroy): Use sane time (crbug.com/146090) here when it's ready. 602 base::Time end_time = std::min(expire_args->end_time, now); 603 604 // -1 because end time in delete directives is inclusive. 605 global_id_directive->set_end_time_usec( 606 (end_time - base::Time::UnixEpoch()).InMicroseconds() - 1); 607 608 // TODO(dubroy): Figure out the proper way to handle an error here. 609 if (web_history) 610 history_service->ProcessLocalDeleteDirective(delete_directive); 611 } 612 613 history_service->ExpireHistory( 614 expire_list, 615 base::Bind(&BrowsingHistoryHandler::RemoveComplete, 616 base::Unretained(this)), 617 &delete_task_tracker_); 618 619 if (web_history) { 620 has_pending_delete_request_ = true; 621 web_history->ExpireHistory( 622 expire_list, 623 base::Bind(&BrowsingHistoryHandler::RemoveWebHistoryComplete, 624 weak_factory_.GetWeakPtr())); 625 } 626 627 #if defined(ENABLE_EXTENSIONS) 628 // If the profile has activity logging enabled also clean up any URLs from 629 // the extension activity log. The extension activity log contains URLS 630 // which websites an extension has activity on so it will indirectly 631 // contain websites that a user has visited. 632 extensions::ActivityLog* activity_log = 633 extensions::ActivityLog::GetInstance(profile); 634 for (std::vector<history::ExpireHistoryArgs>::const_iterator it = 635 expire_list.begin(); it != expire_list.end(); ++it) { 636 activity_log->RemoveURLs(it->urls); 637 } 638 #endif 639 } 640 641 void BrowsingHistoryHandler::HandleClearBrowsingData( 642 const base::ListValue* args) { 643 #if defined(OS_ANDROID) 644 chrome::android::ChromiumApplication::OpenClearBrowsingData( 645 web_ui()->GetWebContents()); 646 #else 647 // TODO(beng): This is an improper direct dependency on Browser. Route this 648 // through some sort of delegate. 649 Browser* browser = chrome::FindBrowserWithWebContents( 650 web_ui()->GetWebContents()); 651 chrome::ShowClearBrowsingDataDialog(browser); 652 #endif 653 } 654 655 void BrowsingHistoryHandler::HandleRemoveBookmark(const base::ListValue* args) { 656 base::string16 url = ExtractStringValue(args); 657 Profile* profile = Profile::FromWebUI(web_ui()); 658 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile); 659 bookmark_utils::RemoveAllBookmarks(model, GURL(url)); 660 } 661 662 // static 663 void BrowsingHistoryHandler::MergeDuplicateResults( 664 std::vector<BrowsingHistoryHandler::HistoryEntry>* results) { 665 std::vector<BrowsingHistoryHandler::HistoryEntry> new_results; 666 // Pre-reserve the size of the new vector. Since we're working with pointers 667 // later on not doing this could lead to the vector being resized and to 668 // pointers to invalid locations. 669 new_results.reserve(results->size()); 670 // Maps a URL to the most recent entry on a particular day. 671 std::map<GURL,BrowsingHistoryHandler::HistoryEntry*> 672 current_day_entries; 673 674 // Keeps track of the day that |current_day_urls| is holding the URLs for, 675 // in order to handle removing per-day duplicates. 676 base::Time current_day_midnight; 677 678 std::sort( 679 results->begin(), results->end(), HistoryEntry::SortByTimeDescending); 680 681 for (std::vector<BrowsingHistoryHandler::HistoryEntry>::const_iterator it = 682 results->begin(); it != results->end(); ++it) { 683 // Reset the list of found URLs when a visit from a new day is encountered. 684 if (current_day_midnight != it->time.LocalMidnight()) { 685 current_day_entries.clear(); 686 current_day_midnight = it->time.LocalMidnight(); 687 } 688 689 // Keep this visit if it's the first visit to this URL on the current day. 690 if (current_day_entries.count(it->url) == 0) { 691 new_results.push_back(*it); 692 current_day_entries[it->url] = &new_results.back(); 693 } else { 694 // Keep track of the timestamps of all visits to the URL on the same day. 695 BrowsingHistoryHandler::HistoryEntry* entry = 696 current_day_entries[it->url]; 697 entry->all_timestamps.insert( 698 it->all_timestamps.begin(), it->all_timestamps.end()); 699 700 if (entry->entry_type != it->entry_type) { 701 entry->entry_type = 702 BrowsingHistoryHandler::HistoryEntry::COMBINED_ENTRY; 703 } 704 } 705 } 706 results->swap(new_results); 707 } 708 709 void BrowsingHistoryHandler::ReturnResultsToFrontEnd() { 710 Profile* profile = Profile::FromWebUI(web_ui()); 711 BookmarkModel* bookmark_model = BookmarkModelFactory::GetForProfile(profile); 712 SupervisedUserService* supervised_user_service = NULL; 713 #if defined(ENABLE_MANAGED_USERS) 714 if (profile->IsSupervised()) 715 supervised_user_service = 716 SupervisedUserServiceFactory::GetForProfile(profile); 717 #endif 718 ProfileSyncService* sync_service = 719 ProfileSyncServiceFactory::GetInstance()->GetForProfile(profile); 720 721 // Combine the local and remote results into |query_results_|, and remove 722 // any duplicates. 723 if (!web_history_query_results_.empty()) { 724 int local_result_count = query_results_.size(); 725 query_results_.insert(query_results_.end(), 726 web_history_query_results_.begin(), 727 web_history_query_results_.end()); 728 MergeDuplicateResults(&query_results_); 729 730 if (local_result_count) { 731 // In the best case, we expect that all local results are duplicated on 732 // the server. Keep track of how many are missing. 733 int missing_count = std::count_if( 734 query_results_.begin(), query_results_.end(), IsLocalOnlyResult); 735 UMA_HISTOGRAM_PERCENTAGE("WebHistory.LocalResultMissingOnServer", 736 missing_count * 100.0 / local_result_count); 737 } 738 } 739 740 // Convert the result vector into a ListValue. 741 base::ListValue results_value; 742 for (std::vector<BrowsingHistoryHandler::HistoryEntry>::iterator it = 743 query_results_.begin(); it != query_results_.end(); ++it) { 744 scoped_ptr<base::Value> value( 745 it->ToValue(bookmark_model, supervised_user_service, sync_service)); 746 results_value.Append(value.release()); 747 } 748 749 web_ui()->CallJavascriptFunction( 750 "historyResult", results_info_value_, results_value); 751 results_info_value_.Clear(); 752 query_results_.clear(); 753 web_history_query_results_.clear(); 754 } 755 756 void BrowsingHistoryHandler::QueryComplete( 757 const base::string16& search_text, 758 const history::QueryOptions& options, 759 HistoryService::Handle request_handle, 760 history::QueryResults* results) { 761 DCHECK_EQ(0U, query_results_.size()); 762 query_results_.reserve(results->size()); 763 const std::string accept_languages = GetAcceptLanguages(); 764 765 for (size_t i = 0; i < results->size(); ++i) { 766 history::URLResult const &page = (*results)[i]; 767 // TODO(dubroy): Use sane time (crbug.com/146090) here when it's ready. 768 query_results_.push_back( 769 HistoryEntry( 770 HistoryEntry::LOCAL_ENTRY, 771 page.url(), 772 page.title(), 773 page.visit_time(), 774 std::string(), 775 !search_text.empty(), 776 page.snippet().text(), 777 page.blocked_visit(), 778 accept_languages)); 779 } 780 781 results_info_value_.SetString("term", search_text); 782 results_info_value_.SetBoolean("finished", results->reached_beginning()); 783 784 // Add the specific dates that were searched to display them. 785 // TODO(sergiu): Put today if the start is in the future. 786 results_info_value_.SetString("queryStartTime", 787 getRelativeDateLocalized(options.begin_time)); 788 if (!options.end_time.is_null()) { 789 results_info_value_.SetString("queryEndTime", 790 getRelativeDateLocalized(options.end_time - 791 base::TimeDelta::FromDays(1))); 792 } else { 793 results_info_value_.SetString("queryEndTime", 794 getRelativeDateLocalized(base::Time::Now())); 795 } 796 if (!web_history_timer_.IsRunning()) 797 ReturnResultsToFrontEnd(); 798 } 799 800 void BrowsingHistoryHandler::WebHistoryQueryComplete( 801 const base::string16& search_text, 802 const history::QueryOptions& options, 803 base::TimeTicks start_time, 804 history::WebHistoryService::Request* request, 805 const base::DictionaryValue* results_value) { 806 base::TimeDelta delta = base::TimeTicks::Now() - start_time; 807 UMA_HISTOGRAM_TIMES("WebHistory.ResponseTime", delta); 808 const std::string accept_languages = GetAcceptLanguages(); 809 810 // If the response came in too late, do nothing. 811 // TODO(dubroy): Maybe show a banner, and prompt the user to reload? 812 if (!web_history_timer_.IsRunning()) 813 return; 814 web_history_timer_.Stop(); 815 816 UMA_HISTOGRAM_ENUMERATION( 817 "WebHistory.QueryCompletion", 818 results_value ? WEB_HISTORY_QUERY_SUCCEEDED : WEB_HISTORY_QUERY_FAILED, 819 NUM_WEB_HISTORY_QUERY_BUCKETS); 820 821 DCHECK_EQ(0U, web_history_query_results_.size()); 822 const base::ListValue* events = NULL; 823 if (results_value && results_value->GetList("event", &events)) { 824 web_history_query_results_.reserve(events->GetSize()); 825 for (unsigned int i = 0; i < events->GetSize(); ++i) { 826 const base::DictionaryValue* event = NULL; 827 const base::DictionaryValue* result = NULL; 828 const base::ListValue* results = NULL; 829 const base::ListValue* ids = NULL; 830 base::string16 url; 831 base::string16 title; 832 base::Time visit_time; 833 834 if (!(events->GetDictionary(i, &event) && 835 event->GetList("result", &results) && 836 results->GetDictionary(0, &result) && 837 result->GetString("url", &url) && 838 result->GetList("id", &ids) && 839 ids->GetSize() > 0)) { 840 LOG(WARNING) << "Improperly formed JSON response from history server."; 841 continue; 842 } 843 // Title is optional, so the return value is ignored here. 844 result->GetString("title", &title); 845 846 // Extract the timestamps of all the visits to this URL. 847 // They are referred to as "IDs" by the server. 848 for (int j = 0; j < static_cast<int>(ids->GetSize()); ++j) { 849 const base::DictionaryValue* id = NULL; 850 std::string timestamp_string; 851 int64 timestamp_usec; 852 853 if (!(ids->GetDictionary(j, &id) && 854 id->GetString("timestamp_usec", ×tamp_string) && 855 base::StringToInt64(timestamp_string, ×tamp_usec))) { 856 NOTREACHED() << "Unable to extract timestamp."; 857 continue; 858 } 859 // The timestamp on the server is a Unix time. 860 base::Time time = base::Time::UnixEpoch() + 861 base::TimeDelta::FromMicroseconds(timestamp_usec); 862 863 // Get the ID of the client that this visit came from. 864 std::string client_id; 865 id->GetString("client_id", &client_id); 866 867 web_history_query_results_.push_back( 868 HistoryEntry( 869 HistoryEntry::REMOTE_ENTRY, 870 GURL(url), 871 title, 872 time, 873 client_id, 874 !search_text.empty(), 875 base::string16(), 876 /* blocked_visit */ false, 877 accept_languages)); 878 } 879 } 880 } else if (results_value) { 881 NOTREACHED() << "Failed to parse JSON response."; 882 } 883 results_info_value_.SetBoolean("hasSyncedResults", results_value != NULL); 884 if (!history_request_consumer_.HasPendingRequests()) 885 ReturnResultsToFrontEnd(); 886 } 887 888 void BrowsingHistoryHandler::RemoveComplete() { 889 urls_to_be_deleted_.clear(); 890 891 // Notify the page that the deletion request is complete, but only if a web 892 // history delete request is not still pending. 893 if (!has_pending_delete_request_) 894 web_ui()->CallJavascriptFunction("deleteComplete"); 895 } 896 897 void BrowsingHistoryHandler::RemoveWebHistoryComplete(bool success) { 898 has_pending_delete_request_ = false; 899 // TODO(dubroy): Should we handle failure somehow? Delete directives will 900 // ensure that the visits are eventually deleted, so maybe it's not necessary. 901 if (!delete_task_tracker_.HasTrackedTasks()) 902 RemoveComplete(); 903 } 904 905 void BrowsingHistoryHandler::SetQueryTimeInWeeks( 906 int offset, history::QueryOptions* options) { 907 // LocalMidnight returns the beginning of the current day so get the 908 // beginning of the next one. 909 base::Time midnight = base::Time::Now().LocalMidnight() + 910 base::TimeDelta::FromDays(1); 911 options->end_time = midnight - 912 base::TimeDelta::FromDays(7 * offset); 913 options->begin_time = midnight - 914 base::TimeDelta::FromDays(7 * (offset + 1)); 915 } 916 917 void BrowsingHistoryHandler::SetQueryTimeInMonths( 918 int offset, history::QueryOptions* options) { 919 // Configure the begin point of the search to the start of the 920 // current month. 921 base::Time::Exploded exploded; 922 base::Time::Now().LocalMidnight().LocalExplode(&exploded); 923 exploded.day_of_month = 1; 924 925 if (offset == 0) { 926 options->begin_time = base::Time::FromLocalExploded(exploded); 927 928 // Set the end time of this first search to null (which will 929 // show results from the future, should the user's clock have 930 // been set incorrectly). 931 options->end_time = base::Time(); 932 } else { 933 // Go back |offset| months in the past. The end time is not inclusive, so 934 // use the first day of the |offset| - 1 and |offset| months (e.g. for 935 // the last month, |offset| = 1, use the first days of the last month and 936 // the current month. 937 exploded.month -= offset - 1; 938 // Set the correct year. 939 normalizeMonths(&exploded); 940 options->end_time = base::Time::FromLocalExploded(exploded); 941 942 exploded.month -= 1; 943 // Set the correct year 944 normalizeMonths(&exploded); 945 options->begin_time = base::Time::FromLocalExploded(exploded); 946 } 947 } 948 949 // Helper function for Observe that determines if there are any differences 950 // between the URLs noticed for deletion and the ones we are expecting. 951 static bool DeletionsDiffer(const history::URLRows& deleted_rows, 952 const std::set<GURL>& urls_to_be_deleted) { 953 if (deleted_rows.size() != urls_to_be_deleted.size()) 954 return true; 955 for (history::URLRows::const_iterator i = deleted_rows.begin(); 956 i != deleted_rows.end(); ++i) { 957 if (urls_to_be_deleted.find(i->url()) == urls_to_be_deleted.end()) 958 return true; 959 } 960 return false; 961 } 962 963 void BrowsingHistoryHandler::Observe( 964 int type, 965 const content::NotificationSource& source, 966 const content::NotificationDetails& details) { 967 if (type != chrome::NOTIFICATION_HISTORY_URLS_DELETED) { 968 NOTREACHED(); 969 return; 970 } 971 history::URLsDeletedDetails* deletedDetails = 972 content::Details<history::URLsDeletedDetails>(details).ptr(); 973 if (deletedDetails->all_history || 974 DeletionsDiffer(deletedDetails->rows, urls_to_be_deleted_)) 975 web_ui()->CallJavascriptFunction("historyDeleted"); 976 } 977 978 std::string BrowsingHistoryHandler::GetAcceptLanguages() const { 979 Profile* profile = Profile::FromWebUI(web_ui()); 980 return profile->GetPrefs()->GetString(prefs::kAcceptLanguages); 981 } 982 983 //////////////////////////////////////////////////////////////////////////////// 984 // 985 // HistoryUI 986 // 987 //////////////////////////////////////////////////////////////////////////////// 988 989 HistoryUI::HistoryUI(content::WebUI* web_ui) : WebUIController(web_ui) { 990 web_ui->AddMessageHandler(new BrowsingHistoryHandler()); 991 web_ui->AddMessageHandler(new MetricsHandler()); 992 993 // On mobile we deal with foreign sessions differently. 994 #if !defined(OS_ANDROID) && !defined(OS_IOS) 995 if (chrome::IsInstantExtendedAPIEnabled()) { 996 web_ui->AddMessageHandler(new browser_sync::ForeignSessionHandler()); 997 web_ui->AddMessageHandler(new NTPLoginHandler()); 998 } 999 #endif 1000 1001 // Set up the chrome://history-frame/ source. 1002 Profile* profile = Profile::FromWebUI(web_ui); 1003 content::WebUIDataSource::Add(profile, CreateHistoryUIHTMLSource(profile)); 1004 } 1005 1006 // static 1007 base::RefCountedMemory* HistoryUI::GetFaviconResourceBytes( 1008 ui::ScaleFactor scale_factor) { 1009 return ResourceBundle::GetSharedInstance(). 1010 LoadDataResourceBytesForScale(IDR_HISTORY_FAVICON, scale_factor); 1011 } 1012