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