The previous interface conflated having a source or referrer with writing them. The source is needed to determine the zone, so that meant we couldn't determine the correct zone when not writing information because the information came from private browsing. This change allows the zone to be established correctly even when not writing URLs, which technically means it leaks a minor amount of information, but nothing sensitive. Differential Revision: https://phabricator.services.mozilla.com/D304318
336 lines
11 KiB
C++
336 lines
11 KiB
C++
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "DownloadPlatform.h"
|
|
#include "nsNetUtil.h"
|
|
#include "nsString.h"
|
|
#include "nsINestedURI.h"
|
|
#include "nsIProtocolHandler.h"
|
|
#include "nsIURI.h"
|
|
#include "nsIFile.h"
|
|
#include "xpcpublic.h"
|
|
|
|
#include "mozilla/dom/Promise.h"
|
|
#include "mozilla/Preferences.h"
|
|
|
|
#define PREF_BDM_ADDTORECENTDOCS "browser.download.manager.addToRecentDocs"
|
|
|
|
#ifdef XP_WIN
|
|
# include <shlobj.h>
|
|
# include <urlmon.h>
|
|
# include "nsILocalFileWin.h"
|
|
# include "WinTaskbar.h"
|
|
# include "WinUtils.h"
|
|
#endif
|
|
|
|
#ifdef XP_MACOSX
|
|
# include <CoreFoundation/CoreFoundation.h>
|
|
# include "../../../xpcom/io/CocoaFileUtils.h"
|
|
#endif
|
|
|
|
#ifdef MOZ_WIDGET_GTK
|
|
# include <gtk/gtk.h>
|
|
#endif
|
|
|
|
using namespace mozilla;
|
|
using dom::Promise;
|
|
|
|
DownloadPlatform* DownloadPlatform::gDownloadPlatformService = nullptr;
|
|
|
|
NS_IMPL_ISUPPORTS(DownloadPlatform, mozIDownloadPlatform);
|
|
|
|
DownloadPlatform* DownloadPlatform::GetDownloadPlatform() {
|
|
if (!gDownloadPlatformService) {
|
|
gDownloadPlatformService = new DownloadPlatform();
|
|
}
|
|
|
|
NS_ADDREF(gDownloadPlatformService);
|
|
|
|
return gDownloadPlatformService;
|
|
}
|
|
|
|
#ifdef MOZ_WIDGET_GTK
|
|
static void gio_set_metadata_done(GObject* source_obj, GAsyncResult* res,
|
|
gpointer user_data) {
|
|
GError* err = nullptr;
|
|
g_file_set_attributes_finish(G_FILE(source_obj), res, nullptr, &err);
|
|
if (err) {
|
|
# ifdef DEBUG
|
|
NS_DebugBreak(NS_DEBUG_WARNING, "Set file metadata failed: ", err->message,
|
|
__FILE__, __LINE__);
|
|
# endif
|
|
g_error_free(err);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef XP_MACOSX
|
|
// Caller is responsible for freeing any result (CF Create Rule)
|
|
CFURLRef CreateCFURLFromNSIURI(nsIURI* aURI) {
|
|
nsAutoCString spec;
|
|
if (aURI) {
|
|
aURI->GetSpec(spec);
|
|
}
|
|
|
|
CFStringRef urlStr = ::CFStringCreateWithCString(
|
|
kCFAllocatorDefault, spec.get(), kCFStringEncodingUTF8);
|
|
if (!urlStr) {
|
|
return nullptr;
|
|
}
|
|
|
|
CFURLRef url = ::CFURLCreateWithString(kCFAllocatorDefault, urlStr, nullptr);
|
|
|
|
::CFRelease(urlStr);
|
|
|
|
return url;
|
|
}
|
|
#endif
|
|
|
|
#ifdef XP_WIN
|
|
static void AddToRecentDocs(nsIFile* aTarget, nsAutoString& aPath) {
|
|
nsString modelId;
|
|
if (mozilla::widget::WinTaskbar::GetAppUserModelID(modelId)) {
|
|
nsCOMPtr<nsIURI> uri;
|
|
if (NS_SUCCEEDED(NS_NewFileURI(getter_AddRefs(uri), aTarget)) && uri) {
|
|
nsCString spec;
|
|
if (NS_SUCCEEDED(uri->GetSpec(spec))) {
|
|
IShellItem2* psi = nullptr;
|
|
if (SUCCEEDED(
|
|
SHCreateItemFromParsingName(NS_ConvertASCIItoUTF16(spec).get(),
|
|
nullptr, IID_PPV_ARGS(&psi)))) {
|
|
SHARDAPPIDINFO info = {psi, modelId.get()};
|
|
::SHAddToRecentDocs(SHARD_APPIDINFO, &info);
|
|
psi->Release();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
::SHAddToRecentDocs(SHARD_PATHW, aPath.get());
|
|
}
|
|
#endif
|
|
|
|
nsresult DownloadPlatform::DownloadDone(nsIURI* aSource, nsIURI* aReferrer,
|
|
nsIFile* aTarget,
|
|
const nsACString& aContentType,
|
|
bool aIsPrivate, JSContext* aCx,
|
|
Promise** aPromise) {
|
|
nsIGlobalObject* globalObject =
|
|
xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx));
|
|
|
|
if (NS_WARN_IF(!globalObject)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
ErrorResult result;
|
|
RefPtr<Promise> promise = Promise::Create(globalObject, result);
|
|
|
|
if (NS_WARN_IF(result.Failed())) {
|
|
return result.StealNSResult();
|
|
}
|
|
|
|
nsresult rv = NS_OK;
|
|
bool pendingAsyncOperations = false;
|
|
|
|
#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_ANDROID) || \
|
|
defined(MOZ_WIDGET_GTK)
|
|
|
|
nsAutoString path;
|
|
if (aTarget && NS_SUCCEEDED(aTarget->GetPath(path))) {
|
|
# if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_ANDROID)
|
|
// On Windows and Gtk, add the download to the system's "recent documents"
|
|
// list, with a pref to disable.
|
|
{
|
|
# ifndef MOZ_WIDGET_ANDROID
|
|
bool addToRecentDocs = Preferences::GetBool(PREF_BDM_ADDTORECENTDOCS);
|
|
if (addToRecentDocs && !aIsPrivate) {
|
|
# ifdef XP_WIN
|
|
AddToRecentDocs(aTarget, path);
|
|
# elif defined(MOZ_WIDGET_GTK)
|
|
GtkRecentManager* manager = gtk_recent_manager_get_default();
|
|
|
|
gchar* uri = g_filename_to_uri(NS_ConvertUTF16toUTF8(path).get(),
|
|
nullptr, nullptr);
|
|
if (uri) {
|
|
gtk_recent_manager_add_item(manager, uri);
|
|
g_free(uri);
|
|
}
|
|
# endif
|
|
}
|
|
# endif
|
|
# ifdef MOZ_WIDGET_GTK
|
|
// Private window should not leak URI to the system (Bug 1535950)
|
|
if (!aIsPrivate) {
|
|
// Use GIO to store the source URI for later display in the file
|
|
// manager.
|
|
GFile* gio_file =
|
|
g_file_new_for_path(NS_ConvertUTF16toUTF8(path).get());
|
|
nsCString source_uri;
|
|
nsresult rv = aSource->GetSpec(source_uri);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
GFileInfo* file_info = g_file_info_new();
|
|
g_file_info_set_attribute_string(file_info, "metadata::download-uri",
|
|
source_uri.get());
|
|
g_file_set_attributes_async(gio_file, file_info, G_FILE_QUERY_INFO_NONE,
|
|
G_PRIORITY_DEFAULT, nullptr,
|
|
gio_set_metadata_done, nullptr);
|
|
g_object_unref(file_info);
|
|
g_object_unref(gio_file);
|
|
}
|
|
# endif
|
|
}
|
|
# endif
|
|
|
|
# ifdef XP_MACOSX
|
|
// On OS X, make the downloads stack bounce.
|
|
CFStringRef observedObject = ::CFStringCreateWithCString(
|
|
kCFAllocatorDefault, NS_ConvertUTF16toUTF8(path).get(),
|
|
kCFStringEncodingUTF8);
|
|
CFNotificationCenterRef center =
|
|
::CFNotificationCenterGetDistributedCenter();
|
|
::CFNotificationCenterPostNotification(
|
|
center, CFSTR("com.apple.DownloadFileFinished"), observedObject,
|
|
nullptr, TRUE);
|
|
::CFRelease(observedObject);
|
|
|
|
// Add OS X origin and referrer file metadata
|
|
CFStringRef pathCFStr = nullptr;
|
|
if (!path.IsEmpty()) {
|
|
pathCFStr = ::CFStringCreateWithCharacters(
|
|
kCFAllocatorDefault, (const UniChar*)path.get(), path.Length());
|
|
}
|
|
if (pathCFStr && !aIsPrivate) {
|
|
bool isFromWeb = IsURLPossiblyFromWeb(aSource);
|
|
nsCOMPtr<nsIURI> source(aSource);
|
|
nsCOMPtr<nsIURI> referrer(aReferrer);
|
|
|
|
rv = NS_DispatchBackgroundTask(
|
|
NS_NewRunnableFunction(
|
|
"DownloadPlatform::DownloadDone",
|
|
[pathCFStr, isFromWeb, source, referrer, promise]() mutable {
|
|
CFURLRef sourceCFURL = CreateCFURLFromNSIURI(source);
|
|
CFURLRef referrerCFURL = CreateCFURLFromNSIURI(referrer);
|
|
|
|
CocoaFileUtils::AddOriginMetadataToFile(pathCFStr, sourceCFURL,
|
|
referrerCFURL);
|
|
CocoaFileUtils::AddQuarantineMetadataToFile(
|
|
pathCFStr, sourceCFURL, referrerCFURL, isFromWeb);
|
|
::CFRelease(pathCFStr);
|
|
if (sourceCFURL) {
|
|
::CFRelease(sourceCFURL);
|
|
}
|
|
if (referrerCFURL) {
|
|
::CFRelease(referrerCFURL);
|
|
}
|
|
|
|
DebugOnly<nsresult> rv =
|
|
NS_DispatchToMainThread(NS_NewRunnableFunction(
|
|
"DownloadPlatform::DownloadDoneResolve",
|
|
[promise = std::move(promise)]() {
|
|
promise->MaybeResolveWithUndefined();
|
|
}));
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
|
// In non-debug builds, if we've for some reason failed to
|
|
// dispatch a runnable to the main thread to resolve the
|
|
// Promise, then it's unlikely we can reject it either. At that
|
|
// point, the Promise is going to remain in pending limbo until
|
|
// its global goes away.
|
|
}),
|
|
NS_DISPATCH_EVENT_MAY_BLOCK);
|
|
|
|
if (NS_SUCCEEDED(rv)) {
|
|
pendingAsyncOperations = true;
|
|
}
|
|
}
|
|
# endif
|
|
}
|
|
|
|
#endif
|
|
|
|
if (!pendingAsyncOperations) {
|
|
promise->MaybeResolveWithUndefined();
|
|
}
|
|
promise.forget(aPromise);
|
|
return rv;
|
|
}
|
|
|
|
nsresult DownloadPlatform::MaybeWriteDownloadOriginInformation(
|
|
nsIFile* aTargetFile, nsIURI* aSourceUrl, nsIReferrerInfo* aReferrerInfo,
|
|
bool aIsPrivate, JSContext* aCx, dom::Promise** aPromise) {
|
|
NS_ENSURE_ARG(aCx);
|
|
NS_ENSURE_ARG(aPromise);
|
|
|
|
nsIGlobalObject* globalObject =
|
|
xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx));
|
|
if (NS_WARN_IF(!globalObject)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
mozilla::ErrorResult result;
|
|
RefPtr domPromise = dom::Promise::Create(globalObject, result);
|
|
if (NS_WARN_IF(result.Failed())) {
|
|
return result.StealNSResult();
|
|
}
|
|
|
|
#ifdef XP_WIN
|
|
NS_ENSURE_ARG(aTargetFile);
|
|
NS_ENSURE_ARG(aSourceUrl);
|
|
|
|
RefPtr mozPromise = widget::WinUtils::MaybeWriteFileZoneId(
|
|
aTargetFile, aSourceUrl, aReferrerInfo, !aIsPrivate);
|
|
MOZ_ASSERT(mozPromise);
|
|
|
|
mozPromise->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[domPromise](bool aSuccess) { domPromise->MaybeResolve(aSuccess); },
|
|
[domPromise](nsresult aRv) { domPromise->MaybeReject(aRv); });
|
|
#else
|
|
// Not an error but nothing was written.
|
|
domPromise->MaybeResolve(false);
|
|
#endif
|
|
|
|
domPromise.forget(aPromise);
|
|
return NS_OK;
|
|
}
|
|
|
|
// Check if a URI is likely to be web-based, by checking its URI flags.
|
|
// If in doubt (e.g. if anything fails during the check) claims things
|
|
// are from the web.
|
|
bool DownloadPlatform::IsURLPossiblyFromWeb(nsIURI* aURI) {
|
|
nsCOMPtr<nsIIOService> ios = do_GetIOService();
|
|
nsCOMPtr<nsIURI> uri = aURI;
|
|
if (!ios) {
|
|
return true;
|
|
}
|
|
|
|
while (uri) {
|
|
// We're not using NS_URIChainHasFlags because we're checking for *any* of 3
|
|
// flags to be present on *all* of the nested URIs, which it can't do.
|
|
uint32_t flags;
|
|
nsresult rv = ios->GetDynamicProtocolFlags(uri, &flags);
|
|
if (NS_FAILED(rv)) {
|
|
return true;
|
|
}
|
|
// If not dangerous to load, not a UI resource and not a local file,
|
|
// assume this is from the web:
|
|
if (!(flags & nsIProtocolHandler::URI_DANGEROUS_TO_LOAD) &&
|
|
!(flags & nsIProtocolHandler::URI_IS_UI_RESOURCE) &&
|
|
!(flags & nsIProtocolHandler::URI_IS_LOCAL_FILE)) {
|
|
return true;
|
|
}
|
|
// Otherwise, check if the URI is nested, and if so go through
|
|
// the loop again:
|
|
nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(uri);
|
|
uri = nullptr;
|
|
if (nestedURI) {
|
|
rv = nestedURI->GetInnerURI(getter_AddRefs(uri));
|
|
if (NS_FAILED(rv)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|