Bug 1746052, add methods to the mime service that compute and validate a filename for a given content type, r=Gijs

The code in SanitizeFilename will be expanded upon in the following patch.

Differential Revision: https://phabricator.services.mozilla.com/D135951
This commit is contained in:
Neil Deakin 2022-05-03 19:44:24 +00:00
parent 976c5b23db
commit 2596b808fa
3 changed files with 503 additions and 327 deletions

View File

@ -8,6 +8,7 @@
interface nsIFile;
interface nsIMIMEInfo;
interface nsIURI;
interface nsIChannel;
%{C++
#define NS_MIMESERVICE_CID \
@ -97,4 +98,103 @@ interface nsIMIMEService : nsISupports {
nsIMIMEInfo getMIMEInfoFromOS(in ACString aType,
in ACString aFileExtension,
out boolean aFound);
/**
* Default filename validation for getValidFileName and
* validateFileNameForSaving where other flags are not true.
* That is, the extension is modified to fit the content type,
* duplicate whitespace is collapsed, and long filenames are
* truncated. A valid content type must be supplied. See the
* description of getValidFileName for more details about how
* the flags are used.
*/
const long VALIDATE_DEFAULT = 0;
/**
* If true, then the filename is only validated to ensure that it is
* acceptable for the file system. If false, then the extension is also
* checked to ensure that it is valid for the content type. If the
* extension is not valid, the filename is modified to have the proper
* extension.
*/
const long VALIDATE_SANITIZE_ONLY = 1;
/**
* Don't collapse strings of duplicate whitespace into a single string.
*/
const long VALIDATE_DONT_COLLAPSE_WHITESPACE = 2;
/**
* Don't truncate long filenames.
*/
const long VALIDATE_DONT_TRUNCATE = 4;
/**
* True to ignore the content type and guess the type from any existing
* extension instead. "application/octet-stream" is used as the default
* if there is no extension or there is no information available for
* the extension.
*/
const long VALIDATE_GUESS_FROM_EXTENSION = 8;
/**
* Generate a valid filename from the channel that can be used to save
* the content of the channel to the local disk.
*
* The filename is determined from the content disposition, the filename
* of the uri, or a default filename. The following modifications are
* applied:
* - If the VALIDATE_SANITIZE_ONLY flag is not specified, then the
* extension of the filename is modified to suit the supplied content type.
* - Path separators (typically / and \) are replaced by underscores (_)
* - Characters that are not valid or would be confusing in filenames are
* replaced by spaces (*, :, etc)
* - Bidi related marks are replaced by underscores (_)
* - Whitespace and periods are removed from the beginning and end.
* - Unless VALIDATE_DONT_COLLAPSE_WHITESPACE is specified, multiple
* consecutive whitespace characters are collapsed to a single space
* character (' ').
* - Unless VALIDATE_DONT_TRUNCATE is specified, the filename is truncated
* to a maximum length, preserving the extension if possible.
*
* If either the VALIDATE_SANITIZE_ONLY or VALIDATE_GUESS_FROM_EXTENSION flags
* are specified, then the content type may be empty. Otherwise, the type must
* not be empty.
*
* The aOriginalURI would be specified if the channel is for a local file but
* it was originally sourced from a different uri.
*
* When saving an image, use validateFileNameForSaving instead and
* pass the result of imgIRequest::GetFileName() as the filename to
* check.
*
* @param aChannel The channel of the content to save.
* @param aType The MIME type to use, which would usually be the
* same as the content type of the channel.
* @param aOriginalURL the source url of the file, but may be null.
* @param aFlags one or more of the flags above.
* @returns The resulting filename.
*/
AString getValidFileName(in nsIChannel aChannel,
in ACString aType,
in nsIURI aOriginalURI,
in unsigned long aFlags);
/**
* Similar to getValidFileName, but used when a specific filename needs
* to be validated. The filename is modified as needed based on the
* content type in the same manner as getValidFileName.
*
* If the filename came from a uri, it should not be escaped, that is,
* any needed unescaping of the filename should happen before calling
* this method.
*
* @param aType The MIME type to use.
* @param aFlags one or more of the flags above.
* @param aFileName The filename to validate.
* @returns The validated filename.
*/
AString validateFileNameForSaving(in AString aFileName,
in ACString aType,
in unsigned long aFlags);
};

View File

@ -114,6 +114,8 @@ using namespace mozilla;
using namespace mozilla::ipc;
using namespace mozilla::dom;
#define kDefaultMaxFileNameLength 255
// Download Folder location constants
#define NS_PREF_DOWNLOAD_DIR "browser.download.dir"
#define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList"
@ -179,108 +181,6 @@ static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI,
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
@ -809,8 +709,10 @@ nsresult nsExternalHelperAppService::DoContentContentProcessHelper(
uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
SanitizeFileName(fileName, EmptyCString(), 0);
RefPtr<nsExternalAppHandler> handler =
new nsExternalAppHandler(nullptr, ""_ns, aContentContext, aWindowContext,
new nsExternalAppHandler(nullptr, u""_ns, aContentContext, aWindowContext,
this, fileName, reason, aForceSave);
if (!handler) {
return NS_ERROR_OUT_OF_MEMORY;
@ -830,98 +732,32 @@ NS_IMETHODIMP nsExternalHelperAppService::CreateListener(
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);
uint32_t contentDisposition = -1;
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) {
if (contentDisposition == nsIChannel::DISPOSITION_ATTACHMENT) {
reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST;
}
}
LOG(("HelperAppService::DoContent: mime '%s', extension '%s'\n",
PromiseFlatCString(aMimeContentType).get(), fileExtension.get()));
*aStreamListener = nullptr;
// 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);
// Get the file extension and name that we will need later
nsCOMPtr<nsIURI> uri;
bool allowURLExtension =
GetFileNameFromChannel(channel, fileName, getter_AddRefs(uri));
// Try to find a mime object by looking at the mime type/extension
nsCOMPtr<nsIMIMEInfo> mimeInfo;
uint32_t flags = VALIDATE_DEFAULT;
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));
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.
@ -929,17 +765,30 @@ NS_IMETHODIMP nsExternalHelperAppService::CreateListener(
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);
if (flags & VALIDATE_GUESS_FROM_EXTENSION) {
if (channel) {
// Replace the content type with what was guessed.
nsAutoCString mimeType;
mimeInfo->GetMIMEType(mimeType);
channel->SetContentType(mimeType);
}
if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
}
}
nsAutoString extension;
int32_t dotidx = fileName.RFind(".");
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, buf, aContentContext, aWindowContext,
this, fileName, reason, aForceSave);
nsExternalAppHandler* handler = new nsExternalAppHandler(
mimeInfo, extension, aContentContext, aWindowContext, this, fileName,
reason, aForceSave);
if (!handler) {
return NS_ERROR_OUT_OF_MEMORY;
}
@ -1428,14 +1277,14 @@ NS_INTERFACE_MAP_BEGIN(nsExternalAppHandler)
NS_INTERFACE_MAP_END
nsExternalAppHandler::nsExternalAppHandler(
nsIMIMEInfo* aMIMEInfo, const nsACString& aTempFileExtension,
nsIMIMEInfo* aMIMEInfo, const nsAString& aFileExtension,
BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext,
nsExternalHelperAppService* aExtProtSvc,
const nsAString& aSuggestedFilename, uint32_t aReason, bool aForceSave)
const nsAString& aSuggestedFileName, uint32_t aReason, bool aForceSave)
: mMimeInfo(aMIMEInfo),
mBrowsingContext(aBrowsingContext),
mWindowContext(aWindowContext),
mSuggestedFileName(aSuggestedFilename),
mSuggestedFileName(aSuggestedFileName),
mForceSave(aForceSave),
mCanceled(false),
mStopRequestIssued(false),
@ -1453,53 +1302,10 @@ nsExternalAppHandler::nsExternalAppHandler(
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);
if (!aFileExtension.IsEmpty() && aFileExtension.First() != '.') {
mFileExtension = char16_t('.');
}
// 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);
mFileExtension.Append(aFileExtension);
mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096);
}
@ -1508,80 +1314,6 @@ 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
@ -2793,7 +2525,7 @@ NS_IMETHODIMP nsExternalAppHandler::PromptForSaveDestination() {
}
if (mSuggestedFileName.IsEmpty()) {
RequestSaveDestination(mTempLeafName, mTempFileExtension);
RequestSaveDestination(mTempLeafName, mFileExtension);
} else {
nsAutoString fileExt;
int32_t pos = mSuggestedFileName.RFindChar('.');
@ -2801,7 +2533,7 @@ NS_IMETHODIMP nsExternalAppHandler::PromptForSaveDestination() {
mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos);
}
if (fileExt.IsEmpty()) {
fileExt = mTempFileExtension;
fileExt = mFileExtension;
}
RequestSaveDestination(mSuggestedFileName, fileExt);
@ -2929,7 +2661,13 @@ NS_IMETHODIMP nsExternalAppHandler::SetDownloadToLaunch(
}
#ifdef XP_WIN
fileToUse->Append(mSuggestedFileName + mTempFileExtension);
// Ensure we don't double-append the file extension if it matches:
if (StringEndsWith(mSuggestedFileName, mFileExtension,
nsCaseInsensitiveStringComparator)) {
fileToUse->Append(mSuggestedFileName);
} else {
fileToUse->Append(mSuggestedFileName + mFileExtension);
}
#else
fileToUse->Append(mSuggestedFileName);
#endif
@ -3484,3 +3222,321 @@ nsresult nsExternalHelperAppService::GetMIMEInfoFromOS(
*aFound = false;
return NS_ERROR_NOT_IMPLEMENTED;
}
bool nsExternalHelperAppService::GetFileNameFromChannel(nsIChannel* aChannel,
nsAString& aFileName,
nsIURI** aURI) {
if (!aChannel) {
return false;
}
aChannel->GetURI(aURI);
nsCOMPtr<nsIURL> url = do_QueryInterface(*aURI);
// Check if we have a POST request, in which case we don't want to use
// the url's extension
bool allowURLExt = !net::ChannelIsPost(aChannel);
// Check if we had a query string - we don't want to check the URL
// extension if a query is present in the URI
// If we already know we don't want to check the URL extension, don't
// bother checking the query
if (url && allowURLExt) {
nsAutoCString query;
// We only care about the query for HTTP and HTTPS URLs
if (url->SchemeIs("http") || url->SchemeIs("https")) {
url->GetQuery(query);
}
// Only get the extension if the query is empty; if it isn't, then the
// extension likely belongs to a cgi script and isn't helpful
allowURLExt = query.IsEmpty();
}
aChannel->GetContentDispositionFilename(aFileName);
return allowURLExt;
}
NS_IMETHODIMP
nsExternalHelperAppService::GetValidFileName(nsIChannel* aChannel,
const nsACString& aType,
nsIURI* aOriginalURI,
uint32_t aFlags,
nsAString& aOutFileName) {
nsCOMPtr<nsIURI> uri;
bool allowURLExtension =
GetFileNameFromChannel(aChannel, aOutFileName, getter_AddRefs(uri));
nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
aOutFileName, aType, uri, aOriginalURI, aFlags, allowURLExtension);
return NS_OK;
}
NS_IMETHODIMP
nsExternalHelperAppService::ValidateFileNameForSaving(
const nsAString& aFileName, const nsACString& aType, uint32_t aFlags,
nsAString& aOutFileName) {
nsAutoString fileName(aFileName);
// Just sanitize the filename only.
if (aFlags & VALIDATE_SANITIZE_ONLY) {
nsAutoString extension;
int32_t dotidx = fileName.RFind(".");
if (dotidx != -1) {
extension = Substring(fileName, dotidx + 1);
}
SanitizeFileName(fileName, NS_ConvertUTF16toUTF8(extension), aFlags);
} else {
nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
fileName, aType, nullptr, nullptr, aFlags, true);
}
aOutFileName = fileName;
return NS_OK;
}
already_AddRefed<nsIMIMEInfo>
nsExternalHelperAppService::ValidateFileNameForSaving(
nsAString& aFileName, const nsACString& aMimeType, nsIURI* aURI,
nsIURI* aOriginalURI, uint32_t aFlags, bool aAllowURLExtension) {
nsAutoString fileName(aFileName);
nsAutoCString extension;
nsCOMPtr<nsIMIMEInfo> mimeInfo;
// 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> mimeService = do_GetService("@mozilla.org/mime;1");
if (mimeService) {
if (fileName.IsEmpty()) {
nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
// Try to extract the file name from the url and use that as a first
// pass as the leaf name of our temp file...
if (url) {
nsAutoCString leafName;
url->GetFileName(leafName);
if (!leafName.IsEmpty()) {
if (NS_SUCCEEDED(UnescapeFragment(leafName, url, fileName))) {
CopyUTF8toUTF16(leafName, aFileName); // use escaped name
}
}
// Only get the extension from the URL if allowed.
if (aAllowURLExtension) {
url->GetFileExtension(extension);
}
}
} else {
// Determine the current extension for the filename.
int32_t dotidx = fileName.RFind(".");
if (dotidx != -1) {
CopyUTF16toUTF8(Substring(fileName, dotidx + 1), extension);
}
}
if (aFlags & VALIDATE_GUESS_FROM_EXTENSION) {
nsAutoCString mimeType;
if (!extension.IsEmpty()) {
mimeService->GetFromTypeAndExtension(EmptyCString(), extension,
getter_AddRefs(mimeInfo));
if (mimeInfo) {
mimeInfo->GetMIMEType(mimeType);
}
}
if (mimeType.IsEmpty()) {
// Extension lookup gave us no useful match, so use octet-stream
// instead.
mimeService->GetFromTypeAndExtension(
nsLiteralCString(APPLICATION_OCTET_STREAM), extension,
getter_AddRefs(mimeInfo));
}
} else if (!aMimeType.IsEmpty()) {
// If this is a binary type, include the extension as a hint to get
// the mime info. For other types, the mime type itself should be
// sufficient.
// The special case for application/ogg is because that type could
// actually be used for a video which can better be determined by the
// extension. This is tested by browser_save_video.js.
bool useExtension = aMimeType.EqualsLiteral(APPLICATION_OCTET_STREAM) ||
aMimeType.EqualsLiteral(BINARY_OCTET_STREAM) ||
aMimeType.EqualsLiteral("application/x-msdownload") ||
aMimeType.EqualsLiteral(APPLICATION_OGG);
mimeService->GetFromTypeAndExtension(
aMimeType, useExtension ? extension : EmptyCString(),
getter_AddRefs(mimeInfo));
if (mimeInfo) {
// But if no primary extension was returned, this mime type is probably
// an unknown type. Look it up again but this time supply the extension.
nsAutoCString primaryExtension;
mimeInfo->GetPrimaryExtension(primaryExtension);
if (primaryExtension.IsEmpty()) {
mimeService->GetFromTypeAndExtension(aMimeType, extension,
getter_AddRefs(mimeInfo));
}
}
}
}
// Windows ignores terminating dots. So we have to as well, so
// that our security checks do "the right thing"
fileName.Trim(".", false);
if (mimeService) {
bool isValidExtension;
if (extension.IsEmpty() ||
NS_FAILED(mimeInfo->ExtensionExists(extension, &isValidExtension)) ||
!isValidExtension) {
nsAutoCString originalExtension(extension);
// If an original url was supplied, see if it has a valid extension.
bool useOldExtension = false;
if (aOriginalURI) {
nsCOMPtr<nsIURL> originalURL(do_QueryInterface(aOriginalURI));
if (originalURL) {
originalURL->GetFileExtension(extension);
if (!extension.IsEmpty()) {
mimeInfo->ExtensionExists(extension, &useOldExtension);
}
}
}
if (!useOldExtension) {
// If the filename doesn't have a valid extension, or we don't know the
// extension, try to use the primary extension for the type. If we don't
// know the primary extension for the type, just continue with the
// existing extension, or leave the filename with no extension.
mimeInfo->GetPrimaryExtension(extension);
}
ModifyExtensionType modify =
ShouldModifyExtension(mimeInfo, originalExtension);
if (modify == ModifyExtension_Replace) {
int32_t dotidx = fileName.RFind(".");
if (dotidx != -1) {
// Remove the existing extension and replace it.
fileName.Truncate(dotidx);
}
}
// Otherwise, just append the proper extension to the end of the
// filename, adding to the invalid extension that might already be there.
if (modify != ModifyExtension_Ignore && !extension.IsEmpty()) {
fileName.AppendLiteral(".");
fileName.Append(NS_ConvertUTF8toUTF16(extension));
}
}
}
// Make the filename safe for the filesystem
SanitizeFileName(fileName, extension, aFlags);
aFileName = fileName;
return mimeInfo.forget();
}
void nsExternalHelperAppService::SanitizeFileName(nsAString& aFileName,
const nsACString& aExtension,
uint32_t aFlags) {
nsAutoString fileName(aFileName);
fileName.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
fileName.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' ');
fileName.StripChar(char16_t(0));
// 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)};
fileName.ReplaceChar(unsafeBidiCharacters, '_');
// Trim whitespace, periods and vowel separators from the beginning and
// end of the filename. Periods are removed to avoid creating hidden files.
fileName.Trim(" .\f\n\r\t\v", true, true);
// Collapse duplicate whitespace.
if (!(aFlags & VALIDATE_DONT_COLLAPSE_WHITESPACE)) {
fileName.CompressWhitespace();
}
// If the filename is too long, truncate it, but preserve the desired
// extension.
if (!(aFlags & VALIDATE_DONT_TRUNCATE) &&
fileName.Length() > kDefaultMaxFileNameLength) {
// This is extremely unlikely, but if the extension is larger than the
// maximum size, just get rid of it.
if (aExtension.Length() >= kDefaultMaxFileNameLength) {
fileName.Truncate(kDefaultMaxFileNameLength - 1);
} else {
fileName.Truncate(kDefaultMaxFileNameLength - aExtension.Length() - 1);
if (!fileName.IsEmpty()) {
if (fileName.Last() != '.') {
fileName.AppendLiteral(".");
}
fileName.Append(NS_ConvertUTF8toUTF16(aExtension));
}
}
}
aFileName = fileName;
}
nsExternalHelperAppService::ModifyExtensionType
nsExternalHelperAppService::ShouldModifyExtension(nsIMIMEInfo* aMimeInfo,
const nsCString& aFileExt) {
nsAutoCString MIMEType;
if (!aMimeInfo || NS_FAILED(aMimeInfo->GetMIMEType(MIMEType))) {
return ModifyExtension_Append;
}
// Determine whether the extensions should be appended or replaced depending
// on the content type.
bool canForce = StringBeginsWith(MIMEType, "image/"_ns) ||
StringBeginsWith(MIMEType, "audio/"_ns) ||
StringBeginsWith(MIMEType, "video/"_ns);
if (!canForce) {
for (const char* mime : forcedExtensionMimetypes) {
if (MIMEType.Equals(mime)) {
if (!StaticPrefs::browser_download_sanitize_non_media_extensions()) {
return ModifyExtension_Ignore;
}
canForce = true;
break;
}
}
if (!canForce) {
return ModifyExtension_Append;
}
}
// If we get here, we know for sure the mimetype allows us to modify the
// existing extension, if it's wrong. Return whether we should replace it
// or append it.
bool knownExtension = false;
// Note that aFileExt is either empty or consists of an extension
// excluding the dot.
if (aFileExt.IsEmpty() ||
(NS_SUCCEEDED(aMimeInfo->ExtensionExists(aFileExt, &knownExtension)) &&
!knownExtension)) {
return ModifyExtension_Replace;
}
return ModifyExtension_Append;
}

