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 // DownloadHistory manages persisting DownloadItems to the history service by 6 // observing a single DownloadManager and all its DownloadItems using an 7 // AllDownloadItemNotifier. 8 // 9 // DownloadHistory decides whether and when to add items to, remove items from, 10 // and update items in the database. DownloadHistory uses DownloadHistoryData to 11 // store per-DownloadItem data such as whether the item is persisted or being 12 // persisted, and the last history::DownloadRow that was passed to the database. 13 // When the DownloadManager and its delegate (ChromeDownloadManagerDelegate) are 14 // initialized, DownloadHistory is created and queries the HistoryService. When 15 // the HistoryService calls back from QueryDownloads() to QueryCallback(), 16 // DownloadHistory uses DownloadManager::CreateDownloadItem() to inform 17 // DownloadManager of these persisted DownloadItems. CreateDownloadItem() 18 // internally calls OnDownloadCreated(), which normally adds items to the 19 // database, so QueryCallback() uses |loading_id_| to disable adding these items 20 // to the database. If a download is removed via OnDownloadRemoved() while the 21 // item is still being added to the database, DownloadHistory uses 22 // |removed_while_adding_| to remember to remove the item when its ItemAdded() 23 // callback is called. All callbacks are bound with a weak pointer to 24 // DownloadHistory to prevent use-after-free bugs. 25 // ChromeDownloadManagerDelegate owns DownloadHistory, and deletes it in 26 // Shutdown(), which is called by DownloadManagerImpl::Shutdown() after all 27 // DownloadItems are destroyed. 28 29 #include "chrome/browser/download/download_history.h" 30 31 #include "base/metrics/histogram.h" 32 #include "chrome/browser/download/download_crx_util.h" 33 #include "chrome/browser/history/download_database.h" 34 #include "chrome/browser/history/download_row.h" 35 #include "chrome/browser/history/history_service.h" 36 #include "content/public/browser/browser_thread.h" 37 #include "content/public/browser/download_item.h" 38 #include "content/public/browser/download_manager.h" 39 40 #if defined(ENABLE_EXTENSIONS) 41 #include "chrome/browser/extensions/api/downloads/downloads_api.h" 42 #endif 43 44 namespace { 45 46 // Per-DownloadItem data. This information does not belong inside DownloadItem, 47 // and keeping maps in DownloadHistory from DownloadItem to this information is 48 // error-prone and complicated. Unfortunately, DownloadHistory::removing_*_ and 49 // removed_while_adding_ cannot be moved into this class partly because 50 // DownloadHistoryData is destroyed when DownloadItems are destroyed, and we 51 // have no control over when DownloadItems are destroyed. 52 class DownloadHistoryData : public base::SupportsUserData::Data { 53 public: 54 enum PersistenceState { 55 NOT_PERSISTED, 56 PERSISTING, 57 PERSISTED, 58 }; 59 60 static DownloadHistoryData* Get(content::DownloadItem* item) { 61 base::SupportsUserData::Data* data = item->GetUserData(kKey); 62 return (data == NULL) ? NULL : 63 static_cast<DownloadHistoryData*>(data); 64 } 65 66 static const DownloadHistoryData* Get(const content::DownloadItem* item) { 67 const base::SupportsUserData::Data* data = item->GetUserData(kKey); 68 return (data == NULL) ? NULL 69 : static_cast<const DownloadHistoryData*>(data); 70 } 71 72 explicit DownloadHistoryData(content::DownloadItem* item) 73 : state_(NOT_PERSISTED), 74 was_restored_from_history_(false) { 75 item->SetUserData(kKey, this); 76 } 77 78 virtual ~DownloadHistoryData() { 79 } 80 81 PersistenceState state() const { return state_; } 82 void SetState(PersistenceState s) { state_ = s; } 83 84 bool was_restored_from_history() const { return was_restored_from_history_; } 85 void set_was_restored_from_history(bool value) { 86 was_restored_from_history_ = value; 87 } 88 89 // This allows DownloadHistory::OnDownloadUpdated() to see what changed in a 90 // DownloadItem if anything, in order to prevent writing to the database 91 // unnecessarily. It is nullified when the item is no longer in progress in 92 // order to save memory. 93 history::DownloadRow* info() { return info_.get(); } 94 void set_info(const history::DownloadRow& i) { 95 info_.reset(new history::DownloadRow(i)); 96 } 97 void clear_info() { 98 info_.reset(); 99 } 100 101 private: 102 static const char kKey[]; 103 104 PersistenceState state_; 105 scoped_ptr<history::DownloadRow> info_; 106 bool was_restored_from_history_; 107 108 DISALLOW_COPY_AND_ASSIGN(DownloadHistoryData); 109 }; 110 111 const char DownloadHistoryData::kKey[] = 112 "DownloadItem DownloadHistoryData"; 113 114 history::DownloadRow GetDownloadRow( 115 content::DownloadItem* item) { 116 std::string by_ext_id, by_ext_name; 117 #if defined(ENABLE_EXTENSIONS) 118 extensions::DownloadedByExtension* by_ext = 119 extensions::DownloadedByExtension::Get(item); 120 if (by_ext) { 121 by_ext_id = by_ext->id(); 122 by_ext_name = by_ext->name(); 123 } 124 #endif 125 126 return history::DownloadRow( 127 item->GetFullPath(), 128 item->GetTargetFilePath(), 129 item->GetUrlChain(), 130 item->GetReferrerUrl(), 131 item->GetMimeType(), 132 item->GetOriginalMimeType(), 133 item->GetStartTime(), 134 item->GetEndTime(), 135 item->GetETag(), 136 item->GetLastModifiedTime(), 137 item->GetReceivedBytes(), 138 item->GetTotalBytes(), 139 item->GetState(), 140 item->GetDangerType(), 141 item->GetLastReason(), 142 item->GetId(), 143 item->GetOpened(), 144 by_ext_id, 145 by_ext_name); 146 } 147 148 bool ShouldUpdateHistory(const history::DownloadRow* previous, 149 const history::DownloadRow& current) { 150 // Ignore url, referrer, mime_type, original_mime_type, start_time, 151 // id, db_handle, which don't change. 152 return ((previous == NULL) || 153 (previous->current_path != current.current_path) || 154 (previous->target_path != current.target_path) || 155 (previous->end_time != current.end_time) || 156 (previous->received_bytes != current.received_bytes) || 157 (previous->total_bytes != current.total_bytes) || 158 (previous->etag != current.etag) || 159 (previous->last_modified != current.last_modified) || 160 (previous->state != current.state) || 161 (previous->danger_type != current.danger_type) || 162 (previous->interrupt_reason != current.interrupt_reason) || 163 (previous->opened != current.opened) || 164 (previous->by_ext_id != current.by_ext_id) || 165 (previous->by_ext_name != current.by_ext_name)); 166 } 167 168 typedef std::vector<history::DownloadRow> InfoVector; 169 170 } // anonymous namespace 171 172 DownloadHistory::HistoryAdapter::HistoryAdapter(HistoryService* history) 173 : history_(history) { 174 } 175 DownloadHistory::HistoryAdapter::~HistoryAdapter() {} 176 177 void DownloadHistory::HistoryAdapter::QueryDownloads( 178 const HistoryService::DownloadQueryCallback& callback) { 179 history_->QueryDownloads(callback); 180 } 181 182 void DownloadHistory::HistoryAdapter::CreateDownload( 183 const history::DownloadRow& info, 184 const HistoryService::DownloadCreateCallback& callback) { 185 history_->CreateDownload(info, callback); 186 } 187 188 void DownloadHistory::HistoryAdapter::UpdateDownload( 189 const history::DownloadRow& data) { 190 history_->UpdateDownload(data); 191 } 192 193 void DownloadHistory::HistoryAdapter::RemoveDownloads( 194 const std::set<uint32>& ids) { 195 history_->RemoveDownloads(ids); 196 } 197 198 DownloadHistory::Observer::Observer() {} 199 DownloadHistory::Observer::~Observer() {} 200 201 // static 202 bool DownloadHistory::IsPersisted(const content::DownloadItem* item) { 203 const DownloadHistoryData* data = DownloadHistoryData::Get(item); 204 return data && (data->state() == DownloadHistoryData::PERSISTED); 205 } 206 207 DownloadHistory::DownloadHistory(content::DownloadManager* manager, 208 scoped_ptr<HistoryAdapter> history) 209 : notifier_(manager, this), 210 history_(history.Pass()), 211 loading_id_(content::DownloadItem::kInvalidId), 212 history_size_(0), 213 weak_ptr_factory_(this) { 214 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 215 content::DownloadManager::DownloadVector items; 216 notifier_.GetManager()->GetAllDownloads(&items); 217 for (content::DownloadManager::DownloadVector::const_iterator 218 it = items.begin(); it != items.end(); ++it) { 219 OnDownloadCreated(notifier_.GetManager(), *it); 220 } 221 history_->QueryDownloads(base::Bind( 222 &DownloadHistory::QueryCallback, weak_ptr_factory_.GetWeakPtr())); 223 } 224 225 DownloadHistory::~DownloadHistory() { 226 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 227 FOR_EACH_OBSERVER(Observer, observers_, OnDownloadHistoryDestroyed()); 228 observers_.Clear(); 229 } 230 231 void DownloadHistory::AddObserver(DownloadHistory::Observer* observer) { 232 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 233 observers_.AddObserver(observer); 234 } 235 236 void DownloadHistory::RemoveObserver(DownloadHistory::Observer* observer) { 237 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 238 observers_.RemoveObserver(observer); 239 } 240 241 bool DownloadHistory::WasRestoredFromHistory( 242 const content::DownloadItem* download) const { 243 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 244 const DownloadHistoryData* data = DownloadHistoryData::Get(download); 245 246 // The OnDownloadCreated handler sets the was_restored_from_history flag when 247 // resetting the loading_id_. So one of the two conditions below will hold for 248 // a download restored from history even if the caller of this method is 249 // racing with our OnDownloadCreated handler. 250 return (data && data->was_restored_from_history()) || 251 download->GetId() == loading_id_; 252 } 253 254 void DownloadHistory::QueryCallback(scoped_ptr<InfoVector> infos) { 255 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 256 // ManagerGoingDown() may have happened before the history loaded. 257 if (!notifier_.GetManager()) 258 return; 259 for (InfoVector::const_iterator it = infos->begin(); 260 it != infos->end(); ++it) { 261 loading_id_ = it->id; 262 content::DownloadItem* item = notifier_.GetManager()->CreateDownloadItem( 263 loading_id_, 264 it->current_path, 265 it->target_path, 266 it->url_chain, 267 it->referrer_url, 268 it->mime_type, 269 it->original_mime_type, 270 it->start_time, 271 it->end_time, 272 it->etag, 273 it->last_modified, 274 it->received_bytes, 275 it->total_bytes, 276 it->state, 277 it->danger_type, 278 it->interrupt_reason, 279 it->opened); 280 #if defined(ENABLE_EXTENSIONS) 281 if (!it->by_ext_id.empty() && !it->by_ext_name.empty()) { 282 new extensions::DownloadedByExtension( 283 item, it->by_ext_id, it->by_ext_name); 284 item->UpdateObservers(); 285 } 286 #endif 287 DCHECK_EQ(DownloadHistoryData::PERSISTED, 288 DownloadHistoryData::Get(item)->state()); 289 ++history_size_; 290 } 291 notifier_.GetManager()->CheckForHistoryFilesRemoval(); 292 } 293 294 void DownloadHistory::MaybeAddToHistory(content::DownloadItem* item) { 295 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 296 297 uint32 download_id = item->GetId(); 298 DownloadHistoryData* data = DownloadHistoryData::Get(item); 299 bool removing = removing_ids_.find(download_id) != removing_ids_.end(); 300 301 // TODO(benjhayden): Remove IsTemporary(). 302 if (download_crx_util::IsExtensionDownload(*item) || 303 item->IsTemporary() || 304 (data->state() != DownloadHistoryData::NOT_PERSISTED) || 305 removing) 306 return; 307 308 data->SetState(DownloadHistoryData::PERSISTING); 309 if (data->info() == NULL) { 310 // Keep the info here regardless of whether the item is in progress so that, 311 // when ItemAdded() calls OnDownloadUpdated(), it can decide whether to 312 // Update the db and/or clear the info. 313 data->set_info(GetDownloadRow(item)); 314 } 315 316 history_->CreateDownload(*data->info(), base::Bind( 317 &DownloadHistory::ItemAdded, weak_ptr_factory_.GetWeakPtr(), 318 download_id)); 319 FOR_EACH_OBSERVER(Observer, observers_, OnDownloadStored( 320 item, *data->info())); 321 } 322 323 void DownloadHistory::ItemAdded(uint32 download_id, bool success) { 324 if (removed_while_adding_.find(download_id) != 325 removed_while_adding_.end()) { 326 removed_while_adding_.erase(download_id); 327 if (success) 328 ScheduleRemoveDownload(download_id); 329 return; 330 } 331 332 if (!notifier_.GetManager()) 333 return; 334 335 content::DownloadItem* item = notifier_.GetManager()->GetDownload( 336 download_id); 337 if (!item) { 338 // This item will have called OnDownloadDestroyed(). If the item should 339 // have been removed from history, then it would have also called 340 // OnDownloadRemoved(), which would have put |download_id| in 341 // removed_while_adding_, handled above. 342 return; 343 } 344 345 DownloadHistoryData* data = DownloadHistoryData::Get(item); 346 347 // The sql INSERT statement failed. Avoid an infinite loop: don't 348 // automatically retry. Retry adding the next time the item is updated by 349 // resetting the state to NOT_PERSISTED. 350 if (!success) { 351 DVLOG(20) << __FUNCTION__ << " INSERT failed id=" << download_id; 352 data->SetState(DownloadHistoryData::NOT_PERSISTED); 353 return; 354 } 355 data->SetState(DownloadHistoryData::PERSISTED); 356 357 UMA_HISTOGRAM_CUSTOM_COUNTS("Download.HistorySize2", 358 history_size_, 359 0/*min*/, 360 (1 << 23)/*max*/, 361 (1 << 7)/*num_buckets*/); 362 ++history_size_; 363 364 // In case the item changed or became temporary while it was being added. 365 // Don't just update all of the item's observers because we're the only 366 // observer that can also see data->state(), which is the only thing that 367 // ItemAdded() changed. 368 OnDownloadUpdated(notifier_.GetManager(), item); 369 } 370 371 void DownloadHistory::OnDownloadCreated( 372 content::DownloadManager* manager, content::DownloadItem* item) { 373 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 374 375 // All downloads should pass through OnDownloadCreated exactly once. 376 CHECK(!DownloadHistoryData::Get(item)); 377 DownloadHistoryData* data = new DownloadHistoryData(item); 378 if (item->GetId() == loading_id_) { 379 data->SetState(DownloadHistoryData::PERSISTED); 380 data->set_was_restored_from_history(true); 381 loading_id_ = content::DownloadItem::kInvalidId; 382 } 383 if (item->GetState() == content::DownloadItem::IN_PROGRESS) { 384 data->set_info(GetDownloadRow(item)); 385 } 386 MaybeAddToHistory(item); 387 } 388 389 void DownloadHistory::OnDownloadUpdated( 390 content::DownloadManager* manager, content::DownloadItem* item) { 391 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 392 393 DownloadHistoryData* data = DownloadHistoryData::Get(item); 394 if (data->state() == DownloadHistoryData::NOT_PERSISTED) { 395 MaybeAddToHistory(item); 396 return; 397 } 398 if (item->IsTemporary()) { 399 OnDownloadRemoved(notifier_.GetManager(), item); 400 return; 401 } 402 403 history::DownloadRow current_info(GetDownloadRow(item)); 404 bool should_update = ShouldUpdateHistory(data->info(), current_info); 405 UMA_HISTOGRAM_ENUMERATION("Download.HistoryPropagatedUpdate", 406 should_update, 2); 407 if (should_update) { 408 history_->UpdateDownload(current_info); 409 FOR_EACH_OBSERVER(Observer, observers_, OnDownloadStored( 410 item, current_info)); 411 } 412 if (item->GetState() == content::DownloadItem::IN_PROGRESS) { 413 data->set_info(current_info); 414 } else { 415 data->clear_info(); 416 } 417 } 418 419 void DownloadHistory::OnDownloadOpened( 420 content::DownloadManager* manager, content::DownloadItem* item) { 421 OnDownloadUpdated(manager, item); 422 } 423 424 void DownloadHistory::OnDownloadRemoved( 425 content::DownloadManager* manager, content::DownloadItem* item) { 426 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 427 428 DownloadHistoryData* data = DownloadHistoryData::Get(item); 429 if (data->state() != DownloadHistoryData::PERSISTED) { 430 if (data->state() == DownloadHistoryData::PERSISTING) { 431 // ScheduleRemoveDownload will be called when history_ calls ItemAdded(). 432 removed_while_adding_.insert(item->GetId()); 433 } 434 return; 435 } 436 ScheduleRemoveDownload(item->GetId()); 437 // This is important: another OnDownloadRemoved() handler could do something 438 // that synchronously fires an OnDownloadUpdated(). 439 data->SetState(DownloadHistoryData::NOT_PERSISTED); 440 // ItemAdded increments history_size_ only if the item wasn't 441 // removed_while_adding_, so the next line does not belong in 442 // ScheduleRemoveDownload(). 443 --history_size_; 444 } 445 446 void DownloadHistory::ScheduleRemoveDownload(uint32 download_id) { 447 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 448 449 // For database efficiency, batch removals together if they happen all at 450 // once. 451 if (removing_ids_.empty()) { 452 content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, 453 base::Bind(&DownloadHistory::RemoveDownloadsBatch, 454 weak_ptr_factory_.GetWeakPtr())); 455 } 456 removing_ids_.insert(download_id); 457 } 458 459 void DownloadHistory::RemoveDownloadsBatch() { 460 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 461 IdSet remove_ids; 462 removing_ids_.swap(remove_ids); 463 history_->RemoveDownloads(remove_ids); 464 FOR_EACH_OBSERVER(Observer, observers_, OnDownloadsRemoved(remove_ids)); 465 } 466