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