From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../exthandler/nsExternalHelperAppService.cpp | 3750 ++++++++++++++++++++ 1 file changed, 3750 insertions(+) create mode 100644 uriloader/exthandler/nsExternalHelperAppService.cpp (limited to 'uriloader/exthandler/nsExternalHelperAppService.cpp') diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp new file mode 100644 index 0000000000..84dc262561 --- /dev/null +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -0,0 +1,3750 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * 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 "base/basictypes.h" + +/* This must occur *after* base/basictypes.h to avoid typedefs conflicts. */ +#include "mozilla/ArrayUtils.h" +#include "mozilla/Base64.h" +#include "mozilla/ResultExtensions.h" + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/RandomNum.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/StaticPtr.h" +#include "nsXULAppAPI.h" + +#include "ExternalHelperAppParent.h" +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsIURI.h" +#include "nsIURL.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsIChannel.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsICategoryManager.h" +#include "nsDependentSubstring.h" +#include "nsSandboxFlags.h" +#include "nsString.h" +#include "nsUnicharUtils.h" +#include "nsIStringEnumerator.h" +#include "nsIStreamListener.h" +#include "nsIMIMEService.h" +#include "nsILoadGroup.h" +#include "nsIWebProgressListener.h" +#include "nsITransfer.h" +#include "nsReadableUtils.h" +#include "nsIRequest.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIInterfaceRequestor.h" +#include "nsThreadUtils.h" +#include "nsIMutableArray.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsOSHelperAppService.h" +#include "nsOSHelperAppServiceChild.h" +#include "nsContentSecurityUtils.h" +#include "nsUTF8Utils.h" +#include "nsUnicodeProperties.h" + +// used to access our datastore of user-configured helper applications +#include "nsIHandlerService.h" +#include "nsIMIMEInfo.h" +#include "nsIHelperAppLauncherDialog.h" +#include "nsIContentDispatchChooser.h" +#include "nsNetUtil.h" +#include "nsIPrivateBrowsingChannel.h" +#include "nsIIOService.h" +#include "nsNetCID.h" + +#include "nsIApplicationReputation.h" + +#include "nsDSURIContentListener.h" +#include "nsMimeTypes.h" +#include "nsMIMEInfoImpl.h" +// used for header disposition information. +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIEncodedChannel.h" +#include "nsIMultiPartChannel.h" +#include "nsIFileChannel.h" +#include "nsIObserverService.h" // so we can be a profile change observer +#include "nsIPropertyBag2.h" // for the 64-bit content length + +#ifdef XP_MACOSX +# include "nsILocalFileMac.h" +#endif + +#include "nsEscape.h" + +#include "nsIStringBundle.h" // XXX needed to localize error msgs +#include "nsIPrompt.h" + +#include "nsITextToSubURI.h" // to unescape the filename + +#include "nsDocShellCID.h" + +#include "nsCRT.h" +#include "nsLocalHandlerApp.h" + +#include "nsIRandomGenerator.h" + +#include "ContentChild.h" +#include "nsXULAppAPI.h" +#include "nsPIDOMWindow.h" +#include "ExternalHelperAppChild.h" + +#include "mozilla/dom/nsHTTPSOnlyUtils.h" + +#ifdef XP_WIN +# include "nsWindowsHelpers.h" +# include "nsLocalFile.h" +#endif + +#include "mozilla/Components.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Preferences.h" +#include "mozilla/ipc/URIUtils.h" + +using namespace mozilla; +using namespace mozilla::ipc; +using namespace mozilla::dom; + +#define kDefaultMaxFileNameLength 254 + +// Download Folder location constants +#define NS_PREF_DOWNLOAD_DIR "browser.download.dir" +#define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList" +enum { + NS_FOLDER_VALUE_DESKTOP = 0, + NS_FOLDER_VALUE_DOWNLOADS = 1, + NS_FOLDER_VALUE_CUSTOM = 2 +}; + +LazyLogModule nsExternalHelperAppService::sLog("HelperAppService"); + +// Using level 3 here because the OSHelperAppServices use a log level +// of LogLevel::Debug (4), and we want less detailed output here +// Using 3 instead of LogLevel::Warning because we don't output warnings +#undef LOG +#define LOG(...) \ + MOZ_LOG(nsExternalHelperAppService::sLog, mozilla::LogLevel::Info, \ + (__VA_ARGS__)) +#define LOG_ENABLED() \ + MOZ_LOG_TEST(nsExternalHelperAppService::sLog, mozilla::LogLevel::Info) + +static const char NEVER_ASK_FOR_SAVE_TO_DISK_PREF[] = + "browser.helperApps.neverAsk.saveToDisk"; +static const char NEVER_ASK_FOR_OPEN_FILE_PREF[] = + "browser.helperApps.neverAsk.openFile"; + +StaticRefPtr sFallbackDownloadDir; + +// Helper functions for Content-Disposition headers + +/** + * Given a URI fragment, unescape it + * @param aFragment The string to unescape + * @param aURI The URI from which this fragment is taken. Only its character set + * will be used. + * @param aResult [out] Unescaped string. + */ +static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI, + nsAString& aResult) { + // We need the unescaper + nsresult rv; + nsCOMPtr textToSubURI = + do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return textToSubURI->UnEscapeURIForUI(aFragment, /* aDontEscape = */ true, + aResult); +} + +/** + * UTF-8 version of UnescapeFragment. + * @param aFragment The string to unescape + * @param aURI The URI from which this fragment is taken. Only its character set + * will be used. + * @param aResult [out] Unescaped string, UTF-8 encoded. + * @note It is safe to pass the same string for aFragment and aResult. + * @note When this function fails, aResult will not be modified. + */ +static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI, + nsACString& aResult) { + nsAutoString result; + nsresult rv = UnescapeFragment(aFragment, aURI, result); + if (NS_SUCCEEDED(rv)) CopyUTF16toUTF8(result, aResult); + return rv; +} + +/** + * Obtains the directory to use. This tends to vary per platform, and + * needs to be consistent throughout our codepaths. For platforms where + * helper apps use the downloads directory, this should be kept in + * sync with DownloadIntegration.sys.mjs. + * + * Optionally skip availability of the directory and storage. + */ +static nsresult GetDownloadDirectory(nsIFile** _directory, + bool aSkipChecks = false) { +#if defined(ANDROID) + return NS_ERROR_FAILURE; +#endif + + bool usePrefDir = !StaticPrefs::browser_download_start_downloads_in_tmp_dir(); + + nsCOMPtr dir; + nsresult rv; + if (usePrefDir) { + // Try to get the users download location, if it's set. + switch (Preferences::GetInt(NS_PREF_DOWNLOAD_FOLDERLIST, -1)) { + case NS_FOLDER_VALUE_DESKTOP: + (void)NS_GetSpecialDirectory(NS_OS_DESKTOP_DIR, getter_AddRefs(dir)); + break; + case NS_FOLDER_VALUE_CUSTOM: { + Preferences::GetComplex(NS_PREF_DOWNLOAD_DIR, NS_GET_IID(nsIFile), + getter_AddRefs(dir)); + if (!dir) break; + + // If we're not checking for availability we're done. + if (aSkipChecks) { + dir.forget(_directory); + return NS_OK; + } + + // We have the directory, and now we need to make sure it exists + nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755); + // If we can't create this and it's not because the file already + // exists, clear out `dir` so we don't return it. + if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_FAILED(rv)) { + dir = nullptr; + } + } break; + case NS_FOLDER_VALUE_DOWNLOADS: + // This is just the OS default location, so fall out + break; + } + if (!dir) { + rv = NS_GetSpecialDirectory(NS_OS_DEFAULT_DOWNLOAD_DIR, + getter_AddRefs(dir)); + if (NS_FAILED(rv)) { + // On some OSes, there is no guarantee this directory exists. + // Fall back to $HOME + Downloads. + if (sFallbackDownloadDir) { + sFallbackDownloadDir->Clone(getter_AddRefs(dir)); + } else { + rv = NS_GetSpecialDirectory(NS_OS_HOME_DIR, getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr bundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoString downloadLocalized; + nsCOMPtr downloadBundle; + rv = bundleService->CreateBundle( + "chrome://mozapps/locale/downloads/downloads.properties", + getter_AddRefs(downloadBundle)); + if (NS_SUCCEEDED(rv)) { + rv = downloadBundle->GetStringFromName("downloadsFolder", + downloadLocalized); + } + if (NS_FAILED(rv)) { + downloadLocalized.AssignLiteral("Downloads"); + } + rv = dir->Append(downloadLocalized); + NS_ENSURE_SUCCESS(rv, rv); + // Can't getter_AddRefs on StaticRefPtr, so do some copying. + nsCOMPtr copy; + dir->Clone(getter_AddRefs(copy)); + sFallbackDownloadDir = copy.forget(); + ClearOnShutdown(&sFallbackDownloadDir); + } + if (aSkipChecks) { + dir.forget(_directory); + return NS_OK; + } + + // We have the directory, and now we need to make sure it exists + rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (rv == NS_ERROR_FILE_ALREADY_EXISTS || NS_SUCCEEDED(rv)) { + dir.forget(_directory); + rv = NS_OK; + } + return rv; + } + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + +#if !defined(XP_MACOSX) && defined(XP_UNIX) + // Ensuring that only the current user can read the file names we end up + // creating. Note that creating directories with a specified permission is + // only supported on Unix platform right now. That's why the above check + // exists. + + uint32_t permissions; + rv = dir->GetPermissions(&permissions); + NS_ENSURE_SUCCESS(rv, rv); + + if (permissions != PR_IRWXU) { + const char* userName = PR_GetEnv("USERNAME"); + if (!userName || !*userName) { + userName = PR_GetEnv("USER"); + } + if (!userName || !*userName) { + userName = PR_GetEnv("LOGNAME"); + } + if (!userName || !*userName) { + userName = "mozillaUser"; + } + + nsAutoString userDir; + userDir.AssignLiteral("mozilla_"); + userDir.AppendASCII(userName); + userDir.ReplaceChar(u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_'); + + int counter = 0; + bool pathExists; + nsCOMPtr finalPath; + + while (true) { + nsAutoString countedUserDir(userDir); + countedUserDir.AppendInt(counter, 10); + dir->Clone(getter_AddRefs(finalPath)); + finalPath->Append(countedUserDir); + + rv = finalPath->Exists(&pathExists); + NS_ENSURE_SUCCESS(rv, rv); + + if (pathExists) { + // If this path has the right permissions, use it. + rv = finalPath->GetPermissions(&permissions); + NS_ENSURE_SUCCESS(rv, rv); + + // Ensuring the path is writable by the current user. + bool isWritable; + rv = finalPath->IsWritable(&isWritable); + NS_ENSURE_SUCCESS(rv, rv); + + if (permissions == PR_IRWXU && isWritable) { + dir = finalPath; + break; + } + } + + rv = finalPath->Create(nsIFile::DIRECTORY_TYPE, PR_IRWXU); + if (NS_SUCCEEDED(rv)) { + dir = finalPath; + break; + } + if (rv != NS_ERROR_FILE_ALREADY_EXISTS) { + // Unexpected error. + return rv; + } + counter++; + } + } + +#endif + } + + NS_ASSERTION(dir, "Somehow we didn't get a download directory!"); + dir.forget(_directory); + return NS_OK; +} + +/** + * Helper for random bytes for the filename of downloaded part files. + */ +nsresult GenerateRandomName(nsACString& result) { + // We will request raw random bytes, and transform that to a base64 string, + // using url-based base64 encoding so that all characters from the base64 + // result will be acceptable for filenames. + // For each three bytes of random data, we will get four bytes of ASCII. + // Request a bit more, to be safe, then truncate in the end. + + nsresult rv; + const uint32_t wantedFileNameLength = 8; + const uint32_t requiredBytesLength = + static_cast((wantedFileNameLength + 1) / 4 * 3); + + uint8_t buffer[requiredBytesLength]; + if (!mozilla::GenerateRandomBytesFromOS(buffer, requiredBytesLength)) { + return NS_ERROR_FAILURE; + } + + nsAutoCString tempLeafName; + // We're forced to specify a padding policy, though this is guaranteed + // not to need padding due to requiredBytesLength being a multiple of 3. + rv = Base64URLEncode(requiredBytesLength, buffer, + Base64URLEncodePaddingPolicy::Omit, tempLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + tempLeafName.Truncate(wantedFileNameLength); + + result.Assign(tempLeafName); + return NS_OK; +} + +/** + * Structure for storing extension->type mappings. + * @see defaultMimeEntries + */ +struct nsDefaultMimeTypeEntry { + const char* mMimeType; + const char* mFileExtension; +}; + +/** + * Default extension->mimetype mappings. These are not overridable. + * If you add types here, make sure they are lowercase, or you'll regret it. + */ +static const nsDefaultMimeTypeEntry defaultMimeEntries[] = { + // The following are those extensions that we're asked about during startup, + // sorted by order used + {IMAGE_GIF, "gif"}, + {TEXT_XML, "xml"}, + {APPLICATION_RDF, "rdf"}, + {IMAGE_PNG, "png"}, + // -- end extensions used during startup + {TEXT_CSS, "css"}, + {IMAGE_JPEG, "jpeg"}, + {IMAGE_JPEG, "jpg"}, + {IMAGE_SVG_XML, "svg"}, + {TEXT_HTML, "html"}, + {TEXT_HTML, "htm"}, + {APPLICATION_XPINSTALL, "xpi"}, + {"application/xhtml+xml", "xhtml"}, + {"application/xhtml+xml", "xht"}, + {TEXT_PLAIN, "txt"}, + {APPLICATION_JSON, "json"}, + {APPLICATION_XJAVASCRIPT, "mjs"}, + {APPLICATION_XJAVASCRIPT, "js"}, + {APPLICATION_XJAVASCRIPT, "jsm"}, + {VIDEO_OGG, "ogv"}, + {VIDEO_OGG, "ogg"}, + {APPLICATION_OGG, "ogg"}, + {AUDIO_OGG, "oga"}, + {AUDIO_OGG, "opus"}, + {APPLICATION_PDF, "pdf"}, + {VIDEO_WEBM, "webm"}, + {AUDIO_WEBM, "webm"}, + {IMAGE_ICO, "ico"}, + {TEXT_PLAIN, "properties"}, + {TEXT_PLAIN, "locale"}, + {TEXT_PLAIN, "ftl"}, +#if defined(MOZ_WMF) + {VIDEO_MP4, "mp4"}, + {AUDIO_MP4, "m4a"}, + {AUDIO_MP3, "mp3"}, +#endif +#ifdef MOZ_RAW + {VIDEO_RAW, "yuv"} +#endif +}; + +/** + * This is a small private struct used to help us initialize some + * default mime types. + */ +struct nsExtraMimeTypeEntry { + const char* mMimeType; + const char* mFileExtensions; + const char* mDescription; +}; + +/** + * This table lists all of the 'extra' content types that we can deduce from + * particular file extensions. These entries also ensure that we provide a good + * descriptive name when we encounter files with these content types and/or + * extensions. These can be overridden by user helper app prefs. If you add + * types here, make sure they are lowercase, or you'll regret it. + */ +static const nsExtraMimeTypeEntry extraMimeEntries[] = { +#if defined(XP_MACOSX) // don't define .bin on the mac...use internet config to + // look that up... + {APPLICATION_OCTET_STREAM, "exe,com", "Binary File"}, +#else + {APPLICATION_OCTET_STREAM, "exe,com,bin", "Binary File"}, +#endif + {APPLICATION_GZIP2, "gz", "gzip"}, + {"application/x-arj", "arj", "ARJ file"}, + {"application/rtf", "rtf", "Rich Text Format File"}, + {APPLICATION_ZIP, "zip", "ZIP Archive"}, + {APPLICATION_XPINSTALL, "xpi", "XPInstall Install"}, + {APPLICATION_PDF, "pdf", "Portable Document Format"}, + {APPLICATION_POSTSCRIPT, "ps,eps,ai", "Postscript File"}, + {APPLICATION_XJAVASCRIPT, "js", "Javascript Source File"}, + {APPLICATION_XJAVASCRIPT, "jsm,mjs", "Javascript Module Source File"}, +#ifdef MOZ_WIDGET_ANDROID + {"application/vnd.android.package-archive", "apk", "Android Package"}, +#endif + + // OpenDocument formats + {"application/vnd.oasis.opendocument.text", "odt", "OpenDocument Text"}, + {"application/vnd.oasis.opendocument.presentation", "odp", + "OpenDocument Presentation"}, + {"application/vnd.oasis.opendocument.spreadsheet", "ods", + "OpenDocument Spreadsheet"}, + {"application/vnd.oasis.opendocument.graphics", "odg", + "OpenDocument Graphics"}, + + // Legacy Microsoft Office + {"application/msword", "doc", "Microsoft Word"}, + {"application/vnd.ms-powerpoint", "ppt", "Microsoft PowerPoint"}, + {"application/vnd.ms-excel", "xls", "Microsoft Excel"}, + + // Office Open XML + {"application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "docx", "Microsoft Word (Open XML)"}, + {"application/" + "vnd.openxmlformats-officedocument.presentationml.presentation", + "pptx", "Microsoft PowerPoint (Open XML)"}, + {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xlsx", "Microsoft Excel (Open XML)"}, + + {IMAGE_ART, "art", "ART Image"}, + {IMAGE_BMP, "bmp", "BMP Image"}, + {IMAGE_GIF, "gif", "GIF Image"}, + {IMAGE_ICO, "ico,cur", "ICO Image"}, + {IMAGE_JPEG, "jpg,jpeg,jfif,pjpeg,pjp", "JPEG Image"}, + {IMAGE_PNG, "png", "PNG Image"}, + {IMAGE_APNG, "apng", "APNG Image"}, + {IMAGE_TIFF, "tiff,tif", "TIFF Image"}, + {IMAGE_XBM, "xbm", "XBM Image"}, + {IMAGE_SVG_XML, "svg", "Scalable Vector Graphics"}, + {IMAGE_WEBP, "webp", "WebP Image"}, + {IMAGE_AVIF, "avif", "AV1 Image File"}, + {IMAGE_JXL, "jxl", "JPEG XL Image File"}, + + {MESSAGE_RFC822, "eml", "RFC-822 data"}, + {TEXT_PLAIN, "txt,text", "Text File"}, + {APPLICATION_JSON, "json", "JavaScript Object Notation"}, + {TEXT_VTT, "vtt", "Web Video Text Tracks"}, + {TEXT_CACHE_MANIFEST, "appcache", "Application Cache Manifest"}, + {TEXT_HTML, "html,htm,shtml,ehtml", "HyperText Markup Language"}, + {"application/xhtml+xml", "xhtml,xht", + "Extensible HyperText Markup Language"}, + {APPLICATION_MATHML_XML, "mml", "Mathematical Markup Language"}, + {APPLICATION_RDF, "rdf", "Resource Description Framework"}, + {"text/csv", "csv", "CSV File"}, + {TEXT_XML, "xml,xsl,xbl", "Extensible Markup Language"}, + {TEXT_CSS, "css", "Style Sheet"}, + {TEXT_VCARD, "vcf,vcard", "Contact Information"}, + {TEXT_CALENDAR, "ics,ical,ifb,icalendar", "iCalendar"}, + {VIDEO_OGG, "ogv,ogg", "Ogg Video"}, + {APPLICATION_OGG, "ogg", "Ogg Video"}, + {AUDIO_OGG, "oga", "Ogg Audio"}, + {AUDIO_OGG, "opus", "Opus Audio"}, + {VIDEO_WEBM, "webm", "Web Media Video"}, + {AUDIO_WEBM, "webm", "Web Media Audio"}, + {AUDIO_MP3, "mp3,mpega,mp2", "MPEG Audio"}, + {VIDEO_MP4, "mp4,m4a,m4b", "MPEG-4 Video"}, + {AUDIO_MP4, "m4a,m4b", "MPEG-4 Audio"}, + {VIDEO_RAW, "yuv", "Raw YUV Video"}, + {AUDIO_WAV, "wav", "Waveform Audio"}, + {VIDEO_3GPP, "3gpp,3gp", "3GPP Video"}, + {VIDEO_3GPP2, "3g2", "3GPP2 Video"}, + {AUDIO_AAC, "aac", "AAC Audio"}, + {AUDIO_FLAC, "flac", "FLAC Audio"}, + {AUDIO_MIDI, "mid", "Standard MIDI Audio"}, + {APPLICATION_WASM, "wasm", "WebAssembly Module"}}; + +static const nsDefaultMimeTypeEntry sForbiddenPrimaryExtensions[] = { + {IMAGE_JPEG, "jfif"}}; + +/** + * File extensions for which decoding should be disabled. + * NOTE: These MUST be lower-case and ASCII. + */ +static const nsDefaultMimeTypeEntry nonDecodableExtensions[] = { + {APPLICATION_GZIP, "gz"}, + {APPLICATION_GZIP, "tgz"}, + {APPLICATION_ZIP, "zip"}, + {APPLICATION_COMPRESS, "z"}, + {APPLICATION_GZIP, "svgz"}}; + +/** + * Mimetypes for which we enforce using a known extension. + * + * In addition to this list, we do this for all audio/, video/ and + * image/ mimetypes. + */ +static const char* forcedExtensionMimetypes[] = { + APPLICATION_PDF, APPLICATION_OGG, APPLICATION_WASM, + TEXT_CALENDAR, TEXT_CSS, TEXT_VCARD}; + +/** + * Primary extensions of types whose descriptions should be overwritten. + * This extension is concatenated with "ExtHandlerDescription" to look up the + * description in unknownContentType.properties. + * NOTE: These MUST be lower-case and ASCII. + */ +static const char* descriptionOverwriteExtensions[] = { + "avif", "jxl", "pdf", "svg", "webp", "xml", +}; + +static StaticRefPtr sExtHelperAppSvcSingleton; + +/** + * In child processes, return an nsOSHelperAppServiceChild for remoting + * OS calls to the parent process. In the parent process itself, use + * nsOSHelperAppService. + */ +/* static */ +already_AddRefed +nsExternalHelperAppService::GetSingleton() { + if (!sExtHelperAppSvcSingleton) { + if (XRE_IsParentProcess()) { + sExtHelperAppSvcSingleton = new nsOSHelperAppService(); + } else { + sExtHelperAppSvcSingleton = new nsOSHelperAppServiceChild(); + } + ClearOnShutdown(&sExtHelperAppSvcSingleton); + } + + return do_AddRef(sExtHelperAppSvcSingleton); +} + +NS_IMPL_ISUPPORTS(nsExternalHelperAppService, nsIExternalHelperAppService, + nsPIExternalAppLauncher, nsIExternalProtocolService, + nsIMIMEService, nsIObserver, nsISupportsWeakReference) + +nsExternalHelperAppService::nsExternalHelperAppService() {} +nsresult nsExternalHelperAppService::Init() { + // Add an observer for profile change + nsCOMPtr obs = mozilla::services::GetObserverService(); + if (!obs) return NS_ERROR_FAILURE; + + nsresult rv = obs->AddObserver(this, "profile-before-change", true); + NS_ENSURE_SUCCESS(rv, rv); + return obs->AddObserver(this, "last-pb-context-exited", true); +} + +nsExternalHelperAppService::~nsExternalHelperAppService() {} + +nsresult nsExternalHelperAppService::DoContentContentProcessHelper( + const nsACString& aMimeContentType, nsIRequest* aRequest, + BrowsingContext* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + // We need to get a hold of a ContentChild so that we can begin forwarding + // this data to the parent. In the HTTP case, this is unfortunate, since + // we're actually passing data from parent->child->parent wastefully, but + // the Right Fix will eventually be to short-circuit those channels on the + // parent side based on some sort of subscription concept. + using mozilla::dom::ContentChild; + using mozilla::dom::ExternalHelperAppChild; + ContentChild* child = ContentChild::GetSingleton(); + if (!child) { + return NS_ERROR_FAILURE; + } + + nsCString disp; + nsCOMPtr uri; + int64_t contentLength = -1; + bool wasFileChannel = false; + uint32_t contentDisposition = -1; + nsAutoString fileName; + nsCOMPtr loadInfo; + + nsCOMPtr channel = do_QueryInterface(aRequest); + if (channel) { + channel->GetURI(getter_AddRefs(uri)); + channel->GetContentLength(&contentLength); + channel->GetContentDisposition(&contentDisposition); + channel->GetContentDispositionFilename(fileName); + channel->GetContentDispositionHeader(disp); + loadInfo = channel->LoadInfo(); + + nsCOMPtr fileChan(do_QueryInterface(aRequest)); + wasFileChannel = fileChan != nullptr; + } + + nsCOMPtr referrer; + NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer)); + + Maybe loadInfoArgs; + MOZ_ALWAYS_SUCCEEDS(LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs)); + + nsCOMPtr props(do_QueryInterface(aRequest)); + // Determine whether a new window was opened specifically for this request + bool shouldCloseWindow = false; + if (props) { + props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns, + &shouldCloseWindow); + } + + // Now we build a protocol for forwarding our data to the parent. The + // protocol will act as a listener on the child-side and create a "real" + // helperAppService listener on the parent-side, via another call to + // DoContent. + RefPtr childListener = new ExternalHelperAppChild(); + MOZ_ALWAYS_TRUE(child->SendPExternalHelperAppConstructor( + childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp, + contentDisposition, fileName, aForceSave, contentLength, wasFileChannel, + referrer, aContentContext, shouldCloseWindow)); + + NS_ADDREF(*aStreamListener = childListener); + + uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE; + + SanitizeFileName(fileName, 0); + + RefPtr handler = + new nsExternalAppHandler(nullptr, u""_ns, aContentContext, aWindowContext, + this, fileName, reason, aForceSave); + if (!handler) { + return NS_ERROR_OUT_OF_MEMORY; + } + + childListener->SetHandler(handler); + return NS_OK; +} + +NS_IMETHODIMP nsExternalHelperAppService::CreateListener( + const nsACString& aMimeContentType, nsIRequest* aRequest, + BrowsingContext* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + MOZ_ASSERT(!XRE_IsContentProcess()); + + nsAutoString fileName; + nsAutoCString fileExtension; + uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE; + + nsCOMPtr channel = do_QueryInterface(aRequest); + if (channel) { + uint32_t contentDisposition = -1; + channel->GetContentDisposition(&contentDisposition); + if (contentDisposition == nsIChannel::DISPOSITION_ATTACHMENT) { + reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST; + } + } + + *aStreamListener = nullptr; + + // Get the file extension and name that we will need later + nsCOMPtr uri; + bool allowURLExtension = + GetFileNameFromChannel(channel, fileName, getter_AddRefs(uri)); + + uint32_t flags = VALIDATE_ALLOW_EMPTY; + if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT, + nsCaseInsensitiveCStringComparator)) { + flags |= VALIDATE_GUESS_FROM_EXTENSION; + } + + nsCOMPtr mimeInfo = ValidateFileNameForSaving( + fileName, aMimeContentType, uri, nullptr, flags, allowURLExtension); + + LOG("Type/Ext lookup found 0x%p\n", mimeInfo.get()); + + // No mimeinfo -> we can't continue. probably OOM. + if (!mimeInfo) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (flags & VALIDATE_GUESS_FROM_EXTENSION) { + if (channel) { + // Replace the content type with what was guessed. + nsAutoCString mimeType; + mimeInfo->GetMIMEType(mimeType); + channel->SetContentType(mimeType); + } + + if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) { + reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED; + } + } + + nsAutoString extension; + int32_t dotidx = fileName.RFind(u"."); + if (dotidx != -1) { + extension = Substring(fileName, dotidx + 1); + } + + // NB: ExternalHelperAppParent depends on this listener always being an + // nsExternalAppHandler. If this changes, make sure to update that code. + nsExternalAppHandler* handler = new nsExternalAppHandler( + mimeInfo, extension, aContentContext, aWindowContext, this, fileName, + reason, aForceSave); + if (!handler) { + return NS_ERROR_OUT_OF_MEMORY; + } + + NS_ADDREF(*aStreamListener = handler); + return NS_OK; +} + +NS_IMETHODIMP nsExternalHelperAppService::DoContent( + const nsACString& aMimeContentType, nsIRequest* aRequest, + nsIInterfaceRequestor* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + // Scripted interface requestors cannot return an instance of the + // (non-scriptable) nsPIDOMWindowOuter or nsPIDOMWindowInner interfaces, so + // get to the window via `nsIDOMWindow`. Unfortunately, at that point we + // don't know whether the thing we got is an inner or outer window, so have to + // work with either one. + RefPtr bc; + nsCOMPtr domWindow = do_GetInterface(aContentContext); + if (nsCOMPtr outerWindow = do_QueryInterface(domWindow)) { + bc = outerWindow->GetBrowsingContext(); + } else if (nsCOMPtr innerWindow = + do_QueryInterface(domWindow)) { + bc = innerWindow->GetBrowsingContext(); + } + + if (XRE_IsContentProcess()) { + return DoContentContentProcessHelper(aMimeContentType, aRequest, bc, + aForceSave, aWindowContext, + aStreamListener); + } + + nsresult rv = CreateListener(aMimeContentType, aRequest, bc, aForceSave, + aWindowContext, aStreamListener); + return rv; +} + +NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension( + const nsACString& aExtension, const nsACString& aEncodingType, + bool* aApplyDecoding) { + *aApplyDecoding = true; + uint32_t i; + for (i = 0; i < ArrayLength(nonDecodableExtensions); ++i) { + if (aExtension.LowerCaseEqualsASCII( + nonDecodableExtensions[i].mFileExtension) && + aEncodingType.LowerCaseEqualsASCII( + nonDecodableExtensions[i].mMimeType)) { + *aApplyDecoding = false; + break; + } + } + return NS_OK; +} + +nsresult nsExternalHelperAppService::GetFileTokenForPath( + const char16_t* aPlatformAppPath, nsIFile** aFile) { + nsDependentString platformAppPath(aPlatformAppPath); + // First, check if we have an absolute path + nsIFile* localFile = nullptr; + nsresult rv = NS_NewLocalFile(platformAppPath, true, &localFile); + if (NS_SUCCEEDED(rv)) { + *aFile = localFile; + bool exists; + if (NS_FAILED((*aFile)->Exists(&exists)) || !exists) { + NS_RELEASE(*aFile); + return NS_ERROR_FILE_NOT_FOUND; + } + return NS_OK; + } + + // Second, check if file exists in mozilla program directory + rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, aFile); + if (NS_SUCCEEDED(rv)) { + rv = (*aFile)->Append(platformAppPath); + if (NS_SUCCEEDED(rv)) { + bool exists = false; + rv = (*aFile)->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) return NS_OK; + } + NS_RELEASE(*aFile); + } + + return NS_ERROR_NOT_AVAILABLE; +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// begin external protocol service default implementation... +////////////////////////////////////////////////////////////////////////////////////////////////////// +NS_IMETHODIMP nsExternalHelperAppService::ExternalProtocolHandlerExists( + const char* aProtocolScheme, bool* aHandlerExists) { + nsCOMPtr handlerInfo; + nsresult rv = GetProtocolHandlerInfo(nsDependentCString(aProtocolScheme), + getter_AddRefs(handlerInfo)); + if (NS_SUCCEEDED(rv)) { + // See if we have any known possible handler apps for this + nsCOMPtr possibleHandlers; + handlerInfo->GetPossibleApplicationHandlers( + getter_AddRefs(possibleHandlers)); + + uint32_t length; + possibleHandlers->GetLength(&length); + if (length) { + *aHandlerExists = true; + return NS_OK; + } + } + + // if not, fall back on an os-based handler + return OSProtocolHandlerExists(aProtocolScheme, aHandlerExists); +} + +NS_IMETHODIMP nsExternalHelperAppService::IsExposedProtocol( + const char* aProtocolScheme, bool* aResult) { + // check the per protocol setting first. it always takes precedence. + // if not set, then use the global setting. + + nsAutoCString prefName("network.protocol-handler.expose."); + prefName += aProtocolScheme; + bool val; + if (NS_SUCCEEDED(Preferences::GetBool(prefName.get(), &val))) { + *aResult = val; + return NS_OK; + } + + // by default, no protocol is exposed. i.e., by default all link clicks must + // go through the external protocol service. most applications override this + // default behavior. + *aResult = Preferences::GetBool("network.protocol-handler.expose-all", false); + + return NS_OK; +} + +static const char kExternalProtocolPrefPrefix[] = + "network.protocol-handler.external."; +static const char kExternalProtocolDefaultPref[] = + "network.protocol-handler.external-default"; + +// static +nsresult nsExternalHelperAppService::EscapeURI(nsIURI* aURI, nsIURI** aResult) { + MOZ_ASSERT(aURI); + MOZ_ASSERT(aResult); + + nsAutoCString spec; + aURI->GetSpec(spec); + + if (spec.Find("%00") != -1) return NS_ERROR_MALFORMED_URI; + + nsAutoCString escapedSpec; + nsresult rv = NS_EscapeURL(spec, esc_AlwaysCopy | esc_ExtHandler, escapedSpec, + fallible); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr ios(do_GetIOService()); + return ios->NewURI(escapedSpec, nullptr, nullptr, aResult); +} + +bool ExternalProtocolIsBlockedBySandbox( + BrowsingContext* aBrowsingContext, + const bool aHasValidUserGestureActivation) { + if (!StaticPrefs::dom_block_external_protocol_navigation_from_sandbox()) { + return false; + } + + if (!aBrowsingContext || aBrowsingContext->IsTop()) { + return false; + } + + uint32_t sandboxFlags = aBrowsingContext->GetSandboxFlags(); + + if (sandboxFlags == SANDBOXED_NONE) { + return false; + } + + if (!(sandboxFlags & SANDBOXED_AUXILIARY_NAVIGATION)) { + return false; + } + + if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION)) { + return false; + } + + if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_CUSTOM_PROTOCOLS)) { + return false; + } + + if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION) && + aHasValidUserGestureActivation) { + return false; + } + + return true; +} + +NS_IMETHODIMP +nsExternalHelperAppService::LoadURI(nsIURI* aURI, + nsIPrincipal* aTriggeringPrincipal, + nsIPrincipal* aRedirectPrincipal, + BrowsingContext* aBrowsingContext, + bool aTriggeredExternally, + bool aHasValidUserGestureActivation) { + NS_ENSURE_ARG_POINTER(aURI); + + if (XRE_IsContentProcess()) { + mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal( + aURI, aTriggeringPrincipal, aRedirectPrincipal, aBrowsingContext, + aTriggeredExternally, aHasValidUserGestureActivation); + return NS_OK; + } + + // Prevent sandboxed BrowsingContexts from navigating to external protocols. + // This only uses the sandbox flags of the target BrowsingContext of the + // load. The navigating document's CSP sandbox flags do not apply. + if (aBrowsingContext && + ExternalProtocolIsBlockedBySandbox(aBrowsingContext, + aHasValidUserGestureActivation)) { + // Log an error to the web console of the sandboxed BrowsingContext. + nsAutoString localizedMsg; + nsAutoCString spec; + aURI->GetSpec(spec); + + AutoTArray params = {NS_ConvertUTF8toUTF16(spec)}; + nsresult rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eSECURITY_PROPERTIES, "SandboxBlockedCustomProtocols", + params, localizedMsg); + NS_ENSURE_SUCCESS(rv, rv); + + // Log to the the parent window of the iframe. If there is no parent, fall + // back to the iframe window itself. + WindowContext* windowContext = aBrowsingContext->GetParentWindowContext(); + if (!windowContext) { + windowContext = aBrowsingContext->GetCurrentWindowContext(); + } + + // Skip logging if we still don't have a WindowContext. + NS_ENSURE_TRUE(windowContext, NS_ERROR_FAILURE); + + nsContentUtils::ReportToConsoleByWindowID( + localizedMsg, nsIScriptError::errorFlag, "Security"_ns, + windowContext->InnerWindowId(), + windowContext->Canonical()->GetDocumentURI()); + + return NS_OK; + } + + nsCOMPtr escapedURI; + nsresult rv = EscapeURI(aURI, getter_AddRefs(escapedURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString scheme; + escapedURI->GetScheme(scheme); + if (scheme.IsEmpty()) return NS_OK; // must have a scheme + + // Deny load if the prefs say to do so + nsAutoCString externalPref(kExternalProtocolPrefPrefix); + externalPref += scheme; + bool allowLoad = false; + if (NS_FAILED(Preferences::GetBool(externalPref.get(), &allowLoad))) { + // no scheme-specific value, check the default + if (NS_FAILED( + Preferences::GetBool(kExternalProtocolDefaultPref, &allowLoad))) { + return NS_OK; // missing default pref + } + } + + if (!allowLoad) { + return NS_OK; // explicitly denied + } + + // Now check if the principal is allowed to access the navigated context. + // We allow navigating subframes, even if not same-origin - non-external + // links can always navigate everywhere, so this is a minor additional + // restriction, only aiming to prevent some types of spoofing attacks + // from otherwise disjoint browsingcontext trees. + if (aBrowsingContext && aTriggeringPrincipal && + !StaticPrefs::security_allow_disjointed_external_uri_loads() && + // Add-on principals are always allowed: + !BasePrincipal::Cast(aTriggeringPrincipal)->AddonPolicy() && + // As is chrome code: + !aTriggeringPrincipal->IsSystemPrincipal()) { + RefPtr bc = aBrowsingContext; + WindowGlobalParent* wgp = bc->Canonical()->GetCurrentWindowGlobal(); + bool foundAccessibleFrame = false; + + // Also allow this load if the target is a toplevel BC and contains a + // non-web-controlled about:blank document + if (bc->IsTop() && !bc->HadOriginalOpener() && wgp) { + RefPtr uri = wgp->GetDocumentURI(); + foundAccessibleFrame = + uri && uri->GetSpecOrDefault().EqualsLiteral("about:blank"); + } + + while (!foundAccessibleFrame) { + if (wgp) { + foundAccessibleFrame = + aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal()); + } + // We have to get the parent via the bc, because there may not + // be a window global for the innermost bc; see bug 1650162. + BrowsingContext* parent = bc->GetParent(); + if (!parent) { + break; + } + bc = parent; + wgp = parent->Canonical()->GetCurrentWindowGlobal(); + } + + if (!foundAccessibleFrame) { + // See if this navigation could have come from a subframe. + nsTArray> contexts; + aBrowsingContext->GetAllBrowsingContextsInSubtree(contexts); + for (const auto& kid : contexts) { + wgp = kid->Canonical()->GetCurrentWindowGlobal(); + if (wgp && aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal())) { + foundAccessibleFrame = true; + break; + } + } + } + + if (!foundAccessibleFrame) { + return NS_OK; // deny the load. + } + } + + nsCOMPtr handler; + rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr chooser = + do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return chooser->HandleURI( + handler, escapedURI, + aRedirectPrincipal ? aRedirectPrincipal : aTriggeringPrincipal, + aBrowsingContext, aTriggeredExternally); +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// Methods related to deleting temporary files on exit +////////////////////////////////////////////////////////////////////////////////////////////////////// + +/* static */ +nsresult nsExternalHelperAppService::DeleteTemporaryFileHelper( + nsIFile* aTemporaryFile, nsCOMArray& aFileList) { + bool isFile = false; + + // as a safety measure, make sure the nsIFile is really a file and not a + // directory object. + aTemporaryFile->IsFile(&isFile); + if (!isFile) return NS_OK; + + aFileList.AppendObject(aTemporaryFile); + + return NS_OK; +} + +NS_IMETHODIMP +nsExternalHelperAppService::DeleteTemporaryFileOnExit(nsIFile* aTemporaryFile) { + return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryFilesList); +} + +NS_IMETHODIMP +nsExternalHelperAppService::DeleteTemporaryPrivateFileWhenPossible( + nsIFile* aTemporaryFile) { + return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryPrivateFilesList); +} + +void nsExternalHelperAppService::ExpungeTemporaryFilesHelper( + nsCOMArray& fileList) { + int32_t numEntries = fileList.Count(); + nsIFile* localFile; + for (int32_t index = 0; index < numEntries; index++) { + localFile = fileList[index]; + if (localFile) { + // First make the file writable, since the temp file is probably readonly. + localFile->SetPermissions(0600); + localFile->Remove(false); + } + } + + fileList.Clear(); +} + +void nsExternalHelperAppService::ExpungeTemporaryFiles() { + ExpungeTemporaryFilesHelper(mTemporaryFilesList); +} + +void nsExternalHelperAppService::ExpungeTemporaryPrivateFiles() { + ExpungeTemporaryFilesHelper(mTemporaryPrivateFilesList); +} + +static const char kExternalWarningPrefPrefix[] = + "network.protocol-handler.warn-external."; +static const char kExternalWarningDefaultPref[] = + "network.protocol-handler.warn-external-default"; + +NS_IMETHODIMP +nsExternalHelperAppService::GetProtocolHandlerInfo( + const nsACString& aScheme, nsIHandlerInfo** aHandlerInfo) { + // XXX enterprise customers should be able to turn this support off with a + // single master pref (maybe use one of the "exposed" prefs here?) + + bool exists; + nsresult rv = GetProtocolHandlerInfoFromOS(aScheme, &exists, aHandlerInfo); + if (NS_FAILED(rv)) { + // Either it knows nothing, or we ran out of memory + return NS_ERROR_FAILURE; + } + + nsCOMPtr handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + bool hasHandler = false; + (void)handlerSvc->Exists(*aHandlerInfo, &hasHandler); + if (hasHandler) { + rv = handlerSvc->FillHandlerInfo(*aHandlerInfo, ""_ns); + if (NS_SUCCEEDED(rv)) return NS_OK; + } + } + + return SetProtocolHandlerDefaults(*aHandlerInfo, exists); +} + +NS_IMETHODIMP +nsExternalHelperAppService::SetProtocolHandlerDefaults( + nsIHandlerInfo* aHandlerInfo, bool aOSHandlerExists) { + // this type isn't in our database, so we've only got an OS default handler, + // if one exists + + if (aOSHandlerExists) { + // we've got a default, so use it + aHandlerInfo->SetPreferredAction(nsIHandlerInfo::useSystemDefault); + + // whether or not to ask the user depends on the warning preference + nsAutoCString scheme; + aHandlerInfo->GetType(scheme); + + nsAutoCString warningPref(kExternalWarningPrefPrefix); + warningPref += scheme; + bool warn; + if (NS_FAILED(Preferences::GetBool(warningPref.get(), &warn))) { + // no scheme-specific value, check the default + warn = Preferences::GetBool(kExternalWarningDefaultPref, true); + } + aHandlerInfo->SetAlwaysAskBeforeHandling(warn); + } else { + // If no OS default existed, we set the preferred action to alwaysAsk. + // This really means not initialized (i.e. there's no available handler) + // to all the code... + aHandlerInfo->SetPreferredAction(nsIHandlerInfo::alwaysAsk); + } + + return NS_OK; +} + +// XPCOM profile change observer +NS_IMETHODIMP +nsExternalHelperAppService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* someData) { + if (!strcmp(aTopic, "profile-before-change")) { + ExpungeTemporaryFiles(); + } else if (!strcmp(aTopic, "last-pb-context-exited")) { + ExpungeTemporaryPrivateFiles(); + } + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// begin external app handler implementation +////////////////////////////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ADDREF(nsExternalAppHandler) +NS_IMPL_RELEASE(nsExternalAppHandler) + +NS_INTERFACE_MAP_BEGIN(nsExternalAppHandler) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIHelperAppLauncher) + NS_INTERFACE_MAP_ENTRY(nsICancelable) + NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver) + NS_INTERFACE_MAP_ENTRY(nsINamed) + NS_INTERFACE_MAP_ENTRY_CONCRETE(nsExternalAppHandler) +NS_INTERFACE_MAP_END + +nsExternalAppHandler::nsExternalAppHandler( + nsIMIMEInfo* aMIMEInfo, const nsAString& aFileExtension, + BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext, + nsExternalHelperAppService* aExtProtSvc, + const nsAString& aSuggestedFileName, uint32_t aReason, bool aForceSave) + : mMimeInfo(aMIMEInfo), + mBrowsingContext(aBrowsingContext), + mWindowContext(aWindowContext), + mSuggestedFileName(aSuggestedFileName), + mForceSave(aForceSave), + mForceSaveInternallyHandled(false), + mCanceled(false), + mStopRequestIssued(false), + mIsFileChannel(false), + mShouldCloseWindow(false), + mHandleInternally(false), + mDialogShowing(false), + mReason(aReason), + mTempFileIsExecutable(false), + mTimeDownloadStarted(0), + mContentLength(-1), + mProgress(0), + mSaver(nullptr), + mDialogProgressListener(nullptr), + mTransfer(nullptr), + mRequest(nullptr), + mExtProtSvc(aExtProtSvc) { + // make sure the extention includes the '.' + if (!aFileExtension.IsEmpty() && aFileExtension.First() != '.') { + mFileExtension = char16_t('.'); + } + mFileExtension.Append(aFileExtension); + + mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096); +} + +nsExternalAppHandler::~nsExternalAppHandler() { + MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted"); +} + +void nsExternalAppHandler::DidDivertRequest(nsIRequest* request) { + MOZ_ASSERT(XRE_IsContentProcess(), "in child process"); + // Remove our request from the child loadGroup + RetargetLoadNotifications(request); +} + +NS_IMETHODIMP nsExternalAppHandler::SetWebProgressListener( + nsIWebProgressListener2* aWebProgressListener) { + // This is always called by nsHelperDlg.js. Go ahead and register the + // progress listener. At this point, we don't have mTransfer. + mDialogProgressListener = aWebProgressListener; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetTargetFile(nsIFile** aTarget) { + if (mFinalFileDestination) + *aTarget = mFinalFileDestination; + else + *aTarget = mTempFile; + + NS_IF_ADDREF(*aTarget); + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetTargetFileIsExecutable(bool* aExec) { + // Use the real target if it's been set + if (mFinalFileDestination) return mFinalFileDestination->IsExecutable(aExec); + + // Otherwise, use the stored executable-ness of the temporary + *aExec = mTempFileIsExecutable; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetTimeDownloadStarted(PRTime* aTime) { + *aTime = mTimeDownloadStarted; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetContentLength(int64_t* aContentLength) { + *aContentLength = mContentLength; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetBrowsingContextId( + uint64_t* aBrowsingContextId) { + *aBrowsingContextId = mBrowsingContext ? mBrowsingContext->Id() : 0; + return NS_OK; +} + +void nsExternalAppHandler::RetargetLoadNotifications(nsIRequest* request) { + // we are going to run the downloading of the helper app in our own little + // docloader / load group context. so go ahead and force the creation of a + // load group and doc loader for us to use... + nsCOMPtr aChannel = do_QueryInterface(request); + if (!aChannel) return; + + bool isPrivate = NS_UsePrivateBrowsing(aChannel); + + nsCOMPtr oldLoadGroup; + aChannel->GetLoadGroup(getter_AddRefs(oldLoadGroup)); + + if (oldLoadGroup) { + oldLoadGroup->RemoveRequest(request, nullptr, NS_BINDING_RETARGETED); + } + + aChannel->SetLoadGroup(nullptr); + aChannel->SetNotificationCallbacks(nullptr); + + nsCOMPtr pbChannel = do_QueryInterface(aChannel); + if (pbChannel) { + pbChannel->SetPrivate(isPrivate); + } +} + +nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) { + // First we need to try to get the destination directory for the temporary + // file. + nsresult rv = GetDownloadDirectory(getter_AddRefs(mTempFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // At this point, we do not have a filename for the temp file. For security + // purposes, this cannot be predictable, so we must use a cryptographic + // quality PRNG to generate one. + nsAutoCString tempLeafName; + rv = GenerateRandomName(tempLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + // now append our extension. + nsAutoCString ext; + mMimeInfo->GetPrimaryExtension(ext); + if (!ext.IsEmpty()) { + ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + if (ext.First() != '.') tempLeafName.Append('.'); + tempLeafName.Append(ext); + } + + // We need to temporarily create a dummy file with the correct + // file extension to determine the executable-ness, so do this before adding + // the extra .part extension. + nsCOMPtr dummyFile; + rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dummyFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the file name without .part + rv = dummyFile->Append(NS_ConvertUTF8toUTF16(tempLeafName)); + NS_ENSURE_SUCCESS(rv, rv); + rv = dummyFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + NS_ENSURE_SUCCESS(rv, rv); + + // Store executable-ness then delete + dummyFile->IsExecutable(&mTempFileIsExecutable); + dummyFile->Remove(false); + + // Add an additional .part to prevent the OS from running this file in the + // default application. + tempLeafName.AppendLiteral(".part"); + + rv = mTempFile->Append(NS_ConvertUTF8toUTF16(tempLeafName)); + // make this file unique!!! + NS_ENSURE_SUCCESS(rv, rv); + rv = mTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + NS_ENSURE_SUCCESS(rv, rv); + + // Now save the temp leaf name, minus the ".part" bit, so we can use it later. + // This is a bit broken in the case when createUnique actually had to append + // some numbers, because then we now have a filename like foo.bar-1.part and + // we'll end up with foo.bar-1.bar as our final filename if we end up using + // this. But the other options are all bad too.... Ideally we'd have a way + // to tell createUnique to put its unique marker before the extension that + // comes before ".part" or something. + rv = mTempFile->GetLeafName(mTempLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(StringEndsWith(mTempLeafName, u".part"_ns), + NS_ERROR_UNEXPECTED); + + // Strip off the ".part" from mTempLeafName + mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1); + + MOZ_ASSERT(!mSaver, "Output file initialization called more than once!"); + mSaver = + do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mSaver->SetObserver(this); + if (NS_FAILED(rv)) { + mSaver = nullptr; + return rv; + } + + rv = mSaver->EnableSha256(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mSaver->EnableSignatureInfo(); + NS_ENSURE_SUCCESS(rv, rv); + LOG("Enabled hashing and signature verification"); + + rv = mSaver->SetTarget(mTempFile, false); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +void nsExternalAppHandler::MaybeApplyDecodingForExtension( + nsIRequest* aRequest) { + MOZ_ASSERT(aRequest); + + nsCOMPtr encChannel = do_QueryInterface(aRequest); + if (!encChannel) { + return; + } + + // Turn off content encoding conversions if needed + bool applyConversion = true; + + // First, check to see if conversion is already disabled. If so, we + // have nothing to do here. + encChannel->GetApplyConversion(&applyConversion); + if (!applyConversion) { + return; + } + + nsCOMPtr sourceURL(do_QueryInterface(mSourceUrl)); + if (sourceURL) { + nsAutoCString extension; + sourceURL->GetFileExtension(extension); + if (!extension.IsEmpty()) { + nsCOMPtr encEnum; + encChannel->GetContentEncodings(getter_AddRefs(encEnum)); + if (encEnum) { + bool hasMore; + nsresult rv = encEnum->HasMore(&hasMore); + if (NS_SUCCEEDED(rv) && hasMore) { + nsAutoCString encType; + rv = encEnum->GetNext(encType); + if (NS_SUCCEEDED(rv) && !encType.IsEmpty()) { + MOZ_ASSERT(mExtProtSvc); + mExtProtSvc->ApplyDecodingForExtension(extension, encType, + &applyConversion); + } + } + } + } + } + + encChannel->SetApplyConversion(applyConversion); +} + +already_AddRefed +nsExternalAppHandler::GetDialogParent() { + nsCOMPtr dialogParent = mWindowContext; + + if (!dialogParent && mBrowsingContext) { + dialogParent = do_QueryInterface(mBrowsingContext->GetDOMWindow()); + } + if (!dialogParent && mBrowsingContext && XRE_IsParentProcess()) { + RefPtr element = mBrowsingContext->Top()->GetEmbedderElement(); + if (element) { + dialogParent = do_QueryInterface(element->OwnerDoc()->GetWindow()); + } + } + return dialogParent.forget(); +} + +NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { + MOZ_ASSERT(request, "OnStartRequest without request?"); + + // Set mTimeDownloadStarted here as the download has already started and + // we want to record the start time before showing the filepicker. + mTimeDownloadStarted = PR_Now(); + + mRequest = request; + + nsCOMPtr aChannel = do_QueryInterface(request); + + nsresult rv; + nsAutoCString MIMEType; + if (mMimeInfo) { + mMimeInfo->GetMIMEType(MIMEType); + } + // Now get the URI + if (aChannel) { + aChannel->GetURI(getter_AddRefs(mSourceUrl)); + // HTTPS-Only/HTTPS-FirstMode tries to upgrade connections to https. Once + // the download is in progress we set that flag so that timeout counter + // measures do not kick in. + nsCOMPtr loadInfo = aChannel->LoadInfo(); + bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + if (nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(isPrivateWin) || + nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(isPrivateWin)) { + uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus(); + httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_DOWNLOAD_IN_PROGRESS; + loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus); + } + } + + if (!mForceSave && StaticPrefs::browser_download_enable_spam_prevention() && + IsDownloadSpam(aChannel)) { + return NS_OK; + } + + mDownloadClassification = + nsContentSecurityUtils::ClassifyDownload(aChannel, MIMEType); + + if (mDownloadClassification == nsITransfer::DOWNLOAD_FORBIDDEN) { + // If the download is rated as forbidden, + // cancel the request so no ui knows about this. + mCanceled = true; + request->Cancel(NS_ERROR_ABORT); + return NS_OK; + } + + nsCOMPtr fileChan(do_QueryInterface(request)); + mIsFileChannel = fileChan != nullptr; + if (!mIsFileChannel) { + // It's possible that this request came from the child process and the + // file channel actually lives there. If this returns true, then our + // mSourceUrl will be an nsIFileURL anyway. + nsCOMPtr parent( + do_QueryInterface(request)); + mIsFileChannel = parent && parent->WasFileChannel(); + } + + // Get content length + if (aChannel) { + aChannel->GetContentLength(&mContentLength); + } + + if (mBrowsingContext) { + mMaybeCloseWindowHelper = new MaybeCloseWindowHelper(mBrowsingContext); + mMaybeCloseWindowHelper->SetShouldCloseWindow(mShouldCloseWindow); + nsCOMPtr props(do_QueryInterface(request, &rv)); + // Determine whether a new window was opened specifically for this request + if (props) { + bool tmp = false; + if (NS_SUCCEEDED( + props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns, &tmp))) { + mMaybeCloseWindowHelper->SetShouldCloseWindow(tmp); + } + } + } + + // retarget all load notifications to our docloader instead of the original + // window's docloader... + RetargetLoadNotifications(request); + + // Close the underlying DOMWindow if it was opened specifically for the + // download. We don't run this in the content process, since we have + // an instance running in the parent as well, which will handle this + // if needed. + if (!XRE_IsContentProcess() && mMaybeCloseWindowHelper) { + mBrowsingContext = mMaybeCloseWindowHelper->MaybeCloseWindow(); + } + + // In an IPC setting, we're allowing the child process, here, to make + // decisions about decoding the channel (e.g. decompression). It will + // still forward the decoded (uncompressed) data back to the parent. + // Con: Uncompressed data means more IPC overhead. + // Pros: ExternalHelperAppParent doesn't need to implement nsIEncodedChannel. + // Parent process doesn't need to expect CPU time on decompression. + MaybeApplyDecodingForExtension(aChannel); + + // At this point, the child process has done everything it can usefully do + // for OnStartRequest. + if (XRE_IsContentProcess()) { + return NS_OK; + } + + rv = SetUpTempFile(aChannel); + if (NS_FAILED(rv)) { + nsresult transferError = rv; + + rv = CreateFailedTransfer(); + if (NS_FAILED(rv)) { + LOG("Failed to create transfer to report failure." + "Will fallback to prompter!"); + } + + mCanceled = true; + request->Cancel(transferError); + + nsAutoString path; + if (mTempFile) mTempFile->GetPath(path); + + SendStatusChange(kWriteError, transferError, request, path); + + return NS_OK; + } + + // Inform channel it is open on behalf of a download to throttle it during + // page loads and prevent its caching. + nsCOMPtr httpInternal = do_QueryInterface(aChannel); + if (httpInternal) { + rv = httpInternal->SetChannelIsForDownload(true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (mSourceUrl->SchemeIs("data")) { + // In case we're downloading a data:// uri + // we don't want to apply AllowTopLevelNavigationToDataURI. + nsCOMPtr loadInfo = aChannel->LoadInfo(); + loadInfo->SetForceAllowDataURI(true); + } + + // now that the temp file is set up, find out if we need to invoke a dialog + // asking the user what they want us to do with this content... + + // We can get here for three reasons: "can't handle", "sniffed type", or + // "server sent content-disposition:attachment". In the first case we want + // to honor the user's "always ask" pref; in the other two cases we want to + // honor it only if the default action is "save". Opening attachments in + // helper apps by default breaks some websites (especially if the attachment + // is one part of a multipart document). Opening sniffed content in helper + // apps by default introduces security holes that we'd rather not have. + + // So let's find out whether the user wants to be prompted. If he does not, + // check mReason and the preferred action to see what we should do. + + bool alwaysAsk = true; + mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk); + if (alwaysAsk) { + // But we *don't* ask if this mimeInfo didn't come from + // our user configuration datastore and the user has said + // at some point in the distant past that they don't + // want to be asked. The latter fact would have been + // stored in pref strings back in the old days. + + bool mimeTypeIsInDatastore = false; + nsCOMPtr handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + handlerSvc->Exists(mMimeInfo, &mimeTypeIsInDatastore); + } + if (!handlerSvc || !mimeTypeIsInDatastore) { + if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_SAVE_TO_DISK_PREF, + MIMEType.get())) { + // Don't need to ask after all. + alwaysAsk = false; + // Make sure action matches pref (save to disk). + mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } else if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_OPEN_FILE_PREF, + MIMEType.get())) { + // Don't need to ask after all. + alwaysAsk = false; + } + } + } else if (MIMEType.EqualsLiteral("text/plain")) { + nsAutoCString ext; + mMimeInfo->GetPrimaryExtension(ext); + // If people are sending us ApplicationReputation-eligible files with + // text/plain mimetypes, enforce asking the user what to do. + if (!ext.IsEmpty()) { + nsAutoCString dummyFileName("f"); + if (ext.First() != '.') { + dummyFileName.Append("."); + } + ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + nsCOMPtr appRep = + components::ApplicationReputation::Service(); + appRep->IsBinary(dummyFileName + ext, &alwaysAsk); + } + } + + int32_t action = nsIMIMEInfo::saveToDisk; + mMimeInfo->GetPreferredAction(&action); + + bool forcePrompt = mReason == nsIHelperAppLauncherDialog::REASON_TYPESNIFFED; + + // OK, now check why we're here + if (!alwaysAsk && forcePrompt) { + // Force asking if we're not saving. See comment back when we fetched the + // alwaysAsk boolean for details. + alwaysAsk = (action != nsIMIMEInfo::saveToDisk); + } + + bool shouldAutomaticallyHandleInternally = + action == nsIMIMEInfo::handleInternally; + + if (aChannel) { + uint32_t disposition = -1; + aChannel->GetContentDisposition(&disposition); + mForceSaveInternallyHandled = + shouldAutomaticallyHandleInternally && + disposition == nsIChannel::DISPOSITION_ATTACHMENT && + mozilla::StaticPrefs:: + browser_download_force_save_internally_handled_attachments(); + } + + // If we're not asking, check we actually know what to do: + if (!alwaysAsk) { + alwaysAsk = action != nsIMIMEInfo::saveToDisk && + action != nsIMIMEInfo::useHelperApp && + action != nsIMIMEInfo::useSystemDefault && + !shouldAutomaticallyHandleInternally; + } + + // If we're handling with the OS default and we are that default, force + // asking, so we don't end up in an infinite loop: + if (!alwaysAsk && action == nsIMIMEInfo::useSystemDefault) { + bool areOSDefault = false; + alwaysAsk = NS_SUCCEEDED(mMimeInfo->IsCurrentAppOSDefault(&areOSDefault)) && + areOSDefault; + } else if (!alwaysAsk && action == nsIMIMEInfo::useHelperApp) { + nsCOMPtr preferredApp; + mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(preferredApp)); + nsCOMPtr handlerApp = do_QueryInterface(preferredApp); + if (handlerApp) { + nsCOMPtr executable; + handlerApp->GetExecutable(getter_AddRefs(executable)); + nsCOMPtr ourselves; + if (executable && + // Despite the name, this really just fetches an nsIFile... + NS_SUCCEEDED(NS_GetSpecialDirectory(XRE_EXECUTABLE_FILE, + getter_AddRefs(ourselves)))) { + ourselves = nsMIMEInfoBase::GetCanonicalExecutable(ourselves); + executable = nsMIMEInfoBase::GetCanonicalExecutable(executable); + bool isSameApp = false; + alwaysAsk = + NS_FAILED(executable->Equals(ourselves, &isSameApp)) || isSameApp; + } + } + } + + // if we were told that we _must_ save to disk without asking, all the stuff + // before this is irrelevant; override it + if (mForceSave || mForceSaveInternallyHandled) { + alwaysAsk = false; + action = nsIMIMEInfo::saveToDisk; + shouldAutomaticallyHandleInternally = false; + } + // Additionally, if we are asked by the OS to open a local file, + // automatically downloading it to create a second copy of that file doesn't + // really make sense. We should ask the user what they want to do. + if (mSourceUrl->SchemeIs("file") && !alwaysAsk && + action == nsIMIMEInfo::saveToDisk) { + alwaysAsk = true; + } + + // If adding new checks, make sure this is the last check before telemetry + // and going ahead with opening the file! +#ifdef XP_WIN + /* We need to see whether the file we've got here could be + * executable. If it could, we had better not try to open it! + * We can skip this check, though, if we have a setting to open in a + * helper app. + */ + if (!alwaysAsk && action != nsIMIMEInfo::saveToDisk && + !shouldAutomaticallyHandleInternally) { + nsCOMPtr prefApp; + mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(prefApp)); + if (action != nsIMIMEInfo::useHelperApp || !prefApp) { + nsCOMPtr fileToTest; + GetTargetFile(getter_AddRefs(fileToTest)); + if (fileToTest) { + bool isExecutable; + rv = fileToTest->IsExecutable(&isExecutable); + if (NS_FAILED(rv) || mTempFileIsExecutable || + isExecutable) { // checking NS_FAILED, because paranoia is good + alwaysAsk = true; + } + } else { // Paranoia is good here too, though this really should not + // happen + NS_WARNING( + "GetDownloadInfo returned a null file after the temp file has been " + "set up! "); + alwaysAsk = true; + } + } + } +#endif + + if (alwaysAsk) { + // Display the dialog + mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // this will create a reference cycle (the dialog holds a reference to us as + // nsIHelperAppLauncher), which will be broken in Cancel or CreateTransfer. + nsCOMPtr dialogParent = GetDialogParent(); + // Don't pop up the downloads panel since we're already going to pop up the + // UCT dialog for basically the same effect. + mDialogShowing = true; + rv = mDialog->Show(this, dialogParent, mReason); + + // what do we do if the dialog failed? I guess we should call Cancel and + // abort the load.... + } else { + // We need to do the save/open immediately, then. + if (action == nsIMIMEInfo::useHelperApp || + action == nsIMIMEInfo::useSystemDefault || + shouldAutomaticallyHandleInternally) { + // Check if the file is local, in which case just launch it from where it + // is. Otherwise, set the file to launch once it's finished downloading. + rv = mIsFileChannel ? LaunchLocalFile() + : SetDownloadToLaunch( + shouldAutomaticallyHandleInternally, nullptr); + } else { + rv = PromptForSaveDestination(); + } + } + return NS_OK; +} + +bool nsExternalAppHandler::IsDownloadSpam(nsIChannel* aChannel) { + nsCOMPtr loadInfo = aChannel->LoadInfo(); + nsCOMPtr permissionManager = + mozilla::services::GetPermissionManager(); + nsCOMPtr principal = loadInfo->TriggeringPrincipal(); + bool exactHostMatch = false; + constexpr auto type = "automatic-download"_ns; + nsCOMPtr permission; + + permissionManager->GetPermissionObject(principal, type, exactHostMatch, + getter_AddRefs(permission)); + + if (permission) { + uint32_t capability; + permission->GetCapability(&capability); + if (capability == nsIPermissionManager::DENY_ACTION) { + mCanceled = true; + aChannel->Cancel(NS_ERROR_ABORT); + return true; + } + if (capability == nsIPermissionManager::ALLOW_ACTION) { + return false; + } + // If no action is set (i.e: null), we set PROMPT_ACTION by default, + // which will notify the Downloads UI to open the panel on the next request. + if (capability == nsIPermissionManager::PROMPT_ACTION) { + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + RefPtr browsingContext; + loadInfo->GetBrowsingContext(getter_AddRefs(browsingContext)); + + nsAutoCString cStringURI; + loadInfo->TriggeringPrincipal()->GetPrePath(cStringURI); + observerService->NotifyObservers( + browsingContext, "blocked-automatic-download", + NS_ConvertASCIItoUTF16(cStringURI.get()).get()); + // FIXME: In order to escape memory leaks, currently we cancel blocked + // downloads. This is temporary solution, because download data should be + // kept in order to restart the blocked download. + mCanceled = true; + aChannel->Cancel(NS_ERROR_ABORT); + // End cancel + return true; + } + } + if (!loadInfo->GetHasValidUserGestureActivation()) { + permissionManager->AddFromPrincipal( + principal, type, nsIPermissionManager::PROMPT_ACTION, + nsIPermissionManager::EXPIRE_NEVER, 0 /* expire time */); + } + + return false; +} + +// Convert error info into proper message text and send OnStatusChange +// notification to the dialog progress listener or nsITransfer implementation. +void nsExternalAppHandler::SendStatusChange(ErrorType type, nsresult rv, + nsIRequest* aRequest, + const nsString& path) { + const char* msgId = nullptr; + switch (rv) { + case NS_ERROR_OUT_OF_MEMORY: + // No memory + msgId = "noMemory"; + break; + + case NS_ERROR_FILE_NO_DEVICE_SPACE: + // Out of space on target volume. + msgId = "diskFull"; + break; + + case NS_ERROR_FILE_READ_ONLY: + // Attempt to write to read/only file. + msgId = "readOnly"; + break; + + case NS_ERROR_FILE_ACCESS_DENIED: + if (type == kWriteError) { + // Attempt to write without sufficient permissions. +#if defined(ANDROID) + // On Android this means the SD card is present but + // unavailable (read-only). + msgId = "SDAccessErrorCardReadOnly"; +#else + msgId = "accessError"; +#endif + } else { + msgId = "launchError"; + } + break; + + case NS_ERROR_FILE_NOT_FOUND: + case NS_ERROR_FILE_UNRECOGNIZED_PATH: + // Helper app not found, let's verify this happened on launch + if (type == kLaunchError) { + msgId = "helperAppNotFound"; + break; + } +#if defined(ANDROID) + else if (type == kWriteError) { + // On Android this means the SD card is missing (not in + // SD slot). + msgId = "SDAccessErrorCardMissing"; + break; + } +#endif + [[fallthrough]]; + + default: + // Generic read/write/launch error message. + switch (type) { + case kReadError: + msgId = "readError"; + break; + case kWriteError: + msgId = "writeError"; + break; + case kLaunchError: + msgId = "launchError"; + break; + } + break; + } + + MOZ_LOG( + nsExternalHelperAppService::sLog, LogLevel::Error, + ("Error: %s, type=%i, listener=0x%p, transfer=0x%p, rv=0x%08" PRIX32 "\n", + msgId, type, mDialogProgressListener.get(), mTransfer.get(), + static_cast(rv))); + + MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Error, + (" path='%s'\n", NS_ConvertUTF16toUTF8(path).get())); + + // Get properties file bundle and extract status string. + nsCOMPtr stringService = + mozilla::components::StringBundle::Service(); + if (stringService) { + nsCOMPtr bundle; + if (NS_SUCCEEDED(stringService->CreateBundle( + "chrome://global/locale/nsWebBrowserPersist.properties", + getter_AddRefs(bundle)))) { + nsAutoString msgText; + AutoTArray strings = {path}; + if (NS_SUCCEEDED(bundle->FormatStringFromName(msgId, strings, msgText))) { + if (mDialogProgressListener) { + // We have a listener, let it handle the error. + mDialogProgressListener->OnStatusChange( + nullptr, (type == kReadError) ? aRequest : nullptr, rv, + msgText.get()); + } else if (mTransfer) { + mTransfer->OnStatusChange(nullptr, + (type == kReadError) ? aRequest : nullptr, + rv, msgText.get()); + } else if (XRE_IsParentProcess()) { + // We don't have a listener. Simply show the alert ourselves. + nsCOMPtr dialogParent = GetDialogParent(); + nsresult qiRv; + nsCOMPtr prompter(do_GetInterface(dialogParent, &qiRv)); + nsAutoString title; + bundle->FormatStringFromName("title", strings, title); + + MOZ_LOG( + nsExternalHelperAppService::sLog, LogLevel::Debug, + ("mBrowsingContext=0x%p, prompter=0x%p, qi rv=0x%08" PRIX32 + ", title='%s', msg='%s'", + mBrowsingContext.get(), prompter.get(), + static_cast(qiRv), NS_ConvertUTF16toUTF8(title).get(), + NS_ConvertUTF16toUTF8(msgText).get())); + + // If we didn't have a prompter we will try and get a window + // instead, get it's docshell and use it to alert the user. + if (!prompter) { + nsCOMPtr window(do_GetInterface(dialogParent)); + if (!window || !window->GetDocShell()) { + return; + } + + prompter = do_GetInterface(window->GetDocShell(), &qiRv); + + MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Debug, + ("No prompter from mBrowsingContext, using DocShell, " + "window=0x%p, docShell=0x%p, " + "prompter=0x%p, qi rv=0x%08" PRIX32, + window.get(), window->GetDocShell(), prompter.get(), + static_cast(qiRv))); + + // If we still don't have a prompter, there's nothing else we + // can do so just return. + if (!prompter) { + MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Error, + ("No prompter from DocShell, no way to alert user")); + return; + } + } + + // We should always have a prompter at this point. + prompter->Alert(title.get(), msgText.get()); + } + } + } + } +} + +NS_IMETHODIMP +nsExternalAppHandler::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + nsresult rv = NS_OK; + // first, check to see if we've been canceled.... + if (mCanceled || !mSaver) { + // then go cancel our underlying channel too + return request->Cancel(NS_BINDING_ABORTED); + } + + // read the data out of the stream and write it to the temp file. + if (count > 0) { + mProgress += count; + + nsCOMPtr saver = do_QueryInterface(mSaver); + rv = saver->OnDataAvailable(request, inStr, sourceOffset, count); + if (NS_SUCCEEDED(rv)) { + // Send progress notification. + if (mTransfer) { + mTransfer->OnProgressChange64(nullptr, request, mProgress, + mContentLength, mProgress, + mContentLength); + } + } else { + // An error occurred, notify listener. + nsAutoString tempFilePath; + if (mTempFile) { + mTempFile->GetPath(tempFilePath); + } + SendStatusChange(kReadError, rv, request, tempFilePath); + + // Cancel the download. + Cancel(rv); + } + } + return rv; +} + +NS_IMETHODIMP nsExternalAppHandler::OnStopRequest(nsIRequest* request, + nsresult aStatus) { + LOG("nsExternalAppHandler::OnStopRequest\n" + " mCanceled=%d, mTransfer=0x%p, aStatus=0x%08" PRIX32 "\n", + mCanceled, mTransfer.get(), static_cast(aStatus)); + + mStopRequestIssued = true; + + // Cancel if the request did not complete successfully. + if (!mCanceled && NS_FAILED(aStatus)) { + // Send error notification. + nsAutoString tempFilePath; + if (mTempFile) mTempFile->GetPath(tempFilePath); + SendStatusChange(kReadError, aStatus, request, tempFilePath); + + Cancel(aStatus); + } + + // first, check to see if we've been canceled.... + if (mCanceled || !mSaver) { + return NS_OK; + } + + return mSaver->Finish(NS_OK); +} + +NS_IMETHODIMP +nsExternalAppHandler::OnTargetChange(nsIBackgroundFileSaver* aSaver, + nsIFile* aTarget) { + return NS_OK; +} + +NS_IMETHODIMP +nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver, + nsresult aStatus) { + LOG("nsExternalAppHandler::OnSaveComplete\n" + " aSaver=0x%p, aStatus=0x%08" PRIX32 ", mCanceled=%d, mTransfer=0x%p\n", + aSaver, static_cast(aStatus), mCanceled, mTransfer.get()); + + if (!mCanceled) { + // Save the hash and signature information + (void)mSaver->GetSha256Hash(mHash); + (void)mSaver->GetSignatureInfo(mSignatureInfo); + + // Free the reference that the saver keeps on us, even if we couldn't get + // the hash. + mSaver = nullptr; + + // Save the redirect information. + nsCOMPtr channel = do_QueryInterface(mRequest); + if (channel) { + nsCOMPtr loadInfo = channel->LoadInfo(); + nsresult rv = NS_OK; + nsCOMPtr redirectChain = + do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + LOG("nsExternalAppHandler: Got %zu redirects\n", + loadInfo->RedirectChain().Length()); + for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) { + redirectChain->AppendElement(entry); + } + mRedirects = redirectChain; + } + + if (NS_FAILED(aStatus)) { + nsAutoString path; + mTempFile->GetPath(path); + + // It may happen when e10s is enabled that there will be no transfer + // object available to communicate status as expected by the system. + // Let's try and create a temporary transfer object to take care of this + // for us, we'll fall back to using the prompt service if we absolutely + // have to. + if (!mTransfer) { + // We don't care if this fails. + CreateFailedTransfer(); + } + + SendStatusChange(kWriteError, aStatus, nullptr, path); + if (!mCanceled) Cancel(aStatus); + return NS_OK; + } + } + + // Notify the transfer object that we are done if the user has chosen an + // action. If the user hasn't chosen an action, the progress listener + // (nsITransfer) will be notified in CreateTransfer. + if (mTransfer) { + NotifyTransfer(aStatus); + } + + return NS_OK; +} + +void nsExternalAppHandler::NotifyTransfer(nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread(), "Must notify on main thread"); + MOZ_ASSERT(mTransfer, "We must have an nsITransfer"); + + LOG("Notifying progress listener"); + + if (NS_SUCCEEDED(aStatus)) { + (void)mTransfer->SetSha256Hash(mHash); + (void)mTransfer->SetSignatureInfo(mSignatureInfo); + (void)mTransfer->SetRedirects(mRedirects); + (void)mTransfer->OnProgressChange64( + nullptr, nullptr, mProgress, mContentLength, mProgress, mContentLength); + } + + (void)mTransfer->OnStateChange(nullptr, nullptr, + nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_REQUEST | + nsIWebProgressListener::STATE_IS_NETWORK, + aStatus); + + // This nsITransfer object holds a reference to us (we are its observer), so + // we need to release the reference to break a reference cycle (and therefore + // to prevent leaking). We do this even if the previous calls failed. + mTransfer = nullptr; +} + +NS_IMETHODIMP nsExternalAppHandler::GetMIMEInfo(nsIMIMEInfo** aMIMEInfo) { + *aMIMEInfo = mMimeInfo; + NS_ADDREF(*aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetSource(nsIURI** aSourceURI) { + NS_ENSURE_ARG(aSourceURI); + *aSourceURI = mSourceUrl; + NS_IF_ADDREF(*aSourceURI); + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetSuggestedFileName( + nsAString& aSuggestedFileName) { + aSuggestedFileName = mSuggestedFileName; + return NS_OK; +} + +nsresult nsExternalAppHandler::CreateTransfer() { + LOG("nsExternalAppHandler::CreateTransfer"); + + MOZ_ASSERT(NS_IsMainThread(), "Must create transfer on main thread"); + // We are back from the helper app dialog (where the user chooses to save or + // open), but we aren't done processing the load. in this case, throw up a + // progress dialog so the user can see what's going on. + // Also, release our reference to mDialog. We don't need it anymore, and we + // need to break the reference cycle. + mDialog = nullptr; + if (!mDialogProgressListener) { + NS_WARNING("The dialog should nullify the dialog progress listener"); + } + // In case of a non acceptable download, we need to cancel the request and + // pass a FailedTransfer for the Download UI. + if (mDownloadClassification != nsITransfer::DOWNLOAD_ACCEPTABLE) { + mCanceled = true; + mRequest->Cancel(NS_ERROR_ABORT); + if (mSaver) { + mSaver->Finish(NS_ERROR_ABORT); + mSaver = nullptr; + } + return CreateFailedTransfer(); + } + nsresult rv; + + // We must be able to create an nsITransfer object. If not, it doesn't matter + // much that we can't launch the helper application or save to disk. Work on + // a local copy rather than mTransfer until we know we succeeded, to make it + // clearer that this function is re-entrant. + nsCOMPtr transfer = + do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Initialize the download + nsCOMPtr target; + rv = NS_NewFileURI(getter_AddRefs(target), mFinalFileDestination); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr channel = do_QueryInterface(mRequest); + nsCOMPtr httpChannel = do_QueryInterface(mRequest); + nsCOMPtr referrerInfo = nullptr; + if (httpChannel) { + referrerInfo = httpChannel->GetReferrerInfo(); + } + + if (mBrowsingContext) { + rv = transfer->InitWithBrowsingContext( + mSourceUrl, target, u""_ns, mMimeInfo, mTimeDownloadStarted, mTempFile, + this, channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification, referrerInfo, !mDialogShowing, + mBrowsingContext, mHandleInternally, nullptr); + } else { + rv = transfer->Init(mSourceUrl, nullptr, target, u""_ns, mMimeInfo, + mTimeDownloadStarted, mTempFile, this, + channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification, referrerInfo, !mDialogShowing); + } + mDialogShowing = false; + + NS_ENSURE_SUCCESS(rv, rv); + + // If we were cancelled since creating the transfer, just return. It is + // always ok to return NS_OK if we are cancelled. Callers of this function + // must call Cancel if CreateTransfer fails, but there's no need to cancel + // twice. + if (mCanceled) { + return NS_OK; + } + rv = transfer->OnStateChange(nullptr, mRequest, + nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_REQUEST | + nsIWebProgressListener::STATE_IS_NETWORK, + NS_OK); + NS_ENSURE_SUCCESS(rv, rv); + + if (mCanceled) { + return NS_OK; + } + + mRequest = nullptr; + // Finally, save the transfer to mTransfer. + mTransfer = transfer; + transfer = nullptr; + + // While we were bringing up the progress dialog, we actually finished + // processing the url. If that's the case then mStopRequestIssued will be + // true and OnSaveComplete has been called. + if (mStopRequestIssued && !mSaver && mTransfer) { + NotifyTransfer(NS_OK); + } + + return rv; +} + +nsresult nsExternalAppHandler::CreateFailedTransfer() { + nsresult rv; + nsCOMPtr transfer = + do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // We won't pass the temp file to the transfer, so if we have one it needs to + // get deleted now. + if (mTempFile) { + if (mSaver) { + mSaver->Finish(NS_BINDING_ABORTED); + mSaver = nullptr; + } + mTempFile->Remove(false); + } + + nsCOMPtr pseudoTarget; + if (!mFinalFileDestination) { + // If we don't have a download directory we're kinda screwed but it's OK + // we'll still report the error via the prompter. + nsCOMPtr pseudoFile; + rv = GetDownloadDirectory(getter_AddRefs(pseudoFile), true); + NS_ENSURE_SUCCESS(rv, rv); + + // Append the default suggested filename. If the user restarts the transfer + // we will re-trigger a filename check anyway to ensure that it is unique. + rv = pseudoFile->Append(mSuggestedFileName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), pseudoFile); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Initialize the target, if present + rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), mFinalFileDestination); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr channel = do_QueryInterface(mRequest); + nsCOMPtr httpChannel = do_QueryInterface(mRequest); + nsCOMPtr referrerInfo = nullptr; + if (httpChannel) { + referrerInfo = httpChannel->GetReferrerInfo(); + } + + if (mBrowsingContext) { + rv = transfer->InitWithBrowsingContext( + mSourceUrl, pseudoTarget, u""_ns, mMimeInfo, mTimeDownloadStarted, + mTempFile, this, channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification, referrerInfo, true, mBrowsingContext, + mHandleInternally, httpChannel); + } else { + rv = transfer->Init(mSourceUrl, nullptr, pseudoTarget, u""_ns, mMimeInfo, + mTimeDownloadStarted, mTempFile, this, + channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification, referrerInfo, true); + } + NS_ENSURE_SUCCESS(rv, rv); + + // Our failed transfer is ready. + mTransfer = std::move(transfer); + + return NS_OK; +} + +nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile* aFile, + bool aDialogWasShown) { + if (aFile) { + if (aDialogWasShown) { + mDialogShowing = true; + } + ContinueSave(aFile); + } else { + Cancel(NS_BINDING_ABORTED); + } + + return NS_OK; +} + +void nsExternalAppHandler::RequestSaveDestination( + const nsString& aDefaultFile, const nsString& aFileExtension) { + // Display the dialog + // XXX Convert to use file picker? No, then embeddors could not do any sort of + // "AutoDownload" w/o showing a prompt + nsresult rv = NS_OK; + if (!mDialog) { + // Get helper app launcher dialog. + mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv); + if (rv != NS_OK) { + Cancel(NS_BINDING_ABORTED); + return; + } + } + + // we want to explicitly unescape aDefaultFile b4 passing into the dialog. we + // can't unescape it because the dialog is implemented by a JS component which + // doesn't have a window so no unescape routine is defined... + + // Now, be sure to keep |this| alive, and the dialog + // If we don't do this, users that close the helper app dialog while the file + // picker is up would cause Cancel() to be called, and the dialog would be + // released, which would release this object too, which would crash. + // See Bug 249143 + RefPtr kungFuDeathGrip(this); + nsCOMPtr dlg(mDialog); + nsCOMPtr dialogParent = GetDialogParent(); + + rv = dlg->PromptForSaveToFileAsync(this, dialogParent, aDefaultFile.get(), + aFileExtension.get(), mForceSave); + if (NS_FAILED(rv)) { + Cancel(NS_BINDING_ABORTED); + } +} + +// PromptForSaveDestination should only be called by the helper app dialog which +// allows the user to say launch with application or save to disk. +NS_IMETHODIMP nsExternalAppHandler::PromptForSaveDestination() { + if (mCanceled) return NS_OK; + + if (mForceSave || mForceSaveInternallyHandled) { + mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } + + if (mSuggestedFileName.IsEmpty()) { + RequestSaveDestination(mTempLeafName, mFileExtension); + } else { + nsAutoString fileExt; + int32_t pos = mSuggestedFileName.RFindChar('.'); + if (pos >= 0) { + mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos); + } + if (fileExt.IsEmpty()) { + fileExt = mFileExtension; + } + + RequestSaveDestination(mSuggestedFileName, fileExt); + } + + return NS_OK; +} +nsresult nsExternalAppHandler::ContinueSave(nsIFile* aNewFileLocation) { + if (mCanceled) return NS_OK; + + MOZ_ASSERT(aNewFileLocation, "Must be called with a non-null file"); + + int32_t action = nsIMIMEInfo::saveToDisk; + mMimeInfo->GetPreferredAction(&action); + mHandleInternally = action == nsIMIMEInfo::handleInternally; + + nsresult rv = NS_OK; + nsCOMPtr fileToUse = aNewFileLocation; + mFinalFileDestination = fileToUse; + + // Move what we have in the final directory, but append .part + // to it, to indicate that it's unfinished. Do not call SetTarget on the + // saver if we are done (Finish has been called) but OnSaverComplete has + // not been called. + if (mFinalFileDestination && mSaver && !mStopRequestIssued) { + nsCOMPtr movedFile; + mFinalFileDestination->Clone(getter_AddRefs(movedFile)); + if (movedFile) { + nsAutoCString randomChars; + rv = GenerateRandomName(randomChars); + if (NS_SUCCEEDED(rv)) { + // Get the leaf name, strip any extensions, then + // add random bytes, followed by the extensions and '.part'. + nsAutoString leafName; + mFinalFileDestination->GetLeafName(leafName); + auto nameWithoutExtensionLength = leafName.FindChar('.'); + nsAutoString extensions(u""); + if (nameWithoutExtensionLength == kNotFound) { + nameWithoutExtensionLength = leafName.Length(); + } else { + extensions = Substring(leafName, nameWithoutExtensionLength); + } + leafName.Truncate(nameWithoutExtensionLength); + + nsAutoString suffix = u"."_ns + NS_ConvertASCIItoUTF16(randomChars) + + extensions + u".part"_ns; +#ifdef XP_WIN + // Deal with MAX_PATH on Windows. Worth noting that the original + // path for mFinalFileDestination must be valid for us to get + // here: either SetDownloadToLaunch or the caller of + // SaveDestinationAvailable has called CreateUnique or similar + // to ensure both a unique name and one that isn't too long. + // The only issue is we're making it longer to get the part + // file path... + nsAutoString path; + mFinalFileDestination->GetPath(path); + CheckedInt fullPathLength = + CheckedInt(path.Length()) + 1 + randomChars.Length() + + ArrayLength(".part"); + if (!fullPathLength.isValid()) { + leafName.Truncate(); + } else if (fullPathLength.value() > MAX_PATH) { + int32_t leafNameRemaining = + (int32_t)leafName.Length() - (fullPathLength.value() - MAX_PATH); + leafName.Truncate(std::max(leafNameRemaining, 0)); + } +#endif + leafName.Append(suffix); + movedFile->SetLeafName(leafName); + + rv = mSaver->SetTarget(movedFile, true); + if (NS_FAILED(rv)) { + nsAutoString path; + mTempFile->GetPath(path); + SendStatusChange(kWriteError, rv, nullptr, path); + Cancel(rv); + return NS_OK; + } + + mTempFile = movedFile; + } + } + } + + // The helper app dialog has told us what to do and we have a final file + // destination. + rv = CreateTransfer(); + // If we fail to create the transfer, Cancel. + if (NS_FAILED(rv)) { + Cancel(rv); + return rv; + } + + return NS_OK; +} + +// SetDownloadToLaunch should only be called by the helper app dialog which +// allows the user to say launch with application or save to disk. +NS_IMETHODIMP nsExternalAppHandler::SetDownloadToLaunch( + bool aHandleInternally, nsIFile* aNewFileLocation) { + if (mCanceled) return NS_OK; + + mHandleInternally = aHandleInternally; + + // Now that the user has elected to launch the downloaded file with a helper + // app, we're justified in removing the 'salted' name. We'll rename to what + // was specified in mSuggestedFileName after the download is done prior to + // launching the helper app. So that any existing file of that name won't be + // overwritten we call CreateUnique(). Also note that we use the same + // directory as originally downloaded so the download can be renamed in place + // later. + nsCOMPtr fileToUse; + if (aNewFileLocation) { + fileToUse = aNewFileLocation; + } else { + (void)GetDownloadDirectory(getter_AddRefs(fileToUse)); + + if (mSuggestedFileName.IsEmpty()) { + // Keep using the leafname of the temp file, since we're just starting a + // helper + mSuggestedFileName = mTempLeafName; + } + +#ifdef XP_WIN + // Ensure we don't double-append the file extension if it matches: + if (StringEndsWith(mSuggestedFileName, mFileExtension, + nsCaseInsensitiveStringComparator)) { + fileToUse->Append(mSuggestedFileName); + } else { + fileToUse->Append(mSuggestedFileName + mFileExtension); + } +#else + fileToUse->Append(mSuggestedFileName); +#endif + } + + nsresult rv = fileToUse->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + if (NS_SUCCEEDED(rv)) { + mFinalFileDestination = fileToUse; + // launch the progress window now that the user has picked the desired + // action. + rv = CreateTransfer(); + if (NS_FAILED(rv)) { + Cancel(rv); + } + } else { + // Cancel the download and report an error. We do not want to end up in + // a state where it appears that we have a normal download that is + // pointing to a file that we did not actually create. + nsAutoString path; + mTempFile->GetPath(path); + SendStatusChange(kWriteError, rv, nullptr, path); + Cancel(rv); + } + return rv; +} + +nsresult nsExternalAppHandler::LaunchLocalFile() { + nsCOMPtr fileUrl(do_QueryInterface(mSourceUrl)); + if (!fileUrl) { + return NS_OK; + } + Cancel(NS_BINDING_ABORTED); + nsCOMPtr file; + nsresult rv = fileUrl->GetFile(getter_AddRefs(file)); + + if (NS_SUCCEEDED(rv)) { + rv = mMimeInfo->LaunchWithFile(file); + if (NS_SUCCEEDED(rv)) return NS_OK; + } + nsAutoString path; + if (file) file->GetPath(path); + // If we get here, an error happened + SendStatusChange(kLaunchError, rv, nullptr, path); + return rv; +} + +NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) { + NS_ENSURE_ARG(NS_FAILED(aReason)); + + if (mCanceled) { + return NS_OK; + } + mCanceled = true; + + if (mSaver) { + // We are still writing to the target file. Give the saver a chance to + // close the target file, then notify the transfer object if necessary in + // the OnSaveComplete callback. + mSaver->Finish(aReason); + mSaver = nullptr; + } else { + if (mStopRequestIssued && mTempFile) { + // This branch can only happen when the user cancels the helper app dialog + // when the request has completed. The temp file has to be removed here, + // because mSaver has been released at that time with the temp file left. + (void)mTempFile->Remove(false); + } + + // Notify the transfer object that the download has been canceled, if the + // user has already chosen an action and we didn't notify already. + if (mTransfer) { + NotifyTransfer(aReason); + } + } + + // Break our reference cycle with the helper app dialog (set up in + // OnStartRequest) + mDialog = nullptr; + mDialogShowing = false; + + mRequest = nullptr; + + // Release the listener, to break the reference cycle with it (we are the + // observer of the listener). + mDialogProgressListener = nullptr; + + return NS_OK; +} + +bool nsExternalAppHandler::GetNeverAskFlagFromPref(const char* prefName, + const char* aContentType) { + // Search the obsolete pref strings. + nsAutoCString prefCString; + Preferences::GetCString(prefName, prefCString); + if (prefCString.IsEmpty()) { + // Default is true, if not found in the pref string. + return true; + } + + NS_UnescapeURL(prefCString); + nsACString::const_iterator start, end; + prefCString.BeginReading(start); + prefCString.EndReading(end); + return !CaseInsensitiveFindInReadable(nsDependentCString(aContentType), start, + end); +} + +NS_IMETHODIMP +nsExternalAppHandler::GetName(nsACString& aName) { + aName.AssignLiteral("nsExternalAppHandler"); + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// The following section contains our nsIMIMEService implementation and related +// methods. +// +////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// nsIMIMEService methods +NS_IMETHODIMP nsExternalHelperAppService::GetFromTypeAndExtension( + const nsACString& aMIMEType, const nsACString& aFileExt, + nsIMIMEInfo** _retval) { + MOZ_ASSERT(!aMIMEType.IsEmpty() || !aFileExt.IsEmpty(), + "Give me something to work with"); + MOZ_DIAGNOSTIC_ASSERT(aFileExt.FindChar('\0') == kNotFound, + "The extension should never contain null characters"); + LOG("Getting mimeinfo from type '%s' ext '%s'\n", + PromiseFlatCString(aMIMEType).get(), PromiseFlatCString(aFileExt).get()); + + *_retval = nullptr; + + // OK... we need a type. Get one. + nsAutoCString typeToUse(aMIMEType); + if (typeToUse.IsEmpty()) { + nsresult rv = GetTypeFromExtension(aFileExt, typeToUse); + if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE; + } + + // We promise to only send lower case mime types to the OS + ToLowerCase(typeToUse); + + // First, ask the OS for a mime info + bool found; + nsresult rv = GetMIMEInfoFromOS(typeToUse, aFileExt, &found, _retval); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + LOG("OS gave back 0x%p - found: %i\n", *_retval, found); + // If we got no mimeinfo, something went wrong. Probably lack of memory. + if (!*_retval) return NS_ERROR_OUT_OF_MEMORY; + + // The handler service can make up for bad mime types by checking the file + // extension. If the mime type is known (in extras or in the handler + // service), we stop it doing so by flipping this bool to true. + bool trustMIMEType = false; + + // Check extras - not everything we support will be known by the OS store, + // unfortunately, and it may even miss some extensions that we know should + // be accepted. We only do this for non-octet-stream mimetypes, because + // our information for octet-stream would lead to us trying to open all such + // files as Binary file with exe, com or bin extension regardless of the + // real extension. + if (!typeToUse.Equals(APPLICATION_OCTET_STREAM, + nsCaseInsensitiveCStringComparator)) { + rv = FillMIMEInfoForMimeTypeFromExtras(typeToUse, !found, *_retval); + LOG("Searched extras (by type), rv 0x%08" PRIX32 "\n", + static_cast(rv)); + trustMIMEType = NS_SUCCEEDED(rv); + found = found || NS_SUCCEEDED(rv); + } + + // Now, let's see if we can find something in our datastore. + // This will not overwrite the OS information that interests us + // (i.e. default application, default app. description) + nsCOMPtr handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + bool hasHandler = false; + (void)handlerSvc->Exists(*_retval, &hasHandler); + if (hasHandler) { + rv = handlerSvc->FillHandlerInfo(*_retval, ""_ns); + LOG("Data source: Via type: retval 0x%08" PRIx32 "\n", + static_cast(rv)); + trustMIMEType = trustMIMEType || NS_SUCCEEDED(rv); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + found = found || NS_SUCCEEDED(rv); + } + + // If we still haven't found anything, try finding a match for + // an extension in extras first: + if (!found && !aFileExt.IsEmpty()) { + rv = FillMIMEInfoForExtensionFromExtras(aFileExt, *_retval); + LOG("Searched extras (by ext), rv 0x%08" PRIX32 "\n", + static_cast(rv)); + } + + // Then check the handler service - but only do so if we really do not know + // the mimetype. This avoids overwriting good mimetype info with bad file + // extension info. + if ((!found || !trustMIMEType) && handlerSvc && !aFileExt.IsEmpty()) { + nsAutoCString overrideType; + rv = handlerSvc->GetTypeFromExtension(aFileExt, overrideType); + if (NS_SUCCEEDED(rv) && !overrideType.IsEmpty()) { + // We can't check handlerSvc->Exists() here, because we have a + // overideType. That's ok, it just results in some console noise. + // (If there's no handler for the override type, it throws) + rv = handlerSvc->FillHandlerInfo(*_retval, overrideType); + LOG("Data source: Via ext: retval 0x%08" PRIx32 "\n", + static_cast(rv)); + found = found || NS_SUCCEEDED(rv); + } + } + + // If we still don't have a match, at least set the file description + // to `${aFileExt} File` if it's empty: + if (!found && !aFileExt.IsEmpty()) { + // XXXzpao This should probably be localized + nsAutoCString desc(aFileExt); + desc.AppendLiteral(" File"); + (*_retval)->SetDescription(NS_ConvertUTF8toUTF16(desc)); + LOG("Falling back to 'File' file description\n"); + } + + // Sometimes, OSes give us bad data. We have a set of forbidden extensions + // for some MIME types. If the primary extension is forbidden, + // overwrite it with a known-good one. See bug 1571247 for context. + nsAutoCString primaryExtension; + (*_retval)->GetPrimaryExtension(primaryExtension); + if (!primaryExtension.EqualsIgnoreCase(PromiseFlatCString(aFileExt).get())) { + if (MaybeReplacePrimaryExtension(primaryExtension, *_retval)) { + (*_retval)->GetPrimaryExtension(primaryExtension); + } + } + + // Finally, check if we got a file extension and if yes, if it is an + // extension on the mimeinfo, in which case we want it to be the primary one + if (!aFileExt.IsEmpty()) { + bool matches = false; + (*_retval)->ExtensionExists(aFileExt, &matches); + LOG("Extension '%s' matches mime info: %i\n", + PromiseFlatCString(aFileExt).get(), matches); + if (matches) { + nsAutoCString fileExt; + ToLowerCase(aFileExt, fileExt); + (*_retval)->SetPrimaryExtension(fileExt); + primaryExtension = fileExt; + } + } + + // Overwrite with a generic description if the primary extension for the + // type is in our list; these are file formats supported by Firefox and + // we don't want other brands positioning themselves as the sole viewer + // for a system. + if (!primaryExtension.IsEmpty()) { + for (const char* ext : descriptionOverwriteExtensions) { + if (primaryExtension.Equals(ext)) { + nsCOMPtr bundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr unknownContentTypeBundle; + rv = bundleService->CreateBundle( + "chrome://mozapps/locale/downloads/unknownContentType.properties", + getter_AddRefs(unknownContentTypeBundle)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString stringName(ext); + stringName.AppendLiteral("ExtHandlerDescription"); + nsAutoString handlerDescription; + rv = unknownContentTypeBundle->GetStringFromName(stringName.get(), + handlerDescription); + if (NS_SUCCEEDED(rv)) { + (*_retval)->SetDescription(handlerDescription); + } + } + break; + } + } + } + + if (LOG_ENABLED()) { + nsAutoCString type; + (*_retval)->GetMIMEType(type); + + LOG("MIME Info Summary: Type '%s', Primary Ext '%s'\n", type.get(), + primaryExtension.get()); + } + + return NS_OK; +} + +bool nsExternalHelperAppService::GetMIMETypeFromDefaultForExtension( + const nsACString& aExtension, nsACString& aMIMEType) { + // First of all, check our default entries + for (auto& entry : defaultMimeEntries) { + if (aExtension.LowerCaseEqualsASCII(entry.mFileExtension)) { + aMIMEType = entry.mMimeType; + return true; + } + } + return false; +} + +NS_IMETHODIMP +nsExternalHelperAppService::GetTypeFromExtension(const nsACString& aFileExt, + nsACString& aContentType) { + // OK. We want to try the following sources of mimetype information, in this + // order: + // 1. defaultMimeEntries array + // 2. OS-provided information + // 3. our "extras" array + // 4. Information from plugins + // 5. The "ext-to-type-mapping" category + // Note that, we are intentionally not looking at the handler service, because + // that can be affected by websites, which leads to undesired behavior. + + // Early return if called with an empty extension parameter + if (aFileExt.IsEmpty()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // First of all, check our default entries + if (GetMIMETypeFromDefaultForExtension(aFileExt, aContentType)) { + return NS_OK; + } + + // Ask OS. + if (GetMIMETypeFromOSForExtension(aFileExt, aContentType)) { + return NS_OK; + } + + // Check extras array. + bool found = GetTypeFromExtras(aFileExt, aContentType); + if (found) { + return NS_OK; + } + + // Let's see if an extension added something + nsCOMPtr catMan( + do_GetService("@mozilla.org/categorymanager;1")); + if (catMan) { + // The extension in the category entry is always stored as lowercase + nsAutoCString lowercaseFileExt(aFileExt); + ToLowerCase(lowercaseFileExt); + // Read the MIME type from the category entry, if available + nsCString type; + nsresult rv = + catMan->GetCategoryEntry("ext-to-type-mapping", lowercaseFileExt, type); + if (NS_SUCCEEDED(rv)) { + aContentType = type; + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExternalHelperAppService::GetPrimaryExtension( + const nsACString& aMIMEType, const nsACString& aFileExt, + nsACString& _retval) { + NS_ENSURE_ARG(!aMIMEType.IsEmpty()); + + nsCOMPtr mi; + nsresult rv = + GetFromTypeAndExtension(aMIMEType, aFileExt, getter_AddRefs(mi)); + if (NS_FAILED(rv)) return rv; + + return mi->GetPrimaryExtension(_retval); +} + +NS_IMETHODIMP nsExternalHelperAppService::GetDefaultTypeFromURI( + nsIURI* aURI, nsACString& aContentType) { + NS_ENSURE_ARG_POINTER(aURI); + nsresult rv = NS_ERROR_NOT_AVAILABLE; + aContentType.Truncate(); + + // Now try to get an nsIURL so we don't have to do our own parsing + nsCOMPtr url = do_QueryInterface(aURI); + if (!url) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsAutoCString ext; + rv = url->GetFileExtension(ext); + if (NS_FAILED(rv)) { + return rv; + } + + if (!ext.IsEmpty()) { + UnescapeFragment(ext, url, ext); + + if (GetMIMETypeFromDefaultForExtension(ext, aContentType)) { + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +}; + +NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromURI( + nsIURI* aURI, nsACString& aContentType) { + NS_ENSURE_ARG_POINTER(aURI); + nsresult rv = NS_ERROR_NOT_AVAILABLE; + aContentType.Truncate(); + + // First look for a file to use. If we have one, we just use that. + nsCOMPtr fileUrl = do_QueryInterface(aURI); + if (fileUrl) { + nsCOMPtr file; + rv = fileUrl->GetFile(getter_AddRefs(file)); + if (NS_SUCCEEDED(rv)) { + rv = GetTypeFromFile(file, aContentType); + if (NS_SUCCEEDED(rv)) { + // we got something! + return rv; + } + } + } + + // Now try to get an nsIURL so we don't have to do our own parsing + nsCOMPtr url = do_QueryInterface(aURI); + if (url) { + nsAutoCString ext; + rv = url->GetFileExtension(ext); + if (NS_FAILED(rv)) return rv; + if (ext.IsEmpty()) return NS_ERROR_NOT_AVAILABLE; + + UnescapeFragment(ext, url, ext); + + return GetTypeFromExtension(ext, aContentType); + } + + // no url, let's give the raw spec a shot + nsAutoCString specStr; + rv = aURI->GetSpec(specStr); + if (NS_FAILED(rv)) return rv; + UnescapeFragment(specStr, aURI, specStr); + + // find the file extension (if any) + int32_t extLoc = specStr.RFindChar('.'); + int32_t specLength = specStr.Length(); + if (-1 != extLoc && extLoc != specLength - 1 && + // nothing over 20 chars long can be sanely considered an + // extension.... Dat dere would be just data. + specLength - extLoc < 20) { + return GetTypeFromExtension(Substring(specStr, extLoc + 1), aContentType); + } + + // We found no information; say so. + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromFile( + nsIFile* aFile, nsACString& aContentType) { + NS_ENSURE_ARG_POINTER(aFile); + nsresult rv; + + // Get the Extension + nsAutoString fileName; + rv = aFile->GetLeafName(fileName); + if (NS_FAILED(rv)) return rv; + + nsAutoCString fileExt; + if (!fileName.IsEmpty()) { + int32_t len = fileName.Length(); + for (int32_t i = len; i >= 0; i--) { + if (fileName[i] == char16_t('.')) { + CopyUTF16toUTF8(Substring(fileName, i + 1), fileExt); + break; + } + } + } + + if (fileExt.IsEmpty()) return NS_ERROR_FAILURE; + + return GetTypeFromExtension(fileExt, aContentType); +} + +nsresult nsExternalHelperAppService::FillMIMEInfoForMimeTypeFromExtras( + const nsACString& aContentType, bool aOverwriteDescription, + nsIMIMEInfo* aMIMEInfo) { + NS_ENSURE_ARG(aMIMEInfo); + + NS_ENSURE_ARG(!aContentType.IsEmpty()); + + // Look for default entry with matching mime type. + nsAutoCString MIMEType(aContentType); + ToLowerCase(MIMEType); + for (auto entry : extraMimeEntries) { + if (MIMEType.Equals(entry.mMimeType)) { + // This is the one. Set attributes appropriately. + nsDependentCString extensions(entry.mFileExtensions); + nsACString::const_iterator start, end; + extensions.BeginReading(start); + extensions.EndReading(end); + while (start != end) { + nsACString::const_iterator cursor = start; + mozilla::Unused << FindCharInReadable(',', cursor, end); + aMIMEInfo->AppendExtension(Substring(start, cursor)); + // If a comma was found, skip it for the next search. + start = cursor != end ? ++cursor : cursor; + } + + nsAutoString desc; + aMIMEInfo->GetDescription(desc); + if (aOverwriteDescription || desc.IsEmpty()) { + aMIMEInfo->SetDescription(NS_ConvertASCIItoUTF16(entry.mDescription)); + } + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsExternalHelperAppService::FillMIMEInfoForExtensionFromExtras( + const nsACString& aExtension, nsIMIMEInfo* aMIMEInfo) { + nsAutoCString type; + bool found = GetTypeFromExtras(aExtension, type); + if (!found) return NS_ERROR_NOT_AVAILABLE; + return FillMIMEInfoForMimeTypeFromExtras(type, true, aMIMEInfo); +} + +bool nsExternalHelperAppService::MaybeReplacePrimaryExtension( + const nsACString& aPrimaryExtension, nsIMIMEInfo* aMIMEInfo) { + for (const auto& entry : sForbiddenPrimaryExtensions) { + if (aPrimaryExtension.LowerCaseEqualsASCII(entry.mFileExtension)) { + nsDependentCString mime(entry.mMimeType); + for (const auto& extraEntry : extraMimeEntries) { + if (mime.LowerCaseEqualsASCII(extraEntry.mMimeType)) { + nsDependentCString goodExts(extraEntry.mFileExtensions); + int32_t commaPos = goodExts.FindChar(','); + commaPos = commaPos == kNotFound ? goodExts.Length() : commaPos; + auto goodExt = Substring(goodExts, 0, commaPos); + aMIMEInfo->SetPrimaryExtension(goodExt); + return true; + } + } + } + } + return false; +} + +bool nsExternalHelperAppService::GetTypeFromExtras(const nsACString& aExtension, + nsACString& aMIMEType) { + NS_ASSERTION(!aExtension.IsEmpty(), "Empty aExtension parameter!"); + + // Look for default entry with matching extension. + nsDependentCString::const_iterator start, end, iter; + int32_t numEntries = ArrayLength(extraMimeEntries); + for (int32_t index = 0; index < numEntries; index++) { + nsDependentCString extList(extraMimeEntries[index].mFileExtensions); + extList.BeginReading(start); + extList.EndReading(end); + iter = start; + while (start != end) { + FindCharInReadable(',', iter, end); + if (Substring(start, iter) + .Equals(aExtension, nsCaseInsensitiveCStringComparator)) { + aMIMEType = extraMimeEntries[index].mMimeType; + return true; + } + if (iter != end) { + ++iter; + } + start = iter; + } + } + + return false; +} + +bool nsExternalHelperAppService::GetMIMETypeFromOSForExtension( + const nsACString& aExtension, nsACString& aMIMEType) { + bool found = false; + nsCOMPtr mimeInfo; + nsresult rv = + GetMIMEInfoFromOS(""_ns, aExtension, &found, getter_AddRefs(mimeInfo)); + return NS_SUCCEEDED(rv) && found && mimeInfo && + NS_SUCCEEDED(mimeInfo->GetMIMEType(aMIMEType)); +} + +nsresult nsExternalHelperAppService::GetMIMEInfoFromOS( + const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + *aMIMEInfo = nullptr; + *aFound = false; + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsExternalHelperAppService::UpdateDefaultAppInfo( + nsIMIMEInfo* aMIMEInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +bool nsExternalHelperAppService::GetFileNameFromChannel(nsIChannel* aChannel, + nsAString& aFileName, + nsIURI** aURI) { + if (!aChannel) { + return false; + } + + aChannel->GetURI(aURI); + nsCOMPtr url = do_QueryInterface(*aURI); + + // Check if we have a POST request, in which case we don't want to use + // the url's extension + bool allowURLExt = !net::ChannelIsPost(aChannel); + + // Check if we had a query string - we don't want to check the URL + // extension if a query is present in the URI + // If we already know we don't want to check the URL extension, don't + // bother checking the query + if (url && allowURLExt) { + nsAutoCString query; + + // We only care about the query for HTTP and HTTPS URLs + if (url->SchemeIs("http") || url->SchemeIs("https")) { + url->GetQuery(query); + } + + // Only get the extension if the query is empty; if it isn't, then the + // extension likely belongs to a cgi script and isn't helpful + allowURLExt = query.IsEmpty(); + } + + aChannel->GetContentDispositionFilename(aFileName); + + return allowURLExt; +} + +NS_IMETHODIMP +nsExternalHelperAppService::GetValidFileName(nsIChannel* aChannel, + const nsACString& aType, + nsIURI* aOriginalURI, + uint32_t aFlags, + nsAString& aOutFileName) { + nsCOMPtr uri; + bool allowURLExtension = + GetFileNameFromChannel(aChannel, aOutFileName, getter_AddRefs(uri)); + + nsCOMPtr mimeInfo = ValidateFileNameForSaving( + aOutFileName, aType, uri, aOriginalURI, aFlags, allowURLExtension); + return NS_OK; +} + +NS_IMETHODIMP +nsExternalHelperAppService::ValidateFileNameForSaving( + const nsAString& aFileName, const nsACString& aType, uint32_t aFlags, + nsAString& aOutFileName) { + nsAutoString fileName(aFileName); + + // Just sanitize the filename only. + if (aFlags & VALIDATE_SANITIZE_ONLY) { + SanitizeFileName(fileName, aFlags); + } else { + nsCOMPtr mimeInfo = ValidateFileNameForSaving( + fileName, aType, nullptr, nullptr, aFlags, true); + } + + aOutFileName = fileName; + return NS_OK; +} + +already_AddRefed +nsExternalHelperAppService::ValidateFileNameForSaving( + nsAString& aFileName, const nsACString& aMimeType, nsIURI* aURI, + nsIURI* aOriginalURI, uint32_t aFlags, bool aAllowURLExtension) { + nsAutoString fileName(aFileName); + nsAutoCString extension; + nsCOMPtr mimeInfo; + + bool isBinaryType = aMimeType.EqualsLiteral(APPLICATION_OCTET_STREAM) || + aMimeType.EqualsLiteral(BINARY_OCTET_STREAM) || + aMimeType.EqualsLiteral("application/x-msdownload"); + + // We don't want to save hidden files starting with a dot, so remove any + // leading periods. This is done first, so that the remainder will be + // treated as the filename, and not an extension. + // Also, Windows ignores terminating dots. So we have to as well, so + // that our security checks do "the right thing" + fileName.Trim("."); + + bool urlIsFile = !!aURI && aURI->SchemeIs("file"); + + // We get the mime service here even though we're the default implementation + // of it, so it's possible to override only the mime service and not need to + // reimplement the whole external helper app service itself. + nsCOMPtr mimeService = do_GetService("@mozilla.org/mime;1"); + if (mimeService) { + if (fileName.IsEmpty()) { + nsCOMPtr url = do_QueryInterface(aURI); + // Try to extract the file name from the url and use that as a first + // pass as the leaf name of our temp file... + if (url) { + nsAutoCString leafName; + url->GetFileName(leafName); + if (!leafName.IsEmpty()) { + if (NS_FAILED(UnescapeFragment(leafName, url, fileName))) { + CopyUTF8toUTF16(leafName, fileName); // use escaped name instead + fileName.Trim("."); + } + } + + // Only get the extension from the URL if allowed, or if this + // is a binary type in which case the type might not be valid + // anyway. + if (aAllowURLExtension || isBinaryType || urlIsFile) { + url->GetFileExtension(extension); + } + } + } else { + // Determine the current extension for the filename. + int32_t dotidx = fileName.RFind(u"."); + if (dotidx != -1) { + CopyUTF16toUTF8(Substring(fileName, dotidx + 1), extension); + } + } + + if (aFlags & VALIDATE_GUESS_FROM_EXTENSION) { + nsAutoCString mimeType; + if (!extension.IsEmpty()) { + mimeService->GetFromTypeAndExtension(EmptyCString(), extension, + getter_AddRefs(mimeInfo)); + if (mimeInfo) { + mimeInfo->GetMIMEType(mimeType); + } + } + + if (mimeType.IsEmpty()) { + // Extension lookup gave us no useful match, so use octet-stream + // instead. + mimeService->GetFromTypeAndExtension( + nsLiteralCString(APPLICATION_OCTET_STREAM), extension, + getter_AddRefs(mimeInfo)); + } + } else if (!aMimeType.IsEmpty()) { + // If this is a binary type, include the extension as a hint to get + // the mime info. For other types, the mime type itself should be + // sufficient. + // Unfortunately, on Windows, the mimetype is usually insufficient. + // Compensate at least on `file` URLs by trusting the extension - + // that's likely what we used to get the mimetype in the first place. + // The special case for application/ogg is because that type could + // actually be used for a video which can better be determined by the + // extension. This is tested by browser_save_video.js. + bool useExtension = + isBinaryType || urlIsFile || aMimeType.EqualsLiteral(APPLICATION_OGG); + mimeService->GetFromTypeAndExtension( + aMimeType, useExtension ? extension : EmptyCString(), + getter_AddRefs(mimeInfo)); + if (mimeInfo) { + // But if no primary extension was returned, this mime type is probably + // an unknown type. Look it up again but this time supply the extension. + nsAutoCString primaryExtension; + mimeInfo->GetPrimaryExtension(primaryExtension); + if (primaryExtension.IsEmpty()) { + mimeService->GetFromTypeAndExtension(aMimeType, extension, + getter_AddRefs(mimeInfo)); + } + } + } + } + + // If an empty filename is allowed, then return early. It will be saved + // using the filename of the temporary file that was created for the download. + if (aFlags & VALIDATE_ALLOW_EMPTY && fileName.IsEmpty()) { + aFileName.Truncate(); + return mimeInfo.forget(); + } + + // This section modifies the extension on the filename if it isn't valid for + // the given content type. + if (mimeInfo) { + bool isValidExtension; + if (extension.IsEmpty() || + NS_FAILED(mimeInfo->ExtensionExists(extension, &isValidExtension)) || + !isValidExtension) { + // Skip these checks for text and binary, so we don't append the unneeded + // .txt or other extension. + if (aMimeType.EqualsLiteral(TEXT_PLAIN) || isBinaryType) { + extension.Truncate(); + } else { + nsAutoCString originalExtension(extension); + // If an original url was supplied, see if it has a valid extension. + bool useOldExtension = false; + if (aOriginalURI) { + nsCOMPtr originalURL(do_QueryInterface(aOriginalURI)); + if (originalURL) { + nsAutoCString uriExtension; + originalURL->GetFileExtension(uriExtension); + if (!uriExtension.IsEmpty()) { + mimeInfo->ExtensionExists(uriExtension, &useOldExtension); + if (useOldExtension) { + extension = uriExtension; + } + } + } + } + + if (!useOldExtension) { + // If the filename doesn't have a valid extension, or we don't know + // the extension, try to use the primary extension for the type. If we + // don't know the primary extension for the type, just continue with + // the existing extension, or leave the filename with no extension. + nsAutoCString primaryExtension; + mimeInfo->GetPrimaryExtension(primaryExtension); + if (!primaryExtension.IsEmpty()) { + extension = primaryExtension; + } + } + + // If an suitable extension was found, we will append to or replace the + // existing extension. + if (!extension.IsEmpty()) { + ModifyExtensionType modify = ShouldModifyExtension( + mimeInfo, aFlags & VALIDATE_FORCE_APPEND_EXTENSION, + originalExtension); + if (modify == ModifyExtension_Replace) { + int32_t dotidx = fileName.RFind(u"."); + if (dotidx != -1) { + // Remove the existing extension and replace it. + fileName.Truncate(dotidx); + } + } + + // Otherwise, just append the proper extension to the end of the + // filename, adding to the invalid extension that might already be + // there. + if (modify != ModifyExtension_Ignore) { + fileName.AppendLiteral("."); + fileName.Append(NS_ConvertUTF8toUTF16(extension)); + } + } + } + } + } + + CheckDefaultFileName(fileName, aFlags); + + // Make the filename safe for the filesystem. + SanitizeFileName(fileName, aFlags); + + aFileName = fileName; + return mimeInfo.forget(); +} + +void nsExternalHelperAppService::CheckDefaultFileName(nsAString& aFileName, + uint32_t aFlags) { + // If no filename is present, use a default filename. + if (!(aFlags & VALIDATE_NO_DEFAULT_FILENAME) && + (aFileName.Length() == 0 || aFileName.RFind(u".") == 0)) { + nsCOMPtr stringService = + mozilla::components::StringBundle::Service(); + if (stringService) { + nsCOMPtr bundle; + if (NS_SUCCEEDED(stringService->CreateBundle( + "chrome://global/locale/contentAreaCommands.properties", + getter_AddRefs(bundle)))) { + nsAutoString defaultFileName; + bundle->GetStringFromName("UntitledSaveFileName", defaultFileName); + // Append any existing extension to the default filename. + aFileName = defaultFileName + aFileName; + } + } + + // Use 'Untitled' as a last resort. + if (!aFileName.Length()) { + aFileName.AssignLiteral("Untitled"); + } + } +} + +void nsExternalHelperAppService::SanitizeFileName(nsAString& aFileName, + uint32_t aFlags) { + nsAutoString fileName(aFileName); + + // Replace known invalid characters. + fileName.ReplaceChar(u"" KNOWN_PATH_SEPARATORS, u'_'); + fileName.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u' '); + fileName.StripChar(char16_t(0)); + + const char16_t *startStr, *endStr; + fileName.BeginReading(startStr); + fileName.EndReading(endStr); + + // True if multiple consecutive whitespace characters should + // be replaced by single space ' '. + bool collapseWhitespace = !(aFlags & VALIDATE_DONT_COLLAPSE_WHITESPACE); + + // The maximum filename length differs based on the platform: + // Windows (FAT/NTFS) stores filenames as a maximum of 255 UTF-16 code units. + // Mac (APFS) stores filenames with a maximum 255 of UTF-8 code units. + // Linux (ext3/ext4...) stores filenames with a maximum 255 bytes. + // So here we just use the maximum of 255 bytes. + // 0 means don't truncate at a maximum size. + const uint32_t maxBytes = + (aFlags & VALIDATE_DONT_TRUNCATE) ? 0 : kDefaultMaxFileNameLength; + + // True if the last character added was whitespace. + bool lastWasWhitespace = false; + + // Length of the filename that fits into the maximum size excluding the + // extension and period. + int32_t longFileNameEnd = -1; + + // Index of the last character added that was not a character that can be + // trimmed off of the end of the string. Trimmable characters are whitespace, + // periods and the vowel separator u'\u180e'. If all the characters after this + // point are trimmable characters, truncate the string to this point after + // iterating over the filename. + int32_t lastNonTrimmable = -1; + + // The number of bytes that the string would occupy if encoded in UTF-8. + uint32_t bytesLength = 0; + + // The length of the extension in bytes. + uint32_t extensionBytesLength = 0; + + // This algorithm iterates over each character in the string and appends it + // or a replacement character if needed to outFileName. + nsAutoString outFileName; + while (startStr < endStr) { + bool err = false; + char32_t nextChar = UTF16CharEnumerator::NextChar(&startStr, endStr, &err); + if (err) { + break; + } + + // nulls are already stripped out above. + MOZ_ASSERT(nextChar != char16_t(0)); + + auto unicodeCategory = unicode::GetGeneralCategory(nextChar); + if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_CONTROL || + unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR || + unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_PARAGRAPH_SEPARATOR) { + // Skip over any control characters and separators. + continue; + } + + if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_SPACE_SEPARATOR || + nextChar == u'\ufeff') { + // Trim out any whitespace characters at the beginning of the filename, + // and only add whitespace in the middle of the filename if the last + // character was not whitespace or if we are not collapsing whitespace. + if (!outFileName.IsEmpty() && + (!lastWasWhitespace || !collapseWhitespace)) { + // Allow the ideographic space if it is present, otherwise replace with + // ' '. + if (nextChar != u'\u3000') { + nextChar = ' '; + } + lastWasWhitespace = true; + } else { + lastWasWhitespace = true; + continue; + } + } else { + lastWasWhitespace = false; + if (nextChar == '.' || nextChar == u'\u180e') { + // Don't add any periods or vowel separators at the beginning of the + // string. Note also that lastNonTrimmable is not adjusted in this + // case, because periods and vowel separators are included in the + // set of characters to trim at the end of the filename. + if (outFileName.IsEmpty()) { + continue; + } + } else { + if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_FORMAT) { + // Replace formatting characters with an underscore. + nextChar = '_'; + } + + // Don't truncate surrogate pairs in the middle. + lastNonTrimmable = + int32_t(outFileName.Length()) + + (NS_IS_HIGH_SURROGATE(H_SURROGATE(nextChar)) ? 2 : 1); + } + } + + if (maxBytes) { + // UTF16CharEnumerator already converts surrogate pairs, so we can use + // a simple computation of byte length here. + uint32_t charBytesLength = nextChar < 0x80 ? 1 + : nextChar < 0x800 ? 2 + : nextChar < 0x10000 ? 3 + : 4; + bytesLength += charBytesLength; + if (bytesLength > maxBytes) { + if (longFileNameEnd == -1) { + longFileNameEnd = int32_t(outFileName.Length()); + } + } + + // If we encounter a period, it could be the start of an extension, so + // start counting the number of bytes in the extension. If another period + // is found, start again since we want to use the last extension found. + if (nextChar == u'.') { + extensionBytesLength = 1; // 1 byte for the period. + } else if (extensionBytesLength) { + extensionBytesLength += charBytesLength; + } + } + + AppendUCS4ToUTF16(nextChar, outFileName); + } + + // If the filename is longer than the maximum allowed filename size, + // truncate it, but preserve the desired extension that is currently + // on the filename. + if (bytesLength > maxBytes && !outFileName.IsEmpty()) { + // Get the sanitized extension from the filename without the dot. + nsAutoString extension; + int32_t dotidx = outFileName.RFind(u"."); + if (dotidx != -1) { + extension = Substring(outFileName, dotidx + 1); + } + + // There are two ways in which the filename should be truncated: + // - If the filename was too long, truncate the name at the length + // of the filename. + // This position is indicated by longFileNameEnd. + // - lastNonTrimmable will indicate the last character that was not + // whitespace, a period, or a vowel separator at the end of the + // the string, so the string should be truncated there as well. + // If both apply, use the earliest position. + if (lastNonTrimmable >= 0) { + // Subtract off the amount for the extension and the period. + // Note that the extension length is in bytes but longFileNameEnd is in + // characters, but if they don't match, it just means we crop off + // more than is necessary. This is OK since it is better than cropping + // off too little. + longFileNameEnd -= extensionBytesLength; + if (longFileNameEnd <= 0) { + // This is extremely unlikely, but if the extension is larger than the + // maximum size, just get rid of it. In this case, the extension + // wouldn't have been an ordinary one we would want to preserve (such + // as .html or .png) so just truncate off the file wherever the first + // period appears. + int32_t dotidx = outFileName.Find(u"."); + outFileName.Truncate(dotidx > 0 ? dotidx : 1); + } else { + outFileName.Truncate(std::min(longFileNameEnd, lastNonTrimmable)); + + // Now that the filename has been truncated, re-append the extension + // again. + if (!extension.IsEmpty()) { + if (outFileName.Last() != '.') { + outFileName.AppendLiteral("."); + } + + outFileName.Append(extension); + } + } + } + } else if (lastNonTrimmable >= 0) { + // Otherwise, the filename wasn't too long, so just trim off the + // extra whitespace and periods at the end. + outFileName.Truncate(lastNonTrimmable); + } + +#ifdef XP_WIN + if (nsLocalFile::CheckForReservedFileName(outFileName)) { + outFileName.Truncate(); + CheckDefaultFileName(outFileName, aFlags); + } + +#endif + + if (!(aFlags & VALIDATE_ALLOW_INVALID_FILENAMES)) { + // If the extension is one these types, replace it with .download, as these + // types of files can have significance on Windows or Linux. + // This happens for any file, not just those with the shortcut mime type. + if (StringEndsWith(outFileName, u".lnk"_ns, + nsCaseInsensitiveStringComparator) || + StringEndsWith(outFileName, u".local"_ns, + nsCaseInsensitiveStringComparator) || + StringEndsWith(outFileName, u".url"_ns, + nsCaseInsensitiveStringComparator) || + StringEndsWith(outFileName, u".scf"_ns, + nsCaseInsensitiveStringComparator) || + StringEndsWith(outFileName, u".desktop"_ns, + nsCaseInsensitiveStringComparator)) { + outFileName.AppendLiteral(".download"); + } + } + + aFileName = outFileName; +} + +nsExternalHelperAppService::ModifyExtensionType +nsExternalHelperAppService::ShouldModifyExtension(nsIMIMEInfo* aMimeInfo, + bool aForceAppend, + const nsCString& aFileExt) { + nsAutoCString MIMEType; + if (!aMimeInfo || NS_FAILED(aMimeInfo->GetMIMEType(MIMEType))) { + return ModifyExtension_Append; + } + + // Determine whether the extensions should be appended or replaced depending + // on the content type. + bool canForce = StringBeginsWith(MIMEType, "image/"_ns) || + StringBeginsWith(MIMEType, "audio/"_ns) || + StringBeginsWith(MIMEType, "video/"_ns) || aFileExt.IsEmpty(); + + if (!canForce) { + for (const char* mime : forcedExtensionMimetypes) { + if (MIMEType.Equals(mime)) { + if (!StaticPrefs::browser_download_sanitize_non_media_extensions()) { + return ModifyExtension_Ignore; + } + canForce = true; + break; + } + } + + if (!canForce) { + return aForceAppend ? ModifyExtension_Append : ModifyExtension_Ignore; + } + } + + // If we get here, we know for sure the mimetype allows us to modify the + // existing extension, if it's wrong. Return whether we should replace it + // or append it. + bool knownExtension = false; + // Note that aFileExt is either empty or consists of an extension + // excluding the dot. + if (aFileExt.IsEmpty() || + (NS_SUCCEEDED(aMimeInfo->ExtensionExists(aFileExt, &knownExtension)) && + !knownExtension)) { + return ModifyExtension_Replace; + } + + return ModifyExtension_Append; +} -- cgit v1.2.3