diff options
Diffstat (limited to 'uriloader/exthandler/nsExternalHelperAppService.cpp')
-rw-r--r-- | uriloader/exthandler/nsExternalHelperAppService.cpp | 2926 |
1 files changed, 2926 insertions, 0 deletions
diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp new file mode 100644 index 0000000000..51a7ee0f6a --- /dev/null +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -0,0 +1,2926 @@ +/* -*- 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/dom/ContentChild.h" +#include "mozilla/dom/TabChild.h" +#include "nsXULAppAPI.h" + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsIURI.h" +#include "nsIURL.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsIChannel.h" +#include "nsIDirectoryService.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsICategoryManager.h" +#include "nsDependentSubstring.h" +#include "nsXPIDLString.h" +#include "nsUnicharUtils.h" +#include "nsIStringEnumerator.h" +#include "nsMemory.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 "nsAutoPtr.h" +#include "nsIMutableArray.h" + +// used to access our datastore of user-configured helper applications +#include "nsIHandlerService.h" +#include "nsIMIMEInfo.h" +#include "nsIRefreshURI.h" // XXX needed to redirect according to Refresh: URI +#include "nsIDocumentLoader.h" // XXX needed to get orig. channel and assoc. refresh uri +#include "nsIHelperAppLauncherDialog.h" +#include "nsIContentDispatchChooser.h" +#include "nsNetUtil.h" +#include "nsIPrivateBrowsingChannel.h" +#include "nsIIOService.h" +#include "nsNetCID.h" + +#include "nsMimeTypes.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 "nsIPluginHost.h" // XXX needed for ext->type mapping (bug 233289) +#include "nsPluginHost.h" +#include "nsEscape.h" + +#include "nsIStringBundle.h" // XXX needed to localize error msgs +#include "nsIPrompt.h" + +#include "nsITextToSubURI.h" // to unescape the filename +#include "nsIMIMEHeaderParam.h" + +#include "nsIWindowWatcher.h" + +#include "nsIDownloadHistory.h" // to mark downloads as visited +#include "nsDocShellCID.h" + +#include "nsCRT.h" +#include "nsLocalHandlerApp.h" + +#include "nsIRandomGenerator.h" + +#include "ContentChild.h" +#include "nsXULAppAPI.h" +#include "nsPIDOMWindow.h" +#include "nsIDocShellTreeOwner.h" +#include "nsIDocShellTreeItem.h" +#include "ExternalHelperAppChild.h" + +#ifdef XP_WIN +#include "nsWindowsHelpers.h" +#endif + +#ifdef MOZ_WIDGET_ANDROID +#include "FennecJNIWrappers.h" +#endif + +#include "mozilla/Preferences.h" +#include "mozilla/ipc/URIUtils.h" + +using namespace mozilla; +using namespace mozilla::ipc; + +// 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::mLog("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(args) MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info, args) +#define LOG_ENABLED() MOZ_LOG_TEST(nsExternalHelperAppService::mLog, 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"; + +// 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) +{ + // First, we need a charset + nsAutoCString originCharset; + nsresult rv = aURI->GetOriginCharset(originCharset); + NS_ENSURE_SUCCESS(rv, rv); + + // Now, we need the unescaper + nsCOMPtr<nsITextToSubURI> textToSubURI = do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return textToSubURI->UnEscapeURIForUI(originCharset, aFragment, 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; +} + +/** + * Given a channel, returns the filename and extension the channel has. + * This uses the URL and other sources (nsIMultiPartChannel). + * Also gives back whether the channel requested external handling (i.e. + * whether Content-Disposition: attachment was sent) + * @param aChannel The channel to extract the filename/extension from + * @param aFileName [out] Reference to the string where the filename should be + * stored. Empty if it could not be retrieved. + * WARNING - this filename may contain characters which the OS does not + * allow as part of filenames! + * @param aExtension [out] Reference to the string where the extension should + * be stored. Empty if it could not be retrieved. Stored in UTF-8. + * @param aAllowURLExtension (optional) Get the extension from the URL if no + * Content-Disposition header is present. Default is true. + * @retval true The server sent Content-Disposition:attachment or equivalent + * @retval false Content-Disposition: inline or no content-disposition header + * was sent. + */ +static bool GetFilenameAndExtensionFromChannel(nsIChannel* aChannel, + nsString& aFileName, + nsCString& aExtension, + bool aAllowURLExtension = true) +{ + aExtension.Truncate(); + /* + * If the channel is an http or part of a multipart channel and we + * have a content disposition header set, then use the file name + * suggested there as the preferred file name to SUGGEST to the + * user. we shouldn't actually use that without their + * permission... otherwise just use our temp file + */ + bool handleExternally = false; + uint32_t disp; + nsresult rv = aChannel->GetContentDisposition(&disp); + if (NS_SUCCEEDED(rv)) + { + aChannel->GetContentDispositionFilename(aFileName); + if (disp == nsIChannel::DISPOSITION_ATTACHMENT) + handleExternally = true; + } + + // If the disposition header didn't work, try the filename from nsIURL + nsCOMPtr<nsIURI> uri; + aChannel->GetURI(getter_AddRefs(uri)); + nsCOMPtr<nsIURL> url(do_QueryInterface(uri)); + if (url && aFileName.IsEmpty()) + { + if (aAllowURLExtension) { + url->GetFileExtension(aExtension); + UnescapeFragment(aExtension, url, aExtension); + + // Windows ignores terminating dots. So we have to as well, so + // that our security checks do "the right thing" + // In case the aExtension consisted only of the dot, the code below will + // extract an aExtension from the filename + aExtension.Trim(".", false); + } + + // try to extract the file name from the url and use that as a first pass as the + // leaf name of our temp file... + nsAutoCString leafName; + url->GetFileName(leafName); + if (!leafName.IsEmpty()) + { + rv = UnescapeFragment(leafName, url, aFileName); + if (NS_FAILED(rv)) + { + CopyUTF8toUTF16(leafName, aFileName); // use escaped name + } + } + } + + // Extract Extension, if we have a filename; otherwise, + // truncate the string + if (aExtension.IsEmpty()) { + if (!aFileName.IsEmpty()) + { + // Windows ignores terminating dots. So we have to as well, so + // that our security checks do "the right thing" + aFileName.Trim(".", false); + + // XXX RFindCharInReadable!! + nsAutoString fileNameStr(aFileName); + int32_t idx = fileNameStr.RFindChar(char16_t('.')); + if (idx != kNotFound) + CopyUTF16toUTF8(StringTail(fileNameStr, fileNameStr.Length() - idx - 1), aExtension); + } + } + + + return handleExternally; +} + +/** + * 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 nsDownloadManager.cpp + * + * Optionally skip availability of the directory and storage. + */ +static nsresult GetDownloadDirectory(nsIFile **_directory, + bool aSkipChecks = false) +{ + nsCOMPtr<nsIFile> dir; +#ifdef XP_MACOSX + // On OS X, we first 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 + bool dirExists = false; + (void) dir->Exists(&dirExists); + if (dirExists) break; + + nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (NS_FAILED(rv)) { + dir = nullptr; + break; + } + } + break; + case NS_FOLDER_VALUE_DOWNLOADS: + // This is just the OS default location, so fall out + break; + } + + if (!dir) { + // If not, we default to the OS X default download location. + nsresult rv = NS_GetSpecialDirectory(NS_OSX_DEFAULT_DOWNLOAD_DIR, + getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + } +#elif defined(ANDROID) + // We ask Java for the temporary download directory. The directory will be + // different depending on whether we have the permission to write to the + // public download directory or not. + // In the case where we do not have the permission we will start the + // download to the app cache directory and later move it to the final + // destination after prompting for the permission. + jni::String::LocalRef downloadDir; + if (jni::IsFennec()) { + downloadDir = java::DownloadsIntegration::GetTemporaryDownloadDirectory(); + } + + nsresult rv; + if (downloadDir) { + nsCOMPtr<nsIFile> ldir; + rv = NS_NewNativeLocalFile(downloadDir->ToCString(), + true, getter_AddRefs(ldir)); + + NS_ENSURE_SUCCESS(rv, rv); + dir = do_QueryInterface(ldir); + + // If we're not checking for availability we're done. + if (aSkipChecks) { + dir.forget(_directory); + return NS_OK; + } + } + else { + return NS_ERROR_FAILURE; + } +#else + // On all other platforms, we default to the systems temporary directory. + nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + +#if defined(XP_UNIX) + // Ensuring that only the current user can read the file names we end up + // creating. Note that Creating directories with specified permission only + // supported on Unix platform right now. That's why above if 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(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_'); + + int counter = 0; + bool pathExists; + nsCOMPtr<nsIFile> 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; + } + else if (rv != NS_ERROR_FILE_ALREADY_EXISTS) { + // Unexpected error. + return rv; + } + + counter++; + } + } + +#endif +#endif + + NS_ASSERTION(dir, "Somehow we didn't get a download directory!"); + dir.forget(_directory); + 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" }, + { TEXT_XUL, "xul" }, + { 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" }, + { VIDEO_OGG, "ogv" }, + { VIDEO_OGG, "ogg" }, + { APPLICATION_OGG, "ogg" }, + { AUDIO_OGG, "oga" }, + { AUDIO_OGG, "opus" }, + { APPLICATION_PDF, "pdf" }, + { VIDEO_WEBM, "webm" }, + { AUDIO_WEBM, "webm" }, +#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; +}; + +#ifdef XP_MACOSX +#define MAC_TYPE(x) x +#else +#define MAC_TYPE(x) 0 +#endif + +/** + * 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_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", "Javascript Module Source File" }, +#ifdef MOZ_WIDGET_ANDROID + { "application/vnd.android.package-archive", "apk", "Android Package" }, +#endif + { IMAGE_ART, "art", "ART Image" }, + { IMAGE_BMP, "bmp", "BMP Image" }, + { IMAGE_GIF, "gif", "GIF Image" }, + { IMAGE_ICO, "ico,cur", "ICO Image" }, + { IMAGE_JPEG, "jpeg,jpg,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" }, + { MESSAGE_RFC822, "eml", "RFC-822 data" }, + { TEXT_PLAIN, "txt,text", "Text File" }, + { 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_XUL, "xul", "XML-Based User Interface Language" }, + { TEXT_XML, "xml,xsl,xbl", "Extensible Markup Language" }, + { TEXT_CSS, "css", "Style Sheet" }, + { TEXT_VCARD, "vcf,vcard", "Contact Information" }, + { VIDEO_OGG, "ogv", "Ogg Video" }, + { VIDEO_OGG, "ogg", "Ogg Video" }, + { APPLICATION_OGG, "ogg", "Ogg Video"}, + { AUDIO_OGG, "oga", "Ogg Audio" }, + { AUDIO_OGG, "opus", "Opus Audio" }, +#ifdef MOZ_WIDGET_GONK + { AUDIO_AMR, "amr", "Adaptive Multi-Rate Audio" }, + { AUDIO_FLAC, "flac", "FLAC Audio" }, + { VIDEO_AVI, "avi", "Audio Video Interleave" }, + { VIDEO_AVI, "divx", "Audio Video Interleave" }, + { VIDEO_MPEG_TS, "ts", "MPEG Transport Stream" }, + { VIDEO_MPEG_TS, "m2ts", "MPEG-2 Transport Stream" }, + { VIDEO_MATROSKA, "mkv", "MATROSKA VIDEO" }, + { AUDIO_MATROSKA, "mka", "MATROSKA AUDIO" }, +#endif + { VIDEO_WEBM, "webm", "Web Media Video" }, + { AUDIO_WEBM, "webm", "Web Media Audio" }, + { AUDIO_MP3, "mp3", "MPEG Audio" }, + { VIDEO_MP4, "mp4", "MPEG-4 Video" }, + { AUDIO_MP4, "m4a", "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" }, +#ifdef MOZ_WIDGET_GONK + // The AUDIO_3GPP has to come after the VIDEO_3GPP entry because the Gallery + // app on Firefox OS depends on the "3gp" extension mapping to the + // "video/3gpp" MIME type. + { AUDIO_3GPP, "3gpp,3gp", "3GPP Audio" }, + { AUDIO_3GPP2, "3g2", "3GPP2 Audio" }, +#endif + { AUDIO_MIDI, "mid", "Standard MIDI Audio" } +}; + +#undef MAC_TYPE + +/** + * 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" } +}; + +NS_IMPL_ISUPPORTS( + nsExternalHelperAppService, + nsIExternalHelperAppService, + nsPIExternalAppLauncher, + nsIExternalProtocolService, + nsIMIMEService, + nsIObserver, + nsISupportsWeakReference) + +nsExternalHelperAppService::nsExternalHelperAppService() +{ +} +nsresult nsExternalHelperAppService::Init() +{ + // Add an observer for profile change + nsCOMPtr<nsIObserverService> 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, + nsIInterfaceRequestor *aContentContext, + bool aForceSave, + nsIInterfaceRequestor *aWindowContext, + nsIStreamListener ** aStreamListener) +{ + nsCOMPtr<nsPIDOMWindowOuter> window = do_GetInterface(aContentContext); + NS_ENSURE_STATE(window); + + // 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<nsIURI> uri; + int64_t contentLength = -1; + bool wasFileChannel = false; + uint32_t contentDisposition = -1; + nsAutoString fileName; + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (channel) { + channel->GetURI(getter_AddRefs(uri)); + channel->GetContentLength(&contentLength); + channel->GetContentDisposition(&contentDisposition); + channel->GetContentDispositionFilename(fileName); + channel->GetContentDispositionHeader(disp); + + nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aRequest)); + wasFileChannel = fileChan != nullptr; + } + + + nsCOMPtr<nsIURI> referrer; + NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer)); + + OptionalURIParams uriParams, referrerParams; + SerializeURI(uri, uriParams); + SerializeURI(referrer, referrerParams); + + // 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. + mozilla::dom::PExternalHelperAppChild *pc = + child->SendPExternalHelperAppConstructor(uriParams, + nsCString(aMimeContentType), + disp, contentDisposition, + fileName, aForceSave, + contentLength, wasFileChannel, + referrerParams, + mozilla::dom::TabChild::GetFrom(window)); + ExternalHelperAppChild *childListener = static_cast<ExternalHelperAppChild *>(pc); + + NS_ADDREF(*aStreamListener = childListener); + + uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE; + + RefPtr<nsExternalAppHandler> handler = + new nsExternalAppHandler(nullptr, EmptyCString(), aContentContext, aWindowContext, this, + fileName, reason, aForceSave); + if (!handler) { + return NS_ERROR_OUT_OF_MEMORY; + } + + childListener->SetHandler(handler); + return NS_OK; +} + +NS_IMETHODIMP nsExternalHelperAppService::DoContent(const nsACString& aMimeContentType, + nsIRequest *aRequest, + nsIInterfaceRequestor *aContentContext, + bool aForceSave, + nsIInterfaceRequestor *aWindowContext, + nsIStreamListener ** aStreamListener) +{ + if (XRE_IsContentProcess()) { + return DoContentContentProcessHelper(aMimeContentType, aRequest, aContentContext, + aForceSave, aWindowContext, aStreamListener); + } + + nsAutoString fileName; + nsAutoCString fileExtension; + uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE; + uint32_t contentDisposition = -1; + + // Get the file extension and name that we will need later + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + nsCOMPtr<nsIURI> uri; + int64_t contentLength = -1; + if (channel) { + channel->GetURI(getter_AddRefs(uri)); + channel->GetContentLength(&contentLength); + channel->GetContentDisposition(&contentDisposition); + channel->GetContentDispositionFilename(fileName); + + // Check if we have a POST request, in which case we don't want to use + // the url's extension + bool allowURLExt = true; + nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(channel); + if (httpChan) { + nsAutoCString requestMethod; + httpChan->GetRequestMethod(requestMethod); + allowURLExt = !requestMethod.EqualsLiteral("POST"); + } + + // 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 (uri && allowURLExt) { + nsCOMPtr<nsIURL> url = do_QueryInterface(uri); + + if (url) { + nsAutoCString query; + + // We only care about the query for HTTP and HTTPS URLs + nsresult rv; + bool isHTTP, isHTTPS; + rv = uri->SchemeIs("http", &isHTTP); + if (NS_FAILED(rv)) { + isHTTP = false; + } + rv = uri->SchemeIs("https", &isHTTPS); + if (NS_FAILED(rv)) { + isHTTPS = false; + } + if (isHTTP || isHTTPS) { + 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(); + } + } + // Extract name & extension + bool isAttachment = GetFilenameAndExtensionFromChannel(channel, fileName, + fileExtension, + allowURLExt); + LOG(("Found extension '%s' (filename is '%s', handling attachment: %i)", + fileExtension.get(), NS_ConvertUTF16toUTF8(fileName).get(), + isAttachment)); + if (isAttachment) { + reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST; + } + } + + LOG(("HelperAppService::DoContent: mime '%s', extension '%s'\n", + PromiseFlatCString(aMimeContentType).get(), fileExtension.get())); + + // 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<nsIMIMEService> mimeSvc(do_GetService(NS_MIMESERVICE_CONTRACTID)); + NS_ENSURE_TRUE(mimeSvc, NS_ERROR_FAILURE); + + // Try to find a mime object by looking at the mime type/extension + nsCOMPtr<nsIMIMEInfo> mimeInfo; + if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT, nsCaseInsensitiveCStringComparator())) { + nsAutoCString mimeType; + if (!fileExtension.IsEmpty()) { + mimeSvc->GetFromTypeAndExtension(EmptyCString(), fileExtension, getter_AddRefs(mimeInfo)); + if (mimeInfo) { + mimeInfo->GetMIMEType(mimeType); + + LOG(("OS-Provided mime type '%s' for extension '%s'\n", + mimeType.get(), fileExtension.get())); + } + } + + if (fileExtension.IsEmpty() || mimeType.IsEmpty()) { + // Extension lookup gave us no useful match + mimeSvc->GetFromTypeAndExtension(NS_LITERAL_CSTRING(APPLICATION_OCTET_STREAM), fileExtension, + getter_AddRefs(mimeInfo)); + mimeType.AssignLiteral(APPLICATION_OCTET_STREAM); + } + + if (channel) { + channel->SetContentType(mimeType); + } + + // Don't overwrite SERVERREQUEST + if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) { + reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED; + } + } else { + mimeSvc->GetFromTypeAndExtension(aMimeContentType, fileExtension, + getter_AddRefs(mimeInfo)); + } + 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; + } + + *aStreamListener = nullptr; + // We want the mimeInfo's primary extension to pass it to + // nsExternalAppHandler + nsAutoCString buf; + mimeInfo->GetPrimaryExtension(buf); + + nsExternalAppHandler * handler = new nsExternalAppHandler(mimeInfo, + buf, + aContentContext, + aWindowContext, + this, + fileName, + reason, + aForceSave); + if (!handler) { + return NS_ERROR_OUT_OF_MEMORY; + } + + NS_ADDREF(*aStreamListener = handler); + return NS_OK; +} + +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<nsIHandlerInfo> handlerInfo; + nsresult rv = GetProtocolHandlerInfo(nsDependentCString(aProtocolScheme), + getter_AddRefs(handlerInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + // See if we have any known possible handler apps for this + nsCOMPtr<nsIMutableArray> 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; +} + +NS_IMETHODIMP nsExternalHelperAppService::LoadUrl(nsIURI * aURL) +{ + return LoadURI(aURL, nullptr); +} + +static const char kExternalProtocolPrefPrefix[] = "network.protocol-handler.external."; +static const char kExternalProtocolDefaultPref[] = "network.protocol-handler.external-default"; + +NS_IMETHODIMP +nsExternalHelperAppService::LoadURI(nsIURI *aURI, + nsIInterfaceRequestor *aWindowContext) +{ + NS_ENSURE_ARG_POINTER(aURI); + + if (XRE_IsContentProcess()) { + URIParams uri; + SerializeURI(aURI, uri); + + nsCOMPtr<nsITabChild> tabChild(do_GetInterface(aWindowContext)); + mozilla::dom::ContentChild::GetSingleton()-> + SendLoadURIExternal(uri, static_cast<dom::TabChild*>(tabChild.get())); + return NS_OK; + } + + nsAutoCString spec; + aURI->GetSpec(spec); + + if (spec.Find("%00") != -1) + return NS_ERROR_MALFORMED_URI; + + spec.ReplaceSubstring("\"", "%22"); + spec.ReplaceSubstring("`", "%60"); + + nsCOMPtr<nsIIOService> ios(do_GetIOService()); + nsCOMPtr<nsIURI> uri; + nsresult rv = ios->NewURI(spec, nullptr, nullptr, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString scheme; + uri->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 + } + + nsCOMPtr<nsIHandlerInfo> handler; + rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler)); + NS_ENSURE_SUCCESS(rv, rv); + + nsHandlerInfoAction preferredAction; + handler->GetPreferredAction(&preferredAction); + bool alwaysAsk = true; + handler->GetAlwaysAskBeforeHandling(&alwaysAsk); + + // if we are not supposed to ask, and the preferred action is to use + // a helper app or the system default, we just launch the URI. + if (!alwaysAsk && (preferredAction == nsIHandlerInfo::useHelperApp || + preferredAction == nsIHandlerInfo::useSystemDefault)) + return handler->LaunchWithURI(uri, aWindowContext); + + nsCOMPtr<nsIContentDispatchChooser> chooser = + do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return chooser->Ask(handler, aWindowContext, uri, + nsIContentDispatchChooser::REASON_CANNOT_HANDLE); +} + +NS_IMETHODIMP nsExternalHelperAppService::GetApplicationDescription(const nsACString& aScheme, nsAString& _retval) +{ + // this method should only be implemented by each OS specific implementation of this service. + return NS_ERROR_NOT_IMPLEMENTED; +} + + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// Methods related to deleting temporary files on exit +////////////////////////////////////////////////////////////////////////////////////////////////////// + +/* static */ +nsresult +nsExternalHelperAppService::DeleteTemporaryFileHelper(nsIFile * aTemporaryFile, + nsCOMArray<nsIFile> &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<nsIFile> &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<nsIHandlerService> handlerSvc = do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + bool hasHandler = false; + (void) handlerSvc->Exists(*aHandlerInfo, &hasHandler); + if (hasHandler) { + rv = handlerSvc->FillHandlerInfo(*aHandlerInfo, EmptyCString()); + if (NS_SUCCEEDED(rv)) + return NS_OK; + } + } + + return SetProtocolHandlerDefaults(*aHandlerInfo, exists); +} + +NS_IMETHODIMP +nsExternalHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString &aScheme, + bool *found, + nsIHandlerInfo **aHandlerInfo) +{ + // intended to be implemented by the subclass + return NS_ERROR_NOT_IMPLEMENTED; +} + +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(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver) +NS_INTERFACE_MAP_END_THREADSAFE + +nsExternalAppHandler::nsExternalAppHandler(nsIMIMEInfo * aMIMEInfo, + const nsCSubstring& aTempFileExtension, + nsIInterfaceRequestor* aContentContext, + nsIInterfaceRequestor* aWindowContext, + nsExternalHelperAppService *aExtProtSvc, + const nsAString& aSuggestedFilename, + uint32_t aReason, bool aForceSave) +: mMimeInfo(aMIMEInfo) +, mContentContext(aContentContext) +, mWindowContext(aWindowContext) +, mWindowToClose(nullptr) +, mSuggestedFileName(aSuggestedFilename) +, mForceSave(aForceSave) +, mCanceled(false) +, mShouldCloseWindow(false) +, mStopRequestIssued(false) +, mReason(aReason) +, mContentLength(-1) +, mProgress(0) +, mSaver(nullptr) +, mDialogProgressListener(nullptr) +, mTransfer(nullptr) +, mRequest(nullptr) +, mExtProtSvc(aExtProtSvc) +{ + + // make sure the extention includes the '.' + if (!aTempFileExtension.IsEmpty() && aTempFileExtension.First() != '.') + mTempFileExtension = char16_t('.'); + AppendUTF8toUTF16(aTempFileExtension, mTempFileExtension); + + // replace platform specific path separator and illegal characters to avoid any confusion + mSuggestedFileName.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + mTempFileExtension.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + + // Remove unsafe bidi characters which might have spoofing implications (bug 511521). + const char16_t unsafeBidiCharacters[] = { + char16_t(0x061c), // Arabic Letter Mark + char16_t(0x200e), // Left-to-Right Mark + char16_t(0x200f), // Right-to-Left Mark + char16_t(0x202a), // Left-to-Right Embedding + char16_t(0x202b), // Right-to-Left Embedding + char16_t(0x202c), // Pop Directional Formatting + char16_t(0x202d), // Left-to-Right Override + char16_t(0x202e), // Right-to-Left Override + char16_t(0x2066), // Left-to-Right Isolate + char16_t(0x2067), // Right-to-Left Isolate + char16_t(0x2068), // First Strong Isolate + char16_t(0x2069), // Pop Directional Isolate + char16_t(0) + }; + mSuggestedFileName.ReplaceChar(unsafeBidiCharacters, '_'); + mTempFileExtension.ReplaceChar(unsafeBidiCharacters, '_'); + + // Make sure extension is correct. + EnsureSuggestedFileName(); + + 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); + MaybeCloseWindow(); +} + +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; +} + +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<nsIChannel> aChannel = do_QueryInterface(request); + if (!aChannel) + return; + + // we need to store off the original (pre redirect!) channel that initiated the load. We do + // this so later on, we can pass any refresh urls associated with the original channel back to the + // window context which started the whole process. More comments about that are listed below.... + // HACK ALERT: it's pretty bogus that we are getting the document channel from the doc loader. + // ideally we should be able to just use mChannel (the channel we are extracting content from) or + // the default load channel associated with the original load group. Unfortunately because + // a redirect may have occurred, the doc loader is the only one with a ptr to the original channel + // which is what we really want.... + + // Note that we need to do this before removing aChannel from the loadgroup, + // since that would mess with the original channel on the loader. + nsCOMPtr<nsIDocumentLoader> origContextLoader = + do_GetInterface(mContentContext); + if (origContextLoader) { + origContextLoader->GetDocumentChannel(getter_AddRefs(mOriginalChannel)); + } + + bool isPrivate = NS_UsePrivateBrowsing(aChannel); + + nsCOMPtr<nsILoadGroup> oldLoadGroup; + aChannel->GetLoadGroup(getter_AddRefs(oldLoadGroup)); + + if(oldLoadGroup) { + oldLoadGroup->RemoveRequest(request, nullptr, NS_BINDING_RETARGETED); + } + + aChannel->SetLoadGroup(nullptr); + aChannel->SetNotificationCallbacks(nullptr); + + nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel); + if (pbChannel) { + pbChannel->SetPrivate(isPrivate); + } +} + +/** + * Make mTempFileExtension contain an extension exactly when its previous value + * is different from mSuggestedFileName's extension, so that it can be appended + * to mSuggestedFileName and form a valid, useful leaf name. + * This is required so that the (renamed) temporary file has the correct extension + * after downloading to make sure the OS will launch the application corresponding + * to the MIME type (which was used to calculate mTempFileExtension). This prevents + * a cgi-script named foobar.exe that returns application/zip from being named + * foobar.exe and executed as an executable file. It also blocks content that + * a web site might provide with a content-disposition header indicating + * filename="foobar.exe" from being downloaded to a file with extension .exe + * and executed. + */ +void nsExternalAppHandler::EnsureSuggestedFileName() +{ + // Make sure there is a mTempFileExtension (not "" or "."). + // Remember that mTempFileExtension will always have the leading "." + // (the check for empty is just to be safe). + if (mTempFileExtension.Length() > 1) + { + // Get mSuggestedFileName's current extension. + nsAutoString fileExt; + int32_t pos = mSuggestedFileName.RFindChar('.'); + if (pos != kNotFound) + mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos); + + // Now, compare fileExt to mTempFileExtension. + if (fileExt.Equals(mTempFileExtension, nsCaseInsensitiveStringComparator())) + { + // Matches -> mTempFileExtension can be empty + mTempFileExtension.Truncate(); + } + } +} + +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. + // We will request raw random bytes, and transform that to a base64 string, + // as all characters from the base64 set are acceptable for filenames. For + // each three bytes of random data, we will get four bytes of ASCII. Request + // a bit more, to be safe, and truncate to the length we want in the end. + + const uint32_t wantedFileNameLength = 8; + const uint32_t requiredBytesLength = + static_cast<uint32_t>((wantedFileNameLength + 1) / 4 * 3); + + nsCOMPtr<nsIRandomGenerator> rg = + do_GetService("@mozilla.org/security/random-generator;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint8_t *buffer; + rv = rg->GenerateRandomBytes(requiredBytesLength, &buffer); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString tempLeafName; + nsDependentCSubstring randomData(reinterpret_cast<const char*>(buffer), requiredBytesLength); + rv = Base64Encode(randomData, tempLeafName); + free(buffer); + buffer = nullptr; + NS_ENSURE_SUCCESS(rv, rv); + + tempLeafName.Truncate(wantedFileNameLength); + + // Base64 characters are alphanumeric (a-zA-Z0-9) and '+' and '/', so we need + // to replace illegal characters -- notably '/' + tempLeafName.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + + // 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<nsIFile> 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, NS_LITERAL_STRING(".part")), + 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<nsIEncodedChannel> 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<nsIURL> sourceURL(do_QueryInterface(mSourceUrl)); + if (sourceURL) + { + nsAutoCString extension; + sourceURL->GetFileExtension(extension); + if (!extension.IsEmpty()) + { + nsCOMPtr<nsIUTF8StringEnumerator> 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 ); + return; +} + +NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest *request, nsISupports * aCtxt) +{ + NS_PRECONDITION(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<nsIChannel> aChannel = do_QueryInterface(request); + + nsresult rv; + + nsCOMPtr<nsIFileChannel> 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<dom::nsIExternalHelperAppParent> parent(do_QueryInterface(request)); + mIsFileChannel = parent && parent->WasFileChannel(); + } + + // Get content length + if (aChannel) { + aChannel->GetContentLength(&mContentLength); + } + + nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(request, &rv)); + // Determine whether a new window was opened specifically for this request + if (props) { + bool tmp = false; + props->GetPropertyAsBool(NS_LITERAL_STRING("docshell.newWindowTarget"), + &tmp); + mShouldCloseWindow = tmp; + } + + // Now get the URI + if (aChannel) { + aChannel->GetURI(getter_AddRefs(mSourceUrl)); + } + + // retarget all load notifications to our docloader instead of the original window's docloader... + RetargetLoadNotifications(request); + + // Check to see if there is a refresh header on the original channel. + if (mOriginalChannel) { + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mOriginalChannel)); + if (httpChannel) { + nsAutoCString refreshHeader; + httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("refresh"), + refreshHeader); + if (!refreshHeader.IsEmpty()) { + mShouldCloseWindow = false; + } + } + } + + // Close the underlying DOMWindow if there is no refresh header + // and it was opened specifically for the download + 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(aChannel && NS_UsePrivateBrowsing(aChannel)); + 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 prevent caching. + nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(aChannel); + if (httpInternal) { + httpInternal->SetChannelIsForDownload(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<nsIHandlerService> handlerSvc = do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + handlerSvc->Exists(mMimeInfo, &mimeTypeIsInDatastore); + } + if (!handlerSvc || !mimeTypeIsInDatastore) { + nsAutoCString MIMEType; + mMimeInfo->GetMIMEType(MIMEType); + 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; + } + } + } + + int32_t action = nsIMIMEInfo::saveToDisk; + mMimeInfo->GetPreferredAction( &action ); + + // OK, now check why we're here + if (!alwaysAsk && mReason != nsIHelperAppLauncherDialog::REASON_CANTHANDLE) { + // Force asking if we're not saving. See comment back when we fetched the + // alwaysAsk boolean for details. + alwaysAsk = (action != nsIMIMEInfo::saveToDisk); + } + + // if we were told that we _must_ save to disk without asking, all the stuff + // before this is irrelevant; override it + if (mForceSave) { + alwaysAsk = false; + action = nsIMIMEInfo::saveToDisk; + } + + 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. + rv = mDialog->Show(this, GetDialogParent(), 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. +#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. + * This code mirrors the code in + * nsExternalAppHandler::LaunchWithApplication so that what we + * test here is as close as possible to what will really be + * happening if we decide to execute + */ + nsCOMPtr<nsIHandlerApp> prefApp; + mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(prefApp)); + if (action != nsIMIMEInfo::useHelperApp || !prefApp) { + nsCOMPtr<nsIFile> fileToTest; + GetTargetFile(getter_AddRefs(fileToTest)); + if (fileToTest) { + bool isExecutable; + rv = fileToTest->IsExecutable(&isExecutable); + if (NS_FAILED(rv) || isExecutable) { // checking NS_FAILED, because paranoia is good + action = nsIMIMEInfo::saveToDisk; + } + } 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! "); + action = nsIMIMEInfo::saveToDisk; + } + } + +#endif + if (action == nsIMIMEInfo::useHelperApp || + action == nsIMIMEInfo::useSystemDefault) { + rv = LaunchWithApplication(nullptr, false); + } else { + rv = SaveToDisk(nullptr, false); + } + } + + return NS_OK; +} + +// 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 nsAFlatString &path) +{ + nsAutoString msgId; + switch (rv) { + case NS_ERROR_OUT_OF_MEMORY: + // No memory + msgId.AssignLiteral("noMemory"); + break; + + case NS_ERROR_FILE_DISK_FULL: + case NS_ERROR_FILE_NO_DEVICE_SPACE: + // Out of space on target volume. + msgId.AssignLiteral("diskFull"); + break; + + case NS_ERROR_FILE_READ_ONLY: + // Attempt to write to read/only file. + msgId.AssignLiteral("readOnly"); + break; + + case NS_ERROR_FILE_ACCESS_DENIED: + if (type == kWriteError) { + // Attempt to write without sufficient permissions. +#if defined(ANDROID) + // On Android (and Gonk), this means the SD card is present but + // unavailable (read-only). + msgId.AssignLiteral("SDAccessErrorCardReadOnly"); +#else + msgId.AssignLiteral("accessError"); +#endif + } else { + msgId.AssignLiteral("launchError"); + } + break; + + case NS_ERROR_FILE_NOT_FOUND: + case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST: + case NS_ERROR_FILE_UNRECOGNIZED_PATH: + // Helper app not found, let's verify this happened on launch + if (type == kLaunchError) { + msgId.AssignLiteral("helperAppNotFound"); + break; + } +#if defined(ANDROID) + else if (type == kWriteError) { + // On Android (and Gonk), this means the SD card is missing (not in + // SD slot). + msgId.AssignLiteral("SDAccessErrorCardMissing"); + break; + } +#endif + MOZ_FALLTHROUGH; + + default: + // Generic read/write/launch error message. + switch (type) { + case kReadError: + msgId.AssignLiteral("readError"); + break; + case kWriteError: + msgId.AssignLiteral("writeError"); + break; + case kLaunchError: + msgId.AssignLiteral("launchError"); + break; + } + break; + } + + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error, + ("Error: %s, type=%i, listener=0x%p, transfer=0x%p, rv=0x%08X\n", + NS_LossyConvertUTF16toASCII(msgId).get(), type, mDialogProgressListener.get(), mTransfer.get(), rv)); + + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error, + (" path='%s'\n", NS_ConvertUTF16toUTF8(path).get())); + + // Get properties file bundle and extract status string. + nsCOMPtr<nsIStringBundleService> stringService = + mozilla::services::GetStringBundleService(); + if (stringService) { + nsCOMPtr<nsIStringBundle> bundle; + if (NS_SUCCEEDED(stringService->CreateBundle("chrome://global/locale/nsWebBrowserPersist.properties", + getter_AddRefs(bundle)))) { + nsXPIDLString msgText; + const char16_t *strings[] = { path.get() }; + if (NS_SUCCEEDED(bundle->FormatStringFromName(msgId.get(), strings, 1, + getter_Copies(msgText)))) { + if (mDialogProgressListener) { + // We have a listener, let it handle the error. + mDialogProgressListener->OnStatusChange(nullptr, (type == kReadError) ? aRequest : nullptr, rv, msgText); + } else if (mTransfer) { + mTransfer->OnStatusChange(nullptr, (type == kReadError) ? aRequest : nullptr, rv, msgText); + } else if (XRE_IsParentProcess()) { + // We don't have a listener. Simply show the alert ourselves. + nsresult qiRv; + nsCOMPtr<nsIPrompt> prompter(do_GetInterface(GetDialogParent(), &qiRv)); + nsXPIDLString title; + bundle->FormatStringFromName(u"title", + strings, + 1, + getter_Copies(title)); + + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Debug, + ("mContentContext=0x%p, prompter=0x%p, qi rv=0x%08X, title='%s', msg='%s'", + mContentContext.get(), + prompter.get(), + 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<nsPIDOMWindowOuter> window(do_GetInterface(GetDialogParent())); + if (!window || !window->GetDocShell()) { + return; + } + + prompter = do_GetInterface(window->GetDocShell(), &qiRv); + + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Debug, + ("No prompter from mContentContext, using DocShell, " \ + "window=0x%p, docShell=0x%p, " \ + "prompter=0x%p, qi rv=0x%08X", + window.get(), + window->GetDocShell(), + prompter.get(), + qiRv)); + + // If we still don't have a prompter, there's nothing else we + // can do so just return. + if (!prompter) { + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error, + ("No prompter from DocShell, no way to alert user")); + return; + } + } + + // We should always have a prompter at this point. + prompter->Alert(title, msgText); + } + } + } + } +} + +NS_IMETHODIMP +nsExternalAppHandler::OnDataAvailable(nsIRequest *request, nsISupports * aCtxt, + 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<nsIStreamListener> saver = do_QueryInterface(mSaver); + rv = saver->OnDataAvailable(request, aCtxt, 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, nsISupports *aCtxt, + nsresult aStatus) +{ + LOG(("nsExternalAppHandler::OnStopRequest\n" + " mCanceled=%d, mTransfer=0x%p, aStatus=0x%08X\n", + mCanceled, mTransfer.get(), 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%08X, mCanceled=%d, mTransfer=0x%p\n", + aSaver, aStatus, mCanceled, mTransfer.get())); + + if (!mCanceled) { + // Save the hash and signature information + (void)mSaver->GetSha256Hash(mHash); + (void)mSaver->GetSignatureInfo(getter_AddRefs(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<nsIChannel> channel = do_QueryInterface(mRequest); + if (channel) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->GetLoadInfo(); + if (loadInfo) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMutableArray> redirectChain = + do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + LOG(("nsExternalAppHandler: Got %u redirects\n", loadInfo->RedirectChain().Length())); + for (nsIPrincipal* principal : loadInfo->RedirectChain()) { + redirectChain->AppendElement(principal, false); + } + 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(channel && NS_UsePrivateBrowsing(channel)); + } + + 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"); + } + 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<nsITransfer> transfer = do_CreateInstance( + NS_TRANSFER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Initialize the download + nsCOMPtr<nsIURI> target; + rv = NS_NewFileURI(getter_AddRefs(target), mFinalFileDestination); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest); + + rv = transfer->Init(mSourceUrl, target, EmptyString(), + mMimeInfo, mTimeDownloadStarted, mTempFile, this, + channel && NS_UsePrivateBrowsing(channel)); + NS_ENSURE_SUCCESS(rv, rv); + + // Now let's add the download to history + nsCOMPtr<nsIDownloadHistory> dh(do_GetService(NS_DOWNLOADHISTORY_CONTRACTID)); + if (dh) { + if (channel && !NS_UsePrivateBrowsing(channel)) { + nsCOMPtr<nsIURI> referrer; + NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer)); + + dh->AddDownload(mSourceUrl, referrer, mTimeDownloadStarted, target); + } + } + + // 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(bool aIsPrivateBrowsing) +{ + nsresult rv; + nsCOMPtr<nsITransfer> transfer = + do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // 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<nsIFile> 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); + + nsCOMPtr<nsIURI> pseudoTarget; + rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), pseudoFile); + NS_ENSURE_SUCCESS(rv, rv); + + rv = transfer->Init(mSourceUrl, pseudoTarget, EmptyString(), + mMimeInfo, mTimeDownloadStarted, nullptr, this, + aIsPrivateBrowsing); + NS_ENSURE_SUCCESS(rv, rv); + + // Our failed transfer is ready. + mTransfer = transfer.forget(); + + return NS_OK; +} + +nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile * aFile) +{ + if (aFile) + ContinueSave(aFile); + else + Cancel(NS_BINDING_ABORTED); + + return NS_OK; +} + +void nsExternalAppHandler::RequestSaveDestination(const nsAFlatString &aDefaultFile, const nsAFlatString &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<nsExternalAppHandler> kungFuDeathGrip(this); + nsCOMPtr<nsIHelperAppLauncherDialog> dlg(mDialog); + + rv = dlg->PromptForSaveToFileAsync(this, + GetDialogParent(), + aDefaultFile.get(), + aFileExtension.get(), + mForceSave); + if (NS_FAILED(rv)) { + Cancel(NS_BINDING_ABORTED); + } +} + +// SaveToDisk should only be called by the helper app dialog which allows +// the user to say launch with application or save to disk. It doesn't actually +// perform the save, it just prompts for the destination file name. +NS_IMETHODIMP nsExternalAppHandler::SaveToDisk(nsIFile * aNewFileLocation, bool aRememberThisPreference) +{ + if (mCanceled) + return NS_OK; + + mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + + if (!aNewFileLocation) { + if (mSuggestedFileName.IsEmpty()) + RequestSaveDestination(mTempLeafName, mTempFileExtension); + else + { + nsAutoString fileExt; + int32_t pos = mSuggestedFileName.RFindChar('.'); + if (pos >= 0) + mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos); + if (fileExt.IsEmpty()) + fileExt = mTempFileExtension; + + RequestSaveDestination(mSuggestedFileName, fileExt); + } + } else { + ContinueSave(aNewFileLocation); + } + + return NS_OK; +} +nsresult nsExternalAppHandler::ContinueSave(nsIFile * aNewFileLocation) +{ + if (mCanceled) + return NS_OK; + + NS_PRECONDITION(aNewFileLocation, "Must be called with a non-null file"); + + nsresult rv = NS_OK; + nsCOMPtr<nsIFile> fileToUse = do_QueryInterface(aNewFileLocation); + mFinalFileDestination = do_QueryInterface(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<nsIFile> movedFile; + mFinalFileDestination->Clone(getter_AddRefs(movedFile)); + if (movedFile) { + // Get the old leaf name and append .part to it + nsAutoString name; + mFinalFileDestination->GetLeafName(name); + name.AppendLiteral(".part"); + movedFile->SetLeafName(name); + + 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; + } + + // now that the user has chosen the file location to save to, it's okay to fire the refresh tag + // if there is one. We don't want to do this before the save as dialog goes away because this dialog + // is modal and we do bad things if you try to load a web page in the underlying window while a modal + // dialog is still up. + ProcessAnyRefreshTags(); + + return NS_OK; +} + + +// LaunchWithApplication should only be called by the helper app dialog which +// allows the user to say launch with application or save to disk. It doesn't +// actually perform launch with application. +NS_IMETHODIMP nsExternalAppHandler::LaunchWithApplication(nsIFile * aApplication, bool aRememberThisPreference) +{ + if (mCanceled) + return NS_OK; + + // user has chosen to launch using an application, fire any refresh tags now... + ProcessAnyRefreshTags(); + + if (mMimeInfo && aApplication) { + PlatformLocalHandlerApp_t *handlerApp = + new PlatformLocalHandlerApp_t(EmptyString(), aApplication); + mMimeInfo->SetPreferredApplicationHandler(handlerApp); + } + + // Now check if the file is local, in which case we won't bother with saving + // it to a temporary directory and just launch it from where it is + nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(mSourceUrl)); + if (fileUrl && mIsFileChannel) { + Cancel(NS_BINDING_ABORTED); + nsCOMPtr<nsIFile> 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; + } + + // 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 nsDownload can rename in place + // later. + nsCOMPtr<nsIFile> fileToUse; + (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 + fileToUse->Append(mSuggestedFileName + mTempFileExtension); +#else + fileToUse->Append(mSuggestedFileName); +#endif + + nsresult rv = fileToUse->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + if(NS_SUCCEEDED(rv)) { + mFinalFileDestination = do_QueryInterface(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; +} + +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; + + mRequest = nullptr; + + // Release the listener, to break the reference cycle with it (we are the + // observer of the listener). + mDialogProgressListener = nullptr; + + return NS_OK; +} + +void nsExternalAppHandler::ProcessAnyRefreshTags() +{ + // one last thing, try to see if the original window context supports a refresh interface... + // Sometimes, when you download content that requires an external handler, there is + // a refresh header associated with the download. This refresh header points to a page + // the content provider wants the user to see after they download the content. How do we + // pass this refresh information back to the caller? For now, try to get the refresh URI + // interface. If the window context where the request originated came from supports this + // then we can force it to process the refresh information (if there is any) from this channel. + if (mContentContext && mOriginalChannel) { + nsCOMPtr<nsIRefreshURI> refreshHandler (do_GetInterface(mContentContext)); + if (refreshHandler) { + refreshHandler->SetupRefreshURI(mOriginalChannel); + } + mOriginalChannel = nullptr; + } +} + +bool nsExternalAppHandler::GetNeverAskFlagFromPref(const char * prefName, const char * aContentType) +{ + // Search the obsolete pref strings. + nsAdoptingCString prefCString = Preferences::GetCString(prefName); + 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); +} + +nsresult nsExternalAppHandler::MaybeCloseWindow() +{ + nsCOMPtr<nsPIDOMWindowOuter> window = do_GetInterface(mContentContext); + NS_ENSURE_STATE(window); + + if (mShouldCloseWindow) { + // Reset the window context to the opener window so that the dependent + // dialogs have a parent + nsCOMPtr<nsPIDOMWindowOuter> opener = window->GetOpener(); + + if (opener && !opener->Closed()) { + mContentContext = do_GetInterface(opener); + + // Now close the old window. Do it on a timer so that we don't run + // into issues trying to close the window before it has fully opened. + NS_ASSERTION(!mTimer, "mTimer was already initialized once!"); + mTimer = do_CreateInstance("@mozilla.org/timer;1"); + if (!mTimer) { + return NS_ERROR_FAILURE; + } + + mTimer->InitWithCallback(this, 0, nsITimer::TYPE_ONE_SHOT); + mWindowToClose = window; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsExternalAppHandler::Notify(nsITimer* timer) +{ + NS_ASSERTION(mWindowToClose, "No window to close after timer fired"); + + mWindowToClose->Close(); + mWindowToClose = nullptr; + mTimer = nullptr; + + 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) +{ + NS_PRECONDITION(!aMIMEType.IsEmpty() || + !aFileExt.IsEmpty(), + "Give me something to work with"); + 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); + + // (1) Ask the OS for a mime info + bool found; + *_retval = GetMIMEInfoFromOS(typeToUse, aFileExt, &found).take(); + 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; + + // (2) 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) + nsresult rv; + nsCOMPtr<nsIHandlerService> handlerSvc = do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + bool hasHandler = false; + (void) handlerSvc->Exists(*_retval, &hasHandler); + if (hasHandler) { + rv = handlerSvc->FillHandlerInfo(*_retval, EmptyCString()); + LOG(("Data source: Via type: retval 0x%08x\n", rv)); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + found = found || NS_SUCCEEDED(rv); + + if (!found || NS_FAILED(rv)) { + // No type match, try extension match + if (!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%08x\n", rv)); + found = found || NS_SUCCEEDED(rv); + } + } + } + } + + // (3) No match yet. Ask extras. + if (!found) { + rv = NS_ERROR_FAILURE; +#ifdef XP_WIN + /* XXX Gross hack to wallpaper over the most common Win32 + * extension issues caused by the fix for bug 116938. See bug + * 120327, comment 271 for why this is needed. Not even sure we + * want to remove this once we have fixed all this stuff to work + * right; any info we get from extras on this type is pretty much + * useless.... + */ + if (!typeToUse.Equals(APPLICATION_OCTET_STREAM, nsCaseInsensitiveCStringComparator())) +#endif + rv = FillMIMEInfoForMimeTypeFromExtras(typeToUse, *_retval); + LOG(("Searched extras (by type), rv 0x%08X\n", rv)); + // If that didn't work out, try file extension from extras + if (NS_FAILED(rv) && !aFileExt.IsEmpty()) { + rv = FillMIMEInfoForExtensionFromExtras(aFileExt, *_retval); + LOG(("Searched extras (by ext), rv 0x%08X\n", rv)); + } + // If that still didn't work, set the file description to "ext File" + if (NS_FAILED(rv) && !aFileExt.IsEmpty()) { + // XXXzpao This should probably be localized + nsAutoCString desc(aFileExt); + desc.AppendLiteral(" File"); + (*_retval)->SetDescription(NS_ConvertASCIItoUTF16(desc)); + LOG(("Falling back to 'File' file description\n")); + } + } + + // 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) + (*_retval)->SetPrimaryExtension(aFileExt); + } + + if (LOG_ENABLED()) { + nsAutoCString type; + (*_retval)->GetMIMEType(type); + + nsAutoCString ext; + (*_retval)->GetPrimaryExtension(ext); + LOG(("MIME Info Summary: Type '%s', Primary Ext '%s'\n", type.get(), ext.get())); + } + + return NS_OK; +} + +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 + for (auto& entry : defaultMimeEntries) { + if (aFileExt.LowerCaseEqualsASCII(entry.mFileExtension)) { + aContentType = entry.mMimeType; + 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; + } + + // Try the plugins + RefPtr<nsPluginHost> pluginHost = nsPluginHost::GetInst(); + if (pluginHost && + pluginHost->HavePluginForExtension(aFileExt, aContentType)) { + return NS_OK; + } + + // Let's see if an extension added something + nsCOMPtr<nsICategoryManager> 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 + nsXPIDLCString type; + nsresult rv = catMan->GetCategoryEntry("ext-to-type-mapping", + lowercaseFileExt.get(), + getter_Copies(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<nsIMIMEInfo> mi; + nsresult rv = GetFromTypeAndExtension(aMIMEType, aFileExt, getter_AddRefs(mi)); + if (NS_FAILED(rv)) + return rv; + + return mi->GetPrimaryExtension(_retval); +} + +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<nsIFileURL> fileUrl = do_QueryInterface(aURI); + if (fileUrl) { + nsCOMPtr<nsIFile> 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<nsIURL> 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(fileName.get() + i + 1, fileExt); + break; + } + } + } + + if (fileExt.IsEmpty()) + return NS_ERROR_FAILURE; + + return GetTypeFromExtension(fileExt, aContentType); +} + +nsresult nsExternalHelperAppService::FillMIMEInfoForMimeTypeFromExtras( + const nsACString& aContentType, nsIMIMEInfo * aMIMEInfo) +{ + NS_ENSURE_ARG( aMIMEInfo ); + + NS_ENSURE_ARG( !aContentType.IsEmpty() ); + + // Look for default entry with matching mime type. + nsAutoCString MIMEType(aContentType); + ToLowerCase(MIMEType); + int32_t numEntries = ArrayLength(extraMimeEntries); + for (int32_t index = 0; index < numEntries; index++) + { + if ( MIMEType.Equals(extraMimeEntries[index].mMimeType) ) + { + // This is the one. Set attributes appropriately. + aMIMEInfo->SetFileExtensions(nsDependentCString(extraMimeEntries[index].mFileExtensions)); + aMIMEInfo->SetDescription(NS_ConvertASCIItoUTF16(extraMimeEntries[index].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, aMIMEInfo); +} + +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<nsIMIMEInfo> mimeInfo = GetMIMEInfoFromOS(EmptyCString(), aExtension, &found); + return found && mimeInfo && NS_SUCCEEDED(mimeInfo->GetMIMEType(aMIMEType)); +} |