Bug 1793914 [wpt PR 36308] - Update wpt metadata, a=testonly
[gecko.git] / uriloader / exthandler / nsExternalHelperAppService.cpp
blob54a8d617308c2fb2ce816bb4a88162b8395c2723
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 255
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.jsm.
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", "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, nsIRequest* aRequest,
640 BrowsingContext* aContentContext, bool aForceSave,
641 nsIInterfaceRequestor* aWindowContext,
642 nsIStreamListener** aStreamListener) {
643 // We need to get a hold of a ContentChild so that we can begin forwarding
644 // this data to the parent. In the HTTP case, this is unfortunate, since
645 // we're actually passing data from parent->child->parent wastefully, but
646 // the Right Fix will eventually be to short-circuit those channels on the
647 // parent side based on some sort of subscription concept.
648 using mozilla::dom::ContentChild;
649 using mozilla::dom::ExternalHelperAppChild;
650 ContentChild* child = ContentChild::GetSingleton();
651 if (!child) {
652 return NS_ERROR_FAILURE;
655 nsCString disp;
656 nsCOMPtr<nsIURI> uri;
657 int64_t contentLength = -1;
658 bool wasFileChannel = false;
659 uint32_t contentDisposition = -1;
660 nsAutoString fileName;
661 nsCOMPtr<nsILoadInfo> loadInfo;
663 nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
664 if (channel) {
665 channel->GetURI(getter_AddRefs(uri));
666 channel->GetContentLength(&contentLength);
667 channel->GetContentDisposition(&contentDisposition);
668 channel->GetContentDispositionFilename(fileName);
669 channel->GetContentDispositionHeader(disp);
670 loadInfo = channel->LoadInfo();
672 nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aRequest));
673 wasFileChannel = fileChan != nullptr;
676 nsCOMPtr<nsIURI> referrer;
677 NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer));
679 Maybe<mozilla::net::LoadInfoArgs> loadInfoArgs;
680 MOZ_ALWAYS_SUCCEEDS(LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs));
682 nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aRequest));
683 // Determine whether a new window was opened specifically for this request
684 bool shouldCloseWindow = false;
685 if (props) {
686 props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns,
687 &shouldCloseWindow);
690 // Now we build a protocol for forwarding our data to the parent. The
691 // protocol will act as a listener on the child-side and create a "real"
692 // helperAppService listener on the parent-side, via another call to
693 // DoContent.
694 RefPtr<ExternalHelperAppChild> childListener = new ExternalHelperAppChild();
695 MOZ_ALWAYS_TRUE(child->SendPExternalHelperAppConstructor(
696 childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp,
697 contentDisposition, fileName, aForceSave, contentLength, wasFileChannel,
698 referrer, aContentContext, shouldCloseWindow));
700 NS_ADDREF(*aStreamListener = childListener);
702 uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
704 SanitizeFileName(fileName, 0);
706 RefPtr<nsExternalAppHandler> handler =
707 new nsExternalAppHandler(nullptr, u""_ns, aContentContext, aWindowContext,
708 this, fileName, reason, aForceSave);
709 if (!handler) {
710 return NS_ERROR_OUT_OF_MEMORY;
713 childListener->SetHandler(handler);
714 return NS_OK;
717 NS_IMETHODIMP nsExternalHelperAppService::CreateListener(
718 const nsACString& aMimeContentType, nsIRequest* aRequest,
719 BrowsingContext* aContentContext, bool aForceSave,
720 nsIInterfaceRequestor* aWindowContext,
721 nsIStreamListener** aStreamListener) {
722 MOZ_ASSERT(!XRE_IsContentProcess());
724 nsAutoString fileName;
725 nsAutoCString fileExtension;
726 uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
728 nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
729 if (channel) {
730 uint32_t contentDisposition = -1;
731 channel->GetContentDisposition(&contentDisposition);
732 if (contentDisposition == nsIChannel::DISPOSITION_ATTACHMENT) {
733 reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST;
737 *aStreamListener = nullptr;
739 // Get the file extension and name that we will need later
740 nsCOMPtr<nsIURI> uri;
741 bool allowURLExtension =
742 GetFileNameFromChannel(channel, fileName, getter_AddRefs(uri));
744 uint32_t flags = VALIDATE_ALLOW_EMPTY;
745 if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT,
746 nsCaseInsensitiveCStringComparator)) {
747 flags |= VALIDATE_GUESS_FROM_EXTENSION;
750 nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
751 fileName, aMimeContentType, uri, nullptr, flags, allowURLExtension);
753 LOG("Type/Ext lookup found 0x%p\n", mimeInfo.get());
755 // No mimeinfo -> we can't continue. probably OOM.
756 if (!mimeInfo) {
757 return NS_ERROR_OUT_OF_MEMORY;
760 if (flags & VALIDATE_GUESS_FROM_EXTENSION) {
761 if (channel) {
762 // Replace the content type with what was guessed.
763 nsAutoCString mimeType;
764 mimeInfo->GetMIMEType(mimeType);
765 channel->SetContentType(mimeType);
768 if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
769 reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
773 nsAutoString extension;
774 int32_t dotidx = fileName.RFind(u".");
775 if (dotidx != -1) {
776 extension = Substring(fileName, dotidx + 1);
779 // NB: ExternalHelperAppParent depends on this listener always being an
780 // nsExternalAppHandler. If this changes, make sure to update that code.
781 nsExternalAppHandler* handler = new nsExternalAppHandler(
782 mimeInfo, extension, aContentContext, aWindowContext, this, fileName,
783 reason, aForceSave);
784 if (!handler) {
785 return NS_ERROR_OUT_OF_MEMORY;
788 NS_ADDREF(*aStreamListener = handler);
789 return NS_OK;
792 NS_IMETHODIMP nsExternalHelperAppService::DoContent(
793 const nsACString& aMimeContentType, nsIRequest* aRequest,
794 nsIInterfaceRequestor* aContentContext, bool aForceSave,
795 nsIInterfaceRequestor* aWindowContext,
796 nsIStreamListener** aStreamListener) {
797 // Scripted interface requestors cannot return an instance of the
798 // (non-scriptable) nsPIDOMWindowOuter or nsPIDOMWindowInner interfaces, so
799 // get to the window via `nsIDOMWindow`. Unfortunately, at that point we
800 // don't know whether the thing we got is an inner or outer window, so have to
801 // work with either one.
802 RefPtr<BrowsingContext> bc;
803 nsCOMPtr<nsIDOMWindow> domWindow = do_GetInterface(aContentContext);
804 if (nsCOMPtr<nsPIDOMWindowOuter> outerWindow = do_QueryInterface(domWindow)) {
805 bc = outerWindow->GetBrowsingContext();
806 } else if (nsCOMPtr<nsPIDOMWindowInner> innerWindow =
807 do_QueryInterface(domWindow)) {
808 bc = innerWindow->GetBrowsingContext();
811 if (XRE_IsContentProcess()) {
812 return DoContentContentProcessHelper(aMimeContentType, aRequest, bc,
813 aForceSave, aWindowContext,
814 aStreamListener);
817 nsresult rv = CreateListener(aMimeContentType, aRequest, bc, aForceSave,
818 aWindowContext, aStreamListener);
819 return rv;
822 NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension(
823 const nsACString& aExtension, const nsACString& aEncodingType,
824 bool* aApplyDecoding) {
825 *aApplyDecoding = true;
826 uint32_t i;
827 for (i = 0; i < ArrayLength(nonDecodableExtensions); ++i) {
828 if (aExtension.LowerCaseEqualsASCII(
829 nonDecodableExtensions[i].mFileExtension) &&
830 aEncodingType.LowerCaseEqualsASCII(
831 nonDecodableExtensions[i].mMimeType)) {
832 *aApplyDecoding = false;
833 break;
836 return NS_OK;
839 nsresult nsExternalHelperAppService::GetFileTokenForPath(
840 const char16_t* aPlatformAppPath, nsIFile** aFile) {
841 nsDependentString platformAppPath(aPlatformAppPath);
842 // First, check if we have an absolute path
843 nsIFile* localFile = nullptr;
844 nsresult rv = NS_NewLocalFile(platformAppPath, true, &localFile);
845 if (NS_SUCCEEDED(rv)) {
846 *aFile = localFile;
847 bool exists;
848 if (NS_FAILED((*aFile)->Exists(&exists)) || !exists) {
849 NS_RELEASE(*aFile);
850 return NS_ERROR_FILE_NOT_FOUND;
852 return NS_OK;
855 // Second, check if file exists in mozilla program directory
856 rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, aFile);
857 if (NS_SUCCEEDED(rv)) {
858 rv = (*aFile)->Append(platformAppPath);
859 if (NS_SUCCEEDED(rv)) {
860 bool exists = false;
861 rv = (*aFile)->Exists(&exists);
862 if (NS_SUCCEEDED(rv) && exists) return NS_OK;
864 NS_RELEASE(*aFile);
867 return NS_ERROR_NOT_AVAILABLE;
870 //////////////////////////////////////////////////////////////////////////////////////////////////////
871 // begin external protocol service default implementation...
872 //////////////////////////////////////////////////////////////////////////////////////////////////////
873 NS_IMETHODIMP nsExternalHelperAppService::ExternalProtocolHandlerExists(
874 const char* aProtocolScheme, bool* aHandlerExists) {
875 nsCOMPtr<nsIHandlerInfo> handlerInfo;
876 nsresult rv = GetProtocolHandlerInfo(nsDependentCString(aProtocolScheme),
877 getter_AddRefs(handlerInfo));
878 if (NS_SUCCEEDED(rv)) {
879 // See if we have any known possible handler apps for this
880 nsCOMPtr<nsIMutableArray> possibleHandlers;
881 handlerInfo->GetPossibleApplicationHandlers(
882 getter_AddRefs(possibleHandlers));
884 uint32_t length;
885 possibleHandlers->GetLength(&length);
886 if (length) {
887 *aHandlerExists = true;
888 return NS_OK;
892 // if not, fall back on an os-based handler
893 return OSProtocolHandlerExists(aProtocolScheme, aHandlerExists);
896 NS_IMETHODIMP nsExternalHelperAppService::IsExposedProtocol(
897 const char* aProtocolScheme, bool* aResult) {
898 // check the per protocol setting first. it always takes precedence.
899 // if not set, then use the global setting.
901 nsAutoCString prefName("network.protocol-handler.expose.");
902 prefName += aProtocolScheme;
903 bool val;
904 if (NS_SUCCEEDED(Preferences::GetBool(prefName.get(), &val))) {
905 *aResult = val;
906 return NS_OK;
909 // by default, no protocol is exposed. i.e., by default all link clicks must
910 // go through the external protocol service. most applications override this
911 // default behavior.
912 *aResult = Preferences::GetBool("network.protocol-handler.expose-all", false);
914 return NS_OK;
917 static const char kExternalProtocolPrefPrefix[] =
918 "network.protocol-handler.external.";
919 static const char kExternalProtocolDefaultPref[] =
920 "network.protocol-handler.external-default";
922 // static
923 nsresult nsExternalHelperAppService::EscapeURI(nsIURI* aURI, nsIURI** aResult) {
924 MOZ_ASSERT(aURI);
925 MOZ_ASSERT(aResult);
927 nsAutoCString spec;
928 aURI->GetSpec(spec);
930 if (spec.Find("%00") != -1) return NS_ERROR_MALFORMED_URI;
932 nsAutoCString escapedSpec;
933 nsresult rv = NS_EscapeURL(spec, esc_AlwaysCopy | esc_ExtHandler, escapedSpec,
934 fallible);
935 NS_ENSURE_SUCCESS(rv, rv);
937 nsCOMPtr<nsIIOService> ios(do_GetIOService());
938 return ios->NewURI(escapedSpec, nullptr, nullptr, aResult);
941 bool ExternalProtocolIsBlockedBySandbox(
942 BrowsingContext* aBrowsingContext,
943 const bool aHasValidUserGestureActivation) {
944 if (!StaticPrefs::dom_block_external_protocol_navigation_from_sandbox()) {
945 return false;
948 if (!aBrowsingContext || aBrowsingContext->IsTop()) {
949 return false;
952 uint32_t sandboxFlags = aBrowsingContext->GetSandboxFlags();
954 if (sandboxFlags == SANDBOXED_NONE) {
955 return false;
958 if (!(sandboxFlags & SANDBOXED_AUXILIARY_NAVIGATION)) {
959 return false;
962 if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION)) {
963 return false;
966 if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_CUSTOM_PROTOCOLS)) {
967 return false;
970 if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION) &&
971 aHasValidUserGestureActivation) {
972 return false;
975 return true;
978 NS_IMETHODIMP
979 nsExternalHelperAppService::LoadURI(nsIURI* aURI,
980 nsIPrincipal* aTriggeringPrincipal,
981 nsIPrincipal* aRedirectPrincipal,
982 BrowsingContext* aBrowsingContext,
983 bool aTriggeredExternally,
984 bool aHasValidUserGestureActivation) {
985 NS_ENSURE_ARG_POINTER(aURI);
987 if (XRE_IsContentProcess()) {
988 mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal(
989 aURI, aTriggeringPrincipal, aRedirectPrincipal, aBrowsingContext,
990 aTriggeredExternally, aHasValidUserGestureActivation);
991 return NS_OK;
994 // Prevent sandboxed BrowsingContexts from navigating to external protocols.
995 // This only uses the sandbox flags of the target BrowsingContext of the
996 // load. The navigating document's CSP sandbox flags do not apply.
997 if (aBrowsingContext &&
998 ExternalProtocolIsBlockedBySandbox(aBrowsingContext,
999 aHasValidUserGestureActivation)) {
1000 // Log an error to the web console of the sandboxed BrowsingContext.
1001 nsAutoString localizedMsg;
1002 nsAutoCString spec;
1003 aURI->GetSpec(spec);
1005 AutoTArray<nsString, 1> params = {NS_ConvertUTF8toUTF16(spec)};
1006 nsresult rv = nsContentUtils::FormatLocalizedString(
1007 nsContentUtils::eSECURITY_PROPERTIES, "SandboxBlockedCustomProtocols",
1008 params, localizedMsg);
1009 NS_ENSURE_SUCCESS(rv, rv);
1011 // Log to the the parent window of the iframe. If there is no parent, fall
1012 // back to the iframe window itself.
1013 WindowContext* windowContext = aBrowsingContext->GetParentWindowContext();
1014 if (!windowContext) {
1015 windowContext = aBrowsingContext->GetCurrentWindowContext();
1018 // Skip logging if we still don't have a WindowContext.
1019 NS_ENSURE_TRUE(windowContext, NS_ERROR_FAILURE);
1021 nsContentUtils::ReportToConsoleByWindowID(
1022 localizedMsg, nsIScriptError::errorFlag, "Security"_ns,
1023 windowContext->InnerWindowId(),
1024 windowContext->Canonical()->GetDocumentURI());
1026 return NS_OK;
1029 nsCOMPtr<nsIURI> escapedURI;
1030 nsresult rv = EscapeURI(aURI, getter_AddRefs(escapedURI));
1031 NS_ENSURE_SUCCESS(rv, rv);
1033 nsAutoCString scheme;
1034 escapedURI->GetScheme(scheme);
1035 if (scheme.IsEmpty()) return NS_OK; // must have a scheme
1037 // Deny load if the prefs say to do so
1038 nsAutoCString externalPref(kExternalProtocolPrefPrefix);
1039 externalPref += scheme;
1040 bool allowLoad = false;
1041 if (NS_FAILED(Preferences::GetBool(externalPref.get(), &allowLoad))) {
1042 // no scheme-specific value, check the default
1043 if (NS_FAILED(
1044 Preferences::GetBool(kExternalProtocolDefaultPref, &allowLoad))) {
1045 return NS_OK; // missing default pref
1049 if (!allowLoad) {
1050 return NS_OK; // explicitly denied
1053 // Now check if the principal is allowed to access the navigated context.
1054 // We allow navigating subframes, even if not same-origin - non-external
1055 // links can always navigate everywhere, so this is a minor additional
1056 // restriction, only aiming to prevent some types of spoofing attacks
1057 // from otherwise disjoint browsingcontext trees.
1058 if (aBrowsingContext && aTriggeringPrincipal &&
1059 !StaticPrefs::security_allow_disjointed_external_uri_loads() &&
1060 // Add-on principals are always allowed:
1061 !BasePrincipal::Cast(aTriggeringPrincipal)->AddonPolicy() &&
1062 // As is chrome code:
1063 !aTriggeringPrincipal->IsSystemPrincipal()) {
1064 RefPtr<BrowsingContext> bc = aBrowsingContext;
1065 WindowGlobalParent* wgp = bc->Canonical()->GetCurrentWindowGlobal();
1066 bool foundAccessibleFrame = false;
1068 // Also allow this load if the target is a toplevel BC and contains a
1069 // non-web-controlled about:blank document
1070 if (bc->IsTop() && !bc->HadOriginalOpener() && wgp) {
1071 RefPtr<nsIURI> uri = wgp->GetDocumentURI();
1072 foundAccessibleFrame =
1073 uri && uri->GetSpecOrDefault().EqualsLiteral("about:blank");
1076 while (!foundAccessibleFrame) {
1077 if (wgp) {
1078 foundAccessibleFrame =
1079 aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal());
1081 // We have to get the parent via the bc, because there may not
1082 // be a window global for the innermost bc; see bug 1650162.
1083 BrowsingContext* parent = bc->GetParent();
1084 if (!parent) {
1085 break;
1087 bc = parent;
1088 wgp = parent->Canonical()->GetCurrentWindowGlobal();
1091 if (!foundAccessibleFrame) {
1092 // See if this navigation could have come from a subframe.
1093 nsTArray<RefPtr<BrowsingContext>> contexts;
1094 aBrowsingContext->GetAllBrowsingContextsInSubtree(contexts);
1095 for (const auto& kid : contexts) {
1096 wgp = kid->Canonical()->GetCurrentWindowGlobal();
1097 if (wgp && aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal())) {
1098 foundAccessibleFrame = true;
1099 break;
1104 if (!foundAccessibleFrame) {
1105 return NS_OK; // deny the load.
1109 nsCOMPtr<nsIHandlerInfo> handler;
1110 rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler));
1111 NS_ENSURE_SUCCESS(rv, rv);
1113 nsCOMPtr<nsIContentDispatchChooser> chooser =
1114 do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv);
1115 NS_ENSURE_SUCCESS(rv, rv);
1117 return chooser->HandleURI(
1118 handler, escapedURI,
1119 aRedirectPrincipal ? aRedirectPrincipal : aTriggeringPrincipal,
1120 aBrowsingContext, aTriggeredExternally);
1123 //////////////////////////////////////////////////////////////////////////////////////////////////////
1124 // Methods related to deleting temporary files on exit
1125 //////////////////////////////////////////////////////////////////////////////////////////////////////
1127 /* static */
1128 nsresult nsExternalHelperAppService::DeleteTemporaryFileHelper(
1129 nsIFile* aTemporaryFile, nsCOMArray<nsIFile>& aFileList) {
1130 bool isFile = false;
1132 // as a safety measure, make sure the nsIFile is really a file and not a
1133 // directory object.
1134 aTemporaryFile->IsFile(&isFile);
1135 if (!isFile) return NS_OK;
1137 aFileList.AppendObject(aTemporaryFile);
1139 return NS_OK;
1142 NS_IMETHODIMP
1143 nsExternalHelperAppService::DeleteTemporaryFileOnExit(nsIFile* aTemporaryFile) {
1144 return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryFilesList);
1147 NS_IMETHODIMP
1148 nsExternalHelperAppService::DeleteTemporaryPrivateFileWhenPossible(
1149 nsIFile* aTemporaryFile) {
1150 return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryPrivateFilesList);
1153 void nsExternalHelperAppService::ExpungeTemporaryFilesHelper(
1154 nsCOMArray<nsIFile>& fileList) {
1155 int32_t numEntries = fileList.Count();
1156 nsIFile* localFile;
1157 for (int32_t index = 0; index < numEntries; index++) {
1158 localFile = fileList[index];
1159 if (localFile) {
1160 // First make the file writable, since the temp file is probably readonly.
1161 localFile->SetPermissions(0600);
1162 localFile->Remove(false);
1166 fileList.Clear();
1169 void nsExternalHelperAppService::ExpungeTemporaryFiles() {
1170 ExpungeTemporaryFilesHelper(mTemporaryFilesList);
1173 void nsExternalHelperAppService::ExpungeTemporaryPrivateFiles() {
1174 ExpungeTemporaryFilesHelper(mTemporaryPrivateFilesList);
1177 static const char kExternalWarningPrefPrefix[] =
1178 "network.protocol-handler.warn-external.";
1179 static const char kExternalWarningDefaultPref[] =
1180 "network.protocol-handler.warn-external-default";
1182 NS_IMETHODIMP
1183 nsExternalHelperAppService::GetProtocolHandlerInfo(
1184 const nsACString& aScheme, nsIHandlerInfo** aHandlerInfo) {
1185 // XXX enterprise customers should be able to turn this support off with a
1186 // single master pref (maybe use one of the "exposed" prefs here?)
1188 bool exists;
1189 nsresult rv = GetProtocolHandlerInfoFromOS(aScheme, &exists, aHandlerInfo);
1190 if (NS_FAILED(rv)) {
1191 // Either it knows nothing, or we ran out of memory
1192 return NS_ERROR_FAILURE;
1195 nsCOMPtr<nsIHandlerService> handlerSvc =
1196 do_GetService(NS_HANDLERSERVICE_CONTRACTID);
1197 if (handlerSvc) {
1198 bool hasHandler = false;
1199 (void)handlerSvc->Exists(*aHandlerInfo, &hasHandler);
1200 if (hasHandler) {
1201 rv = handlerSvc->FillHandlerInfo(*aHandlerInfo, ""_ns);
1202 if (NS_SUCCEEDED(rv)) return NS_OK;
1206 return SetProtocolHandlerDefaults(*aHandlerInfo, exists);
1209 NS_IMETHODIMP
1210 nsExternalHelperAppService::SetProtocolHandlerDefaults(
1211 nsIHandlerInfo* aHandlerInfo, bool aOSHandlerExists) {
1212 // this type isn't in our database, so we've only got an OS default handler,
1213 // if one exists
1215 if (aOSHandlerExists) {
1216 // we've got a default, so use it
1217 aHandlerInfo->SetPreferredAction(nsIHandlerInfo::useSystemDefault);
1219 // whether or not to ask the user depends on the warning preference
1220 nsAutoCString scheme;
1221 aHandlerInfo->GetType(scheme);
1223 nsAutoCString warningPref(kExternalWarningPrefPrefix);
1224 warningPref += scheme;
1225 bool warn;
1226 if (NS_FAILED(Preferences::GetBool(warningPref.get(), &warn))) {
1227 // no scheme-specific value, check the default
1228 warn = Preferences::GetBool(kExternalWarningDefaultPref, true);
1230 aHandlerInfo->SetAlwaysAskBeforeHandling(warn);
1231 } else {
1232 // If no OS default existed, we set the preferred action to alwaysAsk.
1233 // This really means not initialized (i.e. there's no available handler)
1234 // to all the code...
1235 aHandlerInfo->SetPreferredAction(nsIHandlerInfo::alwaysAsk);
1238 return NS_OK;
1241 // XPCOM profile change observer
1242 NS_IMETHODIMP
1243 nsExternalHelperAppService::Observe(nsISupports* aSubject, const char* aTopic,
1244 const char16_t* someData) {
1245 if (!strcmp(aTopic, "profile-before-change")) {
1246 ExpungeTemporaryFiles();
1247 } else if (!strcmp(aTopic, "last-pb-context-exited")) {
1248 ExpungeTemporaryPrivateFiles();
1250 return NS_OK;
1253 //////////////////////////////////////////////////////////////////////////////////////////////////////
1254 // begin external app handler implementation
1255 //////////////////////////////////////////////////////////////////////////////////////////////////////
1257 NS_IMPL_ADDREF(nsExternalAppHandler)
1258 NS_IMPL_RELEASE(nsExternalAppHandler)
1260 NS_INTERFACE_MAP_BEGIN(nsExternalAppHandler)
1261 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener)
1262 NS_INTERFACE_MAP_ENTRY(nsIStreamListener)
1263 NS_INTERFACE_MAP_ENTRY(nsIRequestObserver)
1264 NS_INTERFACE_MAP_ENTRY(nsIHelperAppLauncher)
1265 NS_INTERFACE_MAP_ENTRY(nsICancelable)
1266 NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver)
1267 NS_INTERFACE_MAP_ENTRY(nsINamed)
1268 NS_INTERFACE_MAP_ENTRY_CONCRETE(nsExternalAppHandler)
1269 NS_INTERFACE_MAP_END
1271 nsExternalAppHandler::nsExternalAppHandler(
1272 nsIMIMEInfo* aMIMEInfo, const nsAString& aFileExtension,
1273 BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext,
1274 nsExternalHelperAppService* aExtProtSvc,
1275 const nsAString& aSuggestedFileName, uint32_t aReason, bool aForceSave)
1276 : mMimeInfo(aMIMEInfo),
1277 mBrowsingContext(aBrowsingContext),
1278 mWindowContext(aWindowContext),
1279 mSuggestedFileName(aSuggestedFileName),
1280 mForceSave(aForceSave),
1281 mCanceled(false),
1282 mStopRequestIssued(false),
1283 mIsFileChannel(false),
1284 mShouldCloseWindow(false),
1285 mHandleInternally(false),
1286 mDialogShowing(false),
1287 mReason(aReason),
1288 mTempFileIsExecutable(false),
1289 mTimeDownloadStarted(0),
1290 mContentLength(-1),
1291 mProgress(0),
1292 mSaver(nullptr),
1293 mDialogProgressListener(nullptr),
1294 mTransfer(nullptr),
1295 mRequest(nullptr),
1296 mExtProtSvc(aExtProtSvc) {
1297 // make sure the extention includes the '.'
1298 if (!aFileExtension.IsEmpty() && aFileExtension.First() != '.') {
1299 mFileExtension = char16_t('.');
1301 mFileExtension.Append(aFileExtension);
1303 mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096);
1306 nsExternalAppHandler::~nsExternalAppHandler() {
1307 MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted");
1310 void nsExternalAppHandler::DidDivertRequest(nsIRequest* request) {
1311 MOZ_ASSERT(XRE_IsContentProcess(), "in child process");
1312 // Remove our request from the child loadGroup
1313 RetargetLoadNotifications(request);
1316 NS_IMETHODIMP nsExternalAppHandler::SetWebProgressListener(
1317 nsIWebProgressListener2* aWebProgressListener) {
1318 // This is always called by nsHelperDlg.js. Go ahead and register the
1319 // progress listener. At this point, we don't have mTransfer.
1320 mDialogProgressListener = aWebProgressListener;
1321 return NS_OK;
1324 NS_IMETHODIMP nsExternalAppHandler::GetTargetFile(nsIFile** aTarget) {
1325 if (mFinalFileDestination)
1326 *aTarget = mFinalFileDestination;
1327 else
1328 *aTarget = mTempFile;
1330 NS_IF_ADDREF(*aTarget);
1331 return NS_OK;
1334 NS_IMETHODIMP nsExternalAppHandler::GetTargetFileIsExecutable(bool* aExec) {
1335 // Use the real target if it's been set
1336 if (mFinalFileDestination) return mFinalFileDestination->IsExecutable(aExec);
1338 // Otherwise, use the stored executable-ness of the temporary
1339 *aExec = mTempFileIsExecutable;
1340 return NS_OK;
1343 NS_IMETHODIMP nsExternalAppHandler::GetTimeDownloadStarted(PRTime* aTime) {
1344 *aTime = mTimeDownloadStarted;
1345 return NS_OK;
1348 NS_IMETHODIMP nsExternalAppHandler::GetContentLength(int64_t* aContentLength) {
1349 *aContentLength = mContentLength;
1350 return NS_OK;
1353 NS_IMETHODIMP nsExternalAppHandler::GetBrowsingContextId(
1354 uint64_t* aBrowsingContextId) {
1355 *aBrowsingContextId = mBrowsingContext ? mBrowsingContext->Id() : 0;
1356 return NS_OK;
1359 void nsExternalAppHandler::RetargetLoadNotifications(nsIRequest* request) {
1360 // we are going to run the downloading of the helper app in our own little
1361 // docloader / load group context. so go ahead and force the creation of a
1362 // load group and doc loader for us to use...
1363 nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
1364 if (!aChannel) return;
1366 bool isPrivate = NS_UsePrivateBrowsing(aChannel);
1368 nsCOMPtr<nsILoadGroup> oldLoadGroup;
1369 aChannel->GetLoadGroup(getter_AddRefs(oldLoadGroup));
1371 if (oldLoadGroup) {
1372 oldLoadGroup->RemoveRequest(request, nullptr, NS_BINDING_RETARGETED);
1375 aChannel->SetLoadGroup(nullptr);
1376 aChannel->SetNotificationCallbacks(nullptr);
1378 nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel);
1379 if (pbChannel) {
1380 pbChannel->SetPrivate(isPrivate);
1384 nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) {
1385 // First we need to try to get the destination directory for the temporary
1386 // file.
1387 nsresult rv = GetDownloadDirectory(getter_AddRefs(mTempFile));
1388 NS_ENSURE_SUCCESS(rv, rv);
1390 // At this point, we do not have a filename for the temp file. For security
1391 // purposes, this cannot be predictable, so we must use a cryptographic
1392 // quality PRNG to generate one.
1393 nsAutoCString tempLeafName;
1394 rv = GenerateRandomName(tempLeafName);
1395 NS_ENSURE_SUCCESS(rv, rv);
1397 // now append our extension.
1398 nsAutoCString ext;
1399 mMimeInfo->GetPrimaryExtension(ext);
1400 if (!ext.IsEmpty()) {
1401 ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
1402 if (ext.First() != '.') tempLeafName.Append('.');
1403 tempLeafName.Append(ext);
1406 // We need to temporarily create a dummy file with the correct
1407 // file extension to determine the executable-ness, so do this before adding
1408 // the extra .part extension.
1409 nsCOMPtr<nsIFile> dummyFile;
1410 rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dummyFile));
1411 NS_ENSURE_SUCCESS(rv, rv);
1413 // Set the file name without .part
1414 rv = dummyFile->Append(NS_ConvertUTF8toUTF16(tempLeafName));
1415 NS_ENSURE_SUCCESS(rv, rv);
1416 rv = dummyFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
1417 NS_ENSURE_SUCCESS(rv, rv);
1419 // Store executable-ness then delete
1420 dummyFile->IsExecutable(&mTempFileIsExecutable);
1421 dummyFile->Remove(false);
1423 // Add an additional .part to prevent the OS from running this file in the
1424 // default application.
1425 tempLeafName.AppendLiteral(".part");
1427 rv = mTempFile->Append(NS_ConvertUTF8toUTF16(tempLeafName));
1428 // make this file unique!!!
1429 NS_ENSURE_SUCCESS(rv, rv);
1430 rv = mTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
1431 NS_ENSURE_SUCCESS(rv, rv);
1433 // Now save the temp leaf name, minus the ".part" bit, so we can use it later.
1434 // This is a bit broken in the case when createUnique actually had to append
1435 // some numbers, because then we now have a filename like foo.bar-1.part and
1436 // we'll end up with foo.bar-1.bar as our final filename if we end up using
1437 // this. But the other options are all bad too.... Ideally we'd have a way
1438 // to tell createUnique to put its unique marker before the extension that
1439 // comes before ".part" or something.
1440 rv = mTempFile->GetLeafName(mTempLeafName);
1441 NS_ENSURE_SUCCESS(rv, rv);
1443 NS_ENSURE_TRUE(StringEndsWith(mTempLeafName, u".part"_ns),
1444 NS_ERROR_UNEXPECTED);
1446 // Strip off the ".part" from mTempLeafName
1447 mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1);
1449 MOZ_ASSERT(!mSaver, "Output file initialization called more than once!");
1450 mSaver =
1451 do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv);
1452 NS_ENSURE_SUCCESS(rv, rv);
1454 rv = mSaver->SetObserver(this);
1455 if (NS_FAILED(rv)) {
1456 mSaver = nullptr;
1457 return rv;
1460 rv = mSaver->EnableSha256();
1461 NS_ENSURE_SUCCESS(rv, rv);
1463 rv = mSaver->EnableSignatureInfo();
1464 NS_ENSURE_SUCCESS(rv, rv);
1465 LOG("Enabled hashing and signature verification");
1467 rv = mSaver->SetTarget(mTempFile, false);
1468 NS_ENSURE_SUCCESS(rv, rv);
1470 return rv;
1473 void nsExternalAppHandler::MaybeApplyDecodingForExtension(
1474 nsIRequest* aRequest) {
1475 MOZ_ASSERT(aRequest);
1477 nsCOMPtr<nsIEncodedChannel> encChannel = do_QueryInterface(aRequest);
1478 if (!encChannel) {
1479 return;
1482 // Turn off content encoding conversions if needed
1483 bool applyConversion = true;
1485 // First, check to see if conversion is already disabled. If so, we
1486 // have nothing to do here.
1487 encChannel->GetApplyConversion(&applyConversion);
1488 if (!applyConversion) {
1489 return;
1492 nsCOMPtr<nsIURL> sourceURL(do_QueryInterface(mSourceUrl));
1493 if (sourceURL) {
1494 nsAutoCString extension;
1495 sourceURL->GetFileExtension(extension);
1496 if (!extension.IsEmpty()) {
1497 nsCOMPtr<nsIUTF8StringEnumerator> encEnum;
1498 encChannel->GetContentEncodings(getter_AddRefs(encEnum));
1499 if (encEnum) {
1500 bool hasMore;
1501 nsresult rv = encEnum->HasMore(&hasMore);
1502 if (NS_SUCCEEDED(rv) && hasMore) {
1503 nsAutoCString encType;
1504 rv = encEnum->GetNext(encType);
1505 if (NS_SUCCEEDED(rv) && !encType.IsEmpty()) {
1506 MOZ_ASSERT(mExtProtSvc);
1507 mExtProtSvc->ApplyDecodingForExtension(extension, encType,
1508 &applyConversion);
1515 encChannel->SetApplyConversion(applyConversion);
1518 already_AddRefed<nsIInterfaceRequestor>
1519 nsExternalAppHandler::GetDialogParent() {
1520 nsCOMPtr<nsIInterfaceRequestor> dialogParent = mWindowContext;
1522 if (!dialogParent && mBrowsingContext) {
1523 dialogParent = do_QueryInterface(mBrowsingContext->GetDOMWindow());
1525 if (!dialogParent && mBrowsingContext && XRE_IsParentProcess()) {
1526 RefPtr<Element> element = mBrowsingContext->Top()->GetEmbedderElement();
1527 if (element) {
1528 dialogParent = do_QueryInterface(element->OwnerDoc()->GetWindow());
1531 return dialogParent.forget();
1534 NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
1535 MOZ_ASSERT(request, "OnStartRequest without request?");
1537 // Set mTimeDownloadStarted here as the download has already started and
1538 // we want to record the start time before showing the filepicker.
1539 mTimeDownloadStarted = PR_Now();
1541 mRequest = request;
1543 nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
1545 nsresult rv;
1546 nsAutoCString MIMEType;
1547 if (mMimeInfo) {
1548 mMimeInfo->GetMIMEType(MIMEType);
1550 // Now get the URI
1551 if (aChannel) {
1552 aChannel->GetURI(getter_AddRefs(mSourceUrl));
1553 // HTTPS-Only/HTTPS-FirstMode tries to upgrade connections to https. Once
1554 // the download is in progress we set that flag so that timeout counter
1555 // measures do not kick in.
1556 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1557 bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
1558 if (nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(isPrivateWin) ||
1559 nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(isPrivateWin)) {
1560 uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
1561 httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_DOWNLOAD_IN_PROGRESS;
1562 loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
1566 if (!mForceSave && StaticPrefs::browser_download_enable_spam_prevention() &&
1567 IsDownloadSpam(aChannel)) {
1568 RecordDownloadTelemetry(aChannel, "spam");
1569 return NS_OK;
1572 mDownloadClassification =
1573 nsContentSecurityUtils::ClassifyDownload(aChannel, MIMEType);
1575 if (mDownloadClassification == nsITransfer::DOWNLOAD_FORBIDDEN) {
1576 // If the download is rated as forbidden,
1577 // cancel the request so no ui knows about this.
1578 mCanceled = true;
1579 request->Cancel(NS_ERROR_ABORT);
1580 RecordDownloadTelemetry(aChannel, "forbidden");
1581 return NS_OK;
1584 nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(request));
1585 mIsFileChannel = fileChan != nullptr;
1586 if (!mIsFileChannel) {
1587 // It's possible that this request came from the child process and the
1588 // file channel actually lives there. If this returns true, then our
1589 // mSourceUrl will be an nsIFileURL anyway.
1590 nsCOMPtr<dom::nsIExternalHelperAppParent> parent(
1591 do_QueryInterface(request));
1592 mIsFileChannel = parent && parent->WasFileChannel();
1595 // Get content length
1596 if (aChannel) {
1597 aChannel->GetContentLength(&mContentLength);
1600 if (mBrowsingContext) {
1601 mMaybeCloseWindowHelper = new MaybeCloseWindowHelper(mBrowsingContext);
1602 mMaybeCloseWindowHelper->SetShouldCloseWindow(mShouldCloseWindow);
1603 nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(request, &rv));
1604 // Determine whether a new window was opened specifically for this request
1605 if (props) {
1606 bool tmp = false;
1607 if (NS_SUCCEEDED(
1608 props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns, &tmp))) {
1609 mMaybeCloseWindowHelper->SetShouldCloseWindow(tmp);
1614 // retarget all load notifications to our docloader instead of the original
1615 // window's docloader...
1616 RetargetLoadNotifications(request);
1618 // Close the underlying DOMWindow if it was opened specifically for the
1619 // download. We don't run this in the content process, since we have
1620 // an instance running in the parent as well, which will handle this
1621 // if needed.
1622 if (!XRE_IsContentProcess() && mMaybeCloseWindowHelper) {
1623 mBrowsingContext = mMaybeCloseWindowHelper->MaybeCloseWindow();
1626 // In an IPC setting, we're allowing the child process, here, to make
1627 // decisions about decoding the channel (e.g. decompression). It will
1628 // still forward the decoded (uncompressed) data back to the parent.
1629 // Con: Uncompressed data means more IPC overhead.
1630 // Pros: ExternalHelperAppParent doesn't need to implement nsIEncodedChannel.
1631 // Parent process doesn't need to expect CPU time on decompression.
1632 MaybeApplyDecodingForExtension(aChannel);
1634 // At this point, the child process has done everything it can usefully do
1635 // for OnStartRequest.
1636 if (XRE_IsContentProcess()) {
1637 return NS_OK;
1640 rv = SetUpTempFile(aChannel);
1641 if (NS_FAILED(rv)) {
1642 nsresult transferError = rv;
1644 rv = CreateFailedTransfer();
1645 if (NS_FAILED(rv)) {
1646 LOG("Failed to create transfer to report failure."
1647 "Will fallback to prompter!");
1650 mCanceled = true;
1651 request->Cancel(transferError);
1653 nsAutoString path;
1654 if (mTempFile) mTempFile->GetPath(path);
1656 SendStatusChange(kWriteError, transferError, request, path);
1658 RecordDownloadTelemetry(aChannel, "savefailed");
1660 return NS_OK;
1663 // Inform channel it is open on behalf of a download to throttle it during
1664 // page loads and prevent its caching.
1665 nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(aChannel);
1666 if (httpInternal) {
1667 rv = httpInternal->SetChannelIsForDownload(true);
1668 MOZ_ASSERT(NS_SUCCEEDED(rv));
1671 if (mSourceUrl->SchemeIs("data")) {
1672 // In case we're downloading a data:// uri
1673 // we don't want to apply AllowTopLevelNavigationToDataURI.
1674 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1675 loadInfo->SetForceAllowDataURI(true);
1678 // now that the temp file is set up, find out if we need to invoke a dialog
1679 // asking the user what they want us to do with this content...
1681 // We can get here for three reasons: "can't handle", "sniffed type", or
1682 // "server sent content-disposition:attachment". In the first case we want
1683 // to honor the user's "always ask" pref; in the other two cases we want to
1684 // honor it only if the default action is "save". Opening attachments in
1685 // helper apps by default breaks some websites (especially if the attachment
1686 // is one part of a multipart document). Opening sniffed content in helper
1687 // apps by default introduces security holes that we'd rather not have.
1689 // So let's find out whether the user wants to be prompted. If he does not,
1690 // check mReason and the preferred action to see what we should do.
1692 bool alwaysAsk = true;
1693 mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk);
1694 if (alwaysAsk) {
1695 // But we *don't* ask if this mimeInfo didn't come from
1696 // our user configuration datastore and the user has said
1697 // at some point in the distant past that they don't
1698 // want to be asked. The latter fact would have been
1699 // stored in pref strings back in the old days.
1701 bool mimeTypeIsInDatastore = false;
1702 nsCOMPtr<nsIHandlerService> handlerSvc =
1703 do_GetService(NS_HANDLERSERVICE_CONTRACTID);
1704 if (handlerSvc) {
1705 handlerSvc->Exists(mMimeInfo, &mimeTypeIsInDatastore);
1707 if (!handlerSvc || !mimeTypeIsInDatastore) {
1708 if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_SAVE_TO_DISK_PREF,
1709 MIMEType.get())) {
1710 // Don't need to ask after all.
1711 alwaysAsk = false;
1712 // Make sure action matches pref (save to disk).
1713 mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
1714 } else if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_OPEN_FILE_PREF,
1715 MIMEType.get())) {
1716 // Don't need to ask after all.
1717 alwaysAsk = false;
1720 } else if (MIMEType.EqualsLiteral("text/plain")) {
1721 nsAutoCString ext;
1722 mMimeInfo->GetPrimaryExtension(ext);
1723 // If people are sending us ApplicationReputation-eligible files with
1724 // text/plain mimetypes, enforce asking the user what to do.
1725 if (!ext.IsEmpty()) {
1726 nsAutoCString dummyFileName("f");
1727 if (ext.First() != '.') {
1728 dummyFileName.Append(".");
1730 ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
1731 nsCOMPtr<nsIApplicationReputationService> appRep =
1732 components::ApplicationReputation::Service();
1733 appRep->IsBinary(dummyFileName + ext, &alwaysAsk);
1737 int32_t action = nsIMIMEInfo::saveToDisk;
1738 mMimeInfo->GetPreferredAction(&action);
1740 bool forcePrompt =
1741 mReason == nsIHelperAppLauncherDialog::REASON_TYPESNIFFED ||
1742 (mReason == nsIHelperAppLauncherDialog::REASON_SERVERREQUEST &&
1743 !StaticPrefs::browser_download_improvements_to_download_panel());
1745 // OK, now check why we're here
1746 if (!alwaysAsk && forcePrompt) {
1747 // Force asking if we're not saving. See comment back when we fetched the
1748 // alwaysAsk boolean for details.
1749 alwaysAsk = (action != nsIMIMEInfo::saveToDisk);
1752 bool shouldAutomaticallyHandleInternally =
1753 action == nsIMIMEInfo::handleInternally &&
1754 StaticPrefs::browser_download_improvements_to_download_panel();
1756 // If we're not asking, check we actually know what to do:
1757 if (!alwaysAsk) {
1758 alwaysAsk = action != nsIMIMEInfo::saveToDisk &&
1759 action != nsIMIMEInfo::useHelperApp &&
1760 action != nsIMIMEInfo::useSystemDefault &&
1761 !shouldAutomaticallyHandleInternally;
1764 // If we're handling with the OS default and we are that default, force
1765 // asking, so we don't end up in an infinite loop:
1766 if (!alwaysAsk && action == nsIMIMEInfo::useSystemDefault) {
1767 bool areOSDefault = false;
1768 alwaysAsk = NS_SUCCEEDED(mMimeInfo->IsCurrentAppOSDefault(&areOSDefault)) &&
1769 areOSDefault;
1770 } else if (!alwaysAsk && action == nsIMIMEInfo::useHelperApp) {
1771 nsCOMPtr<nsIHandlerApp> preferredApp;
1772 mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(preferredApp));
1773 nsCOMPtr<nsILocalHandlerApp> handlerApp = do_QueryInterface(preferredApp);
1774 if (handlerApp) {
1775 nsCOMPtr<nsIFile> executable;
1776 handlerApp->GetExecutable(getter_AddRefs(executable));
1777 nsCOMPtr<nsIFile> ourselves;
1778 if (executable &&
1779 // Despite the name, this really just fetches an nsIFile...
1780 NS_SUCCEEDED(NS_GetSpecialDirectory(XRE_EXECUTABLE_FILE,
1781 getter_AddRefs(ourselves)))) {
1782 ourselves = nsMIMEInfoBase::GetCanonicalExecutable(ourselves);
1783 executable = nsMIMEInfoBase::GetCanonicalExecutable(executable);
1784 bool isSameApp = false;
1785 alwaysAsk =
1786 NS_FAILED(executable->Equals(ourselves, &isSameApp)) || isSameApp;
1791 // if we were told that we _must_ save to disk without asking, all the stuff
1792 // before this is irrelevant; override it
1793 if (mForceSave) {
1794 alwaysAsk = false;
1795 action = nsIMIMEInfo::saveToDisk;
1796 shouldAutomaticallyHandleInternally = false;
1798 // Additionally, if we are asked by the OS to open a local file,
1799 // automatically downloading it to create a second copy of that file doesn't
1800 // really make sense. We should ask the user what they want to do.
1801 if (mSourceUrl->SchemeIs("file") && !alwaysAsk &&
1802 action == nsIMIMEInfo::saveToDisk) {
1803 alwaysAsk = true;
1806 // If adding new checks, make sure this is the last check before telemetry
1807 // and going ahead with opening the file!
1808 #ifdef XP_WIN
1809 /* We need to see whether the file we've got here could be
1810 * executable. If it could, we had better not try to open it!
1811 * We can skip this check, though, if we have a setting to open in a
1812 * helper app.
1814 if (!alwaysAsk && action != nsIMIMEInfo::saveToDisk &&
1815 !shouldAutomaticallyHandleInternally) {
1816 nsCOMPtr<nsIHandlerApp> prefApp;
1817 mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(prefApp));
1818 if (action != nsIMIMEInfo::useHelperApp || !prefApp) {
1819 nsCOMPtr<nsIFile> fileToTest;
1820 GetTargetFile(getter_AddRefs(fileToTest));
1821 if (fileToTest) {
1822 bool isExecutable;
1823 rv = fileToTest->IsExecutable(&isExecutable);
1824 if (NS_FAILED(rv) || mTempFileIsExecutable ||
1825 isExecutable) { // checking NS_FAILED, because paranoia is good
1826 alwaysAsk = true;
1828 } else { // Paranoia is good here too, though this really should not
1829 // happen
1830 NS_WARNING(
1831 "GetDownloadInfo returned a null file after the temp file has been "
1832 "set up! ");
1833 alwaysAsk = true;
1837 #endif
1839 nsAutoCString actionTelem;
1840 if (alwaysAsk) {
1841 actionTelem.AssignLiteral("ask");
1842 } else if (shouldAutomaticallyHandleInternally) {
1843 actionTelem.AssignLiteral("internal");
1844 } else if (action == nsIMIMEInfo::useHelperApp ||
1845 action == nsIMIMEInfo::useSystemDefault) {
1846 actionTelem.AssignLiteral("external");
1847 } else {
1848 actionTelem.AssignLiteral("save");
1851 RecordDownloadTelemetry(aChannel, actionTelem.get());
1853 if (alwaysAsk) {
1854 // Display the dialog
1855 mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv);
1856 NS_ENSURE_SUCCESS(rv, rv);
1858 // this will create a reference cycle (the dialog holds a reference to us as
1859 // nsIHelperAppLauncher), which will be broken in Cancel or CreateTransfer.
1860 nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
1861 // Don't pop up the downloads panel since we're already going to pop up the
1862 // UCT dialog for basically the same effect.
1863 mDialogShowing = true;
1864 rv = mDialog->Show(this, dialogParent, mReason);
1866 // what do we do if the dialog failed? I guess we should call Cancel and
1867 // abort the load....
1868 } else {
1869 // We need to do the save/open immediately, then.
1870 if (action == nsIMIMEInfo::useHelperApp ||
1871 action == nsIMIMEInfo::useSystemDefault ||
1872 shouldAutomaticallyHandleInternally) {
1873 // Check if the file is local, in which case just launch it from where it
1874 // is. Otherwise, set the file to launch once it's finished downloading.
1875 rv = mIsFileChannel ? LaunchLocalFile()
1876 : SetDownloadToLaunch(
1877 shouldAutomaticallyHandleInternally, nullptr);
1878 } else {
1879 rv = PromptForSaveDestination();
1882 return NS_OK;
1885 void nsExternalAppHandler::RecordDownloadTelemetry(nsIChannel* aChannel,
1886 const char* aAction) {
1887 // Telemetry for helper app dialog
1889 if (XRE_IsContentProcess()) {
1890 return;
1893 nsAutoCString reason;
1894 switch (mReason) {
1895 case nsIHelperAppLauncherDialog::REASON_SERVERREQUEST:
1896 reason.AssignLiteral("attachment");
1897 break;
1898 case nsIHelperAppLauncherDialog::REASON_TYPESNIFFED:
1899 reason.AssignLiteral("sniffed");
1900 break;
1901 case nsIHelperAppLauncherDialog::REASON_CANTHANDLE:
1902 default:
1903 reason.AssignLiteral("other");
1904 break;
1907 nsAutoCString contentTypeTelem;
1908 nsAutoCString contentType;
1909 aChannel->GetContentType(contentType);
1910 if (contentType.EqualsIgnoreCase(APPLICATION_PDF)) {
1911 contentTypeTelem.AssignLiteral("pdf");
1912 } else if (contentType.EqualsIgnoreCase(APPLICATION_OCTET_STREAM) ||
1913 contentType.EqualsIgnoreCase(BINARY_OCTET_STREAM)) {
1914 contentTypeTelem.AssignLiteral("octetstream");
1915 } else {
1916 contentTypeTelem.AssignLiteral("other");
1919 CopyableTArray<mozilla::Telemetry::EventExtraEntry> extra(1);
1920 extra.AppendElement(
1921 mozilla::Telemetry::EventExtraEntry{"type"_ns, contentTypeTelem});
1922 extra.AppendElement(mozilla::Telemetry::EventExtraEntry{"reason"_ns, reason});
1924 mozilla::Telemetry::RecordEvent(
1925 mozilla::Telemetry::EventID::Downloads_Helpertype_Unknowntype,
1926 mozilla::Some(aAction), mozilla::Some(extra));
1929 bool nsExternalAppHandler::IsDownloadSpam(nsIChannel* aChannel) {
1930 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1931 nsCOMPtr<nsIPermissionManager> permissionManager =
1932 mozilla::services::GetPermissionManager();
1933 nsCOMPtr<nsIPrincipal> principal = loadInfo->TriggeringPrincipal();
1934 bool exactHostMatch = false;
1935 constexpr auto type = "automatic-download"_ns;
1936 nsCOMPtr<nsIPermission> permission;
1938 permissionManager->GetPermissionObject(principal, type, exactHostMatch,
1939 getter_AddRefs(permission));
1941 if (permission) {
1942 uint32_t capability;
1943 permission->GetCapability(&capability);
1944 if (capability == nsIPermissionManager::DENY_ACTION) {
1945 mCanceled = true;
1946 aChannel->Cancel(NS_ERROR_ABORT);
1947 return true;
1949 if (capability == nsIPermissionManager::ALLOW_ACTION) {
1950 return false;
1952 // If no action is set (i.e: null), we set PROMPT_ACTION by default,
1953 // which will notify the Downloads UI to open the panel on the next request.
1954 if (capability == nsIPermissionManager::PROMPT_ACTION) {
1955 nsCOMPtr<nsIObserverService> observerService =
1956 mozilla::services::GetObserverService();
1957 RefPtr<BrowsingContext> browsingContext;
1958 loadInfo->GetBrowsingContext(getter_AddRefs(browsingContext));
1960 nsAutoCString cStringURI;
1961 loadInfo->TriggeringPrincipal()->GetPrePath(cStringURI);
1962 observerService->NotifyObservers(
1963 browsingContext, "blocked-automatic-download",
1964 NS_ConvertASCIItoUTF16(cStringURI.get()).get());
1965 // FIXME: In order to escape memory leaks, currently we cancel blocked
1966 // downloads. This is temporary solution, because download data should be
1967 // kept in order to restart the blocked download.
1968 mCanceled = true;
1969 aChannel->Cancel(NS_ERROR_ABORT);
1970 // End cancel
1971 return true;
1974 if (!loadInfo->GetHasValidUserGestureActivation()) {
1975 permissionManager->AddFromPrincipal(
1976 principal, type, nsIPermissionManager::PROMPT_ACTION,
1977 nsIPermissionManager::EXPIRE_NEVER, 0 /* expire time */);
1980 return false;
1983 // Convert error info into proper message text and send OnStatusChange
1984 // notification to the dialog progress listener or nsITransfer implementation.
1985 void nsExternalAppHandler::SendStatusChange(ErrorType type, nsresult rv,
1986 nsIRequest* aRequest,
1987 const nsString& path) {
1988 const char* msgId = nullptr;
1989 switch (rv) {
1990 case NS_ERROR_OUT_OF_MEMORY:
1991 // No memory
1992 msgId = "noMemory";
1993 break;
1995 case NS_ERROR_FILE_NO_DEVICE_SPACE:
1996 // Out of space on target volume.
1997 msgId = "diskFull";
1998 break;
2000 case NS_ERROR_FILE_READ_ONLY:
2001 // Attempt to write to read/only file.
2002 msgId = "readOnly";
2003 break;
2005 case NS_ERROR_FILE_ACCESS_DENIED:
2006 if (type == kWriteError) {
2007 // Attempt to write without sufficient permissions.
2008 #if defined(ANDROID)
2009 // On Android this means the SD card is present but
2010 // unavailable (read-only).
2011 msgId = "SDAccessErrorCardReadOnly";
2012 #else
2013 msgId = "accessError";
2014 #endif
2015 } else {
2016 msgId = "launchError";
2018 break;
2020 case NS_ERROR_FILE_NOT_FOUND:
2021 case NS_ERROR_FILE_UNRECOGNIZED_PATH:
2022 // Helper app not found, let's verify this happened on launch
2023 if (type == kLaunchError) {
2024 msgId = "helperAppNotFound";
2025 break;
2027 #if defined(ANDROID)
2028 else if (type == kWriteError) {
2029 // On Android this means the SD card is missing (not in
2030 // SD slot).
2031 msgId = "SDAccessErrorCardMissing";
2032 break;
2034 #endif
2035 [[fallthrough]];
2037 default:
2038 // Generic read/write/launch error message.
2039 switch (type) {
2040 case kReadError:
2041 msgId = "readError";
2042 break;
2043 case kWriteError:
2044 msgId = "writeError";
2045 break;
2046 case kLaunchError:
2047 msgId = "launchError";
2048 break;
2050 break;
2053 MOZ_LOG(
2054 nsExternalHelperAppService::sLog, LogLevel::Error,
2055 ("Error: %s, type=%i, listener=0x%p, transfer=0x%p, rv=0x%08" PRIX32 "\n",
2056 msgId, type, mDialogProgressListener.get(), mTransfer.get(),
2057 static_cast<uint32_t>(rv)));
2059 MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Error,
2060 (" path='%s'\n", NS_ConvertUTF16toUTF8(path).get()));
2062 // Get properties file bundle and extract status string.
2063 nsCOMPtr<nsIStringBundleService> stringService =
2064 mozilla::components::StringBundle::Service();
2065 if (stringService) {
2066 nsCOMPtr<nsIStringBundle> bundle;
2067 if (NS_SUCCEEDED(stringService->CreateBundle(
2068 "chrome://global/locale/nsWebBrowserPersist.properties",
2069 getter_AddRefs(bundle)))) {
2070 nsAutoString msgText;
2071 AutoTArray<nsString, 1> strings = {path};
2072 if (NS_SUCCEEDED(bundle->FormatStringFromName(msgId, strings, msgText))) {
2073 if (mDialogProgressListener) {
2074 // We have a listener, let it handle the error.
2075 mDialogProgressListener->OnStatusChange(
2076 nullptr, (type == kReadError) ? aRequest : nullptr, rv,
2077 msgText.get());
2078 } else if (mTransfer) {
2079 mTransfer->OnStatusChange(nullptr,
2080 (type == kReadError) ? aRequest : nullptr,
2081 rv, msgText.get());
2082 } else if (XRE_IsParentProcess()) {
2083 // We don't have a listener. Simply show the alert ourselves.
2084 nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
2085 nsresult qiRv;
2086 nsCOMPtr<nsIPrompt> prompter(do_GetInterface(dialogParent, &qiRv));
2087 nsAutoString title;
2088 bundle->FormatStringFromName("title", strings, title);
2090 MOZ_LOG(
2091 nsExternalHelperAppService::sLog, LogLevel::Debug,
2092 ("mBrowsingContext=0x%p, prompter=0x%p, qi rv=0x%08" PRIX32
2093 ", title='%s', msg='%s'",
2094 mBrowsingContext.get(), prompter.get(),
2095 static_cast<uint32_t>(qiRv), NS_ConvertUTF16toUTF8(title).get(),
2096 NS_ConvertUTF16toUTF8(msgText).get()));
2098 // If we didn't have a prompter we will try and get a window
2099 // instead, get it's docshell and use it to alert the user.
2100 if (!prompter) {
2101 nsCOMPtr<nsPIDOMWindowOuter> window(do_GetInterface(dialogParent));
2102 if (!window || !window->GetDocShell()) {
2103 return;
2106 prompter = do_GetInterface(window->GetDocShell(), &qiRv);
2108 MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Debug,
2109 ("No prompter from mBrowsingContext, using DocShell, "
2110 "window=0x%p, docShell=0x%p, "
2111 "prompter=0x%p, qi rv=0x%08" PRIX32,
2112 window.get(), window->GetDocShell(), prompter.get(),
2113 static_cast<uint32_t>(qiRv)));
2115 // If we still don't have a prompter, there's nothing else we
2116 // can do so just return.
2117 if (!prompter) {
2118 MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Error,
2119 ("No prompter from DocShell, no way to alert user"));
2120 return;
2124 // We should always have a prompter at this point.
2125 prompter->Alert(title.get(), msgText.get());
2132 NS_IMETHODIMP
2133 nsExternalAppHandler::OnDataAvailable(nsIRequest* request,
2134 nsIInputStream* inStr,
2135 uint64_t sourceOffset, uint32_t count) {
2136 nsresult rv = NS_OK;
2137 // first, check to see if we've been canceled....
2138 if (mCanceled || !mSaver) {
2139 // then go cancel our underlying channel too
2140 return request->Cancel(NS_BINDING_ABORTED);
2143 // read the data out of the stream and write it to the temp file.
2144 if (count > 0) {
2145 mProgress += count;
2147 nsCOMPtr<nsIStreamListener> saver = do_QueryInterface(mSaver);
2148 rv = saver->OnDataAvailable(request, inStr, sourceOffset, count);
2149 if (NS_SUCCEEDED(rv)) {
2150 // Send progress notification.
2151 if (mTransfer) {
2152 mTransfer->OnProgressChange64(nullptr, request, mProgress,
2153 mContentLength, mProgress,
2154 mContentLength);
2156 } else {
2157 // An error occurred, notify listener.
2158 nsAutoString tempFilePath;
2159 if (mTempFile) {
2160 mTempFile->GetPath(tempFilePath);
2162 SendStatusChange(kReadError, rv, request, tempFilePath);
2164 // Cancel the download.
2165 Cancel(rv);
2168 return rv;
2171 NS_IMETHODIMP nsExternalAppHandler::OnStopRequest(nsIRequest* request,
2172 nsresult aStatus) {
2173 LOG("nsExternalAppHandler::OnStopRequest\n"
2174 " mCanceled=%d, mTransfer=0x%p, aStatus=0x%08" PRIX32 "\n",
2175 mCanceled, mTransfer.get(), static_cast<uint32_t>(aStatus));
2177 mStopRequestIssued = true;
2179 // Cancel if the request did not complete successfully.
2180 if (!mCanceled && NS_FAILED(aStatus)) {
2181 // Send error notification.
2182 nsAutoString tempFilePath;
2183 if (mTempFile) mTempFile->GetPath(tempFilePath);
2184 SendStatusChange(kReadError, aStatus, request, tempFilePath);
2186 Cancel(aStatus);
2189 // first, check to see if we've been canceled....
2190 if (mCanceled || !mSaver) {
2191 return NS_OK;
2194 return mSaver->Finish(NS_OK);
2197 NS_IMETHODIMP
2198 nsExternalAppHandler::OnTargetChange(nsIBackgroundFileSaver* aSaver,
2199 nsIFile* aTarget) {
2200 return NS_OK;
2203 NS_IMETHODIMP
2204 nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver,
2205 nsresult aStatus) {
2206 LOG("nsExternalAppHandler::OnSaveComplete\n"
2207 " aSaver=0x%p, aStatus=0x%08" PRIX32 ", mCanceled=%d, mTransfer=0x%p\n",
2208 aSaver, static_cast<uint32_t>(aStatus), mCanceled, mTransfer.get());
2210 if (!mCanceled) {
2211 // Save the hash and signature information
2212 (void)mSaver->GetSha256Hash(mHash);
2213 (void)mSaver->GetSignatureInfo(mSignatureInfo);
2215 // Free the reference that the saver keeps on us, even if we couldn't get
2216 // the hash.
2217 mSaver = nullptr;
2219 // Save the redirect information.
2220 nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2221 if (channel) {
2222 nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
2223 nsresult rv = NS_OK;
2224 nsCOMPtr<nsIMutableArray> redirectChain =
2225 do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
2226 NS_ENSURE_SUCCESS(rv, rv);
2227 LOG("nsExternalAppHandler: Got %zu redirects\n",
2228 loadInfo->RedirectChain().Length());
2229 for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) {
2230 redirectChain->AppendElement(entry);
2232 mRedirects = redirectChain;
2235 if (NS_FAILED(aStatus)) {
2236 nsAutoString path;
2237 mTempFile->GetPath(path);
2239 // It may happen when e10s is enabled that there will be no transfer
2240 // object available to communicate status as expected by the system.
2241 // Let's try and create a temporary transfer object to take care of this
2242 // for us, we'll fall back to using the prompt service if we absolutely
2243 // have to.
2244 if (!mTransfer) {
2245 // We don't care if this fails.
2246 CreateFailedTransfer();
2249 SendStatusChange(kWriteError, aStatus, nullptr, path);
2250 if (!mCanceled) Cancel(aStatus);
2251 return NS_OK;
2255 // Notify the transfer object that we are done if the user has chosen an
2256 // action. If the user hasn't chosen an action, the progress listener
2257 // (nsITransfer) will be notified in CreateTransfer.
2258 if (mTransfer) {
2259 NotifyTransfer(aStatus);
2262 return NS_OK;
2265 void nsExternalAppHandler::NotifyTransfer(nsresult aStatus) {
2266 MOZ_ASSERT(NS_IsMainThread(), "Must notify on main thread");
2267 MOZ_ASSERT(mTransfer, "We must have an nsITransfer");
2269 LOG("Notifying progress listener");
2271 if (NS_SUCCEEDED(aStatus)) {
2272 (void)mTransfer->SetSha256Hash(mHash);
2273 (void)mTransfer->SetSignatureInfo(mSignatureInfo);
2274 (void)mTransfer->SetRedirects(mRedirects);
2275 (void)mTransfer->OnProgressChange64(
2276 nullptr, nullptr, mProgress, mContentLength, mProgress, mContentLength);
2279 (void)mTransfer->OnStateChange(nullptr, nullptr,
2280 nsIWebProgressListener::STATE_STOP |
2281 nsIWebProgressListener::STATE_IS_REQUEST |
2282 nsIWebProgressListener::STATE_IS_NETWORK,
2283 aStatus);
2285 // This nsITransfer object holds a reference to us (we are its observer), so
2286 // we need to release the reference to break a reference cycle (and therefore
2287 // to prevent leaking). We do this even if the previous calls failed.
2288 mTransfer = nullptr;
2291 NS_IMETHODIMP nsExternalAppHandler::GetMIMEInfo(nsIMIMEInfo** aMIMEInfo) {
2292 *aMIMEInfo = mMimeInfo;
2293 NS_ADDREF(*aMIMEInfo);
2294 return NS_OK;
2297 NS_IMETHODIMP nsExternalAppHandler::GetSource(nsIURI** aSourceURI) {
2298 NS_ENSURE_ARG(aSourceURI);
2299 *aSourceURI = mSourceUrl;
2300 NS_IF_ADDREF(*aSourceURI);
2301 return NS_OK;
2304 NS_IMETHODIMP nsExternalAppHandler::GetSuggestedFileName(
2305 nsAString& aSuggestedFileName) {
2306 aSuggestedFileName = mSuggestedFileName;
2307 return NS_OK;
2310 nsresult nsExternalAppHandler::CreateTransfer() {
2311 LOG("nsExternalAppHandler::CreateTransfer");
2313 MOZ_ASSERT(NS_IsMainThread(), "Must create transfer on main thread");
2314 // We are back from the helper app dialog (where the user chooses to save or
2315 // open), but we aren't done processing the load. in this case, throw up a
2316 // progress dialog so the user can see what's going on.
2317 // Also, release our reference to mDialog. We don't need it anymore, and we
2318 // need to break the reference cycle.
2319 mDialog = nullptr;
2320 if (!mDialogProgressListener) {
2321 NS_WARNING("The dialog should nullify the dialog progress listener");
2323 // In case of a non acceptable download, we need to cancel the request and
2324 // pass a FailedTransfer for the Download UI.
2325 if (mDownloadClassification != nsITransfer::DOWNLOAD_ACCEPTABLE) {
2326 mCanceled = true;
2327 mRequest->Cancel(NS_ERROR_ABORT);
2328 if (mSaver) {
2329 mSaver->Finish(NS_ERROR_ABORT);
2330 mSaver = nullptr;
2332 return CreateFailedTransfer();
2334 nsresult rv;
2336 // We must be able to create an nsITransfer object. If not, it doesn't matter
2337 // much that we can't launch the helper application or save to disk. Work on
2338 // a local copy rather than mTransfer until we know we succeeded, to make it
2339 // clearer that this function is re-entrant.
2340 nsCOMPtr<nsITransfer> transfer =
2341 do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
2342 NS_ENSURE_SUCCESS(rv, rv);
2344 // Initialize the download
2345 nsCOMPtr<nsIURI> target;
2346 rv = NS_NewFileURI(getter_AddRefs(target), mFinalFileDestination);
2347 NS_ENSURE_SUCCESS(rv, rv);
2349 nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2350 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mRequest);
2351 nsCOMPtr<nsIReferrerInfo> referrerInfo = nullptr;
2352 if (httpChannel) {
2353 referrerInfo = httpChannel->GetReferrerInfo();
2356 if (mBrowsingContext) {
2357 rv = transfer->InitWithBrowsingContext(
2358 mSourceUrl, target, u""_ns, mMimeInfo, mTimeDownloadStarted, mTempFile,
2359 this, channel && NS_UsePrivateBrowsing(channel),
2360 mDownloadClassification, referrerInfo, !mDialogShowing,
2361 mBrowsingContext, mHandleInternally, nullptr);
2362 } else {
2363 rv = transfer->Init(mSourceUrl, nullptr, target, u""_ns, mMimeInfo,
2364 mTimeDownloadStarted, mTempFile, this,
2365 channel && NS_UsePrivateBrowsing(channel),
2366 mDownloadClassification, referrerInfo, !mDialogShowing);
2368 mDialogShowing = false;
2370 NS_ENSURE_SUCCESS(rv, rv);
2372 // If we were cancelled since creating the transfer, just return. It is
2373 // always ok to return NS_OK if we are cancelled. Callers of this function
2374 // must call Cancel if CreateTransfer fails, but there's no need to cancel
2375 // twice.
2376 if (mCanceled) {
2377 return NS_OK;
2379 rv = transfer->OnStateChange(nullptr, mRequest,
2380 nsIWebProgressListener::STATE_START |
2381 nsIWebProgressListener::STATE_IS_REQUEST |
2382 nsIWebProgressListener::STATE_IS_NETWORK,
2383 NS_OK);
2384 NS_ENSURE_SUCCESS(rv, rv);
2386 if (mCanceled) {
2387 return NS_OK;
2390 mRequest = nullptr;
2391 // Finally, save the transfer to mTransfer.
2392 mTransfer = transfer;
2393 transfer = nullptr;
2395 // While we were bringing up the progress dialog, we actually finished
2396 // processing the url. If that's the case then mStopRequestIssued will be
2397 // true and OnSaveComplete has been called.
2398 if (mStopRequestIssued && !mSaver && mTransfer) {
2399 NotifyTransfer(NS_OK);
2402 return rv;
2405 nsresult nsExternalAppHandler::CreateFailedTransfer() {
2406 nsresult rv;
2407 nsCOMPtr<nsITransfer> transfer =
2408 do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
2409 NS_ENSURE_SUCCESS(rv, rv);
2411 // We won't pass the temp file to the transfer, so if we have one it needs to
2412 // get deleted now.
2413 if (mTempFile) {
2414 if (mSaver) {
2415 mSaver->Finish(NS_BINDING_ABORTED);
2416 mSaver = nullptr;
2418 mTempFile->Remove(false);
2421 nsCOMPtr<nsIURI> pseudoTarget;
2422 if (!mFinalFileDestination) {
2423 // If we don't have a download directory we're kinda screwed but it's OK
2424 // we'll still report the error via the prompter.
2425 nsCOMPtr<nsIFile> pseudoFile;
2426 rv = GetDownloadDirectory(getter_AddRefs(pseudoFile), true);
2427 NS_ENSURE_SUCCESS(rv, rv);
2429 // Append the default suggested filename. If the user restarts the transfer
2430 // we will re-trigger a filename check anyway to ensure that it is unique.
2431 rv = pseudoFile->Append(mSuggestedFileName);
2432 NS_ENSURE_SUCCESS(rv, rv);
2434 rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), pseudoFile);
2435 NS_ENSURE_SUCCESS(rv, rv);
2436 } else {
2437 // Initialize the target, if present
2438 rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), mFinalFileDestination);
2439 NS_ENSURE_SUCCESS(rv, rv);
2442 nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2443 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mRequest);
2444 nsCOMPtr<nsIReferrerInfo> referrerInfo = nullptr;
2445 if (httpChannel) {
2446 referrerInfo = httpChannel->GetReferrerInfo();
2449 if (mBrowsingContext) {
2450 rv = transfer->InitWithBrowsingContext(
2451 mSourceUrl, pseudoTarget, u""_ns, mMimeInfo, mTimeDownloadStarted,
2452 mTempFile, this, channel && NS_UsePrivateBrowsing(channel),
2453 mDownloadClassification, referrerInfo, true, mBrowsingContext,
2454 mHandleInternally, httpChannel);
2455 } else {
2456 rv = transfer->Init(mSourceUrl, nullptr, pseudoTarget, u""_ns, mMimeInfo,
2457 mTimeDownloadStarted, mTempFile, this,
2458 channel && NS_UsePrivateBrowsing(channel),
2459 mDownloadClassification, referrerInfo, true);
2461 NS_ENSURE_SUCCESS(rv, rv);
2463 // Our failed transfer is ready.
2464 mTransfer = std::move(transfer);
2466 return NS_OK;
2469 nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile* aFile,
2470 bool aDialogWasShown) {
2471 if (aFile) {
2472 if (aDialogWasShown) {
2473 mDialogShowing = true;
2475 ContinueSave(aFile);
2476 } else {
2477 Cancel(NS_BINDING_ABORTED);
2480 return NS_OK;
2483 void nsExternalAppHandler::RequestSaveDestination(
2484 const nsString& aDefaultFile, const nsString& aFileExtension) {
2485 // Display the dialog
2486 // XXX Convert to use file picker? No, then embeddors could not do any sort of
2487 // "AutoDownload" w/o showing a prompt
2488 nsresult rv = NS_OK;
2489 if (!mDialog) {
2490 // Get helper app launcher dialog.
2491 mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv);
2492 if (rv != NS_OK) {
2493 Cancel(NS_BINDING_ABORTED);
2494 return;
2498 // we want to explicitly unescape aDefaultFile b4 passing into the dialog. we
2499 // can't unescape it because the dialog is implemented by a JS component which
2500 // doesn't have a window so no unescape routine is defined...
2502 // Now, be sure to keep |this| alive, and the dialog
2503 // If we don't do this, users that close the helper app dialog while the file
2504 // picker is up would cause Cancel() to be called, and the dialog would be
2505 // released, which would release this object too, which would crash.
2506 // See Bug 249143
2507 RefPtr<nsExternalAppHandler> kungFuDeathGrip(this);
2508 nsCOMPtr<nsIHelperAppLauncherDialog> dlg(mDialog);
2509 nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
2511 rv = dlg->PromptForSaveToFileAsync(this, dialogParent, aDefaultFile.get(),
2512 aFileExtension.get(), mForceSave);
2513 if (NS_FAILED(rv)) {
2514 Cancel(NS_BINDING_ABORTED);
2518 // PromptForSaveDestination should only be called by the helper app dialog which
2519 // allows the user to say launch with application or save to disk.
2520 NS_IMETHODIMP nsExternalAppHandler::PromptForSaveDestination() {
2521 if (mCanceled) return NS_OK;
2523 if (!StaticPrefs::browser_download_improvements_to_download_panel() ||
2524 mForceSave) {
2525 mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
2528 if (mSuggestedFileName.IsEmpty()) {
2529 RequestSaveDestination(mTempLeafName, mFileExtension);
2530 } else {
2531 nsAutoString fileExt;
2532 int32_t pos = mSuggestedFileName.RFindChar('.');
2533 if (pos >= 0) {
2534 mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos);
2536 if (fileExt.IsEmpty()) {
2537 fileExt = mFileExtension;
2540 RequestSaveDestination(mSuggestedFileName, fileExt);
2543 return NS_OK;
2545 nsresult nsExternalAppHandler::ContinueSave(nsIFile* aNewFileLocation) {
2546 if (mCanceled) return NS_OK;
2548 MOZ_ASSERT(aNewFileLocation, "Must be called with a non-null file");
2550 int32_t action = nsIMIMEInfo::saveToDisk;
2551 mMimeInfo->GetPreferredAction(&action);
2552 mHandleInternally =
2553 action == nsIMIMEInfo::handleInternally &&
2554 StaticPrefs::browser_download_improvements_to_download_panel();
2556 nsresult rv = NS_OK;
2557 nsCOMPtr<nsIFile> fileToUse = aNewFileLocation;
2558 mFinalFileDestination = fileToUse;
2560 // Move what we have in the final directory, but append .part
2561 // to it, to indicate that it's unfinished. Do not call SetTarget on the
2562 // saver if we are done (Finish has been called) but OnSaverComplete has
2563 // not been called.
2564 if (mFinalFileDestination && mSaver && !mStopRequestIssued) {
2565 nsCOMPtr<nsIFile> movedFile;
2566 mFinalFileDestination->Clone(getter_AddRefs(movedFile));
2567 if (movedFile) {
2568 nsAutoCString randomChars;
2569 rv = GenerateRandomName(randomChars);
2570 if (NS_SUCCEEDED(rv)) {
2571 // Get the leaf name, strip any extensions, then
2572 // add random bytes, followed by the extensions and '.part'.
2573 nsAutoString leafName;
2574 mFinalFileDestination->GetLeafName(leafName);
2575 auto nameWithoutExtensionLength = leafName.FindChar('.');
2576 nsAutoString extensions(u"");
2577 if (nameWithoutExtensionLength == kNotFound) {
2578 nameWithoutExtensionLength = leafName.Length();
2579 } else {
2580 extensions = Substring(leafName, nameWithoutExtensionLength);
2582 leafName.Truncate(nameWithoutExtensionLength);
2584 nsAutoString suffix = u"."_ns + NS_ConvertASCIItoUTF16(randomChars) +
2585 extensions + u".part"_ns;
2586 #ifdef XP_WIN
2587 // Deal with MAX_PATH on Windows. Worth noting that the original
2588 // path for mFinalFileDestination must be valid for us to get
2589 // here: either SetDownloadToLaunch or the caller of
2590 // SaveDestinationAvailable has called CreateUnique or similar
2591 // to ensure both a unique name and one that isn't too long.
2592 // The only issue is we're making it longer to get the part
2593 // file path...
2594 nsAutoString path;
2595 mFinalFileDestination->GetPath(path);
2596 CheckedInt<uint16_t> fullPathLength =
2597 CheckedInt<uint16_t>(path.Length()) + 1 + randomChars.Length() +
2598 ArrayLength(".part");
2599 if (!fullPathLength.isValid()) {
2600 leafName.Truncate();
2601 } else if (fullPathLength.value() > MAX_PATH) {
2602 int32_t leafNameRemaining =
2603 (int32_t)leafName.Length() - (fullPathLength.value() - MAX_PATH);
2604 leafName.Truncate(std::max(leafNameRemaining, 0));
2606 #endif
2607 leafName.Append(suffix);
2608 movedFile->SetLeafName(leafName);
2610 rv = mSaver->SetTarget(movedFile, true);
2611 if (NS_FAILED(rv)) {
2612 nsAutoString path;
2613 mTempFile->GetPath(path);
2614 SendStatusChange(kWriteError, rv, nullptr, path);
2615 Cancel(rv);
2616 return NS_OK;
2619 mTempFile = movedFile;
2624 // The helper app dialog has told us what to do and we have a final file
2625 // destination.
2626 rv = CreateTransfer();
2627 // If we fail to create the transfer, Cancel.
2628 if (NS_FAILED(rv)) {
2629 Cancel(rv);
2630 return rv;
2633 return NS_OK;
2636 // SetDownloadToLaunch should only be called by the helper app dialog which
2637 // allows the user to say launch with application or save to disk.
2638 NS_IMETHODIMP nsExternalAppHandler::SetDownloadToLaunch(
2639 bool aHandleInternally, nsIFile* aNewFileLocation) {
2640 if (mCanceled) return NS_OK;
2642 mHandleInternally = aHandleInternally;
2644 // Now that the user has elected to launch the downloaded file with a helper
2645 // app, we're justified in removing the 'salted' name. We'll rename to what
2646 // was specified in mSuggestedFileName after the download is done prior to
2647 // launching the helper app. So that any existing file of that name won't be
2648 // overwritten we call CreateUnique(). Also note that we use the same
2649 // directory as originally downloaded so the download can be renamed in place
2650 // later.
2651 nsCOMPtr<nsIFile> fileToUse;
2652 if (aNewFileLocation &&
2653 StaticPrefs::browser_download_improvements_to_download_panel()) {
2654 fileToUse = aNewFileLocation;
2655 } else {
2656 (void)GetDownloadDirectory(getter_AddRefs(fileToUse));
2658 if (mSuggestedFileName.IsEmpty()) {
2659 // Keep using the leafname of the temp file, since we're just starting a
2660 // helper
2661 mSuggestedFileName = mTempLeafName;
2664 #ifdef XP_WIN
2665 // Ensure we don't double-append the file extension if it matches:
2666 if (StringEndsWith(mSuggestedFileName, mFileExtension,
2667 nsCaseInsensitiveStringComparator)) {
2668 fileToUse->Append(mSuggestedFileName);
2669 } else {
2670 fileToUse->Append(mSuggestedFileName + mFileExtension);
2672 #else
2673 fileToUse->Append(mSuggestedFileName);
2674 #endif
2677 nsresult rv = fileToUse->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
2678 if (NS_SUCCEEDED(rv)) {
2679 mFinalFileDestination = fileToUse;
2680 // launch the progress window now that the user has picked the desired
2681 // action.
2682 rv = CreateTransfer();
2683 if (NS_FAILED(rv)) {
2684 Cancel(rv);
2686 } else {
2687 // Cancel the download and report an error. We do not want to end up in
2688 // a state where it appears that we have a normal download that is
2689 // pointing to a file that we did not actually create.
2690 nsAutoString path;
2691 mTempFile->GetPath(path);
2692 SendStatusChange(kWriteError, rv, nullptr, path);
2693 Cancel(rv);
2695 return rv;
2698 nsresult nsExternalAppHandler::LaunchLocalFile() {
2699 nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(mSourceUrl));
2700 if (!fileUrl) {
2701 return NS_OK;
2703 Cancel(NS_BINDING_ABORTED);
2704 nsCOMPtr<nsIFile> file;
2705 nsresult rv = fileUrl->GetFile(getter_AddRefs(file));
2707 if (NS_SUCCEEDED(rv)) {
2708 rv = mMimeInfo->LaunchWithFile(file);
2709 if (NS_SUCCEEDED(rv)) return NS_OK;
2711 nsAutoString path;
2712 if (file) file->GetPath(path);
2713 // If we get here, an error happened
2714 SendStatusChange(kLaunchError, rv, nullptr, path);
2715 return rv;
2718 NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) {
2719 NS_ENSURE_ARG(NS_FAILED(aReason));
2721 if (mCanceled) {
2722 return NS_OK;
2724 mCanceled = true;
2726 if (mSaver) {
2727 // We are still writing to the target file. Give the saver a chance to
2728 // close the target file, then notify the transfer object if necessary in
2729 // the OnSaveComplete callback.
2730 mSaver->Finish(aReason);
2731 mSaver = nullptr;
2732 } else {
2733 if (mStopRequestIssued && mTempFile) {
2734 // This branch can only happen when the user cancels the helper app dialog
2735 // when the request has completed. The temp file has to be removed here,
2736 // because mSaver has been released at that time with the temp file left.
2737 (void)mTempFile->Remove(false);
2740 // Notify the transfer object that the download has been canceled, if the
2741 // user has already chosen an action and we didn't notify already.
2742 if (mTransfer) {
2743 NotifyTransfer(aReason);
2747 // Break our reference cycle with the helper app dialog (set up in
2748 // OnStartRequest)
2749 mDialog = nullptr;
2750 mDialogShowing = false;
2752 mRequest = nullptr;
2754 // Release the listener, to break the reference cycle with it (we are the
2755 // observer of the listener).
2756 mDialogProgressListener = nullptr;
2758 return NS_OK;
2761 bool nsExternalAppHandler::GetNeverAskFlagFromPref(const char* prefName,
2762 const char* aContentType) {
2763 // Search the obsolete pref strings.
2764 nsAutoCString prefCString;
2765 Preferences::GetCString(prefName, prefCString);
2766 if (prefCString.IsEmpty()) {
2767 // Default is true, if not found in the pref string.
2768 return true;
2771 NS_UnescapeURL(prefCString);
2772 nsACString::const_iterator start, end;
2773 prefCString.BeginReading(start);
2774 prefCString.EndReading(end);
2775 return !CaseInsensitiveFindInReadable(nsDependentCString(aContentType), start,
2776 end);
2779 NS_IMETHODIMP
2780 nsExternalAppHandler::GetName(nsACString& aName) {
2781 aName.AssignLiteral("nsExternalAppHandler");
2782 return NS_OK;
2785 //////////////////////////////////////////////////////////////////////////////////////////////////////////////
2786 // The following section contains our nsIMIMEService implementation and related
2787 // methods.
2789 //////////////////////////////////////////////////////////////////////////////////////////////////////////////
2791 // nsIMIMEService methods
2792 NS_IMETHODIMP nsExternalHelperAppService::GetFromTypeAndExtension(
2793 const nsACString& aMIMEType, const nsACString& aFileExt,
2794 nsIMIMEInfo** _retval) {
2795 MOZ_ASSERT(!aMIMEType.IsEmpty() || !aFileExt.IsEmpty(),
2796 "Give me something to work with");
2797 MOZ_DIAGNOSTIC_ASSERT(aFileExt.FindChar('\0') == kNotFound,
2798 "The extension should never contain null characters");
2799 LOG("Getting mimeinfo from type '%s' ext '%s'\n",
2800 PromiseFlatCString(aMIMEType).get(), PromiseFlatCString(aFileExt).get());
2802 *_retval = nullptr;
2804 // OK... we need a type. Get one.
2805 nsAutoCString typeToUse(aMIMEType);
2806 if (typeToUse.IsEmpty()) {
2807 nsresult rv = GetTypeFromExtension(aFileExt, typeToUse);
2808 if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE;
2811 // We promise to only send lower case mime types to the OS
2812 ToLowerCase(typeToUse);
2814 // First, ask the OS for a mime info
2815 bool found;
2816 nsresult rv = GetMIMEInfoFromOS(typeToUse, aFileExt, &found, _retval);
2817 if (NS_WARN_IF(NS_FAILED(rv))) {
2818 return rv;
2821 LOG("OS gave back 0x%p - found: %i\n", *_retval, found);
2822 // If we got no mimeinfo, something went wrong. Probably lack of memory.
2823 if (!*_retval) return NS_ERROR_OUT_OF_MEMORY;
2825 // The handler service can make up for bad mime types by checking the file
2826 // extension. If the mime type is known (in extras or in the handler
2827 // service), we stop it doing so by flipping this bool to true.
2828 bool trustMIMEType = false;
2830 // Check extras - not everything we support will be known by the OS store,
2831 // unfortunately, and it may even miss some extensions that we know should
2832 // be accepted. We only do this for non-octet-stream mimetypes, because
2833 // our information for octet-stream would lead to us trying to open all such
2834 // files as Binary file with exe, com or bin extension regardless of the
2835 // real extension.
2836 if (!typeToUse.Equals(APPLICATION_OCTET_STREAM,
2837 nsCaseInsensitiveCStringComparator)) {
2838 rv = FillMIMEInfoForMimeTypeFromExtras(typeToUse, !found, *_retval);
2839 LOG("Searched extras (by type), rv 0x%08" PRIX32 "\n",
2840 static_cast<uint32_t>(rv));
2841 trustMIMEType = NS_SUCCEEDED(rv);
2842 found = found || NS_SUCCEEDED(rv);
2845 // Now, let's see if we can find something in our datastore.
2846 // This will not overwrite the OS information that interests us
2847 // (i.e. default application, default app. description)
2848 nsCOMPtr<nsIHandlerService> handlerSvc =
2849 do_GetService(NS_HANDLERSERVICE_CONTRACTID);
2850 if (handlerSvc) {
2851 bool hasHandler = false;
2852 (void)handlerSvc->Exists(*_retval, &hasHandler);
2853 if (hasHandler) {
2854 rv = handlerSvc->FillHandlerInfo(*_retval, ""_ns);
2855 LOG("Data source: Via type: retval 0x%08" PRIx32 "\n",
2856 static_cast<uint32_t>(rv));
2857 trustMIMEType = trustMIMEType || NS_SUCCEEDED(rv);
2858 } else {
2859 rv = NS_ERROR_NOT_AVAILABLE;
2862 found = found || NS_SUCCEEDED(rv);
2865 // If we still haven't found anything, try finding a match for
2866 // an extension in extras first:
2867 if (!found && !aFileExt.IsEmpty()) {
2868 rv = FillMIMEInfoForExtensionFromExtras(aFileExt, *_retval);
2869 LOG("Searched extras (by ext), rv 0x%08" PRIX32 "\n",
2870 static_cast<uint32_t>(rv));
2873 // Then check the handler service - but only do so if we really do not know
2874 // the mimetype. This avoids overwriting good mimetype info with bad file
2875 // extension info.
2876 if ((!found || !trustMIMEType) && handlerSvc && !aFileExt.IsEmpty()) {
2877 nsAutoCString overrideType;
2878 rv = handlerSvc->GetTypeFromExtension(aFileExt, overrideType);
2879 if (NS_SUCCEEDED(rv) && !overrideType.IsEmpty()) {
2880 // We can't check handlerSvc->Exists() here, because we have a
2881 // overideType. That's ok, it just results in some console noise.
2882 // (If there's no handler for the override type, it throws)
2883 rv = handlerSvc->FillHandlerInfo(*_retval, overrideType);
2884 LOG("Data source: Via ext: retval 0x%08" PRIx32 "\n",
2885 static_cast<uint32_t>(rv));
2886 found = found || NS_SUCCEEDED(rv);
2890 // If we still don't have a match, at least set the file description
2891 // to `${aFileExt} File` if it's empty:
2892 if (!found && !aFileExt.IsEmpty()) {
2893 // XXXzpao This should probably be localized
2894 nsAutoCString desc(aFileExt);
2895 desc.AppendLiteral(" File");
2896 (*_retval)->SetDescription(NS_ConvertUTF8toUTF16(desc));
2897 LOG("Falling back to 'File' file description\n");
2900 // Sometimes, OSes give us bad data. We have a set of forbidden extensions
2901 // for some MIME types. If the primary extension is forbidden,
2902 // overwrite it with a known-good one. See bug 1571247 for context.
2903 nsAutoCString primaryExtension;
2904 (*_retval)->GetPrimaryExtension(primaryExtension);
2905 if (!primaryExtension.EqualsIgnoreCase(PromiseFlatCString(aFileExt).get())) {
2906 if (MaybeReplacePrimaryExtension(primaryExtension, *_retval)) {
2907 (*_retval)->GetPrimaryExtension(primaryExtension);
2911 // Finally, check if we got a file extension and if yes, if it is an
2912 // extension on the mimeinfo, in which case we want it to be the primary one
2913 if (!aFileExt.IsEmpty()) {
2914 bool matches = false;
2915 (*_retval)->ExtensionExists(aFileExt, &matches);
2916 LOG("Extension '%s' matches mime info: %i\n",
2917 PromiseFlatCString(aFileExt).get(), matches);
2918 if (matches) {
2919 nsAutoCString fileExt;
2920 ToLowerCase(aFileExt, fileExt);
2921 (*_retval)->SetPrimaryExtension(fileExt);
2922 primaryExtension = fileExt;
2926 // Overwrite with a generic description if the primary extension for the
2927 // type is in our list; these are file formats supported by Firefox and
2928 // we don't want other brands positioning themselves as the sole viewer
2929 // for a system.
2930 if (!primaryExtension.IsEmpty()) {
2931 for (const char* ext : descriptionOverwriteExtensions) {
2932 if (primaryExtension.Equals(ext)) {
2933 nsCOMPtr<nsIStringBundleService> bundleService =
2934 do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
2935 NS_ENSURE_SUCCESS(rv, rv);
2936 nsCOMPtr<nsIStringBundle> unknownContentTypeBundle;
2937 rv = bundleService->CreateBundle(
2938 "chrome://mozapps/locale/downloads/unknownContentType.properties",
2939 getter_AddRefs(unknownContentTypeBundle));
2940 if (NS_SUCCEEDED(rv)) {
2941 nsAutoCString stringName(ext);
2942 stringName.AppendLiteral("ExtHandlerDescription");
2943 nsAutoString handlerDescription;
2944 rv = unknownContentTypeBundle->GetStringFromName(stringName.get(),
2945 handlerDescription);
2946 if (NS_SUCCEEDED(rv)) {
2947 (*_retval)->SetDescription(handlerDescription);
2950 break;
2955 if (LOG_ENABLED()) {
2956 nsAutoCString type;
2957 (*_retval)->GetMIMEType(type);
2959 LOG("MIME Info Summary: Type '%s', Primary Ext '%s'\n", type.get(),
2960 primaryExtension.get());
2963 return NS_OK;
2966 NS_IMETHODIMP
2967 nsExternalHelperAppService::GetTypeFromExtension(const nsACString& aFileExt,
2968 nsACString& aContentType) {
2969 // OK. We want to try the following sources of mimetype information, in this
2970 // order:
2971 // 1. defaultMimeEntries array
2972 // 2. OS-provided information
2973 // 3. our "extras" array
2974 // 4. Information from plugins
2975 // 5. The "ext-to-type-mapping" category
2976 // Note that, we are intentionally not looking at the handler service, because
2977 // that can be affected by websites, which leads to undesired behavior.
2979 // Early return if called with an empty extension parameter
2980 if (aFileExt.IsEmpty()) {
2981 return NS_ERROR_NOT_AVAILABLE;
2984 // First of all, check our default entries
2985 for (auto& entry : defaultMimeEntries) {
2986 if (aFileExt.LowerCaseEqualsASCII(entry.mFileExtension)) {
2987 aContentType = entry.mMimeType;
2988 return NS_OK;
2992 // Ask OS.
2993 if (GetMIMETypeFromOSForExtension(aFileExt, aContentType)) {
2994 return NS_OK;
2997 // Check extras array.
2998 bool found = GetTypeFromExtras(aFileExt, aContentType);
2999 if (found) {
3000 return NS_OK;
3003 // Let's see if an extension added something
3004 nsCOMPtr<nsICategoryManager> catMan(
3005 do_GetService("@mozilla.org/categorymanager;1"));
3006 if (catMan) {
3007 // The extension in the category entry is always stored as lowercase
3008 nsAutoCString lowercaseFileExt(aFileExt);
3009 ToLowerCase(lowercaseFileExt);
3010 // Read the MIME type from the category entry, if available
3011 nsCString type;
3012 nsresult rv =
3013 catMan->GetCategoryEntry("ext-to-type-mapping", lowercaseFileExt, type);
3014 if (NS_SUCCEEDED(rv)) {
3015 aContentType = type;
3016 return NS_OK;
3020 return NS_ERROR_NOT_AVAILABLE;
3023 NS_IMETHODIMP nsExternalHelperAppService::GetPrimaryExtension(
3024 const nsACString& aMIMEType, const nsACString& aFileExt,
3025 nsACString& _retval) {
3026 NS_ENSURE_ARG(!aMIMEType.IsEmpty());
3028 nsCOMPtr<nsIMIMEInfo> mi;
3029 nsresult rv =
3030 GetFromTypeAndExtension(aMIMEType, aFileExt, getter_AddRefs(mi));
3031 if (NS_FAILED(rv)) return rv;
3033 return mi->GetPrimaryExtension(_retval);
3036 NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromURI(
3037 nsIURI* aURI, nsACString& aContentType) {
3038 NS_ENSURE_ARG_POINTER(aURI);
3039 nsresult rv = NS_ERROR_NOT_AVAILABLE;
3040 aContentType.Truncate();
3042 // First look for a file to use. If we have one, we just use that.
3043 nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(aURI);
3044 if (fileUrl) {
3045 nsCOMPtr<nsIFile> file;
3046 rv = fileUrl->GetFile(getter_AddRefs(file));
3047 if (NS_SUCCEEDED(rv)) {
3048 rv = GetTypeFromFile(file, aContentType);
3049 if (NS_SUCCEEDED(rv)) {
3050 // we got something!
3051 return rv;
3056 // Now try to get an nsIURL so we don't have to do our own parsing
3057 nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
3058 if (url) {
3059 nsAutoCString ext;
3060 rv = url->GetFileExtension(ext);
3061 if (NS_FAILED(rv)) return rv;
3062 if (ext.IsEmpty()) return NS_ERROR_NOT_AVAILABLE;
3064 UnescapeFragment(ext, url, ext);
3066 return GetTypeFromExtension(ext, aContentType);
3069 // no url, let's give the raw spec a shot
3070 nsAutoCString specStr;
3071 rv = aURI->GetSpec(specStr);
3072 if (NS_FAILED(rv)) return rv;
3073 UnescapeFragment(specStr, aURI, specStr);
3075 // find the file extension (if any)
3076 int32_t extLoc = specStr.RFindChar('.');
3077 int32_t specLength = specStr.Length();
3078 if (-1 != extLoc && extLoc != specLength - 1 &&
3079 // nothing over 20 chars long can be sanely considered an
3080 // extension.... Dat dere would be just data.
3081 specLength - extLoc < 20) {
3082 return GetTypeFromExtension(Substring(specStr, extLoc + 1), aContentType);
3085 // We found no information; say so.
3086 return NS_ERROR_NOT_AVAILABLE;
3089 NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromFile(
3090 nsIFile* aFile, nsACString& aContentType) {
3091 NS_ENSURE_ARG_POINTER(aFile);
3092 nsresult rv;
3094 // Get the Extension
3095 nsAutoString fileName;
3096 rv = aFile->GetLeafName(fileName);
3097 if (NS_FAILED(rv)) return rv;
3099 nsAutoCString fileExt;
3100 if (!fileName.IsEmpty()) {
3101 int32_t len = fileName.Length();
3102 for (int32_t i = len; i >= 0; i--) {
3103 if (fileName[i] == char16_t('.')) {
3104 CopyUTF16toUTF8(Substring(fileName, i + 1), fileExt);
3105 break;
3110 if (fileExt.IsEmpty()) return NS_ERROR_FAILURE;
3112 return GetTypeFromExtension(fileExt, aContentType);
3115 nsresult nsExternalHelperAppService::FillMIMEInfoForMimeTypeFromExtras(
3116 const nsACString& aContentType, bool aOverwriteDescription,
3117 nsIMIMEInfo* aMIMEInfo) {
3118 NS_ENSURE_ARG(aMIMEInfo);
3120 NS_ENSURE_ARG(!aContentType.IsEmpty());
3122 // Look for default entry with matching mime type.
3123 nsAutoCString MIMEType(aContentType);
3124 ToLowerCase(MIMEType);
3125 for (auto entry : extraMimeEntries) {
3126 if (MIMEType.Equals(entry.mMimeType)) {
3127 // This is the one. Set attributes appropriately.
3128 nsDependentCString extensions(entry.mFileExtensions);
3129 nsACString::const_iterator start, end;
3130 extensions.BeginReading(start);
3131 extensions.EndReading(end);
3132 while (start != end) {
3133 nsACString::const_iterator cursor = start;
3134 mozilla::Unused << FindCharInReadable(',', cursor, end);
3135 aMIMEInfo->AppendExtension(Substring(start, cursor));
3136 // If a comma was found, skip it for the next search.
3137 start = cursor != end ? ++cursor : cursor;
3140 nsAutoString desc;
3141 aMIMEInfo->GetDescription(desc);
3142 if (aOverwriteDescription || desc.IsEmpty()) {
3143 aMIMEInfo->SetDescription(NS_ConvertASCIItoUTF16(entry.mDescription));
3145 return NS_OK;
3149 return NS_ERROR_NOT_AVAILABLE;
3152 nsresult nsExternalHelperAppService::FillMIMEInfoForExtensionFromExtras(
3153 const nsACString& aExtension, nsIMIMEInfo* aMIMEInfo) {
3154 nsAutoCString type;
3155 bool found = GetTypeFromExtras(aExtension, type);
3156 if (!found) return NS_ERROR_NOT_AVAILABLE;
3157 return FillMIMEInfoForMimeTypeFromExtras(type, true, aMIMEInfo);
3160 bool nsExternalHelperAppService::MaybeReplacePrimaryExtension(
3161 const nsACString& aPrimaryExtension, nsIMIMEInfo* aMIMEInfo) {
3162 for (const auto& entry : sForbiddenPrimaryExtensions) {
3163 if (aPrimaryExtension.LowerCaseEqualsASCII(entry.mFileExtension)) {
3164 nsDependentCString mime(entry.mMimeType);
3165 for (const auto& extraEntry : extraMimeEntries) {
3166 if (mime.LowerCaseEqualsASCII(extraEntry.mMimeType)) {
3167 nsDependentCString goodExts(extraEntry.mFileExtensions);
3168 int32_t commaPos = goodExts.FindChar(',');
3169 commaPos = commaPos == kNotFound ? goodExts.Length() : commaPos;
3170 auto goodExt = Substring(goodExts, 0, commaPos);
3171 aMIMEInfo->SetPrimaryExtension(goodExt);
3172 return true;
3177 return false;
3180 bool nsExternalHelperAppService::GetTypeFromExtras(const nsACString& aExtension,
3181 nsACString& aMIMEType) {
3182 NS_ASSERTION(!aExtension.IsEmpty(), "Empty aExtension parameter!");
3184 // Look for default entry with matching extension.
3185 nsDependentCString::const_iterator start, end, iter;
3186 int32_t numEntries = ArrayLength(extraMimeEntries);
3187 for (int32_t index = 0; index < numEntries; index++) {
3188 nsDependentCString extList(extraMimeEntries[index].mFileExtensions);
3189 extList.BeginReading(start);
3190 extList.EndReading(end);
3191 iter = start;
3192 while (start != end) {
3193 FindCharInReadable(',', iter, end);
3194 if (Substring(start, iter)
3195 .Equals(aExtension, nsCaseInsensitiveCStringComparator)) {
3196 aMIMEType = extraMimeEntries[index].mMimeType;
3197 return true;
3199 if (iter != end) {
3200 ++iter;
3202 start = iter;
3206 return false;
3209 bool nsExternalHelperAppService::GetMIMETypeFromOSForExtension(
3210 const nsACString& aExtension, nsACString& aMIMEType) {
3211 bool found = false;
3212 nsCOMPtr<nsIMIMEInfo> mimeInfo;
3213 nsresult rv =
3214 GetMIMEInfoFromOS(""_ns, aExtension, &found, getter_AddRefs(mimeInfo));
3215 return NS_SUCCEEDED(rv) && found && mimeInfo &&
3216 NS_SUCCEEDED(mimeInfo->GetMIMEType(aMIMEType));
3219 nsresult nsExternalHelperAppService::GetMIMEInfoFromOS(
3220 const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound,
3221 nsIMIMEInfo** aMIMEInfo) {
3222 *aMIMEInfo = nullptr;
3223 *aFound = false;
3224 return NS_ERROR_NOT_IMPLEMENTED;
3227 bool nsExternalHelperAppService::GetFileNameFromChannel(nsIChannel* aChannel,
3228 nsAString& aFileName,
3229 nsIURI** aURI) {
3230 if (!aChannel) {
3231 return false;
3234 aChannel->GetURI(aURI);
3235 nsCOMPtr<nsIURL> url = do_QueryInterface(*aURI);
3237 // Check if we have a POST request, in which case we don't want to use
3238 // the url's extension
3239 bool allowURLExt = !net::ChannelIsPost(aChannel);
3241 // Check if we had a query string - we don't want to check the URL
3242 // extension if a query is present in the URI
3243 // If we already know we don't want to check the URL extension, don't
3244 // bother checking the query
3245 if (url && allowURLExt) {
3246 nsAutoCString query;
3248 // We only care about the query for HTTP and HTTPS URLs
3249 if (url->SchemeIs("http") || url->SchemeIs("https")) {
3250 url->GetQuery(query);
3253 // Only get the extension if the query is empty; if it isn't, then the
3254 // extension likely belongs to a cgi script and isn't helpful
3255 allowURLExt = query.IsEmpty();
3258 aChannel->GetContentDispositionFilename(aFileName);
3260 return allowURLExt;
3263 NS_IMETHODIMP
3264 nsExternalHelperAppService::GetValidFileName(nsIChannel* aChannel,
3265 const nsACString& aType,
3266 nsIURI* aOriginalURI,
3267 uint32_t aFlags,
3268 nsAString& aOutFileName) {
3269 nsCOMPtr<nsIURI> uri;
3270 bool allowURLExtension =
3271 GetFileNameFromChannel(aChannel, aOutFileName, getter_AddRefs(uri));
3273 nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
3274 aOutFileName, aType, uri, aOriginalURI, aFlags, allowURLExtension);
3275 return NS_OK;
3278 NS_IMETHODIMP
3279 nsExternalHelperAppService::ValidateFileNameForSaving(
3280 const nsAString& aFileName, const nsACString& aType, uint32_t aFlags,
3281 nsAString& aOutFileName) {
3282 nsAutoString fileName(aFileName);
3284 // Just sanitize the filename only.
3285 if (aFlags & VALIDATE_SANITIZE_ONLY) {
3286 SanitizeFileName(fileName, aFlags);
3287 } else {
3288 nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
3289 fileName, aType, nullptr, nullptr, aFlags, true);
3292 aOutFileName = fileName;
3293 return NS_OK;
3296 already_AddRefed<nsIMIMEInfo>
3297 nsExternalHelperAppService::ValidateFileNameForSaving(
3298 nsAString& aFileName, const nsACString& aMimeType, nsIURI* aURI,
3299 nsIURI* aOriginalURI, uint32_t aFlags, bool aAllowURLExtension) {
3300 nsAutoString fileName(aFileName);
3301 nsAutoCString extension;
3302 nsCOMPtr<nsIMIMEInfo> mimeInfo;
3304 bool isBinaryType = aMimeType.EqualsLiteral(APPLICATION_OCTET_STREAM) ||
3305 aMimeType.EqualsLiteral(BINARY_OCTET_STREAM) ||
3306 aMimeType.EqualsLiteral("application/x-msdownload");
3308 // We get the mime service here even though we're the default implementation
3309 // of it, so it's possible to override only the mime service and not need to
3310 // reimplement the whole external helper app service itself.
3311 nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
3312 if (mimeService) {
3313 if (fileName.IsEmpty()) {
3314 nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
3315 // Try to extract the file name from the url and use that as a first
3316 // pass as the leaf name of our temp file...
3317 if (url) {
3318 nsAutoCString leafName;
3319 url->GetFileName(leafName);
3320 if (!leafName.IsEmpty()) {
3321 if (NS_SUCCEEDED(UnescapeFragment(leafName, url, fileName))) {
3322 CopyUTF8toUTF16(leafName, aFileName); // use escaped name
3326 // Only get the extension from the URL if allowed, or if this
3327 // is a binary type in which case the type might not be valid
3328 // anyway.
3329 if (aAllowURLExtension || isBinaryType) {
3330 url->GetFileExtension(extension);
3333 } else {
3334 // Determine the current extension for the filename.
3335 int32_t dotidx = fileName.RFind(u".");
3336 if (dotidx != -1) {
3337 CopyUTF16toUTF8(Substring(fileName, dotidx + 1), extension);
3341 if (aFlags & VALIDATE_GUESS_FROM_EXTENSION) {
3342 nsAutoCString mimeType;
3343 if (!extension.IsEmpty()) {
3344 mimeService->GetFromTypeAndExtension(EmptyCString(), extension,
3345 getter_AddRefs(mimeInfo));
3346 if (mimeInfo) {
3347 mimeInfo->GetMIMEType(mimeType);
3351 if (mimeType.IsEmpty()) {
3352 // Extension lookup gave us no useful match, so use octet-stream
3353 // instead.
3354 mimeService->GetFromTypeAndExtension(
3355 nsLiteralCString(APPLICATION_OCTET_STREAM), extension,
3356 getter_AddRefs(mimeInfo));
3358 } else if (!aMimeType.IsEmpty()) {
3359 // If this is a binary type, include the extension as a hint to get
3360 // the mime info. For other types, the mime type itself should be
3361 // sufficient.
3362 // The special case for application/ogg is because that type could
3363 // actually be used for a video which can better be determined by the
3364 // extension. This is tested by browser_save_video.js.
3365 bool useExtension =
3366 isBinaryType || aMimeType.EqualsLiteral(APPLICATION_OGG);
3367 mimeService->GetFromTypeAndExtension(
3368 aMimeType, useExtension ? extension : EmptyCString(),
3369 getter_AddRefs(mimeInfo));
3370 if (mimeInfo) {
3371 // But if no primary extension was returned, this mime type is probably
3372 // an unknown type. Look it up again but this time supply the extension.
3373 nsAutoCString primaryExtension;
3374 mimeInfo->GetPrimaryExtension(primaryExtension);
3375 if (primaryExtension.IsEmpty()) {
3376 mimeService->GetFromTypeAndExtension(aMimeType, extension,
3377 getter_AddRefs(mimeInfo));
3383 // Windows ignores terminating dots. So we have to as well, so
3384 // that our security checks do "the right thing"
3385 fileName.Trim(".", false);
3387 // If an empty filename is allowed, then return early. It will be saved
3388 // using the filename of the temporary file that was created for the download.
3389 if (aFlags & VALIDATE_ALLOW_EMPTY && fileName.IsEmpty()) {
3390 aFileName.Truncate();
3391 return mimeInfo.forget();
3394 // This section modifies the extension on the filename if it isn't valid for
3395 // the given content type.
3396 if (mimeInfo) {
3397 bool isValidExtension;
3398 if (extension.IsEmpty() ||
3399 NS_FAILED(mimeInfo->ExtensionExists(extension, &isValidExtension)) ||
3400 !isValidExtension) {
3401 // Skip these checks for text and binary, so we don't append the unneeded
3402 // .txt or other extension.
3403 if (aMimeType.EqualsLiteral(TEXT_PLAIN) || isBinaryType) {
3404 extension.Truncate();
3405 } else {
3406 nsAutoCString originalExtension(extension);
3407 // If an original url was supplied, see if it has a valid extension.
3408 bool useOldExtension = false;
3409 if (aOriginalURI) {
3410 nsCOMPtr<nsIURL> originalURL(do_QueryInterface(aOriginalURI));
3411 if (originalURL) {
3412 nsAutoCString uriExtension;
3413 originalURL->GetFileExtension(uriExtension);
3414 if (!uriExtension.IsEmpty()) {
3415 mimeInfo->ExtensionExists(uriExtension, &useOldExtension);
3416 if (useOldExtension) {
3417 extension = uriExtension;
3423 if (!useOldExtension) {
3424 // If the filename doesn't have a valid extension, or we don't know
3425 // the extension, try to use the primary extension for the type. If we
3426 // don't know the primary extension for the type, just continue with
3427 // the existing extension, or leave the filename with no extension.
3428 nsAutoCString primaryExtension;
3429 mimeInfo->GetPrimaryExtension(primaryExtension);
3430 if (!primaryExtension.IsEmpty()) {
3431 extension = primaryExtension;
3435 // If an suitable extension was found, we will append to or replace the
3436 // existing extension.
3437 if (!extension.IsEmpty()) {
3438 ModifyExtensionType modify = ShouldModifyExtension(
3439 mimeInfo, aFlags & VALIDATE_FORCE_APPEND_EXTENSION,
3440 originalExtension);
3441 if (modify == ModifyExtension_Replace) {
3442 int32_t dotidx = fileName.RFind(u".");
3443 if (dotidx != -1) {
3444 // Remove the existing extension and replace it.
3445 fileName.Truncate(dotidx);
3449 // Otherwise, just append the proper extension to the end of the
3450 // filename, adding to the invalid extension that might already be
3451 // there.
3452 if (modify != ModifyExtension_Ignore) {
3453 fileName.AppendLiteral(".");
3454 fileName.Append(NS_ConvertUTF8toUTF16(extension));
3461 #ifdef XP_WIN
3462 nsLocalFile::CheckForReservedFileName(fileName);
3463 #endif
3465 // If the extension is .lnk or .local, replace it with .download, as these
3466 // types of files can have signifance on Windows. This happens for any file,
3467 // not just those with the shortcut mime type.
3468 if (StringEndsWith(fileName, u".lnk"_ns) ||
3469 StringEndsWith(fileName, u".local"_ns)) {
3470 fileName.AppendLiteral(".download");
3473 // If no filename is present, use a default filename.
3474 if (!(aFlags & VALIDATE_NO_DEFAULT_FILENAME) &&
3475 (fileName.Length() == 0 || fileName.RFind(u".") == 0)) {
3476 nsCOMPtr<nsIStringBundleService> stringService =
3477 mozilla::components::StringBundle::Service();
3478 if (stringService) {
3479 nsCOMPtr<nsIStringBundle> bundle;
3480 if (NS_SUCCEEDED(stringService->CreateBundle(
3481 "chrome://global/locale/contentAreaCommands.properties",
3482 getter_AddRefs(bundle)))) {
3483 nsAutoString defaultFileName;
3484 bundle->GetStringFromName("DefaultSaveFileName", defaultFileName);
3485 // Append any existing extension to the default filename.
3486 fileName = defaultFileName + fileName;
3490 // Use 'index' as a last resort.
3491 if (!fileName.Length()) {
3492 fileName.AssignLiteral("index");
3496 // Make the filename safe for the filesystem.
3497 SanitizeFileName(fileName, aFlags);
3499 aFileName = fileName;
3500 return mimeInfo.forget();
3503 void nsExternalHelperAppService::SanitizeFileName(nsAString& aFileName,
3504 uint32_t aFlags) {
3505 nsAutoString fileName(aFileName);
3507 // Replace known invalid characters.
3508 fileName.ReplaceChar(u"" KNOWN_PATH_SEPARATORS, u'_');
3509 fileName.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u' ');
3510 fileName.StripChar(char16_t(0));
3512 const char16_t *startStr, *endStr;
3513 fileName.BeginReading(startStr);
3514 fileName.EndReading(endStr);
3516 // True if multiple consecutive whitespace characters should
3517 // be replaced by single space ' '.
3518 bool collapseWhitespace = !(aFlags & VALIDATE_DONT_COLLAPSE_WHITESPACE);
3520 // The maximum filename length differs based on the platform:
3521 // Windows (FAT/NTFS) stores filenames as a maximum of 255 UTF-16 code units.
3522 // Mac (APFS) stores filenames with a maximum 255 of UTF-8 code units.
3523 // Linux (ext3/ext4...) stores filenames with a maximum 255 bytes.
3524 // So here we just use the maximum of 255 bytes.
3525 // 0 means don't truncate at a maximum size.
3526 const uint32_t maxBytes =
3527 (aFlags & VALIDATE_DONT_TRUNCATE) ? 0 : kDefaultMaxFileNameLength;
3529 // True if the last character added was whitespace.
3530 bool lastWasWhitespace = false;
3532 // Length of the filename that fits into the maximum size excluding the
3533 // extension and period.
3534 int32_t longFileNameEnd = -1;
3536 // Index of the last character added that was not a character that can be
3537 // trimmed off of the end of the string. Trimmable characters are whitespace,
3538 // periods and the vowel separator u'\u180e'. If all the characters after this
3539 // point are trimmable characters, truncate the string to this point after
3540 // iterating over the filename.
3541 int32_t lastNonTrimmable = -1;
3543 // The number of bytes that the string would occupy if encoded in UTF-8.
3544 uint32_t bytesLength = 0;
3546 // The length of the extension in bytes.
3547 uint32_t extensionBytesLength = 0;
3549 // This algorithm iterates over each character in the string and appends it
3550 // or a replacement character if needed to outFileName.
3551 nsAutoString outFileName;
3552 while (startStr < endStr) {
3553 bool err = false;
3554 char32_t nextChar = UTF16CharEnumerator::NextChar(&startStr, endStr, &err);
3555 if (err) {
3556 break;
3559 // nulls are already stripped out above.
3560 MOZ_ASSERT(nextChar != char16_t(0));
3562 auto unicodeCategory = unicode::GetGeneralCategory(nextChar);
3563 if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_CONTROL ||
3564 unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR ||
3565 unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_PARAGRAPH_SEPARATOR) {
3566 // Skip over any control characters and separators.
3567 continue;
3570 if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_SPACE_SEPARATOR ||
3571 nextChar == u'\ufeff') {
3572 // Trim out any whitespace characters at the beginning of the filename,
3573 // and only add whitespace in the middle of the filename if the last
3574 // character was not whitespace or if we are not collapsing whitespace.
3575 if (!outFileName.IsEmpty() &&
3576 (!lastWasWhitespace || !collapseWhitespace)) {
3577 // Allow the ideographic space if it is present, otherwise replace with
3578 // ' '.
3579 if (nextChar != u'\u3000') {
3580 nextChar = ' ';
3582 lastWasWhitespace = true;
3583 } else {
3584 lastWasWhitespace = true;
3585 continue;
3587 } else {
3588 lastWasWhitespace = false;
3589 if (nextChar == '.' || nextChar == u'\u180e') {
3590 // Don't add any periods or vowel separators at the beginning of the
3591 // string. Note also that lastNonTrimmable is not adjusted in this
3592 // case, because periods and vowel separators are included in the
3593 // set of characters to trim at the end of the filename.
3594 if (outFileName.IsEmpty()) {
3595 continue;
3597 } else {
3598 if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_FORMAT) {
3599 // Replace formatting characters with an underscore.
3600 nextChar = '_';
3603 // Don't truncate surrogate pairs in the middle.
3604 lastNonTrimmable =
3605 int32_t(outFileName.Length()) +
3606 (NS_IS_HIGH_SURROGATE(H_SURROGATE(nextChar)) ? 2 : 1);
3610 if (maxBytes) {
3611 // UTF16CharEnumerator already converts surrogate pairs, so we can use
3612 // a simple computation of byte length here.
3613 uint32_t charBytesLength = nextChar < 0x80 ? 1
3614 : nextChar < 0x800 ? 2
3615 : nextChar < 0x10000 ? 3
3616 : 4;
3617 bytesLength += charBytesLength;
3618 if (bytesLength > maxBytes) {
3619 if (longFileNameEnd == -1) {
3620 longFileNameEnd = int32_t(outFileName.Length());
3624 // If we encounter a period, it could be the start of an extension, so
3625 // start counting the number of bytes in the extension. If another period
3626 // is found, start again since we want to use the last extension found.
3627 if (nextChar == u'.') {
3628 extensionBytesLength = 1; // 1 byte for the period.
3629 } else if (extensionBytesLength) {
3630 extensionBytesLength += charBytesLength;
3634 AppendUCS4ToUTF16(nextChar, outFileName);
3637 // If the filename is longer than the maximum allowed filename size,
3638 // truncate it, but preserve the desired extension that is currently
3639 // on the filename.
3640 if (bytesLength > maxBytes && !outFileName.IsEmpty()) {
3641 // Get the sanitized extension from the filename without the dot.
3642 nsAutoString extension;
3643 int32_t dotidx = outFileName.RFind(u".");
3644 if (dotidx != -1) {
3645 extension = Substring(outFileName, dotidx + 1);
3648 // There are two ways in which the filename should be truncated:
3649 // - If the filename was too long, truncate the name at the length
3650 // of the filename.
3651 // This position is indicated by longFileNameEnd.
3652 // - lastNonTrimmable will indicate the last character that was not
3653 // whitespace, a period, or a vowel separator at the end of the
3654 // the string, so the string should be truncated there as well.
3655 // If both apply, use the earliest position.
3656 if (lastNonTrimmable >= 0) {
3657 // Subtract off the amount for the extension and the period.
3658 // Note that the extension length is in bytes but longFileNameEnd is in
3659 // characters, but if they don't match, it just means we crop off
3660 // more than is necessary. This is OK since it is better than cropping
3661 // off too little.
3662 longFileNameEnd -= extensionBytesLength;
3663 if (longFileNameEnd <= 0) {
3664 // This is extremely unlikely, but if the extension is larger than the
3665 // maximum size, just get rid of it. In this case, the extension
3666 // wouldn't have been an ordinary one we would want to preserve (such
3667 // as .html or .png) so just truncate off the file wherever the first
3668 // period appears.
3669 int32_t dotidx = outFileName.Find(u".");
3670 outFileName.Truncate(dotidx > 0 ? dotidx : 1);
3671 } else {
3672 outFileName.Truncate(std::min(longFileNameEnd, lastNonTrimmable));
3674 // Now that the filename has been truncated, re-append the extension
3675 // again.
3676 if (!extension.IsEmpty()) {
3677 if (outFileName.Last() != '.') {
3678 outFileName.AppendLiteral(".");
3681 outFileName.Append(extension);
3685 } else if (lastNonTrimmable >= 0) {
3686 // Otherwise, the filename wasn't too long, so just trim off the
3687 // extra whitespace and periods at the end.
3688 outFileName.Truncate(lastNonTrimmable);
3691 aFileName = outFileName;
3694 nsExternalHelperAppService::ModifyExtensionType
3695 nsExternalHelperAppService::ShouldModifyExtension(nsIMIMEInfo* aMimeInfo,
3696 bool aForceAppend,
3697 const nsCString& aFileExt) {
3698 nsAutoCString MIMEType;
3699 if (!aMimeInfo || NS_FAILED(aMimeInfo->GetMIMEType(MIMEType))) {
3700 return ModifyExtension_Append;
3703 // Determine whether the extensions should be appended or replaced depending
3704 // on the content type.
3705 bool canForce = StringBeginsWith(MIMEType, "image/"_ns) ||
3706 StringBeginsWith(MIMEType, "audio/"_ns) ||
3707 StringBeginsWith(MIMEType, "video/"_ns) || aFileExt.IsEmpty();
3709 if (!canForce) {
3710 for (const char* mime : forcedExtensionMimetypes) {
3711 if (MIMEType.Equals(mime)) {
3712 if (!StaticPrefs::browser_download_sanitize_non_media_extensions()) {
3713 return ModifyExtension_Ignore;
3715 canForce = true;
3716 break;
3720 if (!canForce) {
3721 return aForceAppend ? ModifyExtension_Append : ModifyExtension_Ignore;
3725 // If we get here, we know for sure the mimetype allows us to modify the
3726 // existing extension, if it's wrong. Return whether we should replace it
3727 // or append it.
3728 bool knownExtension = false;
3729 // Note that aFileExt is either empty or consists of an extension
3730 // excluding the dot.
3731 if (aFileExt.IsEmpty() ||
3732 (NS_SUCCEEDED(aMimeInfo->ExtensionExists(aFileExt, &knownExtension)) &&
3733 !knownExtension)) {
3734 return ModifyExtension_Replace;
3737 return ModifyExtension_Append;