no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / uriloader / exthandler / nsExternalHelperAppService.cpp
blob4573e28470c5112f5ac2c5dd53e7a9d1ceedb943
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim:expandtab:shiftwidth=2:tabstop=2:cin:
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "base/basictypes.h"
9 /* This must occur *after* base/basictypes.h to avoid typedefs conflicts. */
10 #include "mozilla/ArrayUtils.h"
11 #include "mozilla/Base64.h"
12 #include "mozilla/ResultExtensions.h"
14 #include "mozilla/dom/ContentChild.h"
15 #include "mozilla/dom/BrowserChild.h"
16 #include "mozilla/dom/CanonicalBrowsingContext.h"
17 #include "mozilla/dom/Document.h"
18 #include "mozilla/dom/Element.h"
19 #include "mozilla/dom/WindowGlobalParent.h"
20 #include "mozilla/RandomNum.h"
21 #include "mozilla/StaticPrefs_dom.h"
22 #include "mozilla/StaticPrefs_security.h"
23 #include "mozilla/StaticPtr.h"
24 #include "nsXULAppAPI.h"
26 #include "ExternalHelperAppParent.h"
27 #include "nsExternalHelperAppService.h"
28 #include "nsCExternalHandlerService.h"
29 #include "nsIURI.h"
30 #include "nsIURL.h"
31 #include "nsIFile.h"
32 #include "nsIFileURL.h"
33 #include "nsIChannel.h"
34 #include "nsAppDirectoryServiceDefs.h"
35 #include "nsICategoryManager.h"
36 #include "nsDependentSubstring.h"
37 #include "nsSandboxFlags.h"
38 #include "nsString.h"
39 #include "nsUnicharUtils.h"
40 #include "nsIStringEnumerator.h"
41 #include "nsIStreamListener.h"
42 #include "nsIMIMEService.h"
43 #include "nsILoadGroup.h"
44 #include "nsIWebProgressListener.h"
45 #include "nsITransfer.h"
46 #include "nsReadableUtils.h"
47 #include "nsIRequest.h"
48 #include "nsDirectoryServiceDefs.h"
49 #include "nsIInterfaceRequestor.h"
50 #include "nsThreadUtils.h"
51 #include "nsIMutableArray.h"
52 #include "nsIRedirectHistoryEntry.h"
53 #include "nsOSHelperAppService.h"
54 #include "nsOSHelperAppServiceChild.h"
55 #include "nsContentSecurityUtils.h"
56 #include "nsUTF8Utils.h"
57 #include "nsUnicodeProperties.h"
59 // used to access our datastore of user-configured helper applications
60 #include "nsIHandlerService.h"
61 #include "nsIMIMEInfo.h"
62 #include "nsIHelperAppLauncherDialog.h"
63 #include "nsIContentDispatchChooser.h"
64 #include "nsNetUtil.h"
65 #include "nsIPrivateBrowsingChannel.h"
66 #include "nsIIOService.h"
67 #include "nsNetCID.h"
69 #include "nsIApplicationReputation.h"
71 #include "nsDSURIContentListener.h"
72 #include "nsMimeTypes.h"
73 #include "nsMIMEInfoImpl.h"
74 // used for header disposition information.
75 #include "nsIHttpChannel.h"
76 #include "nsIHttpChannelInternal.h"
77 #include "nsIEncodedChannel.h"
78 #include "nsIMultiPartChannel.h"
79 #include "nsIFileChannel.h"
80 #include "nsIObserverService.h" // so we can be a profile change observer
81 #include "nsIPropertyBag2.h" // for the 64-bit content length
83 #ifdef XP_MACOSX
84 # include "nsILocalFileMac.h"
85 #endif
87 #include "nsEscape.h"
89 #include "nsIStringBundle.h" // XXX needed to localize error msgs
90 #include "nsIPrompt.h"
92 #include "nsITextToSubURI.h" // to unescape the filename
94 #include "nsDocShellCID.h"
96 #include "nsCRT.h"
97 #include "nsLocalHandlerApp.h"
99 #include "nsIRandomGenerator.h"
101 #include "ContentChild.h"
102 #include "nsXULAppAPI.h"
103 #include "nsPIDOMWindow.h"
104 #include "ExternalHelperAppChild.h"
106 #include "mozilla/dom/nsHTTPSOnlyUtils.h"
108 #ifdef XP_WIN
109 # include "nsWindowsHelpers.h"
110 # include "nsLocalFile.h"
111 #endif
113 #include "mozilla/Components.h"
114 #include "mozilla/ClearOnShutdown.h"
115 #include "mozilla/Preferences.h"
116 #include "mozilla/ipc/URIUtils.h"
118 using namespace mozilla;
119 using namespace mozilla::ipc;
120 using namespace mozilla::dom;
122 #define kDefaultMaxFileNameLength 254
124 // Download Folder location constants
125 #define NS_PREF_DOWNLOAD_DIR "browser.download.dir"
126 #define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList"
127 enum {
128 NS_FOLDER_VALUE_DESKTOP = 0,
129 NS_FOLDER_VALUE_DOWNLOADS = 1,
130 NS_FOLDER_VALUE_CUSTOM = 2
133 LazyLogModule nsExternalHelperAppService::sLog("HelperAppService");
135 // Using level 3 here because the OSHelperAppServices use a log level
136 // of LogLevel::Debug (4), and we want less detailed output here
137 // Using 3 instead of LogLevel::Warning because we don't output warnings
138 #undef LOG
139 #define LOG(...) \
140 MOZ_LOG(nsExternalHelperAppService::sLog, mozilla::LogLevel::Info, \
141 (__VA_ARGS__))
142 #define LOG_ENABLED() \
143 MOZ_LOG_TEST(nsExternalHelperAppService::sLog, mozilla::LogLevel::Info)
145 static const char NEVER_ASK_FOR_SAVE_TO_DISK_PREF[] =
146 "browser.helperApps.neverAsk.saveToDisk";
147 static const char NEVER_ASK_FOR_OPEN_FILE_PREF[] =
148 "browser.helperApps.neverAsk.openFile";
150 StaticRefPtr<nsIFile> sFallbackDownloadDir;
152 // Helper functions for Content-Disposition headers
155 * Given a URI fragment, unescape it
156 * @param aFragment The string to unescape
157 * @param aURI The URI from which this fragment is taken. Only its character set
158 * will be used.
159 * @param aResult [out] Unescaped string.
161 static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI,
162 nsAString& aResult) {
163 // We need the unescaper
164 nsresult rv;
165 nsCOMPtr<nsITextToSubURI> textToSubURI =
166 do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv);
167 NS_ENSURE_SUCCESS(rv, rv);
169 return textToSubURI->UnEscapeURIForUI(aFragment, /* aDontEscape = */ true,
170 aResult);
174 * UTF-8 version of UnescapeFragment.
175 * @param aFragment The string to unescape
176 * @param aURI The URI from which this fragment is taken. Only its character set
177 * will be used.
178 * @param aResult [out] Unescaped string, UTF-8 encoded.
179 * @note It is safe to pass the same string for aFragment and aResult.
180 * @note When this function fails, aResult will not be modified.
182 static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI,
183 nsACString& aResult) {
184 nsAutoString result;
185 nsresult rv = UnescapeFragment(aFragment, aURI, result);
186 if (NS_SUCCEEDED(rv)) CopyUTF16toUTF8(result, aResult);
187 return rv;
191 * Obtains the directory to use. This tends to vary per platform, and
192 * needs to be consistent throughout our codepaths. For platforms where
193 * helper apps use the downloads directory, this should be kept in
194 * sync with DownloadIntegration.sys.mjs.
196 * Optionally skip availability of the directory and storage.
198 static nsresult GetDownloadDirectory(nsIFile** _directory,
199 bool aSkipChecks = false) {
200 #if defined(ANDROID)
201 return NS_ERROR_FAILURE;
202 #endif
204 bool usePrefDir = !StaticPrefs::browser_download_start_downloads_in_tmp_dir();
206 nsCOMPtr<nsIFile> dir;
207 nsresult rv;
208 if (usePrefDir) {
209 // Try to get the users download location, if it's set.
210 switch (Preferences::GetInt(NS_PREF_DOWNLOAD_FOLDERLIST, -1)) {
211 case NS_FOLDER_VALUE_DESKTOP:
212 (void)NS_GetSpecialDirectory(NS_OS_DESKTOP_DIR, getter_AddRefs(dir));
213 break;
214 case NS_FOLDER_VALUE_CUSTOM: {
215 Preferences::GetComplex(NS_PREF_DOWNLOAD_DIR, NS_GET_IID(nsIFile),
216 getter_AddRefs(dir));
217 if (!dir) break;
219 // If we're not checking for availability we're done.
220 if (aSkipChecks) {
221 dir.forget(_directory);
222 return NS_OK;
225 // We have the directory, and now we need to make sure it exists
226 nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755);
227 // If we can't create this and it's not because the file already
228 // exists, clear out `dir` so we don't return it.
229 if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_FAILED(rv)) {
230 dir = nullptr;
232 } break;
233 case NS_FOLDER_VALUE_DOWNLOADS:
234 // This is just the OS default location, so fall out
235 break;
237 if (!dir) {
238 rv = NS_GetSpecialDirectory(NS_OS_DEFAULT_DOWNLOAD_DIR,
239 getter_AddRefs(dir));
240 if (NS_FAILED(rv)) {
241 // On some OSes, there is no guarantee this directory exists.
242 // Fall back to $HOME + Downloads.
243 if (sFallbackDownloadDir) {
244 sFallbackDownloadDir->Clone(getter_AddRefs(dir));
245 } else {
246 rv = NS_GetSpecialDirectory(NS_OS_HOME_DIR, getter_AddRefs(dir));
247 NS_ENSURE_SUCCESS(rv, rv);
249 nsCOMPtr<nsIStringBundleService> bundleService =
250 do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
251 NS_ENSURE_SUCCESS(rv, rv);
252 nsAutoString downloadLocalized;
253 nsCOMPtr<nsIStringBundle> downloadBundle;
254 rv = bundleService->CreateBundle(
255 "chrome://mozapps/locale/downloads/downloads.properties",
256 getter_AddRefs(downloadBundle));
257 if (NS_SUCCEEDED(rv)) {
258 rv = downloadBundle->GetStringFromName("downloadsFolder",
259 downloadLocalized);
261 if (NS_FAILED(rv)) {
262 downloadLocalized.AssignLiteral("Downloads");
264 rv = dir->Append(downloadLocalized);
265 NS_ENSURE_SUCCESS(rv, rv);
266 // Can't getter_AddRefs on StaticRefPtr, so do some copying.
267 nsCOMPtr<nsIFile> copy;
268 dir->Clone(getter_AddRefs(copy));
269 sFallbackDownloadDir = copy.forget();
270 ClearOnShutdown(&sFallbackDownloadDir);
272 if (aSkipChecks) {
273 dir.forget(_directory);
274 return NS_OK;
277 // We have the directory, and now we need to make sure it exists
278 rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755);
279 if (rv == NS_ERROR_FILE_ALREADY_EXISTS || NS_SUCCEEDED(rv)) {
280 dir.forget(_directory);
281 rv = NS_OK;
283 return rv;
285 NS_ENSURE_SUCCESS(rv, rv);
287 } else {
288 rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dir));
289 NS_ENSURE_SUCCESS(rv, rv);
291 #if !defined(XP_MACOSX) && defined(XP_UNIX)
292 // Ensuring that only the current user can read the file names we end up
293 // creating. Note that creating directories with a specified permission is
294 // only supported on Unix platform right now. That's why the above check
295 // exists.
297 uint32_t permissions;
298 rv = dir->GetPermissions(&permissions);
299 NS_ENSURE_SUCCESS(rv, rv);
301 if (permissions != PR_IRWXU) {
302 const char* userName = PR_GetEnv("USERNAME");
303 if (!userName || !*userName) {
304 userName = PR_GetEnv("USER");
306 if (!userName || !*userName) {
307 userName = PR_GetEnv("LOGNAME");
309 if (!userName || !*userName) {
310 userName = "mozillaUser";
313 nsAutoString userDir;
314 userDir.AssignLiteral("mozilla_");
315 userDir.AppendASCII(userName);
316 userDir.ReplaceChar(u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_');
318 int counter = 0;
319 bool pathExists;
320 nsCOMPtr<nsIFile> finalPath;
322 while (true) {
323 nsAutoString countedUserDir(userDir);
324 countedUserDir.AppendInt(counter, 10);
325 dir->Clone(getter_AddRefs(finalPath));
326 finalPath->Append(countedUserDir);
328 rv = finalPath->Exists(&pathExists);
329 NS_ENSURE_SUCCESS(rv, rv);
331 if (pathExists) {
332 // If this path has the right permissions, use it.
333 rv = finalPath->GetPermissions(&permissions);
334 NS_ENSURE_SUCCESS(rv, rv);
336 // Ensuring the path is writable by the current user.
337 bool isWritable;
338 rv = finalPath->IsWritable(&isWritable);
339 NS_ENSURE_SUCCESS(rv, rv);
341 if (permissions == PR_IRWXU && isWritable) {
342 dir = finalPath;
343 break;
347 rv = finalPath->Create(nsIFile::DIRECTORY_TYPE, PR_IRWXU);
348 if (NS_SUCCEEDED(rv)) {
349 dir = finalPath;
350 break;
352 if (rv != NS_ERROR_FILE_ALREADY_EXISTS) {
353 // Unexpected error.
354 return rv;
356 counter++;
360 #endif
363 NS_ASSERTION(dir, "Somehow we didn't get a download directory!");
364 dir.forget(_directory);
365 return NS_OK;
369 * Helper for random bytes for the filename of downloaded part files.
371 nsresult GenerateRandomName(nsACString& result) {
372 // We will request raw random bytes, and transform that to a base64 string,
373 // using url-based base64 encoding so that all characters from the base64
374 // result will be acceptable for filenames.
375 // For each three bytes of random data, we will get four bytes of ASCII.
376 // Request a bit more, to be safe, then truncate in the end.
378 nsresult rv;
379 const uint32_t wantedFileNameLength = 8;
380 const uint32_t requiredBytesLength =
381 static_cast<uint32_t>((wantedFileNameLength + 1) / 4 * 3);
383 uint8_t buffer[requiredBytesLength];
384 if (!mozilla::GenerateRandomBytesFromOS(buffer, requiredBytesLength)) {
385 return NS_ERROR_FAILURE;
388 nsAutoCString tempLeafName;
389 // We're forced to specify a padding policy, though this is guaranteed
390 // not to need padding due to requiredBytesLength being a multiple of 3.
391 rv = Base64URLEncode(requiredBytesLength, buffer,
392 Base64URLEncodePaddingPolicy::Omit, tempLeafName);
393 NS_ENSURE_SUCCESS(rv, rv);
395 tempLeafName.Truncate(wantedFileNameLength);
397 result.Assign(tempLeafName);
398 return NS_OK;
402 * Structure for storing extension->type mappings.
403 * @see defaultMimeEntries
405 struct nsDefaultMimeTypeEntry {
406 const char* mMimeType;
407 const char* mFileExtension;
411 * Default extension->mimetype mappings. These are not overridable.
412 * If you add types here, make sure they are lowercase, or you'll regret it.
414 static const nsDefaultMimeTypeEntry defaultMimeEntries[] = {
415 // The following are those extensions that we're asked about during startup,
416 // sorted by order used
417 {IMAGE_GIF, "gif"},
418 {TEXT_XML, "xml"},
419 {APPLICATION_RDF, "rdf"},
420 {IMAGE_PNG, "png"},
421 // -- end extensions used during startup
422 {TEXT_CSS, "css"},
423 {IMAGE_JPEG, "jpeg"},
424 {IMAGE_JPEG, "jpg"},
425 {IMAGE_SVG_XML, "svg"},
426 {TEXT_HTML, "html"},
427 {TEXT_HTML, "htm"},
428 {APPLICATION_XPINSTALL, "xpi"},
429 {"application/xhtml+xml", "xhtml"},
430 {"application/xhtml+xml", "xht"},
431 {TEXT_PLAIN, "txt"},
432 {APPLICATION_JSON, "json"},
433 {APPLICATION_XJAVASCRIPT, "mjs"},
434 {APPLICATION_XJAVASCRIPT, "js"},
435 {APPLICATION_XJAVASCRIPT, "jsm"},
436 {VIDEO_OGG, "ogv"},
437 {VIDEO_OGG, "ogg"},
438 {APPLICATION_OGG, "ogg"},
439 {AUDIO_OGG, "oga"},
440 {AUDIO_OGG, "opus"},
441 {APPLICATION_PDF, "pdf"},
442 {VIDEO_WEBM, "webm"},
443 {AUDIO_WEBM, "webm"},
444 {IMAGE_ICO, "ico"},
445 {TEXT_PLAIN, "properties"},
446 {TEXT_PLAIN, "locale"},
447 {TEXT_PLAIN, "ftl"},
448 #if defined(MOZ_WMF)
449 {VIDEO_MP4, "mp4"},
450 {AUDIO_MP4, "m4a"},
451 {AUDIO_MP3, "mp3"},
452 #endif
453 #ifdef MOZ_RAW
454 {VIDEO_RAW, "yuv"}
455 #endif
459 * This is a small private struct used to help us initialize some
460 * default mime types.
462 struct nsExtraMimeTypeEntry {
463 const char* mMimeType;
464 const char* mFileExtensions;
465 const char* mDescription;
469 * This table lists all of the 'extra' content types that we can deduce from
470 * particular file extensions. These entries also ensure that we provide a good
471 * descriptive name when we encounter files with these content types and/or
472 * extensions. These can be overridden by user helper app prefs. If you add
473 * types here, make sure they are lowercase, or you'll regret it.
475 static const nsExtraMimeTypeEntry extraMimeEntries[] = {
476 #if defined(XP_MACOSX) // don't define .bin on the mac...use internet config to
477 // look that up...
478 {APPLICATION_OCTET_STREAM, "exe,com", "Binary File"},
479 #else
480 {APPLICATION_OCTET_STREAM, "exe,com,bin", "Binary File"},
481 #endif
482 {APPLICATION_GZIP2, "gz", "gzip"},
483 {"application/x-arj", "arj", "ARJ file"},
484 {"application/rtf", "rtf", "Rich Text Format File"},
485 {APPLICATION_ZIP, "zip", "ZIP Archive"},
486 {APPLICATION_XPINSTALL, "xpi", "XPInstall Install"},
487 {APPLICATION_PDF, "pdf", "Portable Document Format"},
488 {APPLICATION_POSTSCRIPT, "ps,eps,ai", "Postscript File"},
489 {APPLICATION_XJAVASCRIPT, "js", "Javascript Source File"},
490 {APPLICATION_XJAVASCRIPT, "jsm,mjs", "Javascript Module Source File"},
491 #ifdef MOZ_WIDGET_ANDROID
492 {"application/vnd.android.package-archive", "apk", "Android Package"},
493 #endif
495 // OpenDocument formats
496 {"application/vnd.oasis.opendocument.text", "odt", "OpenDocument Text"},
497 {"application/vnd.oasis.opendocument.presentation", "odp",
498 "OpenDocument Presentation"},
499 {"application/vnd.oasis.opendocument.spreadsheet", "ods",
500 "OpenDocument Spreadsheet"},
501 {"application/vnd.oasis.opendocument.graphics", "odg",
502 "OpenDocument Graphics"},
504 // Legacy Microsoft Office
505 {"application/msword", "doc", "Microsoft Word"},
506 {"application/vnd.ms-powerpoint", "ppt", "Microsoft PowerPoint"},
507 {"application/vnd.ms-excel", "xls", "Microsoft Excel"},
509 // Office Open XML
510 {"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
511 "docx", "Microsoft Word (Open XML)"},
512 {"application/"
513 "vnd.openxmlformats-officedocument.presentationml.presentation",
514 "pptx", "Microsoft PowerPoint (Open XML)"},
515 {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
516 "xlsx", "Microsoft Excel (Open XML)"},
518 {IMAGE_ART, "art", "ART Image"},
519 {IMAGE_BMP, "bmp", "BMP Image"},
520 {IMAGE_GIF, "gif", "GIF Image"},
521 {IMAGE_ICO, "ico,cur", "ICO Image"},
522 {IMAGE_JPEG, "jpg,jpeg,jfif,pjpeg,pjp", "JPEG Image"},
523 {IMAGE_PNG, "png", "PNG Image"},
524 {IMAGE_APNG, "apng", "APNG Image"},
525 {IMAGE_TIFF, "tiff,tif", "TIFF Image"},
526 {IMAGE_XBM, "xbm", "XBM Image"},
527 {IMAGE_SVG_XML, "svg", "Scalable Vector Graphics"},
528 {IMAGE_WEBP, "webp", "WebP Image"},
529 {IMAGE_AVIF, "avif", "AV1 Image File"},
530 {IMAGE_JXL, "jxl", "JPEG XL Image File"},
532 {MESSAGE_RFC822, "eml", "RFC-822 data"},
533 {TEXT_PLAIN, "txt,text", "Text File"},
534 {APPLICATION_JSON, "json", "JavaScript Object Notation"},
535 {TEXT_VTT, "vtt", "Web Video Text Tracks"},
536 {TEXT_CACHE_MANIFEST, "appcache", "Application Cache Manifest"},
537 {TEXT_HTML, "html,htm,shtml,ehtml", "HyperText Markup Language"},
538 {"application/xhtml+xml", "xhtml,xht",
539 "Extensible HyperText Markup Language"},
540 {APPLICATION_MATHML_XML, "mml", "Mathematical Markup Language"},
541 {APPLICATION_RDF, "rdf", "Resource Description Framework"},
542 {"text/csv", "csv", "CSV File"},
543 {TEXT_XML, "xml,xsl,xbl", "Extensible Markup Language"},
544 {TEXT_CSS, "css", "Style Sheet"},
545 {TEXT_VCARD, "vcf,vcard", "Contact Information"},
546 {TEXT_CALENDAR, "ics,ical,ifb,icalendar", "iCalendar"},
547 {VIDEO_OGG, "ogv,ogg", "Ogg Video"},
548 {APPLICATION_OGG, "ogg", "Ogg Video"},
549 {AUDIO_OGG, "oga", "Ogg Audio"},
550 {AUDIO_OGG, "opus", "Opus Audio"},
551 {VIDEO_WEBM, "webm", "Web Media Video"},
552 {AUDIO_WEBM, "webm", "Web Media Audio"},
553 {AUDIO_MP3, "mp3,mpega,mp2", "MPEG Audio"},
554 {VIDEO_MP4, "mp4,m4a,m4b", "MPEG-4 Video"},
555 {AUDIO_MP4, "m4a,m4b", "MPEG-4 Audio"},
556 {VIDEO_RAW, "yuv", "Raw YUV Video"},
557 {AUDIO_WAV, "wav", "Waveform Audio"},
558 {VIDEO_3GPP, "3gpp,3gp", "3GPP Video"},
559 {VIDEO_3GPP2, "3g2", "3GPP2 Video"},
560 {AUDIO_AAC, "aac", "AAC Audio"},
561 {AUDIO_FLAC, "flac", "FLAC Audio"},
562 {AUDIO_MIDI, "mid", "Standard MIDI Audio"},
563 {APPLICATION_WASM, "wasm", "WebAssembly Module"}};
565 static const nsDefaultMimeTypeEntry sForbiddenPrimaryExtensions[] = {
566 {IMAGE_JPEG, "jfif"}};
569 * File extensions for which decoding should be disabled.
570 * NOTE: These MUST be lower-case and ASCII.
572 static const nsDefaultMimeTypeEntry nonDecodableExtensions[] = {
573 {APPLICATION_GZIP, "gz"},
574 {APPLICATION_GZIP, "tgz"},
575 {APPLICATION_ZIP, "zip"},
576 {APPLICATION_COMPRESS, "z"},
577 {APPLICATION_GZIP, "svgz"}};
580 * Mimetypes for which we enforce using a known extension.
582 * In addition to this list, we do this for all audio/, video/ and
583 * image/ mimetypes.
585 static const char* forcedExtensionMimetypes[] = {
586 APPLICATION_PDF, APPLICATION_OGG, APPLICATION_WASM,
587 TEXT_CALENDAR, TEXT_CSS, TEXT_VCARD};
590 * Primary extensions of types whose descriptions should be overwritten.
591 * This extension is concatenated with "ExtHandlerDescription" to look up the
592 * description in unknownContentType.properties.
593 * NOTE: These MUST be lower-case and ASCII.
595 static const char* descriptionOverwriteExtensions[] = {
596 "avif", "jxl", "pdf", "svg", "webp", "xml",
599 static StaticRefPtr<nsExternalHelperAppService> sExtHelperAppSvcSingleton;
602 * In child processes, return an nsOSHelperAppServiceChild for remoting
603 * OS calls to the parent process. In the parent process itself, use
604 * nsOSHelperAppService.
606 /* static */
607 already_AddRefed<nsExternalHelperAppService>
608 nsExternalHelperAppService::GetSingleton() {
609 if (!sExtHelperAppSvcSingleton) {
610 if (XRE_IsParentProcess()) {
611 sExtHelperAppSvcSingleton = new nsOSHelperAppService();
612 } else {
613 sExtHelperAppSvcSingleton = new nsOSHelperAppServiceChild();
615 ClearOnShutdown(&sExtHelperAppSvcSingleton);
618 return do_AddRef(sExtHelperAppSvcSingleton);
621 NS_IMPL_ISUPPORTS(nsExternalHelperAppService, nsIExternalHelperAppService,
622 nsPIExternalAppLauncher, nsIExternalProtocolService,
623 nsIMIMEService, nsIObserver, nsISupportsWeakReference)
625 nsExternalHelperAppService::nsExternalHelperAppService() {}
626 nsresult nsExternalHelperAppService::Init() {
627 // Add an observer for profile change
628 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
629 if (!obs) return NS_ERROR_FAILURE;
631 nsresult rv = obs->AddObserver(this, "profile-before-change", true);
632 NS_ENSURE_SUCCESS(rv, rv);
633 return obs->AddObserver(this, "last-pb-context-exited", true);
636 nsExternalHelperAppService::~nsExternalHelperAppService() {}
638 nsresult nsExternalHelperAppService::DoContentContentProcessHelper(
639 const nsACString& aMimeContentType, nsIChannel* aChannel,
640 BrowsingContext* aContentContext, bool aForceSave,
641 nsIInterfaceRequestor* aWindowContext,
642 nsIStreamListener** aStreamListener) {
643 NS_ENSURE_ARG_POINTER(aChannel);
645 // We need to get a hold of a ContentChild so that we can begin forwarding
646 // this data to the parent. In the HTTP case, this is unfortunate, since
647 // we're actually passing data from parent->child->parent wastefully, but
648 // the Right Fix will eventually be to short-circuit those channels on the
649 // parent side based on some sort of subscription concept.
650 using mozilla::dom::ContentChild;
651 using mozilla::dom::ExternalHelperAppChild;
652 ContentChild* child = ContentChild::GetSingleton();
653 if (!child) {
654 return NS_ERROR_FAILURE;
657 nsCString disp;
658 nsCOMPtr<nsIURI> uri;
659 int64_t contentLength = -1;
660 bool wasFileChannel = false;
661 uint32_t contentDisposition = -1;
662 nsAutoString fileName;
663 nsCOMPtr<nsILoadInfo> loadInfo;
665 aChannel->GetURI(getter_AddRefs(uri));
666 aChannel->GetContentLength(&contentLength);
667 aChannel->GetContentDisposition(&contentDisposition);
668 aChannel->GetContentDispositionFilename(fileName);
669 aChannel->GetContentDispositionHeader(disp);
670 loadInfo = aChannel->LoadInfo();
672 nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aChannel));
673 wasFileChannel = fileChan != nullptr;
675 nsCOMPtr<nsIURI> referrer;
676 NS_GetReferrerFromChannel(aChannel, getter_AddRefs(referrer));
678 mozilla::net::LoadInfoArgs loadInfoArgs;
679 MOZ_ALWAYS_SUCCEEDS(LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs));
681 nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aChannel));
682 // Determine whether a new window was opened specifically for this request
683 bool shouldCloseWindow = false;
684 if (props) {
685 props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns,
686 &shouldCloseWindow);
689 // Now we build a protocol for forwarding our data to the parent. The
690 // protocol will act as a listener on the child-side and create a "real"
691 // helperAppService listener on the parent-side, via another call to
692 // DoContent.
693 RefPtr<ExternalHelperAppChild> childListener = new ExternalHelperAppChild();
694 MOZ_ALWAYS_TRUE(child->SendPExternalHelperAppConstructor(
695 childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp,
696 contentDisposition, fileName, aForceSave, contentLength, wasFileChannel,
697 referrer, aContentContext, shouldCloseWindow));
699 NS_ADDREF(*aStreamListener = childListener);
701 uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
703 SanitizeFileName(fileName, 0);
705 RefPtr<nsExternalAppHandler> handler =
706 new nsExternalAppHandler(nullptr, u""_ns, aContentContext, aWindowContext,
707 this, fileName, reason, aForceSave);
708 if (!handler) {
709 return NS_ERROR_OUT_OF_MEMORY;
712 childListener->SetHandler(handler);
713 return NS_OK;
716 NS_IMETHODIMP nsExternalHelperAppService::CreateListener(
717 const nsACString& aMimeContentType, nsIChannel* aChannel,
718 BrowsingContext* aContentContext, bool aForceSave,
719 nsIInterfaceRequestor* aWindowContext,
720 nsIStreamListener** aStreamListener) {
721 MOZ_ASSERT(!XRE_IsContentProcess());
722 NS_ENSURE_ARG_POINTER(aChannel);
724 nsAutoString fileName;
725 nsAutoCString fileExtension;
726 uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
728 uint32_t contentDisposition = -1;
729 aChannel->GetContentDisposition(&contentDisposition);
730 if (contentDisposition == nsIChannel::DISPOSITION_ATTACHMENT) {
731 reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST;
734 *aStreamListener = nullptr;
736 // Get the file extension and name that we will need later
737 nsCOMPtr<nsIURI> uri;
738 bool allowURLExtension =
739 GetFileNameFromChannel(aChannel, fileName, getter_AddRefs(uri));
741 uint32_t flags = VALIDATE_ALLOW_EMPTY;
742 if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT,
743 nsCaseInsensitiveCStringComparator)) {
744 flags |= VALIDATE_GUESS_FROM_EXTENSION;
747 nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
748 fileName, aMimeContentType, uri, nullptr, flags, allowURLExtension);
750 LOG("Type/Ext lookup found 0x%p\n", mimeInfo.get());
752 // No mimeinfo -> we can't continue. probably OOM.
753 if (!mimeInfo) {
754 return NS_ERROR_OUT_OF_MEMORY;
757 if (flags & VALIDATE_GUESS_FROM_EXTENSION) {
758 // Replace the content type with what was guessed.
759 nsAutoCString mimeType;
760 mimeInfo->GetMIMEType(mimeType);
761 aChannel->SetContentType(mimeType);
763 if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
764 reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
768 nsAutoString extension;
769 int32_t dotidx = fileName.RFind(u".");
770 if (dotidx != -1) {
771 extension = Substring(fileName, dotidx + 1);
774 // NB: ExternalHelperAppParent depends on this listener always being an
775 // nsExternalAppHandler. If this changes, make sure to update that code.
776 nsExternalAppHandler* handler = new nsExternalAppHandler(
777 mimeInfo, extension, aContentContext, aWindowContext, this, fileName,
778 reason, aForceSave);
779 if (!handler) {
780 return NS_ERROR_OUT_OF_MEMORY;
783 NS_ADDREF(*aStreamListener = handler);
784 return NS_OK;
787 NS_IMETHODIMP nsExternalHelperAppService::DoContent(
788 const nsACString& aMimeContentType, nsIChannel* aChannel,
789 nsIInterfaceRequestor* aContentContext, bool aForceSave,
790 nsIInterfaceRequestor* aWindowContext,
791 nsIStreamListener** aStreamListener) {
792 // Scripted interface requestors cannot return an instance of the
793 // (non-scriptable) nsPIDOMWindowOuter or nsPIDOMWindowInner interfaces, so
794 // get to the window via `nsIDOMWindow`. Unfortunately, at that point we
795 // don't know whether the thing we got is an inner or outer window, so have to
796 // work with either one.
797 RefPtr<BrowsingContext> bc;
798 nsCOMPtr<nsIDOMWindow> domWindow = do_GetInterface(aContentContext);
799 if (nsCOMPtr<nsPIDOMWindowOuter> outerWindow = do_QueryInterface(domWindow)) {
800 bc = outerWindow->GetBrowsingContext();
801 } else if (nsCOMPtr<nsPIDOMWindowInner> innerWindow =
802 do_QueryInterface(domWindow)) {
803 bc = innerWindow->GetBrowsingContext();
806 if (XRE_IsContentProcess()) {
807 return DoContentContentProcessHelper(aMimeContentType, aChannel, bc,
808 aForceSave, aWindowContext,
809 aStreamListener);
812 nsresult rv = CreateListener(aMimeContentType, aChannel, bc, aForceSave,
813 aWindowContext, aStreamListener);
814 return rv;
817 NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension(
818 const nsACString& aExtension, const nsACString& aEncodingType,
819 bool* aApplyDecoding) {
820 *aApplyDecoding = true;
821 uint32_t i;
822 for (i = 0; i < ArrayLength(nonDecodableExtensions); ++i) {
823 if (aExtension.LowerCaseEqualsASCII(
824 nonDecodableExtensions[i].mFileExtension) &&
825 aEncodingType.LowerCaseEqualsASCII(
826 nonDecodableExtensions[i].mMimeType)) {
827 *aApplyDecoding = false;
828 break;
831 return NS_OK;
834 nsresult nsExternalHelperAppService::GetFileTokenForPath(
835 const char16_t* aPlatformAppPath, nsIFile** aFile) {
836 nsDependentString platformAppPath(aPlatformAppPath);
837 // First, check if we have an absolute path
838 nsIFile* localFile = nullptr;
839 nsresult rv = NS_NewLocalFile(platformAppPath, true, &localFile);
840 if (NS_SUCCEEDED(rv)) {
841 *aFile = localFile;
842 bool exists;
843 if (NS_FAILED((*aFile)->Exists(&exists)) || !exists) {
844 NS_RELEASE(*aFile);
845 return NS_ERROR_FILE_NOT_FOUND;
847 return NS_OK;
850 // Second, check if file exists in mozilla program directory
851 rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, aFile);
852 if (NS_SUCCEEDED(rv)) {
853 rv = (*aFile)->Append(platformAppPath);
854 if (NS_SUCCEEDED(rv)) {
855 bool exists = false;
856 rv = (*aFile)->Exists(&exists);
857 if (NS_SUCCEEDED(rv) && exists) return NS_OK;
859 NS_RELEASE(*aFile);
862 return NS_ERROR_NOT_AVAILABLE;
865 //////////////////////////////////////////////////////////////////////////////////////////////////////
866 // begin external protocol service default implementation...
867 //////////////////////////////////////////////////////////////////////////////////////////////////////
868 NS_IMETHODIMP nsExternalHelperAppService::ExternalProtocolHandlerExists(
869 const char* aProtocolScheme, bool* aHandlerExists) {
870 nsCOMPtr<nsIHandlerInfo> handlerInfo;
871 nsresult rv = GetProtocolHandlerInfo(nsDependentCString(aProtocolScheme),
872 getter_AddRefs(handlerInfo));
873 if (NS_SUCCEEDED(rv)) {
874 // See if we have any known possible handler apps for this
875 nsCOMPtr<nsIMutableArray> possibleHandlers;
876 handlerInfo->GetPossibleApplicationHandlers(
877 getter_AddRefs(possibleHandlers));
879 uint32_t length;
880 possibleHandlers->GetLength(&length);
881 if (length) {
882 *aHandlerExists = true;
883 return NS_OK;
887 // if not, fall back on an os-based handler
888 return OSProtocolHandlerExists(aProtocolScheme, aHandlerExists);
891 NS_IMETHODIMP nsExternalHelperAppService::IsExposedProtocol(
892 const char* aProtocolScheme, bool* aResult) {
893 // check the per protocol setting first. it always takes precedence.
894 // if not set, then use the global setting.
896 nsAutoCString prefName("network.protocol-handler.expose.");
897 prefName += aProtocolScheme;
898 bool val;
899 if (NS_SUCCEEDED(Preferences::GetBool(prefName.get(), &val))) {
900 *aResult = val;
901 return NS_OK;
904 // by default, no protocol is exposed. i.e., by default all link clicks must
905 // go through the external protocol service. most applications override this
906 // default behavior.
907 *aResult = Preferences::GetBool("network.protocol-handler.expose-all", false);
909 return NS_OK;
912 static const char kExternalProtocolPrefPrefix[] =
913 "network.protocol-handler.external.";
914 static const char kExternalProtocolDefaultPref[] =
915 "network.protocol-handler.external-default";
917 // static
918 nsresult nsExternalHelperAppService::EscapeURI(nsIURI* aURI, nsIURI** aResult) {
919 MOZ_ASSERT(aURI);
920 MOZ_ASSERT(aResult);
922 nsAutoCString spec;
923 aURI->GetSpec(spec);
925 if (spec.Find("%00") != -1) return NS_ERROR_MALFORMED_URI;
927 nsAutoCString escapedSpec;
928 nsresult rv = NS_EscapeURL(spec, esc_AlwaysCopy | esc_ExtHandler, escapedSpec,
929 fallible);
930 NS_ENSURE_SUCCESS(rv, rv);
932 nsCOMPtr<nsIIOService> ios(do_GetIOService());
933 return ios->NewURI(escapedSpec, nullptr, nullptr, aResult);
936 bool ExternalProtocolIsBlockedBySandbox(
937 BrowsingContext* aBrowsingContext,
938 const bool aHasValidUserGestureActivation) {
939 if (!StaticPrefs::dom_block_external_protocol_navigation_from_sandbox()) {
940 return false;
943 if (!aBrowsingContext || aBrowsingContext->IsTop()) {
944 return false;
947 uint32_t sandboxFlags = aBrowsingContext->GetSandboxFlags();
949 if (sandboxFlags == SANDBOXED_NONE) {
950 return false;
953 if (!(sandboxFlags & SANDBOXED_AUXILIARY_NAVIGATION)) {
954 return false;
957 if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION)) {
958 return false;
961 if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_CUSTOM_PROTOCOLS)) {
962 return false;
965 if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION) &&
966 aHasValidUserGestureActivation) {
967 return false;
970 return true;
973 NS_IMETHODIMP
974 nsExternalHelperAppService::LoadURI(nsIURI* aURI,
975 nsIPrincipal* aTriggeringPrincipal,
976 nsIPrincipal* aRedirectPrincipal,
977 BrowsingContext* aBrowsingContext,
978 bool aTriggeredExternally,
979 bool aHasValidUserGestureActivation) {
980 NS_ENSURE_ARG_POINTER(aURI);
982 if (XRE_IsContentProcess()) {
983 mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal(
984 aURI, aTriggeringPrincipal, aRedirectPrincipal, aBrowsingContext,
985 aTriggeredExternally, aHasValidUserGestureActivation);
986 return NS_OK;
989 // Prevent sandboxed BrowsingContexts from navigating to external protocols.
990 // This only uses the sandbox flags of the target BrowsingContext of the
991 // load. The navigating document's CSP sandbox flags do not apply.
992 if (aBrowsingContext &&
993 ExternalProtocolIsBlockedBySandbox(aBrowsingContext,
994 aHasValidUserGestureActivation)) {
995 // Log an error to the web console of the sandboxed BrowsingContext.
996 nsAutoString localizedMsg;
997 nsAutoCString spec;
998 aURI->GetSpec(spec);
1000 AutoTArray<nsString, 1> params = {NS_ConvertUTF8toUTF16(spec)};
1001 nsresult rv = nsContentUtils::FormatLocalizedString(
1002 nsContentUtils::eSECURITY_PROPERTIES, "SandboxBlockedCustomProtocols",
1003 params, localizedMsg);
1004 NS_ENSURE_SUCCESS(rv, rv);
1006 // Log to the the parent window of the iframe. If there is no parent, fall
1007 // back to the iframe window itself.
1008 WindowContext* windowContext = aBrowsingContext->GetParentWindowContext();
1009 if (!windowContext) {
1010 windowContext = aBrowsingContext->GetCurrentWindowContext();
1013 // Skip logging if we still don't have a WindowContext.
1014 NS_ENSURE_TRUE(windowContext, NS_ERROR_FAILURE);
1016 nsContentUtils::ReportToConsoleByWindowID(
1017 localizedMsg, nsIScriptError::errorFlag, "Security"_ns,
1018 windowContext->InnerWindowId(),
1019 windowContext->Canonical()->GetDocumentURI());
1021 return NS_OK;
1024 nsCOMPtr<nsIURI> escapedURI;
1025 nsresult rv = EscapeURI(aURI, getter_AddRefs(escapedURI));
1026 NS_ENSURE_SUCCESS(rv, rv);
1028 nsAutoCString scheme;
1029 escapedURI->GetScheme(scheme);
1030 if (scheme.IsEmpty()) return NS_OK; // must have a scheme
1032 // Deny load if the prefs say to do so
1033 nsAutoCString externalPref(kExternalProtocolPrefPrefix);
1034 externalPref += scheme;
1035 bool allowLoad = false;
1036 if (NS_FAILED(Preferences::GetBool(externalPref.get(), &allowLoad))) {
1037 // no scheme-specific value, check the default
1038 if (NS_FAILED(
1039 Preferences::GetBool(kExternalProtocolDefaultPref, &allowLoad))) {
1040 return NS_OK; // missing default pref
1044 if (!allowLoad) {
1045 return NS_OK; // explicitly denied
1048 // Now check if the principal is allowed to access the navigated context.
1049 // We allow navigating subframes, even if not same-origin - non-external
1050 // links can always navigate everywhere, so this is a minor additional
1051 // restriction, only aiming to prevent some types of spoofing attacks
1052 // from otherwise disjoint browsingcontext trees.
1053 if (aBrowsingContext && aTriggeringPrincipal &&
1054 !StaticPrefs::security_allow_disjointed_external_uri_loads() &&
1055 // Add-on principals are always allowed:
1056 !BasePrincipal::Cast(aTriggeringPrincipal)->AddonPolicy() &&
1057 // As is chrome code:
1058 !aTriggeringPrincipal->IsSystemPrincipal()) {
1059 RefPtr<BrowsingContext> bc = aBrowsingContext;
1060 WindowGlobalParent* wgp = bc->Canonical()->GetCurrentWindowGlobal();
1061 bool foundAccessibleFrame = false;
1063 // Also allow this load if the target is a toplevel BC and contains a
1064 // non-web-controlled about:blank document
1065 if (bc->IsTop() && !bc->HadOriginalOpener() && wgp) {
1066 RefPtr<nsIURI> uri = wgp->GetDocumentURI();
1067 foundAccessibleFrame =
1068 uri && uri->GetSpecOrDefault().EqualsLiteral("about:blank");
1071 while (!foundAccessibleFrame) {
1072 if (wgp) {
1073 foundAccessibleFrame =
1074 aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal());
1076 // We have to get the parent via the bc, because there may not
1077 // be a window global for the innermost bc; see bug 1650162.
1078 BrowsingContext* parent = bc->GetParent();
1079 if (!parent) {
1080 break;
1082 bc = parent;
1083 wgp = parent->Canonical()->GetCurrentWindowGlobal();
1086 if (!foundAccessibleFrame) {
1087 // See if this navigation could have come from a subframe.
1088 nsTArray<RefPtr<BrowsingContext>> contexts;
1089 aBrowsingContext->GetAllBrowsingContextsInSubtree(contexts);
1090 for (const auto& kid : contexts) {
1091 wgp = kid->Canonical()->GetCurrentWindowGlobal();
1092 if (wgp && aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal())) {
1093 foundAccessibleFrame = true;
1094 break;
1099 if (!foundAccessibleFrame) {
1100 return NS_OK; // deny the load.
1104 nsCOMPtr<nsIHandlerInfo> handler;
1105 rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler));
1106 NS_ENSURE_SUCCESS(rv, rv);
1108 nsCOMPtr<nsIContentDispatchChooser> chooser =
1109 do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv);
1110 NS_ENSURE_SUCCESS(rv, rv);
1112 return chooser->HandleURI(
1113 handler, escapedURI,
1114 aRedirectPrincipal ? aRedirectPrincipal : aTriggeringPrincipal,
1115 aBrowsingContext, aTriggeredExternally);
1118 //////////////////////////////////////////////////////////////////////////////////////////////////////
1119 // Methods related to deleting temporary files on exit
1120 //////////////////////////////////////////////////////////////////////////////////////////////////////
1122 /* static */
1123 nsresult nsExternalHelperAppService::DeleteTemporaryFileHelper(
1124 nsIFile* aTemporaryFile, nsCOMArray<nsIFile>& aFileList) {
1125 bool isFile = false;
1127 // as a safety measure, make sure the nsIFile is really a file and not a
1128 // directory object.
1129 aTemporaryFile->IsFile(&isFile);
1130 if (!isFile) return NS_OK;
1132 aFileList.AppendObject(aTemporaryFile);
1134 return NS_OK;
1137 NS_IMETHODIMP
1138 nsExternalHelperAppService::DeleteTemporaryFileOnExit(nsIFile* aTemporaryFile) {
1139 return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryFilesList);
1142 NS_IMETHODIMP
1143 nsExternalHelperAppService::DeleteTemporaryPrivateFileWhenPossible(
1144 nsIFile* aTemporaryFile) {
1145 return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryPrivateFilesList);
1148 void nsExternalHelperAppService::ExpungeTemporaryFilesHelper(
1149 nsCOMArray<nsIFile>& fileList) {
1150 int32_t numEntries = fileList.Count();
1151 nsIFile* localFile;
1152 for (int32_t index = 0; index < numEntries; index++) {
1153 localFile = fileList[index];
1154 if (localFile) {
1155 // First make the file writable, since the temp file is probably readonly.
1156 localFile->SetPermissions(0600);
1157 localFile->Remove(false);
1161 fileList.Clear();
1164 void nsExternalHelperAppService::ExpungeTemporaryFiles() {
1165 ExpungeTemporaryFilesHelper(mTemporaryFilesList);
1168 void nsExternalHelperAppService::ExpungeTemporaryPrivateFiles() {
1169 ExpungeTemporaryFilesHelper(mTemporaryPrivateFilesList);
1172 static const char kExternalWarningPrefPrefix[] =
1173 "network.protocol-handler.warn-external.";
1174 static const char kExternalWarningDefaultPref[] =
1175 "network.protocol-handler.warn-external-default";
1177 NS_IMETHODIMP
1178 nsExternalHelperAppService::GetProtocolHandlerInfo(
1179 const nsACString& aScheme, nsIHandlerInfo** aHandlerInfo) {
1180 // XXX enterprise customers should be able to turn this support off with a
1181 // single master pref (maybe use one of the "exposed" prefs here?)
1183 bool exists;
1184 nsresult rv = GetProtocolHandlerInfoFromOS(aScheme, &exists, aHandlerInfo);
1185 if (NS_FAILED(rv)) {
1186 // Either it knows nothing, or we ran out of memory
1187 return NS_ERROR_FAILURE;
1190 nsCOMPtr<nsIHandlerService> handlerSvc =
1191 do_GetService(NS_HANDLERSERVICE_CONTRACTID);
1192 if (handlerSvc) {
1193 bool hasHandler = false;
1194 (void)handlerSvc->Exists(*aHandlerInfo, &hasHandler);
1195 if (hasHandler) {
1196 rv = handlerSvc->FillHandlerInfo(*aHandlerInfo, ""_ns);
1197 if (NS_SUCCEEDED(rv)) return NS_OK;
1201 return SetProtocolHandlerDefaults(*aHandlerInfo, exists);
1204 NS_IMETHODIMP
1205 nsExternalHelperAppService::SetProtocolHandlerDefaults(
1206 nsIHandlerInfo* aHandlerInfo, bool aOSHandlerExists) {
1207 // this type isn't in our database, so we've only got an OS default handler,
1208 // if one exists
1210 if (aOSHandlerExists) {
1211 // we've got a default, so use it
1212 aHandlerInfo->SetPreferredAction(nsIHandlerInfo::useSystemDefault);
1214 // whether or not to ask the user depends on the warning preference
1215 nsAutoCString scheme;
1216 aHandlerInfo->GetType(scheme);
1218 nsAutoCString warningPref(kExternalWarningPrefPrefix);
1219 warningPref += scheme;
1220 bool warn;
1221 if (NS_FAILED(Preferences::GetBool(warningPref.get(), &warn))) {
1222 // no scheme-specific value, check the default
1223 warn = Preferences::GetBool(kExternalWarningDefaultPref, true);
1225 aHandlerInfo->SetAlwaysAskBeforeHandling(warn);
1226 } else {
1227 // If no OS default existed, we set the preferred action to alwaysAsk.
1228 // This really means not initialized (i.e. there's no available handler)
1229 // to all the code...
1230 aHandlerInfo->SetPreferredAction(nsIHandlerInfo::alwaysAsk);
1233 return NS_OK;
1236 // XPCOM profile change observer
1237 NS_IMETHODIMP
1238 nsExternalHelperAppService::Observe(nsISupports* aSubject, const char* aTopic,
1239 const char16_t* someData) {
1240 if (!strcmp(aTopic, "profile-before-change")) {
1241 ExpungeTemporaryFiles();
1242 } else if (!strcmp(aTopic, "last-pb-context-exited")) {
1243 ExpungeTemporaryPrivateFiles();
1245 return NS_OK;
1248 //////////////////////////////////////////////////////////////////////////////////////////////////////
1249 // begin external app handler implementation
1250 //////////////////////////////////////////////////////////////////////////////////////////////////////
1252 NS_IMPL_ADDREF(nsExternalAppHandler)
1253 NS_IMPL_RELEASE(nsExternalAppHandler)
1255 NS_INTERFACE_MAP_BEGIN(nsExternalAppHandler)
1256 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener)
1257 NS_INTERFACE_MAP_ENTRY(nsIStreamListener)
1258 NS_INTERFACE_MAP_ENTRY(nsIRequestObserver)
1259 NS_INTERFACE_MAP_ENTRY(nsIHelperAppLauncher)
1260 NS_INTERFACE_MAP_ENTRY(nsICancelable)
1261 NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver)
1262 NS_INTERFACE_MAP_ENTRY(nsINamed)
1263 NS_INTERFACE_MAP_ENTRY_CONCRETE(nsExternalAppHandler)
1264 NS_INTERFACE_MAP_END
1266 nsExternalAppHandler::nsExternalAppHandler(
1267 nsIMIMEInfo* aMIMEInfo, const nsAString& aFileExtension,
1268 BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext,
1269 nsExternalHelperAppService* aExtProtSvc,
1270 const nsAString& aSuggestedFileName, uint32_t aReason, bool aForceSave)
1271 : mMimeInfo(aMIMEInfo),
1272 mBrowsingContext(aBrowsingContext),
1273 mWindowContext(aWindowContext),
1274 mSuggestedFileName(aSuggestedFileName),
1275 mForceSave(aForceSave),
1276 mForceSaveInternallyHandled(false),
1277 mCanceled(false),
1278 mStopRequestIssued(false),
1279 mIsFileChannel(false),
1280 mShouldCloseWindow(false),
1281 mHandleInternally(false),
1282 mDialogShowing(false),
1283 mReason(aReason),
1284 mTempFileIsExecutable(false),
1285 mTimeDownloadStarted(0),
1286 mContentLength(-1),
1287 mProgress(0),
1288 mSaver(nullptr),
1289 mDialogProgressListener(nullptr),
1290 mTransfer(nullptr),
1291 mRequest(nullptr),
1292 mExtProtSvc(aExtProtSvc) {
1293 // make sure the extention includes the '.'
1294 if (!aFileExtension.IsEmpty() && aFileExtension.First() != '.') {
1295 mFileExtension = char16_t('.');
1297 mFileExtension.Append(aFileExtension);
1299 mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096);
1302 nsExternalAppHandler::~nsExternalAppHandler() {
1303 MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted");
1306 void nsExternalAppHandler::DidDivertRequest(nsIRequest* request) {
1307 MOZ_ASSERT(XRE_IsContentProcess(), "in child process");
1308 // Remove our request from the child loadGroup
1309 RetargetLoadNotifications(request);
1312 NS_IMETHODIMP nsExternalAppHandler::SetWebProgressListener(
1313 nsIWebProgressListener2* aWebProgressListener) {
1314 // This is always called by nsHelperDlg.js. Go ahead and register the
1315 // progress listener. At this point, we don't have mTransfer.
1316 mDialogProgressListener = aWebProgressListener;
1317 return NS_OK;
1320 NS_IMETHODIMP nsExternalAppHandler::GetTargetFile(nsIFile** aTarget) {
1321 if (mFinalFileDestination)
1322 *aTarget = mFinalFileDestination;
1323 else
1324 *aTarget = mTempFile;
1326 NS_IF_ADDREF(*aTarget);
1327 return NS_OK;
1330 NS_IMETHODIMP nsExternalAppHandler::GetTargetFileIsExecutable(bool* aExec) {
1331 // Use the real target if it's been set
1332 if (mFinalFileDestination) return mFinalFileDestination->IsExecutable(aExec);
1334 // Otherwise, use the stored executable-ness of the temporary
1335 *aExec = mTempFileIsExecutable;
1336 return NS_OK;
1339 NS_IMETHODIMP nsExternalAppHandler::GetTimeDownloadStarted(PRTime* aTime) {
1340 *aTime = mTimeDownloadStarted;
1341 return NS_OK;
1344 NS_IMETHODIMP nsExternalAppHandler::GetContentLength(int64_t* aContentLength) {
1345 *aContentLength = mContentLength;
1346 return NS_OK;
1349 NS_IMETHODIMP nsExternalAppHandler::GetBrowsingContextId(
1350 uint64_t* aBrowsingContextId) {
1351 *aBrowsingContextId = mBrowsingContext ? mBrowsingContext->Id() : 0;
1352 return NS_OK;
1355 void nsExternalAppHandler::RetargetLoadNotifications(nsIRequest* request) {
1356 // we are going to run the downloading of the helper app in our own little
1357 // docloader / load group context. so go ahead and force the creation of a
1358 // load group and doc loader for us to use...
1359 nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
1360 if (!aChannel) return;
1362 bool isPrivate = NS_UsePrivateBrowsing(aChannel);
1364 nsCOMPtr<nsILoadGroup> oldLoadGroup;
1365 aChannel->GetLoadGroup(getter_AddRefs(oldLoadGroup));
1367 if (oldLoadGroup) {
1368 oldLoadGroup->RemoveRequest(request, nullptr, NS_BINDING_RETARGETED);
1371 aChannel->SetLoadGroup(nullptr);
1372 aChannel->SetNotificationCallbacks(nullptr);
1374 nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel);
1375 if (pbChannel) {
1376 pbChannel->SetPrivate(isPrivate);
1380 nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) {
1381 // First we need to try to get the destination directory for the temporary
1382 // file.
1383 nsresult rv = GetDownloadDirectory(getter_AddRefs(mTempFile));
1384 NS_ENSURE_SUCCESS(rv, rv);
1386 // At this point, we do not have a filename for the temp file. For security
1387 // purposes, this cannot be predictable, so we must use a cryptographic
1388 // quality PRNG to generate one.
1389 nsAutoCString tempLeafName;
1390 rv = GenerateRandomName(tempLeafName);
1391 NS_ENSURE_SUCCESS(rv, rv);
1393 // now append our extension.
1394 nsAutoCString ext;
1395 mMimeInfo->GetPrimaryExtension(ext);
1396 if (!ext.IsEmpty()) {
1397 ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
1398 if (ext.First() != '.') tempLeafName.Append('.');
1399 tempLeafName.Append(ext);
1402 // We need to temporarily create a dummy file with the correct
1403 // file extension to determine the executable-ness, so do this before adding
1404 // the extra .part extension.
1405 nsCOMPtr<nsIFile> dummyFile;
1406 rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dummyFile));
1407 NS_ENSURE_SUCCESS(rv, rv);
1409 // Set the file name without .part
1410 rv = dummyFile->Append(NS_ConvertUTF8toUTF16(tempLeafName));
1411 NS_ENSURE_SUCCESS(rv, rv);
1412 rv = dummyFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
1413 NS_ENSURE_SUCCESS(rv, rv);
1415 // Store executable-ness then delete
1416 dummyFile->IsExecutable(&mTempFileIsExecutable);
1417 dummyFile->Remove(false);
1419 // Add an additional .part to prevent the OS from running this file in the
1420 // default application.
1421 tempLeafName.AppendLiteral(".part");
1423 rv = mTempFile->Append(NS_ConvertUTF8toUTF16(tempLeafName));
1424 // make this file unique!!!
1425 NS_ENSURE_SUCCESS(rv, rv);
1426 rv = mTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
1427 NS_ENSURE_SUCCESS(rv, rv);
1429 // Now save the temp leaf name, minus the ".part" bit, so we can use it later.
1430 // This is a bit broken in the case when createUnique actually had to append
1431 // some numbers, because then we now have a filename like foo.bar-1.part and
1432 // we'll end up with foo.bar-1.bar as our final filename if we end up using
1433 // this. But the other options are all bad too.... Ideally we'd have a way
1434 // to tell createUnique to put its unique marker before the extension that
1435 // comes before ".part" or something.
1436 rv = mTempFile->GetLeafName(mTempLeafName);
1437 NS_ENSURE_SUCCESS(rv, rv);
1439 NS_ENSURE_TRUE(StringEndsWith(mTempLeafName, u".part"_ns),
1440 NS_ERROR_UNEXPECTED);
1442 // Strip off the ".part" from mTempLeafName
1443 mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1);
1445 MOZ_ASSERT(!mSaver, "Output file initialization called more than once!");
1446 mSaver =
1447 do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv);
1448 NS_ENSURE_SUCCESS(rv, rv);
1450 rv = mSaver->SetObserver(this);
1451 if (NS_FAILED(rv)) {
1452 mSaver = nullptr;
1453 return rv;
1456 rv = mSaver->EnableSha256();
1457 NS_ENSURE_SUCCESS(rv, rv);
1459 rv = mSaver->EnableSignatureInfo();
1460 NS_ENSURE_SUCCESS(rv, rv);
1461 LOG("Enabled hashing and signature verification");
1463 rv = mSaver->SetTarget(mTempFile, false);
1464 NS_ENSURE_SUCCESS(rv, rv);
1466 return rv;
1469 void nsExternalAppHandler::MaybeApplyDecodingForExtension(
1470 nsIRequest* aRequest) {
1471 MOZ_ASSERT(aRequest);
1473 nsCOMPtr<nsIEncodedChannel> encChannel = do_QueryInterface(aRequest);
1474 if (!encChannel) {
1475 return;
1478 // Turn off content encoding conversions if needed
1479 bool applyConversion = true;
1481 // First, check to see if conversion is already disabled. If so, we
1482 // have nothing to do here.
1483 encChannel->GetApplyConversion(&applyConversion);
1484 if (!applyConversion) {
1485 return;
1488 nsCOMPtr<nsIURL> sourceURL(do_QueryInterface(mSourceUrl));
1489 if (sourceURL) {
1490 nsAutoCString extension;
1491 sourceURL->GetFileExtension(extension);
1492 if (!extension.IsEmpty()) {
1493 nsCOMPtr<nsIUTF8StringEnumerator> encEnum;
1494 encChannel->GetContentEncodings(getter_AddRefs(encEnum));
1495 if (encEnum) {
1496 bool hasMore;
1497 nsresult rv = encEnum->HasMore(&hasMore);
1498 if (NS_SUCCEEDED(rv) && hasMore) {
1499 nsAutoCString encType;
1500 rv = encEnum->GetNext(encType);
1501 if (NS_SUCCEEDED(rv) && !encType.IsEmpty()) {
1502 MOZ_ASSERT(mExtProtSvc);
1503 mExtProtSvc->ApplyDecodingForExtension(extension, encType,
1504 &applyConversion);
1511 encChannel->SetApplyConversion(applyConversion);
1514 already_AddRefed<nsIInterfaceRequestor>
1515 nsExternalAppHandler::GetDialogParent() {
1516 nsCOMPtr<nsIInterfaceRequestor> dialogParent = mWindowContext;
1518 if (!dialogParent && mBrowsingContext) {
1519 dialogParent = do_QueryInterface(mBrowsingContext->GetDOMWindow());
1521 if (!dialogParent && mBrowsingContext && XRE_IsParentProcess()) {
1522 RefPtr<Element> element = mBrowsingContext->Top()->GetEmbedderElement();
1523 if (element) {
1524 dialogParent = do_QueryInterface(element->OwnerDoc()->GetWindow());
1527 return dialogParent.forget();
1530 NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
1531 MOZ_ASSERT(request, "OnStartRequest without request?");
1533 // Set mTimeDownloadStarted here as the download has already started and
1534 // we want to record the start time before showing the filepicker.
1535 mTimeDownloadStarted = PR_Now();
1537 mRequest = request;
1539 nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
1541 nsresult rv;
1542 nsAutoCString MIMEType;
1543 if (mMimeInfo) {
1544 mMimeInfo->GetMIMEType(MIMEType);
1546 // Now get the URI
1547 if (aChannel) {
1548 aChannel->GetURI(getter_AddRefs(mSourceUrl));
1549 // HTTPS-Only/HTTPS-FirstMode tries to upgrade connections to https. Once
1550 // the download is in progress we set that flag so that timeout counter
1551 // measures do not kick in.
1552 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1553 bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
1554 if (nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(isPrivateWin) ||
1555 nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(isPrivateWin)) {
1556 uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
1557 httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_DOWNLOAD_IN_PROGRESS;
1558 loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
1562 if (!mForceSave && StaticPrefs::browser_download_enable_spam_prevention() &&
1563 IsDownloadSpam(aChannel)) {
1564 return NS_OK;
1567 mDownloadClassification =
1568 nsContentSecurityUtils::ClassifyDownload(aChannel, MIMEType);
1570 if (mDownloadClassification == nsITransfer::DOWNLOAD_FORBIDDEN) {
1571 // If the download is rated as forbidden,
1572 // cancel the request so no ui knows about this.
1573 mCanceled = true;
1574 request->Cancel(NS_ERROR_ABORT);
1575 return NS_OK;
1578 nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(request));
1579 mIsFileChannel = fileChan != nullptr;
1580 if (!mIsFileChannel) {
1581 // It's possible that this request came from the child process and the
1582 // file channel actually lives there. If this returns true, then our
1583 // mSourceUrl will be an nsIFileURL anyway.
1584 nsCOMPtr<dom::nsIExternalHelperAppParent> parent(
1585 do_QueryInterface(request));
1586 mIsFileChannel = parent && parent->WasFileChannel();
1589 // Get content length
1590 if (aChannel) {
1591 aChannel->GetContentLength(&mContentLength);
1594 if (mBrowsingContext) {
1595 mMaybeCloseWindowHelper = new MaybeCloseWindowHelper(mBrowsingContext);
1596 mMaybeCloseWindowHelper->SetShouldCloseWindow(mShouldCloseWindow);
1597 nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(request, &rv));
1598 // Determine whether a new window was opened specifically for this request
1599 if (props) {
1600 bool tmp = false;
1601 if (NS_SUCCEEDED(
1602 props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns, &tmp))) {
1603 mMaybeCloseWindowHelper->SetShouldCloseWindow(tmp);
1608 // retarget all load notifications to our docloader instead of the original
1609 // window's docloader...
1610 RetargetLoadNotifications(request);
1612 // Close the underlying DOMWindow if it was opened specifically for the
1613 // download. We don't run this in the content process, since we have
1614 // an instance running in the parent as well, which will handle this
1615 // if needed.
1616 if (!XRE_IsContentProcess() && mMaybeCloseWindowHelper) {
1617 mBrowsingContext = mMaybeCloseWindowHelper->MaybeCloseWindow();
1620 // In an IPC setting, we're allowing the child process, here, to make
1621 // decisions about decoding the channel (e.g. decompression). It will
1622 // still forward the decoded (uncompressed) data back to the parent.
1623 // Con: Uncompressed data means more IPC overhead.
1624 // Pros: ExternalHelperAppParent doesn't need to implement nsIEncodedChannel.
1625 // Parent process doesn't need to expect CPU time on decompression.
1626 MaybeApplyDecodingForExtension(aChannel);
1628 // At this point, the child process has done everything it can usefully do
1629 // for OnStartRequest.
1630 if (XRE_IsContentProcess()) {
1631 return NS_OK;
1634 rv = SetUpTempFile(aChannel);
1635 if (NS_FAILED(rv)) {
1636 nsresult transferError = rv;
1638 rv = CreateFailedTransfer();
1639 if (NS_FAILED(rv)) {
1640 LOG("Failed to create transfer to report failure."
1641 "Will fallback to prompter!");
1644 mCanceled = true;
1645 request->Cancel(transferError);
1647 nsAutoString path;
1648 if (mTempFile) mTempFile->GetPath(path);
1650 SendStatusChange(kWriteError, transferError, request, path);
1652 return NS_OK;
1655 // Inform channel it is open on behalf of a download to throttle it during
1656 // page loads and prevent its caching.
1657 nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(aChannel);
1658 if (httpInternal) {
1659 rv = httpInternal->SetChannelIsForDownload(true);
1660 MOZ_ASSERT(NS_SUCCEEDED(rv));
1663 if (mSourceUrl->SchemeIs("data")) {
1664 // In case we're downloading a data:// uri
1665 // we don't want to apply AllowTopLevelNavigationToDataURI.
1666 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1667 loadInfo->SetForceAllowDataURI(true);
1670 // now that the temp file is set up, find out if we need to invoke a dialog
1671 // asking the user what they want us to do with this content...
1673 // We can get here for three reasons: "can't handle", "sniffed type", or
1674 // "server sent content-disposition:attachment". In the first case we want
1675 // to honor the user's "always ask" pref; in the other two cases we want to
1676 // honor it only if the default action is "save". Opening attachments in
1677 // helper apps by default breaks some websites (especially if the attachment
1678 // is one part of a multipart document). Opening sniffed content in helper
1679 // apps by default introduces security holes that we'd rather not have.
1681 // So let's find out whether the user wants to be prompted. If he does not,
1682 // check mReason and the preferred action to see what we should do.
1684 bool alwaysAsk = true;
1685 mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk);
1686 if (alwaysAsk) {
1687 // But we *don't* ask if this mimeInfo didn't come from
1688 // our user configuration datastore and the user has said
1689 // at some point in the distant past that they don't
1690 // want to be asked. The latter fact would have been
1691 // stored in pref strings back in the old days.
1693 bool mimeTypeIsInDatastore = false;
1694 nsCOMPtr<nsIHandlerService> handlerSvc =
1695 do_GetService(NS_HANDLERSERVICE_CONTRACTID);
1696 if (handlerSvc) {
1697 handlerSvc->Exists(mMimeInfo, &mimeTypeIsInDatastore);
1699 if (!handlerSvc || !mimeTypeIsInDatastore) {
1700 if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_SAVE_TO_DISK_PREF,
1701 MIMEType.get())) {
1702 // Don't need to ask after all.
1703 alwaysAsk = false;
1704 // Make sure action matches pref (save to disk).
1705 mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
1706 } else if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_OPEN_FILE_PREF,
1707 MIMEType.get())) {
1708 // Don't need to ask after all.
1709 alwaysAsk = false;
1712 } else if (MIMEType.EqualsLiteral("text/plain")) {
1713 nsAutoCString ext;
1714 mMimeInfo->GetPrimaryExtension(ext);
1715 // If people are sending us ApplicationReputation-eligible files with
1716 // text/plain mimetypes, enforce asking the user what to do.
1717 if (!ext.IsEmpty()) {
1718 nsAutoCString dummyFileName("f");
1719 if (ext.First() != '.') {
1720 dummyFileName.Append(".");
1722 ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
1723 nsCOMPtr<nsIApplicationReputationService> appRep =
1724 components::ApplicationReputation::Service();
1725 appRep->IsBinary(dummyFileName + ext, &alwaysAsk);
1729 int32_t action = nsIMIMEInfo::saveToDisk;
1730 mMimeInfo->GetPreferredAction(&action);
1732 bool forcePrompt = mReason == nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
1734 // OK, now check why we're here
1735 if (!alwaysAsk && forcePrompt) {
1736 // Force asking if we're not saving. See comment back when we fetched the
1737 // alwaysAsk boolean for details.
1738 alwaysAsk = (action != nsIMIMEInfo::saveToDisk);
1741 bool shouldAutomaticallyHandleInternally =
1742 action == nsIMIMEInfo::handleInternally;
1744 if (aChannel) {
1745 uint32_t disposition = -1;
1746 aChannel->GetContentDisposition(&disposition);
1747 mForceSaveInternallyHandled =
1748 shouldAutomaticallyHandleInternally &&
1749 disposition == nsIChannel::DISPOSITION_ATTACHMENT &&
1750 mozilla::StaticPrefs::
1751 browser_download_force_save_internally_handled_attachments();
1754 // If we're not asking, check we actually know what to do:
1755 if (!alwaysAsk) {
1756 alwaysAsk = action != nsIMIMEInfo::saveToDisk &&
1757 action != nsIMIMEInfo::useHelperApp &&
1758 action != nsIMIMEInfo::useSystemDefault &&
1759 !shouldAutomaticallyHandleInternally;
1762 // If we're handling with the OS default and we are that default, force
1763 // asking, so we don't end up in an infinite loop:
1764 if (!alwaysAsk && action == nsIMIMEInfo::useSystemDefault) {
1765 bool areOSDefault = false;
1766 alwaysAsk = NS_SUCCEEDED(mMimeInfo->IsCurrentAppOSDefault(&areOSDefault)) &&
1767 areOSDefault;
1768 } else if (!alwaysAsk && action == nsIMIMEInfo::useHelperApp) {
1769 nsCOMPtr<nsIHandlerApp> preferredApp;
1770 mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(preferredApp));
1771 nsCOMPtr<nsILocalHandlerApp> handlerApp = do_QueryInterface(preferredApp);
1772 if (handlerApp) {
1773 nsCOMPtr<nsIFile> executable;
1774 handlerApp->GetExecutable(getter_AddRefs(executable));
1775 nsCOMPtr<nsIFile> ourselves;
1776 if (executable &&
1777 // Despite the name, this really just fetches an nsIFile...
1778 NS_SUCCEEDED(NS_GetSpecialDirectory(XRE_EXECUTABLE_FILE,
1779 getter_AddRefs(ourselves)))) {
1780 ourselves = nsMIMEInfoBase::GetCanonicalExecutable(ourselves);
1781 executable = nsMIMEInfoBase::GetCanonicalExecutable(executable);
1782 bool isSameApp = false;
1783 alwaysAsk =
1784 NS_FAILED(executable->Equals(ourselves, &isSameApp)) || isSameApp;
1789 // if we were told that we _must_ save to disk without asking, all the stuff
1790 // before this is irrelevant; override it
1791 if (mForceSave || mForceSaveInternallyHandled) {
1792 alwaysAsk = false;
1793 action = nsIMIMEInfo::saveToDisk;
1794 shouldAutomaticallyHandleInternally = false;
1796 // Additionally, if we are asked by the OS to open a local file,
1797 // automatically downloading it to create a second copy of that file doesn't
1798 // really make sense. We should ask the user what they want to do.
1799 if (mSourceUrl->SchemeIs("file") && !alwaysAsk &&
1800 action == nsIMIMEInfo::saveToDisk) {
1801 alwaysAsk = true;
1804 // If adding new checks, make sure this is the last check before telemetry
1805 // and going ahead with opening the file!
1806 #ifdef XP_WIN
1807 /* We need to see whether the file we've got here could be
1808 * executable. If it could, we had better not try to open it!
1809 * We can skip this check, though, if we have a setting to open in a
1810 * helper app.
1812 if (!alwaysAsk && action != nsIMIMEInfo::saveToDisk &&
1813 !shouldAutomaticallyHandleInternally) {
1814 nsCOMPtr<nsIHandlerApp> prefApp;
1815 mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(prefApp));
1816 if (action != nsIMIMEInfo::useHelperApp || !prefApp) {
1817 nsCOMPtr<nsIFile> fileToTest;
1818 GetTargetFile(getter_AddRefs(fileToTest));
1819 if (fileToTest) {
1820 bool isExecutable;
1821 rv = fileToTest->IsExecutable(&isExecutable);
1822 if (NS_FAILED(rv) || mTempFileIsExecutable ||
1823 isExecutable) { // checking NS_FAILED, because paranoia is good
1824 alwaysAsk = true;
1826 } else { // Paranoia is good here too, though this really should not
1827 // happen
1828 NS_WARNING(
1829 "GetDownloadInfo returned a null file after the temp file has been "
1830 "set up! ");
1831 alwaysAsk = true;
1835 #endif
1837 if (alwaysAsk) {
1838 // Display the dialog
1839 mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv);
1840 NS_ENSURE_SUCCESS(rv, rv);
1842 // this will create a reference cycle (the dialog holds a reference to us as
1843 // nsIHelperAppLauncher), which will be broken in Cancel or CreateTransfer.
1844 nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
1845 // Don't pop up the downloads panel since we're already going to pop up the
1846 // UCT dialog for basically the same effect.
1847 mDialogShowing = true;
1848 rv = mDialog->Show(this, dialogParent, mReason);
1850 // what do we do if the dialog failed? I guess we should call Cancel and
1851 // abort the load....
1852 } else {
1853 // We need to do the save/open immediately, then.
1854 if (action == nsIMIMEInfo::useHelperApp ||
1855 action == nsIMIMEInfo::useSystemDefault ||
1856 shouldAutomaticallyHandleInternally) {
1857 // Check if the file is local, in which case just launch it from where it
1858 // is. Otherwise, set the file to launch once it's finished downloading.
1859 rv = mIsFileChannel ? LaunchLocalFile()
1860 : SetDownloadToLaunch(
1861 shouldAutomaticallyHandleInternally, nullptr);
1862 } else {
1863 rv = PromptForSaveDestination();
1866 return NS_OK;
1869 bool nsExternalAppHandler::IsDownloadSpam(nsIChannel* aChannel) {
1870 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1871 nsCOMPtr<nsIPermissionManager> permissionManager =
1872 mozilla::services::GetPermissionManager();
1873 nsCOMPtr<nsIPrincipal> principal = loadInfo->TriggeringPrincipal();
1874 bool exactHostMatch = false;
1875 constexpr auto type = "automatic-download"_ns;
1876 nsCOMPtr<nsIPermission> permission;
1878 permissionManager->GetPermissionObject(principal, type, exactHostMatch,
1879 getter_AddRefs(permission));
1881 if (permission) {
1882 uint32_t capability;
1883 permission->GetCapability(&capability);
1884 if (capability == nsIPermissionManager::DENY_ACTION) {
1885 mCanceled = true;
1886 aChannel->Cancel(NS_ERROR_ABORT);
1887 return true;
1889 if (capability == nsIPermissionManager::ALLOW_ACTION) {
1890 return false;
1892 // If no action is set (i.e: null), we set PROMPT_ACTION by default,
1893 // which will notify the Downloads UI to open the panel on the next request.
1894 if (capability == nsIPermissionManager::PROMPT_ACTION) {
1895 nsCOMPtr<nsIObserverService> observerService =
1896 mozilla::services::GetObserverService();
1897 RefPtr<BrowsingContext> browsingContext;
1898 loadInfo->GetBrowsingContext(getter_AddRefs(browsingContext));
1900 nsAutoCString cStringURI;
1901 loadInfo->TriggeringPrincipal()->GetPrePath(cStringURI);
1902 observerService->NotifyObservers(
1903 browsingContext, "blocked-automatic-download",
1904 NS_ConvertASCIItoUTF16(cStringURI.get()).get());
1905 // FIXME: In order to escape memory leaks, currently we cancel blocked
1906 // downloads. This is temporary solution, because download data should be
1907 // kept in order to restart the blocked download.
1908 mCanceled = true;
1909 aChannel->Cancel(NS_ERROR_ABORT);
1910 // End cancel
1911 return true;
1914 if (!loadInfo->GetHasValidUserGestureActivation()) {
1915 permissionManager->AddFromPrincipal(
1916 principal, type, nsIPermissionManager::PROMPT_ACTION,
1917 nsIPermissionManager::EXPIRE_NEVER, 0 /* expire time */);
1920 return false;
1923 // Convert error info into proper message text and send OnStatusChange
1924 // notification to the dialog progress listener or nsITransfer implementation.
1925 void nsExternalAppHandler::SendStatusChange(ErrorType type, nsresult rv,
1926 nsIRequest* aRequest,
1927 const nsString& path) {
1928 const char* msgId = nullptr;
1929 switch (rv) {
1930 case NS_ERROR_OUT_OF_MEMORY:
1931 // No memory
1932 msgId = "noMemory";
1933 break;
1935 case NS_ERROR_FILE_NO_DEVICE_SPACE:
1936 // Out of space on target volume.
1937 msgId = "diskFull";
1938 break;
1940 case NS_ERROR_FILE_READ_ONLY:
1941 // Attempt to write to read/only file.
1942 msgId = "readOnly";
1943 break;
1945 case NS_ERROR_FILE_ACCESS_DENIED:
1946 if (type == kWriteError) {
1947 // Attempt to write without sufficient permissions.
1948 #if defined(ANDROID)
1949 // On Android this means the SD card is present but
1950 // unavailable (read-only).
1951 msgId = "SDAccessErrorCardReadOnly";
1952 #else
1953 msgId = "accessError";
1954 #endif
1955 } else {
1956 msgId = "launchError";
1958 break;
1960 case NS_ERROR_FILE_NOT_FOUND:
1961 case NS_ERROR_FILE_UNRECOGNIZED_PATH:
1962 // Helper app not found, let's verify this happened on launch
1963 if (type == kLaunchError) {
1964 msgId = "helperAppNotFound";
1965 break;
1967 #if defined(ANDROID)
1968 else if (type == kWriteError) {
1969 // On Android this means the SD card is missing (not in
1970 // SD slot).
1971 msgId = "SDAccessErrorCardMissing";
1972 break;
1974 #endif
1975 [[fallthrough]];
1977 default:
1978 // Generic read/write/launch error message.
1979 switch (type) {
1980 case kReadError:
1981 msgId = "readError";
1982 break;
1983 case kWriteError:
1984 msgId = "writeError";
1985 break;
1986 case kLaunchError:
1987 msgId = "launchError";
1988 break;
1990 break;
1993 MOZ_LOG(
1994 nsExternalHelperAppService::sLog, LogLevel::Error,
1995 ("Error: %s, type=%i, listener=0x%p, transfer=0x%p, rv=0x%08" PRIX32 "\n",
1996 msgId, type, mDialogProgressListener.get(), mTransfer.get(),
1997 static_cast<uint32_t>(rv)));
1999 MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Error,
2000 (" path='%s'\n", NS_ConvertUTF16toUTF8(path).get()));
2002 // Get properties file bundle and extract status string.
2003 nsCOMPtr<nsIStringBundleService> stringService =
2004 mozilla::components::StringBundle::Service();
2005 if (stringService) {
2006 nsCOMPtr<nsIStringBundle> bundle;
2007 if (NS_SUCCEEDED(stringService->CreateBundle(
2008 "chrome://global/locale/nsWebBrowserPersist.properties",
2009 getter_AddRefs(bundle)))) {
2010 nsAutoString msgText;
2011 AutoTArray<nsString, 1> strings = {path};
2012 if (NS_SUCCEEDED(bundle->FormatStringFromName(msgId, strings, msgText))) {
2013 if (mDialogProgressListener) {
2014 // We have a listener, let it handle the error.
2015 mDialogProgressListener->OnStatusChange(
2016 nullptr, (type == kReadError) ? aRequest : nullptr, rv,
2017 msgText.get());
2018 } else if (mTransfer) {
2019 mTransfer->OnStatusChange(nullptr,
2020 (type == kReadError) ? aRequest : nullptr,
2021 rv, msgText.get());
2022 } else if (XRE_IsParentProcess()) {
2023 // We don't have a listener. Simply show the alert ourselves.
2024 nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
2025 nsresult qiRv;
2026 nsCOMPtr<nsIPrompt> prompter(do_GetInterface(dialogParent, &qiRv));
2027 nsAutoString title;
2028 bundle->FormatStringFromName("title", strings, title);
2030 MOZ_LOG(
2031 nsExternalHelperAppService::sLog, LogLevel::Debug,
2032 ("mBrowsingContext=0x%p, prompter=0x%p, qi rv=0x%08" PRIX32
2033 ", title='%s', msg='%s'",
2034 mBrowsingContext.get(), prompter.get(),
2035 static_cast<uint32_t>(qiRv), NS_ConvertUTF16toUTF8(title).get(),
2036 NS_ConvertUTF16toUTF8(msgText).get()));
2038 // If we didn't have a prompter we will try and get a window
2039 // instead, get it's docshell and use it to alert the user.
2040 if (!prompter) {
2041 nsCOMPtr<nsPIDOMWindowOuter> window(do_GetInterface(dialogParent));
2042 if (!window || !window->GetDocShell()) {
2043 return;
2046 prompter = do_GetInterface(window->GetDocShell(), &qiRv);
2048 MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Debug,
2049 ("No prompter from mBrowsingContext, using DocShell, "
2050 "window=0x%p, docShell=0x%p, "
2051 "prompter=0x%p, qi rv=0x%08" PRIX32,
2052 window.get(), window->GetDocShell(), prompter.get(),
2053 static_cast<uint32_t>(qiRv)));
2055 // If we still don't have a prompter, there's nothing else we
2056 // can do so just return.
2057 if (!prompter) {
2058 MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Error,
2059 ("No prompter from DocShell, no way to alert user"));
2060 return;
2064 // We should always have a prompter at this point.
2065 prompter->Alert(title.get(), msgText.get());
2072 NS_IMETHODIMP
2073 nsExternalAppHandler::OnDataAvailable(nsIRequest* request,
2074 nsIInputStream* inStr,
2075 uint64_t sourceOffset, uint32_t count) {
2076 nsresult rv = NS_OK;
2077 // first, check to see if we've been canceled....
2078 if (mCanceled || !mSaver) {
2079 // then go cancel our underlying channel too
2080 return request->Cancel(NS_BINDING_ABORTED);
2083 // read the data out of the stream and write it to the temp file.
2084 if (count > 0) {
2085 mProgress += count;
2087 nsCOMPtr<nsIStreamListener> saver = do_QueryInterface(mSaver);
2088 rv = saver->OnDataAvailable(request, inStr, sourceOffset, count);
2089 if (NS_SUCCEEDED(rv)) {
2090 // Send progress notification.
2091 if (mTransfer) {
2092 mTransfer->OnProgressChange64(nullptr, request, mProgress,
2093 mContentLength, mProgress,
2094 mContentLength);
2096 } else {
2097 // An error occurred, notify listener.
2098 nsAutoString tempFilePath;
2099 if (mTempFile) {
2100 mTempFile->GetPath(tempFilePath);
2102 SendStatusChange(kReadError, rv, request, tempFilePath);
2104 // Cancel the download.
2105 Cancel(rv);
2108 return rv;
2111 NS_IMETHODIMP nsExternalAppHandler::OnStopRequest(nsIRequest* request,
2112 nsresult aStatus) {
2113 LOG("nsExternalAppHandler::OnStopRequest\n"
2114 " mCanceled=%d, mTransfer=0x%p, aStatus=0x%08" PRIX32 "\n",
2115 mCanceled, mTransfer.get(), static_cast<uint32_t>(aStatus));
2117 mStopRequestIssued = true;
2119 // Cancel if the request did not complete successfully.
2120 if (!mCanceled && NS_FAILED(aStatus)) {
2121 // Send error notification.
2122 nsAutoString tempFilePath;
2123 if (mTempFile) mTempFile->GetPath(tempFilePath);
2124 SendStatusChange(kReadError, aStatus, request, tempFilePath);
2126 Cancel(aStatus);
2129 // first, check to see if we've been canceled....
2130 if (mCanceled || !mSaver) {
2131 return NS_OK;
2134 return mSaver->Finish(NS_OK);
2137 NS_IMETHODIMP
2138 nsExternalAppHandler::OnTargetChange(nsIBackgroundFileSaver* aSaver,
2139 nsIFile* aTarget) {
2140 return NS_OK;
2143 NS_IMETHODIMP
2144 nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver,
2145 nsresult aStatus) {
2146 LOG("nsExternalAppHandler::OnSaveComplete\n"
2147 " aSaver=0x%p, aStatus=0x%08" PRIX32 ", mCanceled=%d, mTransfer=0x%p\n",
2148 aSaver, static_cast<uint32_t>(aStatus), mCanceled, mTransfer.get());
2150 if (!mCanceled) {
2151 // Save the hash and signature information
2152 (void)mSaver->GetSha256Hash(mHash);
2153 (void)mSaver->GetSignatureInfo(mSignatureInfo);
2155 // Free the reference that the saver keeps on us, even if we couldn't get
2156 // the hash.
2157 mSaver = nullptr;
2159 // Save the redirect information.
2160 nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2161 if (channel) {
2162 nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
2163 nsresult rv = NS_OK;
2164 nsCOMPtr<nsIMutableArray> redirectChain =
2165 do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
2166 NS_ENSURE_SUCCESS(rv, rv);
2167 LOG("nsExternalAppHandler: Got %zu redirects\n",
2168 loadInfo->RedirectChain().Length());
2169 for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) {
2170 redirectChain->AppendElement(entry);
2172 mRedirects = redirectChain;
2175 if (NS_FAILED(aStatus)) {
2176 nsAutoString path;
2177 mTempFile->GetPath(path);
2179 // It may happen when e10s is enabled that there will be no transfer
2180 // object available to communicate status as expected by the system.
2181 // Let's try and create a temporary transfer object to take care of this
2182 // for us, we'll fall back to using the prompt service if we absolutely
2183 // have to.
2184 if (!mTransfer) {
2185 // We don't care if this fails.
2186 CreateFailedTransfer();
2189 SendStatusChange(kWriteError, aStatus, nullptr, path);
2190 if (!mCanceled) Cancel(aStatus);
2191 return NS_OK;
2195 // Notify the transfer object that we are done if the user has chosen an
2196 // action. If the user hasn't chosen an action, the progress listener
2197 // (nsITransfer) will be notified in CreateTransfer.
2198 if (mTransfer) {
2199 NotifyTransfer(aStatus);
2202 return NS_OK;
2205 void nsExternalAppHandler::NotifyTransfer(nsresult aStatus) {
2206 MOZ_ASSERT(NS_IsMainThread(), "Must notify on main thread");
2207 MOZ_ASSERT(mTransfer, "We must have an nsITransfer");
2209 LOG("Notifying progress listener");
2211 if (NS_SUCCEEDED(aStatus)) {
2212 (void)mTransfer->SetSha256Hash(mHash);
2213 (void)mTransfer->SetSignatureInfo(mSignatureInfo);
2214 (void)mTransfer->SetRedirects(mRedirects);
2215 (void)mTransfer->OnProgressChange64(
2216 nullptr, nullptr, mProgress, mContentLength, mProgress, mContentLength);
2219 (void)mTransfer->OnStateChange(nullptr, nullptr,
2220 nsIWebProgressListener::STATE_STOP |
2221 nsIWebProgressListener::STATE_IS_REQUEST |
2222 nsIWebProgressListener::STATE_IS_NETWORK,
2223 aStatus);
2225 // This nsITransfer object holds a reference to us (we are its observer), so
2226 // we need to release the reference to break a reference cycle (and therefore
2227 // to prevent leaking). We do this even if the previous calls failed.
2228 mTransfer = nullptr;
2231 NS_IMETHODIMP nsExternalAppHandler::GetMIMEInfo(nsIMIMEInfo** aMIMEInfo) {
2232 *aMIMEInfo = mMimeInfo;
2233 NS_ADDREF(*aMIMEInfo);
2234 return NS_OK;
2237 NS_IMETHODIMP nsExternalAppHandler::GetSource(nsIURI** aSourceURI) {
2238 NS_ENSURE_ARG(aSourceURI);
2239 *aSourceURI = mSourceUrl;
2240 NS_IF_ADDREF(*aSourceURI);
2241 return NS_OK;
2244 NS_IMETHODIMP nsExternalAppHandler::GetSuggestedFileName(
2245 nsAString& aSuggestedFileName) {
2246 aSuggestedFileName = mSuggestedFileName;
2247 return NS_OK;
2250 nsresult nsExternalAppHandler::CreateTransfer() {
2251 LOG("nsExternalAppHandler::CreateTransfer");
2253 MOZ_ASSERT(NS_IsMainThread(), "Must create transfer on main thread");
2254 // We are back from the helper app dialog (where the user chooses to save or
2255 // open), but we aren't done processing the load. in this case, throw up a
2256 // progress dialog so the user can see what's going on.
2257 // Also, release our reference to mDialog. We don't need it anymore, and we
2258 // need to break the reference cycle.
2259 mDialog = nullptr;
2260 if (!mDialogProgressListener) {
2261 NS_WARNING("The dialog should nullify the dialog progress listener");
2263 // In case of a non acceptable download, we need to cancel the request and
2264 // pass a FailedTransfer for the Download UI.
2265 if (mDownloadClassification != nsITransfer::DOWNLOAD_ACCEPTABLE) {
2266 mCanceled = true;
2267 mRequest->Cancel(NS_ERROR_ABORT);
2268 if (mSaver) {
2269 mSaver->Finish(NS_ERROR_ABORT);
2270 mSaver = nullptr;
2272 return CreateFailedTransfer();
2274 nsresult rv;
2276 // We must be able to create an nsITransfer object. If not, it doesn't matter
2277 // much that we can't launch the helper application or save to disk. Work on
2278 // a local copy rather than mTransfer until we know we succeeded, to make it
2279 // clearer that this function is re-entrant.
2280 nsCOMPtr<nsITransfer> transfer =
2281 do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
2282 NS_ENSURE_SUCCESS(rv, rv);
2284 // Initialize the download
2285 nsCOMPtr<nsIURI> target;
2286 rv = NS_NewFileURI(getter_AddRefs(target), mFinalFileDestination);
2287 NS_ENSURE_SUCCESS(rv, rv);
2289 nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2290 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mRequest);
2291 nsCOMPtr<nsIReferrerInfo> referrerInfo = nullptr;
2292 if (httpChannel) {
2293 referrerInfo = httpChannel->GetReferrerInfo();
2296 if (mBrowsingContext) {
2297 rv = transfer->InitWithBrowsingContext(
2298 mSourceUrl, target, u""_ns, mMimeInfo, mTimeDownloadStarted, mTempFile,
2299 this, channel && NS_UsePrivateBrowsing(channel),
2300 mDownloadClassification, referrerInfo, !mDialogShowing,
2301 mBrowsingContext, mHandleInternally, nullptr);
2302 } else {
2303 rv = transfer->Init(mSourceUrl, nullptr, target, u""_ns, mMimeInfo,
2304 mTimeDownloadStarted, mTempFile, this,
2305 channel && NS_UsePrivateBrowsing(channel),
2306 mDownloadClassification, referrerInfo, !mDialogShowing);
2308 mDialogShowing = false;
2310 NS_ENSURE_SUCCESS(rv, rv);
2312 // If we were cancelled since creating the transfer, just return. It is
2313 // always ok to return NS_OK if we are cancelled. Callers of this function
2314 // must call Cancel if CreateTransfer fails, but there's no need to cancel
2315 // twice.
2316 if (mCanceled) {
2317 return NS_OK;
2319 rv = transfer->OnStateChange(nullptr, mRequest,
2320 nsIWebProgressListener::STATE_START |
2321 nsIWebProgressListener::STATE_IS_REQUEST |
2322 nsIWebProgressListener::STATE_IS_NETWORK,
2323 NS_OK);
2324 NS_ENSURE_SUCCESS(rv, rv);
2326 if (mCanceled) {
2327 return NS_OK;
2330 mRequest = nullptr;
2331 // Finally, save the transfer to mTransfer.
2332 mTransfer = transfer;
2333 transfer = nullptr;
2335 // While we were bringing up the progress dialog, we actually finished
2336 // processing the url. If that's the case then mStopRequestIssued will be
2337 // true and OnSaveComplete has been called.
2338 if (mStopRequestIssued && !mSaver && mTransfer) {
2339 NotifyTransfer(NS_OK);
2342 return rv;
2345 nsresult nsExternalAppHandler::CreateFailedTransfer() {
2346 nsresult rv;
2347 nsCOMPtr<nsITransfer> transfer =
2348 do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
2349 NS_ENSURE_SUCCESS(rv, rv);
2351 // We won't pass the temp file to the transfer, so if we have one it needs to
2352 // get deleted now.
2353 if (mTempFile) {
2354 if (mSaver) {
2355 mSaver->Finish(NS_BINDING_ABORTED);
2356 mSaver = nullptr;
2358 mTempFile->Remove(false);
2361 nsCOMPtr<nsIURI> pseudoTarget;
2362 if (!mFinalFileDestination) {
2363 // If we don't have a download directory we're kinda screwed but it's OK
2364 // we'll still report the error via the prompter.
2365 nsCOMPtr<nsIFile> pseudoFile;
2366 rv = GetDownloadDirectory(getter_AddRefs(pseudoFile), true);
2367 NS_ENSURE_SUCCESS(rv, rv);
2369 // Append the default suggested filename. If the user restarts the transfer
2370 // we will re-trigger a filename check anyway to ensure that it is unique.
2371 rv = pseudoFile->Append(mSuggestedFileName);
2372 NS_ENSURE_SUCCESS(rv, rv);
2374 rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), pseudoFile);
2375 NS_ENSURE_SUCCESS(rv, rv);
2376 } else {
2377 // Initialize the target, if present
2378 rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), mFinalFileDestination);
2379 NS_ENSURE_SUCCESS(rv, rv);
2382 nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2383 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mRequest);
2384 nsCOMPtr<nsIReferrerInfo> referrerInfo = nullptr;
2385 if (httpChannel) {
2386 referrerInfo = httpChannel->GetReferrerInfo();
2389 if (mBrowsingContext) {
2390 rv = transfer->InitWithBrowsingContext(
2391 mSourceUrl, pseudoTarget, u""_ns, mMimeInfo, mTimeDownloadStarted,
2392 mTempFile, this, channel && NS_UsePrivateBrowsing(channel),
2393 mDownloadClassification, referrerInfo, true, mBrowsingContext,
2394 mHandleInternally, httpChannel);
2395 } else {
2396 rv = transfer->Init(mSourceUrl, nullptr, pseudoTarget, u""_ns, mMimeInfo,
2397 mTimeDownloadStarted, mTempFile, this,
2398 channel && NS_UsePrivateBrowsing(channel),
2399 mDownloadClassification, referrerInfo, true);
2401 NS_ENSURE_SUCCESS(rv, rv);
2403 // Our failed transfer is ready.
2404 mTransfer = std::move(transfer);
2406 return NS_OK;
2409 nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile* aFile,
2410 bool aDialogWasShown) {
2411 if (aFile) {
2412 if (aDialogWasShown) {
2413 mDialogShowing = true;
2415 ContinueSave(aFile);
2416 } else {
2417 Cancel(NS_BINDING_ABORTED);
2420 return NS_OK;
2423 void nsExternalAppHandler::RequestSaveDestination(
2424 const nsString& aDefaultFile, const nsString& aFileExtension) {
2425 // Display the dialog
2426 // XXX Convert to use file picker? No, then embeddors could not do any sort of
2427 // "AutoDownload" w/o showing a prompt
2428 nsresult rv = NS_OK;
2429 if (!mDialog) {
2430 // Get helper app launcher dialog.
2431 mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv);
2432 if (rv != NS_OK) {
2433 Cancel(NS_BINDING_ABORTED);
2434 return;
2438 // we want to explicitly unescape aDefaultFile b4 passing into the dialog. we
2439 // can't unescape it because the dialog is implemented by a JS component which
2440 // doesn't have a window so no unescape routine is defined...
2442 // Now, be sure to keep |this| alive, and the dialog
2443 // If we don't do this, users that close the helper app dialog while the file
2444 // picker is up would cause Cancel() to be called, and the dialog would be
2445 // released, which would release this object too, which would crash.
2446 // See Bug 249143
2447 RefPtr<nsExternalAppHandler> kungFuDeathGrip(this);
2448 nsCOMPtr<nsIHelperAppLauncherDialog> dlg(mDialog);
2449 nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
2451 rv = dlg->PromptForSaveToFileAsync(this, dialogParent, aDefaultFile.get(),
2452 aFileExtension.get(), mForceSave);
2453 if (NS_FAILED(rv)) {
2454 Cancel(NS_BINDING_ABORTED);
2458 // PromptForSaveDestination should only be called by the helper app dialog which
2459 // allows the user to say launch with application or save to disk.
2460 NS_IMETHODIMP nsExternalAppHandler::PromptForSaveDestination() {
2461 if (mCanceled) return NS_OK;
2463 if (mForceSave || mForceSaveInternallyHandled) {
2464 mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
2467 if (mSuggestedFileName.IsEmpty()) {
2468 RequestSaveDestination(mTempLeafName, mFileExtension);
2469 } else {
2470 nsAutoString fileExt;
2471 int32_t pos = mSuggestedFileName.RFindChar('.');
2472 if (pos >= 0) {
2473 mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos);
2475 if (fileExt.IsEmpty()) {
2476 fileExt = mFileExtension;
2479 RequestSaveDestination(mSuggestedFileName, fileExt);
2482 return NS_OK;
2484 nsresult nsExternalAppHandler::ContinueSave(nsIFile* aNewFileLocation) {
2485 if (mCanceled) return NS_OK;
2487 MOZ_ASSERT(aNewFileLocation, "Must be called with a non-null file");
2489 int32_t action = nsIMIMEInfo::saveToDisk;
2490 mMimeInfo->GetPreferredAction(&action);
2491 mHandleInternally = action == nsIMIMEInfo::handleInternally;
2493 nsresult rv = NS_OK;
2494 nsCOMPtr<nsIFile> fileToUse = aNewFileLocation;
2495 mFinalFileDestination = fileToUse;
2497 // Move what we have in the final directory, but append .part
2498 // to it, to indicate that it's unfinished. Do not call SetTarget on the
2499 // saver if we are done (Finish has been called) but OnSaverComplete has
2500 // not been called.
2501 if (mFinalFileDestination && mSaver && !mStopRequestIssued) {
2502 nsCOMPtr<nsIFile> movedFile;
2503 mFinalFileDestination->Clone(getter_AddRefs(movedFile));
2504 if (movedFile) {
2505 nsAutoCString randomChars;
2506 rv = GenerateRandomName(randomChars);
2507 if (NS_SUCCEEDED(rv)) {
2508 // Get the leaf name, strip any extensions, then
2509 // add random bytes, followed by the extensions and '.part'.
2510 nsAutoString leafName;
2511 mFinalFileDestination->GetLeafName(leafName);
2512 auto nameWithoutExtensionLength = leafName.FindChar('.');
2513 nsAutoString extensions(u"");
2514 if (nameWithoutExtensionLength == kNotFound) {
2515 nameWithoutExtensionLength = leafName.Length();
2516 } else {
2517 extensions = Substring(leafName, nameWithoutExtensionLength);
2519 leafName.Truncate(nameWithoutExtensionLength);
2521 nsAutoString suffix = u"."_ns + NS_ConvertASCIItoUTF16(randomChars) +
2522 extensions + u".part"_ns;
2523 #ifdef XP_WIN
2524 // Deal with MAX_PATH on Windows. Worth noting that the original
2525 // path for mFinalFileDestination must be valid for us to get
2526 // here: either SetDownloadToLaunch or the caller of
2527 // SaveDestinationAvailable has called CreateUnique or similar
2528 // to ensure both a unique name and one that isn't too long.
2529 // The only issue is we're making it longer to get the part
2530 // file path...
2531 nsAutoString path;
2532 mFinalFileDestination->GetPath(path);
2533 CheckedInt<uint16_t> fullPathLength =
2534 CheckedInt<uint16_t>(path.Length()) + 1 + randomChars.Length() +
2535 ArrayLength(".part");
2536 if (!fullPathLength.isValid()) {
2537 leafName.Truncate();
2538 } else if (fullPathLength.value() > MAX_PATH) {
2539 int32_t leafNameRemaining =
2540 (int32_t)leafName.Length() - (fullPathLength.value() - MAX_PATH);
2541 leafName.Truncate(std::max(leafNameRemaining, 0));
2543 #endif
2544 leafName.Append(suffix);
2545 movedFile->SetLeafName(leafName);
2547 rv = mSaver->SetTarget(movedFile, true);
2548 if (NS_FAILED(rv)) {
2549 nsAutoString path;
2550 mTempFile->GetPath(path);
2551 SendStatusChange(kWriteError, rv, nullptr, path);
2552 Cancel(rv);
2553 return NS_OK;
2556 mTempFile = movedFile;
2561 // The helper app dialog has told us what to do and we have a final file
2562 // destination.
2563 rv = CreateTransfer();
2564 // If we fail to create the transfer, Cancel.
2565 if (NS_FAILED(rv)) {
2566 Cancel(rv);
2567 return rv;
2570 return NS_OK;
2573 // SetDownloadToLaunch should only be called by the helper app dialog which
2574 // allows the user to say launch with application or save to disk.
2575 NS_IMETHODIMP nsExternalAppHandler::SetDownloadToLaunch(
2576 bool aHandleInternally, nsIFile* aNewFileLocation) {
2577 if (mCanceled) return NS_OK;
2579 mHandleInternally = aHandleInternally;
2581 // Now that the user has elected to launch the downloaded file with a helper
2582 // app, we're justified in removing the 'salted' name. We'll rename to what
2583 // was specified in mSuggestedFileName after the download is done prior to
2584 // launching the helper app. So that any existing file of that name won't be
2585 // overwritten we call CreateUnique(). Also note that we use the same
2586 // directory as originally downloaded so the download can be renamed in place
2587 // later.
2588 nsCOMPtr<nsIFile> fileToUse;
2589 if (aNewFileLocation) {
2590 fileToUse = aNewFileLocation;
2591 } else {
2592 (void)GetDownloadDirectory(getter_AddRefs(fileToUse));
2594 if (mSuggestedFileName.IsEmpty()) {
2595 // Keep using the leafname of the temp file, since we're just starting a
2596 // helper
2597 mSuggestedFileName = mTempLeafName;
2600 #ifdef XP_WIN
2601 // Ensure we don't double-append the file extension if it matches:
2602 if (StringEndsWith(mSuggestedFileName, mFileExtension,
2603 nsCaseInsensitiveStringComparator)) {
2604 fileToUse->Append(mSuggestedFileName);
2605 } else {
2606 fileToUse->Append(mSuggestedFileName + mFileExtension);
2608 #else
2609 fileToUse->Append(mSuggestedFileName);
2610 #endif
2613 nsresult rv = fileToUse->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
2614 if (NS_SUCCEEDED(rv)) {
2615 mFinalFileDestination = fileToUse;
2616 // launch the progress window now that the user has picked the desired
2617 // action.
2618 rv = CreateTransfer();
2619 if (NS_FAILED(rv)) {
2620 Cancel(rv);
2622 } else {
2623 // Cancel the download and report an error. We do not want to end up in
2624 // a state where it appears that we have a normal download that is
2625 // pointing to a file that we did not actually create.
2626 nsAutoString path;
2627 mTempFile->GetPath(path);
2628 SendStatusChange(kWriteError, rv, nullptr, path);
2629 Cancel(rv);
2631 return rv;
2634 nsresult nsExternalAppHandler::LaunchLocalFile() {
2635 nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(mSourceUrl));
2636 if (!fileUrl) {
2637 return NS_OK;
2639 Cancel(NS_BINDING_ABORTED);
2640 nsCOMPtr<nsIFile> file;
2641 nsresult rv = fileUrl->GetFile(getter_AddRefs(file));
2643 if (NS_SUCCEEDED(rv)) {
2644 rv = mMimeInfo->LaunchWithFile(file);
2645 if (NS_SUCCEEDED(rv)) return NS_OK;
2647 nsAutoString path;
2648 if (file) file->GetPath(path);
2649 // If we get here, an error happened
2650 SendStatusChange(kLaunchError, rv, nullptr, path);
2651 return rv;
2654 NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) {
2655 NS_ENSURE_ARG(NS_FAILED(aReason));
2657 if (mCanceled) {
2658 return NS_OK;
2660 mCanceled = true;
2662 if (mSaver) {
2663 // We are still writing to the target file. Give the saver a chance to
2664 // close the target file, then notify the transfer object if necessary in
2665 // the OnSaveComplete callback.
2666 mSaver->Finish(aReason);
2667 mSaver = nullptr;
2668 } else {
2669 if (mStopRequestIssued && mTempFile) {
2670 // This branch can only happen when the user cancels the helper app dialog
2671 // when the request has completed. The temp file has to be removed here,
2672 // because mSaver has been released at that time with the temp file left.
2673 (void)mTempFile->Remove(false);
2676 // Notify the transfer object that the download has been canceled, if the
2677 // user has already chosen an action and we didn't notify already.
2678 if (mTransfer) {
2679 NotifyTransfer(aReason);
2683 // Break our reference cycle with the helper app dialog (set up in
2684 // OnStartRequest)
2685 mDialog = nullptr;
2686 mDialogShowing = false;
2688 mRequest = nullptr;
2690 // Release the listener, to break the reference cycle with it (we are the
2691 // observer of the listener).
2692 mDialogProgressListener = nullptr;
2694 return NS_OK;
2697 bool nsExternalAppHandler::GetNeverAskFlagFromPref(const char* prefName,
2698 const char* aContentType) {
2699 // Search the obsolete pref strings.
2700 nsAutoCString prefCString;
2701 Preferences::GetCString(prefName, prefCString);
2702 if (prefCString.IsEmpty()) {
2703 // Default is true, if not found in the pref string.
2704 return true;
2707 NS_UnescapeURL(prefCString);
2708 nsACString::const_iterator start, end;
2709 prefCString.BeginReading(start);
2710 prefCString.EndReading(end);
2711 return !CaseInsensitiveFindInReadable(nsDependentCString(aContentType), start,
2712 end);
2715 NS_IMETHODIMP
2716 nsExternalAppHandler::GetName(nsACString& aName) {
2717 aName.AssignLiteral("nsExternalAppHandler");
2718 return NS_OK;
2721 //////////////////////////////////////////////////////////////////////////////////////////////////////////////
2722 // The following section contains our nsIMIMEService implementation and related
2723 // methods.
2725 //////////////////////////////////////////////////////////////////////////////////////////////////////////////
2727 // nsIMIMEService methods
2728 NS_IMETHODIMP nsExternalHelperAppService::GetFromTypeAndExtension(
2729 const nsACString& aMIMEType, const nsACString& aFileExt,
2730 nsIMIMEInfo** _retval) {
2731 MOZ_ASSERT(!aMIMEType.IsEmpty() || !aFileExt.IsEmpty(),
2732 "Give me something to work with");
2733 MOZ_DIAGNOSTIC_ASSERT(aFileExt.FindChar('\0') == kNotFound,
2734 "The extension should never contain null characters");
2735 LOG("Getting mimeinfo from type '%s' ext '%s'\n",
2736 PromiseFlatCString(aMIMEType).get(), PromiseFlatCString(aFileExt).get());
2738 *_retval = nullptr;
2740 // OK... we need a type. Get one.
2741 nsAutoCString typeToUse(aMIMEType);
2742 if (typeToUse.IsEmpty()) {
2743 nsresult rv = GetTypeFromExtension(aFileExt, typeToUse);
2744 if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE;
2747 // We promise to only send lower case mime types to the OS
2748 ToLowerCase(typeToUse);
2750 // First, ask the OS for a mime info
2751 bool found;
2752 nsresult rv = GetMIMEInfoFromOS(typeToUse, aFileExt, &found, _retval);
2753 if (NS_WARN_IF(NS_FAILED(rv))) {
2754 return rv;
2757 LOG("OS gave back 0x%p - found: %i\n", *_retval, found);
2758 // If we got no mimeinfo, something went wrong. Probably lack of memory.
2759 if (!*_retval) return NS_ERROR_OUT_OF_MEMORY;
2761 // The handler service can make up for bad mime types by checking the file
2762 // extension. If the mime type is known (in extras or in the handler
2763 // service), we stop it doing so by flipping this bool to true.
2764 bool trustMIMEType = false;
2766 // Check extras - not everything we support will be known by the OS store,
2767 // unfortunately, and it may even miss some extensions that we know should
2768 // be accepted. We only do this for non-octet-stream mimetypes, because
2769 // our information for octet-stream would lead to us trying to open all such
2770 // files as Binary file with exe, com or bin extension regardless of the
2771 // real extension.
2772 if (!typeToUse.Equals(APPLICATION_OCTET_STREAM,
2773 nsCaseInsensitiveCStringComparator)) {
2774 rv = FillMIMEInfoForMimeTypeFromExtras(typeToUse, !found, *_retval);
2775 LOG("Searched extras (by type), rv 0x%08" PRIX32 "\n",
2776 static_cast<uint32_t>(rv));
2777 trustMIMEType = NS_SUCCEEDED(rv);
2778 found = found || NS_SUCCEEDED(rv);
2781 // Now, let's see if we can find something in our datastore.
2782 // This will not overwrite the OS information that interests us
2783 // (i.e. default application, default app. description)
2784 nsCOMPtr<nsIHandlerService> handlerSvc =
2785 do_GetService(NS_HANDLERSERVICE_CONTRACTID);
2786 if (handlerSvc) {
2787 bool hasHandler = false;
2788 (void)handlerSvc->Exists(*_retval, &hasHandler);
2789 if (hasHandler) {
2790 rv = handlerSvc->FillHandlerInfo(*_retval, ""_ns);
2791 LOG("Data source: Via type: retval 0x%08" PRIx32 "\n",
2792 static_cast<uint32_t>(rv));
2793 trustMIMEType = trustMIMEType || NS_SUCCEEDED(rv);
2794 } else {
2795 rv = NS_ERROR_NOT_AVAILABLE;
2798 found = found || NS_SUCCEEDED(rv);
2801 // If we still haven't found anything, try finding a match for
2802 // an extension in extras first:
2803 if (!found && !aFileExt.IsEmpty()) {
2804 rv = FillMIMEInfoForExtensionFromExtras(aFileExt, *_retval);
2805 LOG("Searched extras (by ext), rv 0x%08" PRIX32 "\n",
2806 static_cast<uint32_t>(rv));
2809 // Then check the handler service - but only do so if we really do not know
2810 // the mimetype. This avoids overwriting good mimetype info with bad file
2811 // extension info.
2812 if ((!found || !trustMIMEType) && handlerSvc && !aFileExt.IsEmpty()) {
2813 nsAutoCString overrideType;
2814 rv = handlerSvc->GetTypeFromExtension(aFileExt, overrideType);
2815 if (NS_SUCCEEDED(rv) && !overrideType.IsEmpty()) {
2816 // We can't check handlerSvc->Exists() here, because we have a
2817 // overideType. That's ok, it just results in some console noise.
2818 // (If there's no handler for the override type, it throws)
2819 rv = handlerSvc->FillHandlerInfo(*_retval, overrideType);
2820 LOG("Data source: Via ext: retval 0x%08" PRIx32 "\n",
2821 static_cast<uint32_t>(rv));
2822 found = found || NS_SUCCEEDED(rv);
2826 // If we still don't have a match, at least set the file description
2827 // to `${aFileExt} File` if it's empty:
2828 if (!found && !aFileExt.IsEmpty()) {
2829 // XXXzpao This should probably be localized
2830 nsAutoCString desc(aFileExt);
2831 desc.AppendLiteral(" File");
2832 (*_retval)->SetDescription(NS_ConvertUTF8toUTF16(desc));
2833 LOG("Falling back to 'File' file description\n");
2836 // Sometimes, OSes give us bad data. We have a set of forbidden extensions
2837 // for some MIME types. If the primary extension is forbidden,
2838 // overwrite it with a known-good one. See bug 1571247 for context.
2839 nsAutoCString primaryExtension;
2840 (*_retval)->GetPrimaryExtension(primaryExtension);
2841 if (!primaryExtension.EqualsIgnoreCase(PromiseFlatCString(aFileExt).get())) {
2842 if (MaybeReplacePrimaryExtension(primaryExtension, *_retval)) {
2843 (*_retval)->GetPrimaryExtension(primaryExtension);
2847 // Finally, check if we got a file extension and if yes, if it is an
2848 // extension on the mimeinfo, in which case we want it to be the primary one
2849 if (!aFileExt.IsEmpty()) {
2850 bool matches = false;
2851 (*_retval)->ExtensionExists(aFileExt, &matches);
2852 LOG("Extension '%s' matches mime info: %i\n",
2853 PromiseFlatCString(aFileExt).get(), matches);
2854 if (matches) {
2855 nsAutoCString fileExt;
2856 ToLowerCase(aFileExt, fileExt);
2857 (*_retval)->SetPrimaryExtension(fileExt);
2858 primaryExtension = fileExt;
2862 // Overwrite with a generic description if the primary extension for the
2863 // type is in our list; these are file formats supported by Firefox and
2864 // we don't want other brands positioning themselves as the sole viewer
2865 // for a system.
2866 if (!primaryExtension.IsEmpty()) {
2867 for (const char* ext : descriptionOverwriteExtensions) {
2868 if (primaryExtension.Equals(ext)) {
2869 nsCOMPtr<nsIStringBundleService> bundleService =
2870 do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
2871 NS_ENSURE_SUCCESS(rv, rv);
2872 nsCOMPtr<nsIStringBundle> unknownContentTypeBundle;
2873 rv = bundleService->CreateBundle(
2874 "chrome://mozapps/locale/downloads/unknownContentType.properties",
2875 getter_AddRefs(unknownContentTypeBundle));
2876 if (NS_SUCCEEDED(rv)) {
2877 nsAutoCString stringName(ext);
2878 stringName.AppendLiteral("ExtHandlerDescription");
2879 nsAutoString handlerDescription;
2880 rv = unknownContentTypeBundle->GetStringFromName(stringName.get(),
2881 handlerDescription);
2882 if (NS_SUCCEEDED(rv)) {
2883 (*_retval)->SetDescription(handlerDescription);
2886 break;
2891 if (LOG_ENABLED()) {
2892 nsAutoCString type;
2893 (*_retval)->GetMIMEType(type);
2895 LOG("MIME Info Summary: Type '%s', Primary Ext '%s'\n", type.get(),
2896 primaryExtension.get());
2899 return NS_OK;
2902 bool nsExternalHelperAppService::GetMIMETypeFromDefaultForExtension(
2903 const nsACString& aExtension, nsACString& aMIMEType) {
2904 // First of all, check our default entries
2905 for (auto& entry : defaultMimeEntries) {
2906 if (aExtension.LowerCaseEqualsASCII(entry.mFileExtension)) {
2907 aMIMEType = entry.mMimeType;
2908 return true;
2911 return false;
2914 NS_IMETHODIMP
2915 nsExternalHelperAppService::GetTypeFromExtension(const nsACString& aFileExt,
2916 nsACString& aContentType) {
2917 // OK. We want to try the following sources of mimetype information, in this
2918 // order:
2919 // 1. defaultMimeEntries array
2920 // 2. OS-provided information
2921 // 3. our "extras" array
2922 // 4. Information from plugins
2923 // 5. The "ext-to-type-mapping" category
2924 // Note that, we are intentionally not looking at the handler service, because
2925 // that can be affected by websites, which leads to undesired behavior.
2927 // Early return if called with an empty extension parameter
2928 if (aFileExt.IsEmpty()) {
2929 return NS_ERROR_NOT_AVAILABLE;
2932 // First of all, check our default entries
2933 if (GetMIMETypeFromDefaultForExtension(aFileExt, aContentType)) {
2934 return NS_OK;
2937 // Ask OS.
2938 if (GetMIMETypeFromOSForExtension(aFileExt, aContentType)) {
2939 return NS_OK;
2942 // Check extras array.
2943 bool found = GetTypeFromExtras(aFileExt, aContentType);
2944 if (found) {
2945 return NS_OK;
2948 // Let's see if an extension added something
2949 nsCOMPtr<nsICategoryManager> catMan(
2950 do_GetService("@mozilla.org/categorymanager;1"));
2951 if (catMan) {
2952 // The extension in the category entry is always stored as lowercase
2953 nsAutoCString lowercaseFileExt(aFileExt);
2954 ToLowerCase(lowercaseFileExt);
2955 // Read the MIME type from the category entry, if available
2956 nsCString type;
2957 nsresult rv =
2958 catMan->GetCategoryEntry("ext-to-type-mapping", lowercaseFileExt, type);
2959 if (NS_SUCCEEDED(rv)) {
2960 aContentType = type;
2961 return NS_OK;
2965 return NS_ERROR_NOT_AVAILABLE;
2968 NS_IMETHODIMP nsExternalHelperAppService::GetPrimaryExtension(
2969 const nsACString& aMIMEType, const nsACString& aFileExt,
2970 nsACString& _retval) {
2971 NS_ENSURE_ARG(!aMIMEType.IsEmpty());
2973 nsCOMPtr<nsIMIMEInfo> mi;
2974 nsresult rv =
2975 GetFromTypeAndExtension(aMIMEType, aFileExt, getter_AddRefs(mi));
2976 if (NS_FAILED(rv)) return rv;
2978 return mi->GetPrimaryExtension(_retval);
2981 NS_IMETHODIMP nsExternalHelperAppService::GetDefaultTypeFromURI(
2982 nsIURI* aURI, nsACString& aContentType) {
2983 NS_ENSURE_ARG_POINTER(aURI);
2984 nsresult rv = NS_ERROR_NOT_AVAILABLE;
2985 aContentType.Truncate();
2987 // Now try to get an nsIURL so we don't have to do our own parsing
2988 nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
2989 if (!url) {
2990 return NS_ERROR_NOT_AVAILABLE;
2993 nsAutoCString ext;
2994 rv = url->GetFileExtension(ext);
2995 if (NS_FAILED(rv)) {
2996 return rv;
2999 if (!ext.IsEmpty()) {
3000 UnescapeFragment(ext, url, ext);
3002 if (GetMIMETypeFromDefaultForExtension(ext, aContentType)) {
3003 return NS_OK;
3007 return NS_ERROR_NOT_AVAILABLE;
3010 NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromURI(
3011 nsIURI* aURI, nsACString& aContentType) {
3012 NS_ENSURE_ARG_POINTER(aURI);
3013 nsresult rv = NS_ERROR_NOT_AVAILABLE;
3014 aContentType.Truncate();
3016 // First look for a file to use. If we have one, we just use that.
3017 nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(aURI);
3018 if (fileUrl) {
3019 nsCOMPtr<nsIFile> file;
3020 rv = fileUrl->GetFile(getter_AddRefs(file));
3021 if (NS_SUCCEEDED(rv)) {
3022 rv = GetTypeFromFile(file, aContentType);
3023 if (NS_SUCCEEDED(rv)) {
3024 // we got something!
3025 return rv;
3030 // Now try to get an nsIURL so we don't have to do our own parsing
3031 nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
3032 if (url) {
3033 nsAutoCString ext;
3034 rv = url->GetFileExtension(ext);
3035 if (NS_FAILED(rv)) return rv;
3036 if (ext.IsEmpty()) return NS_ERROR_NOT_AVAILABLE;
3038 UnescapeFragment(ext, url, ext);
3040 return GetTypeFromExtension(ext, aContentType);
3043 // no url, let's give the raw spec a shot
3044 nsAutoCString specStr;
3045 rv = aURI->GetSpec(specStr);
3046 if (NS_FAILED(rv)) return rv;
3047 UnescapeFragment(specStr, aURI, specStr);
3049 // find the file extension (if any)
3050 int32_t extLoc = specStr.RFindChar('.');
3051 int32_t specLength = specStr.Length();
3052 if (-1 != extLoc && extLoc != specLength - 1 &&
3053 // nothing over 20 chars long can be sanely considered an
3054 // extension.... Dat dere would be just data.
3055 specLength - extLoc < 20) {
3056 return GetTypeFromExtension(Substring(specStr, extLoc + 1), aContentType);
3059 // We found no information; say so.
3060 return NS_ERROR_NOT_AVAILABLE;
3063 NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromFile(
3064 nsIFile* aFile, nsACString& aContentType) {
3065 NS_ENSURE_ARG_POINTER(aFile);
3066 nsresult rv;
3068 // Get the Extension
3069 nsAutoString fileName;
3070 rv = aFile->GetLeafName(fileName);
3071 if (NS_FAILED(rv)) return rv;
3073 nsAutoCString fileExt;
3074 if (!fileName.IsEmpty()) {
3075 int32_t len = fileName.Length();
3076 for (int32_t i = len; i >= 0; i--) {
3077 if (fileName[i] == char16_t('.')) {
3078 CopyUTF16toUTF8(Substring(fileName, i + 1), fileExt);
3079 break;
3084 if (fileExt.IsEmpty()) return NS_ERROR_FAILURE;
3086 return GetTypeFromExtension(fileExt, aContentType);
3089 nsresult nsExternalHelperAppService::FillMIMEInfoForMimeTypeFromExtras(
3090 const nsACString& aContentType, bool aOverwriteDescription,
3091 nsIMIMEInfo* aMIMEInfo) {
3092 NS_ENSURE_ARG(aMIMEInfo);
3094 NS_ENSURE_ARG(!aContentType.IsEmpty());
3096 // Look for default entry with matching mime type.
3097 nsAutoCString MIMEType(aContentType);
3098 ToLowerCase(MIMEType);
3099 for (auto entry : extraMimeEntries) {
3100 if (MIMEType.Equals(entry.mMimeType)) {
3101 // This is the one. Set attributes appropriately.
3102 nsDependentCString extensions(entry.mFileExtensions);
3103 nsACString::const_iterator start, end;
3104 extensions.BeginReading(start);
3105 extensions.EndReading(end);
3106 while (start != end) {
3107 nsACString::const_iterator cursor = start;
3108 mozilla::Unused << FindCharInReadable(',', cursor, end);
3109 aMIMEInfo->AppendExtension(Substring(start, cursor));
3110 // If a comma was found, skip it for the next search.
3111 start = cursor != end ? ++cursor : cursor;
3114 nsAutoString desc;
3115 aMIMEInfo->GetDescription(desc);
3116 if (aOverwriteDescription || desc.IsEmpty()) {
3117 aMIMEInfo->SetDescription(NS_ConvertASCIItoUTF16(entry.mDescription));
3119 return NS_OK;
3123 return NS_ERROR_NOT_AVAILABLE;
3126 nsresult nsExternalHelperAppService::FillMIMEInfoForExtensionFromExtras(
3127 const nsACString& aExtension, nsIMIMEInfo* aMIMEInfo) {
3128 nsAutoCString type;
3129 bool found = GetTypeFromExtras(aExtension, type);
3130 if (!found) return NS_ERROR_NOT_AVAILABLE;
3131 return FillMIMEInfoForMimeTypeFromExtras(type, true, aMIMEInfo);
3134 bool nsExternalHelperAppService::MaybeReplacePrimaryExtension(
3135 const nsACString& aPrimaryExtension, nsIMIMEInfo* aMIMEInfo) {
3136 for (const auto& entry : sForbiddenPrimaryExtensions) {
3137 if (aPrimaryExtension.LowerCaseEqualsASCII(entry.mFileExtension)) {
3138 nsDependentCString mime(entry.mMimeType);
3139 for (const auto& extraEntry : extraMimeEntries) {
3140 if (mime.LowerCaseEqualsASCII(extraEntry.mMimeType)) {
3141 nsDependentCString goodExts(extraEntry.mFileExtensions);
3142 int32_t commaPos = goodExts.FindChar(',');
3143 commaPos = commaPos == kNotFound ? goodExts.Length() : commaPos;
3144 auto goodExt = Substring(goodExts, 0, commaPos);
3145 aMIMEInfo->SetPrimaryExtension(goodExt);
3146 return true;
3151 return false;
3154 bool nsExternalHelperAppService::GetTypeFromExtras(const nsACString& aExtension,
3155 nsACString& aMIMEType) {
3156 NS_ASSERTION(!aExtension.IsEmpty(), "Empty aExtension parameter!");
3158 // Look for default entry with matching extension.
3159 nsDependentCString::const_iterator start, end, iter;
3160 int32_t numEntries = ArrayLength(extraMimeEntries);
3161 for (int32_t index = 0; index < numEntries; index++) {
3162 nsDependentCString extList(extraMimeEntries[index].mFileExtensions);
3163 extList.BeginReading(start);
3164 extList.EndReading(end);
3165 iter = start;
3166 while (start != end) {
3167 FindCharInReadable(',', iter, end);
3168 if (Substring(start, iter)
3169 .Equals(aExtension, nsCaseInsensitiveCStringComparator)) {
3170 aMIMEType = extraMimeEntries[index].mMimeType;
3171 return true;
3173 if (iter != end) {
3174 ++iter;
3176 start = iter;
3180 return false;
3183 bool nsExternalHelperAppService::GetMIMETypeFromOSForExtension(
3184 const nsACString& aExtension, nsACString& aMIMEType) {
3185 bool found = false;
3186 nsCOMPtr<nsIMIMEInfo> mimeInfo;
3187 nsresult rv =
3188 GetMIMEInfoFromOS(""_ns, aExtension, &found, getter_AddRefs(mimeInfo));
3189 return NS_SUCCEEDED(rv) && found && mimeInfo &&
3190 NS_SUCCEEDED(mimeInfo->GetMIMEType(aMIMEType));
3193 nsresult nsExternalHelperAppService::GetMIMEInfoFromOS(
3194 const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound,
3195 nsIMIMEInfo** aMIMEInfo) {
3196 *aMIMEInfo = nullptr;
3197 *aFound = false;
3198 return NS_ERROR_NOT_IMPLEMENTED;
3201 nsresult nsExternalHelperAppService::UpdateDefaultAppInfo(
3202 nsIMIMEInfo* aMIMEInfo) {
3203 return NS_ERROR_NOT_IMPLEMENTED;
3206 bool nsExternalHelperAppService::GetFileNameFromChannel(nsIChannel* aChannel,
3207 nsAString& aFileName,
3208 nsIURI** aURI) {
3209 if (!aChannel) {
3210 return false;
3213 aChannel->GetURI(aURI);
3214 nsCOMPtr<nsIURL> url = do_QueryInterface(*aURI);
3216 // Check if we have a POST request, in which case we don't want to use
3217 // the url's extension
3218 bool allowURLExt = !net::ChannelIsPost(aChannel);
3220 // Check if we had a query string - we don't want to check the URL
3221 // extension if a query is present in the URI
3222 // If we already know we don't want to check the URL extension, don't
3223 // bother checking the query
3224 if (url && allowURLExt) {
3225 nsAutoCString query;
3227 // We only care about the query for HTTP and HTTPS URLs
3228 if (url->SchemeIs("http") || url->SchemeIs("https")) {
3229 url->GetQuery(query);
3232 // Only get the extension if the query is empty; if it isn't, then the
3233 // extension likely belongs to a cgi script and isn't helpful
3234 allowURLExt = query.IsEmpty();
3237 aChannel->GetContentDispositionFilename(aFileName);
3239 return allowURLExt;
3242 NS_IMETHODIMP
3243 nsExternalHelperAppService::GetValidFileName(nsIChannel* aChannel,
3244 const nsACString& aType,
3245 nsIURI* aOriginalURI,
3246 uint32_t aFlags,
3247 nsAString& aOutFileName) {
3248 nsCOMPtr<nsIURI> uri;
3249 bool allowURLExtension =
3250 GetFileNameFromChannel(aChannel, aOutFileName, getter_AddRefs(uri));
3252 nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
3253 aOutFileName, aType, uri, aOriginalURI, aFlags, allowURLExtension);
3254 return NS_OK;
3257 NS_IMETHODIMP
3258 nsExternalHelperAppService::ValidateFileNameForSaving(
3259 const nsAString& aFileName, const nsACString& aType, uint32_t aFlags,
3260 nsAString& aOutFileName) {
3261 nsAutoString fileName(aFileName);
3263 // Just sanitize the filename only.
3264 if (aFlags & VALIDATE_SANITIZE_ONLY) {
3265 SanitizeFileName(fileName, aFlags);
3266 } else {
3267 nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
3268 fileName, aType, nullptr, nullptr, aFlags, true);
3271 aOutFileName = fileName;
3272 return NS_OK;
3275 already_AddRefed<nsIMIMEInfo>
3276 nsExternalHelperAppService::ValidateFileNameForSaving(
3277 nsAString& aFileName, const nsACString& aMimeType, nsIURI* aURI,
3278 nsIURI* aOriginalURI, uint32_t aFlags, bool aAllowURLExtension) {
3279 nsAutoString fileName(aFileName);
3280 nsAutoCString extension;
3281 nsCOMPtr<nsIMIMEInfo> mimeInfo;
3283 bool isBinaryType = aMimeType.EqualsLiteral(APPLICATION_OCTET_STREAM) ||
3284 aMimeType.EqualsLiteral(BINARY_OCTET_STREAM) ||
3285 aMimeType.EqualsLiteral("application/x-msdownload");
3287 // We don't want to save hidden files starting with a dot, so remove any
3288 // leading periods. This is done first, so that the remainder will be
3289 // treated as the filename, and not an extension.
3290 // Also, Windows ignores terminating dots. So we have to as well, so
3291 // that our security checks do "the right thing"
3292 fileName.Trim(".");
3294 bool urlIsFile = !!aURI && aURI->SchemeIs("file");
3296 // We get the mime service here even though we're the default implementation
3297 // of it, so it's possible to override only the mime service and not need to
3298 // reimplement the whole external helper app service itself.
3299 nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
3300 if (mimeService) {
3301 if (fileName.IsEmpty()) {
3302 nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
3303 // Try to extract the file name from the url and use that as a first
3304 // pass as the leaf name of our temp file...
3305 if (url) {
3306 nsAutoCString leafName;
3307 url->GetFileName(leafName);
3308 if (!leafName.IsEmpty()) {
3309 if (NS_FAILED(UnescapeFragment(leafName, url, fileName))) {
3310 CopyUTF8toUTF16(leafName, fileName); // use escaped name instead
3311 fileName.Trim(".");
3315 // Only get the extension from the URL if allowed, or if this
3316 // is a binary type in which case the type might not be valid
3317 // anyway.
3318 if (aAllowURLExtension || isBinaryType || urlIsFile) {
3319 url->GetFileExtension(extension);
3322 } else {
3323 // Determine the current extension for the filename.
3324 int32_t dotidx = fileName.RFind(u".");
3325 if (dotidx != -1) {
3326 CopyUTF16toUTF8(Substring(fileName, dotidx + 1), extension);
3330 if (aFlags & VALIDATE_GUESS_FROM_EXTENSION) {
3331 nsAutoCString mimeType;
3332 if (!extension.IsEmpty()) {
3333 mimeService->GetFromTypeAndExtension(EmptyCString(), extension,
3334 getter_AddRefs(mimeInfo));
3335 if (mimeInfo) {
3336 mimeInfo->GetMIMEType(mimeType);
3340 if (mimeType.IsEmpty()) {
3341 // Extension lookup gave us no useful match, so use octet-stream
3342 // instead.
3343 mimeService->GetFromTypeAndExtension(
3344 nsLiteralCString(APPLICATION_OCTET_STREAM), extension,
3345 getter_AddRefs(mimeInfo));
3347 } else if (!aMimeType.IsEmpty()) {
3348 // If this is a binary type, include the extension as a hint to get
3349 // the mime info. For other types, the mime type itself should be
3350 // sufficient.
3351 // Unfortunately, on Windows, the mimetype is usually insufficient.
3352 // Compensate at least on `file` URLs by trusting the extension -
3353 // that's likely what we used to get the mimetype in the first place.
3354 // The special case for application/ogg is because that type could
3355 // actually be used for a video which can better be determined by the
3356 // extension. This is tested by browser_save_video.js.
3357 bool useExtension =
3358 isBinaryType || urlIsFile || aMimeType.EqualsLiteral(APPLICATION_OGG);
3359 mimeService->GetFromTypeAndExtension(
3360 aMimeType, useExtension ? extension : EmptyCString(),
3361 getter_AddRefs(mimeInfo));
3362 if (mimeInfo) {
3363 // But if no primary extension was returned, this mime type is probably
3364 // an unknown type. Look it up again but this time supply the extension.
3365 nsAutoCString primaryExtension;
3366 mimeInfo->GetPrimaryExtension(primaryExtension);
3367 if (primaryExtension.IsEmpty()) {
3368 mimeService->GetFromTypeAndExtension(aMimeType, extension,
3369 getter_AddRefs(mimeInfo));
3375 // If an empty filename is allowed, then return early. It will be saved
3376 // using the filename of the temporary file that was created for the download.
3377 if (aFlags & VALIDATE_ALLOW_EMPTY && fileName.IsEmpty()) {
3378 aFileName.Truncate();
3379 return mimeInfo.forget();
3382 // This section modifies the extension on the filename if it isn't valid for
3383 // the given content type.
3384 if (mimeInfo) {
3385 bool isValidExtension;
3386 if (extension.IsEmpty() ||
3387 NS_FAILED(mimeInfo->ExtensionExists(extension, &isValidExtension)) ||
3388 !isValidExtension) {
3389 // Skip these checks for text and binary, so we don't append the unneeded
3390 // .txt or other extension.
3391 if (aMimeType.EqualsLiteral(TEXT_PLAIN) || isBinaryType) {
3392 extension.Truncate();
3393 } else {
3394 nsAutoCString originalExtension(extension);
3395 // If an original url was supplied, see if it has a valid extension.
3396 bool useOldExtension = false;
3397 if (aOriginalURI) {
3398 nsCOMPtr<nsIURL> originalURL(do_QueryInterface(aOriginalURI));
3399 if (originalURL) {
3400 nsAutoCString uriExtension;
3401 originalURL->GetFileExtension(uriExtension);
3402 if (!uriExtension.IsEmpty()) {
3403 mimeInfo->ExtensionExists(uriExtension, &useOldExtension);
3404 if (useOldExtension) {
3405 extension = uriExtension;
3411 if (!useOldExtension) {
3412 // If the filename doesn't have a valid extension, or we don't know
3413 // the extension, try to use the primary extension for the type. If we
3414 // don't know the primary extension for the type, just continue with
3415 // the existing extension, or leave the filename with no extension.
3416 nsAutoCString primaryExtension;
3417 mimeInfo->GetPrimaryExtension(primaryExtension);
3418 if (!primaryExtension.IsEmpty()) {
3419 extension = primaryExtension;
3423 // If an suitable extension was found, we will append to or replace the
3424 // existing extension.
3425 if (!extension.IsEmpty()) {
3426 ModifyExtensionType modify = ShouldModifyExtension(
3427 mimeInfo, aFlags & VALIDATE_FORCE_APPEND_EXTENSION,
3428 originalExtension);
3429 if (modify == ModifyExtension_Replace) {
3430 int32_t dotidx = fileName.RFind(u".");
3431 if (dotidx != -1) {
3432 // Remove the existing extension and replace it.
3433 fileName.Truncate(dotidx);
3437 // Otherwise, just append the proper extension to the end of the
3438 // filename, adding to the invalid extension that might already be
3439 // there.
3440 if (modify != ModifyExtension_Ignore) {
3441 fileName.AppendLiteral(".");
3442 fileName.Append(NS_ConvertUTF8toUTF16(extension));
3449 CheckDefaultFileName(fileName, aFlags);
3451 // Make the filename safe for the filesystem.
3452 SanitizeFileName(fileName, aFlags);
3454 aFileName = fileName;
3455 return mimeInfo.forget();
3458 void nsExternalHelperAppService::CheckDefaultFileName(nsAString& aFileName,
3459 uint32_t aFlags) {
3460 // If no filename is present, use a default filename.
3461 if (!(aFlags & VALIDATE_NO_DEFAULT_FILENAME) &&
3462 (aFileName.Length() == 0 || aFileName.RFind(u".") == 0)) {
3463 nsCOMPtr<nsIStringBundleService> stringService =
3464 mozilla::components::StringBundle::Service();
3465 if (stringService) {
3466 nsCOMPtr<nsIStringBundle> bundle;
3467 if (NS_SUCCEEDED(stringService->CreateBundle(
3468 "chrome://global/locale/contentAreaCommands.properties",
3469 getter_AddRefs(bundle)))) {
3470 nsAutoString defaultFileName;
3471 bundle->GetStringFromName("UntitledSaveFileName", defaultFileName);
3472 // Append any existing extension to the default filename.
3473 aFileName = defaultFileName + aFileName;
3477 // Use 'Untitled' as a last resort.
3478 if (!aFileName.Length()) {
3479 aFileName.AssignLiteral("Untitled");
3484 void nsExternalHelperAppService::SanitizeFileName(nsAString& aFileName,
3485 uint32_t aFlags) {
3486 nsAutoString fileName(aFileName);
3488 // Replace known invalid characters.
3489 fileName.ReplaceChar(u"" KNOWN_PATH_SEPARATORS, u'_');
3490 fileName.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u' ');
3491 fileName.StripChar(char16_t(0));
3493 const char16_t *startStr, *endStr;
3494 fileName.BeginReading(startStr);
3495 fileName.EndReading(endStr);
3497 // True if multiple consecutive whitespace characters should
3498 // be replaced by single space ' '.
3499 bool collapseWhitespace = !(aFlags & VALIDATE_DONT_COLLAPSE_WHITESPACE);
3501 // The maximum filename length differs based on the platform:
3502 // Windows (FAT/NTFS) stores filenames as a maximum of 255 UTF-16 code units.
3503 // Mac (APFS) stores filenames with a maximum 255 of UTF-8 code units.
3504 // Linux (ext3/ext4...) stores filenames with a maximum 255 bytes.
3505 // So here we just use the maximum of 255 bytes.
3506 // 0 means don't truncate at a maximum size.
3507 const uint32_t maxBytes =
3508 (aFlags & VALIDATE_DONT_TRUNCATE) ? 0 : kDefaultMaxFileNameLength;
3510 // True if the last character added was whitespace.
3511 bool lastWasWhitespace = false;
3513 // Length of the filename that fits into the maximum size excluding the
3514 // extension and period.
3515 int32_t longFileNameEnd = -1;
3517 // Index of the last character added that was not a character that can be
3518 // trimmed off of the end of the string. Trimmable characters are whitespace,
3519 // periods and the vowel separator u'\u180e'. If all the characters after this
3520 // point are trimmable characters, truncate the string to this point after
3521 // iterating over the filename.
3522 int32_t lastNonTrimmable = -1;
3524 // The number of bytes that the string would occupy if encoded in UTF-8.
3525 uint32_t bytesLength = 0;
3527 // The length of the extension in bytes.
3528 uint32_t extensionBytesLength = 0;
3530 // This algorithm iterates over each character in the string and appends it
3531 // or a replacement character if needed to outFileName.
3532 nsAutoString outFileName;
3533 while (startStr < endStr) {
3534 bool err = false;
3535 char32_t nextChar = UTF16CharEnumerator::NextChar(&startStr, endStr, &err);
3536 if (err) {
3537 break;
3540 // nulls are already stripped out above.
3541 MOZ_ASSERT(nextChar != char16_t(0));
3543 auto unicodeCategory = unicode::GetGeneralCategory(nextChar);
3544 if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_CONTROL ||
3545 unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR ||
3546 unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_PARAGRAPH_SEPARATOR) {
3547 // Skip over any control characters and separators.
3548 continue;
3551 if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_SPACE_SEPARATOR ||
3552 nextChar == u'\ufeff') {
3553 // Trim out any whitespace characters at the beginning of the filename,
3554 // and only add whitespace in the middle of the filename if the last
3555 // character was not whitespace or if we are not collapsing whitespace.
3556 if (!outFileName.IsEmpty() &&
3557 (!lastWasWhitespace || !collapseWhitespace)) {
3558 // Allow the ideographic space if it is present, otherwise replace with
3559 // ' '.
3560 if (nextChar != u'\u3000') {
3561 nextChar = ' ';
3563 lastWasWhitespace = true;
3564 } else {
3565 lastWasWhitespace = true;
3566 continue;
3568 } else {
3569 lastWasWhitespace = false;
3570 if (nextChar == '.' || nextChar == u'\u180e') {
3571 // Don't add any periods or vowel separators at the beginning of the
3572 // string. Note also that lastNonTrimmable is not adjusted in this
3573 // case, because periods and vowel separators are included in the
3574 // set of characters to trim at the end of the filename.
3575 if (outFileName.IsEmpty()) {
3576 continue;
3578 } else {
3579 if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_FORMAT) {
3580 // Replace formatting characters with an underscore.
3581 nextChar = '_';
3584 // Don't truncate surrogate pairs in the middle.
3585 lastNonTrimmable =
3586 int32_t(outFileName.Length()) +
3587 (NS_IS_HIGH_SURROGATE(H_SURROGATE(nextChar)) ? 2 : 1);
3591 if (maxBytes) {
3592 // UTF16CharEnumerator already converts surrogate pairs, so we can use
3593 // a simple computation of byte length here.
3594 uint32_t charBytesLength = nextChar < 0x80 ? 1
3595 : nextChar < 0x800 ? 2
3596 : nextChar < 0x10000 ? 3
3597 : 4;
3598 bytesLength += charBytesLength;
3599 if (bytesLength > maxBytes) {
3600 if (longFileNameEnd == -1) {
3601 longFileNameEnd = int32_t(outFileName.Length());
3605 // If we encounter a period, it could be the start of an extension, so
3606 // start counting the number of bytes in the extension. If another period
3607 // is found, start again since we want to use the last extension found.
3608 if (nextChar == u'.') {
3609 extensionBytesLength = 1; // 1 byte for the period.
3610 } else if (extensionBytesLength) {
3611 extensionBytesLength += charBytesLength;
3615 AppendUCS4ToUTF16(nextChar, outFileName);
3618 // If the filename is longer than the maximum allowed filename size,
3619 // truncate it, but preserve the desired extension that is currently
3620 // on the filename.
3621 if (bytesLength > maxBytes && !outFileName.IsEmpty()) {
3622 // Get the sanitized extension from the filename without the dot.
3623 nsAutoString extension;
3624 int32_t dotidx = outFileName.RFind(u".");
3625 if (dotidx != -1) {
3626 extension = Substring(outFileName, dotidx + 1);
3629 // There are two ways in which the filename should be truncated:
3630 // - If the filename was too long, truncate the name at the length
3631 // of the filename.
3632 // This position is indicated by longFileNameEnd.
3633 // - lastNonTrimmable will indicate the last character that was not
3634 // whitespace, a period, or a vowel separator at the end of the
3635 // the string, so the string should be truncated there as well.
3636 // If both apply, use the earliest position.
3637 if (lastNonTrimmable >= 0) {
3638 // Subtract off the amount for the extension and the period.
3639 // Note that the extension length is in bytes but longFileNameEnd is in
3640 // characters, but if they don't match, it just means we crop off
3641 // more than is necessary. This is OK since it is better than cropping
3642 // off too little.
3643 longFileNameEnd -= extensionBytesLength;
3644 if (longFileNameEnd <= 0) {
3645 // This is extremely unlikely, but if the extension is larger than the
3646 // maximum size, just get rid of it. In this case, the extension
3647 // wouldn't have been an ordinary one we would want to preserve (such
3648 // as .html or .png) so just truncate off the file wherever the first
3649 // period appears.
3650 int32_t dotidx = outFileName.Find(u".");
3651 outFileName.Truncate(dotidx > 0 ? dotidx : 1);
3652 } else {
3653 outFileName.Truncate(std::min(longFileNameEnd, lastNonTrimmable));
3655 // Now that the filename has been truncated, re-append the extension
3656 // again.
3657 if (!extension.IsEmpty()) {
3658 if (outFileName.Last() != '.') {
3659 outFileName.AppendLiteral(".");
3662 outFileName.Append(extension);
3666 } else if (lastNonTrimmable >= 0) {
3667 // Otherwise, the filename wasn't too long, so just trim off the
3668 // extra whitespace and periods at the end.
3669 outFileName.Truncate(lastNonTrimmable);
3672 #ifdef XP_WIN
3673 if (nsLocalFile::CheckForReservedFileName(outFileName)) {
3674 outFileName.Truncate();
3675 CheckDefaultFileName(outFileName, aFlags);
3678 #endif
3680 if (!(aFlags & VALIDATE_ALLOW_INVALID_FILENAMES)) {
3681 // If the extension is one these types, replace it with .download, as these
3682 // types of files can have significance on Windows or Linux.
3683 // This happens for any file, not just those with the shortcut mime type.
3684 if (StringEndsWith(outFileName, u".lnk"_ns,
3685 nsCaseInsensitiveStringComparator) ||
3686 StringEndsWith(outFileName, u".local"_ns,
3687 nsCaseInsensitiveStringComparator) ||
3688 StringEndsWith(outFileName, u".url"_ns,
3689 nsCaseInsensitiveStringComparator) ||
3690 StringEndsWith(outFileName, u".scf"_ns,
3691 nsCaseInsensitiveStringComparator) ||
3692 StringEndsWith(outFileName, u".desktop"_ns,
3693 nsCaseInsensitiveStringComparator)) {
3694 outFileName.AppendLiteral(".download");
3698 aFileName = outFileName;
3701 nsExternalHelperAppService::ModifyExtensionType
3702 nsExternalHelperAppService::ShouldModifyExtension(nsIMIMEInfo* aMimeInfo,
3703 bool aForceAppend,
3704 const nsCString& aFileExt) {
3705 nsAutoCString MIMEType;
3706 if (!aMimeInfo || NS_FAILED(aMimeInfo->GetMIMEType(MIMEType))) {
3707 return ModifyExtension_Append;
3710 // Determine whether the extensions should be appended or replaced depending
3711 // on the content type.
3712 bool canForce = StringBeginsWith(MIMEType, "image/"_ns) ||
3713 StringBeginsWith(MIMEType, "audio/"_ns) ||
3714 StringBeginsWith(MIMEType, "video/"_ns) || aFileExt.IsEmpty();
3716 if (!canForce) {
3717 for (const char* mime : forcedExtensionMimetypes) {
3718 if (MIMEType.Equals(mime)) {
3719 if (!StaticPrefs::browser_download_sanitize_non_media_extensions()) {
3720 return ModifyExtension_Ignore;
3722 canForce = true;
3723 break;
3727 if (!canForce) {
3728 return aForceAppend ? ModifyExtension_Append : ModifyExtension_Ignore;
3732 // If we get here, we know for sure the mimetype allows us to modify the
3733 // existing extension, if it's wrong. Return whether we should replace it
3734 // or append it.
3735 bool knownExtension = false;
3736 // Note that aFileExt is either empty or consists of an extension
3737 // excluding the dot.
3738 if (aFileExt.IsEmpty() ||
3739 (NS_SUCCEEDED(aMimeInfo->ExtensionExists(aFileExt, &knownExtension)) &&
3740 !knownExtension)) {
3741 return ModifyExtension_Replace;
3744 return ModifyExtension_Append;