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