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
Get(content::DownloadItem * item)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
Get(const content::DownloadItem * item)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
DownloadHistoryData(content::DownloadItem * item)72 explicit DownloadHistoryData(content::DownloadItem* item)
73 : state_(NOT_PERSISTED),
74 was_restored_from_history_(false) {
75 item->SetUserData(kKey, this);
76 }
77
~DownloadHistoryData()78 virtual ~DownloadHistoryData() {
79 }
80
state() const81 PersistenceState state() const { return state_; }
SetState(PersistenceState s)82 void SetState(PersistenceState s) { state_ = s; }
83
was_restored_from_history() const84 bool was_restored_from_history() const { return was_restored_from_history_; }
set_was_restored_from_history(bool value)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.
info()93 history::DownloadRow* info() { return info_.get(); }
set_info(const history::DownloadRow & i)94 void set_info(const history::DownloadRow& i) {
95 info_.reset(new history::DownloadRow(i));
96 }
clear_info()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
GetDownloadRow(content::DownloadItem * item)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
ShouldUpdateHistory(const history::DownloadRow * previous,const history::DownloadRow & current)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
HistoryAdapter(HistoryService * history)172 DownloadHistory::HistoryAdapter::HistoryAdapter(HistoryService* history)
173 : history_(history) {
174 }
~HistoryAdapter()175 DownloadHistory::HistoryAdapter::~HistoryAdapter() {}
176
QueryDownloads(const HistoryService::DownloadQueryCallback & callback)177 void DownloadHistory::HistoryAdapter::QueryDownloads(
178 const HistoryService::DownloadQueryCallback& callback) {
179 history_->QueryDownloads(callback);
180 }
181
CreateDownload(const history::DownloadRow & info,const HistoryService::DownloadCreateCallback & callback)182 void DownloadHistory::HistoryAdapter::CreateDownload(
183 const history::DownloadRow& info,
184 const HistoryService::DownloadCreateCallback& callback) {
185 history_->CreateDownload(info, callback);
186 }
187
UpdateDownload(const history::DownloadRow & data)188 void DownloadHistory::HistoryAdapter::UpdateDownload(
189 const history::DownloadRow& data) {
190 history_->UpdateDownload(data);
191 }
192
RemoveDownloads(const std::set<uint32> & ids)193 void DownloadHistory::HistoryAdapter::RemoveDownloads(
194 const std::set<uint32>& ids) {
195 history_->RemoveDownloads(ids);
196 }
197
Observer()198 DownloadHistory::Observer::Observer() {}
~Observer()199 DownloadHistory::Observer::~Observer() {}
200
201 // static
IsPersisted(const content::DownloadItem * item)202 bool DownloadHistory::IsPersisted(const content::DownloadItem* item) {
203 const DownloadHistoryData* data = DownloadHistoryData::Get(item);
204 return data && (data->state() == DownloadHistoryData::PERSISTED);
205 }
206
DownloadHistory(content::DownloadManager * manager,scoped_ptr<HistoryAdapter> history)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
~DownloadHistory()225 DownloadHistory::~DownloadHistory() {
226 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
227 FOR_EACH_OBSERVER(Observer, observers_, OnDownloadHistoryDestroyed());
228 observers_.Clear();
229 }
230
AddObserver(DownloadHistory::Observer * observer)231 void DownloadHistory::AddObserver(DownloadHistory::Observer* observer) {
232 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
233 observers_.AddObserver(observer);
234 }
235
RemoveObserver(DownloadHistory::Observer * observer)236 void DownloadHistory::RemoveObserver(DownloadHistory::Observer* observer) {
237 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
238 observers_.RemoveObserver(observer);
239 }
240
WasRestoredFromHistory(const content::DownloadItem * download) const241 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
QueryCallback(scoped_ptr<InfoVector> infos)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
MaybeAddToHistory(content::DownloadItem * item)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
ItemAdded(uint32 download_id,bool success)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
OnDownloadCreated(content::DownloadManager * manager,content::DownloadItem * item)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
OnDownloadUpdated(content::DownloadManager * manager,content::DownloadItem * item)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
OnDownloadOpened(content::DownloadManager * manager,content::DownloadItem * item)419 void DownloadHistory::OnDownloadOpened(
420 content::DownloadManager* manager, content::DownloadItem* item) {
421 OnDownloadUpdated(manager, item);
422 }
423
OnDownloadRemoved(content::DownloadManager * manager,content::DownloadItem * item)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
ScheduleRemoveDownload(uint32 download_id)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
RemoveDownloadsBatch()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