Source code

Revision control

Other Tools

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim:expandtab:shiftwidth=2:tabstop=2:cin:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "base/basictypes.h"
/* This must occur *after* base/basictypes.h to avoid typedefs conflicts. */
#include "mozilla/ArrayUtils.h"
#include "mozilla/Base64.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/StaticPrefs_security.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 "nsAppDirectoryServiceDefs.h"
#include "nsICategoryManager.h"
#include "nsDependentSubstring.h"
#include "nsString.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 "nsIMutableArray.h"
#include "nsIRedirectHistoryEntry.h"
#include "nsOSHelperAppService.h"
#include "nsOSHelperAppServiceChild.h"
#include "nsContentSecurityUtils.h"
// used to access our datastore of user-configured helper applications
#include "nsIHandlerService.h"
#include "nsIMIMEInfo.h"
#include "nsIHelperAppLauncherDialog.h"
#include "nsIContentDispatchChooser.h"
#include "nsNetUtil.h"
#include "nsIPrivateBrowsingChannel.h"
#include "nsIIOService.h"
#include "nsNetCID.h"
#include "nsIApplicationReputation.h"
#include "nsDSURIContentListener.h"
#include "nsMimeTypes.h"
// 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 "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 "nsDocShellCID.h"
#include "nsCRT.h"
#include "nsLocalHandlerApp.h"
#include "nsIRandomGenerator.h"
#include "ContentChild.h"
#include "nsXULAppAPI.h"
#include "nsPIDOMWindow.h"
#include "ExternalHelperAppChild.h"
#ifdef XP_WIN
# include "nsWindowsHelpers.h"
#endif
#include "mozilla/Components.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Preferences.h"
#include "mozilla/ipc/URIUtils.h"
using namespace mozilla;
using namespace mozilla::ipc;
using namespace mozilla::dom;
// 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) {
// We need the unescaper
nsresult rv;
nsCOMPtr<nsITextToSubURI> textToSubURI =
do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
return textToSubURI->UnEscapeURIForUI(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);
bool gotFileNameFromURI = false;
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()) {
gotFileNameFromURI = true;
rv = UnescapeFragment(leafName, url, aFileName);
if (NS_FAILED(rv)) {
CopyUTF8toUTF16(leafName, aFileName); // use escaped name
}
}
}
// If we have a filename and no extension, remove trailing dots from the
// filename and extract the extension if that is possible.
if (aExtension.IsEmpty() && !aFileName.IsEmpty()) {
// Windows ignores terminating dots. So we have to as well, so
// that our security checks do "the right thing"
aFileName.Trim(".", false);
// We can get an extension if the filename is from a header, or if getting
// it from the URL was allowed.
bool canGetExtensionFromFilename =
!gotFileNameFromURI || aAllowURLExtension;
// ... , or if the mimetype is meaningless and we have nothing to go on:
if (!canGetExtensionFromFilename) {
nsAutoCString contentType;
if (NS_SUCCEEDED(aChannel->GetContentType(contentType))) {
canGetExtensionFromFilename =
contentType.EqualsIgnoreCase(APPLICATION_OCTET_STREAM) ||
contentType.EqualsIgnoreCase("binary/octet-stream") ||
contentType.EqualsIgnoreCase("application/x-msdownload");
}
}
if (canGetExtensionFromFilename) {
// 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 DownloadIntegration.jsm.
*
* 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)
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"},
{IMAGE_PNG, "png"},
// -- end extensions used during startup
{TEXT_CSS, "css"},
{IMAGE_JPEG, "jpeg"},
{IMAGE_JPEG, "jpg"},
{IMAGE_SVG_XML, "svg"},
{TEXT_HTML, "html"},
{TEXT_HTML, "htm"},
{APPLICATION_XPINSTALL, "xpi"},
{"application/xhtml+xml", "xhtml"},
{"application/xhtml+xml", "xht"},
{TEXT_PLAIN, "txt"},
{APPLICATION_JSON, "json"},
{APPLICATION_XJAVASCRIPT, "js"},
{APPLICATION_XJAVASCRIPT, "jsm"},
{VIDEO_OGG, "ogv"},
{VIDEO_OGG, "ogg"},
{APPLICATION_OGG, "ogg"},
{AUDIO_OGG, "oga"},
{AUDIO_OGG, "opus"},
{APPLICATION_PDF, "pdf"},
{VIDEO_WEBM, "webm"},
{AUDIO_WEBM, "webm"},
{IMAGE_ICO, "ico"},
{TEXT_PLAIN, "properties"},
{TEXT_PLAIN, "locale"},
{TEXT_PLAIN, "ftl"},
#if defined(MOZ_WMF)
{VIDEO_MP4, "mp4"},
{AUDIO_MP4, "m4a"},
{AUDIO_MP3, "mp3"},
#endif
#ifdef MOZ_RAW
{VIDEO_RAW, "yuv"}
#endif
};
/**
* This is a small private struct used to help us initialize some
* default mime types.
*/
struct nsExtraMimeTypeEntry {
const char* mMimeType;
const char* mFileExtensions;
const char* mDescription;
};
/**
* This table lists all of the 'extra' content types that we can deduce from
* particular file extensions. These entries also ensure that we provide a good
* descriptive name when we encounter files with these content types and/or
* extensions. These can be overridden by user helper app prefs. If you add
* types here, make sure they are lowercase, or you'll regret it.
*/
static const nsExtraMimeTypeEntry extraMimeEntries[] = {
#if defined(XP_MACOSX) // don't define .bin on the mac...use internet config to
// look that up...
{APPLICATION_OCTET_STREAM, "exe,com", "Binary File"},
#else
{APPLICATION_OCTET_STREAM, "exe,com,bin", "Binary File"},
#endif
{APPLICATION_GZIP2, "gz", "gzip"},
{"application/x-arj", "arj", "ARJ file"},
{"application/rtf", "rtf", "Rich Text Format File"},
{APPLICATION_ZIP, "zip", "ZIP Archive"},
{APPLICATION_XPINSTALL, "xpi", "XPInstall Install"},
{APPLICATION_PDF, "pdf", "Portable Document Format"},
{APPLICATION_POSTSCRIPT, "ps,eps,ai", "Postscript File"},
{APPLICATION_XJAVASCRIPT, "js", "Javascript Source File"},
{APPLICATION_XJAVASCRIPT, "jsm,mjs", "Javascript Module Source File"},
#ifdef MOZ_WIDGET_ANDROID
{"application/vnd.android.package-archive", "apk", "Android Package"},
#endif
// OpenDocument formats
{"application/vnd.oasis.opendocument.text", "odt", "OpenDocument Text"},
{"application/vnd.oasis.opendocument.presentation", "odp",
"OpenDocument Presentation"},
{"application/vnd.oasis.opendocument.spreadsheet", "ods",
"OpenDocument Spreadsheet"},
{"application/vnd.oasis.opendocument.graphics", "odg",
"OpenDocument Graphics"},
// Legacy Microsoft Office
{"application/msword", "doc", "Microsoft Word"},
{"application/vnd.ms-powerpoint", "ppt", "Microsoft PowerPoint"},
{"application/vnd.ms-excel", "xls", "Microsoft Excel"},
// Office Open XML
{"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"docx", "Microsoft Word (Open XML)"},
{"application/"
"vnd.openxmlformats-officedocument.presentationml.presentation",
"pptx", "Microsoft PowerPoint (Open XML)"},
{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xlsx", "Microsoft Excel (Open XML)"},
// Note: if you add new image types, please also update the list in
// contentAreaUtils.js to match.
{IMAGE_ART, "art", "ART Image"},
{IMAGE_BMP, "bmp", "BMP Image"},
{IMAGE_GIF, "gif", "GIF Image"},
{IMAGE_ICO, "ico,cur", "ICO Image"},
{IMAGE_JPEG, "jpg,jpeg,jfif,pjpeg,pjp", "JPEG Image"},
{IMAGE_PNG, "png", "PNG Image"},
{IMAGE_APNG, "apng", "APNG Image"},
{IMAGE_TIFF, "tiff,tif", "TIFF Image"},
{IMAGE_XBM, "xbm", "XBM Image"},
{IMAGE_SVG_XML, "svg", "Scalable Vector Graphics"},
{IMAGE_WEBP, "webp", "WebP Image"},
{IMAGE_AVIF, "avif", "AV1 Image File"},
{MESSAGE_RFC822, "eml", "RFC-822 data"},
{TEXT_PLAIN, "txt,text", "Text File"},
{APPLICATION_JSON, "json", "JavaScript Object Notation"},
{TEXT_VTT, "vtt", "Web Video Text Tracks"},
{TEXT_CACHE_MANIFEST, "appcache", "Application Cache Manifest"},
{TEXT_HTML, "html,htm,shtml,ehtml", "HyperText Markup Language"},
{"application/xhtml+xml", "xhtml,xht",
"Extensible HyperText Markup Language"},
{APPLICATION_MATHML_XML, "mml", "Mathematical Markup Language"},
{APPLICATION_RDF, "rdf", "Resource Description Framework"},
{"text/csv", "csv", "CSV File"},
{TEXT_XML, "xml,xsl,xbl", "Extensible Markup Language"},
{TEXT_CSS, "css", "Style Sheet"},
{TEXT_VCARD, "vcf,vcard", "Contact Information"},
{TEXT_CALENDAR, "ics", "iCalendar"},
{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"},
{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"},
{AUDIO_AAC, "aac", "AAC Audio"},
{AUDIO_FLAC, "flac", "FLAC Audio"},
{AUDIO_MIDI, "mid", "Standard MIDI Audio"},
{APPLICATION_WASM, "wasm", "WebAssembly Module"}};
static const nsDefaultMimeTypeEntry sForbiddenPrimaryExtensions[] = {
{IMAGE_JPEG, "jfif"}};
/**
* File extensions for which decoding should be disabled.
* NOTE: These MUST be lower-case and ASCII.
*/
static const nsDefaultMimeTypeEntry nonDecodableExtensions[] = {
{APPLICATION_GZIP, "gz"},
{APPLICATION_GZIP, "tgz"},
{APPLICATION_ZIP, "zip"},
{APPLICATION_COMPRESS, "z"},
{APPLICATION_GZIP, "svgz"}};
/**
* Mimetypes for which we enforce using a known extension.
*
* In addition to this list, we do this for all audio/, video/ and
* image/ mimetypes.
*/
static const char* forcedExtensionMimetypes[] = {
// OpenDocument formats
"application/vnd.oasis.opendocument.text",
"application/vnd.oasis.opendocument.presentation",
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.oasis.opendocument.graphics",
// Legacy Microsoft Office
"application/msword", "application/vnd.ms-powerpoint",
"application/vnd.ms-excel",
// Office Open XML
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
// Note: zip and json mimetypes are commonly used with a variety of
// extensions; don't add them here. It's a similar story for text/xml,
// but slightly worse because we can use it when sniffing for a mimetype
// if one hasn't been provided, so don't re-add that here either.
APPLICATION_PDF,
APPLICATION_OGG,
APPLICATION_WASM,
TEXT_CALENDAR, TEXT_CSS, TEXT_VCARD};
/**
* Primary extensions of types whose descriptions should be overwritten.
* This extension is concatenated with "ExtHandlerDescription" to look up the
* description in unknownContentType.properties.
* NOTE: These MUST be lower-case and ASCII.
*/
static const char* descriptionOverwriteExtensions[] = {
"avif", "pdf", "svg", "webp", "xml",
};
static StaticRefPtr<nsExternalHelperAppService> sExtHelperAppSvcSingleton;
/**
* On Mac child processes, return an nsOSHelperAppServiceChild for remoting
* OS calls to the parent process. On all other platforms use
* nsOSHelperAppService.
*/
/* static */
already_AddRefed<nsExternalHelperAppService>
nsExternalHelperAppService::GetSingleton() {
if (!sExtHelperAppSvcSingleton) {
#ifdef XP_MACOSX
if (XRE_IsParentProcess()) {
sExtHelperAppSvcSingleton = new nsOSHelperAppService();
} else {
sExtHelperAppSvcSingleton = new nsOSHelperAppServiceChild();
}
#else
sExtHelperAppSvcSingleton = new nsOSHelperAppService();
#endif /* XP_MACOSX */
ClearOnShutdown(&sExtHelperAppSvcSingleton);
}
return do_AddRef(sExtHelperAppSvcSingleton);
}
NS_IMPL_ISUPPORTS(nsExternalHelperAppService, nsIExternalHelperAppService,
nsPIExternalAppLauncher, nsIExternalProtocolService,
nsIMIMEService, nsIObserver, nsISupportsWeakReference)
nsExternalHelperAppService::nsExternalHelperAppService() {}
nsresult nsExternalHelperAppService::Init() {
// Add an observer for profile change
nsCOMPtr<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,
BrowsingContext* aContentContext, bool aForceSave,
nsIInterfaceRequestor* aWindowContext,
nsIStreamListener** aStreamListener) {
// We need to get a hold of a ContentChild so that we can begin forwarding
// this data to the parent. In the HTTP case, this is unfortunate, since
// we're actually passing data from parent->child->parent wastefully, but
// the Right Fix will eventually be to short-circuit those channels on the
// parent side based on some sort of subscription concept.
using mozilla::dom::ContentChild;
using mozilla::dom::ExternalHelperAppChild;
ContentChild* child = ContentChild::GetSingleton();
if (!child) {
return NS_ERROR_FAILURE;
}
nsCString disp;
nsCOMPtr<nsIURI> uri;
int64_t contentLength = -1;
bool wasFileChannel = false;
uint32_t contentDisposition = -1;
nsAutoString fileName;
nsCOMPtr<nsILoadInfo> loadInfo;
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);
loadInfo = channel->LoadInfo();
nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aRequest));
wasFileChannel = fileChan != nullptr;
}
nsCOMPtr<nsIURI> referrer;
NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer));
Maybe<mozilla::net::LoadInfoArgs> loadInfoArgs;
MOZ_ALWAYS_SUCCEEDS(LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs));
nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aRequest));
// Determine whether a new window was opened specifically for this request
bool shouldCloseWindow = false;
if (props) {
props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns,
&shouldCloseWindow);
}
// Now we build a protocol for forwarding our data to the parent. The
// protocol will act as a listener on the child-side and create a "real"
// helperAppService listener on the parent-side, via another call to
// DoContent.
RefPtr<ExternalHelperAppChild> childListener = new ExternalHelperAppChild();
MOZ_ALWAYS_TRUE(child->SendPExternalHelperAppConstructor(
childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp,
contentDisposition, fileName, aForceSave, contentLength, wasFileChannel,
referrer, aContentContext, shouldCloseWindow));
NS_ADDREF(*aStreamListener = childListener);
uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
RefPtr<nsExternalAppHandler> handler =
new nsExternalAppHandler(nullptr, ""_ns, aContentContext, aWindowContext,
this, fileName, reason, aForceSave);
if (!handler) {
return NS_ERROR_OUT_OF_MEMORY;
}
childListener->SetHandler(handler);
return NS_OK;
}
NS_IMETHODIMP nsExternalHelperAppService::CreateListener(
const nsACString& aMimeContentType, nsIRequest* aRequest,
BrowsingContext* aContentContext, bool aForceSave,
nsIInterfaceRequestor* aWindowContext,
nsIStreamListener** aStreamListener) {
MOZ_ASSERT(!XRE_IsContentProcess());
nsAutoString fileName;
nsAutoCString fileExtension;
uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
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 = !net::ChannelIsPost(channel);
// 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
if (uri->SchemeIs("http") || uri->SchemeIs("https")) {
url->GetQuery(query);
}
// Only get the extension if the query is empty; if it isn't, then the
// extension likely belongs to a cgi script and isn't helpful
allowURLExt = query.IsEmpty();
}
}
// 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(""_ns, 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(
nsLiteralCString(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);
// NB: ExternalHelperAppParent depends on this listener always being an
// nsExternalAppHandler. If this changes, make sure to update that code.
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::DoContent(
const nsACString& aMimeContentType, nsIRequest* aRequest,
nsIInterfaceRequestor* aContentContext, bool aForceSave,
nsIInterfaceRequestor* aWindowContext,
nsIStreamListener** aStreamListener) {
// Scripted interface requestors cannot return an instance of the
// (non-scriptable) nsPIDOMWindowOuter or nsPIDOMWindowInner interfaces, so
// get to the window via `nsIDOMWindow`. Unfortunately, at that point we
// don't know whether the thing we got is an inner or outer window, so have to
// work with either one.
RefPtr<BrowsingContext> bc;
nsCOMPtr<nsIDOMWindow> domWindow = do_GetInterface(aContentContext);
if (nsCOMPtr<nsPIDOMWindowOuter> outerWindow = do_QueryInterface(domWindow)) {
bc = outerWindow->GetBrowsingContext();
} else if (nsCOMPtr<nsPIDOMWindowInner> innerWindow =
do_QueryInterface(domWindow)) {
bc = innerWindow->GetBrowsingContext();
}
if (XRE_IsContentProcess()) {
return DoContentContentProcessHelper(aMimeContentType, aRequest, bc,
aForceSave, aWindowContext,
aStreamListener);
}
nsresult rv = CreateListener(aMimeContentType, aRequest, bc, aForceSave,
aWindowContext, aStreamListener);
return rv;
}
NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension(
const nsACString& aExtension, const nsACString& aEncodingType,
bool* aApplyDecoding) {
*aApplyDecoding = true;
uint32_t i;
for (i = 0; i < ArrayLength(nonDecodableExtensions); ++i) {
if (aExtension.LowerCaseEqualsASCII(
nonDecodableExtensions[i].mFileExtension) &&
aEncodingType.LowerCaseEqualsASCII(
nonDecodableExtensions[i].mMimeType)) {
*aApplyDecoding = false;
break;
}
}
return NS_OK;
}
nsresult nsExternalHelperAppService::GetFileTokenForPath(
const char16_t* aPlatformAppPath, nsIFile** aFile) {
nsDependentString platformAppPath(aPlatformAppPath);
// First, check if we have an absolute path
nsIFile* localFile = nullptr;
nsresult rv = NS_NewLocalFile(platformAppPath, true, &localFile);
if (NS_SUCCEEDED(rv)) {
*aFile = localFile;
bool exists;
if (NS_FAILED((*aFile)->Exists(&exists)) || !exists) {
NS_RELEASE(*aFile);
return NS_ERROR_FILE_NOT_FOUND;
}
return NS_OK;
}
// Second, check if file exists in mozilla program directory
rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, aFile);
if (NS_SUCCEEDED(rv)) {
rv = (*aFile)->Append(platformAppPath);
if (NS_SUCCEEDED(rv)) {
bool exists = false;
rv = (*aFile)->Exists(&exists);
if (NS_SUCCEEDED(rv) && exists) return NS_OK;
}
NS_RELEASE(*aFile);
}
return NS_ERROR_NOT_AVAILABLE;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
// begin external protocol service default implementation...
//////////////////////////////////////////////////////////////////////////////////////////////////////
NS_IMETHODIMP nsExternalHelperAppService::ExternalProtocolHandlerExists(
const char* aProtocolScheme, bool* aHandlerExists) {
nsCOMPtr<nsIHandlerInfo> handlerInfo;
nsresult rv = GetProtocolHandlerInfo(nsDependentCString(aProtocolScheme),
getter_AddRefs(handlerInfo));
if (NS_SUCCEEDED(rv)) {
// See if we have any known possible handler apps for this
nsCOMPtr<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;
}
static const char kExternalProtocolPrefPrefix[] =
"network.protocol-handler.external.";
static const char kExternalProtocolDefaultPref[] =
"network.protocol-handler.external-default";
NS_IMETHODIMP
nsExternalHelperAppService::LoadURI(nsIURI* aURI,
nsIPrincipal* aTriggeringPrincipal,
BrowsingContext* aBrowsingContext) {
NS_ENSURE_ARG_POINTER(aURI);
if (XRE_IsContentProcess()) {
mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal(
aURI, aTriggeringPrincipal, aBrowsingContext);
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
}
// Now check if the principal is allowed to access the navigated context.
// We allow navigating subframes, even if not same-origin - non-external
// links can always navigate everywhere, so this is a minor additional
// restriction, only aiming to prevent some types of spoofing attacks
// from otherwise disjoint browsingcontext trees.
if (aBrowsingContext && aTriggeringPrincipal &&
!StaticPrefs::security_allow_disjointed_external_uri_loads() &&
// Add-on principals are always allowed:
!BasePrincipal::Cast(aTriggeringPrincipal)->AddonPolicy() &&
// As is chrome code:
!aTriggeringPrincipal->IsSystemPrincipal()) {
RefPtr<BrowsingContext> bc = aBrowsingContext;
WindowGlobalParent* wgp = bc->Canonical()->GetCurrentWindowGlobal();
bool foundAccessibleFrame = false;
// Also allow this load if the target is a toplevel BC and contains a
// non-web-controlled about:blank document
if (bc->IsTop() && !bc->HadOriginalOpener() && wgp) {
RefPtr<nsIURI> uri = wgp->GetDocumentURI();
foundAccessibleFrame =
uri && uri->GetSpecOrDefault().EqualsLiteral("about:blank");
}
while (!foundAccessibleFrame) {
if (wgp) {
foundAccessibleFrame =
aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal());
}
// We have to get the parent via the bc, because there may not
// be a window global for the innermost bc; see bug 1650162.
BrowsingContext* parent = bc->GetParent();
if (!parent) {
break;
}
bc = parent;
wgp = parent->Canonical()->GetCurrentWindowGlobal();
}
if (!foundAccessibleFrame) {
// See if this navigation could have come from a subframe.
nsTArray<RefPtr<BrowsingContext>> contexts;
aBrowsingContext->GetAllBrowsingContextsInSubtree(contexts);
for (const auto& kid : contexts) {
wgp = kid->Canonical()->GetCurrentWindowGlobal();
if (wgp && aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal())) {
foundAccessibleFrame = true;
break;
}
}
}
if (!foundAccessibleFrame) {
return NS_OK; // deny the load.
}
}
nsCOMPtr<nsIHandlerInfo> handler;
rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIContentDispatchChooser> chooser =
do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);
return chooser->HandleURI(handler, uri, aTriggeringPrincipal,
aBrowsingContext);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
// 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, ""_ns);
if (NS_SUCCEEDED(rv)) return NS_OK;
}
}
return SetProtocolHandlerDefaults(*aHandlerInfo, exists);
}
NS_IMETHODIMP
nsExternalHelperAppService::SetProtocolHandlerDefaults(
nsIHandlerInfo* aHandlerInfo, bool aOSHandlerExists) {
// this type isn't in our database, so we've only got an OS default handler,
// if one exists
if (aOSHandlerExists) {
// we've got a default, so use it
aHandlerInfo->SetPreferredAction(nsIHandlerInfo::useSystemDefault);
// whether or not to ask the user depends on the warning preference
nsAutoCString scheme;
aHandlerInfo->GetType(scheme);
nsAutoCString warningPref(kExternalWarningPrefPrefix);
warningPref += scheme;
bool warn;
if (NS_FAILED(Preferences::GetBool(warningPref.get(), &warn))) {
// no scheme-specific value, check the default
warn = Preferences::GetBool(kExternalWarningDefaultPref, true);
}
aHandlerInfo->SetAlwaysAskBeforeHandling(warn);
} else {
// If no OS default existed, we set the preferred action to alwaysAsk.
// This really means not initialized (i.e. there's no available handler)
// to all the code...
aHandlerInfo->SetPreferredAction(nsIHandlerInfo::alwaysAsk);
}
return NS_OK;
}
// XPCOM profile change observer
NS_IMETHODIMP
nsExternalHelperAppService::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* someData) {
if (!strcmp(aTopic, "profile-before-change")) {
ExpungeTemporaryFiles();
} else if (!strcmp(aTopic, "last-pb-context-exited")) {
ExpungeTemporaryPrivateFiles();
}
return NS_OK;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
// begin external app handler implementation
//////////////////////////////////////////////////////////////////////////////////////////////////////
NS_IMPL_ADDREF(nsExternalAppHandler)
NS_IMPL_RELEASE(nsExternalAppHandler)
NS_INTERFACE_MAP_BEGIN(nsExternalAppHandler)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener)
NS_INTERFACE_MAP_ENTRY(nsIStreamListener)
NS_INTERFACE_MAP_ENTRY(nsIRequestObserver)
NS_INTERFACE_MAP_ENTRY(nsIHelperAppLauncher)
NS_INTERFACE_MAP_ENTRY(nsICancelable)
NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver)
NS_INTERFACE_MAP_ENTRY(nsINamed)
NS_INTERFACE_MAP_ENTRY_CONCRETE(nsExternalAppHandler)
NS_INTERFACE_MAP_END
nsExternalAppHandler::nsExternalAppHandler(
nsIMIMEInfo* aMIMEInfo, const nsACString& aTempFileExtension,
BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext,
nsExternalHelperAppService* aExtProtSvc,
const nsAString& aSuggestedFilename, uint32_t aReason, bool aForceSave)
: mMimeInfo(aMIMEInfo),
mBrowsingContext(aBrowsingContext),
mWindowContext(aWindowContext),
mSuggestedFileName(aSuggestedFilename),
mForceSave(aForceSave),
mCanceled(false),
mStopRequestIssued(false),
mIsFileChannel(false),
mShouldCloseWindow(false),
mHandleInternally(false),
mReason(aReason),
mTempFileIsExecutable(false),
mTimeDownloadStarted(0),
mContentLength(-1),
mProgress(0),
mSaver(nullptr),
mDialogProgressListener(nullptr),
mTransfer(nullptr),
mRequest(nullptr),
mExtProtSvc(aExtProtSvc) {
// make sure the extention includes the '.'
if (!aTempFileExtension.IsEmpty() && aTempFileExtension.First() != '.')
mTempFileExtension = char16_t('.');
AppendUTF8toUTF16(aTempFileExtension, mTempFileExtension);
// Get mSuggestedFileName's current file extension.
nsAutoString originalFileExt;
int32_t pos = mSuggestedFileName.RFindChar('.');
if (pos != kNotFound) {
mSuggestedFileName.Right(originalFileExt,
mSuggestedFileName.Length() - pos);
}
// replace platform specific path separator and illegal characters to avoid
// any confusion.
// Try to keep the use of spaces or underscores in sync with the Downloads
// code sanitization in DownloadPaths.jsm
mSuggestedFileName.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
mSuggestedFileName.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' ');
mSuggestedFileName.ReplaceChar(char16_t(0), '_');
mTempFileExtension.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
mTempFileExtension.ReplaceChar(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, '_');
// Remove trailing or leading spaces that we may have generated while
// sanitizing.
mSuggestedFileName.CompressWhitespace();
mTempFileExtension.CompressWhitespace();
EnsureCorrectExtension(originalFileExt);
mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096);
}
nsExternalAppHandler::~nsExternalAppHandler() {
MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted");
}
bool nsExternalAppHandler::ShouldForceExtension(const nsString& aFileExt) {
nsAutoCString MIMEType;
if (!mMimeInfo || NS_FAILED(mMimeInfo->GetMIMEType(MIMEType))) {
return false;
}
bool canForce = StringBeginsWith(MIMEType, "image/"_ns) ||
StringBeginsWith(MIMEType, "audio/"_ns) ||
StringBeginsWith(MIMEType, "video/"_ns);
if (!canForce &&
StaticPrefs::browser_download_sanitize_non_media_extensions()) {
for (const char* mime : forcedExtensionMimetypes) {
if (MIMEType.Equals(mime)) {
canForce = true;
break;
}
}
if (!canForce) {
return false;
}
}
// If we get here, we know for sure the mimetype allows us to overwrite the
// existing extension, if it's wrong. Return whether the extension is wrong:
bool knownExtension = false;
// Note that aFileExt is either empty or consists of an extension
// *including the dot* which we remove for ExtensionExists().
return (
aFileExt.IsEmpty() || aFileExt.EqualsLiteral(".") ||
(NS_SUCCEEDED(mMimeInfo->ExtensionExists(
Substring(NS_ConvertUTF16toUTF8(aFileExt), 1), &knownExtension)) &&
!knownExtension));
}
void nsExternalAppHandler::EnsureCorrectExtension(const nsString& aFileExt) {
// If we don't have an extension (which will include the .),
// just short-circuit.
if (mTempFileExtension.Length() <= 1) {
return;
}
// After removing trailing whitespaces from the name, if we have a
// temp file extension, there are broadly 2 cases where we want to
// replace the extension.
// First, if the file extension contains invalid characters.
// Second, for document type mimetypes, if the extension is either
// missing or not valid for this mimetype.
bool replaceExtension =
(aFileExt.FindCharInSet(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS) !=
kNotFound) ||
ShouldForceExtension(aFileExt);
if (replaceExtension) {
int32_t pos = mSuggestedFileName.RFindChar('.');
if (pos != kNotFound) {
mSuggestedFileName =
Substring(mSuggestedFileName, 0, pos) + mTempFileExtension;
} else {
mSuggestedFileName.Append(mTempFileExtension);
}
}
/*
* Ensure we don't double-append the file extension if it matches:
*/
if (replaceExtension ||
aFileExt.Equals(mTempFileExtension, nsCaseInsensitiveStringComparator)) {
// Matches -> mTempFileExtension can be empty
mTempFileExtension.Truncate();
}
}
void nsExternalAppHandler::DidDivertRequest(nsIRequest* request) {
MOZ_ASSERT(XRE_IsContentProcess(), "in child process");
// Remove our request from the child loadGroup
RetargetLoadNotifications(request);
}
NS_IMETHODIMP nsExternalAppHandler::SetWebProgressListener(
nsIWebProgressListener2* aWebProgressListener) {
// This is always called by nsHelperDlg.js. Go ahead and register the
// progress listener. At this point, we don't have mTransfer.
mDialogProgressListener = aWebProgressListener;
return NS_OK;
}
NS_IMETHODIMP nsExternalAppHandler::GetTargetFile(nsIFile** aTarget) {
if (mFinalFileDestination)
*aTarget = mFinalFileDestination;
else
*aTarget = mTempFile;
NS_IF_ADDREF(*aTarget);
return NS_OK;
}
NS_IMETHODIMP nsExternalAppHandler::GetTargetFileIsExecutable(bool* aExec) {
// Use the real target if it's been set
if (mFinalFileDestination) return mFinalFileDestination->IsExecutable(aExec);
// Otherwise, use the stored executable-ness of the temporary
*aExec = mTempFileIsExecutable;
return NS_OK;
}
NS_IMETHODIMP nsExternalAppHandler::GetTimeDownloadStarted(PRTime* aTime) {
*aTime = mTimeDownloadStarted;
return NS_OK;
}
NS_IMETHODIMP nsExternalAppHandler::GetContentLength(int64_t* aContentLength) {
*aContentLength = mContentLength;
return NS_OK;
}
NS_IMETHODIMP nsExternalAppHandler::GetBrowsingContextId(
uint64_t* aBrowsingContextId) {
*aBrowsingContextId = mBrowsingContext->Id();
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;
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);
}
}
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, u".part"_ns),
NS_ERROR_UNEXPECTED);
// Strip off the ".part" from mTempLeafName
mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1);
MOZ_ASSERT(!mSaver, "Output file initialization called more than once!");
mSaver =
do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = mSaver->SetObserver(this);
if (NS_FAILED(rv)) {
mSaver = nullptr;
return rv;
}
rv = mSaver->EnableSha256();
NS_ENSURE_SUCCESS(rv, rv);
rv = mSaver->EnableSignatureInfo();
NS_ENSURE_SUCCESS(rv, rv);
LOG(("Enabled hashing and signature verification"));
rv = mSaver->SetTarget(mTempFile, false);
NS_ENSURE_SUCCESS(rv, rv);
return rv;
}
void nsExternalAppHandler::MaybeApplyDecodingForExtension(
nsIRequest* aRequest) {
MOZ_ASSERT(aRequest);
nsCOMPtr<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);
}
already_AddRefed<nsIInterfaceRequestor>
nsExternalAppHandler::GetDialogParent() {
nsCOMPtr<nsIInterfaceRequestor> dialogParent = mWindowContext;
if (!dialogParent && mBrowsingContext) {
dialogParent = do_QueryInterface(mBrowsingContext->GetDOMWindow());
}
if (!dialogParent && mBrowsingContext && XRE_IsParentProcess()) {
RefPtr<Element> element = mBrowsingContext->Top()->GetEmbedderElement();
if (element) {
dialogParent = do_QueryInterface(element->OwnerDoc()->GetWindow());
}
}
return dialogParent.forget();
}
NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
MOZ_ASSERT(request, "OnStartRequest without request?");
// Set mTimeDownloadStarted here as the download has already started and
// we want to record the start time before showing the filepicker.
mTimeDownloadStarted = PR_Now();
mRequest = request;
nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
nsresult rv;
nsAutoCString MIMEType;
if (mMimeInfo) {
mMimeInfo->GetMIMEType(MIMEType);
}
// Now get the URI
if (aChannel) {
aChannel->GetURI(getter_AddRefs(mSourceUrl));
}
mDownloadClassification =
nsContentSecurityUtils::ClassifyDownload(aChannel, MIMEType);
if (mDownloadClassification == nsITransfer::DOWNLOAD_FORBIDDEN) {
// If the download is rated as forbidden,
// cancel the request so no ui knows about this.
mCanceled = true;
request->Cancel(NS_ERROR_ABORT);
return NS_OK;
}
nsCOMPtr<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();
}