Source code
Revision control
Copy as Markdown
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
#include "base/basictypes.h"
/* This must occur *after* base/basictypes.h to avoid typedefs conflicts. */
#include "mozilla/ArrayUtils.h"
#include "mozilla/Base64.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/BrowserChild.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/WindowGlobalParent.h"
#include "mozilla/RandomNum.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_security.h"
#include "mozilla/StaticPtr.h"
#include "nsXULAppAPI.h"
#include "ExternalHelperAppParent.h"
#include "nsExternalHelperAppService.h"
#include "nsCExternalHandlerService.h"
#include "nsIURI.h"
#include "nsIURL.h"
#include "nsIFile.h"
#include "nsIFileURL.h"
#include "nsIChannel.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsICategoryManager.h"
#include "nsDependentSubstring.h"
#include "nsSandboxFlags.h"
#include "nsString.h"
#include "nsUnicharUtils.h"
#include "nsIStringEnumerator.h"
#include "nsIStreamListener.h"
#include "nsIMIMEService.h"
#include "nsILoadGroup.h"
#include "nsIWebProgressListener.h"
#include "nsITransfer.h"
#include "nsReadableUtils.h"
#include "nsIRequest.h"
#include "nsDirectoryServiceDefs.h"
#include "nsIInterfaceRequestor.h"
#include "nsThreadUtils.h"
#include "nsIMutableArray.h"
#include "nsIRedirectHistoryEntry.h"
#include "nsOSHelperAppService.h"
#include "nsOSHelperAppServiceChild.h"
#include "nsContentSecurityUtils.h"
#include "nsUTF8Utils.h"
#include "nsUnicodeProperties.h"
// used to access our datastore of user-configured helper applications
#include "nsIHandlerService.h"
#include "nsIMIMEInfo.h"
#include "nsIHelperAppLauncherDialog.h"
#include "nsIContentDispatchChooser.h"
#include "nsNetUtil.h"
#include "nsIPrivateBrowsingChannel.h"
#include "nsIIOService.h"
#include "nsNetCID.h"
#include "nsIApplicationReputation.h"
#include "nsDSURIContentListener.h"
#include "nsMimeTypes.h"
#include "nsMIMEInfoImpl.h"
// used for header disposition information.
#include "nsIHttpChannel.h"
#include "nsIHttpChannelInternal.h"
#include "nsIEncodedChannel.h"
#include "nsIMultiPartChannel.h"
#include "nsIFileChannel.h"
#include "nsIObserverService.h" // so we can be a profile change observer
#include "nsIPropertyBag2.h" // for the 64-bit content length
#ifdef XP_MACOSX
# include "nsILocalFileMac.h"
#endif
#include "nsEscape.h"
#include "nsIStringBundle.h" // XXX needed to localize error msgs
#include "nsIPrompt.h"
#include "nsITextToSubURI.h" // to unescape the filename
#include "nsDocShellCID.h"
#include "nsCRT.h"
#include "nsLocalHandlerApp.h"
#include "nsIRandomGenerator.h"
#include "ContentChild.h"
#include "nsXULAppAPI.h"
#include "nsPIDOMWindow.h"
#include "ExternalHelperAppChild.h"
#include "mozilla/dom/nsHTTPSOnlyUtils.h"
#ifdef XP_WIN
# include "nsWindowsHelpers.h"
# include "nsLocalFile.h"
#endif
#include "mozilla/Components.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Preferences.h"
#include "mozilla/ipc/URIUtils.h"
using namespace mozilla;
using namespace mozilla::ipc;
using namespace mozilla::dom;
#define kDefaultMaxFileNameLength 254
// Download Folder location constants
#define NS_PREF_DOWNLOAD_DIR "browser.download.dir"
#define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList"
enum {
NS_FOLDER_VALUE_DESKTOP = 0,
NS_FOLDER_VALUE_DOWNLOADS = 1,
NS_FOLDER_VALUE_CUSTOM = 2
};
LazyLogModule nsExternalHelperAppService::sLog("HelperAppService");
// Using level 3 here because the OSHelperAppServices use a log level
// of LogLevel::Debug (4), and we want less detailed output here
// Using 3 instead of LogLevel::Warning because we don't output warnings
#undef LOG
#define LOG(...) \
MOZ_LOG(nsExternalHelperAppService::sLog, mozilla::LogLevel::Info, \
(__VA_ARGS__))
#define LOG_ENABLED() \
MOZ_LOG_TEST(nsExternalHelperAppService::sLog, mozilla::LogLevel::Info)
static const char NEVER_ASK_FOR_SAVE_TO_DISK_PREF[] =
"browser.helperApps.neverAsk.saveToDisk";
static const char NEVER_ASK_FOR_OPEN_FILE_PREF[] =
"browser.helperApps.neverAsk.openFile";
StaticRefPtr<nsIFile> sFallbackDownloadDir;
// Helper functions for Content-Disposition headers
/**
* Given a URI fragment, unescape it
* @param aFragment The string to unescape
* @param aURI The URI from which this fragment is taken. Only its character set
* will be used.
* @param aResult [out] Unescaped string.
*/
static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI,
nsAString& aResult) {
// We need the unescaper
nsresult rv;
nsCOMPtr<nsITextToSubURI> textToSubURI =
do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
return textToSubURI->UnEscapeURIForUI(aFragment, /* aDontEscape = */ true,
aResult);
}
/**
* UTF-8 version of UnescapeFragment.
* @param aFragment The string to unescape
* @param aURI The URI from which this fragment is taken. Only its character set
* will be used.
* @param aResult [out] Unescaped string, UTF-8 encoded.
* @note It is safe to pass the same string for aFragment and aResult.
* @note When this function fails, aResult will not be modified.
*/
static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI,
nsACString& aResult) {
nsAutoString result;
nsresult rv = UnescapeFragment(aFragment, aURI, result);
if (NS_SUCCEEDED(rv)) CopyUTF16toUTF8(result, aResult);
return rv;
}
static Result<nsCOMPtr<nsIFile>, nsresult> GetOsTmpDownloadDirectory() {
nsCOMPtr<nsIFile> dir;
MOZ_TRY(NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dir)));
#if !defined(XP_MACOSX) && defined(XP_UNIX)
// Ensuring that only the current user can read the file names we end up
// creating. Note that creating directories with a specified permission is
// only supported on Unix platform right now. That's why the above check
// exists.
uint32_t permissions;
MOZ_TRY(dir->GetPermissions(&permissions));
if (permissions != PR_IRWXU) {
const char* userName = PR_GetEnv("USERNAME");
if (!userName || !*userName) {
userName = PR_GetEnv("USER");
}
if (!userName || !*userName) {
userName = PR_GetEnv("LOGNAME");
}
if (!userName || !*userName) {
userName = "mozillaUser";
}
nsAutoString userDir;
userDir.AssignLiteral("mozilla_");
userDir.AppendASCII(userName);
userDir.ReplaceChar(u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_');
int counter = 0;
bool pathExists;
nsCOMPtr<nsIFile> finalPath;
while (true) {
nsAutoString countedUserDir(userDir);
countedUserDir.AppendInt(counter, 10);
dir->Clone(getter_AddRefs(finalPath));
finalPath->Append(countedUserDir);
MOZ_TRY(finalPath->Exists(&pathExists));
if (pathExists) {
// If this path has the right permissions, use it.
MOZ_TRY(finalPath->GetPermissions(&permissions));
// Ensuring the path is writable by the current user.
bool isWritable;
MOZ_TRY(finalPath->IsWritable(&isWritable));
if (permissions == PR_IRWXU && isWritable) {
dir = finalPath;
break;
}
}
nsresult const rv = finalPath->Create(nsIFile::DIRECTORY_TYPE, PR_IRWXU);
if (NS_SUCCEEDED(rv)) {
dir = finalPath;
break;
}
if (rv != NS_ERROR_FILE_ALREADY_EXISTS) {
// Unexpected error.
return Err(rv);
}
counter++;
}
}
#endif
NS_ASSERTION(dir, "Somehow we didn't get a download directory!");
return dir;
}
/**
* Given an alleged download directory, either create it, or confirm that it
* already exists and is usable.
*/
static nsresult EnsureDirectoryExists(nsIFile* aDir) {
nsresult const rv = aDir->Create(nsIFile::DIRECTORY_TYPE, 0755);
if (rv == NS_ERROR_FILE_ALREADY_EXISTS || NS_SUCCEEDED(rv)) {
return NS_OK;
}
return rv;
};
/**
* Obtains the final directory to save downloads to. 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 the function of the same name in DownloadIntegration.sys.mjs.
*
* Optionally skip availability of the directory and storage.
*/
static Result<nsCOMPtr<nsIFile>, nsresult> GetPreferredDownloadsDirectory(
bool aSkipChecks = false) {
#if defined(ANDROID)
return Err(NS_ERROR_FAILURE);
#endif
nsresult rv;
// Try to get the users download location, if it's set.
switch (Preferences::GetInt(NS_PREF_DOWNLOAD_FOLDERLIST, -1)) {
case NS_FOLDER_VALUE_DESKTOP: {
nsCOMPtr<nsIFile> dir;
if (NS_SUCCEEDED(
NS_GetSpecialDirectory(NS_OS_DESKTOP_DIR, getter_AddRefs(dir)))) {
return dir;
}
} break;
case NS_FOLDER_VALUE_CUSTOM: {
nsCOMPtr<nsIFile> dir;
Preferences::GetComplex(NS_PREF_DOWNLOAD_DIR, NS_GET_IID(nsIFile),
getter_AddRefs(dir));
if (!dir) break;
// Check for availability if requested.
if (!aSkipChecks && NS_FAILED(EnsureDirectoryExists(dir))) {
break;
}
return dir;
} break;
default:
case NS_FOLDER_VALUE_DOWNLOADS:
// This is just the OS default location, so fall out
break;
}
// Fallthrough: get OS default directory
nsCOMPtr<nsIFile> dir;
rv = NS_GetSpecialDirectory(NS_OS_DEFAULT_DOWNLOAD_DIR, getter_AddRefs(dir));
if (NS_SUCCEEDED(rv)) {
return dir;
}
// On some OSes, there is no guarantee that `NS_OS_DEFAULT_DOWNLOAD_DIR`
// exists. Fall back to $HOME + Downloads.
// If we've done this before, use the cached value:
if (sFallbackDownloadDir) {
MOZ_TRY(sFallbackDownloadDir->Clone(getter_AddRefs(dir)));
return dir;
}
MOZ_TRY(NS_GetSpecialDirectory(NS_OS_HOME_DIR, getter_AddRefs(dir)));
// Get the appropriate translation of "Downloads"...
nsAutoString downloadLocalized;
rv = [&downloadLocalized]() {
nsresult rv;
nsCOMPtr<nsIStringBundleService> bundleService =
do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIStringBundle> downloadBundle;
rv = bundleService->CreateBundle(
"chrome://mozapps/locale/downloads/downloads.properties",
getter_AddRefs(downloadBundle));
NS_ENSURE_SUCCESS(rv, rv);
return downloadBundle->GetStringFromName("downloadsFolder",
downloadLocalized);
}();
// ... or, failing that, just use "Downloads".
if (NS_FAILED(rv)) {
downloadLocalized.AssignLiteral("Downloads");
}
MOZ_TRY(dir->Append(downloadLocalized));
// Cache the result for next time.
{
// Can't getter_AddRefs on StaticRefPtr, so do some copying.
nsCOMPtr<nsIFile> copy;
dir->Clone(getter_AddRefs(copy));
sFallbackDownloadDir = copy.forget();
ClearOnShutdown(&sFallbackDownloadDir);
}
// Check for availability if requested.
if (!aSkipChecks) {
MOZ_TRY(EnsureDirectoryExists(dir));
}
return dir;
}
NS_IMETHODIMP nsExternalHelperAppService::GetPreferredDownloadsDirectory(
nsIFile** aOutFile) {
auto res = ::GetPreferredDownloadsDirectory();
if (res.isErr()) return res.unwrapErr();
res.unwrap().forget(aOutFile);
return NS_OK;
}
/**
* Obtains the initial directory to save downloads to. (This may differ from the
* actual download directory if "browser.download.start_downloads_in_tmp_dir" is
* set.)
*
* Optionally, skip availability of the directory and storage.
*/
static Result<nsCOMPtr<nsIFile>, nsresult> GetInitialDownloadDirectory(
bool aSkipChecks = false) {
#if defined(ANDROID)
return Err(NS_ERROR_FAILURE);
#endif
if (StaticPrefs::browser_download_start_downloads_in_tmp_dir()) {
return GetOsTmpDownloadDirectory();
}
return GetPreferredDownloadsDirectory(aSkipChecks);
}
/**
* Helper for random bytes for the filename of downloaded part files.
*/
nsresult GenerateRandomName(nsACString& result) {
// We will request raw random bytes, and transform that to a base64 string,
// using url-based base64 encoding so that all characters from the base64
// result will be acceptable for filenames.
// For each three bytes of random data, we will get four bytes of ASCII.
// Request a bit more, to be safe, then truncate in the end.
nsresult rv;
const uint32_t wantedFileNameLength = 8;
const uint32_t requiredBytesLength =
static_cast<uint32_t>((wantedFileNameLength + 1) / 4 * 3);
uint8_t buffer[requiredBytesLength];
if (!mozilla::GenerateRandomBytesFromOS(buffer, requiredBytesLength)) {
return NS_ERROR_FAILURE;
}
nsAutoCString tempLeafName;
// We're forced to specify a padding policy, though this is guaranteed
// not to need padding due to requiredBytesLength being a multiple of 3.
rv = Base64URLEncode(requiredBytesLength, buffer,
Base64URLEncodePaddingPolicy::Omit, tempLeafName);
NS_ENSURE_SUCCESS(rv, rv);
tempLeafName.Truncate(wantedFileNameLength);
result.Assign(tempLeafName);
return NS_OK;
}
/**
* Structure for storing extension->type mappings.
* @see defaultMimeEntries
*/
struct nsDefaultMimeTypeEntry {
const char* mMimeType;
const char* mFileExtension;
};
/**
* Default extension->mimetype mappings. These are not overridable.
* If you add types here, make sure they are lowercase, or you'll regret it.
*/
static const nsDefaultMimeTypeEntry defaultMimeEntries[] = {
// The following are those extensions that we're asked about during startup,
// sorted by order used
{IMAGE_GIF, "gif"},
{TEXT_XML, "xml"},
{APPLICATION_RDF, "rdf"},
{IMAGE_PNG, "png"},
// -- end extensions used during startup
{TEXT_CSS, "css"},
{IMAGE_JPEG, "jpeg"},
{IMAGE_JPEG, "jpg"},
{IMAGE_SVG_XML, "svg"},
{TEXT_HTML, "html"},
{TEXT_HTML, "htm"},
{APPLICATION_XPINSTALL, "xpi"},
{"application/xhtml+xml", "xhtml"},
{"application/xhtml+xml", "xht"},
{TEXT_PLAIN, "txt"},
{APPLICATION_JSON, "json"},
{APPLICATION_XJAVASCRIPT, "mjs"},
{APPLICATION_XJAVASCRIPT, "js"},
{APPLICATION_XJAVASCRIPT, "jsm"},
{VIDEO_OGG, "ogv"},
{VIDEO_OGG, "ogg"},
{APPLICATION_OGG, "ogg"},
{AUDIO_OGG, "oga"},
{AUDIO_OGG, "opus"},
{APPLICATION_PDF, "pdf"},
{VIDEO_WEBM, "webm"},
{AUDIO_WEBM, "webm"},
{IMAGE_ICO, "ico"},
{TEXT_PLAIN, "properties"},
{TEXT_PLAIN, "locale"},
{TEXT_PLAIN, "ftl"},
#if defined(MOZ_WMF)
{VIDEO_MP4, "mp4"},
{AUDIO_MP4, "m4a"},
{AUDIO_MP3, "mp3"},
#endif
#ifdef MOZ_RAW
{VIDEO_RAW, "yuv"}
#endif
};
/**
* This is a small private struct used to help us initialize some
* default mime types.
*/
struct nsExtraMimeTypeEntry {
const char* mMimeType;
const char* mFileExtensions;
const char* mDescription;
};
/**
* This table lists all of the 'extra' content types that we can deduce from
* particular file extensions. These entries also ensure that we provide a good
* descriptive name when we encounter files with these content types and/or
* extensions. These can be overridden by user helper app prefs. If you add
* types here, make sure they are lowercase, or you'll regret it.
*/
static const nsExtraMimeTypeEntry extraMimeEntries[] = {
#if defined(XP_MACOSX) // don't define .bin on the mac...use internet config to
// look that up...
{APPLICATION_OCTET_STREAM, "exe,com", "Binary File"},
#else
{APPLICATION_OCTET_STREAM, "exe,com,bin", "Binary File"},
#endif
{APPLICATION_GZIP2, "gz", "gzip"},
{"application/x-arj", "arj", "ARJ file"},
{"application/rtf", "rtf", "Rich Text Format File"},
{APPLICATION_ZIP, "zip", "ZIP Archive"},
{APPLICATION_XPINSTALL, "xpi", "XPInstall Install"},
{APPLICATION_PDF, "pdf", "Portable Document Format"},
{APPLICATION_POSTSCRIPT, "ps,eps,ai", "Postscript File"},
{APPLICATION_XJAVASCRIPT, "js", "Javascript Source File"},
{APPLICATION_XJAVASCRIPT, "jsm,mjs", "Javascript Module Source File"},
#ifdef MOZ_WIDGET_ANDROID
{"application/vnd.android.package-archive", "apk", "Android Package"},
#endif
// OpenDocument formats
{"application/vnd.oasis.opendocument.text", "odt", "OpenDocument Text"},
{"application/vnd.oasis.opendocument.presentation", "odp",
"OpenDocument Presentation"},
{"application/vnd.oasis.opendocument.spreadsheet", "ods",
"OpenDocument Spreadsheet"},
{"application/vnd.oasis.opendocument.graphics", "odg",
"OpenDocument Graphics"},
// Legacy Microsoft Office
{"application/msword", "doc", "Microsoft Word"},
{"application/vnd.ms-powerpoint", "ppt", "Microsoft PowerPoint"},
{"application/vnd.ms-excel", "xls", "Microsoft Excel"},
// Office Open XML
{"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"docx", "Microsoft Word (Open XML)"},
{"application/"
"vnd.openxmlformats-officedocument.presentationml.presentation",
"pptx", "Microsoft PowerPoint (Open XML)"},
{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xlsx", "Microsoft Excel (Open XML)"},
{IMAGE_ART, "art", "ART Image"},
{IMAGE_BMP, "bmp", "BMP Image"},
{IMAGE_GIF, "gif", "GIF Image"},
{IMAGE_ICO, "ico,cur", "ICO Image"},
{IMAGE_JPEG, "jpg,jpeg,jfif,pjpeg,pjp", "JPEG Image"},
{IMAGE_PNG, "png", "PNG Image"},
{IMAGE_APNG, "apng", "APNG Image"},
{IMAGE_TIFF, "tiff,tif", "TIFF Image"},
{IMAGE_XBM, "xbm", "XBM Image"},
{IMAGE_SVG_XML, "svg", "Scalable Vector Graphics"},
{IMAGE_WEBP, "webp", "WebP Image"},
{IMAGE_AVIF, "avif", "AV1 Image File"},
{IMAGE_JXL, "jxl", "JPEG XL Image File"},
{MESSAGE_RFC822, "eml", "RFC-822 data"},
{TEXT_PLAIN, "txt,text", "Text File"},
{APPLICATION_JSON, "json", "JavaScript Object Notation"},
{TEXT_VTT, "vtt", "Web Video Text Tracks"},
{TEXT_CACHE_MANIFEST, "appcache", "Application Cache Manifest"},
{TEXT_HTML, "html,htm,shtml,ehtml", "HyperText Markup Language"},
{"application/xhtml+xml", "xhtml,xht",
"Extensible HyperText Markup Language"},
{APPLICATION_MATHML_XML, "mml", "Mathematical Markup Language"},
{APPLICATION_RDF, "rdf", "Resource Description Framework"},
{"text/csv", "csv", "CSV File"},
{TEXT_XML, "xml,xsl,xbl", "Extensible Markup Language"},
{TEXT_CSS, "css", "Style Sheet"},
{TEXT_VCARD, "vcf,vcard", "Contact Information"},
{TEXT_CALENDAR, "ics,ical,ifb,icalendar", "iCalendar"},
{VIDEO_OGG, "ogv,ogg", "Ogg Video"},
{APPLICATION_OGG, "ogg", "Ogg Video"},
{AUDIO_OGG, "oga", "Ogg Audio"},
{AUDIO_OGG, "opus", "Opus Audio"},
{VIDEO_WEBM, "webm", "Web Media Video"},
{AUDIO_WEBM, "webm", "Web Media Audio"},
{AUDIO_MP3, "mp3,mpega,mp2", "MPEG Audio"},
{VIDEO_MP4, "mp4,m4a,m4b", "MPEG-4 Video"},
{AUDIO_MP4, "m4a,m4b", "MPEG-4 Audio"},
{VIDEO_RAW, "yuv", "Raw YUV Video"},
{AUDIO_WAV, "wav", "Waveform Audio"},
{VIDEO_3GPP, "3gpp,3gp", "3GPP Video"},
{VIDEO_3GPP2, "3g2", "3GPP2 Video"},
{AUDIO_AAC, "aac", "AAC Audio"},
{AUDIO_FLAC, "flac", "FLAC Audio"},
{AUDIO_MIDI, "mid", "Standard MIDI Audio"},
{APPLICATION_WASM, "wasm", "WebAssembly Module"}};
static const nsDefaultMimeTypeEntry sForbiddenPrimaryExtensions[] = {
{IMAGE_JPEG, "jfif"}};
/**
* File extensions for which decoding should be disabled.
* NOTE: These MUST be lower-case and ASCII.
*/
static const nsDefaultMimeTypeEntry nonDecodableExtensions[] = {
{APPLICATION_GZIP, "gz"},
{APPLICATION_GZIP, "tgz"},
{APPLICATION_ZIP, "zip"},
{APPLICATION_COMPRESS, "z"},
{APPLICATION_GZIP, "svgz"}};
/**
* Mimetypes for which we enforce using a known extension.
*
* In addition to this list, we do this for all audio/, video/ and
* image/ mimetypes.
*/
static const char* forcedExtensionMimetypes[] = {
APPLICATION_PDF, APPLICATION_OGG, APPLICATION_WASM,
TEXT_CALENDAR, TEXT_CSS, TEXT_VCARD};
/**
* Primary extensions of types whose descriptions should be overwritten.
* This extension is concatenated with "ExtHandlerDescription" to look up the
* description in unknownContentType.properties.
* NOTE: These MUST be lower-case and ASCII.
*/
static const char* descriptionOverwriteExtensions[] = {
"avif", "jxl", "pdf", "svg", "webp", "xml",
};
static StaticRefPtr<nsExternalHelperAppService> sExtHelperAppSvcSingleton;
/**
* In child processes, return an nsOSHelperAppServiceChild for remoting
* OS calls to the parent process. In the parent process itself, use
* nsOSHelperAppService.
*/
/* static */
already_AddRefed<nsExternalHelperAppService>
nsExternalHelperAppService::GetSingleton() {
if (!sExtHelperAppSvcSingleton) {
if (XRE_IsParentProcess()) {
sExtHelperAppSvcSingleton = new nsOSHelperAppService();
} else {
sExtHelperAppSvcSingleton = new nsOSHelperAppServiceChild();
}
ClearOnShutdown(&sExtHelperAppSvcSingleton);
}
return do_AddRef(sExtHelperAppSvcSingleton);
}
NS_IMPL_ISUPPORTS(nsExternalHelperAppService, nsIExternalHelperAppService,
nsPIExternalAppLauncher, nsIExternalProtocolService,
nsIMIMEService, nsIObserver, nsISupportsWeakReference)
nsExternalHelperAppService::nsExternalHelperAppService() {}
nsresult nsExternalHelperAppService::Init() {
// Add an observer for profile change
nsCOMPtr<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, nsIChannel* aChannel,
BrowsingContext* aContentContext, bool aForceSave,
nsIInterfaceRequestor* aWindowContext,
nsIStreamListener** aStreamListener) {
NS_ENSURE_ARG_POINTER(aChannel);
// 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;
aChannel->GetURI(getter_AddRefs(uri));
aChannel->GetContentLength(&contentLength);
aChannel->GetContentDisposition(&contentDisposition);
aChannel->GetContentDispositionFilename(fileName);
aChannel->GetContentDispositionHeader(disp);
loadInfo = aChannel->LoadInfo();
nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aChannel));
wasFileChannel = fileChan != nullptr;
nsCOMPtr<nsIURI> referrer;
NS_GetReferrerFromChannel(aChannel, getter_AddRefs(referrer));
mozilla::net::LoadInfoArgs loadInfoArgs;
MOZ_ALWAYS_SUCCEEDS(LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs));
// 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));
NS_ADDREF(*aStreamListener = childListener);
uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
SanitizeFileName(fileName, 0);
RefPtr<nsExternalAppHandler> handler =
new nsExternalAppHandler(nullptr, u""_ns, aContentContext, aWindowContext,
this, fileName, reason, aForceSave);
if (!handler) {
return NS_ERROR_OUT_OF_MEMORY;
}
childListener->SetHandler(handler);
return NS_OK;
}
NS_IMETHODIMP nsExternalHelperAppService::CreateListener(
const nsACString& aMimeContentType, nsIChannel* aChannel,
BrowsingContext* aContentContext, bool aForceSave,
nsIInterfaceRequestor* aWindowContext,
nsIStreamListener** aStreamListener) {
MOZ_ASSERT(!XRE_IsContentProcess());
NS_ENSURE_ARG_POINTER(aChannel);
nsAutoString fileName;
nsAutoCString fileExtension;
uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
uint32_t contentDisposition = -1;
aChannel->GetContentDisposition(&contentDisposition);
if (contentDisposition == nsIChannel::DISPOSITION_ATTACHMENT) {
reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST;
}
*aStreamListener = nullptr;
// Get the file extension and name that we will need later
nsCOMPtr<nsIURI> uri;
bool allowURLExtension =
GetFileNameFromChannel(aChannel, fileName, getter_AddRefs(uri));
uint32_t flags = VALIDATE_ALLOW_EMPTY;
if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT,
nsCaseInsensitiveCStringComparator)) {
flags |= VALIDATE_GUESS_FROM_EXTENSION;
}
nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
fileName, aMimeContentType, uri, nullptr, flags, allowURLExtension);
LOG("Type/Ext lookup found 0x%p\n", mimeInfo.get());
// No mimeinfo -> we can't continue. probably OOM.
if (!mimeInfo) {
return NS_ERROR_OUT_OF_MEMORY;
}
if (flags & VALIDATE_GUESS_FROM_EXTENSION) {
// Replace the content type with what was guessed.
nsAutoCString mimeType;
mimeInfo->GetMIMEType(mimeType);
aChannel->SetContentType(mimeType);
if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
}
}
nsAutoString extension;
int32_t dotidx = fileName.RFind(u".");
if (dotidx != -1) {
extension = Substring(fileName, dotidx + 1);
}
// NB: ExternalHelperAppParent depends on this listener always being an
// nsExternalAppHandler. If this changes, make sure to update that code.
nsExternalAppHandler* handler = new nsExternalAppHandler(
mimeInfo, extension, aContentContext, aWindowContext, this, fileName,
reason, aForceSave);
if (!handler) {
return NS_ERROR_OUT_OF_MEMORY;
}
NS_ADDREF(*aStreamListener = handler);
return NS_OK;
}
NS_IMETHODIMP nsExternalHelperAppService::DoContent(
const nsACString& aMimeContentType, nsIChannel* aChannel,
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, aChannel, bc,
aForceSave, aWindowContext,
aStreamListener);
}
nsresult rv = CreateListener(aMimeContentType, aChannel, 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 < std::size(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, &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";
// static
nsresult nsExternalHelperAppService::EscapeURI(nsIURI* aURI, nsIURI** aResult) {
MOZ_ASSERT(aURI);
MOZ_ASSERT(aResult);
nsAutoCString spec;
aURI->GetSpec(spec);
if (spec.Find("%00") != -1) return NS_ERROR_MALFORMED_URI;
nsAutoCString escapedSpec;
nsresult rv = NS_EscapeURL(spec, esc_AlwaysCopy | esc_ExtHandler, escapedSpec,
fallible);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIIOService> ios(do_GetIOService());
return ios->NewURI(escapedSpec, nullptr, nullptr, aResult);
}
bool ExternalProtocolIsBlockedBySandbox(
BrowsingContext* aBrowsingContext,
const bool aHasValidUserGestureActivation) {
if (!StaticPrefs::dom_block_external_protocol_navigation_from_sandbox()) {
return false;
}
if (!aBrowsingContext || aBrowsingContext->IsTop()) {
return false;
}
uint32_t sandboxFlags = aBrowsingContext->GetSandboxFlags();
if (sandboxFlags == SANDBOXED_NONE) {
return false;
}
if (!(sandboxFlags & SANDBOXED_AUXILIARY_NAVIGATION)) {
return false;
}
if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION)) {
return false;
}
if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_CUSTOM_PROTOCOLS)) {
return false;
}
if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION) &&
aHasValidUserGestureActivation) {
return false;
}
return true;
}
NS_IMETHODIMP
nsExternalHelperAppService::LoadURI(nsIURI* aURI,
nsIPrincipal* aTriggeringPrincipal,
nsIPrincipal* aRedirectPrincipal,
BrowsingContext* aBrowsingContext,
bool aTriggeredExternally,
bool aHasValidUserGestureActivation,
bool aNewWindowTarget) {
NS_ENSURE_ARG_POINTER(aURI);
if (XRE_IsContentProcess()) {
mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal(
aURI, aTriggeringPrincipal, aRedirectPrincipal, aBrowsingContext,
aTriggeredExternally, aHasValidUserGestureActivation, aNewWindowTarget);
return NS_OK;
}
// Prevent sandboxed BrowsingContexts from navigating to external protocols.
// This only uses the sandbox flags of the target BrowsingContext of the
// load. The navigating document's CSP sandbox flags do not apply.
if (aBrowsingContext &&
ExternalProtocolIsBlockedBySandbox(aBrowsingContext,
aHasValidUserGestureActivation)) {
// Log an error to the web console of the sandboxed BrowsingContext.
nsAutoString localizedMsg;