/* 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 # include # include "nsILocalFileWin.h" # include "WinTaskbar.h" #endif #ifdef XP_MACOSX # include # include "../../../xpcom/io/CocoaFileUtils.h" #endif #ifdef MOZ_WIDGET_GTK # include #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 NULL; } CFURLRef url = ::CFURLCreateWithString(kCFAllocatorDefault, urlStr, NULL); ::CFRelease(urlStr); return url; } #endif #ifdef XP_WIN static void AddToRecentDocs(nsIFile* aTarget, nsAutoString& aPath) { nsString modelId; if (mozilla::widget::WinTaskbar::GetAppUserModelID(modelId)) { nsCOMPtr 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::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 = NULL; if (!path.IsEmpty()) { pathCFStr = ::CFStringCreateWithCharacters( kCFAllocatorDefault, (const UniChar*)path.get(), path.Length()); } if (pathCFStr && !aIsPrivate) { bool isFromWeb = IsURLPossiblyFromWeb(aSource); nsCOMPtr source(aSource); nsCOMPtr 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 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::MapUrlToZone(const nsAString& aURL, uint32_t* aZone) { #ifdef XP_WIN RefPtr inetSecMgr; if (FAILED(CoCreateInstance(CLSID_InternetSecurityManager, NULL, CLSCTX_ALL, IID_IInternetSecurityManager, getter_AddRefs(inetSecMgr)))) { return NS_ERROR_UNEXPECTED; } DWORD zone; if (inetSecMgr->MapUrlToZone(PromiseFlatString(aURL).get(), &zone, 0) != S_OK) { return NS_ERROR_UNEXPECTED; } else { *aZone = zone; } return NS_OK; #else return NS_ERROR_NOT_IMPLEMENTED; #endif } // 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 ios = do_GetIOService(); nsCOMPtr 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 nestedURI = do_QueryInterface(uri); uri = nullptr; if (nestedURI) { rv = nestedURI->GetInnerURI(getter_AddRefs(uri)); if (NS_FAILED(rv)) { return true; } } } return false; }