View File

@ -199,6 +199,32 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService,
*/
void ExpungeTemporaryPrivateFiles();
bool GetFileNameFromChannel(nsIChannel* aChannel, nsAString& aFileName,
nsIURI** aURI);
// Internal version of the method from nsIMIMEService.
already_AddRefed<nsIMIMEInfo> ValidateFileNameForSaving(
nsAString& aFileName, const nsACString& aMimeType, nsIURI* aURI,
nsIURI* aOriginalURI, uint32_t aFlags, bool aAllowURLExtension);
void SanitizeFileName(nsAString& aFileName, const nsACString& aExtension,
uint32_t aFlags);
/**
* Helper routine that checks how we should modify an extension
* for this file.
*/
enum ModifyExtensionType {
// Replace an invalid extension with the preferred one.
ModifyExtension_Replace = 0,
// Append the preferred extension after any existing one.
ModifyExtension_Append = 1,
// Don't modify the extension.
ModifyExtension_Ignore = 2
};
ModifyExtensionType ShouldModifyExtension(nsIMIMEInfo* aMimeInfo,
const nsCString& aFileExt);
/**
* Array for the files that should be deleted
*/
@ -251,15 +277,15 @@ class nsExternalAppHandler final : public nsIStreamListener,
* in which case dialogs will be parented to
* aContentContext.
* @param mExtProtSvc nsExternalHelperAppService on creation
* @param aFileName The filename to use
* @param aSuggestedFileName The filename to use
* @param aReason A constant from nsIHelperAppLauncherDialog
* indicating why the request is handled by a helper app.
*/
nsExternalAppHandler(nsIMIMEInfo* aMIMEInfo, const nsACString& aFileExtension,
nsExternalAppHandler(nsIMIMEInfo* aMIMEInfo, const nsAString& aFileExtension,
mozilla::dom::BrowsingContext* aBrowsingContext,
nsIInterfaceRequestor* aWindowContext,
nsExternalHelperAppService* aExtProtSvc,
const nsAString& aFilename, uint32_t aReason,
const nsAString& aSuggestedFileName, uint32_t aReason,
bool aForceSave);
/**
@ -284,7 +310,7 @@ class nsExternalAppHandler final : public nsIStreamListener,
nsCOMPtr<nsIFile> mTempFile;
nsCOMPtr<nsIURI> mSourceUrl;
nsString mTempFileExtension;
nsString mFileExtension;
nsString mTempLeafName;
/**
@ -473,12 +499,6 @@ class nsExternalAppHandler final : public nsIStreamListener,
*/
bool GetNeverAskFlagFromPref(const char* prefName, const char* aContentType);
/**
* Helper routine that checks whether we should enforce an extension
* for this file.
*/
bool ShouldForceExtension(const nsString& aFileExt);
/**
* Helper routine to ensure that mSuggestedFileName ends in the correct
* extension, in case the original extension contains invalid characters
@ -486,7 +506,7 @@ class nsExternalAppHandler final : public nsIStreamListener,
* extension (image/, video/, and audio/ based mimetypes, and a few specific
* document types).
*
* It also ensure that mTempFileExtension only contains an extension
* It also ensure that mFileExtension only contains an extension
* when it is different from mSuggestedFileName's extension.
*/
void EnsureCorrectExtension(const nsString& aFileExt);