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