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