--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_downloadLastDir.js @@ -95,17 +95,17 @@ function test() { "gDownloadLastDir should be the expected global last dir" ); launcher.saveDestinationAvailable = null; aWin.close(); aCallback(); }; - launcherDialog.promptForSaveToFileAsync(launcher, aWin, "", "", false); + launcherDialog.promptForSaveToFileAsync(launcher, aWin, null, null, null); } testOnWindow(false, function(win, downloadDir) { testDownloadDir(win, downloadDir, file1, tmpDir, dir1, dir1, function() { testOnWindow(true, function(win1, downloadDir1) { testDownloadDir( win1, downloadDir1,
--- a/dom/base/nsContentAreaDragDrop.cpp +++ b/dom/base/nsContentAreaDragDrop.cpp @@ -256,33 +256,52 @@ nsContentAreaDragDropDataProvider::GetFl // content processes. if (XRE_IsParentProcess()) { rv = aTransferable->GetTransferData(kImageRequestMime, getter_AddRefs(tmp)); NS_ENSURE_SUCCESS(rv, rv); supportsString = do_QueryInterface(tmp); if (!supportsString) return NS_ERROR_FAILURE; - nsAutoString contentType; - supportsString->GetData(contentType); + nsAutoString imageRequestMime; + supportsString->GetData(imageRequestMime); - nsCOMPtr<nsIMIMEService> mimeService = - do_GetService("@mozilla.org/mime;1"); - if (NS_WARN_IF(!mimeService)) { - return NS_ERROR_FAILURE; - } + // If we have a MIME type, check the extension is compatible + if (!imageRequestMime.IsEmpty()) { + // Build a URL to get the filename extension + nsCOMPtr<nsIURL> imageURL = do_QueryInterface(sourceURI, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString extension; + rv = imageURL->GetFileExtension(extension); + NS_ENSURE_SUCCESS(rv, rv); - mimeService->ValidateFileNameForSaving( - targetFilename, NS_ConvertUTF16toUTF8(contentType), - nsIMIMEService::VALIDATE_DEFAULT, targetFilename); - } else { - // make the filename safe for the filesystem - targetFilename.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, - '-'); + NS_ConvertUTF16toUTF8 mimeCString(imageRequestMime); + bool isValidExtension; + nsAutoCString primaryExtension; + rv = CheckAndGetExtensionForMime(extension, mimeCString, + &isValidExtension, &primaryExtension); + NS_ENSURE_SUCCESS(rv, rv); + + if (!isValidExtension && !primaryExtension.IsEmpty()) { + // The filename extension is missing or incompatible + // with the MIME type, replace it with the primary + // extension. + nsAutoCString newFileName; + rv = imageURL->GetFileBaseName(newFileName); + NS_ENSURE_SUCCESS(rv, rv); + newFileName.Append("."); + newFileName.Append(primaryExtension); + CopyUTF8toUTF16(newFileName, targetFilename); + } + } } + // make the filename safe for the filesystem + targetFilename.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, + '-'); #endif /* defined(XP_MACOSX) */ // get the target directory from the kFilePromiseDirectoryMime // flavor nsCOMPtr<nsISupports> dirPrimitive; rv = aTransferable->GetTransferData(kFilePromiseDirectoryMime, getter_AddRefs(dirPrimitive)); NS_ENSURE_SUCCESS(rv, rv); @@ -412,37 +431,64 @@ nsresult DragDataProducer::GetImageData( NS_ENSURE_SUCCESS(rv, rv); // pass out the image source string CopyUTF8toUTF16(spec, mImageSourceString); nsCString mimeType; aRequest->GetMimeType(getter_Copies(mimeType)); - nsAutoCString fileName; - aRequest->GetFileName(fileName); - #if defined(XP_MACOSX) // Save the MIME type so we can make sure the extension // is compatible (and replace it if it isn't) when the // image is dropped. On Mac, we need to get the OS MIME // handler information in the parent due to sandboxing. CopyUTF8toUTF16(mimeType, mImageRequestMime); - CopyUTF8toUTF16(fileName, mImageDestFileName); #else nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1"); if (NS_WARN_IF(!mimeService)) { return NS_ERROR_FAILURE; } + nsCOMPtr<nsIMIMEInfo> mimeInfo; + mimeService->GetFromTypeAndExtension(mimeType, ""_ns, + getter_AddRefs(mimeInfo)); + if (mimeInfo) { + nsAutoCString extension; + imgUrl->GetFileExtension(extension); + + bool validExtension; + if (extension.IsEmpty() || + NS_FAILED(mimeInfo->ExtensionExists(extension, &validExtension)) || + !validExtension) { + // Fix the file extension in the URL + nsAutoCString primaryExtension; + mimeInfo->GetPrimaryExtension(primaryExtension); + if (!primaryExtension.IsEmpty()) { + rv = NS_MutateURI(imgUrl) + .Apply(&nsIURLMutator::SetFileExtension, primaryExtension, + nullptr) + .Finalize(imgUrl); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } +#endif /* defined(XP_MACOSX) */ + + nsAutoCString fileName; + imgUrl->GetFileName(fileName); + + NS_UnescapeURL(fileName); + +#if !defined(XP_MACOSX) + // make the filename safe for the filesystem + fileName.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '-'); +#endif + CopyUTF8toUTF16(fileName, mImageDestFileName); - mimeService->ValidateFileNameForSaving(mImageDestFileName, mimeType, - nsIMIMEService::VALIDATE_DEFAULT, - mImageDestFileName); -#endif // and the image object mImage = aImage; } return NS_OK; }
--- a/dom/base/nsCopySupport.cpp +++ b/dom/base/nsCopySupport.cpp @@ -27,16 +27,18 @@ #include "nsIContentViewerEdit.h" #include "nsISelectionController.h" #include "nsPIDOMWindow.h" #include "mozilla/dom/Document.h" #include "nsHTMLDocument.h" #include "nsGkAtoms.h" #include "nsIFrame.h" +#include "nsIURI.h" +#include "nsGenericHTMLElement.h" // image copy stuff #include "nsIImageLoadingContent.h" #include "nsIInterfaceRequestorUtils.h" #include "nsContentUtils.h" #include "nsContentCID.h" #ifdef XP_WIN @@ -597,49 +599,80 @@ static nsresult AppendImagePromise(nsITr NS_ENSURE_SUCCESS(rv, rv); if (isMultipart) { return NS_OK; } nsCOMPtr<nsINode> node = do_QueryInterface(aImageElement, &rv); NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1"); - if (NS_WARN_IF(!mimeService)) { - return NS_ERROR_FAILURE; - } + // Fix the file extension in the URL if necessary + nsCOMPtr<nsIMIMEService> mimeService = + do_GetService(NS_MIMESERVICE_CONTRACTID); + NS_ENSURE_TRUE(mimeService, NS_OK); nsCOMPtr<nsIURI> imgUri; rv = aImgRequest->GetFinalURI(getter_AddRefs(imgUri)); NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURL> imgUrl = do_QueryInterface(imgUri); + NS_ENSURE_TRUE(imgUrl, NS_OK); + + nsAutoCString extension; + rv = imgUrl->GetFileExtension(extension); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString mimeType; + rv = aImgRequest->GetMimeType(getter_Copies(mimeType)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMIMEInfo> mimeInfo; + mimeService->GetFromTypeAndExtension(mimeType, ""_ns, + getter_AddRefs(mimeInfo)); + NS_ENSURE_TRUE(mimeInfo, NS_OK); + nsAutoCString spec; - rv = imgUri->GetSpec(spec); + rv = imgUrl->GetSpec(spec); NS_ENSURE_SUCCESS(rv, rv); // pass out the image source string nsString imageSourceString; CopyUTF8toUTF16(spec, imageSourceString); - nsCString mimeType; - rv = aImgRequest->GetMimeType(getter_Copies(mimeType)); - NS_ENSURE_SUCCESS(rv, rv); + bool validExtension; + if (extension.IsEmpty() || + NS_FAILED(mimeInfo->ExtensionExists(extension, &validExtension)) || + !validExtension) { + // Fix the file extension in the URL + nsAutoCString primaryExtension; + mimeInfo->GetPrimaryExtension(primaryExtension); + if (!primaryExtension.IsEmpty()) { + rv = NS_MutateURI(imgUri) + .Apply(&nsIURLMutator::SetFileExtension, primaryExtension, + nullptr) + .Finalize(imgUrl); + NS_ENSURE_SUCCESS(rv, rv); + } + } nsAutoCString fileName; - rv = aImgRequest->GetFileName(fileName); - NS_ENSURE_SUCCESS(rv, rv); + imgUrl->GetFileName(fileName); + + NS_UnescapeURL(fileName); - nsAutoString validFileName = NS_ConvertUTF8toUTF16(fileName); - mimeService->ValidateFileNameForSaving( - validFileName, mimeType, nsIMIMEService::VALIDATE_DEFAULT, validFileName); + // make the filename safe for the filesystem + fileName.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '-'); + + nsString imageDestFileName; + CopyUTF8toUTF16(fileName, imageDestFileName); rv = AppendString(aTransferable, imageSourceString, kFilePromiseURLMime); NS_ENSURE_SUCCESS(rv, rv); - rv = AppendString(aTransferable, validFileName, kFilePromiseDestFilename); + rv = AppendString(aTransferable, imageDestFileName, kFilePromiseDestFilename); NS_ENSURE_SUCCESS(rv, rv); aTransferable->SetRequestingPrincipal(node->NodePrincipal()); aTransferable->SetContentPolicyType(nsIContentPolicy::TYPE_INTERNAL_IMAGE); // add the dataless file promise flavor return aTransferable->AddDataFlavor(kFilePromiseMime); }
--- a/dom/webbrowserpersist/nsWebBrowserPersist.cpp +++ b/dom/webbrowserpersist/nsWebBrowserPersist.cpp @@ -2147,36 +2147,88 @@ nsresult nsWebBrowserPersist::CalculateA // Get the content type from the MIME service if (contentType.IsEmpty()) { nsCOMPtr<nsIURI> uri; aChannel->GetOriginalURI(getter_AddRefs(uri)); mMIMEService->GetTypeFromURI(uri, contentType); } - // Validate the filename + // Append the extension onto the file if (!contentType.IsEmpty()) { - nsAutoString newFileName; - if (NS_SUCCEEDED(mMIMEService->GetValidFileName( - aChannel, contentType, aOriginalURIWithExtension, - nsIMIMEService::VALIDATE_DEFAULT, newFileName))) { - nsCOMPtr<nsIFile> localFile; - GetLocalFileFromURI(aURI, getter_AddRefs(localFile)); - if (localFile) { - localFile->SetLeafName(newFileName); - - // Resync the URI with the file after the extension has been appended - return NS_MutateURI(aURI) - .Apply(&nsIFileURLMutator::SetFile, localFile) + nsCOMPtr<nsIMIMEInfo> mimeInfo; + mMIMEService->GetFromTypeAndExtension(contentType, ""_ns, + getter_AddRefs(mimeInfo)); + + nsCOMPtr<nsIFile> localFile; + GetLocalFileFromURI(aURI, getter_AddRefs(localFile)); + + if (mimeInfo) { + nsCOMPtr<nsIURL> url(do_QueryInterface(aURI)); + NS_ENSURE_TRUE(url, NS_ERROR_FAILURE); + + nsAutoCString newFileName; + url->GetFileName(newFileName); + + // Test if the current extension is current for the mime type + bool hasExtension = false; + int32_t ext = newFileName.RFind("."); + if (ext != -1) { + mimeInfo->ExtensionExists(Substring(newFileName, ext + 1), + &hasExtension); + } + + // Append the mime file extension + nsAutoCString fileExt; + if (!hasExtension) { + // Test if previous extension is acceptable + nsCOMPtr<nsIURL> oldurl(do_QueryInterface(aOriginalURIWithExtension)); + NS_ENSURE_TRUE(oldurl, NS_ERROR_FAILURE); + oldurl->GetFileExtension(fileExt); + bool useOldExt = false; + if (!fileExt.IsEmpty()) { + mimeInfo->ExtensionExists(fileExt, &useOldExt); + } + + // If the url doesn't have an 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 url extension. + if (!useOldExt) { + nsAutoCString primaryExt; + mimeInfo->GetPrimaryExtension(primaryExt); + if (!primaryExt.IsEmpty()) { + fileExt = primaryExt; + } + } + + if (!fileExt.IsEmpty()) { + uint32_t newLength = newFileName.Length() + fileExt.Length() + 1; + if (newLength > kDefaultMaxFilenameLength) { + if (fileExt.Length() > kDefaultMaxFilenameLength / 2) + fileExt.Truncate(kDefaultMaxFilenameLength / 2); + + uint32_t diff = kDefaultMaxFilenameLength - 1 - fileExt.Length(); + if (newFileName.Length() > diff) newFileName.Truncate(diff); + } + newFileName.Append('.'); + newFileName.Append(fileExt); + } + + if (localFile) { + localFile->SetLeafName(NS_ConvertUTF8toUTF16(newFileName)); + + // Resync the URI with the file after the extension has been appended + return NS_MutateURI(url) + .Apply(&nsIFileURLMutator::SetFile, localFile) + .Finalize(aOutURI); + } + return NS_MutateURI(url) + .Apply(&nsIURLMutator::SetFileName, newFileName, nullptr) .Finalize(aOutURI); } - return NS_MutateURI(aURI) - .Apply(&nsIURLMutator::SetFileName, - NS_ConvertUTF16toUTF8(newFileName), nullptr) - .Finalize(aOutURI); } } // TODO (:valentin) This method should always clone aURI aOutURI = aURI; return NS_OK; }
--- a/image/imgIRequest.idl +++ b/image/imgIRequest.idl @@ -96,24 +96,16 @@ interface imgIRequest : nsIRequest */ readonly attribute nsIURI finalURI; readonly attribute imgINotificationObserver notificationObserver; readonly attribute string mimeType; /** - * The filename that should be used when saving the image. This is determined - * from the Content-Disposition, if present, or the uri of the image. This - * filename should be validated using nsIMIMEService::GetValidFilenameForSaving - * before creating the file. - */ - readonly attribute ACString fileName; - - /** * Clone this request; the returned request will have aObserver as the * observer. aObserver will be notified synchronously (before the clone() * call returns) with all the notifications that have already been dispatched * for this image load. */ imgIRequest clone(in imgINotificationObserver aObserver); /**
--- a/image/imgRequest.cpp +++ b/image/imgRequest.cpp @@ -26,25 +26,23 @@ #include "nsIHttpChannel.h" #include "nsMimeTypes.h" #include "nsIInterfaceRequestorUtils.h" #include "nsISupportsPrimitives.h" #include "nsIScriptSecurityManager.h" #include "nsComponentManagerUtils.h" #include "nsContentUtils.h" -#include "nsEscape.h" #include "plstr.h" // PL_strcasestr(...) #include "prtime.h" // for PR_Now #include "nsNetUtil.h" #include "nsIProtocolHandler.h" #include "imgIRequest.h" #include "nsProperties.h" -#include "nsIURL.h" #include "mozilla/IntegerPrintfMacros.h" #include "mozilla/SizeOfState.h" using namespace mozilla; using namespace mozilla::image; #define LOG_TEST(level) (MOZ_LOG_TEST(gImgLog, (level))) @@ -458,40 +456,16 @@ bool imgRequest::HasConsumers() const { } already_AddRefed<image::Image> imgRequest::GetImage() const { MutexAutoLock lock(mMutex); RefPtr<image::Image> image = mImage; return image.forget(); } -void imgRequest::GetFileName(nsACString& aFileName) { - nsAutoString fileName; - - nsCOMPtr<nsISupportsCString> supportscstr; - if (NS_SUCCEEDED(mProperties->Get("content-disposition", - NS_GET_IID(nsISupportsCString), - getter_AddRefs(supportscstr))) && - supportscstr) { - nsAutoCString cdHeader; - supportscstr->GetData(cdHeader); - NS_GetFilenameFromDisposition(fileName, cdHeader); - } - - if (fileName.IsEmpty()) { - nsCOMPtr<nsIURL> imgUrl(do_QueryInterface(mURI)); - if (imgUrl) { - imgUrl->GetFileName(aFileName); - NS_UnescapeURL(aFileName); - } - } else { - aFileName = NS_ConvertUTF16toUTF8(fileName); - } -} - int32_t imgRequest::Priority() const { int32_t priority = nsISupportsPriority::PRIORITY_NORMAL; nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mRequest); if (p) { p->GetPriority(&priority); } return priority; }
--- a/image/imgRequest.h +++ b/image/imgRequest.h @@ -146,18 +146,16 @@ class imgRequest final : public nsIStrea bool IsChrome() const; bool IsData() const; nsresult GetImageErrorCode(void); /// Returns a non-owning pointer to this imgRequest's MIME type. const char* GetMimeType() const { return mContentType.get(); } - void GetFileName(nsACString& aFileName); - /// @return the priority of the underlying network request, or /// PRIORITY_NORMAL if it doesn't support nsISupportsPriority. int32_t Priority() const; /// Adjust the priority of the underlying network request by @aDelta on behalf /// of @aProxy. void AdjustPriority(imgRequestProxy* aProxy, int32_t aDelta);
--- a/image/imgRequestProxy.cpp +++ b/image/imgRequestProxy.cpp @@ -742,26 +742,16 @@ imgRequestProxy::GetMimeType(char** aMim return NS_ERROR_FAILURE; } *aMimeType = NS_xstrdup(type); return NS_OK; } -NS_IMETHODIMP -imgRequestProxy::GetFileName(nsACString& aFileName) { - if (!GetOwner()) { - return NS_ERROR_FAILURE; - } - - GetOwner()->GetFileName(aFileName); - return NS_OK; -} - imgRequestProxy* imgRequestProxy::NewClonedProxy() { return new imgRequestProxy(); } NS_IMETHODIMP imgRequestProxy::Clone(imgINotificationObserver* aObserver, imgIRequest** aClone) { nsresult result;
--- a/netwerk/mime/nsIMIMEService.idl +++ b/netwerk/mime/nsIMIMEService.idl @@ -3,17 +3,16 @@ * 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 "nsISupports.idl" interface nsIFile; interface nsIMIMEInfo; interface nsIURI; -interface nsIChannel; %{C++ #define NS_MIMESERVICE_CID \ { /* 03af31da-3109-11d3-8cd0-0060b0fc14a3 */ \ 0x03af31da, \ 0x3109, \ 0x11d3, \ {0x8c, 0xd0, 0x00, 0x60, 0xb0, 0xfc, 0x14, 0xa3} \ @@ -93,129 +92,9 @@ interface nsIMIMEService : nsISupports { * @return A nsIMIMEInfo object. This function must return * a MIMEInfo object if it can allocate one. The * only justifiable reason for not returning one is * an out-of-memory error. */ 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; - - /** - * If the filename is empty, return the empty filename - * without modification. - */ - const long VALIDATE_ALLOW_EMPTY = 16; - - /** - * Don't apply a default filename if the non-extension portion of the - * filename is empty. - */ - const long VALIDATE_NO_DEFAULT_FILENAME = 32; - - /** - * 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, either ' ' or an ideographic space 0x3000 if present. - * - Unless VALIDATE_DONT_TRUNCATE is specified, the filename is truncated - * to a maximum length, preserving the extension if possible. - * - Some filenames are invalid on certain platforms. These are replaced if - * possible. - * - * If the VALIDATE_NO_DEFAULT_FILENAME flag is not specified, and after the - * rules above are applied, the resulting filename is empty, a default - * filename is used. - * - * If the VALIDATE_ALLOW_EMPTY flag is specified, an empty string may be - * returned only if the filename could not be determined or was blank. - * - * 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); };
--- a/toolkit/components/downloads/DownloadPaths.jsm +++ b/toolkit/components/downloads/DownloadPaths.jsm @@ -5,32 +5,93 @@ /** * Provides methods for giving names and paths to files being downloaded. */ "use strict"; var EXPORTED_SYMBOLS = ["DownloadPaths"]; +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "AppConstants", + "resource://gre/modules/AppConstants.jsm" +); + +/** + * Platform-dependent regular expression used by the "sanitize" method. + */ +XPCOMUtils.defineLazyGetter(this, "gConvertToSpaceRegExp", () => { + // Note: we remove colons everywhere to avoid issues in subresource URL + // parsing, as well as filename restrictions on some OSes (see bug 1562176). + /* eslint-disable no-control-regex */ + switch (AppConstants.platform) { + // On mobile devices, the file system may be very limited in what it + // considers valid characters. To avoid errors, sanitize conservatively. + case "android": + return /[\x00-\x1f\x7f-\x9f:*?|"<>;,+=\[\]]+/g; + case "win": + return /[\x00-\x1f\x7f-\x9f:*?|]+/g; + default: + return /[\x00-\x1f\x7f-\x9f:]+/g; + } + /* eslint-enable no-control-regex */ +}); + var DownloadPaths = { /** - * Sanitizes an arbitrary string via mimeSvc.validateFileNameForSaving. + * Sanitizes an arbitrary string for use as the local file name of a download. + * The input is often a document title or a manually edited name. The output + * can be an empty string if the input does not include any valid character. + * + * The length of the resulting string is not limited, because restrictions + * apply to the full path name after the target folder has been added. + * + * Splitting the base name and extension to add a counter or to identify the + * file type should only be done after the sanitization process, because it + * can alter the final part of the string or remove leading dots. + * + * Runs of slashes and backslashes are replaced with an underscore. + * + * On Windows, the angular brackets `<` and `>` are replaced with parentheses, + * and double quotes are replaced with single quotes. + * + * Runs of control characters are replaced with a space. On Mac, colons are + * also included in this group. On Windows, stars, question marks, and pipes + * are additionally included. On Android, semicolons, commas, plus signs, + * equal signs, and brackets are additionally included. + * + * Leading and trailing dots and whitespace are removed on all platforms. This + * avoids the accidental creation of hidden files on Unix and invalid or + * inaccessible file names on Windows. These characters are not removed when + * located at the end of the base name or at the beginning of the extension. * * @param {string} leafName The full leaf name to sanitize * @param {boolean} [compressWhitespaces] Whether consecutive whitespaces * should be compressed. */ sanitize(leafName, { compressWhitespaces = true } = {}) { - const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); - - let flags = mimeSvc.VALIDATE_SANITIZE_ONLY | mimeSvc.VALIDATE_DONT_TRUNCATE; - if (!compressWhitespaces) { - flags |= mimeSvc.VALIDATE_DONT_COLLAPSE_WHITESPACE; + if (AppConstants.platform == "win") { + leafName = leafName + .replace(/</g, "(") + .replace(/>/g, ")") + .replace(/"/g, "'"); } - return mimeSvc.validateFileNameForSaving(leafName, "", flags); + leafName = leafName + .replace(/[\\/]+/g, "_") + .replace(/[\u200e\u200f\u202a-\u202e]/g, "") + .replace(gConvertToSpaceRegExp, " "); + if (compressWhitespaces) { + leafName = leafName.replace(/\s{2,}/g, " "); + } + return leafName.replace(/^[\s\u180e.]+|[\s\u180e.]+$/g, ""); }, /** * Creates a uniquely-named file starting from the name of the provided file. * If a file with the provided name already exists, the function attempts to * create nice alternatives, like "base(1).ext" (instead of "base-1.ext"). * * If a unique name cannot be found, the function throws the XPCOM exception
--- a/toolkit/components/downloads/test/unit/test_DownloadPaths.js +++ b/toolkit/components/downloads/test/unit/test_DownloadPaths.js @@ -35,55 +35,55 @@ add_task(async function test_sanitize() const kSpecialChars = 'A:*?|""<<>>;,+=[]B][=+,;>><<""|?*:C'; if (AppConstants.platform == "android") { testSanitize(kSpecialChars, "A B C"); testSanitize(" :: Website :: ", "Website"); testSanitize("* Website!", "Website!"); testSanitize("Website | Page!", "Website Page!"); testSanitize("Directory Listing: /a/b/", "Directory Listing _a_b_"); } else if (AppConstants.platform == "win") { - testSanitize(kSpecialChars, "A ;,+=[]B][=+,; C"); + testSanitize(kSpecialChars, "A ''(());,+=[]B][=+,;))(('' C"); testSanitize(" :: Website :: ", "Website"); testSanitize("* Website!", "Website!"); testSanitize("Website | Page!", "Website Page!"); testSanitize("Directory Listing: /a/b/", "Directory Listing _a_b_"); } else if (AppConstants.platform == "macosx") { - testSanitize(kSpecialChars, "A ;,+=[]B][=+,; C"); + testSanitize(kSpecialChars, 'A *?|""<<>>;,+=[]B][=+,;>><<""|?* C'); testSanitize(" :: Website :: ", "Website"); - testSanitize("* Website!", "Website!"); - testSanitize("Website | Page!", "Website Page!"); + testSanitize("* Website!", "* Website!"); + testSanitize("Website | Page!", "Website | Page!"); testSanitize("Directory Listing: /a/b/", "Directory Listing _a_b_"); } else { - testSanitize(kSpecialChars, "A ;,+=[]B][=+,; C"); + testSanitize(kSpecialChars, kSpecialChars.replace(/[:]/g, " ")); testSanitize(" :: Website :: ", "Website"); - testSanitize("* Website!", "Website!"); - testSanitize("Website | Page!", "Website Page!"); + testSanitize("* Website!", "* Website!"); + testSanitize("Website | Page!", "Website | Page!"); testSanitize("Directory Listing: /a/b/", "Directory Listing _a_b_"); } // Conversion of consecutive runs of slashes and backslashes to underscores. - testSanitize("\\ \\\\Website\\/Page// /", "_ __Website__Page__ _"); + testSanitize("\\ \\\\Website\\/Page// /", "_ _Website_Page_ _"); // Removal of leading and trailing whitespace and dots after conversion. testSanitize(" Website ", "Website"); testSanitize(". . Website . Page . .", "Website . Page"); testSanitize(" File . txt ", "File . txt"); testSanitize("\f\n\r\t\v\x00\x1f\x7f\x80\x9f\xa0 . txt", "txt"); testSanitize("\u1680\u180e\u2000\u2008\u200a . txt", "txt"); testSanitize("\u2028\u2029\u202f\u205f\u3000\ufeff . txt", "txt"); // Strings with whitespace and dots only. testSanitize(".", ""); testSanitize("..", ""); testSanitize(" ", ""); testSanitize(" . ", ""); // Stripping of BIDI formatting characters. - testSanitize("\u200e \u202b\u202c\u202d\u202etest\x7f\u200f", "_ ____test _"); - testSanitize("AB\x7f\u202a\x7f\u202a\x7fCD", "AB _ _ CD"); + testSanitize("\u200e \u202b\u202c\u202d\u202etest\x7f\u200f", "test"); + testSanitize("AB\x7f\u202a\x7f\u202a\x7fCD", "AB CD"); // Stripping of colons: testSanitize("foo:bar", "foo bar"); // not compressing whitespaces. testSanitize("foo : bar", "foo bar", { compressWhitespaces: false }); });
--- a/toolkit/content/contentAreaUtils.js +++ b/toolkit/content/contentAreaUtils.js @@ -563,57 +563,47 @@ function initFileInfo( aFI, aURL, aURLCharset, aDocument, aContentType, aContentDisposition ) { try { - let uriExt = null; // Get an nsIURI object from aURL if possible: try { aFI.uri = makeURI(aURL, aURLCharset); // Assuming nsiUri is valid, calling QueryInterface(...) on it will // populate extra object fields (eg filename and file extension). - uriExt = aFI.uri.QueryInterface(Ci.nsIURL).fileExtension; + var url = aFI.uri.QueryInterface(Ci.nsIURL); + aFI.fileExt = url.fileExtension; } catch (e) {} // Get the default filename: - let fileName = getDefaultFileName( + aFI.fileName = getDefaultFileName( aFI.suggestedFileName || aFI.fileName, aFI.uri, aDocument, aContentDisposition ); - - let mimeService = this.getMIMEService(); - aFI.fileName = mimeService.validateFileNameForSaving( - fileName, - aContentType, - mimeService.VALIDATE_DEFAULT - ); - - // If uriExt is blank, consider: aFI.suggestedFileName is supplied if - // saveURL(...) was the original caller (hence both aContentType and + // If aFI.fileExt is still blank, consider: aFI.suggestedFileName is supplied + // if saveURL(...) was the original caller (hence both aContentType and // aDocument are blank). If they were saving a link to a website then make // the extension .htm . if ( - !uriExt && + !aFI.fileExt && !aDocument && !aContentType && /^http(s?):\/\//i.test(aURL) ) { aFI.fileExt = "htm"; aFI.fileBaseName = aFI.fileName; } else { - let idx = aFI.fileName.lastIndexOf("."); - aFI.fileBaseName = - idx >= 0 ? aFI.fileName.substring(0, idx) : aFI.fileName; - aFI.fileExt = idx >= 0 ? aFI.fileName.substring(idx + 1) : null; + aFI.fileExt = getDefaultExtension(aFI.fileName, aFI.uri, aContentType); + aFI.fileBaseName = getFileBaseName(aFI.fileName); } } catch (e) {} } /** * Given the Filepicker Parameters (aFpP), show the file picker dialog, * prompting the user to confirm (or change) the fileName. * @param aFpP @@ -650,17 +640,19 @@ function promiseTargetFile( // Default to the user's default downloads directory configured // through download prefs. let dirPath = await Downloads.getPreferredDownloadsDirectory(); let dirExists = await OS.File.exists(dirPath); let dir = new FileUtils.File(dirPath); if (useDownloadDir && dirExists) { - dir.append(aFpP.fileInfo.fileName); + dir.append( + getNormalizedLeafName(aFpP.fileInfo.fileName, aFpP.fileInfo.fileExt) + ); aFpP.file = uniqueFile(dir); return true; } // We must prompt for the file name explicitly. // If we must prompt because we were asked to... let file = await new Promise(resolve => { if (useDownloadDir) { @@ -691,17 +683,20 @@ function promiseTargetFile( fp.init( window, ContentAreaUtils.stringBundle.GetStringFromName(titleKey), Ci.nsIFilePicker.modeSave ); fp.displayDirectory = dir; fp.defaultExtension = aFpP.fileInfo.fileExt; - fp.defaultString = aFpP.fileInfo.fileName; + fp.defaultString = getNormalizedLeafName( + aFpP.fileInfo.fileName, + aFpP.fileInfo.fileExt + ); appendFiltersForContentType( fp, aFpP.contentType, aFpP.fileInfo.fileExt, aFpP.saveMode ); // The index of the selected filter is only preserved and restored if there's @@ -1021,76 +1016,94 @@ function getDefaultFileName( "name", charset, true, dummy ); } catch (e) {} } if (fileName) { - return Services.textToSubURI.unEscapeURIForUI( - fileName, - /* dontEscape = */ true + return validateFileName( + Services.textToSubURI.unEscapeURIForUI( + fileName, + /* dontEscape = */ true + ) ); } } let docTitle; if (aDocument && aDocument.title && aDocument.title.trim()) { // If the document looks like HTML or XML, try to use its original title. + docTitle = validateFileName(aDocument.title); let contentType = aDocument.contentType; if ( contentType == "application/xhtml+xml" || contentType == "application/xml" || contentType == "image/svg+xml" || contentType == "text/html" || contentType == "text/xml" ) { // 2) Use the document title - return aDocument.title; + return docTitle; } } try { var url = aURI.QueryInterface(Ci.nsIURL); if (url.fileName != "") { // 3) Use the actual file name, if present - return Services.textToSubURI.unEscapeURIForUI( - url.fileName, - /* dontEscape = */ true + return validateFileName( + Services.textToSubURI.unEscapeURIForUI( + url.fileName, + /* dontEscape = */ true + ) ); } } catch (e) { // This is something like a data: and so forth URI... no filename here. } // Don't use the title if it's from a data URI if (docTitle && aURI?.scheme != "data") { // 4) Use the document title return docTitle; } if (aDefaultFileName) { // 5) Use the caller-provided name, if any - return aDefaultFileName; + return validateFileName(aDefaultFileName); + } + + // 6) If this is a directory, use the last directory name + var path = aURI.pathQueryRef.match(/\/([^\/]+)\/$/); + if (path && path.length > 1) { + return validateFileName(path[1]); } try { if (aURI.host) { - // 6) Use the host. - return aURI.host; + // 7) Use the host. + return validateFileName(aURI.host); } } catch (e) { // Some files have no information at all, like Javascript generated pages } - - return ""; + try { + // 8) Use the default file name + return ContentAreaUtils.stringBundle.GetStringFromName( + "DefaultSaveFileName" + ); + } catch (e) { + // in case localized string cannot be found + } + // 9) If all else fails, use "index" + return "index"; } -// This is only used after the user has entered a filename. function validateFileName(aFileName) { let processed = DownloadPaths.sanitize(aFileName) || "_"; if (AppConstants.platform == "android") { // If a large part of the filename has been sanitized, then we // will use a default filename instead if (processed.replace(/_/g, "").length <= processed.length / 2) { // We purposefully do not use a localized default filename, // which we could have done using @@ -1106,16 +1119,130 @@ function validateFileName(aFileName) { processed += "." + suffix; } } } } return processed; } +// This is the set of image extensions supported by extraMimeEntries in +// nsExternalHelperAppService. +const kImageExtensions = new Set([ + "art", + "bmp", + "gif", + "ico", + "cur", + "jpeg", + "jpg", + "jfif", + "pjpeg", + "pjp", + "png", + "apng", + "tiff", + "tif", + "xbm", + "svg", + "webp", + "avif", + "jxl", +]); + +function getNormalizedLeafName(aFile, aDefaultExtension) { + if (!aDefaultExtension) { + return aFile; + } + + if (AppConstants.platform == "win") { + // Remove trailing dots and spaces on windows + aFile = aFile.replace(/[\s.]+$/, ""); + } + + // Remove leading dots + aFile = aFile.replace(/^\.+/, ""); + + // Include the default extension in the file name to which we're saving. + var i = aFile.lastIndexOf("."); + let previousExtension = aFile.substr(i + 1).toLowerCase(); + if (previousExtension != aDefaultExtension.toLowerCase()) { + // Suffixing the extension is the safe bet - it preserves the previous + // extension in case that's meaningful (e.g. various text files served + // with text/plain will end up as `foo.cpp.txt`, in the worst case). + // However, for images, the extension derived from the URL should be + // replaced if the content type indicates a different filetype - this makes + // sure that we treat e.g. feature-tested webp/avif images correctly. + if (kImageExtensions.has(previousExtension)) { + return aFile.substr(0, i + 1) + aDefaultExtension; + } + return aFile + "." + aDefaultExtension; + } + + return aFile; +} + +function getDefaultExtension(aFilename, aURI, aContentType) { + if ( + aContentType == "text/plain" || + aContentType == "application/octet-stream" || + aURI.scheme == "ftp" + ) { + return ""; + } // temporary fix for bug 120327 + + // First try the extension from the filename + var url = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec("http://example.com") // construct the URL + .setFilePath(aFilename) + .finalize() + .QueryInterface(Ci.nsIURL); + + var ext = url.fileExtension; + + // This mirrors some code in nsExternalHelperAppService::DoContent + // Use the filename first and then the URI if that fails + + // For images, rely solely on the mime type if known. + // All the extension is going to do is lie to us. + var lookupExt = ext; + if (aContentType?.startsWith("image/")) { + lookupExt = ""; + } + var mimeInfo = getMIMEInfoForType(aContentType, lookupExt); + + if (ext && mimeInfo && mimeInfo.extensionExists(ext)) { + return ext; + } + + // Well, that failed. Now try the extension from the URI + var urlext; + try { + url = aURI.QueryInterface(Ci.nsIURL); + urlext = url.fileExtension; + } catch (e) {} + + if (urlext && mimeInfo && mimeInfo.extensionExists(urlext)) { + return urlext; + } + + // That failed as well. If we could lookup the MIME use the primary + // extension for that type. + try { + if (mimeInfo) { + return mimeInfo.primaryExtension; + } + } catch (e) {} + + // Fall back on the extensions in the filename and URI for lack + // of anything better. + return ext || urlext; +} + function GetSaveModeForContentType(aContentType, aDocument) { // We can only save a complete page if we have a loaded document, if (!aDocument) { return SAVEMODE_FILEONLY; } // Find the possible save modes using the provided content type var saveMode = SAVEMODE_FILEONLY;
--- a/toolkit/mozapps/downloads/HelperAppDlg.jsm +++ b/toolkit/mozapps/downloads/HelperAppDlg.jsm @@ -289,23 +289,21 @@ nsUnknownContentTypeDialog.prototype = { ); if (autodownload) { // Retrieve the user's default download directory let preferredDir = await Downloads.getPreferredDownloadsDirectory(); let defaultFolder = new FileUtils.File(preferredDir); try { - if (aDefaultFileName) { - result = this.validateLeafName( - defaultFolder, - aDefaultFileName, - aSuggestedFileExtension - ); - } + result = this.validateLeafName( + defaultFolder, + aDefaultFileName, + aSuggestedFileExtension + ); } catch (ex) { // When the default download directory is write-protected, // prompt the user for a different target file. } // Check to make sure we have a valid directory, otherwise, prompt if (result) { // This path is taken when we have a writable default download directory.
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -46,18 +46,16 @@ #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" @@ -100,30 +98,27 @@ #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 255 - // 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 }; @@ -180,16 +175,118 @@ static nsresult UnescapeFragment(const n 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, @@ -510,16 +607,18 @@ static const nsExtraMimeTypeEntry extraM {"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"}, @@ -705,20 +804,18 @@ nsresult nsExternalHelperAppService::DoC childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp, contentDisposition, fileName, aForceSave, contentLength, wasFileChannel, referrer, aContentContext, shouldCloseWindow)); NS_ADDREF(*aStreamListener = childListener); uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE; - SanitizeFileName(fileName, EmptyCString(), 0); - RefPtr<nsExternalAppHandler> handler = - new nsExternalAppHandler(nullptr, u""_ns, aContentContext, aWindowContext, + new nsExternalAppHandler(nullptr, ""_ns, aContentContext, aWindowContext, this, fileName, reason, aForceSave); if (!handler) { return NS_ERROR_OUT_OF_MEMORY; } childListener->SetHandler(handler); return NS_OK; } @@ -728,73 +825,126 @@ NS_IMETHODIMP nsExternalHelperAppService 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) { - uint32_t contentDisposition = -1; + channel->GetURI(getter_AddRefs(uri)); + channel->GetContentLength(&contentLength); channel->GetContentDisposition(&contentDisposition); - if (contentDisposition == nsIChannel::DISPOSITION_ATTACHMENT) { + 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; } } - *aStreamListener = nullptr; - - // Get the file extension and name that we will need later - nsCOMPtr<nsIURI> uri; - bool allowURLExtension = - GetFileNameFromChannel(channel, fileName, getter_AddRefs(uri)); - - uint32_t flags = VALIDATE_ALLOW_EMPTY; + 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)) { - flags |= VALIDATE_GUESS_FROM_EXTENSION; + 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)); } - - 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) { - 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); - } + *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, extension, aContentContext, aWindowContext, this, fileName, - reason, aForceSave); + 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; } @@ -1273,24 +1423,24 @@ NS_INTERFACE_MAP_BEGIN(nsExternalAppHand NS_INTERFACE_MAP_ENTRY(nsIHelperAppLauncher) NS_INTERFACE_MAP_ENTRY(nsICancelable) NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver) NS_INTERFACE_MAP_ENTRY(nsINamed) NS_INTERFACE_MAP_ENTRY_CONCRETE(nsExternalAppHandler) NS_INTERFACE_MAP_END nsExternalAppHandler::nsExternalAppHandler( - nsIMIMEInfo* aMIMEInfo, const nsAString& aFileExtension, + nsIMIMEInfo* aMIMEInfo, const nsACString& aTempFileExtension, 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), mIsFileChannel(false), mShouldCloseWindow(false), mHandleInternally(false), mReason(aReason), mTempFileIsExecutable(false), @@ -1298,28 +1448,145 @@ nsExternalAppHandler::nsExternalAppHandl mContentLength(-1), mProgress(0), mSaver(nullptr), mDialogProgressListener(nullptr), mTransfer(nullptr), mRequest(nullptr), mExtProtSvc(aExtProtSvc) { // make sure the extention includes the '.' - if (!aFileExtension.IsEmpty() && aFileExtension.First() != '.') { - mFileExtension = char16_t('.'); + 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); } - mFileExtension.Append(aFileExtension); + + // 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) { @@ -2521,25 +2788,25 @@ NS_IMETHODIMP nsExternalAppHandler::Prom if (mCanceled) return NS_OK; if (!StaticPrefs::browser_download_improvements_to_download_panel() || mForceSave) { mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); } if (mSuggestedFileName.IsEmpty()) { - RequestSaveDestination(mTempLeafName, mFileExtension); + RequestSaveDestination(mTempLeafName, mTempFileExtension); } else { nsAutoString fileExt; int32_t pos = mSuggestedFileName.RFindChar('.'); if (pos >= 0) { mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos); } if (fileExt.IsEmpty()) { - fileExt = mFileExtension; + fileExt = mTempFileExtension; } RequestSaveDestination(mSuggestedFileName, fileExt); } return NS_OK; } nsresult nsExternalAppHandler::ContinueSave(nsIFile* aNewFileLocation) { @@ -2657,24 +2924,17 @@ NS_IMETHODIMP nsExternalAppHandler::SetD if (mSuggestedFileName.IsEmpty()) { // Keep using the leafname of the temp file, since we're just starting a // helper mSuggestedFileName = mTempLeafName; } #ifdef XP_WIN - // Ensure we don't double-append the file extension if it matches: - if (StringEndsWith(mSuggestedFileName, mFileExtension, - nsCaseInsensitiveStringComparator)) { - fileToUse->Append(mSuggestedFileName); - } else { - GetFileNameFromChannel fileToUse->Append(mSuggestedFileName + - mFileExtension); - } + fileToUse->Append(mSuggestedFileName + mTempFileExtension); #else fileToUse->Append(mSuggestedFileName); #endif } nsresult rv = fileToUse->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); if (NS_SUCCEEDED(rv)) { mFinalFileDestination = fileToUse; @@ -3219,479 +3479,8 @@ bool nsExternalHelperAppService::GetMIME nsresult nsExternalHelperAppService::GetMIMEInfoFromOS( const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound, nsIMIMEInfo** aMIMEInfo) { *aMIMEInfo = nullptr; *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; - - bool isBinaryType = aMimeType.EqualsLiteral(APPLICATION_OCTET_STREAM) || - aMimeType.EqualsLiteral(BINARY_OCTET_STREAM) || - aMimeType.EqualsLiteral("application/x-msdownload"); - - // 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, or if this - // is a binary type in which case the type might not be valid - // anyway. - if (aAllowURLExtension || isBinaryType) { - 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 = - isBinaryType || 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 an empty filename is allowed, then return early. It will be saved - // using the filename of the temporary file that was created for the download. - if (aFlags & VALIDATE_ALLOW_EMPTY && fileName.IsEmpty()) { - aFileName.Truncate(); - return mimeInfo.forget(); - } - - if (mimeInfo) { - bool isValidExtension; - if (extension.IsEmpty() || - NS_FAILED(mimeInfo->ExtensionExists(extension, &isValidExtension)) || - !isValidExtension) { - // Skip these checks for text and binary, so we don't append the unneeded - // .txt or other extension. - if (aMimeType.EqualsLiteral(TEXT_PLAIN) || isBinaryType) { - extension.Truncate(); - } else { - nsAutoCString originalExtension(extension); - // If an original url was supplied, see if it has a valid extension. - bool useOldExtension = false; - if (aOriginalURI) { - nsCOMPtr<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)); - } - } - } - } - -#ifdef XP_WIN - nsLocalFile::CheckForReservedFileName(fileName); -#endif - - // If no filename is present, use a default filename. - if (!(aFlags & VALIDATE_NO_DEFAULT_FILENAME) && - (fileName.Length() == 0 || fileName.RFind(".") == 0)) { - nsCOMPtr<nsIStringBundleService> stringService = - mozilla::components::StringBundle::Service(); - if (stringService) { - nsCOMPtr<nsIStringBundle> bundle; - if (NS_SUCCEEDED(stringService->CreateBundle( - "chrome://global/locale/contentAreaCommands.properties", - getter_AddRefs(bundle)))) { - nsAutoString defaultFileName; - bundle->GetStringFromName("DefaultSaveFileName", defaultFileName); - // Append any existing extension to the default filename. - fileName = defaultFileName + fileName; - } - } - - // Use 'index' as a last resort. - if (!fileName.Length()) { - fileName.AssignLiteral("index"); - } - } - - // 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); - - // Replace characters - fileName.ReplaceChar(KNOWN_PATH_SEPARATORS, '_'); - fileName.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' '); - fileName.StripChar(char16_t(0)); - - const char16_t *startStr, *endStr; - fileName.BeginReading(startStr); - fileName.EndReading(endStr); - - // True if multiple consecutive whitespace characters should - // be replaced by single space ' '. - bool collapseWhitespace = !(aFlags & VALIDATE_DONT_COLLAPSE_WHITESPACE); - - // The maximum filename length differs based on the platform: - // Windows (FAT/NTFS) stores filenames as a maximum of 255 UTF-16 code units. - // Mac (APFS) stores filenames with a maximum 255 of UTF-8 code units. - // Linux (ext3/ext4...) stores filenames with a maximum 255 bytes. - // So here we just use the maximum of 255 bytes. - uint32_t maxBytes = 0; // 0 means don't truncate at a maximum size. - if (!(aFlags & VALIDATE_DONT_TRUNCATE)) { - maxBytes = 255 - aExtension.Length() - 1; - } - - // True if the last character added was whitespace. - bool lastWasWhitespace = false; - - // True if the filename is too long and must be truncated. - bool longFileName = false; - - // Length of the filename that fits into the maximum size excluding the - // extension and period. - int32_t longFileNameEnd = -1; - - // Index of the last character added that was not a character that can be - // trimmed off of the end of the string. Trimmable characters are whitespace, - // periods and the vowel separator u'\u180e'. If all the characters after this - // point are trimmable characters, truncate the string to this point after - // iterating over the filename. - int32_t lastNonTrimmable = -1; - - // The number of bytes that the string would occupy if encoded in UTF-8. - uint32_t bytesLength = 0; - - // This algorithm iterates over each character in the string and appends it - // or a replacement character if needed to outFileName. - nsAutoString outFileName; - while (startStr < endStr) { - bool err = false; - char32_t nextChar = UTF16CharEnumerator::NextChar(&startStr, endStr, &err); - if (err) { - break; - } - - if (nextChar == char16_t(0)) { - continue; - } - - auto unicodeCategory = unicode::GetGeneralCategory(nextChar); - if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_CONTROL || - unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR || - unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_PARAGRAPH_SEPARATOR) { - // Skip over any control characters and separators. - continue; - } - - if (maxBytes) { - // UTF16CharEnumerator already converts surrogate pairs, so we can use - // a simple computation of byte length here. - bytesLength += nextChar < 0x80 ? 1 - : nextChar < 0x800 ? 2 - : nextChar < 0x10000 ? 3 - : 4; - if (bytesLength > maxBytes) { - if (longFileNameEnd == -1) { - longFileNameEnd = int32_t(outFileName.Length()); - } - if (bytesLength > 255) { - longFileName = true; - break; - } - } - } - - if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_SPACE_SEPARATOR || - nextChar == u'\ufeff') { - // Trim out any whitespace characters at the beginning of the filename, - // and only add whitespace in the middle of the filename if the last - // character was not whitespace or if we are not collapsing whitespace. - if (!outFileName.IsEmpty() && - (!lastWasWhitespace || !collapseWhitespace)) { - // Allow the ideographic space if it is present, otherwise replace with - // ' '. - if (nextChar != u'\u3000') { - nextChar = ' '; - } - lastWasWhitespace = true; - } else { - lastWasWhitespace = true; - continue; - } - } else { - lastWasWhitespace = false; - if (nextChar == '.' || nextChar == u'\u180e') { - // Don't add any periods or vowel separators at the beginning of the - // string. Note also that lastNonTrimmable is not adjusted in this - // case, because periods and vowel separators are included in the - // set of characters to trim at the end of the filename. - if (outFileName.IsEmpty()) { - continue; - } - } else { - if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_FORMAT) { - // Replace formatting characters with an underscore. - nextChar = '_'; - } - - lastNonTrimmable = int32_t(outFileName.Length()) + 1; - } - } - - AppendUCS4ToUTF16(nextChar, outFileName); - } - - // There are two ways in which the filename should be truncated: - // - If the filename was too long, truncate the name at the length - // of the filename minus the space needed for the extension and period. - // This position is indicated by longFileNameEnd. - // - lastNonTrimmable will indicate the last character that was not - // whitespace, a period, or a vowel separator at the end of the - // the string, so the string should be truncated there as well. - // If both apply, use the earliest position. - if (lastNonTrimmable >= 0) { - outFileName.Truncate(longFileName - ? std::min(longFileNameEnd, lastNonTrimmable) - : lastNonTrimmable); - } - - // If the filename is too long, truncate it, but preserve the desired - // extension. - if (!maxBytes && !(aFlags & VALIDATE_DONT_TRUNCATE) && - outFileName.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) { - outFileName.Truncate(kDefaultMaxFileNameLength - 1); - } else { - outFileName.Truncate(kDefaultMaxFileNameLength - aExtension.Length() - 1); - longFileName = true; - } - } - - if (longFileName && !outFileName.IsEmpty()) { - if (outFileName.Last() != '.') { - outFileName.AppendLiteral("."); - } - - outFileName.Append(NS_ConvertUTF8toUTF16(aExtension)); - } - - aFileName = outFileName; -} - -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; -}
--- a/uriloader/exthandler/nsExternalHelperAppService.h +++ b/uriloader/exthandler/nsExternalHelperAppService.h @@ -194,42 +194,16 @@ class nsExternalHelperAppService : publi void ExpungeTemporaryFiles(); /** * Functions related to the tempory file cleanup service provided by * nsExternalHelperAppService (for the temporary files added during * the private browsing mode) */ 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 */ nsCOMArray<nsIFile> mTemporaryFilesList; /** * Array for the files that should be deleted (for the temporary files * added during the private browsing mode) */ @@ -272,25 +246,25 @@ class nsExternalAppHandler final : publi * @param aFileExtension The extension we need to append to our temp file, * INCLUDING the ".". e.g. .mp3 * @param aContentContext dom Window context, as passed to DoContent. * @param aWindowContext Top level window context used in dialog parenting, * as passed to DoContent. This parameter may be null, * in which case dialogs will be parented to * aContentContext. * @param mExtProtSvc nsExternalHelperAppService on creation - * @param aSuggestedFileName The filename to use + * @param aFileName The filename to use * @param aReason A constant from nsIHelperAppLauncherDialog * indicating why the request is handled by a helper app. */ - nsExternalAppHandler(nsIMIMEInfo* aMIMEInfo, const nsAString& aFileExtension, + nsExternalAppHandler(nsIMIMEInfo* aMIMEInfo, const nsACString& aFileExtension, mozilla::dom::BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext, nsExternalHelperAppService* aExtProtSvc, - const nsAString& aSuggestedFileName, uint32_t aReason, + const nsAString& aFilename, uint32_t aReason, bool aForceSave); /** * Clean up after the request was diverted to the parent process. */ void DidDivertRequest(nsIRequest* request); /** @@ -305,17 +279,17 @@ class nsExternalAppHandler final : publi void RecordDownloadTelemetry(nsIChannel* aChannel, const char* aAction); bool IsDownloadSpam(nsIChannel* aChannel); ~nsExternalAppHandler(); nsCOMPtr<nsIFile> mTempFile; nsCOMPtr<nsIURI> mSourceUrl; - nsString mFileExtension; + nsString mTempFileExtension; nsString mTempLeafName; /** * The MIME Info for this load. Will never be null. */ nsCOMPtr<nsIMIMEInfo> mMimeInfo; /** @@ -495,23 +469,29 @@ class nsExternalAppHandler final : publi void NotifyTransfer(nsresult aStatus); /** * Helper routine that searches a pref string for a given mime type */ 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 * or if this download is for a mimetype where we enforce using a specific * extension (image/, video/, and audio/ based mimetypes, and a few specific * document types). * - * It also ensure that mFileExtension only contains an extension + * It also ensure that mTempFileExtension only contains an extension * when it is different from mSuggestedFileName's extension. */ void EnsureCorrectExtension(const nsString& aFileExt); typedef enum { kReadError, kWriteError, kLaunchError } ErrorType; /** * Utility function to send proper error notification to web progress listener */
--- a/uriloader/exthandler/tests/mochitest/browser.ini +++ b/uriloader/exthandler/tests/mochitest/browser.ini @@ -103,19 +103,16 @@ support-files = support-files = redirect_helper.sjs [browser_protocol_ask_dialog_permission.js] support-files = redirect_helper.sjs script_redirect.html [browser_protocolhandler_loop.js] [browser_remember_download_option.js] -[browser_save_filenames.js] -support-files = - save_filenames.html [browser_txt_download_save_as.js] support-files = file_txt_attachment_test.txt file_txt_attachment_test.txt^headers^ !/toolkit/content/tests/browser/common/mockTransfer.js [browser_web_protocol_handlers.js] [browser_ftp_protocol_handlers.js] support-files =
--- a/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js +++ b/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js @@ -62,30 +62,18 @@ add_setup(async function() { const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); const mimeInfo = MIMEService.getFromTypeAndExtension( "application/octet-stream", "bin" ); mimeInfo.alwaysAskBeforeHandling = true; HandlerService.store(mimeInfo); - // On Mac, .bin is application/macbinary - let mimeInfoMac; - if (AppConstants.platform == "macosx") { - mimeInfoMac = MIMEService.getFromTypeAndExtension( - "application/macbinary", - "bin" - ); - mimeInfoMac.alwaysAskBeforeHandling = true; - HandlerService.store(mimeInfoMac); - } - registerCleanupFunction(() => { HandlerService.remove(mimeInfo); - HandlerService.remove(mimeInfoMac); }); }); add_task(async function simple_navigation() { // Tests that simple navigation gives us the right windowContext (that is, // the window that we're using). await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( browser
deleted file mode 100644 --- a/uriloader/exthandler/tests/mochitest/browser_save_filenames.js +++ /dev/null @@ -1,737 +0,0 @@ -// There are at least seven different ways in a which a file can be saved or downloaded. This -// test ensures that the filename is determined correctly when saving in these ways. The seven -// ways are: -// - save the file individually from the File menu -// - save as complete web page (this uses a different codepath than the previous one) -// - dragging an image to the local file system -// - copy an image and paste it as a file to the local file system (windows only) -// - open a link with content-disposition set to attachment -// - open a link with the download attribute -// - save a link or image from the context menu - -requestLongerTimeout(5); - -let types = { - text: "text/plain", - png: "image/png", - jpeg: "image/jpeg", - webp: "image/webp", - otherimage: "image/unknown", - // Other js types (application/javascript and text/javascript) are handled by the system - // inconsistently, but application/x-javascript is handled by the external helper app service, - // so it is used here for this test. - js: "application/x-javascript", - binary: "application/octet-stream", - gook: "application/x-gook", -}; - -const PNG_DATA = atob( - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + - "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" -); - -const JPEG_DATA = atob( - "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4z" + - "NDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEB" + - "AxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS" + - "0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKz" + - "tLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgEC" + - "BAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpj" + - "ZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6" + - "/9oADAMBAAIRAxEAPwD3+iiigD//2Q==" -); - -const WEBP_DATA = atob( - "UklGRiIAAABXRUJQVlA4TBUAAAAvY8AYAAfQ/4j+B4CE8H+/ENH/VCIA" -); - -const DEFAULT_INDEX_FILENAME = - AppConstants.platform == "win" ? "index.htm" : "index.html"; - -const PROMISE_FILENAME_TYPE = "application/x-moz-file-promise-dest-filename"; - -let MockFilePicker = SpecialPowers.MockFilePicker; -MockFilePicker.init(window); - -let expectedItems; -let sendAsAttachment = false; -let httpServer = null; - -function handleRequest(aRequest, aResponse) { - const queryString = new URLSearchParams(aRequest.queryString); - let type = queryString.get("type"); - let filename = queryString.get("filename"); - let dispname = queryString.get("dispname"); - - aResponse.setStatusLine(aRequest.httpVersion, 200); - if (type) { - aResponse.setHeader("Content-Type", types[type]); - } - - if (dispname) { - let dispositionType = sendAsAttachment ? "attachment" : "inline"; - aResponse.setHeader( - "Content-Disposition", - dispositionType + ';name="' + dispname + '"' - ); - } else if (filename) { - let dispositionType = sendAsAttachment ? "attachment" : "inline"; - aResponse.setHeader( - "Content-Disposition", - dispositionType + ';filename="' + filename + '"' - ); - } else if (sendAsAttachment) { - aResponse.setHeader("Content-Disposition", "attachment"); - } - - if (type == "png") { - aResponse.write(PNG_DATA); - } else if (type == "jpeg") { - aResponse.write(JPEG_DATA); - } else if (type == "webp") { - aResponse.write(WEBP_DATA); - } else { - aResponse.write("// Some Text"); - } -} - -function handleBasicImageRequest(aRequest, aResponse) { - aResponse.setHeader("Content-Type", "image/png"); - aResponse.write(PNG_DATA); -} - -function handleRedirect(aRequest, aResponse) { - const queryString = new URLSearchParams(aRequest.queryString); - let filename = queryString.get("filename"); - - aResponse.setStatusLine(aRequest.httpVersion, 302); - aResponse.setHeader("Location", "/bell" + filename[0] + "?" + queryString); -} - -function promiseDownloadFinished(list) { - return new Promise(resolve => { - list.addView({ - onDownloadChanged(download) { - if (download.stopped) { - list.removeView(this); - resolve(download); - } - }, - }); - }); -} - -// nsIFile::CreateUnique crops long filenames if the path is too long, but -// we don't know exactly how long depending on the full path length, so -// for those save methods that use CreateUnique, instead just verify that -// the filename starts with the right string and has the correct extension. -function checkShortenedFilename(actual, expected) { - if (actual.length < expected.length) { - let actualDot = actual.lastIndexOf("."); - let actualExtension = actual.substring(actualDot); - let expectedExtension = expected.substring(expected.lastIndexOf(".")); - if ( - actualExtension == expectedExtension && - expected.startsWith(actual.substring(0, actualDot)) - ) { - return true; - } - } - - return false; -} - -add_task(async function init() { - const { HttpServer } = ChromeUtils.import( - "resource://testing-common/httpd.js" - ); - httpServer = new HttpServer(); - httpServer.start(8000); - - // Need to load the page from localhost:8000 as the download attribute - // only applies to links from the same domain. - let saveFilenamesPage = FileUtils.getFile( - "CurWorkD", - "/browser/uriloader/exthandler/tests/mochitest/save_filenames.html".split( - "/" - ) - ); - httpServer.registerFile("/save_filenames.html", saveFilenamesPage); - - // A variety of different scripts are set up to better ensure uniqueness. - httpServer.registerPathHandler("/save_filename.sjs", handleRequest); - httpServer.registerPathHandler("/save_thename.sjs", handleRequest); - httpServer.registerPathHandler("/getdata.png", handleRequest); - httpServer.registerPathHandler("/base", handleRequest); - httpServer.registerPathHandler("/basedata", handleRequest); - httpServer.registerPathHandler("/basetext", handleRequest); - httpServer.registerPathHandler("/text2.txt", handleRequest); - httpServer.registerPathHandler("/text3.gonk", handleRequest); - httpServer.registerPathHandler("/basic.png", handleBasicImageRequest); - httpServer.registerPathHandler("/aquamarine.jpeg", handleBasicImageRequest); - httpServer.registerPathHandler("/lazuli.exe", handleBasicImageRequest); - httpServer.registerPathHandler("/redir", handleRedirect); - httpServer.registerPathHandler("/bellr", handleRequest); - httpServer.registerPathHandler("/bellg", handleRequest); - httpServer.registerPathHandler("/bellb", handleRequest); - - await BrowserTestUtils.openNewForegroundTab( - gBrowser, - "http://localhost:8000/save_filenames.html" - ); - - expectedItems = await getItems("items"); -}); - -function getItems(parentid) { - return SpecialPowers.spawn( - gBrowser.selectedBrowser, - [parentid, AppConstants.platform], - (id, platform) => { - let elements = []; - let elem = content.document.getElementById(id).firstElementChild; - while (elem) { - let filename = - elem.dataset["filenamePlatform" + platform] || elem.dataset.filename; - let url = elem.getAttribute("src"); - let draggable = - elem.localName == "img" && elem.dataset.nodrag != "true"; - let unknown = elem.dataset.unknown; - let noattach = elem.dataset.noattach; - elements.push({ draggable, unknown, filename, url, noattach }); - elem = elem.nextElementSibling; - } - return elements; - } - ); -} - -function getDirectoryEntries(dir) { - let files = []; - let entries = dir.directoryEntries; - while (true) { - let file = entries.nextFile; - if (!file) { - break; - } - files.push(file.leafName); - } - entries.close(); - return files; -} - -// This test saves the document as a complete web page and verifies -// that the resources are saved with the correct filename. -add_task(async function save_document() { - let browser = gBrowser.selectedBrowser; - - let tmp = SpecialPowers.Services.dirsvc.get("TmpD", Ci.nsIFile); - const baseFilename = "test_save_filenames_" + Date.now(); - - let tmpFile = tmp.clone(); - tmpFile.append(baseFilename + "_document.html"); - let tmpDir = tmp.clone(); - tmpDir.append(baseFilename + "_document_files"); - - MockFilePicker.displayDirectory = tmpDir; - MockFilePicker.showCallback = function(fp) { - MockFilePicker.setFiles([tmpFile]); - MockFilePicker.filterIndex = 0; // kSaveAsType_Complete - }; - - let downloadsList = await Downloads.getList(Downloads.PUBLIC); - let savePromise = new Promise((resolve, reject) => { - downloadsList.addView({ - onDownloadChanged(download) { - if (download.succeeded) { - downloadsList.removeView(this); - downloadsList.removeFinished(); - resolve(); - } - }, - }); - }); - saveBrowser(browser); - await savePromise; - - let filesSaved = getDirectoryEntries(tmpDir); - - for (let idx = 0; idx < expectedItems.length; idx++) { - let filename = expectedItems[idx].filename; - if (idx == 66 && AppConstants.platform == "win") { - // This is special-cased on Windows. The default filename will be used, since - // the filename is invalid, but since the previous test file has the same issue, - // this second file will be saved with a number suffix added to it. --> - filename = "index_002"; - } - - let file = tmpDir.clone(); - file.append(filename); - - let fileIdx = -1; - // Use checkShortenedFilename to check long filenames. - if (filename.length > 240) { - for (let t = 0; t < filesSaved.length; t++) { - if ( - filesSaved[t].length > 60 && - checkShortenedFilename(filesSaved[t], filename) - ) { - fileIdx = t; - break; - } - } - } else { - fileIdx = filesSaved.indexOf(filename); - } - - ok( - fileIdx >= 0, - "file i" + - idx + - " " + - filename + - " was saved with the correct name using saveDocument" - ); - if (fileIdx >= 0) { - // If found, remove it from the list. At end of the test, the - // list should be empty. - filesSaved.splice(fileIdx, 1); - } - } - - is(filesSaved.length, 0, "all files accounted for"); - tmpDir.remove(true); - tmpFile.remove(false); -}); - -// This test simulates dragging the images in the document and ensuring that -// the correct filename is used for each one. -// On Mac, the data is added in the parent process instead, so we cannot -// test dragging directly. -if (AppConstants.platform != "macosx") { - add_task(async function drag_files() { - let browser = gBrowser.selectedBrowser; - - await SpecialPowers.spawn(browser, [PROMISE_FILENAME_TYPE], type => { - content.addEventListener("dragstart", event => { - content.draggedFile = event.dataTransfer.getData(type); - event.preventDefault(); - }); - }); - - for (let idx = 0; idx < expectedItems.length; idx++) { - if (!expectedItems[idx].draggable) { - // You can't drag non-images and invalid images. - continue; - } - - await BrowserTestUtils.synthesizeMouse( - "#i" + idx, - 1, - 1, - { type: "mousedown" }, - browser - ); - await BrowserTestUtils.synthesizeMouse( - "#i" + idx, - 11, - 11, - { type: "mousemove" }, - browser - ); - await BrowserTestUtils.synthesizeMouse( - "#i" + idx, - 20, - 20, - { type: "mousemove" }, - browser - ); - await BrowserTestUtils.synthesizeMouse( - "#i" + idx, - 20, - 20, - { type: "mouseup" }, - browser - ); - - let draggedFile = await SpecialPowers.spawn(browser, [], () => { - let file = content.draggedFile; - content.draggedFile = null; - return file; - }); - - is( - draggedFile, - expectedItems[idx].filename, - "i" + - idx + - " " + - expectedItems[idx].filename + - " was saved with the correct name when dragging" - ); - } - }); -} - -// This test checks that copying an image provides the right filename -// for pasting to the local file system. This is only implemented on Windows. -if (AppConstants.platform == "win") { - add_task(async function copy_image() { - for (let idx = 0; idx < expectedItems.length; idx++) { - if (!expectedItems[idx].draggable) { - // You can't context-click on non-images. - continue; - } - - let data = await SpecialPowers.spawn( - gBrowser.selectedBrowser, - [idx, PROMISE_FILENAME_TYPE], - (imagenum, type) => { - // No need to wait for the data to be really on the clipboard, we only - // need the promise data added when the command is performed. - SpecialPowers.setCommandNode( - content, - content.document.getElementById("i" + imagenum) - ); - SpecialPowers.doCommand(content, "cmd_copyImageContents"); - - return SpecialPowers.getClipboardData(type); - } - ); - - is( - data, - expectedItems[idx].filename, - "i" + - idx + - " " + - expectedItems[idx].filename + - " was saved with the correct name when copying" - ); - } - }); -} - -// This test checks the default filename selected when selecting to save -// a file from either the context menu or what would happen when save page -// as was selected from the file menu. Note that this tests a filename assigned -// when using content-disposition: inline. -add_task(async function saveas_files() { - // Iterate over each item and try saving them from the context menu, - // and the Save Page As command on the File menu. - for (let testname of ["context menu", "save page as"]) { - for (let idx = 0; idx < expectedItems.length; idx++) { - let menu; - if (testname == "context menu") { - if (!expectedItems[idx].draggable) { - // You can't context-click on non-images. - continue; - } - - menu = document.getElementById("contentAreaContextMenu"); - let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown"); - BrowserTestUtils.synthesizeMouse( - "#i" + idx, - 5, - 5, - { type: "contextmenu", button: 2 }, - gBrowser.selectedBrowser - ); - await popupShown; - } else { - if (expectedItems[idx].unknown == "typeonly") { - // Items marked with unknown="typeonly" have unknown content types and - // will be downloaded instead of opened in a tab. - let list = await Downloads.getList(Downloads.PUBLIC); - let downloadFinishedPromise = promiseDownloadFinished(list); - - await BrowserTestUtils.openNewForegroundTab( - gBrowser, - expectedItems[idx].url - ); - - let download = await downloadFinishedPromise; - - let filename = PathUtils.filename(download.target.path); - is( - filename, - expectedItems[idx].filename, - "open link" + - idx + - " " + - expectedItems[idx].filename + - " was downloaded with the correct name when opened as a url" - ); - - try { - await IOUtils.remove(download.target.path); - } catch (ex) {} - - await BrowserTestUtils.removeTab(gBrowser.selectedTab); - continue; - } - - await BrowserTestUtils.openNewForegroundTab( - gBrowser, - expectedItems[idx].url - ); - } - - let filename = await new Promise(resolve => { - MockFilePicker.showCallback = function(fp) { - setTimeout(() => { - resolve(fp.defaultString); - }, 0); - return Ci.nsIFilePicker.returnCancel; - }; - - if (testname == "context menu") { - let menuitem = document.getElementById("context-saveimage"); - menu.activateItem(menuitem); - } else if (testname == "save page as") { - document.getElementById("Browser:SavePage").doCommand(); - } - }); - - // Trying to open an unknown or binary type will just open a blank - // page, so trying to save will just save the blank page with the - // filename index.html. - let expectedFilename = expectedItems[idx].unknown - ? DEFAULT_INDEX_FILENAME - : expectedItems[idx].filename; - - // When saving via contentAreaUtils.js, the content disposition name - // field is used as an alternate. - if (expectedFilename == "save_thename.png") { - expectedFilename = "withname.png"; - } - - is( - filename, - expectedFilename, - "i" + - idx + - " " + - expectedFilename + - " was saved with the correct name " + - testname - ); - - if (testname == "save page as") { - await BrowserTestUtils.removeTab(gBrowser.selectedTab); - } - } - } -}); - -// This test checks that links that result in files with -// content-disposition: attachment are saved with the right filenames. -add_task(async function save_links() { - sendAsAttachment = true; - - // Create some links based on each image and insert them into the document. - await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { - let doc = content.document; - let insertPos = doc.getElementById("attachment-links"); - - let idx = 0; - let elem = doc.getElementById("items").firstElementChild; - while (elem) { - let attachmentlink = doc.createElement("a"); - attachmentlink.id = "attachmentlink" + idx; - attachmentlink.href = elem.localName == "object" ? elem.data : elem.src; - attachmentlink.textContent = elem.dataset.filename; - insertPos.appendChild(attachmentlink); - insertPos.appendChild(doc.createTextNode(" ")); - - elem = elem.nextElementSibling; - idx++; - } - }); - - let list = await Downloads.getList(Downloads.PUBLIC); - - for (let idx = 0; idx < expectedItems.length; idx++) { - // Skip the items that won't have a content-disposition. - if (expectedItems[idx].noattach) { - continue; - } - - let downloadFinishedPromise = promiseDownloadFinished(list); - - BrowserTestUtils.synthesizeMouse( - "#attachmentlink" + idx, - 5, - 5, - {}, - gBrowser.selectedBrowser - ); - - let download = await downloadFinishedPromise; - - let filename = PathUtils.filename(download.target.path); - - let expectedFilename = expectedItems[idx].filename; - if (AppConstants.platform == "win" && idx == 54) { - // On Windows, .txt is added when saving as an attachment - // to avoid this looking like an executable. This - // is done in validateLeafName in HelperAppDlg.jsm. - // XXXndeakin should we do this for all save mechanisms? - expectedFilename += ".txt"; - } - - // Use checkShortenedFilename to check long filenames. - if (expectedItems[idx].filename.length > 240) { - ok( - checkShortenedFilename(filename, expectedFilename), - "attachmentlink" + - idx + - " " + - expectedFilename + - " was saved with the correct name when opened as attachment (with long name)" - ); - } else { - is( - filename, - expectedFilename, - "attachmentlink" + - idx + - " " + - expectedFilename + - " was saved with the correct name when opened as attachment" - ); - } - - try { - await IOUtils.remove(download.target.path); - } catch (ex) {} - } - - sendAsAttachment = false; -}); - -// This test checks some cases where links to images are saved using Save Link As, -// and when opening them in a new tab and then using Save Page As. -add_task(async function saveas_image_links() { - let links = await getItems("links"); - - // Iterate over each link and try saving the links from the context menu, - // and then after opening a new tab for that link and then selecting - // the Save Page As command on the File menu. - for (let testname of ["save link as", "save link then save page as"]) { - for (let idx = 0; idx < links.length; idx++) { - let menu = document.getElementById("contentAreaContextMenu"); - let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown"); - BrowserTestUtils.synthesizeMouse( - "#link" + idx, - 5, - 5, - { type: "contextmenu", button: 2 }, - gBrowser.selectedBrowser - ); - await popupShown; - - let promptPromise = new Promise(resolve => { - MockFilePicker.showCallback = function(fp) { - setTimeout(() => { - resolve(fp.defaultString); - }, 0); - return Ci.nsIFilePicker.returnCancel; - }; - }); - - if (testname == "save link as") { - let menuitem = document.getElementById("context-savelink"); - menu.activateItem(menuitem); - } else { - let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); - - let menuitem = document.getElementById("context-openlinkintab"); - menu.activateItem(menuitem); - - let tab = await newTabPromise; - await BrowserTestUtils.switchTab(gBrowser, tab); - - document.getElementById("Browser:SavePage").doCommand(); - } - - let filename = await promptPromise; - - let expectedFilename = links[idx].filename; - // Only codepaths that go through contentAreaUtils.js use the - // name from the content disposition. - if (testname == "save link as" && expectedFilename == "four.png") { - expectedFilename = "save_filename.png"; - } - - is( - filename, - expectedFilename, - "i" + - idx + - " " + - expectedFilename + - " link was saved with the correct name " + - testname - ); - - if (testname == "save link then save page as") { - await BrowserTestUtils.removeTab(gBrowser.selectedTab); - } - } - } -}); - -// This test checks that links that with a download attribute -// are saved with the right filenames. -add_task(async function save_download_links() { - let downloads = await getItems("downloads"); - - let list = await Downloads.getList(Downloads.PUBLIC); - for (let idx = 0; idx < downloads.length; idx++) { - let downloadFinishedPromise = promiseDownloadFinished(list); - - BrowserTestUtils.synthesizeMouse( - "#download" + idx, - 2, - 2, - {}, - gBrowser.selectedBrowser - ); - - let download = await downloadFinishedPromise; - - let filename = PathUtils.filename(download.target.path); - - if (downloads[idx].filename.length > 240) { - ok( - checkShortenedFilename(filename, downloads[idx].filename), - "download" + - idx + - " " + - downloads[idx].filename + - " was saved with the correct name when link has download attribute" - ); - } else { - is( - filename, - downloads[idx].filename, - "download" + - idx + - " " + - downloads[idx].filename + - " was saved with the correct name when link has download attribute" - ); - } - - try { - await IOUtils.remove(download.target.path); - } catch (ex) {} - } -}); - -add_task(async () => { - BrowserTestUtils.removeTab(gBrowser.selectedTab); - MockFilePicker.cleanup(); - await new Promise(resolve => httpServer.stop(resolve)); -});
deleted file mode 100644 --- a/uriloader/exthandler/tests/mochitest/save_filenames.html +++ /dev/null @@ -1,296 +0,0 @@ -<html> -<head> -<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> -</head> -<body> -<style> - img { padding: 10px; border: 1px solid red; } - a { padding-left: 10px; } -</style> - -<span id="items"> - -<!-- simple filename --> -<img id="i0" src="http://localhost:8000/basic.png" - data-noattach="true" data-filename="basic.png"> - -<!-- simple filename with content disposition --> -<img id="i1" src="http://localhost:8000/save_filename.sjs?type=png&filename=simple.png" data-filename="simple.png"> - -<!-- invalid characters in the filename --> -<img id="i2" src="http://localhost:8000/save_filename.sjs?type=png&filename=invalidfilename/a:b*c%63d.png" data-filename="invalidfilename_a b ccd.png"> - -<!-- invalid extension for a png image --> -<img id="i3" src="http://localhost:8000/save_filename.sjs?type=png&filename=invalidextension.pang" data-filename="invalidextension.png"> - -<!-- jpeg extension for a png image --> -<img id="i4" src="http://localhost:8000/save_filename.sjs?type=png&filename=reallyapng.jpeg" data-filename="reallyapng.png"> - -<!-- txt extension for a png image --> -<img id="i5" src="http://localhost:8000/save_filename.sjs?type=png&filename=nottext.txt" data-filename="nottext.png"> - -<!-- no extension for a png image --> -<img id="i6" src="http://localhost:8000/save_filename.sjs?type=png&filename=noext" data-filename="noext.png"> - -<!-- empty extension for a png image --> -<img id="i7" src="http://localhost:8000/save_filename.sjs?type=png&filename=noextdot." data-filename="noextdot.png"> - -<!-- long filename --> -<img id="i8" src="http://localhost:8000/save_filename.sjs?type=png&filename=averylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.png" - data-filename="averylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfi.png"> - -<!-- long filename with invalid extension --> -<img id="i9" src="http://localhost:8000/save_filename.sjs?type=png&filename=bverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.exe" - data-filename="bverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfi.png"> - -<!-- long filename with invalid extension --> -<img id="i10" src="http://localhost:8000/save_filename.sjs?type=png&filename=cverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.exe.jpg" - data-filename="cverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfi.png"> - -<!-- jpeg with jpg extension --> -<img id="i11" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=thejpg.jpg" data-filename="thejpg.jpg"> - -<!-- jpeg with jpeg extension --> -<img id="i12" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=thejpg.jpeg" data-filename="thejpg.jpeg"> - -<!-- jpeg with invalid extension --> -<img id="i13" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=morejpg.exe" data-filename="morejpg.jpg" data-filename-platformlinux="morejpg.jpeg"> - -<!-- jpeg with multiple extensions --> -<img id="i14" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=anotherjpg.jpg.exe" data-filename="anotherjpg.jpg.jpg" data-filename-platformlinux="anotherjpg.jpg.jpeg"> - -<!-- jpeg with no filename portion --> -<img id="i15" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=.jpg" data-filename="index.jpg"> - -<!-- png with no filename portion and invalid extension --> -<img id="i16" src="http://localhost:8000/save_filename.sjs?type=png&filename=.exe" data-filename="index.png"> - -<!-- png with escaped characters --> -<img id="i17" src="http://localhost:8000/save_filename.sjs?type=png&filename=first%20file.png" data-filename="first file.png"> - -<!-- png with more escaped characters --> -<img id="i18" src="http://localhost:8000/save_filename.sjs?type=png&filename=second%32file%2Eexe" data-filename="second2file.png"> - -<!-- unknown type with png extension --> -<img id="i19" src="http://localhost:8000/save_filename.sjs?type=gook&filename=gook1.png" - data-nodrag="true" data-unknown="typeonly" data-filename="gook1.png"> - -<!-- unknown type with exe extension --> -<img id="i20" src="http://localhost:8000/save_filename.sjs?type=gook&filename=gook2.exe" - data-nodrag="true" data-unknown="typeonly" data-filename="gook2.exe"> - -<!-- unknown type with no extension --> -<img id="i21" src="http://localhost:8000/save_filename.sjs?type=gook&filename=gook3" - data-nodrag="true" data-unknown="typeonly" data-filename="gook3"> - -<!-- simple script --> -<script id="i22" src="http://localhost:8000/save_filename.sjs?type=js&filename=script1.js" data-filename="script1.js"></script> - -<!-- script with invalid extension. Windows doesn't have an association for application/x-javascript - so doesn't handle it. --> -<script id="i23" src="http://localhost:8000/save_filename.sjs?type=js&filename=script2.exe" - data-filename="script2.exe.js"></script> - -<!-- script with escaped characters --> -<script id="i24" src="http://localhost:8000/save_filename.sjs?type=js&filename=script%20%33.exe" - data-filename="script 3.exe.js"></script> - -<!-- script with long filename --> -<script id="i25" src="http://localhost:8000/save_filename.sjs?type=js&filename=script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789.js" - data-filename="script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456.js"></script> - -<!-- binary with exe extension --> -<object id="i26" data="http://localhost:8000/save_filename.sjs?type=binary&filename=download1.exe" - data-unknown="true" data-filename="download1.exe"></object> - -<!-- binary with invalid extension --> -<object id="i27" data="http://localhost:8000/save_filename.sjs?type=binary&filename=download2.png" - data-unknown="true" data-filename="download2.png"></object> - -<!-- binary with no extension --> -<object id="i28" data="http://localhost:8000/save_filename.sjs?type=binary&filename=downloadnoext" - data-unknown="true" data-filename="downloadnoext"></object> - -<!-- binary with no other invalid characters --> -<object id="i29" data="http://localhost:8000/save_filename.sjs?type=binary&filename=binary^%31%20exe.exe" - data-unknown="true" data-filename="binary^1 exe.exe"></object> - -<!-- unknown image type with no extension, but ending in png --> -<img id="i30" src="http://localhost:8000/save_filename.sjs?type=otherimage&filename=specialpng" - data-unknown="typeonly" data-nodrag="true" data-filename="specialpng"> - -<!-- unknown image type with no extension, but ending in many dots --> -<img id="i31" src="http://localhost:8000/save_filename.sjs?type=otherimage&filename=extrapng..." - data-unknown="typeonly" data-nodrag="true" data-filename="extrapng"> - -<!-- image type with no content-disposition filename specified --> -<img id="i32" src="http://localhost:8000/save_filename.sjs?type=png" data-filename="save_filename.png"> - -<!-- binary with no content-disposition filename specified --> -<object id="i33" data="http://localhost:8000/save_filename.sjs?type=binary" - data-unknown="true" data-filename="save_filename.sjs"></object> - -<!-- image where url has png extension --> -<img id="i34" src="http://localhost:8000/getdata.png?type=png&filename=override.png" data-filename="override.png"> - -<!-- image where url has png extension but content disposition has incorrect extension --> -<img id="i35" src="http://localhost:8000/getdata.png?type=png&filename=flower.jpeg" data-filename="flower.png"> - -<!-- image where url has png extension but content disposition does not --> -<img id="i36" src="http://localhost:8000/getdata.png?type=png&filename=ruby" data-filename="ruby.png"> - -<!-- image where url has png extension but content disposition has invalid characters --> -<img id="i37" src="http://localhost:8000/getdata.png?type=png&filename=sapphire/data" data-filename="sapphire_data.png"> - -<!-- image where neither content disposition or url have an extension --> -<img id="i38" src="http://localhost:8000/base?type=png&filename=emerald" data-filename="emerald.png"> - -<!-- image where filename is not specified --> -<img id="i39" src="http://localhost:8000/base?type=png" data-filename="base.png"> - -<!-- simple script where url filename has no extension --> -<script id="i40" src="http://localhost:8000/base?type=js&filename=script4.js" data-filename="script4.js"></script> - -<!-- script where url filename has no extension and invalid extension in content disposition filename --> -<script id="i41" src="http://localhost:8000/base?type=js&filename=script5.exe" - data-filename="script5.exe.js"></script> - -<!-- script where url filename has no extension and escaped characters in content disposition filename--> -<script id="i42" src="http://localhost:8000/base?type=js&filename=script%20%36.exe" - data-filename="script 6.exe.js"></script> - -<!-- text where filename is present --> -<img id="i43" src="http://localhost:8000/getdata.png?type=text&filename=readme.txt" - data-nodrag="true" data-filename="readme.txt"> - -<!-- text where filename is present with a different extension --> -<img id="i44" src="http://localhost:8000/getdata.png?type=text&filename=main.cpp" - data-nodrag="true" data-filename="main.cpp"> - -<!-- text where extension is not present --> -<img id="i45" src="http://localhost:8000/getdata.png?type=text&filename=readme" - data-nodrag="true" data-filename="readme"> - -<!-- text where extension is not present and url does not have extension --> -<img id="i46" src="http://localhost:8000/base?type=text&filename=info" - data-nodrag="true" data-filename="info"> - -<!-- text where filename is not present --> -<img id="i47" src="http://localhost:8000/basetext?type=text" - data-nodrag="true" data-filename="basetext"> - -<!-- text where url has extension --> -<img id="i48" src="http://localhost:8000/text2.txt?type=text" - data-nodrag="true" data-filename="text2.txt"> - -<!-- text where url has extension --> -<img id="i49" src="http://localhost:8000/text3.gonk?type=text" - data-nodrag="true" data-filename="text3.gonk"> - -<!-- text with long filename --> -<img id="i50" src="http://localhost:8000/text3.gonk?type=text&filename=text0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789text0123456789zztext0123456789zztext0123456789zztext01234567.exe.txt" data-nodrag="true" data-filename="text0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456.txt"> - -<!-- webp image --> -<img id="i51" src="http://localhost:8000/save_filename.sjs?type=webp&filename=webpimage.webp" - data-filename="webpimage.webp"> - -<!-- webp image with jpg extension --> -<img id="i52" src="http://localhost:8000/save_filename.sjs?type=webp&filename=realwebpimage.jpg" - data-filename="realwebpimage.webp"> - -<!-- no content type specified --> -<img id="i53" src="http://localhost:8000/save_filename.sjs?&filename=notype.png" - data-nodrag="true" data-filename="notype.png"> - -<!-- no content type specified. Note that on Windows, .txt is - appended when saving as an attachment. This is special-cased - in browser_save_filenames.js. --> -<img id="i54" src="http://localhost:8000/save_filename.sjs?&filename=notypebin.exe" - data-nodrag="true" data-filename="notypebin.exe"> - -<!-- extension contains invalid characters --> -<img id="i55" src="http://localhost:8000/save_filename.sjs?type=png&filename=extinvalid.a?*" - data-filename="extinvalid.png"> - -<!-- filename with redirect and content disposition --> -<img id="i56" src="http://localhost:8000/redir?type=png&filename=red.png" data-filename="red.png"> - -<!-- filename with redirect and different type --> -<img id="i57" src="http://localhost:8000/redir?type=jpeg&filename=green.png" - data-filename="green.jpg" data-filename-platformlinux="green.jpeg"> - -<!-- filename with redirect and binary type --> -<object id="i58" data="http://localhost:8000/redir?type=binary&filename=blue.png" - data-unknown="true" data-filename="blue.png"></object> - -<!-- filename in url with incorrect extension --> -<img id="i59" src="http://localhost:8000/aquamarine.jpeg" - data-noattach="true" data-filename="aquamarine.png"> - -<!-- filename in url with exe extension, but returns a png image --> -<img id="i60" src="http://localhost:8000/lazuli.exe" - data-noattach="true" data-filename="lazuli.png"> - -<!-- filename with leading, trailing and duplicate spaces --> -<img id="i61" src="http://localhost:8000/save_filename.sjs?type=png&filename= with spaces.png " - data-filename="with spaces.png"> - -<!-- filename with leading and trailing periods --> -<img id="i62" src="http://localhost:8000/save_filename.sjs?type=png&filename=..with..dots..png.." - data-filename="with..dots..png"> - -<!-- filename with non-ascii character --> -<img id="i63" src="http://localhost:8000/base?type=png&filename=s%C3%B6meescapes.%C3%B7ng" data-filename="sömeescapes.png"> - -<!-- filename with content disposition name assigned. The name is only used - when selecting to manually save, otherwise it is ignored. --> -<img id="i64" src="http://localhost:8000/save_thename.sjs?type=png&dispname=withname" - data-filename="save_thename.png"> - -<!-- reserved filename on Windows --> -<img id="i65" src="http://localhost:8000/save_filename.sjs?type=text&filename=com1" - data-nodrag="true" data-filename="com1" data-filename-platformwin="index"> - -<!-- reserved filename with extension on Windows --> -<img id="i66" src="http://localhost:8000/save_filename.sjs?type=text&filename=com2.any" - data-nodrag="true" data-filename="com2.any" data-filename-platformwin="index"> - -</span> - -<!-- This set is used to test the filename specified by the download attribute is validated correctly. --> -<span id="downloads"> - <a id="download0" href="http://localhost:8000/base" download="pearl.png" data-filename="pearl.png">Link</a> - <a id="download1" href="http://localhost:8000/save_filename.sjs?type=png" download="opal.jpeg" data-filename="opal.png">Link</a> - <a id="download2" href="http://localhost:8000/save_filename.sjs?type=jpeg" - download="amethyst.png" data-filename="amethyst.jpg" - data-filename-platformlinux="amethyst.jpeg">Link</a> - <a id="download3" href="http://localhost:8000/save_filename.sjs?type=text" - download="onyx.png" data-filename="onyx.png">Link</a> - <!-- The content-disposition overrides the download attribute. --> - <a id="download4" href="http://localhost:8000/save_filename.sjs?type=png&filename=fakename.jpeg" download="topaz.jpeg" data-filename="fakename.png">Link</a> - <a id="download5" href="http://localhost:8000/save_filename.sjs?type=png" - download="amber?.png" data-filename="amber .png">Link</a> - <a id="download6" href="http://localhost:8000/save_filename.sjs?type=jpeg" - download="jade.:*jpeg" data-filename="jade.jpg" - data-filename-platformlinux="jade.jpeg">Link</a>> - <a id="download7" href="http://localhost:8000/save_filename.sjs?type=png" - download="thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename.png" - data-filename="thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisavery.png">Link</a> - <a id="download8" href="http://localhost:8000/base" - download="	
 ᠎᠎ spa ced.png 	
 ᠎᠎ " - data-filename="spa ced.png">Link</a> -</span> - -<span id="links"> - <a id="link0" href="http://localhost:8000/save_filename.sjs?type=png&filename=one.png" data-filename="one.png">One</a> - <a id="link1" href="http://localhost:8000/save_filename.sjs?type=png&filename=two.jpeg" data-filename="two.png">Two</a> - <a id="link2" href="http://localhost:8000/save_filename.sjs?type=png&filename=three.con" data-filename="three.png">Three</a> - <a id="link3" href="http://localhost:8000/save_filename.sjs?type=png&dispname=four" data-filename="four.png">Four</a> -</span> - -<!-- The content-disposition attachment generates links from the images/objects/scripts above - and inserts them here. --> -<p id="attachment-links"> -</p> - -</body></html>
deleted file mode 100644 --- a/uriloader/exthandler/tests/unit/test_filename_sanitize.js +++ /dev/null @@ -1,135 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. -http://creativecommons.org/publicdomain/zero/1.0/ */ - -// This test verifies that -// nsIMIMEService.validateFileNameForSaving sanitizes filenames -// properly with different flags. - -"use strict"; - -add_task(async function validate_filename_method() { - let mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); - - function checkFilename(filename, flags) { - return mimeService.validateFileNameForSaving(filename, "image/png", flags); - } - - Assert.equal(checkFilename("basicfile.png", 0), "basicfile.png"); - Assert.equal(checkFilename(" whitespace.png ", 0), "whitespace.png"); - Assert.equal( - checkFilename(" .whitespaceanddots.png...", 0), - "whitespaceanddots.png" - ); - Assert.equal( - checkFilename(" \u00a0 \u00a0 extrawhitespace.png \u00a0 \u00a0 ", 0), - "extrawhitespace.png" - ); - Assert.equal( - checkFilename(" filename with whitespace.png ", 0), - "filename with whitespace.png" - ); - Assert.equal(checkFilename("\\path.png", 0), "_path.png"); - Assert.equal( - checkFilename("\\path*and/$?~file.png", 0), - "_path and_$ ~file.png" - ); - Assert.equal( - checkFilename(" \u180e whit\u180ee.png \u180e", 0), - "whit\u180ee.png" - ); - Assert.equal(checkFilename("簡単簡単簡単", 0), "簡単簡単簡単.png"); - Assert.equal(checkFilename(" happy\u061c\u2069.png", 0), "happy__.png"); - Assert.equal( - checkFilename("12345678".repeat(31) + "abcdefgh.png", 0), - "12345678".repeat(31) + "abc.png" - ); - Assert.equal( - checkFilename("簡単".repeat(41) + ".png", 0), - "簡単".repeat(41) + ".png" - ); - Assert.equal( - checkFilename("簡単".repeat(42) + ".png", 0), - "簡単".repeat(41) + "簡.png" - ); - Assert.equal( - checkFilename("簡単".repeat(56) + ".png", 0), - "簡単".repeat(41) + "簡.png" - ); - Assert.equal(checkFilename("café.png", 0), "café.png"); - Assert.equal( - checkFilename("café".repeat(50) + ".png", 0), - "café".repeat(50) + ".png" - ); - Assert.equal( - checkFilename("café".repeat(51) + ".png", 0), - "café".repeat(50) + "c.png" - ); - - Assert.equal( - checkFilename("\u{100001}\u{100002}.png", 0), - "\u{100001}\u{100002}.png" - ); - Assert.equal( - checkFilename("\u{100001}\u{100002}".repeat(31) + ".png", 0), - "\u{100001}\u{100002}".repeat(31) + ".png" - ); - Assert.equal( - checkFilename("\u{100001}\u{100002}".repeat(32) + ".png", 0), - "\u{100001}\u{100002}".repeat(31) + ".png" - ); - - Assert.equal( - checkFilename("noextensionfile".repeat(16), 0), - "noextensionfile".repeat(16) + ".png" - ); - Assert.equal( - checkFilename("noextensionfile".repeat(17), 0), - "noextensionfile".repeat(16) + "noextension.png" - ); - Assert.equal( - checkFilename("noextensionfile".repeat(16) + "noextensionfil.", 0), - "noextensionfile".repeat(16) + "noextension.png" - ); - - Assert.equal(checkFilename(" first .png ", 0), "first .png"); - Assert.equal( - checkFilename( - " second .png ", - mimeService.VALIDATE_DONT_COLLAPSE_WHITESPACE - ), - "second .png" - ); - - // For whatever reason, the Android mime handler accepts the .jpeg - // extension for image/png, so skip this test there. - if (AppConstants.platform != "android") { - Assert.equal(checkFilename("thi/*rd.jpeg", 0), "thi_ rd.png"); - } - - Assert.equal( - checkFilename("f*\\ourth file.jpg", mimeService.VALIDATE_SANITIZE_ONLY), - "f _ourth file.jpg" - ); - Assert.equal( - checkFilename( - "f*\\ift h.jpe*\\g", - mimeService.VALIDATE_SANITIZE_ONLY | - mimeService.VALIDATE_DONT_COLLAPSE_WHITESPACE - ), - "f _ift h.jpe _g" - ); - Assert.equal(checkFilename("sixth.j pe/*g", 0), "sixth.png"); - - let repeatStr = "12345678".repeat(31); - Assert.equal( - checkFilename( - repeatStr + "seventh.png", - mimeService.VALIDATE_DONT_TRUNCATE - ), - repeatStr + "seventh.png" - ); - Assert.equal( - checkFilename(repeatStr + "seventh.png", 0), - repeatStr + "sev.png" - ); -});
--- a/uriloader/exthandler/tests/unit/xpcshell.ini +++ b/uriloader/exthandler/tests/unit/xpcshell.ini @@ -10,17 +10,16 @@ skip-if = os == "android" appname == "thunderbird" [test_downloads_improvements_migration.js] # No default stored handlers on android given lack of support. # No default stored handlers on Thunderbird. skip-if = os == "android" appname == "thunderbird" -[test_filename_sanitize.js] [test_getFromTypeAndExtension.js] [test_getMIMEInfo_pdf.js] [test_getMIMEInfo_unknown_mime_type.js] run-if = os == "win" # Windows only test [test_getTypeFromExtension_ext_to_type_mapping.js] [test_getTypeFromExtension_with_empty_Content_Type.js] run-if = os == "win" # Windows only test [test_badMIMEType.js]
--- a/widget/windows/nsDataObj.cpp +++ b/widget/windows/nsDataObj.cpp @@ -39,17 +39,16 @@ #include "mozilla/Preferences.h" #include "nsContentUtils.h" #include "nsIPrincipal.h" #include "nsNativeCharsetUtils.h" #include "nsMimeTypes.h" #include "imgIEncoder.h" #include "imgITools.h" #include "WinUtils.h" -#include "nsLocalFile.h" #include "mozilla/LazyIdleThread.h" #include <algorithm> using namespace mozilla; using namespace mozilla::glue; using namespace mozilla::widget; @@ -1084,28 +1083,58 @@ nsDataObj ::GetFileContents(FORMATETC& a else NS_WARNING("Not yet implemented\n"); return res; } // GetFileContents // +// Given a unicode string, we ensure that it contains only characters which are +// valid within the file system. Remove all forbidden characters from the name, +// and completely disallow any title that starts with a forbidden name and +// extension (e.g. "nul" is invalid, but "nul." and "nul.txt" are also invalid +// and will cause problems). +// +// It would seem that this is more functionality suited to being in nsIFile. +// +static void MangleTextToValidFilename(nsString& aText) { + static const char* forbiddenNames[] = { + "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", + "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", + "LPT8", "LPT9", "CON", "PRN", "AUX", "NUL", "CLOCK$"}; + + aText.StripChars(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS); + aText.CompressWhitespace(true, true); + uint32_t nameLen; + for (size_t n = 0; n < ArrayLength(forbiddenNames); ++n) { + nameLen = (uint32_t)strlen(forbiddenNames[n]); + if (aText.EqualsIgnoreCase(forbiddenNames[n], nameLen)) { + // invalid name is either the entire string, or a prefix with a period + if (aText.Length() == nameLen || aText.CharAt(nameLen) == char16_t('.')) { + aText.Truncate(); + break; + } + } + } +} + +// // Given a unicode string, convert it down to a valid local charset filename // with the supplied extension. This ensures that we do not cut MBCS characters // in the middle. // // It would seem that this is more functionality suited to being in nsIFile. // static bool CreateFilenameFromTextA(nsString& aText, const char* aExtension, char* aFilename, uint32_t aFilenameLen) { // ensure that the supplied name doesn't have invalid characters. If // a valid mangled filename couldn't be created then it will leave the // text empty. - nsLocalFile::CheckForReservedFileName(aText); + MangleTextToValidFilename(aText); if (aText.IsEmpty()) return false; // repeatably call WideCharToMultiByte as long as the title doesn't fit in the // buffer available to us. Continually reduce the length of the source title // until the MBCS version will fit in the buffer with room for the supplied // extension. Doing it this way ensures that even in MBCS environments there // will be a valid MBCS filename of the correct length. int maxUsableFilenameLen = @@ -1127,17 +1156,17 @@ static bool CreateFilenameFromTextA(nsSt } } static bool CreateFilenameFromTextW(nsString& aText, const wchar_t* aExtension, wchar_t* aFilename, uint32_t aFilenameLen) { // ensure that the supplied name doesn't have invalid characters. If // a valid mangled filename couldn't be created then it will leave the // text empty. - nsLocalFile::CheckForReservedFileName(aText); + MangleTextToValidFilename(aText); if (aText.IsEmpty()) return false; const int extensionLen = wcslen(aExtension); if (aText.Length() + extensionLen + 1 > aFilenameLen) aText.Truncate(aFilenameLen - extensionLen - 1); wcscpy(&aFilename[0], aText.get()); wcscpy(&aFilename[aText.Length()], aExtension); return true; @@ -2149,17 +2178,17 @@ HRESULT nsDataObj::GetDownloadDetails(ns nsAutoCString urlFileName; sourceURL->GetFileName(urlFileName); NS_UnescapeURL(urlFileName); CopyUTF8toUTF16(urlFileName, srcFileName); } if (srcFileName.IsEmpty()) return E_FAIL; // make the name safe for the filesystem - nsLocalFile::CheckForReservedFileName(srcFileName); + MangleTextToValidFilename(srcFileName); sourceURI.swap(*aSourceURI); aFilename = srcFileName; return S_OK; } HRESULT nsDataObj::GetFileDescriptor_IStreamA(FORMATETC& aFE, STGMEDIUM& aSTG) { HGLOBAL fileGroupDescHandle =
--- a/xpcom/base/nsCRTGlue.h +++ b/xpcom/base/nsCRTGlue.h @@ -124,26 +124,19 @@ void NS_MakeRandomString(char* aBuf, int #define FF '\f' #define TAB '\t' #define CRSTR "\015" #define LFSTR "\012" #define CRLF "\015\012" /* A CR LF equivalent string */ -#if defined(ANDROID) -// On mobile devices, the file system may be very limited in what it -// considers valid characters. To avoid errors, sanitize conservatively. -# define OS_FILE_ILLEGAL_CHARACTERS "/:*?\"<>|;,+=[]" -#else -// Otherwise, we use the most restrictive filesystem as our default set of -// illegal filename characters. This is currently Windows. -# define OS_FILE_ILLEGAL_CHARACTERS "/:*?\"<>|" -#endif - +// We use the most restrictive filesystem as our default set of illegal filename +// characters. This is currently Windows. +#define OS_FILE_ILLEGAL_CHARACTERS "/:*?\"<>|" // We also provide a list of all known file path separators for all filesystems. // This can be used in replacement of FILE_PATH_SEPARATOR when you need to // identify or replace all known path separators. #define KNOWN_PATH_SEPARATORS "\\/" #if defined(XP_MACOSX) # define FILE_PATH_SEPARATOR "/" #elif defined(XP_WIN) @@ -155,18 +148,13 @@ void NS_MakeRandomString(char* aBuf, int #endif // Not all these control characters are illegal in all OSs, but we don't really // want them appearing in filenames #define CONTROL_CHARACTERS \ "\001\002\003\004\005\006\007" \ "\010\011\012\013\014\015\016\017" \ "\020\021\022\023\024\025\026\027" \ - "\030\031\032\033\034\035\036\037" \ - "\177" \ - "\200\201\202\203\204\205\206\207" \ - "\210\211\212\213\214\215\216\217" \ - "\220\221\222\223\224\225\226\227" \ - "\230\231\232\233\234\235\236\237" + "\030\031\032\033\034\035\036\037" #define FILE_ILLEGAL_CHARACTERS CONTROL_CHARACTERS OS_FILE_ILLEGAL_CHARACTERS #endif // nsCRTGlue_h__
--- a/xpcom/io/nsLocalFileWin.cpp +++ b/xpcom/io/nsLocalFileWin.cpp @@ -186,36 +186,16 @@ nsresult nsLocalFile::RevealFile(const n CoTaskMemFree(dir); CoTaskMemFree(item); } return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; } -// static -void nsLocalFile::CheckForReservedFileName(nsString& aFileName) { - static const char* forbiddenNames[] = { - "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", - "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", - "LPT8", "LPT9", "CON", "PRN", "AUX", "NUL", "CLOCK$"}; - - uint32_t nameLen; - for (size_t n = 0; n < ArrayLength(forbiddenNames); ++n) { - nameLen = (uint32_t)strlen(forbiddenNames[n]); - if (aFileName.EqualsIgnoreCase(forbiddenNames[n], nameLen)) { - // invalid name is either the entire string, or a prefix with a period - if (aFileName.Length() == nameLen || - aFileName.CharAt(nameLen) == char16_t('.')) { - aFileName.Truncate(); - } - } - } -} - class nsDriveEnumerator : public nsSimpleEnumerator, public nsIDirectoryEnumerator { public: explicit nsDriveEnumerator(bool aUseDOSDevicePathSyntax); NS_DECL_ISUPPORTS_INHERITED NS_DECL_NSISIMPLEENUMERATOR NS_FORWARD_NSISIMPLEENUMERATORBASE(nsSimpleEnumerator::) nsresult Init();
--- a/xpcom/io/nsLocalFileWin.h +++ b/xpcom/io/nsLocalFileWin.h @@ -44,20 +44,16 @@ class nsLocalFile final : public nsILoca public: // Removes registry command handler parameters, quotes, and expands // environment strings. static bool CleanupCmdHandlerPath(nsAString& aCommandHandler); // Called off the main thread to open the window revealing the file static nsresult RevealFile(const nsString& aResolvedPath); - // Checks if the filename is one of the windows reserved filenames - // (com1, com2, etc...) and truncates the string if so. - static void CheckForReservedFileName(nsString& aFileName); - private: // CopyMove and CopySingleFile constants for |options| parameter: enum CopyFileOption { FollowSymlinks = 1u << 0, Move = 1u << 1, SkipNtfsAclReset = 1u << 2, Rename = 1u << 3 };