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 #include "libcef/browser/download_manager_delegate.h"
6
7 #include <tuple>
8
9 #include "include/cef_download_handler.h"
10 #include "libcef/browser/alloy/alloy_browser_host_impl.h"
11 #include "libcef/browser/context.h"
12 #include "libcef/browser/download_item_impl.h"
13 #include "libcef/browser/thread_util.h"
14
15 #include "base/bind.h"
16 #include "base/files/file_util.h"
17 #include "base/logging.h"
18 #include "base/path_service.h"
19 #include "base/strings/string_util.h"
20 #include "base/strings/utf_string_conversions.h"
21 #include "chrome/common/chrome_constants.h"
22 #include "content/public/browser/browser_context.h"
23 #include "content/public/browser/download_item_utils.h"
24 #include "content/public/browser/web_contents.h"
25 #include "net/base/filename_util.h"
26 #include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h"
27
28 using content::DownloadManager;
29 using content::WebContents;
30 using download::DownloadItem;
31
32 namespace {
33
34 // Helper function to retrieve the CefDownloadHandler.
GetDownloadHandler(CefRefPtr<AlloyBrowserHostImpl> browser)35 CefRefPtr<CefDownloadHandler> GetDownloadHandler(
36 CefRefPtr<AlloyBrowserHostImpl> browser) {
37 CefRefPtr<CefClient> client = browser->GetClient();
38 if (client.get())
39 return client->GetDownloadHandler();
40 return nullptr;
41 }
42
43 // CefBeforeDownloadCallback implementation.
44 class CefBeforeDownloadCallbackImpl : public CefBeforeDownloadCallback {
45 public:
CefBeforeDownloadCallbackImpl(const base::WeakPtr<DownloadManager> & manager,uint32 download_id,const base::FilePath & suggested_name,content::DownloadTargetCallback callback)46 CefBeforeDownloadCallbackImpl(const base::WeakPtr<DownloadManager>& manager,
47 uint32 download_id,
48 const base::FilePath& suggested_name,
49 content::DownloadTargetCallback callback)
50 : manager_(manager),
51 download_id_(download_id),
52 suggested_name_(suggested_name),
53 callback_(std::move(callback)) {}
54
55 CefBeforeDownloadCallbackImpl(const CefBeforeDownloadCallbackImpl&) = delete;
56 CefBeforeDownloadCallbackImpl& operator=(
57 const CefBeforeDownloadCallbackImpl&) = delete;
58
Continue(const CefString & download_path,bool show_dialog)59 void Continue(const CefString& download_path, bool show_dialog) override {
60 if (CEF_CURRENTLY_ON_UIT()) {
61 if (download_id_ <= 0)
62 return;
63
64 if (manager_) {
65 base::FilePath path = base::FilePath(download_path);
66 CEF_POST_USER_VISIBLE_TASK(
67 base::BindOnce(&CefBeforeDownloadCallbackImpl::GenerateFilename,
68 manager_, download_id_, suggested_name_, path,
69 show_dialog, std::move(callback_)));
70 }
71
72 download_id_ = 0;
73 } else {
74 CEF_POST_TASK(CEF_UIT,
75 base::BindOnce(&CefBeforeDownloadCallbackImpl::Continue,
76 this, download_path, show_dialog));
77 }
78 }
79
80 private:
GenerateFilename(base::WeakPtr<DownloadManager> manager,uint32 download_id,const base::FilePath & suggested_name,const base::FilePath & download_path,bool show_dialog,content::DownloadTargetCallback callback)81 static void GenerateFilename(base::WeakPtr<DownloadManager> manager,
82 uint32 download_id,
83 const base::FilePath& suggested_name,
84 const base::FilePath& download_path,
85 bool show_dialog,
86 content::DownloadTargetCallback callback) {
87 CEF_REQUIRE_BLOCKING();
88
89 base::FilePath suggested_path = download_path;
90 if (!suggested_path.empty()) {
91 // Create the directory if necessary.
92 base::FilePath dir_path = suggested_path.DirName();
93 if (!base::DirectoryExists(dir_path) &&
94 !base::CreateDirectory(dir_path)) {
95 NOTREACHED() << "failed to create the download directory";
96 suggested_path.clear();
97 }
98 }
99
100 if (suggested_path.empty()) {
101 if (base::PathService::Get(base::DIR_TEMP, &suggested_path)) {
102 // Use the temp directory.
103 suggested_path = suggested_path.Append(suggested_name);
104 } else {
105 // Use the current working directory.
106 suggested_path = suggested_name;
107 }
108 }
109
110 CEF_POST_TASK(
111 CEF_UIT,
112 base::BindOnce(&CefBeforeDownloadCallbackImpl::ChooseDownloadPath,
113 manager, download_id, suggested_path, show_dialog,
114 std::move(callback)));
115 }
116
ChooseDownloadPath(base::WeakPtr<DownloadManager> manager,uint32 download_id,const base::FilePath & suggested_path,bool show_dialog,content::DownloadTargetCallback callback)117 static void ChooseDownloadPath(base::WeakPtr<DownloadManager> manager,
118 uint32 download_id,
119 const base::FilePath& suggested_path,
120 bool show_dialog,
121 content::DownloadTargetCallback callback) {
122 if (!manager)
123 return;
124
125 DownloadItem* item = manager->GetDownload(download_id);
126 if (!item || item->GetState() != DownloadItem::IN_PROGRESS)
127 return;
128
129 bool handled = false;
130
131 if (show_dialog) {
132 WebContents* web_contents =
133 content::DownloadItemUtils::GetWebContents(item);
134 CefRefPtr<AlloyBrowserHostImpl> browser =
135 AlloyBrowserHostImpl::GetBrowserForContents(web_contents);
136 if (browser.get()) {
137 handled = true;
138
139 CefFileDialogRunner::FileChooserParams params;
140 params.mode = blink::mojom::FileChooserParams::Mode::kSave;
141 if (!suggested_path.empty()) {
142 params.default_file_name = suggested_path;
143 if (!suggested_path.Extension().empty()) {
144 params.accept_types.push_back(
145 CefString(suggested_path.Extension()));
146 }
147 }
148
149 browser->RunFileChooser(
150 params,
151 base::BindOnce(
152 &CefBeforeDownloadCallbackImpl::ChooseDownloadPathCallback,
153 std::move(callback)));
154 }
155 }
156
157 if (!handled) {
158 std::move(callback).Run(
159 suggested_path, DownloadItem::TARGET_DISPOSITION_OVERWRITE,
160 download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
161 download::DownloadItem::MixedContentStatus::UNKNOWN, suggested_path,
162 absl::nullopt /*download_schedule*/,
163 download::DOWNLOAD_INTERRUPT_REASON_NONE);
164 }
165 }
166
ChooseDownloadPathCallback(content::DownloadTargetCallback callback,int selected_accept_filter,const std::vector<base::FilePath> & file_paths)167 static void ChooseDownloadPathCallback(
168 content::DownloadTargetCallback callback,
169 int selected_accept_filter,
170 const std::vector<base::FilePath>& file_paths) {
171 DCHECK_LE(file_paths.size(), (size_t)1);
172
173 base::FilePath path;
174 if (file_paths.size() > 0)
175 path = file_paths.front();
176
177 // The download will be cancelled if |path| is empty.
178 std::move(callback).Run(path, DownloadItem::TARGET_DISPOSITION_OVERWRITE,
179 download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
180 download::DownloadItem::MixedContentStatus::UNKNOWN,
181 path, absl::nullopt /*download_schedule*/,
182 download::DOWNLOAD_INTERRUPT_REASON_NONE);
183 }
184
185 base::WeakPtr<DownloadManager> manager_;
186 uint32 download_id_;
187 base::FilePath suggested_name_;
188 content::DownloadTargetCallback callback_;
189
190 IMPLEMENT_REFCOUNTING(CefBeforeDownloadCallbackImpl);
191 };
192
193 // CefDownloadItemCallback implementation.
194 class CefDownloadItemCallbackImpl : public CefDownloadItemCallback {
195 public:
CefDownloadItemCallbackImpl(const base::WeakPtr<DownloadManager> & manager,uint32 download_id)196 explicit CefDownloadItemCallbackImpl(
197 const base::WeakPtr<DownloadManager>& manager,
198 uint32 download_id)
199 : manager_(manager), download_id_(download_id) {}
200
201 CefDownloadItemCallbackImpl(const CefDownloadItemCallbackImpl&) = delete;
202 CefDownloadItemCallbackImpl& operator=(const CefDownloadItemCallbackImpl&) =
203 delete;
204
Cancel()205 void Cancel() override {
206 CEF_POST_TASK(CEF_UIT,
207 base::BindOnce(&CefDownloadItemCallbackImpl::DoCancel, this));
208 }
209
Pause()210 void Pause() override {
211 CEF_POST_TASK(CEF_UIT,
212 base::BindOnce(&CefDownloadItemCallbackImpl::DoPause, this));
213 }
214
Resume()215 void Resume() override {
216 CEF_POST_TASK(CEF_UIT,
217 base::BindOnce(&CefDownloadItemCallbackImpl::DoResume, this));
218 }
219
220 private:
DoCancel()221 void DoCancel() {
222 if (download_id_ <= 0)
223 return;
224
225 if (manager_) {
226 DownloadItem* item = manager_->GetDownload(download_id_);
227 if (item && item->GetState() == DownloadItem::IN_PROGRESS)
228 item->Cancel(true);
229 }
230
231 download_id_ = 0;
232 }
233
DoPause()234 void DoPause() {
235 if (download_id_ <= 0)
236 return;
237
238 if (manager_) {
239 DownloadItem* item = manager_->GetDownload(download_id_);
240 if (item && item->GetState() == DownloadItem::IN_PROGRESS)
241 item->Pause();
242 }
243 }
244
DoResume()245 void DoResume() {
246 if (download_id_ <= 0)
247 return;
248
249 if (manager_) {
250 DownloadItem* item = manager_->GetDownload(download_id_);
251 if (item && item->CanResume())
252 item->Resume(true);
253 }
254 }
255
256 base::WeakPtr<DownloadManager> manager_;
257 uint32 download_id_;
258
259 IMPLEMENT_REFCOUNTING(CefDownloadItemCallbackImpl);
260 };
261
262 } // namespace
263
CefDownloadManagerDelegate(DownloadManager * manager)264 CefDownloadManagerDelegate::CefDownloadManagerDelegate(DownloadManager* manager)
265 : manager_(manager), manager_ptr_factory_(manager) {
266 DCHECK(manager);
267 manager->AddObserver(this);
268
269 DownloadManager::DownloadVector items;
270 manager->GetAllDownloads(&items);
271 DownloadManager::DownloadVector::const_iterator it = items.begin();
272 for (; it != items.end(); ++it)
273 OnDownloadCreated(manager, *it);
274 }
275
~CefDownloadManagerDelegate()276 CefDownloadManagerDelegate::~CefDownloadManagerDelegate() {
277 if (manager_) {
278 manager_->SetDelegate(nullptr);
279 manager_->RemoveObserver(this);
280 }
281
282 while (!item_browser_map_.empty())
283 OnDownloadDestroyed(item_browser_map_.begin()->first);
284 }
285
OnDownloadUpdated(DownloadItem * download)286 void CefDownloadManagerDelegate::OnDownloadUpdated(DownloadItem* download) {
287 CefRefPtr<AlloyBrowserHostImpl> browser = GetBrowser(download);
288 CefRefPtr<CefDownloadHandler> handler;
289 if (browser.get())
290 handler = GetDownloadHandler(browser);
291
292 if (handler.get()) {
293 CefRefPtr<CefDownloadItemImpl> download_item(
294 new CefDownloadItemImpl(download));
295 CefRefPtr<CefDownloadItemCallback> callback(new CefDownloadItemCallbackImpl(
296 manager_ptr_factory_.GetWeakPtr(), download->GetId()));
297
298 handler->OnDownloadUpdated(browser.get(), download_item.get(), callback);
299
300 std::ignore = download_item->Detach(nullptr);
301 }
302 }
303
OnDownloadDestroyed(DownloadItem * item)304 void CefDownloadManagerDelegate::OnDownloadDestroyed(DownloadItem* item) {
305 item->RemoveObserver(this);
306
307 AlloyBrowserHostImpl* browser = nullptr;
308
309 ItemBrowserMap::iterator it = item_browser_map_.find(item);
310 DCHECK(it != item_browser_map_.end());
311 if (it != item_browser_map_.end()) {
312 browser = it->second;
313 item_browser_map_.erase(it);
314 }
315
316 if (browser) {
317 // Determine if any remaining DownloadItems are associated with the same
318 // browser. If not, then unregister as an observer.
319 bool has_remaining = false;
320 ItemBrowserMap::const_iterator it2 = item_browser_map_.begin();
321 for (; it2 != item_browser_map_.end(); ++it2) {
322 if (it2->second == browser) {
323 has_remaining = true;
324 break;
325 }
326 }
327
328 if (!has_remaining)
329 browser->RemoveObserver(this);
330 }
331 }
332
OnDownloadCreated(DownloadManager * manager,DownloadItem * item)333 void CefDownloadManagerDelegate::OnDownloadCreated(DownloadManager* manager,
334 DownloadItem* item) {
335 // This callback may arrive after DetermineDownloadTarget, so we allow
336 // association from either method.
337 CefRefPtr<AlloyBrowserHostImpl> browser = GetOrAssociateBrowser(item);
338 if (!browser) {
339 // If the download is rejected (e.g. ALT+click on an invalid protocol link)
340 // then an "interrupted" download will be started via DownloadManagerImpl::
341 // StartDownloadWithId (originating from CreateInterruptedDownload) with no
342 // associated WebContents and consequently no associated CEF browser. In
343 // that case DetermineDownloadTarget will be called before this method.
344 // TODO(cef): Figure out how to expose this via a client callback.
345 const std::vector<GURL>& url_chain = item->GetUrlChain();
346 if (!url_chain.empty()) {
347 LOG(INFO) << "Rejected download of " << url_chain.back().spec();
348 }
349 item->Cancel(true);
350 }
351 }
352
ManagerGoingDown(DownloadManager * manager)353 void CefDownloadManagerDelegate::ManagerGoingDown(DownloadManager* manager) {
354 DCHECK_EQ(manager, manager_);
355 manager->SetDelegate(nullptr);
356 manager->RemoveObserver(this);
357 manager_ptr_factory_.InvalidateWeakPtrs();
358 manager_ = nullptr;
359 }
360
DetermineDownloadTarget(DownloadItem * item,content::DownloadTargetCallback * callback)361 bool CefDownloadManagerDelegate::DetermineDownloadTarget(
362 DownloadItem* item,
363 content::DownloadTargetCallback* callback) {
364 if (!item->GetForcedFilePath().empty()) {
365 std::move(*callback).Run(
366 item->GetForcedFilePath(), DownloadItem::TARGET_DISPOSITION_OVERWRITE,
367 download::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
368 download::DownloadItem::MixedContentStatus::UNKNOWN,
369 item->GetForcedFilePath(), absl::nullopt /*download_schedule*/,
370 download::DOWNLOAD_INTERRUPT_REASON_NONE);
371 return true;
372 }
373
374 // This callback may arrive before OnDownloadCreated, so we allow association
375 // from either method.
376 CefRefPtr<AlloyBrowserHostImpl> browser = GetOrAssociateBrowser(item);
377 CefRefPtr<CefDownloadHandler> handler;
378 if (browser.get())
379 handler = GetDownloadHandler(browser);
380
381 if (handler.get()) {
382 base::FilePath suggested_name = net::GenerateFileName(
383 item->GetURL(), item->GetContentDisposition(), std::string(),
384 item->GetSuggestedFilename(), item->GetMimeType(), "download");
385
386 CefRefPtr<CefDownloadItemImpl> download_item(new CefDownloadItemImpl(item));
387 CefRefPtr<CefBeforeDownloadCallback> callbackObj(
388 new CefBeforeDownloadCallbackImpl(manager_ptr_factory_.GetWeakPtr(),
389 item->GetId(), suggested_name,
390 std::move(*callback)));
391
392 handler->OnBeforeDownload(browser.get(), download_item.get(),
393 suggested_name.value(), callbackObj);
394
395 std::ignore = download_item->Detach(nullptr);
396 }
397
398 return true;
399 }
400
GetNextId(content::DownloadIdCallback callback)401 void CefDownloadManagerDelegate::GetNextId(
402 content::DownloadIdCallback callback) {
403 static uint32 next_id = DownloadItem::kInvalidId + 1;
404 std::move(callback).Run(next_id++);
405 }
406
ApplicationClientIdForFileScanning()407 std::string CefDownloadManagerDelegate::ApplicationClientIdForFileScanning() {
408 const CefSettings& settings = CefContext::Get()->settings();
409 if (settings.application_client_id_for_file_scanning.length > 0) {
410 return CefString(&settings.application_client_id_for_file_scanning)
411 .ToString();
412 }
413 return std::string();
414 }
415
OnBrowserDestroyed(CefBrowserHostBase * browser)416 void CefDownloadManagerDelegate::OnBrowserDestroyed(
417 CefBrowserHostBase* browser) {
418 ItemBrowserMap::iterator it = item_browser_map_.begin();
419 for (; it != item_browser_map_.end(); ++it) {
420 if (it->second == browser) {
421 // Don't call back into browsers that have been destroyed. We're not
422 // canceling the download so it will continue silently until it completes
423 // or until the associated browser context is destroyed.
424 it->second = nullptr;
425 }
426 }
427 }
428
GetOrAssociateBrowser(download::DownloadItem * item)429 AlloyBrowserHostImpl* CefDownloadManagerDelegate::GetOrAssociateBrowser(
430 download::DownloadItem* item) {
431 ItemBrowserMap::const_iterator it = item_browser_map_.find(item);
432 if (it != item_browser_map_.end())
433 return it->second;
434
435 AlloyBrowserHostImpl* browser = nullptr;
436 content::WebContents* contents =
437 content::DownloadItemUtils::GetWebContents(item);
438 if (contents) {
439 browser = AlloyBrowserHostImpl::GetBrowserForContents(contents).get();
440 DCHECK(browser);
441 }
442 if (!browser)
443 return nullptr;
444
445 item->AddObserver(this);
446
447 item_browser_map_.insert(std::make_pair(item, browser));
448
449 // Register as an observer so that we can cancel associated DownloadItems when
450 // the browser is destroyed.
451 if (!browser->HasObserver(this))
452 browser->AddObserver(this);
453
454 return browser;
455 }
456
GetBrowser(DownloadItem * item)457 AlloyBrowserHostImpl* CefDownloadManagerDelegate::GetBrowser(
458 DownloadItem* item) {
459 ItemBrowserMap::const_iterator it = item_browser_map_.find(item);
460 if (it != item_browser_map_.end())
461 return it->second;
462
463 // If the download is rejected (e.g. ALT+click on an invalid protocol link)
464 // then an "interrupted" download will be started via DownloadManagerImpl::
465 // StartDownloadWithId (originating from CreateInterruptedDownload) with no
466 // associated WebContents and consequently no associated CEF browser. In that
467 // case DetermineDownloadTarget will be called before OnDownloadCreated.
468 DCHECK(!content::DownloadItemUtils::GetWebContents(item));
469 return nullptr;
470 }
471