1 // Copyright 2015 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/downloads/downloads_list_tracker.h"
6 
7 #include <iterator>
8 #include <utility>
9 #include <vector>
10 
11 #include "base/bind.h"
12 #include "base/bind_helpers.h"
13 #include "base/i18n/rtl.h"
14 #include "base/i18n/unicodestring.h"
15 #include "base/strings/string16.h"
16 #include "base/strings/string_number_conversions.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "base/time/time.h"
19 #include "base/value_conversions.h"
20 #include "base/values.h"
21 #include "chrome/browser/download/download_crx_util.h"
22 #include "chrome/browser/download/download_item_model.h"
23 #include "chrome/browser/download/download_query.h"
24 #include "chrome/browser/extensions/api/downloads/downloads_api.h"
25 #include "chrome/browser/profiles/profile.h"
26 #include "chrome/browser/ui/webui/downloads/downloads.mojom.h"
27 #include "components/download/public/common/download_danger_type.h"
28 #include "components/download/public/common/download_item.h"
29 #include "content/public/browser/browser_context.h"
30 #include "content/public/browser/download_item_utils.h"
31 #include "content/public/browser/download_manager.h"
32 #include "extensions/browser/extension_registry.h"
33 #include "mojo/public/cpp/bindings/pending_remote.h"
34 #include "mojo/public/cpp/bindings/remote.h"
35 #include "net/base/filename_util.h"
36 #include "third_party/icu/source/i18n/unicode/datefmt.h"
37 #include "ui/base/l10n/time_format.h"
38 
39 using content::BrowserContext;
40 using download::DownloadItem;
41 using content::DownloadManager;
42 
43 using DownloadVector = DownloadManager::DownloadVector;
44 
45 namespace {
46 
47 // Returns a string constant to be used as the |danger_type| value in
48 // CreateDownloadData(). This can be the empty string, if the danger type is not
49 // relevant for the UI.
GetDangerTypeString(download::DownloadDangerType danger_type)50 const char* GetDangerTypeString(download::DownloadDangerType danger_type) {
51   switch (danger_type) {
52     case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE:
53       return "DANGEROUS_FILE";
54     case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL:
55       return "DANGEROUS_URL";
56     case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT:
57       return "DANGEROUS_CONTENT";
58     case download::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT:
59       return "UNCOMMON_CONTENT";
60     case download::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST:
61       return "DANGEROUS_HOST";
62     case download::DOWNLOAD_DANGER_TYPE_POTENTIALLY_UNWANTED:
63       return "POTENTIALLY_UNWANTED";
64     case download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING:
65       return "ASYNC_SCANNING";
66     case download::DOWNLOAD_DANGER_TYPE_BLOCKED_PASSWORD_PROTECTED:
67       return "BLOCKED_PASSWORD_PROTECTED";
68     case download::DOWNLOAD_DANGER_TYPE_BLOCKED_TOO_LARGE:
69       return "BLOCKED_TOO_LARGE";
70     case download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_WARNING:
71       return "SENSITIVE_CONTENT_WARNING";
72     case download::DOWNLOAD_DANGER_TYPE_SENSITIVE_CONTENT_BLOCK:
73       return "SENSITIVE_CONTENT_BLOCK";
74     case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_SAFE:
75       return "DEEP_SCANNED_SAFE";
76     case download::DOWNLOAD_DANGER_TYPE_DEEP_SCANNED_OPENED_DANGEROUS:
77       return "DEEP_SCANNED_OPENED_DANGEROUS";
78     case download::DOWNLOAD_DANGER_TYPE_BLOCKED_UNSUPPORTED_FILETYPE:
79       return "BLOCKED_UNSUPPORTED_FILE_TYPE";
80     case download::DOWNLOAD_DANGER_TYPE_PROMPT_FOR_SCANNING:
81     case download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS:
82     case download::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT:
83     case download::DOWNLOAD_DANGER_TYPE_USER_VALIDATED:
84     case download::DOWNLOAD_DANGER_TYPE_WHITELISTED_BY_POLICY:
85     case download::DOWNLOAD_DANGER_TYPE_MAX:
86       break;
87   }
88 
89   // Don't return a danger type string if it is NOT_DANGEROUS,
90   // MAYBE_DANGEROUS_CONTENT, or USER_VALIDATED, or WHITELISTED_BY_POLICY.
91   return "";
92 }
93 
94 // TODO(dbeam): if useful elsewhere, move to base/i18n/time_formatting.h?
TimeFormatLongDate(const base::Time & time)95 std::string TimeFormatLongDate(const base::Time& time) {
96   std::unique_ptr<icu::DateFormat> formatter(
97       icu::DateFormat::createDateInstance(icu::DateFormat::kLong));
98   icu::UnicodeString date_string;
99   formatter->format(static_cast<UDate>(time.ToDoubleT() * 1000), date_string);
100   return base::UTF16ToUTF8(base::i18n::UnicodeStringToString16(date_string));
101 }
102 
103 }  // namespace
104 
DownloadsListTracker(DownloadManager * download_manager,mojo::PendingRemote<downloads::mojom::Page> page)105 DownloadsListTracker::DownloadsListTracker(
106     DownloadManager* download_manager,
107     mojo::PendingRemote<downloads::mojom::Page> page)
108     : main_notifier_(download_manager, this),
109       page_(std::move(page)),
110       should_show_(base::BindRepeating(&DownloadsListTracker::ShouldShow,
111                                        base::Unretained(this))) {
112   Init();
113 }
114 
~DownloadsListTracker()115 DownloadsListTracker::~DownloadsListTracker() {}
116 
Reset()117 void DownloadsListTracker::Reset() {
118   if (sending_updates_)
119     page_->ClearAll();
120   sent_to_page_ = 0u;
121 }
122 
SetSearchTerms(const std::vector<std::string> & search_terms)123 bool DownloadsListTracker::SetSearchTerms(
124     const std::vector<std::string>& search_terms) {
125   std::vector<base::string16> new_terms;
126   new_terms.resize(search_terms.size());
127 
128   for (const auto& t : search_terms)
129     new_terms.push_back(base::UTF8ToUTF16(t));
130 
131   if (new_terms == search_terms_)
132     return false;
133 
134   search_terms_.swap(new_terms);
135   RebuildSortedItems();
136   return true;
137 }
138 
StartAndSendChunk()139 void DownloadsListTracker::StartAndSendChunk() {
140   sending_updates_ = true;
141 
142   CHECK_LE(sent_to_page_, sorted_items_.size());
143 
144   auto it = sorted_items_.begin();
145   std::advance(it, sent_to_page_);
146 
147   std::vector<downloads::mojom::DataPtr> list;
148   while (it != sorted_items_.end() && list.size() < chunk_size_) {
149     list.push_back(CreateDownloadData(*it));
150     ++it;
151   }
152 
153   size_t list_size = list.size();
154   page_->InsertItems(static_cast<int>(sent_to_page_), std::move(list));
155 
156   sent_to_page_ += list_size;
157 }
158 
Stop()159 void DownloadsListTracker::Stop() {
160   sending_updates_ = false;
161 }
162 
GetMainNotifierManager() const163 DownloadManager* DownloadsListTracker::GetMainNotifierManager() const {
164   return main_notifier_.GetManager();
165 }
166 
GetOriginalNotifierManager() const167 DownloadManager* DownloadsListTracker::GetOriginalNotifierManager() const {
168   return original_notifier_ ? original_notifier_->GetManager() : nullptr;
169 }
170 
OnDownloadCreated(DownloadManager * manager,DownloadItem * download_item)171 void DownloadsListTracker::OnDownloadCreated(DownloadManager* manager,
172                                              DownloadItem* download_item) {
173   DCHECK_EQ(0u, sorted_items_.count(download_item));
174   if (should_show_.Run(*download_item))
175     InsertItem(sorted_items_.insert(download_item).first);
176 }
177 
OnDownloadUpdated(DownloadManager * manager,DownloadItem * download_item)178 void DownloadsListTracker::OnDownloadUpdated(DownloadManager* manager,
179                                              DownloadItem* download_item) {
180   auto current_position = sorted_items_.find(download_item);
181   bool is_showing = current_position != sorted_items_.end();
182   bool should_show = should_show_.Run(*download_item);
183 
184   if (!is_showing && should_show)
185     InsertItem(sorted_items_.insert(download_item).first);
186   else if (is_showing && !should_show)
187     RemoveItem(current_position);
188   else if (is_showing)
189     UpdateItem(current_position);
190 }
191 
OnDownloadRemoved(DownloadManager * manager,DownloadItem * download_item)192 void DownloadsListTracker::OnDownloadRemoved(DownloadManager* manager,
193                                              DownloadItem* download_item) {
194   auto current_position = sorted_items_.find(download_item);
195   if (current_position != sorted_items_.end())
196     RemoveItem(current_position);
197 }
198 
DownloadsListTracker(DownloadManager * download_manager,mojo::PendingRemote<downloads::mojom::Page> page,base::Callback<bool (const DownloadItem &)> should_show)199 DownloadsListTracker::DownloadsListTracker(
200     DownloadManager* download_manager,
201     mojo::PendingRemote<downloads::mojom::Page> page,
202     base::Callback<bool(const DownloadItem&)> should_show)
203     : main_notifier_(download_manager, this),
204       page_(std::move(page)),
205       should_show_(should_show) {
206   DCHECK(page_);
207   Init();
208 }
209 
CreateDownloadData(download::DownloadItem * download_item) const210 downloads::mojom::DataPtr DownloadsListTracker::CreateDownloadData(
211     download::DownloadItem* download_item) const {
212   // TODO(asanka): Move towards using download_model here for getting status and
213   // progress. The difference currently only matters to Drive downloads and
214   // those don't show up on the downloads page, but should.
215   DownloadItemModel download_model(download_item);
216 
217   auto file_value = downloads::mojom::Data::New();
218 
219   file_value->started =
220       static_cast<int>(download_item->GetStartTime().ToTimeT());
221   file_value->since_string = base::UTF16ToUTF8(
222       ui::TimeFormat::RelativeDate(download_item->GetStartTime(), NULL));
223   file_value->date_string = TimeFormatLongDate(download_item->GetStartTime());
224 
225   file_value->id = base::NumberToString(download_item->GetId());
226 
227   base::FilePath download_path(download_item->GetTargetFilePath());
228   file_value->file_path = download_path.AsUTF8Unsafe();
229   file_value->file_url = net::FilePathToFileURL(download_path).spec();
230 
231   extensions::DownloadedByExtension* by_ext =
232       extensions::DownloadedByExtension::Get(download_item);
233   std::string by_ext_id;
234   std::string by_ext_name;
235   if (by_ext) {
236     by_ext_id = by_ext->id();
237     // TODO(dbeam): why doesn't DownloadsByExtension::name() return a string16?
238     by_ext_name = by_ext->name();
239 
240     // Lookup the extension's current name() in case the user changed their
241     // language. This won't work if the extension was uninstalled, so the name
242     // might be the wrong language.
243     auto* profile = Profile::FromBrowserContext(
244         content::DownloadItemUtils::GetBrowserContext(download_item));
245     auto* registry = extensions::ExtensionRegistry::Get(profile);
246     const extensions::Extension* extension = registry->GetExtensionById(
247         by_ext->id(), extensions::ExtensionRegistry::EVERYTHING);
248     if (extension)
249       by_ext_name = extension->name();
250   }
251   file_value->by_ext_id = by_ext_id;
252   file_value->by_ext_name = by_ext_name;
253 
254   // Keep file names as LTR. TODO(dbeam): why?
255   base::string16 file_name =
256       download_item->GetFileNameToReportUser().LossyDisplayName();
257   file_name = base::i18n::GetDisplayStringInLTRDirectionality(file_name);
258   file_value->file_name = base::UTF16ToUTF8(file_name);
259   file_value->url = download_item->GetURL().spec();
260   file_value->total = static_cast<int>(download_item->GetTotalBytes());
261   file_value->file_externally_removed =
262       download_item->GetFileExternallyRemoved();
263   file_value->resume = download_item->CanResume();
264   file_value->otr = IsIncognito(*download_item);
265 
266   const char* danger_type = GetDangerTypeString(download_item->GetDangerType());
267   base::string16 last_reason_text;
268   // -2 is invalid, -1 means indeterminate, and 0-100 are in-progress.
269   int percent = -2;
270   base::string16 progress_status_text;
271   bool retry = false;
272   const char* state = nullptr;
273 
274   switch (download_item->GetState()) {
275     case download::DownloadItem::IN_PROGRESS: {
276       if (download_item->IsDangerous()) {
277         state = "DANGEROUS";
278       } else if (download_item->IsMixedContent()) {
279         state = "MIXED_CONTENT";
280       } else if (download_item->GetDangerType() ==
281                  download::DOWNLOAD_DANGER_TYPE_ASYNC_SCANNING) {
282         state = "ASYNC_SCANNING";
283       } else if (download_item->IsPaused()) {
284         state = "PAUSED";
285       } else {
286         state = "IN_PROGRESS";
287       }
288       progress_status_text = download_model.GetTabProgressStatusText();
289       percent = download_item->PercentComplete();
290       break;
291     }
292 
293     case download::DownloadItem::INTERRUPTED:
294       state = "INTERRUPTED";
295       progress_status_text = download_model.GetTabProgressStatusText();
296 
297       if (download_item->CanResume())
298         percent = download_item->PercentComplete();
299 
300       // TODO(asanka): last_reason_text should be set via
301       // download_model.GetInterruptReasonText(). But we are using
302       // GetStatusText() as a temporary measure until the layout is fixed to
303       // accommodate the longer string. http://crbug.com/609255
304       last_reason_text = download_model.GetStatusText();
305       if (download::DOWNLOAD_INTERRUPT_REASON_CRASH ==
306               download_item->GetLastReason() &&
307           !download_item->CanResume()) {
308         retry = true;
309       }
310       break;
311 
312     case download::DownloadItem::CANCELLED:
313       state = "CANCELLED";
314       retry = true;
315       break;
316 
317     case download::DownloadItem::COMPLETE:
318       DCHECK(!download_item->IsDangerous());
319       state = "COMPLETE";
320       break;
321 
322     case download::DownloadItem::MAX_DOWNLOAD_STATE:
323       NOTREACHED();
324   }
325 
326   DCHECK(state);
327 
328   file_value->danger_type = danger_type;
329   file_value->last_reason_text = base::UTF16ToUTF8(last_reason_text);
330   file_value->percent = percent;
331   file_value->progress_status_text = base::UTF16ToUTF8(progress_status_text);
332   file_value->retry = retry;
333   file_value->state = state;
334 
335   return file_value;
336 }
337 
IsIncognito(const DownloadItem & item) const338 bool DownloadsListTracker::IsIncognito(const DownloadItem& item) const {
339   return GetOriginalNotifierManager() && GetMainNotifierManager() &&
340       GetMainNotifierManager()->GetDownload(item.GetId()) == &item;
341 }
342 
GetItemForTesting(size_t index) const343 const DownloadItem* DownloadsListTracker::GetItemForTesting(size_t index)
344     const {
345   if (index >= sorted_items_.size())
346     return nullptr;
347 
348   auto it = sorted_items_.begin();
349   std::advance(it, index);
350   return *it;
351 }
352 
SetChunkSizeForTesting(size_t chunk_size)353 void DownloadsListTracker::SetChunkSizeForTesting(size_t chunk_size) {
354   CHECK_EQ(0u, sent_to_page_);
355   chunk_size_ = chunk_size;
356 }
357 
ShouldShow(const DownloadItem & item) const358 bool DownloadsListTracker::ShouldShow(const DownloadItem& item) const {
359   return !download_crx_util::IsTrustedExtensionDownload(
360              Profile::FromBrowserContext(
361                  GetMainNotifierManager()->GetBrowserContext()),
362              item) &&
363          !item.IsTemporary() && !item.IsTransient() &&
364          !item.GetFileNameToReportUser().empty() &&
365          !item.GetTargetFilePath().empty() && !item.GetURL().is_empty() &&
366          DownloadItemModel(const_cast<DownloadItem*>(&item))
367              .ShouldShowInShelf() &&
368          DownloadQuery::MatchesQuery(search_terms_, item);
369 }
370 
operator ()(const download::DownloadItem * a,const download::DownloadItem * b) const371 bool DownloadsListTracker::StartTimeComparator::operator()(
372     const download::DownloadItem* a,
373     const download::DownloadItem* b) const {
374   return a->GetStartTime() > b->GetStartTime();
375 }
376 
Init()377 void DownloadsListTracker::Init() {
378   Profile* profile = Profile::FromBrowserContext(
379       GetMainNotifierManager()->GetBrowserContext());
380   if (profile->IsOffTheRecord()) {
381     Profile* original_profile = profile->GetOriginalProfile();
382     original_notifier_ = std::make_unique<download::AllDownloadItemNotifier>(
383         BrowserContext::GetDownloadManager(original_profile), this);
384   }
385 
386   RebuildSortedItems();
387 }
388 
RebuildSortedItems()389 void DownloadsListTracker::RebuildSortedItems() {
390   DownloadVector all_items, visible_items;
391 
392   GetMainNotifierManager()->GetAllDownloads(&all_items);
393 
394   if (GetOriginalNotifierManager())
395     GetOriginalNotifierManager()->GetAllDownloads(&all_items);
396 
397   DownloadQuery query;
398   query.AddFilter(should_show_);
399   query.Search(all_items.begin(), all_items.end(), &visible_items);
400 
401   SortedSet sorted_items(visible_items.begin(), visible_items.end());
402   sorted_items_.swap(sorted_items);
403 }
404 
InsertItem(const SortedSet::iterator & insert)405 void DownloadsListTracker::InsertItem(const SortedSet::iterator& insert) {
406   if (!sending_updates_)
407     return;
408 
409   size_t index = GetIndex(insert);
410   if (index >= chunk_size_ && index >= sent_to_page_)
411     return;
412 
413   std::vector<downloads::mojom::DataPtr> list;
414   list.push_back(CreateDownloadData(*insert));
415 
416   page_->InsertItems(static_cast<int>(index), std::move(list));
417 
418   sent_to_page_++;
419 }
420 
UpdateItem(const SortedSet::iterator & update)421 void DownloadsListTracker::UpdateItem(const SortedSet::iterator& update) {
422   if (!sending_updates_ || GetIndex(update) >= sent_to_page_)
423     return;
424 
425   page_->UpdateItem(static_cast<int>(GetIndex(update)),
426                     CreateDownloadData(*update));
427 }
428 
GetIndex(const SortedSet::iterator & item) const429 size_t DownloadsListTracker::GetIndex(const SortedSet::iterator& item) const {
430   // TODO(dbeam): this could be log(N) if |item| was random access.
431   return std::distance(sorted_items_.begin(), item);
432 }
433 
RemoveItem(const SortedSet::iterator & remove)434 void DownloadsListTracker::RemoveItem(const SortedSet::iterator& remove) {
435   if (sending_updates_) {
436     size_t index = GetIndex(remove);
437 
438     if (index < sent_to_page_) {
439       page_->RemoveItem(static_cast<int>(index));
440       sent_to_page_--;
441     }
442   }
443   sorted_items_.erase(remove);
444 }
445