/* -*- 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; }