Bug 1703654 - Deal with Maybe<> not trivially_copyable on build-linux64-base-toolchai...
[gecko.git] / uriloader / exthandler / nsExternalHelperAppService.cpp
blob1ddfd9f652e581a3afc2bdb35bc5ff8b2aec65d7
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/WindowGlobalParent.h"
18 #include "mozilla/RandomNum.h"
19 #include "mozilla/StaticPrefs_dom.h"
20 #include "mozilla/StaticPrefs_security.h"
21 #include "mozilla/StaticPtr.h"
22 #include "nsXULAppAPI.h"
24 #include "nsExternalHelperAppService.h"
25 #include "nsCExternalHandlerService.h"
26 #include "nsIURI.h"
27 #include "nsIURL.h"
28 #include "nsIFile.h"
29 #include "nsIFileURL.h"
30 #include "nsIChannel.h"
31 #include "nsAppDirectoryServiceDefs.h"
32 #include "nsICategoryManager.h"
33 #include "nsDependentSubstring.h"
34 #include "nsSandboxFlags.h"
35 #include "nsString.h"
36 #include "nsUnicharUtils.h"
37 #include "nsIStringEnumerator.h"
38 #include "nsMemory.h"
39 #include "nsIStreamListener.h"
40 #include "nsIMIMEService.h"
41 #include "nsILoadGroup.h"
42 #include "nsIWebProgressListener.h"
43 #include "nsITransfer.h"
44 #include "nsReadableUtils.h"
45 #include "nsIRequest.h"
46 #include "nsDirectoryServiceDefs.h"
47 #include "nsIInterfaceRequestor.h"
48 #include "nsThreadUtils.h"
49 #include "nsIMutableArray.h"
50 #include "nsIRedirectHistoryEntry.h"
51 #include "nsOSHelperAppService.h"
52 #include "nsOSHelperAppServiceChild.h"
53 #include "nsContentSecurityUtils.h"
55 // used to access our datastore of user-configured helper applications
56 #include "nsIHandlerService.h"
57 #include "nsIMIMEInfo.h"
58 #include "nsIHelperAppLauncherDialog.h"
59 #include "nsIContentDispatchChooser.h"
60 #include "nsNetUtil.h"
61 #include "nsIPrivateBrowsingChannel.h"
62 #include "nsIIOService.h"
63 #include "nsNetCID.h"
65 #include "nsIApplicationReputation.h"
67 #include "nsDSURIContentListener.h"
68 #include "nsMimeTypes.h"
69 #include "nsMIMEInfoImpl.h"
70 // used for header disposition information.
71 #include "nsIHttpChannel.h"
72 #include "nsIHttpChannelInternal.h"
73 #include "nsIEncodedChannel.h"
74 #include "nsIMultiPartChannel.h"
75 #include "nsIFileChannel.h"
76 #include "nsIObserverService.h" // so we can be a profile change observer
77 #include "nsIPropertyBag2.h" // for the 64-bit content length
79 #ifdef XP_MACOSX
80 # include "nsILocalFileMac.h"
81 #endif
83 #include "nsEscape.h"
85 #include "nsIStringBundle.h" // XXX needed to localize error msgs
86 #include "nsIPrompt.h"
88 #include "nsITextToSubURI.h" // to unescape the filename
90 #include "nsDocShellCID.h"
92 #include "nsCRT.h"
93 #include "nsLocalHandlerApp.h"
95 #include "nsIRandomGenerator.h"
97 #include "ContentChild.h"
98 #include "nsXULAppAPI.h"
99 #include "nsPIDOMWindow.h"
100 #include "ExternalHelperAppChild.h"
102 #include "mozilla/dom/nsHTTPSOnlyUtils.h"
104 #ifdef XP_WIN
105 # include "nsWindowsHelpers.h"
106 #endif
108 #include "mozilla/Components.h"
109 #include "mozilla/ClearOnShutdown.h"
110 #include "mozilla/Preferences.h"
111 #include "mozilla/ipc/URIUtils.h"
113 using namespace mozilla;
114 using namespace mozilla::ipc;
115 using namespace mozilla::dom;
117 // Download Folder location constants
118 #define NS_PREF_DOWNLOAD_DIR "browser.download.dir"
119 #define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList"
120 enum {
121 NS_FOLDER_VALUE_DESKTOP = 0,
122 NS_FOLDER_VALUE_DOWNLOADS = 1,
123 NS_FOLDER_VALUE_CUSTOM = 2
126 LazyLogModule nsExternalHelperAppService::mLog("HelperAppService");
128 // Using level 3 here because the OSHelperAppServices use a log level
129 // of LogLevel::Debug (4), and we want less detailed output here
130 // Using 3 instead of LogLevel::Warning because we don't output warnings
131 #undef LOG
132 #define LOG(args) \
133 MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info, args)
134 #define LOG_ENABLED() \
135 MOZ_LOG_TEST(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info)
137 static const char NEVER_ASK_FOR_SAVE_TO_DISK_PREF[] =
138 "browser.helperApps.neverAsk.saveToDisk";
139 static const char NEVER_ASK_FOR_OPEN_FILE_PREF[] =
140 "browser.helperApps.neverAsk.openFile";
142 StaticRefPtr<nsIFile> sFallbackDownloadDir;
144 // Helper functions for Content-Disposition headers
147 * Given a URI fragment, unescape it
148 * @param aFragment The string to unescape
149 * @param aURI The URI from which this fragment is taken. Only its character set
150 * will be used.
151 * @param aResult [out] Unescaped string.
153 static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI,
154 nsAString& aResult) {
155 // We need the unescaper
156 nsresult rv;
157 nsCOMPtr<nsITextToSubURI> textToSubURI =
158 do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv);
159 NS_ENSURE_SUCCESS(rv, rv);
161 return textToSubURI->UnEscapeURIForUI(aFragment, /* aDontEscape = */ true,
162 aResult);
166 * UTF-8 version of UnescapeFragment.
167 * @param aFragment The string to unescape
168 * @param aURI The URI from which this fragment is taken. Only its character set
169 * will be used.
170 * @param aResult [out] Unescaped string, UTF-8 encoded.
171 * @note It is safe to pass the same string for aFragment and aResult.
172 * @note When this function fails, aResult will not be modified.
174 static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI,
175 nsACString& aResult) {
176 nsAutoString result;
177 nsresult rv = UnescapeFragment(aFragment, aURI, result);
178 if (NS_SUCCEEDED(rv)) CopyUTF16toUTF8(result, aResult);
179 return rv;
183 * Given a channel, returns the filename and extension the channel has.
184 * This uses the URL and other sources (nsIMultiPartChannel).
185 * Also gives back whether the channel requested external handling (i.e.
186 * whether Content-Disposition: attachment was sent)
187 * @param aChannel The channel to extract the filename/extension from
188 * @param aFileName [out] Reference to the string where the filename should be
189 * stored. Empty if it could not be retrieved.
190 * WARNING - this filename may contain characters which the OS does not
191 * allow as part of filenames!
192 * @param aExtension [out] Reference to the string where the extension should
193 * be stored. Empty if it could not be retrieved. Stored in UTF-8.
194 * @param aAllowURLExtension (optional) Get the extension from the URL if no
195 * Content-Disposition header is present. Default is true.
196 * @retval true The server sent Content-Disposition:attachment or equivalent
197 * @retval false Content-Disposition: inline or no content-disposition header
198 * was sent.
200 static bool GetFilenameAndExtensionFromChannel(nsIChannel* aChannel,
201 nsString& aFileName,
202 nsCString& aExtension,
203 bool aAllowURLExtension = true) {
204 aExtension.Truncate();
206 * If the channel is an http or part of a multipart channel and we
207 * have a content disposition header set, then use the file name
208 * suggested there as the preferred file name to SUGGEST to the
209 * user. we shouldn't actually use that without their
210 * permission... otherwise just use our temp file
212 bool handleExternally = false;
213 uint32_t disp;
214 nsresult rv = aChannel->GetContentDisposition(&disp);
215 bool gotFileNameFromURI = false;
216 if (NS_SUCCEEDED(rv)) {
217 aChannel->GetContentDispositionFilename(aFileName);
218 if (disp == nsIChannel::DISPOSITION_ATTACHMENT) handleExternally = true;
221 // If the disposition header didn't work, try the filename from nsIURL
222 nsCOMPtr<nsIURI> uri;
223 aChannel->GetURI(getter_AddRefs(uri));
224 nsCOMPtr<nsIURL> url(do_QueryInterface(uri));
225 if (url && aFileName.IsEmpty()) {
226 if (aAllowURLExtension) {
227 url->GetFileExtension(aExtension);
228 UnescapeFragment(aExtension, url, aExtension);
230 // Windows ignores terminating dots. So we have to as well, so
231 // that our security checks do "the right thing"
232 // In case the aExtension consisted only of the dot, the code below will
233 // extract an aExtension from the filename
234 aExtension.Trim(".", false);
237 // try to extract the file name from the url and use that as a first pass as
238 // the leaf name of our temp file...
239 nsAutoCString leafName;
240 url->GetFileName(leafName);
241 if (!leafName.IsEmpty()) {
242 gotFileNameFromURI = true;
243 rv = UnescapeFragment(leafName, url, aFileName);
244 if (NS_FAILED(rv)) {
245 CopyUTF8toUTF16(leafName, aFileName); // use escaped name
250 // If we have a filename and no extension, remove trailing dots from the
251 // filename and extract the extension if that is possible.
252 if (aExtension.IsEmpty() && !aFileName.IsEmpty()) {
253 // Windows ignores terminating dots. So we have to as well, so
254 // that our security checks do "the right thing"
255 aFileName.Trim(".", false);
256 // We can get an extension if the filename is from a header, or if getting
257 // it from the URL was allowed.
258 bool canGetExtensionFromFilename =
259 !gotFileNameFromURI || aAllowURLExtension;
260 // ... , or if the mimetype is meaningless and we have nothing to go on:
261 if (!canGetExtensionFromFilename) {
262 nsAutoCString contentType;
263 if (NS_SUCCEEDED(aChannel->GetContentType(contentType))) {
264 canGetExtensionFromFilename =
265 contentType.EqualsIgnoreCase(APPLICATION_OCTET_STREAM) ||
266 contentType.EqualsIgnoreCase("binary/octet-stream") ||
267 contentType.EqualsIgnoreCase("application/x-msdownload");
271 if (canGetExtensionFromFilename) {
272 // XXX RFindCharInReadable!!
273 nsAutoString fileNameStr(aFileName);
274 int32_t idx = fileNameStr.RFindChar(char16_t('.'));
275 if (idx != kNotFound)
276 CopyUTF16toUTF8(StringTail(fileNameStr, fileNameStr.Length() - idx - 1),
277 aExtension);
281 return handleExternally;
285 * Obtains the directory to use. This tends to vary per platform, and
286 * needs to be consistent throughout our codepaths. For platforms where
287 * helper apps use the downloads directory, this should be kept in
288 * sync with DownloadIntegration.jsm.
290 * Optionally skip availability of the directory and storage.
292 static nsresult GetDownloadDirectory(nsIFile** _directory,
293 bool aSkipChecks = false) {
294 #if defined(ANDROID)
295 return NS_ERROR_FAILURE;
296 #endif
298 bool usePrefDir =
299 StaticPrefs::browser_download_improvements_to_download_panel();
300 #ifdef XP_MACOSX
301 usePrefDir = true;
302 #endif
304 nsCOMPtr<nsIFile> dir;
305 nsresult rv;
306 if (usePrefDir) {
307 // Try to get the users download location, if it's set.
308 switch (Preferences::GetInt(NS_PREF_DOWNLOAD_FOLDERLIST, -1)) {
309 case NS_FOLDER_VALUE_DESKTOP:
310 (void)NS_GetSpecialDirectory(NS_OS_DESKTOP_DIR, getter_AddRefs(dir));
311 break;
312 case NS_FOLDER_VALUE_CUSTOM: {
313 Preferences::GetComplex(NS_PREF_DOWNLOAD_DIR, NS_GET_IID(nsIFile),
314 getter_AddRefs(dir));
315 if (!dir) break;
317 // If we're not checking for availability we're done.
318 if (aSkipChecks) {
319 dir.forget(_directory);
320 return NS_OK;
323 // We have the directory, and now we need to make sure it exists
324 nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755);
325 // If we can't create this and it's not because the file already
326 // exists, clear out `dir` so we don't return it.
327 if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_FAILED(rv)) {
328 dir = nullptr;
330 } break;
331 case NS_FOLDER_VALUE_DOWNLOADS:
332 // This is just the OS default location, so fall out
333 break;
335 if (!dir) {
336 rv = NS_GetSpecialDirectory(NS_OS_DEFAULT_DOWNLOAD_DIR,
337 getter_AddRefs(dir));
338 if (NS_FAILED(rv)) {
339 // On some OSes, there is no guarantee this directory exists.
340 // Fall back to $HOME + Downloads.
341 if (sFallbackDownloadDir) {
342 sFallbackDownloadDir->Clone(getter_AddRefs(dir));
343 } else {
344 rv = NS_GetSpecialDirectory(NS_OS_HOME_DIR, getter_AddRefs(dir));
345 NS_ENSURE_SUCCESS(rv, rv);
347 nsCOMPtr<nsIStringBundleService> bundleService =
348 do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
349 NS_ENSURE_SUCCESS(rv, rv);
350 nsAutoString downloadLocalized;
351 nsCOMPtr<nsIStringBundle> downloadBundle;
352 rv = bundleService->CreateBundle(
353 "chrome://mozapps/locale/downloads/downloads.properties",
354 getter_AddRefs(downloadBundle));
355 if (NS_SUCCEEDED(rv)) {
356 rv = downloadBundle->GetStringFromName("DownloadsFolder",
357 downloadLocalized);
359 if (NS_FAILED(rv)) {
360 downloadLocalized.AssignLiteral("Downloads");
362 rv = dir->Append(downloadLocalized);
363 NS_ENSURE_SUCCESS(rv, rv);
364 // Can't getter_AddRefs on StaticRefPtr, so do some copying.
365 nsCOMPtr<nsIFile> copy;
366 dir->Clone(getter_AddRefs(copy));
367 sFallbackDownloadDir = copy.forget();
368 ClearOnShutdown(&sFallbackDownloadDir);
370 if (aSkipChecks) {
371 dir.forget(_directory);
372 return NS_OK;
375 // We have the directory, and now we need to make sure it exists
376 rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755);
377 if (rv == NS_ERROR_FILE_ALREADY_EXISTS || NS_SUCCEEDED(rv)) {
378 dir.forget(_directory);
379 rv = NS_OK;
381 return rv;
383 NS_ENSURE_SUCCESS(rv, rv);
385 } else {
386 rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dir));
387 NS_ENSURE_SUCCESS(rv, rv);
389 #if !defined(XP_MACOSX) && defined(XP_UNIX)
390 // Ensuring that only the current user can read the file names we end up
391 // creating. Note that creating directories with a specified permission is
392 // only supported on Unix platform right now. That's why the above check
393 // exists.
395 uint32_t permissions;
396 rv = dir->GetPermissions(&permissions);
397 NS_ENSURE_SUCCESS(rv, rv);
399 if (permissions != PR_IRWXU) {
400 const char* userName = PR_GetEnv("USERNAME");
401 if (!userName || !*userName) {
402 userName = PR_GetEnv("USER");
404 if (!userName || !*userName) {
405 userName = PR_GetEnv("LOGNAME");
407 if (!userName || !*userName) {
408 userName = "mozillaUser";
411 nsAutoString userDir;
412 userDir.AssignLiteral("mozilla_");
413 userDir.AppendASCII(userName);
414 userDir.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_');
416 int counter = 0;
417 bool pathExists;
418 nsCOMPtr<nsIFile> finalPath;
420 while (true) {
421 nsAutoString countedUserDir(userDir);
422 countedUserDir.AppendInt(counter, 10);
423 dir->Clone(getter_AddRefs(finalPath));
424 finalPath->Append(countedUserDir);
426 rv = finalPath->Exists(&pathExists);
427 NS_ENSURE_SUCCESS(rv, rv);
429 if (pathExists) {
430 // If this path has the right permissions, use it.
431 rv = finalPath->GetPermissions(&permissions);
432 NS_ENSURE_SUCCESS(rv, rv);
434 // Ensuring the path is writable by the current user.
435 bool isWritable;
436 rv = finalPath->IsWritable(&isWritable);
437 NS_ENSURE_SUCCESS(rv, rv);
439 if (permissions == PR_IRWXU && isWritable) {
440 dir = finalPath;
441 break;
445 rv = finalPath->Create(nsIFile::DIRECTORY_TYPE, PR_IRWXU);
446 if (NS_SUCCEEDED(rv)) {
447 dir = finalPath;
448 break;
450 if (rv != NS_ERROR_FILE_ALREADY_EXISTS) {
451 // Unexpected error.
452 return rv;
454 counter++;
458 #endif
461 NS_ASSERTION(dir, "Somehow we didn't get a download directory!");
462 dir.forget(_directory);
463 return NS_OK;
467 * Helper for random bytes for the filename of downloaded part files.
469 nsresult GenerateRandomName(nsACString& result) {
470 // We will request raw random bytes, and transform that to a base64 string,
471 // using url-based base64 encoding so that all characters from the base64
472 // result will be acceptable for filenames.
473 // For each three bytes of random data, we will get four bytes of ASCII.
474 // Request a bit more, to be safe, then truncate in the end.
476 nsresult rv;
477 const uint32_t wantedFileNameLength = 8;
478 const uint32_t requiredBytesLength =
479 static_cast<uint32_t>((wantedFileNameLength + 1) / 4 * 3);
481 uint8_t buffer[requiredBytesLength];
482 if (!mozilla::GenerateRandomBytesFromOS(buffer, requiredBytesLength)) {
483 return NS_ERROR_FAILURE;
486 nsAutoCString tempLeafName;
487 // We're forced to specify a padding policy, though this is guaranteed
488 // not to need padding due to requiredBytesLength being a multiple of 3.
489 rv = Base64URLEncode(requiredBytesLength, buffer,
490 Base64URLEncodePaddingPolicy::Omit, tempLeafName);
491 NS_ENSURE_SUCCESS(rv, rv);
493 tempLeafName.Truncate(wantedFileNameLength);
495 result.Assign(tempLeafName);
496 return NS_OK;
500 * Structure for storing extension->type mappings.
501 * @see defaultMimeEntries
503 struct nsDefaultMimeTypeEntry {
504 const char* mMimeType;
505 const char* mFileExtension;
509 * Default extension->mimetype mappings. These are not overridable.
510 * If you add types here, make sure they are lowercase, or you'll regret it.
512 static const nsDefaultMimeTypeEntry defaultMimeEntries[] = {
513 // The following are those extensions that we're asked about during startup,
514 // sorted by order used
515 {IMAGE_GIF, "gif"},
516 {TEXT_XML, "xml"},
517 {APPLICATION_RDF, "rdf"},
518 {IMAGE_PNG, "png"},
519 // -- end extensions used during startup
520 {TEXT_CSS, "css"},
521 {IMAGE_JPEG, "jpeg"},
522 {IMAGE_JPEG, "jpg"},
523 {IMAGE_SVG_XML, "svg"},
524 {TEXT_HTML, "html"},
525 {TEXT_HTML, "htm"},
526 {APPLICATION_XPINSTALL, "xpi"},
527 {"application/xhtml+xml", "xhtml"},
528 {"application/xhtml+xml", "xht"},
529 {TEXT_PLAIN, "txt"},
530 {APPLICATION_JSON, "json"},
531 {APPLICATION_XJAVASCRIPT, "js"},
532 {APPLICATION_XJAVASCRIPT, "jsm"},
533 {VIDEO_OGG, "ogv"},
534 {VIDEO_OGG, "ogg"},
535 {APPLICATION_OGG, "ogg"},
536 {AUDIO_OGG, "oga"},
537 {AUDIO_OGG, "opus"},
538 {APPLICATION_PDF, "pdf"},
539 {VIDEO_WEBM, "webm"},
540 {AUDIO_WEBM, "webm"},
541 {IMAGE_ICO, "ico"},
542 {TEXT_PLAIN, "properties"},
543 {TEXT_PLAIN, "locale"},
544 {TEXT_PLAIN, "ftl"},
545 #if defined(MOZ_WMF)
546 {VIDEO_MP4, "mp4"},
547 {AUDIO_MP4, "m4a"},
548 {AUDIO_MP3, "mp3"},
549 #endif
550 #ifdef MOZ_RAW
551 {VIDEO_RAW, "yuv"}
552 #endif
556 * This is a small private struct used to help us initialize some
557 * default mime types.
559 struct nsExtraMimeTypeEntry {
560 const char* mMimeType;
561 const char* mFileExtensions;
562 const char* mDescription;
566 * This table lists all of the 'extra' content types that we can deduce from
567 * particular file extensions. These entries also ensure that we provide a good
568 * descriptive name when we encounter files with these content types and/or
569 * extensions. These can be overridden by user helper app prefs. If you add
570 * types here, make sure they are lowercase, or you'll regret it.
572 static const nsExtraMimeTypeEntry extraMimeEntries[] = {
573 #if defined(XP_MACOSX) // don't define .bin on the mac...use internet config to
574 // look that up...
575 {APPLICATION_OCTET_STREAM, "exe,com", "Binary File"},
576 #else
577 {APPLICATION_OCTET_STREAM, "exe,com,bin", "Binary File"},
578 #endif
579 {APPLICATION_GZIP2, "gz", "gzip"},
580 {"application/x-arj", "arj", "ARJ file"},
581 {"application/rtf", "rtf", "Rich Text Format File"},
582 {APPLICATION_ZIP, "zip", "ZIP Archive"},
583 {APPLICATION_XPINSTALL, "xpi", "XPInstall Install"},
584 {APPLICATION_PDF, "pdf", "Portable Document Format"},
585 {APPLICATION_POSTSCRIPT, "ps,eps,ai", "Postscript File"},
586 {APPLICATION_XJAVASCRIPT, "js", "Javascript Source File"},
587 {APPLICATION_XJAVASCRIPT, "jsm,mjs", "Javascript Module Source File"},
588 #ifdef MOZ_WIDGET_ANDROID
589 {"application/vnd.android.package-archive", "apk", "Android Package"},
590 #endif
592 // OpenDocument formats
593 {"application/vnd.oasis.opendocument.text", "odt", "OpenDocument Text"},
594 {"application/vnd.oasis.opendocument.presentation", "odp",
595 "OpenDocument Presentation"},
596 {"application/vnd.oasis.opendocument.spreadsheet", "ods",
597 "OpenDocument Spreadsheet"},
598 {"application/vnd.oasis.opendocument.graphics", "odg",
599 "OpenDocument Graphics"},
601 // Legacy Microsoft Office
602 {"application/msword", "doc", "Microsoft Word"},
603 {"application/vnd.ms-powerpoint", "ppt", "Microsoft PowerPoint"},
604 {"application/vnd.ms-excel", "xls", "Microsoft Excel"},
606 // Office Open XML
607 {"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
608 "docx", "Microsoft Word (Open XML)"},
609 {"application/"
610 "vnd.openxmlformats-officedocument.presentationml.presentation",
611 "pptx", "Microsoft PowerPoint (Open XML)"},
612 {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
613 "xlsx", "Microsoft Excel (Open XML)"},
615 // Note: if you add new image types, please also update the list in
616 // contentAreaUtils.js to match.
617 {IMAGE_ART, "art", "ART Image"},
618 {IMAGE_BMP, "bmp", "BMP Image"},
619 {IMAGE_GIF, "gif", "GIF Image"},
620 {IMAGE_ICO, "ico,cur", "ICO Image"},
621 {IMAGE_JPEG, "jpg,jpeg,jfif,pjpeg,pjp", "JPEG Image"},
622 {IMAGE_PNG, "png", "PNG Image"},
623 {IMAGE_APNG, "apng", "APNG Image"},
624 {IMAGE_TIFF, "tiff,tif", "TIFF Image"},
625 {IMAGE_XBM, "xbm", "XBM Image"},
626 {IMAGE_SVG_XML, "svg", "Scalable Vector Graphics"},
627 {IMAGE_WEBP, "webp", "WebP Image"},
628 {IMAGE_AVIF, "avif", "AV1 Image File"},
629 {IMAGE_JXL, "jxl", "JPEG XL Image File"},
631 {MESSAGE_RFC822, "eml", "RFC-822 data"},
632 {TEXT_PLAIN, "txt,text", "Text File"},
633 {APPLICATION_JSON, "json", "JavaScript Object Notation"},
634 {TEXT_VTT, "vtt", "Web Video Text Tracks"},
635 {TEXT_CACHE_MANIFEST, "appcache", "Application Cache Manifest"},
636 {TEXT_HTML, "html,htm,shtml,ehtml", "HyperText Markup Language"},
637 {"application/xhtml+xml", "xhtml,xht",
638 "Extensible HyperText Markup Language"},
639 {APPLICATION_MATHML_XML, "mml", "Mathematical Markup Language"},
640 {APPLICATION_RDF, "rdf", "Resource Description Framework"},
641 {"text/csv", "csv", "CSV File"},
642 {TEXT_XML, "xml,xsl,xbl", "Extensible Markup Language"},
643 {TEXT_CSS, "css", "Style Sheet"},
644 {TEXT_VCARD, "vcf,vcard", "Contact Information"},
645 {TEXT_CALENDAR, "ics,ical,ifb,icalendar", "iCalendar"},
646 {VIDEO_OGG, "ogv", "Ogg Video"},
647 {VIDEO_OGG, "ogg", "Ogg Video"},
648 {APPLICATION_OGG, "ogg", "Ogg Video"},
649 {AUDIO_OGG, "oga", "Ogg Audio"},
650 {AUDIO_OGG, "opus", "Opus Audio"},
651 {VIDEO_WEBM, "webm", "Web Media Video"},
652 {AUDIO_WEBM, "webm", "Web Media Audio"},
653 {AUDIO_MP3, "mp3", "MPEG Audio"},
654 {VIDEO_MP4, "mp4", "MPEG-4 Video"},
655 {AUDIO_MP4, "m4a", "MPEG-4 Audio"},
656 {VIDEO_RAW, "yuv", "Raw YUV Video"},
657 {AUDIO_WAV, "wav", "Waveform Audio"},
658 {VIDEO_3GPP, "3gpp,3gp", "3GPP Video"},
659 {VIDEO_3GPP2, "3g2", "3GPP2 Video"},
660 {AUDIO_AAC, "aac", "AAC Audio"},
661 {AUDIO_FLAC, "flac", "FLAC Audio"},
662 {AUDIO_MIDI, "mid", "Standard MIDI Audio"},
663 {APPLICATION_WASM, "wasm", "WebAssembly Module"}};
665 static const nsDefaultMimeTypeEntry sForbiddenPrimaryExtensions[] = {
666 {IMAGE_JPEG, "jfif"}};
669 * File extensions for which decoding should be disabled.
670 * NOTE: These MUST be lower-case and ASCII.
672 static const nsDefaultMimeTypeEntry nonDecodableExtensions[] = {
673 {APPLICATION_GZIP, "gz"},
674 {APPLICATION_GZIP, "tgz"},
675 {APPLICATION_ZIP, "zip"},
676 {APPLICATION_COMPRESS, "z"},
677 {APPLICATION_GZIP, "svgz"}};
680 * Mimetypes for which we enforce using a known extension.
682 * In addition to this list, we do this for all audio/, video/ and
683 * image/ mimetypes.
685 static const char* forcedExtensionMimetypes[] = {
686 // Note: zip and json mimetypes are commonly used with a variety of
687 // extensions; don't add them here. It's a similar story for text/xml,
688 // but slightly worse because we can use it when sniffing for a mimetype
689 // if one hasn't been provided, so don't re-add that here either.
690 APPLICATION_PDF, APPLICATION_OGG, APPLICATION_WASM,
691 TEXT_CALENDAR, TEXT_CSS, TEXT_VCARD};
694 * Primary extensions of types whose descriptions should be overwritten.
695 * This extension is concatenated with "ExtHandlerDescription" to look up the
696 * description in unknownContentType.properties.
697 * NOTE: These MUST be lower-case and ASCII.
699 static const char* descriptionOverwriteExtensions[] = {
700 "avif", "jxl", "pdf", "svg", "webp", "xml",
703 static StaticRefPtr<nsExternalHelperAppService> sExtHelperAppSvcSingleton;
706 * On Mac child processes, return an nsOSHelperAppServiceChild for remoting
707 * OS calls to the parent process. On all other platforms use
708 * nsOSHelperAppService.
710 /* static */
711 already_AddRefed<nsExternalHelperAppService>
712 nsExternalHelperAppService::GetSingleton() {
713 if (!sExtHelperAppSvcSingleton) {
714 #if defined(XP_MACOSX) || defined(XP_WIN)
715 if (XRE_IsParentProcess()) {
716 sExtHelperAppSvcSingleton = new nsOSHelperAppService();
717 } else {
718 sExtHelperAppSvcSingleton = new nsOSHelperAppServiceChild();
720 #else
721 sExtHelperAppSvcSingleton = new nsOSHelperAppService();
722 #endif // defined(XP_MACOSX) || defined(XP_WIN)
723 ClearOnShutdown(&sExtHelperAppSvcSingleton);
726 return do_AddRef(sExtHelperAppSvcSingleton);
729 NS_IMPL_ISUPPORTS(nsExternalHelperAppService, nsIExternalHelperAppService,
730 nsPIExternalAppLauncher, nsIExternalProtocolService,
731 nsIMIMEService, nsIObserver, nsISupportsWeakReference)
733 nsExternalHelperAppService::nsExternalHelperAppService() {}
734 nsresult nsExternalHelperAppService::Init() {
735 // Add an observer for profile change
736 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
737 if (!obs) return NS_ERROR_FAILURE;
739 nsresult rv = obs->AddObserver(this, "profile-before-change", true);
740 NS_ENSURE_SUCCESS(rv, rv);
741 return obs->AddObserver(this, "last-pb-context-exited", true);
744 nsExternalHelperAppService::~nsExternalHelperAppService() {}
746 nsresult nsExternalHelperAppService::DoContentContentProcessHelper(
747 const nsACString& aMimeContentType, nsIRequest* aRequest,
748 BrowsingContext* aContentContext, bool aForceSave,
749 nsIInterfaceRequestor* aWindowContext,
750 nsIStreamListener** aStreamListener) {
751 // We need to get a hold of a ContentChild so that we can begin forwarding
752 // this data to the parent. In the HTTP case, this is unfortunate, since
753 // we're actually passing data from parent->child->parent wastefully, but
754 // the Right Fix will eventually be to short-circuit those channels on the
755 // parent side based on some sort of subscription concept.
756 using mozilla::dom::ContentChild;
757 using mozilla::dom::ExternalHelperAppChild;
758 ContentChild* child = ContentChild::GetSingleton();
759 if (!child) {
760 return NS_ERROR_FAILURE;
763 nsCString disp;
764 nsCOMPtr<nsIURI> uri;
765 int64_t contentLength = -1;
766 bool wasFileChannel = false;
767 uint32_t contentDisposition = -1;
768 nsAutoString fileName;
769 nsCOMPtr<nsILoadInfo> loadInfo;
771 nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
772 if (channel) {
773 channel->GetURI(getter_AddRefs(uri));
774 channel->GetContentLength(&contentLength);
775 channel->GetContentDisposition(&contentDisposition);
776 channel->GetContentDispositionFilename(fileName);
777 channel->GetContentDispositionHeader(disp);
778 loadInfo = channel->LoadInfo();
780 nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aRequest));
781 wasFileChannel = fileChan != nullptr;
784 nsCOMPtr<nsIURI> referrer;
785 NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer));
787 Maybe<mozilla::net::LoadInfoArgs> loadInfoArgs;
788 MOZ_ALWAYS_SUCCEEDS(LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs));
790 nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aRequest));
791 // Determine whether a new window was opened specifically for this request
792 bool shouldCloseWindow = false;
793 if (props) {
794 props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns,
795 &shouldCloseWindow);
798 // Now we build a protocol for forwarding our data to the parent. The
799 // protocol will act as a listener on the child-side and create a "real"
800 // helperAppService listener on the parent-side, via another call to
801 // DoContent.
802 RefPtr<ExternalHelperAppChild> childListener = new ExternalHelperAppChild();
803 MOZ_ALWAYS_TRUE(child->SendPExternalHelperAppConstructor(
804 childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp,
805 contentDisposition, fileName, aForceSave, contentLength, wasFileChannel,
806 referrer, aContentContext, shouldCloseWindow));
808 NS_ADDREF(*aStreamListener = childListener);
810 uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
812 RefPtr<nsExternalAppHandler> handler =
813 new nsExternalAppHandler(nullptr, ""_ns, aContentContext, aWindowContext,
814 this, fileName, reason, aForceSave);
815 if (!handler) {
816 return NS_ERROR_OUT_OF_MEMORY;
819 childListener->SetHandler(handler);
820 return NS_OK;
823 NS_IMETHODIMP nsExternalHelperAppService::CreateListener(
824 const nsACString& aMimeContentType, nsIRequest* aRequest,
825 BrowsingContext* aContentContext, bool aForceSave,
826 nsIInterfaceRequestor* aWindowContext,
827 nsIStreamListener** aStreamListener) {
828 MOZ_ASSERT(!XRE_IsContentProcess());
830 nsAutoString fileName;
831 nsAutoCString fileExtension;
832 uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
833 uint32_t contentDisposition = -1;
835 // Get the file extension and name that we will need later
836 nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
837 nsCOMPtr<nsIURI> uri;
838 int64_t contentLength = -1;
839 if (channel) {
840 channel->GetURI(getter_AddRefs(uri));
841 channel->GetContentLength(&contentLength);
842 channel->GetContentDisposition(&contentDisposition);
843 channel->GetContentDispositionFilename(fileName);
845 // Check if we have a POST request, in which case we don't want to use
846 // the url's extension
847 bool allowURLExt = !net::ChannelIsPost(channel);
849 // Check if we had a query string - we don't want to check the URL
850 // extension if a query is present in the URI
851 // If we already know we don't want to check the URL extension, don't
852 // bother checking the query
853 if (uri && allowURLExt) {
854 nsCOMPtr<nsIURL> url = do_QueryInterface(uri);
856 if (url) {
857 nsAutoCString query;
859 // We only care about the query for HTTP and HTTPS URLs
860 if (uri->SchemeIs("http") || uri->SchemeIs("https")) {
861 url->GetQuery(query);
864 // Only get the extension if the query is empty; if it isn't, then the
865 // extension likely belongs to a cgi script and isn't helpful
866 allowURLExt = query.IsEmpty();
869 // Extract name & extension
870 bool isAttachment = GetFilenameAndExtensionFromChannel(
871 channel, fileName, fileExtension, allowURLExt);
872 LOG(("Found extension '%s' (filename is '%s', handling attachment: %i)",
873 fileExtension.get(), NS_ConvertUTF16toUTF8(fileName).get(),
874 isAttachment));
875 if (isAttachment) {
876 reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST;
880 LOG(("HelperAppService::DoContent: mime '%s', extension '%s'\n",
881 PromiseFlatCString(aMimeContentType).get(), fileExtension.get()));
883 // We get the mime service here even though we're the default implementation
884 // of it, so it's possible to override only the mime service and not need to
885 // reimplement the whole external helper app service itself.
886 nsCOMPtr<nsIMIMEService> mimeSvc(do_GetService(NS_MIMESERVICE_CONTRACTID));
887 NS_ENSURE_TRUE(mimeSvc, NS_ERROR_FAILURE);
889 // Try to find a mime object by looking at the mime type/extension
890 nsCOMPtr<nsIMIMEInfo> mimeInfo;
891 if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT,
892 nsCaseInsensitiveCStringComparator)) {
893 nsAutoCString mimeType;
894 if (!fileExtension.IsEmpty()) {
895 mimeSvc->GetFromTypeAndExtension(""_ns, fileExtension,
896 getter_AddRefs(mimeInfo));
897 if (mimeInfo) {
898 mimeInfo->GetMIMEType(mimeType);
900 LOG(("OS-Provided mime type '%s' for extension '%s'\n", mimeType.get(),
901 fileExtension.get()));
905 if (fileExtension.IsEmpty() || mimeType.IsEmpty()) {
906 // Extension lookup gave us no useful match
907 mimeSvc->GetFromTypeAndExtension(
908 nsLiteralCString(APPLICATION_OCTET_STREAM), fileExtension,
909 getter_AddRefs(mimeInfo));
910 mimeType.AssignLiteral(APPLICATION_OCTET_STREAM);
913 if (channel) {
914 channel->SetContentType(mimeType);
917 // Don't overwrite SERVERREQUEST
918 if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
919 reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
921 } else {
922 mimeSvc->GetFromTypeAndExtension(aMimeContentType, fileExtension,
923 getter_AddRefs(mimeInfo));
925 LOG(("Type/Ext lookup found 0x%p\n", mimeInfo.get()));
927 // No mimeinfo -> we can't continue. probably OOM.
928 if (!mimeInfo) {
929 return NS_ERROR_OUT_OF_MEMORY;
932 *aStreamListener = nullptr;
933 // We want the mimeInfo's primary extension to pass it to
934 // nsExternalAppHandler
935 nsAutoCString buf;
936 mimeInfo->GetPrimaryExtension(buf);
938 // NB: ExternalHelperAppParent depends on this listener always being an
939 // nsExternalAppHandler. If this changes, make sure to update that code.
940 nsExternalAppHandler* handler =
941 new nsExternalAppHandler(mimeInfo, buf, aContentContext, aWindowContext,
942 this, fileName, reason, aForceSave);
943 if (!handler) {
944 return NS_ERROR_OUT_OF_MEMORY;
947 NS_ADDREF(*aStreamListener = handler);
948 return NS_OK;
951 NS_IMETHODIMP nsExternalHelperAppService::DoContent(
952 const nsACString& aMimeContentType, nsIRequest* aRequest,
953 nsIInterfaceRequestor* aContentContext, bool aForceSave,
954 nsIInterfaceRequestor* aWindowContext,
955 nsIStreamListener** aStreamListener) {
956 // Scripted interface requestors cannot return an instance of the
957 // (non-scriptable) nsPIDOMWindowOuter or nsPIDOMWindowInner interfaces, so
958 // get to the window via `nsIDOMWindow`. Unfortunately, at that point we
959 // don't know whether the thing we got is an inner or outer window, so have to
960 // work with either one.
961 RefPtr<BrowsingContext> bc;
962 nsCOMPtr<nsIDOMWindow> domWindow = do_GetInterface(aContentContext);
963 if (nsCOMPtr<nsPIDOMWindowOuter> outerWindow = do_QueryInterface(domWindow)) {
964 bc = outerWindow->GetBrowsingContext();
965 } else if (nsCOMPtr<nsPIDOMWindowInner> innerWindow =
966 do_QueryInterface(domWindow)) {
967 bc = innerWindow->GetBrowsingContext();
970 if (XRE_IsContentProcess()) {
971 return DoContentContentProcessHelper(aMimeContentType, aRequest, bc,
972 aForceSave, aWindowContext,
973 aStreamListener);
976 nsresult rv = CreateListener(aMimeContentType, aRequest, bc, aForceSave,
977 aWindowContext, aStreamListener);
978 return rv;
981 NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension(
982 const nsACString& aExtension, const nsACString& aEncodingType,
983 bool* aApplyDecoding) {
984 *aApplyDecoding = true;
985 uint32_t i;
986 for (i = 0; i < ArrayLength(nonDecodableExtensions); ++i) {
987 if (aExtension.LowerCaseEqualsASCII(
988 nonDecodableExtensions[i].mFileExtension) &&
989 aEncodingType.LowerCaseEqualsASCII(
990 nonDecodableExtensions[i].mMimeType)) {
991 *aApplyDecoding = false;
992 break;
995 return NS_OK;
998 nsresult nsExternalHelperAppService::GetFileTokenForPath(
999 const char16_t* aPlatformAppPath, nsIFile** aFile) {
1000 nsDependentString platformAppPath(aPlatformAppPath);
1001 // First, check if we have an absolute path
1002 nsIFile* localFile = nullptr;
1003 nsresult rv = NS_NewLocalFile(platformAppPath, true, &localFile);
1004 if (NS_SUCCEEDED(rv)) {
1005 *aFile = localFile;
1006 bool exists;
1007 if (NS_FAILED((*aFile)->Exists(&exists)) || !exists) {
1008 NS_RELEASE(*aFile);
1009 return NS_ERROR_FILE_NOT_FOUND;
1011 return NS_OK;
1014 // Second, check if file exists in mozilla program directory
1015 rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, aFile);
1016 if (NS_SUCCEEDED(rv)) {
1017 rv = (*aFile)->Append(platformAppPath);
1018 if (NS_SUCCEEDED(rv)) {
1019 bool exists = false;
1020 rv = (*aFile)->Exists(&exists);
1021 if (NS_SUCCEEDED(rv) && exists) return NS_OK;
1023 NS_RELEASE(*aFile);
1026 return NS_ERROR_NOT_AVAILABLE;
1029 //////////////////////////////////////////////////////////////////////////////////////////////////////
1030 // begin external protocol service default implementation...
1031 //////////////////////////////////////////////////////////////////////////////////////////////////////
1032 NS_IMETHODIMP nsExternalHelperAppService::ExternalProtocolHandlerExists(
1033 const char* aProtocolScheme, bool* aHandlerExists) {
1034 nsCOMPtr<nsIHandlerInfo> handlerInfo;
1035 nsresult rv = GetProtocolHandlerInfo(nsDependentCString(aProtocolScheme),
1036 getter_AddRefs(handlerInfo));
1037 if (NS_SUCCEEDED(rv)) {
1038 // See if we have any known possible handler apps for this
1039 nsCOMPtr<nsIMutableArray> possibleHandlers;
1040 handlerInfo->GetPossibleApplicationHandlers(
1041 getter_AddRefs(possibleHandlers));
1043 uint32_t length;
1044 possibleHandlers->GetLength(&length);
1045 if (length) {
1046 *aHandlerExists = true;
1047 return NS_OK;
1051 // if not, fall back on an os-based handler
1052 return OSProtocolHandlerExists(aProtocolScheme, aHandlerExists);
1055 NS_IMETHODIMP nsExternalHelperAppService::IsExposedProtocol(
1056 const char* aProtocolScheme, bool* aResult) {
1057 // check the per protocol setting first. it always takes precedence.
1058 // if not set, then use the global setting.
1060 nsAutoCString prefName("network.protocol-handler.expose.");
1061 prefName += aProtocolScheme;
1062 bool val;
1063 if (NS_SUCCEEDED(Preferences::GetBool(prefName.get(), &val))) {
1064 *aResult = val;
1065 return NS_OK;
1068 // by default, no protocol is exposed. i.e., by default all link clicks must
1069 // go through the external protocol service. most applications override this
1070 // default behavior.
1071 *aResult = Preferences::GetBool("network.protocol-handler.expose-all", false);
1073 return NS_OK;
1076 static const char kExternalProtocolPrefPrefix[] =
1077 "network.protocol-handler.external.";
1078 static const char kExternalProtocolDefaultPref[] =
1079 "network.protocol-handler.external-default";
1081 // static
1082 nsresult nsExternalHelperAppService::EscapeURI(nsIURI* aURI, nsIURI** aResult) {
1083 MOZ_ASSERT(aURI);
1084 MOZ_ASSERT(aResult);
1086 nsAutoCString spec;
1087 aURI->GetSpec(spec);
1089 if (spec.Find("%00") != -1) return NS_ERROR_MALFORMED_URI;
1091 nsAutoCString escapedSpec;
1092 nsresult rv = NS_EscapeURL(spec, esc_AlwaysCopy | esc_ExtHandler, escapedSpec,
1093 fallible);
1094 NS_ENSURE_SUCCESS(rv, rv);
1096 nsCOMPtr<nsIIOService> ios(do_GetIOService());
1097 return ios->NewURI(escapedSpec, nullptr, nullptr, aResult);
1100 bool ExternalProtocolIsBlockedBySandbox(
1101 BrowsingContext* aBrowsingContext,
1102 const bool aHasValidUserGestureActivation) {
1103 if (!StaticPrefs::dom_block_external_protocol_navigation_from_sandbox()) {
1104 return false;
1107 if (!aBrowsingContext || aBrowsingContext->IsTop()) {
1108 return false;
1111 uint32_t sandboxFlags = aBrowsingContext->GetSandboxFlags();
1113 if (sandboxFlags == SANDBOXED_NONE) {
1114 return false;
1117 if (!(sandboxFlags & SANDBOXED_AUXILIARY_NAVIGATION)) {
1118 return false;
1121 if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION)) {
1122 return false;
1125 if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_CUSTOM_PROTOCOLS)) {
1126 return false;
1129 if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION) &&
1130 aHasValidUserGestureActivation) {
1131 return false;
1134 return true;
1137 NS_IMETHODIMP
1138 nsExternalHelperAppService::LoadURI(nsIURI* aURI,
1139 nsIPrincipal* aTriggeringPrincipal,
1140 nsIPrincipal* aRedirectPrincipal,
1141 BrowsingContext* aBrowsingContext,
1142 bool aTriggeredExternally,
1143 bool aHasValidUserGestureActivation) {
1144 NS_ENSURE_ARG_POINTER(aURI);
1146 if (XRE_IsContentProcess()) {
1147 mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal(
1148 aURI, aTriggeringPrincipal, aRedirectPrincipal, aBrowsingContext,
1149 aTriggeredExternally, aHasValidUserGestureActivation);
1150 return NS_OK;
1153 // Prevent sandboxed BrowsingContexts from navigating to external protocols.
1154 // This only uses the sandbox flags of the target BrowsingContext of the
1155 // load. The navigating document's CSP sandbox flags do not apply.
1156 if (aBrowsingContext &&
1157 ExternalProtocolIsBlockedBySandbox(aBrowsingContext,
1158 aHasValidUserGestureActivation)) {
1159 // Log an error to the web console of the sandboxed BrowsingContext.
1160 nsAutoString localizedMsg;
1161 nsAutoCString spec;
1162 aURI->GetSpec(spec);
1164 AutoTArray<nsString, 1> params = {NS_ConvertUTF8toUTF16(spec)};
1165 nsresult rv = nsContentUtils::FormatLocalizedString(
1166 nsContentUtils::eSECURITY_PROPERTIES, "SandboxBlockedCustomProtocols",
1167 params, localizedMsg);
1168 NS_ENSURE_SUCCESS(rv, rv);
1170 // Log to the the parent window of the iframe. If there is no parent, fall
1171 // back to the iframe window itself.
1172 WindowContext* windowContext = aBrowsingContext->GetParentWindowContext();
1173 if (!windowContext) {
1174 windowContext = aBrowsingContext->GetCurrentWindowContext();
1177 // Skip logging if we still don't have a WindowContext.
1178 NS_ENSURE_TRUE(windowContext, NS_ERROR_FAILURE);
1180 nsContentUtils::ReportToConsoleByWindowID(
1181 localizedMsg, nsIScriptError::errorFlag, "Security"_ns,
1182 windowContext->InnerWindowId(),
1183 windowContext->Canonical()->GetDocumentURI());
1185 return NS_OK;
1188 nsCOMPtr<nsIURI> escapedURI;
1189 nsresult rv = EscapeURI(aURI, getter_AddRefs(escapedURI));
1190 NS_ENSURE_SUCCESS(rv, rv);
1192 nsAutoCString scheme;
1193 escapedURI->GetScheme(scheme);
1194 if (scheme.IsEmpty()) return NS_OK; // must have a scheme
1196 // Deny load if the prefs say to do so
1197 nsAutoCString externalPref(kExternalProtocolPrefPrefix);
1198 externalPref += scheme;
1199 bool allowLoad = false;
1200 if (NS_FAILED(Preferences::GetBool(externalPref.get(), &allowLoad))) {
1201 // no scheme-specific value, check the default
1202 if (NS_FAILED(
1203 Preferences::GetBool(kExternalProtocolDefaultPref, &allowLoad))) {
1204 return NS_OK; // missing default pref
1208 if (!allowLoad) {
1209 return NS_OK; // explicitly denied
1212 // Now check if the principal is allowed to access the navigated context.
1213 // We allow navigating subframes, even if not same-origin - non-external
1214 // links can always navigate everywhere, so this is a minor additional
1215 // restriction, only aiming to prevent some types of spoofing attacks
1216 // from otherwise disjoint browsingcontext trees.
1217 if (aBrowsingContext && aTriggeringPrincipal &&
1218 !StaticPrefs::security_allow_disjointed_external_uri_loads() &&
1219 // Add-on principals are always allowed:
1220 !BasePrincipal::Cast(aTriggeringPrincipal)->AddonPolicy() &&
1221 // As is chrome code:
1222 !aTriggeringPrincipal->IsSystemPrincipal()) {
1223 RefPtr<BrowsingContext> bc = aBrowsingContext;
1224 WindowGlobalParent* wgp = bc->Canonical()->GetCurrentWindowGlobal();
1225 bool foundAccessibleFrame = false;
1227 // Also allow this load if the target is a toplevel BC and contains a
1228 // non-web-controlled about:blank document
1229 if (bc->IsTop() && !bc->HadOriginalOpener() && wgp) {
1230 RefPtr<nsIURI> uri = wgp->GetDocumentURI();
1231 foundAccessibleFrame =
1232 uri && uri->GetSpecOrDefault().EqualsLiteral("about:blank");
1235 while (!foundAccessibleFrame) {
1236 if (wgp) {
1237 foundAccessibleFrame =
1238 aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal());
1240 // We have to get the parent via the bc, because there may not
1241 // be a window global for the innermost bc; see bug 1650162.
1242 BrowsingContext* parent = bc->GetParent();
1243 if (!parent) {
1244 break;
1246 bc = parent;
1247 wgp = parent->Canonical()->GetCurrentWindowGlobal();
1250 if (!foundAccessibleFrame) {
1251 // See if this navigation could have come from a subframe.
1252 nsTArray<RefPtr<BrowsingContext>> contexts;
1253 aBrowsingContext->GetAllBrowsingContextsInSubtree(contexts);
1254 for (const auto& kid : contexts) {
1255 wgp = kid->Canonical()->GetCurrentWindowGlobal();
1256 if (wgp && aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal())) {
1257 foundAccessibleFrame = true;
1258 break;
1263 if (!foundAccessibleFrame) {
1264 return NS_OK; // deny the load.
1268 nsCOMPtr<nsIHandlerInfo> handler;
1269 rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler));
1270 NS_ENSURE_SUCCESS(rv, rv);
1272 nsCOMPtr<nsIContentDispatchChooser> chooser =
1273 do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv);
1274 NS_ENSURE_SUCCESS(rv, rv);
1276 return chooser->HandleURI(
1277 handler, escapedURI,
1278 aRedirectPrincipal ? aRedirectPrincipal : aTriggeringPrincipal,
1279 aBrowsingContext, aTriggeredExternally);
1282 //////////////////////////////////////////////////////////////////////////////////////////////////////
1283 // Methods related to deleting temporary files on exit
1284 //////////////////////////////////////////////////////////////////////////////////////////////////////
1286 /* static */
1287 nsresult nsExternalHelperAppService::DeleteTemporaryFileHelper(
1288 nsIFile* aTemporaryFile, nsCOMArray<nsIFile>& aFileList) {
1289 bool isFile = false;
1291 // as a safety measure, make sure the nsIFile is really a file and not a
1292 // directory object.
1293 aTemporaryFile->IsFile(&isFile);
1294 if (!isFile) return NS_OK;
1296 aFileList.AppendObject(aTemporaryFile);
1298 return NS_OK;
1301 NS_IMETHODIMP
1302 nsExternalHelperAppService::DeleteTemporaryFileOnExit(nsIFile* aTemporaryFile) {
1303 return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryFilesList);
1306 NS_IMETHODIMP
1307 nsExternalHelperAppService::DeleteTemporaryPrivateFileWhenPossible(
1308 nsIFile* aTemporaryFile) {
1309 return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryPrivateFilesList);
1312 void nsExternalHelperAppService::ExpungeTemporaryFilesHelper(
1313 nsCOMArray<nsIFile>& fileList) {
1314 int32_t numEntries = fileList.Count();
1315 nsIFile* localFile;
1316 for (int32_t index = 0; index < numEntries; index++) {
1317 localFile = fileList[index];
1318 if (localFile) {
1319 // First make the file writable, since the temp file is probably readonly.
1320 localFile->SetPermissions(0600);
1321 localFile->Remove(false);
1325 fileList.Clear();
1328 void nsExternalHelperAppService::ExpungeTemporaryFiles() {
1329 ExpungeTemporaryFilesHelper(mTemporaryFilesList);
1332 void nsExternalHelperAppService::ExpungeTemporaryPrivateFiles() {
1333 ExpungeTemporaryFilesHelper(mTemporaryPrivateFilesList);
1336 static const char kExternalWarningPrefPrefix[] =
1337 "network.protocol-handler.warn-external.";
1338 static const char kExternalWarningDefaultPref[] =
1339 "network.protocol-handler.warn-external-default";
1341 NS_IMETHODIMP
1342 nsExternalHelperAppService::GetProtocolHandlerInfo(
1343 const nsACString& aScheme, nsIHandlerInfo** aHandlerInfo) {
1344 // XXX enterprise customers should be able to turn this support off with a
1345 // single master pref (maybe use one of the "exposed" prefs here?)
1347 bool exists;
1348 nsresult rv = GetProtocolHandlerInfoFromOS(aScheme, &exists, aHandlerInfo);
1349 if (NS_FAILED(rv)) {
1350 // Either it knows nothing, or we ran out of memory
1351 return NS_ERROR_FAILURE;
1354 nsCOMPtr<nsIHandlerService> handlerSvc =
1355 do_GetService(NS_HANDLERSERVICE_CONTRACTID);
1356 if (handlerSvc) {
1357 bool hasHandler = false;
1358 (void)handlerSvc->Exists(*aHandlerInfo, &hasHandler);
1359 if (hasHandler) {
1360 rv = handlerSvc->FillHandlerInfo(*aHandlerInfo, ""_ns);
1361 if (NS_SUCCEEDED(rv)) return NS_OK;
1365 return SetProtocolHandlerDefaults(*aHandlerInfo, exists);
1368 NS_IMETHODIMP
1369 nsExternalHelperAppService::SetProtocolHandlerDefaults(
1370 nsIHandlerInfo* aHandlerInfo, bool aOSHandlerExists) {
1371 // this type isn't in our database, so we've only got an OS default handler,
1372 // if one exists
1374 if (aOSHandlerExists) {
1375 // we've got a default, so use it
1376 aHandlerInfo->SetPreferredAction(nsIHandlerInfo::useSystemDefault);
1378 // whether or not to ask the user depends on the warning preference
1379 nsAutoCString scheme;
1380 aHandlerInfo->GetType(scheme);
1382 nsAutoCString warningPref(kExternalWarningPrefPrefix);
1383 warningPref += scheme;
1384 bool warn;
1385 if (NS_FAILED(Preferences::GetBool(warningPref.get(), &warn))) {
1386 // no scheme-specific value, check the default
1387 warn = Preferences::GetBool(kExternalWarningDefaultPref, true);
1389 aHandlerInfo->SetAlwaysAskBeforeHandling(warn);
1390 } else {
1391 // If no OS default existed, we set the preferred action to alwaysAsk.
1392 // This really means not initialized (i.e. there's no available handler)
1393 // to all the code...
1394 aHandlerInfo->SetPreferredAction(nsIHandlerInfo::alwaysAsk);
1397 return NS_OK;
1400 // XPCOM profile change observer
1401 NS_IMETHODIMP
1402 nsExternalHelperAppService::Observe(nsISupports* aSubject, const char* aTopic,
1403 const char16_t* someData) {
1404 if (!strcmp(aTopic, "profile-before-change")) {
1405 ExpungeTemporaryFiles();
1406 } else if (!strcmp(aTopic, "last-pb-context-exited")) {
1407 ExpungeTemporaryPrivateFiles();
1409 return NS_OK;
1412 //////////////////////////////////////////////////////////////////////////////////////////////////////
1413 // begin external app handler implementation
1414 //////////////////////////////////////////////////////////////////////////////////////////////////////
1416 NS_IMPL_ADDREF(nsExternalAppHandler)
1417 NS_IMPL_RELEASE(nsExternalAppHandler)
1419 NS_INTERFACE_MAP_BEGIN(nsExternalAppHandler)
1420 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener)
1421 NS_INTERFACE_MAP_ENTRY(nsIStreamListener)
1422 NS_INTERFACE_MAP_ENTRY(nsIRequestObserver)
1423 NS_INTERFACE_MAP_ENTRY(nsIHelperAppLauncher)
1424 NS_INTERFACE_MAP_ENTRY(nsICancelable)
1425 NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver)
1426 NS_INTERFACE_MAP_ENTRY(nsINamed)
1427 NS_INTERFACE_MAP_ENTRY_CONCRETE(nsExternalAppHandler)
1428 NS_INTERFACE_MAP_END
1430 nsExternalAppHandler::nsExternalAppHandler(
1431 nsIMIMEInfo* aMIMEInfo, const nsACString& aTempFileExtension,
1432 BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext,
1433 nsExternalHelperAppService* aExtProtSvc,
1434 const nsAString& aSuggestedFilename, uint32_t aReason, bool aForceSave)
1435 : mMimeInfo(aMIMEInfo),
1436 mBrowsingContext(aBrowsingContext),
1437 mWindowContext(aWindowContext),
1438 mSuggestedFileName(aSuggestedFilename),
1439 mForceSave(aForceSave),
1440 mCanceled(false),
1441 mStopRequestIssued(false),
1442 mIsFileChannel(false),
1443 mShouldCloseWindow(false),
1444 mHandleInternally(false),
1445 mReason(aReason),
1446 mTempFileIsExecutable(false),
1447 mTimeDownloadStarted(0),
1448 mContentLength(-1),
1449 mProgress(0),
1450 mSaver(nullptr),
1451 mDialogProgressListener(nullptr),
1452 mTransfer(nullptr),
1453 mRequest(nullptr),
1454 mExtProtSvc(aExtProtSvc) {
1455 // make sure the extention includes the '.'
1456 if (!aTempFileExtension.IsEmpty() && aTempFileExtension.First() != '.')
1457 mTempFileExtension = char16_t('.');
1458 AppendUTF8toUTF16(aTempFileExtension, mTempFileExtension);
1460 // Get mSuggestedFileName's current file extension.
1461 nsAutoString originalFileExt;
1462 int32_t pos = mSuggestedFileName.RFindChar('.');
1463 if (pos != kNotFound) {
1464 mSuggestedFileName.Right(originalFileExt,
1465 mSuggestedFileName.Length() - pos);
1468 // replace platform specific path separator and illegal characters to avoid
1469 // any confusion.
1470 // Try to keep the use of spaces or underscores in sync with the Downloads
1471 // code sanitization in DownloadPaths.jsm
1472 mSuggestedFileName.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
1473 mSuggestedFileName.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' ');
1474 mSuggestedFileName.ReplaceChar(char16_t(0), '_');
1475 mTempFileExtension.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
1476 mTempFileExtension.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' ');
1478 // Remove unsafe bidi characters which might have spoofing implications (bug
1479 // 511521).
1480 const char16_t unsafeBidiCharacters[] = {
1481 char16_t(0x061c), // Arabic Letter Mark
1482 char16_t(0x200e), // Left-to-Right Mark
1483 char16_t(0x200f), // Right-to-Left Mark
1484 char16_t(0x202a), // Left-to-Right Embedding
1485 char16_t(0x202b), // Right-to-Left Embedding
1486 char16_t(0x202c), // Pop Directional Formatting
1487 char16_t(0x202d), // Left-to-Right Override
1488 char16_t(0x202e), // Right-to-Left Override
1489 char16_t(0x2066), // Left-to-Right Isolate
1490 char16_t(0x2067), // Right-to-Left Isolate
1491 char16_t(0x2068), // First Strong Isolate
1492 char16_t(0x2069), // Pop Directional Isolate
1493 char16_t(0)};
1494 mSuggestedFileName.ReplaceChar(unsafeBidiCharacters, '_');
1495 mTempFileExtension.ReplaceChar(unsafeBidiCharacters, '_');
1497 // Remove trailing or leading spaces that we may have generated while
1498 // sanitizing.
1499 mSuggestedFileName.CompressWhitespace();
1500 mTempFileExtension.CompressWhitespace();
1502 EnsureCorrectExtension(originalFileExt);
1504 mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096);
1507 nsExternalAppHandler::~nsExternalAppHandler() {
1508 MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted");
1511 bool nsExternalAppHandler::ShouldForceExtension(const nsString& aFileExt) {
1512 nsAutoCString MIMEType;
1513 if (!mMimeInfo || NS_FAILED(mMimeInfo->GetMIMEType(MIMEType))) {
1514 return false;
1517 bool canForce = StringBeginsWith(MIMEType, "image/"_ns) ||
1518 StringBeginsWith(MIMEType, "audio/"_ns) ||
1519 StringBeginsWith(MIMEType, "video/"_ns);
1521 if (!canForce &&
1522 StaticPrefs::browser_download_sanitize_non_media_extensions()) {
1523 for (const char* mime : forcedExtensionMimetypes) {
1524 if (MIMEType.Equals(mime)) {
1525 canForce = true;
1526 break;
1530 if (!canForce) {
1531 return false;
1534 // If we get here, we know for sure the mimetype allows us to overwrite the
1535 // existing extension, if it's wrong. Return whether the extension is wrong:
1537 bool knownExtension = false;
1538 // Note that aFileExt is either empty or consists of an extension
1539 // *including the dot* which we remove for ExtensionExists().
1540 return (
1541 aFileExt.IsEmpty() || aFileExt.EqualsLiteral(".") ||
1542 (NS_SUCCEEDED(mMimeInfo->ExtensionExists(
1543 Substring(NS_ConvertUTF16toUTF8(aFileExt), 1), &knownExtension)) &&
1544 !knownExtension));
1547 void nsExternalAppHandler::EnsureCorrectExtension(const nsString& aFileExt) {
1548 // If we don't have an extension (which will include the .),
1549 // just short-circuit.
1550 if (mTempFileExtension.Length() <= 1) {
1551 return;
1554 // After removing trailing whitespaces from the name, if we have a
1555 // temp file extension, there are broadly 2 cases where we want to
1556 // replace the extension.
1557 // First, if the file extension contains invalid characters.
1558 // Second, for document type mimetypes, if the extension is either
1559 // missing or not valid for this mimetype.
1560 bool replaceExtension =
1561 (aFileExt.FindCharInSet(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS) !=
1562 kNotFound) ||
1563 ShouldForceExtension(aFileExt);
1565 if (replaceExtension) {
1566 int32_t pos = mSuggestedFileName.RFindChar('.');
1567 if (pos != kNotFound) {
1568 mSuggestedFileName =
1569 Substring(mSuggestedFileName, 0, pos) + mTempFileExtension;
1570 } else {
1571 mSuggestedFileName.Append(mTempFileExtension);
1576 * Ensure we don't double-append the file extension if it matches:
1578 if (replaceExtension ||
1579 aFileExt.Equals(mTempFileExtension, nsCaseInsensitiveStringComparator)) {
1580 // Matches -> mTempFileExtension can be empty
1581 mTempFileExtension.Truncate();
1585 void nsExternalAppHandler::DidDivertRequest(nsIRequest* request) {
1586 MOZ_ASSERT(XRE_IsContentProcess(), "in child process");
1587 // Remove our request from the child loadGroup
1588 RetargetLoadNotifications(request);
1591 NS_IMETHODIMP nsExternalAppHandler::SetWebProgressListener(
1592 nsIWebProgressListener2* aWebProgressListener) {
1593 // This is always called by nsHelperDlg.js. Go ahead and register the
1594 // progress listener. At this point, we don't have mTransfer.
1595 mDialogProgressListener = aWebProgressListener;
1596 return NS_OK;
1599 NS_IMETHODIMP nsExternalAppHandler::GetTargetFile(nsIFile** aTarget) {
1600 if (mFinalFileDestination)
1601 *aTarget = mFinalFileDestination;
1602 else
1603 *aTarget = mTempFile;
1605 NS_IF_ADDREF(*aTarget);
1606 return NS_OK;
1609 NS_IMETHODIMP nsExternalAppHandler::GetTargetFileIsExecutable(bool* aExec) {
1610 // Use the real target if it's been set
1611 if (mFinalFileDestination) return mFinalFileDestination->IsExecutable(aExec);
1613 // Otherwise, use the stored executable-ness of the temporary
1614 *aExec = mTempFileIsExecutable;
1615 return NS_OK;
1618 NS_IMETHODIMP nsExternalAppHandler::GetTimeDownloadStarted(PRTime* aTime) {
1619 *aTime = mTimeDownloadStarted;
1620 return NS_OK;
1623 NS_IMETHODIMP nsExternalAppHandler::GetContentLength(int64_t* aContentLength) {
1624 *aContentLength = mContentLength;
1625 return NS_OK;
1628 NS_IMETHODIMP nsExternalAppHandler::GetBrowsingContextId(
1629 uint64_t* aBrowsingContextId) {
1630 *aBrowsingContextId = mBrowsingContext ? mBrowsingContext->Id() : 0;
1631 return NS_OK;
1634 void nsExternalAppHandler::RetargetLoadNotifications(nsIRequest* request) {
1635 // we are going to run the downloading of the helper app in our own little
1636 // docloader / load group context. so go ahead and force the creation of a
1637 // load group and doc loader for us to use...
1638 nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
1639 if (!aChannel) return;
1641 bool isPrivate = NS_UsePrivateBrowsing(aChannel);
1643 nsCOMPtr<nsILoadGroup> oldLoadGroup;
1644 aChannel->GetLoadGroup(getter_AddRefs(oldLoadGroup));
1646 if (oldLoadGroup) {
1647 oldLoadGroup->RemoveRequest(request, nullptr, NS_BINDING_RETARGETED);
1650 aChannel->SetLoadGroup(nullptr);
1651 aChannel->SetNotificationCallbacks(nullptr);
1653 nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel);
1654 if (pbChannel) {
1655 pbChannel->SetPrivate(isPrivate);
1659 nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) {
1660 // First we need to try to get the destination directory for the temporary
1661 // file.
1662 nsresult rv = GetDownloadDirectory(getter_AddRefs(mTempFile));
1663 NS_ENSURE_SUCCESS(rv, rv);
1665 // At this point, we do not have a filename for the temp file. For security
1666 // purposes, this cannot be predictable, so we must use a cryptographic
1667 // quality PRNG to generate one.
1668 nsAutoCString tempLeafName;
1669 rv = GenerateRandomName(tempLeafName);
1670 NS_ENSURE_SUCCESS(rv, rv);
1672 // now append our extension.
1673 nsAutoCString ext;
1674 mMimeInfo->GetPrimaryExtension(ext);
1675 if (!ext.IsEmpty()) {
1676 ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
1677 if (ext.First() != '.') tempLeafName.Append('.');
1678 tempLeafName.Append(ext);
1681 // We need to temporarily create a dummy file with the correct
1682 // file extension to determine the executable-ness, so do this before adding
1683 // the extra .part extension.
1684 nsCOMPtr<nsIFile> dummyFile;
1685 rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dummyFile));
1686 NS_ENSURE_SUCCESS(rv, rv);
1688 // Set the file name without .part
1689 rv = dummyFile->Append(NS_ConvertUTF8toUTF16(tempLeafName));
1690 NS_ENSURE_SUCCESS(rv, rv);
1691 rv = dummyFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
1692 NS_ENSURE_SUCCESS(rv, rv);
1694 // Store executable-ness then delete
1695 dummyFile->IsExecutable(&mTempFileIsExecutable);
1696 dummyFile->Remove(false);
1698 // Add an additional .part to prevent the OS from running this file in the
1699 // default application.
1700 tempLeafName.AppendLiteral(".part");
1702 rv = mTempFile->Append(NS_ConvertUTF8toUTF16(tempLeafName));
1703 // make this file unique!!!
1704 NS_ENSURE_SUCCESS(rv, rv);
1705 rv = mTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
1706 NS_ENSURE_SUCCESS(rv, rv);
1708 // Now save the temp leaf name, minus the ".part" bit, so we can use it later.
1709 // This is a bit broken in the case when createUnique actually had to append
1710 // some numbers, because then we now have a filename like foo.bar-1.part and
1711 // we'll end up with foo.bar-1.bar as our final filename if we end up using
1712 // this. But the other options are all bad too.... Ideally we'd have a way
1713 // to tell createUnique to put its unique marker before the extension that
1714 // comes before ".part" or something.
1715 rv = mTempFile->GetLeafName(mTempLeafName);
1716 NS_ENSURE_SUCCESS(rv, rv);
1718 NS_ENSURE_TRUE(StringEndsWith(mTempLeafName, u".part"_ns),
1719 NS_ERROR_UNEXPECTED);
1721 // Strip off the ".part" from mTempLeafName
1722 mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1);
1724 MOZ_ASSERT(!mSaver, "Output file initialization called more than once!");
1725 mSaver =
1726 do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv);
1727 NS_ENSURE_SUCCESS(rv, rv);
1729 rv = mSaver->SetObserver(this);
1730 if (NS_FAILED(rv)) {
1731 mSaver = nullptr;
1732 return rv;
1735 rv = mSaver->EnableSha256();
1736 NS_ENSURE_SUCCESS(rv, rv);
1738 rv = mSaver->EnableSignatureInfo();
1739 NS_ENSURE_SUCCESS(rv, rv);
1740 LOG(("Enabled hashing and signature verification"));
1742 rv = mSaver->SetTarget(mTempFile, false);
1743 NS_ENSURE_SUCCESS(rv, rv);
1745 return rv;
1748 void nsExternalAppHandler::MaybeApplyDecodingForExtension(
1749 nsIRequest* aRequest) {
1750 MOZ_ASSERT(aRequest);
1752 nsCOMPtr<nsIEncodedChannel> encChannel = do_QueryInterface(aRequest);
1753 if (!encChannel) {
1754 return;
1757 // Turn off content encoding conversions if needed
1758 bool applyConversion = true;
1760 // First, check to see if conversion is already disabled. If so, we
1761 // have nothing to do here.
1762 encChannel->GetApplyConversion(&applyConversion);
1763 if (!applyConversion) {
1764 return;
1767 nsCOMPtr<nsIURL> sourceURL(do_QueryInterface(mSourceUrl));
1768 if (sourceURL) {
1769 nsAutoCString extension;
1770 sourceURL->GetFileExtension(extension);
1771 if (!extension.IsEmpty()) {
1772 nsCOMPtr<nsIUTF8StringEnumerator> encEnum;
1773 encChannel->GetContentEncodings(getter_AddRefs(encEnum));
1774 if (encEnum) {
1775 bool hasMore;
1776 nsresult rv = encEnum->HasMore(&hasMore);
1777 if (NS_SUCCEEDED(rv) && hasMore) {
1778 nsAutoCString encType;
1779 rv = encEnum->GetNext(encType);
1780 if (NS_SUCCEEDED(rv) && !encType.IsEmpty()) {
1781 MOZ_ASSERT(mExtProtSvc);
1782 mExtProtSvc->ApplyDecodingForExtension(extension, encType,
1783 &applyConversion);
1790 encChannel->SetApplyConversion(applyConversion);
1793 already_AddRefed<nsIInterfaceRequestor>
1794 nsExternalAppHandler::GetDialogParent() {
1795 nsCOMPtr<nsIInterfaceRequestor> dialogParent = mWindowContext;
1797 if (!dialogParent && mBrowsingContext) {
1798 dialogParent = do_QueryInterface(mBrowsingContext->GetDOMWindow());
1800 if (!dialogParent && mBrowsingContext && XRE_IsParentProcess()) {
1801 RefPtr<Element> element = mBrowsingContext->Top()->GetEmbedderElement();
1802 if (element) {
1803 dialogParent = do_QueryInterface(element->OwnerDoc()->GetWindow());
1806 return dialogParent.forget();
1809 NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
1810 MOZ_ASSERT(request, "OnStartRequest without request?");
1812 // Set mTimeDownloadStarted here as the download has already started and
1813 // we want to record the start time before showing the filepicker.
1814 mTimeDownloadStarted = PR_Now();
1816 mRequest = request;
1818 nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
1820 nsresult rv;
1821 nsAutoCString MIMEType;
1822 if (mMimeInfo) {
1823 mMimeInfo->GetMIMEType(MIMEType);
1825 // Now get the URI
1826 if (aChannel) {
1827 aChannel->GetURI(getter_AddRefs(mSourceUrl));
1828 // HTTPS-Only/HTTPS-FirstMode tries to upgrade connections to https. Once
1829 // the download is in progress we set that flag so that timeout counter
1830 // measures do not kick in.
1831 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1832 bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
1833 if (nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(isPrivateWin) ||
1834 nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(isPrivateWin)) {
1835 uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
1836 httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_DOWNLOAD_IN_PROGRESS;
1837 loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
1841 if (!mForceSave && StaticPrefs::browser_download_enable_spam_prevention() &&
1842 IsDownloadSpam(aChannel)) {
1843 RecordDownloadTelemetry(aChannel, "spam");
1844 return NS_OK;
1847 mDownloadClassification =
1848 nsContentSecurityUtils::ClassifyDownload(aChannel, MIMEType);
1850 if (mDownloadClassification == nsITransfer::DOWNLOAD_FORBIDDEN) {
1851 // If the download is rated as forbidden,
1852 // cancel the request so no ui knows about this.
1853 mCanceled = true;
1854 request->Cancel(NS_ERROR_ABORT);
1855 RecordDownloadTelemetry(aChannel, "forbidden");
1856 return NS_OK;
1859 nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(request));
1860 mIsFileChannel = fileChan != nullptr;
1861 if (!mIsFileChannel) {
1862 // It's possible that this request came from the child process and the
1863 // file channel actually lives there. If this returns true, then our
1864 // mSourceUrl will be an nsIFileURL anyway.
1865 nsCOMPtr<dom::nsIExternalHelperAppParent> parent(
1866 do_QueryInterface(request));
1867 mIsFileChannel = parent && parent->WasFileChannel();
1870 // Get content length
1871 if (aChannel) {
1872 aChannel->GetContentLength(&mContentLength);
1875 if (mBrowsingContext) {
1876 mMaybeCloseWindowHelper = new MaybeCloseWindowHelper(mBrowsingContext);
1877 mMaybeCloseWindowHelper->SetShouldCloseWindow(mShouldCloseWindow);
1878 nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(request, &rv));
1879 // Determine whether a new window was opened specifically for this request
1880 if (props) {
1881 bool tmp = false;
1882 if (NS_SUCCEEDED(
1883 props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns, &tmp))) {
1884 mMaybeCloseWindowHelper->SetShouldCloseWindow(tmp);
1889 // retarget all load notifications to our docloader instead of the original
1890 // window's docloader...
1891 RetargetLoadNotifications(request);
1893 // Close the underlying DOMWindow if it was opened specifically for the
1894 // download. We don't run this in the content process, since we have
1895 // an instance running in the parent as well, which will handle this
1896 // if needed.
1897 if (!XRE_IsContentProcess() && mMaybeCloseWindowHelper) {
1898 mBrowsingContext = mMaybeCloseWindowHelper->MaybeCloseWindow();
1901 // In an IPC setting, we're allowing the child process, here, to make
1902 // decisions about decoding the channel (e.g. decompression). It will
1903 // still forward the decoded (uncompressed) data back to the parent.
1904 // Con: Uncompressed data means more IPC overhead.
1905 // Pros: ExternalHelperAppParent doesn't need to implement nsIEncodedChannel.
1906 // Parent process doesn't need to expect CPU time on decompression.
1907 MaybeApplyDecodingForExtension(aChannel);
1909 // At this point, the child process has done everything it can usefully do
1910 // for OnStartRequest.
1911 if (XRE_IsContentProcess()) {
1912 return NS_OK;
1915 rv = SetUpTempFile(aChannel);
1916 if (NS_FAILED(rv)) {
1917 nsresult transferError = rv;
1919 rv = CreateFailedTransfer();
1920 if (NS_FAILED(rv)) {
1921 LOG(
1922 ("Failed to create transfer to report failure."
1923 "Will fallback to prompter!"));
1926 mCanceled = true;
1927 request->Cancel(transferError);
1929 nsAutoString path;
1930 if (mTempFile) mTempFile->GetPath(path);
1932 SendStatusChange(kWriteError, transferError, request, path);
1934 RecordDownloadTelemetry(aChannel, "savefailed");
1936 return NS_OK;
1939 // Inform channel it is open on behalf of a download to throttle it during
1940 // page loads and prevent its caching.
1941 nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(aChannel);
1942 if (httpInternal) {
1943 rv = httpInternal->SetChannelIsForDownload(true);
1944 MOZ_ASSERT(NS_SUCCEEDED(rv));
1947 if (mSourceUrl->SchemeIs("data")) {
1948 // In case we're downloading a data:// uri
1949 // we don't want to apply AllowTopLevelNavigationToDataURI.
1950 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1951 loadInfo->SetForceAllowDataURI(true);
1954 // now that the temp file is set up, find out if we need to invoke a dialog
1955 // asking the user what they want us to do with this content...
1957 // We can get here for three reasons: "can't handle", "sniffed type", or
1958 // "server sent content-disposition:attachment". In the first case we want
1959 // to honor the user's "always ask" pref; in the other two cases we want to
1960 // honor it only if the default action is "save". Opening attachments in
1961 // helper apps by default breaks some websites (especially if the attachment
1962 // is one part of a multipart document). Opening sniffed content in helper
1963 // apps by default introduces security holes that we'd rather not have.
1965 // So let's find out whether the user wants to be prompted. If he does not,
1966 // check mReason and the preferred action to see what we should do.
1968 bool alwaysAsk = true;
1969 mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk);
1970 if (alwaysAsk) {
1971 // But we *don't* ask if this mimeInfo didn't come from
1972 // our user configuration datastore and the user has said
1973 // at some point in the distant past that they don't
1974 // want to be asked. The latter fact would have been
1975 // stored in pref strings back in the old days.
1977 bool mimeTypeIsInDatastore = false;
1978 nsCOMPtr<nsIHandlerService> handlerSvc =
1979 do_GetService(NS_HANDLERSERVICE_CONTRACTID);
1980 if (handlerSvc) {
1981 handlerSvc->Exists(mMimeInfo, &mimeTypeIsInDatastore);
1983 if (!handlerSvc || !mimeTypeIsInDatastore) {
1984 if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_SAVE_TO_DISK_PREF,
1985 MIMEType.get())) {
1986 // Don't need to ask after all.
1987 alwaysAsk = false;
1988 // Make sure action matches pref (save to disk).
1989 mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
1990 } else if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_OPEN_FILE_PREF,
1991 MIMEType.get())) {
1992 // Don't need to ask after all.
1993 alwaysAsk = false;
1996 } else if (MIMEType.EqualsLiteral("text/plain")) {
1997 nsAutoCString ext;
1998 mMimeInfo->GetPrimaryExtension(ext);
1999 // If people are sending us ApplicationReputation-eligible files with
2000 // text/plain mimetypes, enforce asking the user what to do.
2001 if (!ext.IsEmpty()) {
2002 nsAutoCString dummyFileName("f");
2003 if (ext.First() != '.') {
2004 dummyFileName.Append(".");
2006 ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
2007 nsCOMPtr<nsIApplicationReputationService> appRep =
2008 components::ApplicationReputation::Service();
2009 appRep->IsBinary(dummyFileName + ext, &alwaysAsk);
2013 int32_t action = nsIMIMEInfo::saveToDisk;
2014 mMimeInfo->GetPreferredAction(&action);
2016 bool forcePrompt =
2017 mReason == nsIHelperAppLauncherDialog::REASON_TYPESNIFFED ||
2018 (mReason == nsIHelperAppLauncherDialog::REASON_SERVERREQUEST &&
2019 !StaticPrefs::browser_download_improvements_to_download_panel());
2021 // OK, now check why we're here
2022 if (!alwaysAsk && forcePrompt) {
2023 // Force asking if we're not saving. See comment back when we fetched the
2024 // alwaysAsk boolean for details.
2025 alwaysAsk = (action != nsIMIMEInfo::saveToDisk);
2028 bool shouldAutomaticallyHandleInternally =
2029 action == nsIMIMEInfo::handleInternally &&
2030 StaticPrefs::browser_download_improvements_to_download_panel();
2032 // If we're not asking, check we actually know what to do:
2033 if (!alwaysAsk) {
2034 alwaysAsk = action != nsIMIMEInfo::saveToDisk &&
2035 action != nsIMIMEInfo::useHelperApp &&
2036 action != nsIMIMEInfo::useSystemDefault &&
2037 !shouldAutomaticallyHandleInternally;
2040 // If we're handling with the OS default and we are that default, force
2041 // asking, so we don't end up in an infinite loop:
2042 if (!alwaysAsk && action == nsIMIMEInfo::useSystemDefault) {
2043 bool areOSDefault = false;
2044 alwaysAsk = NS_SUCCEEDED(mMimeInfo->IsCurrentAppOSDefault(&areOSDefault)) &&
2045 areOSDefault;
2046 } else if (!alwaysAsk && action == nsIMIMEInfo::useHelperApp) {
2047 nsCOMPtr<nsIHandlerApp> preferredApp;
2048 mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(preferredApp));
2049 nsCOMPtr<nsILocalHandlerApp> handlerApp = do_QueryInterface(preferredApp);
2050 if (handlerApp) {
2051 nsCOMPtr<nsIFile> executable;
2052 handlerApp->GetExecutable(getter_AddRefs(executable));
2053 nsCOMPtr<nsIFile> ourselves;
2054 if (executable &&
2055 // Despite the name, this really just fetches an nsIFile...
2056 NS_SUCCEEDED(NS_GetSpecialDirectory(XRE_EXECUTABLE_FILE,
2057 getter_AddRefs(ourselves)))) {
2058 ourselves = nsMIMEInfoBase::GetCanonicalExecutable(ourselves);
2059 executable = nsMIMEInfoBase::GetCanonicalExecutable(executable);
2060 bool isSameApp = false;
2061 alwaysAsk =
2062 NS_FAILED(executable->Equals(ourselves, &isSameApp)) || isSameApp;
2067 // if we were told that we _must_ save to disk without asking, all the stuff
2068 // before this is irrelevant; override it
2069 if (mForceSave) {
2070 alwaysAsk = false;
2071 action = nsIMIMEInfo::saveToDisk;
2072 shouldAutomaticallyHandleInternally = false;
2074 // Additionally, if we are asked by the OS to open a local file,
2075 // automatically downloading it to create a second copy of that file doesn't
2076 // really make sense. We should ask the user what they want to do.
2077 if (mSourceUrl->SchemeIs("file") && !alwaysAsk &&
2078 action == nsIMIMEInfo::saveToDisk) {
2079 alwaysAsk = true;
2082 // If adding new checks, make sure this is the last check before telemetry
2083 // and going ahead with opening the file!
2084 #ifdef XP_WIN
2085 /* We need to see whether the file we've got here could be
2086 * executable. If it could, we had better not try to open it!
2087 * We can skip this check, though, if we have a setting to open in a
2088 * helper app.
2090 if (!alwaysAsk && action != nsIMIMEInfo::saveToDisk &&
2091 !shouldAutomaticallyHandleInternally) {
2092 nsCOMPtr<nsIHandlerApp> prefApp;
2093 mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(prefApp));
2094 if (action != nsIMIMEInfo::useHelperApp || !prefApp) {
2095 nsCOMPtr<nsIFile> fileToTest;
2096 GetTargetFile(getter_AddRefs(fileToTest));
2097 if (fileToTest) {
2098 bool isExecutable;
2099 rv = fileToTest->IsExecutable(&isExecutable);
2100 if (NS_FAILED(rv) || mTempFileIsExecutable ||
2101 isExecutable) { // checking NS_FAILED, because paranoia is good
2102 alwaysAsk = true;
2104 } else { // Paranoia is good here too, though this really should not
2105 // happen
2106 NS_WARNING(
2107 "GetDownloadInfo returned a null file after the temp file has been "
2108 "set up! ");
2109 alwaysAsk = true;
2113 #endif
2115 nsAutoCString actionTelem;
2116 if (alwaysAsk) {
2117 actionTelem.AssignLiteral("ask");
2118 } else if (shouldAutomaticallyHandleInternally) {
2119 actionTelem.AssignLiteral("internal");
2120 } else if (action == nsIMIMEInfo::useHelperApp ||
2121 action == nsIMIMEInfo::useSystemDefault) {
2122 actionTelem.AssignLiteral("external");
2123 } else {
2124 actionTelem.AssignLiteral("save");
2127 RecordDownloadTelemetry(aChannel, actionTelem.get());
2129 if (alwaysAsk) {
2130 // Display the dialog
2131 mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv);
2132 NS_ENSURE_SUCCESS(rv, rv);
2134 // this will create a reference cycle (the dialog holds a reference to us as
2135 // nsIHelperAppLauncher), which will be broken in Cancel or CreateTransfer.
2136 nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
2137 rv = mDialog->Show(this, dialogParent, mReason);
2139 // what do we do if the dialog failed? I guess we should call Cancel and
2140 // abort the load....
2141 } else {
2142 // We need to do the save/open immediately, then.
2143 if (action == nsIMIMEInfo::useHelperApp ||
2144 action == nsIMIMEInfo::useSystemDefault ||
2145 shouldAutomaticallyHandleInternally) {
2146 // Check if the file is local, in which case just launch it from where it
2147 // is. Otherwise, set the file to launch once it's finished downloading.
2148 rv = mIsFileChannel ? LaunchLocalFile()
2149 : SetDownloadToLaunch(
2150 shouldAutomaticallyHandleInternally, nullptr);
2151 } else {
2152 rv = PromptForSaveDestination();
2155 return NS_OK;
2158 void nsExternalAppHandler::RecordDownloadTelemetry(nsIChannel* aChannel,
2159 const char* aAction) {
2160 // Telemetry for helper app dialog
2162 if (XRE_IsContentProcess()) {
2163 return;
2166 nsAutoCString reason;
2167 switch (mReason) {
2168 case nsIHelperAppLauncherDialog::REASON_SERVERREQUEST:
2169 reason.AssignLiteral("attachment");
2170 break;
2171 case nsIHelperAppLauncherDialog::REASON_TYPESNIFFED:
2172 reason.AssignLiteral("sniffed");
2173 break;
2174 case nsIHelperAppLauncherDialog::REASON_CANTHANDLE:
2175 default:
2176 reason.AssignLiteral("other");
2177 break;
2180 nsAutoCString contentTypeTelem;
2181 nsAutoCString contentType;
2182 aChannel->GetContentType(contentType);
2183 if (contentType.EqualsIgnoreCase(APPLICATION_PDF)) {
2184 contentTypeTelem.AssignLiteral("pdf");
2185 } else if (contentType.EqualsIgnoreCase(APPLICATION_OCTET_STREAM) ||
2186 contentType.EqualsIgnoreCase(BINARY_OCTET_STREAM)) {
2187 contentTypeTelem.AssignLiteral("octetstream");
2188 } else {
2189 contentTypeTelem.AssignLiteral("other");
2192 CopyableTArray<mozilla::Telemetry::EventExtraEntry> extra(1);
2193 extra.AppendElement(
2194 mozilla::Telemetry::EventExtraEntry{"type"_ns, contentTypeTelem});
2195 extra.AppendElement(mozilla::Telemetry::EventExtraEntry{"reason"_ns, reason});
2197 mozilla::Telemetry::RecordEvent(
2198 mozilla::Telemetry::EventID::Downloads_Helpertype_Unknowntype,
2199 mozilla::Some(aAction), mozilla::Some(extra));
2202 bool nsExternalAppHandler::IsDownloadSpam(nsIChannel* aChannel) {
2203 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
2204 nsCOMPtr<nsIPermissionManager> permissionManager =
2205 mozilla::services::GetPermissionManager();
2206 nsCOMPtr<nsIPrincipal> principal = loadInfo->TriggeringPrincipal();
2207 bool exactHostMatch = false;
2208 constexpr auto type = "automatic-download"_ns;
2209 nsCOMPtr<nsIPermission> permission;
2211 permissionManager->GetPermissionObject(principal, type, exactHostMatch,
2212 getter_AddRefs(permission));
2214 if (permission) {
2215 uint32_t capability;
2216 permission->GetCapability(&capability);
2217 if (capability == nsIPermissionManager::DENY_ACTION) {
2218 mCanceled = true;
2219 aChannel->Cancel(NS_ERROR_ABORT);
2220 return true;
2222 if (capability == nsIPermissionManager::ALLOW_ACTION) {
2223 return false;
2225 // If no action is set (i.e: null), we set PROMPT_ACTION by default,
2226 // which will notify the Downloads UI to open the panel on the next request.
2227 if (capability == nsIPermissionManager::PROMPT_ACTION) {
2228 nsCOMPtr<nsIObserverService> observerService =
2229 mozilla::services::GetObserverService();
2231 nsAutoCString cStringURI;
2232 loadInfo->TriggeringPrincipal()->GetPrePath(cStringURI);
2233 observerService->NotifyObservers(
2234 nullptr, "blocked-automatic-download",
2235 NS_ConvertASCIItoUTF16(cStringURI.get()).get());
2236 // FIXME: In order to escape memory leaks, currently we cancel blocked
2237 // downloads. This is temporary solution, because download data should be
2238 // kept in order to restart the blocked download.
2239 mCanceled = true;
2240 aChannel->Cancel(NS_ERROR_ABORT);
2241 // End cancel
2242 return true;
2245 if (!loadInfo->GetHasValidUserGestureActivation()) {
2246 permissionManager->AddFromPrincipal(
2247 principal, type, nsIPermissionManager::PROMPT_ACTION,
2248 nsIPermissionManager::EXPIRE_NEVER, 0 /* expire time */);
2251 return false;
2254 // Convert error info into proper message text and send OnStatusChange
2255 // notification to the dialog progress listener or nsITransfer implementation.
2256 void nsExternalAppHandler::SendStatusChange(ErrorType type, nsresult rv,
2257 nsIRequest* aRequest,
2258 const nsString& path) {
2259 const char* msgId = nullptr;
2260 switch (rv) {
2261 case NS_ERROR_OUT_OF_MEMORY:
2262 // No memory
2263 msgId = "noMemory";
2264 break;
2266 case NS_ERROR_FILE_NO_DEVICE_SPACE:
2267 // Out of space on target volume.
2268 msgId = "diskFull";
2269 break;
2271 case NS_ERROR_FILE_READ_ONLY:
2272 // Attempt to write to read/only file.
2273 msgId = "readOnly";
2274 break;
2276 case NS_ERROR_FILE_ACCESS_DENIED:
2277 if (type == kWriteError) {
2278 // Attempt to write without sufficient permissions.
2279 #if defined(ANDROID)
2280 // On Android this means the SD card is present but
2281 // unavailable (read-only).
2282 msgId = "SDAccessErrorCardReadOnly";
2283 #else
2284 msgId = "accessError";
2285 #endif
2286 } else {
2287 msgId = "launchError";
2289 break;
2291 case NS_ERROR_FILE_NOT_FOUND:
2292 case NS_ERROR_FILE_UNRECOGNIZED_PATH:
2293 // Helper app not found, let's verify this happened on launch
2294 if (type == kLaunchError) {
2295 msgId = "helperAppNotFound";
2296 break;
2298 #if defined(ANDROID)
2299 else if (type == kWriteError) {
2300 // On Android this means the SD card is missing (not in
2301 // SD slot).
2302 msgId = "SDAccessErrorCardMissing";
2303 break;
2305 #endif
2306 [[fallthrough]];
2308 default:
2309 // Generic read/write/launch error message.
2310 switch (type) {
2311 case kReadError:
2312 msgId = "readError";
2313 break;
2314 case kWriteError:
2315 msgId = "writeError";
2316 break;
2317 case kLaunchError:
2318 msgId = "launchError";
2319 break;
2321 break;
2324 MOZ_LOG(
2325 nsExternalHelperAppService::mLog, LogLevel::Error,
2326 ("Error: %s, type=%i, listener=0x%p, transfer=0x%p, rv=0x%08" PRIX32 "\n",
2327 msgId, type, mDialogProgressListener.get(), mTransfer.get(),
2328 static_cast<uint32_t>(rv)));
2330 MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error,
2331 (" path='%s'\n", NS_ConvertUTF16toUTF8(path).get()));
2333 // Get properties file bundle and extract status string.
2334 nsCOMPtr<nsIStringBundleService> stringService =
2335 mozilla::components::StringBundle::Service();
2336 if (stringService) {
2337 nsCOMPtr<nsIStringBundle> bundle;
2338 if (NS_SUCCEEDED(stringService->CreateBundle(
2339 "chrome://global/locale/nsWebBrowserPersist.properties",
2340 getter_AddRefs(bundle)))) {
2341 nsAutoString msgText;
2342 AutoTArray<nsString, 1> strings = {path};
2343 if (NS_SUCCEEDED(bundle->FormatStringFromName(msgId, strings, msgText))) {
2344 if (mDialogProgressListener) {
2345 // We have a listener, let it handle the error.
2346 mDialogProgressListener->OnStatusChange(
2347 nullptr, (type == kReadError) ? aRequest : nullptr, rv,
2348 msgText.get());
2349 } else if (mTransfer) {
2350 mTransfer->OnStatusChange(nullptr,
2351 (type == kReadError) ? aRequest : nullptr,
2352 rv, msgText.get());
2353 } else if (XRE_IsParentProcess()) {
2354 // We don't have a listener. Simply show the alert ourselves.
2355 nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
2356 nsresult qiRv;
2357 nsCOMPtr<nsIPrompt> prompter(do_GetInterface(dialogParent, &qiRv));
2358 nsAutoString title;
2359 bundle->FormatStringFromName("title", strings, title);
2361 MOZ_LOG(
2362 nsExternalHelperAppService::mLog, LogLevel::Debug,
2363 ("mBrowsingContext=0x%p, prompter=0x%p, qi rv=0x%08" PRIX32
2364 ", title='%s', msg='%s'",
2365 mBrowsingContext.get(), prompter.get(),
2366 static_cast<uint32_t>(qiRv), NS_ConvertUTF16toUTF8(title).get(),
2367 NS_ConvertUTF16toUTF8(msgText).get()));
2369 // If we didn't have a prompter we will try and get a window
2370 // instead, get it's docshell and use it to alert the user.
2371 if (!prompter) {
2372 nsCOMPtr<nsPIDOMWindowOuter> window(do_GetInterface(dialogParent));
2373 if (!window || !window->GetDocShell()) {
2374 return;
2377 prompter = do_GetInterface(window->GetDocShell(), &qiRv);
2379 MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Debug,
2380 ("No prompter from mBrowsingContext, using DocShell, "
2381 "window=0x%p, docShell=0x%p, "
2382 "prompter=0x%p, qi rv=0x%08" PRIX32,
2383 window.get(), window->GetDocShell(), prompter.get(),
2384 static_cast<uint32_t>(qiRv)));
2386 // If we still don't have a prompter, there's nothing else we
2387 // can do so just return.
2388 if (!prompter) {
2389 MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error,
2390 ("No prompter from DocShell, no way to alert user"));
2391 return;
2395 // We should always have a prompter at this point.
2396 prompter->Alert(title.get(), msgText.get());
2403 NS_IMETHODIMP
2404 nsExternalAppHandler::OnDataAvailable(nsIRequest* request,
2405 nsIInputStream* inStr,
2406 uint64_t sourceOffset, uint32_t count) {
2407 nsresult rv = NS_OK;
2408 // first, check to see if we've been canceled....
2409 if (mCanceled || !mSaver) {
2410 // then go cancel our underlying channel too
2411 return request->Cancel(NS_BINDING_ABORTED);
2414 // read the data out of the stream and write it to the temp file.
2415 if (count > 0) {
2416 mProgress += count;
2418 nsCOMPtr<nsIStreamListener> saver = do_QueryInterface(mSaver);
2419 rv = saver->OnDataAvailable(request, inStr, sourceOffset, count);
2420 if (NS_SUCCEEDED(rv)) {
2421 // Send progress notification.
2422 if (mTransfer) {
2423 mTransfer->OnProgressChange64(nullptr, request, mProgress,
2424 mContentLength, mProgress,
2425 mContentLength);
2427 } else {
2428 // An error occurred, notify listener.
2429 nsAutoString tempFilePath;
2430 if (mTempFile) {
2431 mTempFile->GetPath(tempFilePath);
2433 SendStatusChange(kReadError, rv, request, tempFilePath);
2435 // Cancel the download.
2436 Cancel(rv);
2439 return rv;
2442 NS_IMETHODIMP nsExternalAppHandler::OnStopRequest(nsIRequest* request,
2443 nsresult aStatus) {
2444 LOG(
2445 ("nsExternalAppHandler::OnStopRequest\n"
2446 " mCanceled=%d, mTransfer=0x%p, aStatus=0x%08" PRIX32 "\n",
2447 mCanceled, mTransfer.get(), static_cast<uint32_t>(aStatus)));
2449 mStopRequestIssued = true;
2451 // Cancel if the request did not complete successfully.
2452 if (!mCanceled && NS_FAILED(aStatus)) {
2453 // Send error notification.
2454 nsAutoString tempFilePath;
2455 if (mTempFile) mTempFile->GetPath(tempFilePath);
2456 SendStatusChange(kReadError, aStatus, request, tempFilePath);
2458 Cancel(aStatus);
2461 // first, check to see if we've been canceled....
2462 if (mCanceled || !mSaver) {
2463 return NS_OK;
2466 return mSaver->Finish(NS_OK);
2469 NS_IMETHODIMP
2470 nsExternalAppHandler::OnTargetChange(nsIBackgroundFileSaver* aSaver,
2471 nsIFile* aTarget) {
2472 return NS_OK;
2475 NS_IMETHODIMP
2476 nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver,
2477 nsresult aStatus) {
2478 LOG(
2479 ("nsExternalAppHandler::OnSaveComplete\n"
2480 " aSaver=0x%p, aStatus=0x%08" PRIX32 ", mCanceled=%d, mTransfer=0x%p\n",
2481 aSaver, static_cast<uint32_t>(aStatus), mCanceled, mTransfer.get()));
2483 if (!mCanceled) {
2484 // Save the hash and signature information
2485 (void)mSaver->GetSha256Hash(mHash);
2486 (void)mSaver->GetSignatureInfo(mSignatureInfo);
2488 // Free the reference that the saver keeps on us, even if we couldn't get
2489 // the hash.
2490 mSaver = nullptr;
2492 // Save the redirect information.
2493 nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2494 if (channel) {
2495 nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
2496 nsresult rv = NS_OK;
2497 nsCOMPtr<nsIMutableArray> redirectChain =
2498 do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
2499 NS_ENSURE_SUCCESS(rv, rv);
2500 LOG(("nsExternalAppHandler: Got %zu redirects\n",
2501 loadInfo->RedirectChain().Length()));
2502 for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) {
2503 redirectChain->AppendElement(entry);
2505 mRedirects = redirectChain;
2508 if (NS_FAILED(aStatus)) {
2509 nsAutoString path;
2510 mTempFile->GetPath(path);
2512 // It may happen when e10s is enabled that there will be no transfer
2513 // object available to communicate status as expected by the system.
2514 // Let's try and create a temporary transfer object to take care of this
2515 // for us, we'll fall back to using the prompt service if we absolutely
2516 // have to.
2517 if (!mTransfer) {
2518 // We don't care if this fails.
2519 CreateFailedTransfer();
2522 SendStatusChange(kWriteError, aStatus, nullptr, path);
2523 if (!mCanceled) Cancel(aStatus);
2524 return NS_OK;
2528 // Notify the transfer object that we are done if the user has chosen an
2529 // action. If the user hasn't chosen an action, the progress listener
2530 // (nsITransfer) will be notified in CreateTransfer.
2531 if (mTransfer) {
2532 NotifyTransfer(aStatus);
2535 return NS_OK;
2538 void nsExternalAppHandler::NotifyTransfer(nsresult aStatus) {
2539 MOZ_ASSERT(NS_IsMainThread(), "Must notify on main thread");
2540 MOZ_ASSERT(mTransfer, "We must have an nsITransfer");
2542 LOG(("Notifying progress listener"));
2544 if (NS_SUCCEEDED(aStatus)) {
2545 (void)mTransfer->SetSha256Hash(mHash);
2546 (void)mTransfer->SetSignatureInfo(mSignatureInfo);
2547 (void)mTransfer->SetRedirects(mRedirects);
2548 (void)mTransfer->OnProgressChange64(
2549 nullptr, nullptr, mProgress, mContentLength, mProgress, mContentLength);
2552 (void)mTransfer->OnStateChange(nullptr, nullptr,
2553 nsIWebProgressListener::STATE_STOP |
2554 nsIWebProgressListener::STATE_IS_REQUEST |
2555 nsIWebProgressListener::STATE_IS_NETWORK,
2556 aStatus);
2558 // This nsITransfer object holds a reference to us (we are its observer), so
2559 // we need to release the reference to break a reference cycle (and therefore
2560 // to prevent leaking). We do this even if the previous calls failed.
2561 mTransfer = nullptr;
2564 NS_IMETHODIMP nsExternalAppHandler::GetMIMEInfo(nsIMIMEInfo** aMIMEInfo) {
2565 *aMIMEInfo = mMimeInfo;
2566 NS_ADDREF(*aMIMEInfo);
2567 return NS_OK;
2570 NS_IMETHODIMP nsExternalAppHandler::GetSource(nsIURI** aSourceURI) {
2571 NS_ENSURE_ARG(aSourceURI);
2572 *aSourceURI = mSourceUrl;
2573 NS_IF_ADDREF(*aSourceURI);
2574 return NS_OK;
2577 NS_IMETHODIMP nsExternalAppHandler::GetSuggestedFileName(
2578 nsAString& aSuggestedFileName) {
2579 aSuggestedFileName = mSuggestedFileName;
2580 return NS_OK;
2583 nsresult nsExternalAppHandler::CreateTransfer() {
2584 LOG(("nsExternalAppHandler::CreateTransfer"));
2586 MOZ_ASSERT(NS_IsMainThread(), "Must create transfer on main thread");
2587 // We are back from the helper app dialog (where the user chooses to save or
2588 // open), but we aren't done processing the load. in this case, throw up a
2589 // progress dialog so the user can see what's going on.
2590 // Also, release our reference to mDialog. We don't need it anymore, and we
2591 // need to break the reference cycle.
2592 mDialog = nullptr;
2593 if (!mDialogProgressListener) {
2594 NS_WARNING("The dialog should nullify the dialog progress listener");
2596 // In case of a non acceptable download, we need to cancel the request and
2597 // pass a FailedTransfer for the Download UI.
2598 if (mDownloadClassification != nsITransfer::DOWNLOAD_ACCEPTABLE) {
2599 mCanceled = true;
2600 mRequest->Cancel(NS_ERROR_ABORT);
2601 if (mSaver) {
2602 mSaver->Finish(NS_ERROR_ABORT);
2603 mSaver = nullptr;
2605 return CreateFailedTransfer();
2607 nsresult rv;
2609 // We must be able to create an nsITransfer object. If not, it doesn't matter
2610 // much that we can't launch the helper application or save to disk. Work on
2611 // a local copy rather than mTransfer until we know we succeeded, to make it
2612 // clearer that this function is re-entrant.
2613 nsCOMPtr<nsITransfer> transfer =
2614 do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
2615 NS_ENSURE_SUCCESS(rv, rv);
2617 // Initialize the download
2618 nsCOMPtr<nsIURI> target;
2619 rv = NS_NewFileURI(getter_AddRefs(target), mFinalFileDestination);
2620 NS_ENSURE_SUCCESS(rv, rv);
2622 nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2623 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mRequest);
2624 nsCOMPtr<nsIReferrerInfo> referrerInfo = nullptr;
2625 if (httpChannel) {
2626 referrerInfo = httpChannel->GetReferrerInfo();
2629 if (mBrowsingContext) {
2630 rv = transfer->InitWithBrowsingContext(
2631 mSourceUrl, target, u""_ns, mMimeInfo, mTimeDownloadStarted, mTempFile,
2632 this, channel && NS_UsePrivateBrowsing(channel),
2633 mDownloadClassification, referrerInfo, mBrowsingContext,
2634 mHandleInternally);
2635 } else {
2636 rv = transfer->Init(mSourceUrl, target, u""_ns, mMimeInfo,
2637 mTimeDownloadStarted, mTempFile, this,
2638 channel && NS_UsePrivateBrowsing(channel),
2639 mDownloadClassification, referrerInfo);
2642 NS_ENSURE_SUCCESS(rv, rv);
2644 // If we were cancelled since creating the transfer, just return. It is
2645 // always ok to return NS_OK if we are cancelled. Callers of this function
2646 // must call Cancel if CreateTransfer fails, but there's no need to cancel
2647 // twice.
2648 if (mCanceled) {
2649 return NS_OK;
2651 rv = transfer->OnStateChange(nullptr, mRequest,
2652 nsIWebProgressListener::STATE_START |
2653 nsIWebProgressListener::STATE_IS_REQUEST |
2654 nsIWebProgressListener::STATE_IS_NETWORK,
2655 NS_OK);
2656 NS_ENSURE_SUCCESS(rv, rv);
2658 if (mCanceled) {
2659 return NS_OK;
2662 mRequest = nullptr;
2663 // Finally, save the transfer to mTransfer.
2664 mTransfer = transfer;
2665 transfer = nullptr;
2667 // While we were bringing up the progress dialog, we actually finished
2668 // processing the url. If that's the case then mStopRequestIssued will be
2669 // true and OnSaveComplete has been called.
2670 if (mStopRequestIssued && !mSaver && mTransfer) {
2671 NotifyTransfer(NS_OK);
2674 return rv;
2677 nsresult nsExternalAppHandler::CreateFailedTransfer() {
2678 nsresult rv;
2679 nsCOMPtr<nsITransfer> transfer =
2680 do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
2681 NS_ENSURE_SUCCESS(rv, rv);
2683 // We won't pass the temp file to the transfer, so if we have one it needs to
2684 // get deleted now.
2685 if (mTempFile) {
2686 if (mSaver) {
2687 mSaver->Finish(NS_BINDING_ABORTED);
2688 mSaver = nullptr;
2690 mTempFile->Remove(false);
2693 nsCOMPtr<nsIURI> pseudoTarget;
2694 if (!mFinalFileDestination) {
2695 // If we don't have a download directory we're kinda screwed but it's OK
2696 // we'll still report the error via the prompter.
2697 nsCOMPtr<nsIFile> pseudoFile;
2698 rv = GetDownloadDirectory(getter_AddRefs(pseudoFile), true);
2699 NS_ENSURE_SUCCESS(rv, rv);
2701 // Append the default suggested filename. If the user restarts the transfer
2702 // we will re-trigger a filename check anyway to ensure that it is unique.
2703 rv = pseudoFile->Append(mSuggestedFileName);
2704 NS_ENSURE_SUCCESS(rv, rv);
2706 rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), pseudoFile);
2707 NS_ENSURE_SUCCESS(rv, rv);
2708 } else {
2709 // Initialize the target, if present
2710 rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), mFinalFileDestination);
2711 NS_ENSURE_SUCCESS(rv, rv);
2714 nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2715 nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mRequest);
2716 nsCOMPtr<nsIReferrerInfo> referrerInfo = nullptr;
2717 if (httpChannel) {
2718 referrerInfo = httpChannel->GetReferrerInfo();
2721 if (mBrowsingContext) {
2722 rv = transfer->InitWithBrowsingContext(
2723 mSourceUrl, pseudoTarget, u""_ns, mMimeInfo, mTimeDownloadStarted,
2724 mTempFile, this, channel && NS_UsePrivateBrowsing(channel),
2725 mDownloadClassification, referrerInfo, mBrowsingContext,
2726 mHandleInternally);
2727 } else {
2728 rv = transfer->Init(mSourceUrl, pseudoTarget, u""_ns, mMimeInfo,
2729 mTimeDownloadStarted, mTempFile, this,
2730 channel && NS_UsePrivateBrowsing(channel),
2731 mDownloadClassification, referrerInfo);
2733 NS_ENSURE_SUCCESS(rv, rv);
2735 // Our failed transfer is ready.
2736 mTransfer = std::move(transfer);
2738 return NS_OK;
2741 nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile* aFile) {
2742 if (aFile)
2743 ContinueSave(aFile);
2744 else
2745 Cancel(NS_BINDING_ABORTED);
2747 return NS_OK;
2750 void nsExternalAppHandler::RequestSaveDestination(
2751 const nsString& aDefaultFile, const nsString& aFileExtension) {
2752 // Display the dialog
2753 // XXX Convert to use file picker? No, then embeddors could not do any sort of
2754 // "AutoDownload" w/o showing a prompt
2755 nsresult rv = NS_OK;
2756 if (!mDialog) {
2757 // Get helper app launcher dialog.
2758 mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv);
2759 if (rv != NS_OK) {
2760 Cancel(NS_BINDING_ABORTED);
2761 return;
2765 // we want to explicitly unescape aDefaultFile b4 passing into the dialog. we
2766 // can't unescape it because the dialog is implemented by a JS component which
2767 // doesn't have a window so no unescape routine is defined...
2769 // Now, be sure to keep |this| alive, and the dialog
2770 // If we don't do this, users that close the helper app dialog while the file
2771 // picker is up would cause Cancel() to be called, and the dialog would be
2772 // released, which would release this object too, which would crash.
2773 // See Bug 249143
2774 RefPtr<nsExternalAppHandler> kungFuDeathGrip(this);
2775 nsCOMPtr<nsIHelperAppLauncherDialog> dlg(mDialog);
2776 nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
2778 rv = dlg->PromptForSaveToFileAsync(this, dialogParent, aDefaultFile.get(),
2779 aFileExtension.get(), mForceSave);
2780 if (NS_FAILED(rv)) {
2781 Cancel(NS_BINDING_ABORTED);
2785 // PromptForSaveDestination should only be called by the helper app dialog which
2786 // allows the user to say launch with application or save to disk.
2787 NS_IMETHODIMP nsExternalAppHandler::PromptForSaveDestination() {
2788 if (mCanceled) return NS_OK;
2790 if (!StaticPrefs::browser_download_improvements_to_download_panel() ||
2791 mForceSave) {
2792 mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
2795 if (mSuggestedFileName.IsEmpty()) {
2796 RequestSaveDestination(mTempLeafName, mTempFileExtension);
2797 } else {
2798 nsAutoString fileExt;
2799 int32_t pos = mSuggestedFileName.RFindChar('.');
2800 if (pos >= 0) {
2801 mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos);
2803 if (fileExt.IsEmpty()) {
2804 fileExt = mTempFileExtension;
2807 RequestSaveDestination(mSuggestedFileName, fileExt);
2810 return NS_OK;
2812 nsresult nsExternalAppHandler::ContinueSave(nsIFile* aNewFileLocation) {
2813 if (mCanceled) return NS_OK;
2815 MOZ_ASSERT(aNewFileLocation, "Must be called with a non-null file");
2817 int32_t action = nsIMIMEInfo::saveToDisk;
2818 mMimeInfo->GetPreferredAction(&action);
2819 mHandleInternally =
2820 action == nsIMIMEInfo::handleInternally &&
2821 StaticPrefs::browser_download_improvements_to_download_panel();
2823 nsresult rv = NS_OK;
2824 nsCOMPtr<nsIFile> fileToUse = aNewFileLocation;
2825 mFinalFileDestination = fileToUse;
2827 // Move what we have in the final directory, but append .part
2828 // to it, to indicate that it's unfinished. Do not call SetTarget on the
2829 // saver if we are done (Finish has been called) but OnSaverComplete has
2830 // not been called.
2831 if (mFinalFileDestination && mSaver && !mStopRequestIssued) {
2832 nsCOMPtr<nsIFile> movedFile;
2833 mFinalFileDestination->Clone(getter_AddRefs(movedFile));
2834 if (movedFile) {
2835 nsAutoCString randomChars;
2836 rv = GenerateRandomName(randomChars);
2837 if (NS_SUCCEEDED(rv)) {
2838 // Get the leaf name, strip any extensions, then
2839 // add random bytes, followed by the extensions and '.part'.
2840 nsAutoString leafName;
2841 mFinalFileDestination->GetLeafName(leafName);
2842 auto nameWithoutExtensionLength = leafName.FindChar('.');
2843 nsAutoString extensions(u"");
2844 if (nameWithoutExtensionLength == kNotFound) {
2845 nameWithoutExtensionLength = leafName.Length();
2846 } else {
2847 extensions = Substring(leafName, nameWithoutExtensionLength);
2849 leafName.Truncate(nameWithoutExtensionLength);
2851 nsAutoString suffix = u"."_ns + NS_ConvertASCIItoUTF16(randomChars) +
2852 extensions + u".part"_ns;
2853 #ifdef XP_WIN
2854 // Deal with MAX_PATH on Windows. Worth noting that the original
2855 // path for mFinalFileDestination must be valid for us to get
2856 // here: either SetDownloadToLaunch or the caller of
2857 // SaveDestinationAvailable has called CreateUnique or similar
2858 // to ensure both a unique name and one that isn't too long.
2859 // The only issue is we're making it longer to get the part
2860 // file path...
2861 nsAutoString path;
2862 mFinalFileDestination->GetPath(path);
2863 CheckedInt<uint16_t> fullPathLength =
2864 CheckedInt<uint16_t>(path.Length()) + 1 + randomChars.Length() +
2865 ArrayLength(".part");
2866 if (!fullPathLength.isValid()) {
2867 leafName.Truncate();
2868 } else if (fullPathLength.value() > MAX_PATH) {
2869 int32_t leafNameRemaining =
2870 (int32_t)leafName.Length() - (fullPathLength.value() - MAX_PATH);
2871 leafName.Truncate(std::max(leafNameRemaining, 0));
2873 #endif
2874 leafName.Append(suffix);
2875 movedFile->SetLeafName(leafName);
2877 rv = mSaver->SetTarget(movedFile, true);
2878 if (NS_FAILED(rv)) {
2879 nsAutoString path;
2880 mTempFile->GetPath(path);
2881 SendStatusChange(kWriteError, rv, nullptr, path);
2882 Cancel(rv);
2883 return NS_OK;
2886 mTempFile = movedFile;
2891 // The helper app dialog has told us what to do and we have a final file
2892 // destination.
2893 rv = CreateTransfer();
2894 // If we fail to create the transfer, Cancel.
2895 if (NS_FAILED(rv)) {
2896 Cancel(rv);
2897 return rv;
2900 return NS_OK;
2903 // SetDownloadToLaunch should only be called by the helper app dialog which
2904 // allows the user to say launch with application or save to disk.
2905 NS_IMETHODIMP nsExternalAppHandler::SetDownloadToLaunch(
2906 bool aHandleInternally, nsIFile* aNewFileLocation) {
2907 if (mCanceled) return NS_OK;
2909 mHandleInternally = aHandleInternally;
2911 // Now that the user has elected to launch the downloaded file with a helper
2912 // app, we're justified in removing the 'salted' name. We'll rename to what
2913 // was specified in mSuggestedFileName after the download is done prior to
2914 // launching the helper app. So that any existing file of that name won't be
2915 // overwritten we call CreateUnique(). Also note that we use the same
2916 // directory as originally downloaded so the download can be renamed in place
2917 // later.
2918 nsCOMPtr<nsIFile> fileToUse;
2919 if (aNewFileLocation &&
2920 StaticPrefs::browser_download_improvements_to_download_panel()) {
2921 fileToUse = aNewFileLocation;
2922 } else {
2923 (void)GetDownloadDirectory(getter_AddRefs(fileToUse));
2925 if (mSuggestedFileName.IsEmpty()) {
2926 // Keep using the leafname of the temp file, since we're just starting a
2927 // helper
2928 mSuggestedFileName = mTempLeafName;
2931 #ifdef XP_WIN
2932 fileToUse->Append(mSuggestedFileName + mTempFileExtension);
2933 #else
2934 fileToUse->Append(mSuggestedFileName);
2935 #endif
2938 nsresult rv = fileToUse->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
2939 if (NS_SUCCEEDED(rv)) {
2940 mFinalFileDestination = fileToUse;
2941 // launch the progress window now that the user has picked the desired
2942 // action.
2943 rv = CreateTransfer();
2944 if (NS_FAILED(rv)) {
2945 Cancel(rv);
2947 } else {
2948 // Cancel the download and report an error. We do not want to end up in
2949 // a state where it appears that we have a normal download that is
2950 // pointing to a file that we did not actually create.
2951 nsAutoString path;
2952 mTempFile->GetPath(path);
2953 SendStatusChange(kWriteError, rv, nullptr, path);
2954 Cancel(rv);
2956 return rv;
2959 nsresult nsExternalAppHandler::LaunchLocalFile() {
2960 nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(mSourceUrl));
2961 if (!fileUrl) {
2962 return NS_OK;
2964 Cancel(NS_BINDING_ABORTED);
2965 nsCOMPtr<nsIFile> file;
2966 nsresult rv = fileUrl->GetFile(getter_AddRefs(file));
2968 if (NS_SUCCEEDED(rv)) {
2969 rv = mMimeInfo->LaunchWithFile(file);
2970 if (NS_SUCCEEDED(rv)) return NS_OK;
2972 nsAutoString path;
2973 if (file) file->GetPath(path);
2974 // If we get here, an error happened
2975 SendStatusChange(kLaunchError, rv, nullptr, path);
2976 return rv;
2979 NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) {
2980 NS_ENSURE_ARG(NS_FAILED(aReason));
2982 if (mCanceled) {
2983 return NS_OK;
2985 mCanceled = true;
2987 if (mSaver) {
2988 // We are still writing to the target file. Give the saver a chance to
2989 // close the target file, then notify the transfer object if necessary in
2990 // the OnSaveComplete callback.
2991 mSaver->Finish(aReason);
2992 mSaver = nullptr;
2993 } else {
2994 if (mStopRequestIssued && mTempFile) {
2995 // This branch can only happen when the user cancels the helper app dialog
2996 // when the request has completed. The temp file has to be removed here,
2997 // because mSaver has been released at that time with the temp file left.
2998 (void)mTempFile->Remove(false);
3001 // Notify the transfer object that the download has been canceled, if the
3002 // user has already chosen an action and we didn't notify already.
3003 if (mTransfer) {
3004 NotifyTransfer(aReason);
3008 // Break our reference cycle with the helper app dialog (set up in
3009 // OnStartRequest)
3010 mDialog = nullptr;
3012 mRequest = nullptr;
3014 // Release the listener, to break the reference cycle with it (we are the
3015 // observer of the listener).
3016 mDialogProgressListener = nullptr;
3018 return NS_OK;
3021 bool nsExternalAppHandler::GetNeverAskFlagFromPref(const char* prefName,
3022 const char* aContentType) {
3023 // Search the obsolete pref strings.
3024 nsAutoCString prefCString;
3025 Preferences::GetCString(prefName, prefCString);
3026 if (prefCString.IsEmpty()) {
3027 // Default is true, if not found in the pref string.
3028 return true;
3031 NS_UnescapeURL(prefCString);
3032 nsACString::const_iterator start, end;
3033 prefCString.BeginReading(start);
3034 prefCString.EndReading(end);
3035 return !CaseInsensitiveFindInReadable(nsDependentCString(aContentType), start,
3036 end);
3039 NS_IMETHODIMP
3040 nsExternalAppHandler::GetName(nsACString& aName) {
3041 aName.AssignLiteral("nsExternalAppHandler");
3042 return NS_OK;
3045 //////////////////////////////////////////////////////////////////////////////////////////////////////////////
3046 // The following section contains our nsIMIMEService implementation and related
3047 // methods.
3049 //////////////////////////////////////////////////////////////////////////////////////////////////////////////
3051 // nsIMIMEService methods
3052 NS_IMETHODIMP nsExternalHelperAppService::GetFromTypeAndExtension(
3053 const nsACString& aMIMEType, const nsACString& aFileExt,
3054 nsIMIMEInfo** _retval) {
3055 MOZ_ASSERT(!aMIMEType.IsEmpty() || !aFileExt.IsEmpty(),
3056 "Give me something to work with");
3057 MOZ_DIAGNOSTIC_ASSERT(aFileExt.FindChar('\0') == kNotFound,
3058 "The extension should never contain null characters");
3059 LOG(("Getting mimeinfo from type '%s' ext '%s'\n",
3060 PromiseFlatCString(aMIMEType).get(),
3061 PromiseFlatCString(aFileExt).get()));
3063 *_retval = nullptr;
3065 // OK... we need a type. Get one.
3066 nsAutoCString typeToUse(aMIMEType);
3067 if (typeToUse.IsEmpty()) {
3068 nsresult rv = GetTypeFromExtension(aFileExt, typeToUse);
3069 if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE;
3072 // We promise to only send lower case mime types to the OS
3073 ToLowerCase(typeToUse);
3075 // First, ask the OS for a mime info
3076 bool found;
3077 nsresult rv = GetMIMEInfoFromOS(typeToUse, aFileExt, &found, _retval);
3078 if (NS_WARN_IF(NS_FAILED(rv))) {
3079 return rv;
3082 LOG(("OS gave back 0x%p - found: %i\n", *_retval, found));
3083 // If we got no mimeinfo, something went wrong. Probably lack of memory.
3084 if (!*_retval) return NS_ERROR_OUT_OF_MEMORY;
3086 // The handler service can make up for bad mime types by checking the file
3087 // extension. If the mime type is known (in extras or in the handler
3088 // service), we stop it doing so by flipping this bool to true.
3089 bool trustMIMEType = false;
3091 // Check extras - not everything we support will be known by the OS store,
3092 // unfortunately, and it may even miss some extensions that we know should
3093 // be accepted. We only do this for non-octet-stream mimetypes, because
3094 // our information for octet-stream would lead to us trying to open all such
3095 // files as Binary file with exe, com or bin extension regardless of the
3096 // real extension.
3097 if (!typeToUse.Equals(APPLICATION_OCTET_STREAM,
3098 nsCaseInsensitiveCStringComparator)) {
3099 rv = FillMIMEInfoForMimeTypeFromExtras(typeToUse, !found, *_retval);
3100 LOG(("Searched extras (by type), rv 0x%08" PRIX32 "\n",
3101 static_cast<uint32_t>(rv)));
3102 trustMIMEType = NS_SUCCEEDED(rv);
3103 found = found || NS_SUCCEEDED(rv);
3106 // Now, let's see if we can find something in our datastore.
3107 // This will not overwrite the OS information that interests us
3108 // (i.e. default application, default app. description)
3109 nsCOMPtr<nsIHandlerService> handlerSvc =
3110 do_GetService(NS_HANDLERSERVICE_CONTRACTID);
3111 if (handlerSvc) {
3112 bool hasHandler = false;
3113 (void)handlerSvc->Exists(*_retval, &hasHandler);
3114 if (hasHandler) {
3115 rv = handlerSvc->FillHandlerInfo(*_retval, ""_ns);
3116 LOG(("Data source: Via type: retval 0x%08" PRIx32 "\n",
3117 static_cast<uint32_t>(rv)));
3118 trustMIMEType = trustMIMEType || NS_SUCCEEDED(rv);
3119 } else {
3120 rv = NS_ERROR_NOT_AVAILABLE;
3123 found = found || NS_SUCCEEDED(rv);
3126 // If we still haven't found anything, try finding a match for
3127 // an extension in extras first:
3128 if (!found && !aFileExt.IsEmpty()) {
3129 rv = FillMIMEInfoForExtensionFromExtras(aFileExt, *_retval);
3130 LOG(("Searched extras (by ext), rv 0x%08" PRIX32 "\n",
3131 static_cast<uint32_t>(rv)));
3134 // Then check the handler service - but only do so if we really do not know
3135 // the mimetype. This avoids overwriting good mimetype info with bad file
3136 // extension info.
3137 if ((!found || !trustMIMEType) && handlerSvc && !aFileExt.IsEmpty()) {
3138 nsAutoCString overrideType;
3139 rv = handlerSvc->GetTypeFromExtension(aFileExt, overrideType);
3140 if (NS_SUCCEEDED(rv) && !overrideType.IsEmpty()) {
3141 // We can't check handlerSvc->Exists() here, because we have a
3142 // overideType. That's ok, it just results in some console noise.
3143 // (If there's no handler for the override type, it throws)
3144 rv = handlerSvc->FillHandlerInfo(*_retval, overrideType);
3145 LOG(("Data source: Via ext: retval 0x%08" PRIx32 "\n",
3146 static_cast<uint32_t>(rv)));
3147 found = found || NS_SUCCEEDED(rv);
3151 // If we still don't have a match, at least set the file description
3152 // to `${aFileExt} File` if it's empty:
3153 if (!found && !aFileExt.IsEmpty()) {
3154 // XXXzpao This should probably be localized
3155 nsAutoCString desc(aFileExt);
3156 desc.AppendLiteral(" File");
3157 (*_retval)->SetDescription(NS_ConvertUTF8toUTF16(desc));
3158 LOG(("Falling back to 'File' file description\n"));
3161 // Sometimes, OSes give us bad data. We have a set of forbidden extensions
3162 // for some MIME types. If the primary extension is forbidden,
3163 // overwrite it with a known-good one. See bug 1571247 for context.
3164 nsAutoCString primaryExtension;
3165 (*_retval)->GetPrimaryExtension(primaryExtension);
3166 if (!primaryExtension.EqualsIgnoreCase(PromiseFlatCString(aFileExt).get())) {
3167 if (MaybeReplacePrimaryExtension(primaryExtension, *_retval)) {
3168 (*_retval)->GetPrimaryExtension(primaryExtension);
3172 // Finally, check if we got a file extension and if yes, if it is an
3173 // extension on the mimeinfo, in which case we want it to be the primary one
3174 if (!aFileExt.IsEmpty()) {
3175 bool matches = false;
3176 (*_retval)->ExtensionExists(aFileExt, &matches);
3177 LOG(("Extension '%s' matches mime info: %i\n",
3178 PromiseFlatCString(aFileExt).get(), matches));
3179 if (matches) {
3180 nsAutoCString fileExt;
3181 ToLowerCase(aFileExt, fileExt);
3182 (*_retval)->SetPrimaryExtension(fileExt);
3183 primaryExtension = fileExt;
3187 // Overwrite with a generic description if the primary extension for the
3188 // type is in our list; these are file formats supported by Firefox and
3189 // we don't want other brands positioning themselves as the sole viewer
3190 // for a system.
3191 if (!primaryExtension.IsEmpty()) {
3192 for (const char* ext : descriptionOverwriteExtensions) {
3193 if (primaryExtension.Equals(ext)) {
3194 nsCOMPtr<nsIStringBundleService> bundleService =
3195 do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
3196 NS_ENSURE_SUCCESS(rv, rv);
3197 nsCOMPtr<nsIStringBundle> unknownContentTypeBundle;
3198 rv = bundleService->CreateBundle(
3199 "chrome://mozapps/locale/downloads/unknownContentType.properties",
3200 getter_AddRefs(unknownContentTypeBundle));
3201 if (NS_SUCCEEDED(rv)) {
3202 nsAutoCString stringName(ext);
3203 stringName.AppendLiteral("ExtHandlerDescription");
3204 nsAutoString handlerDescription;
3205 rv = unknownContentTypeBundle->GetStringFromName(stringName.get(),
3206 handlerDescription);
3207 if (NS_SUCCEEDED(rv)) {
3208 (*_retval)->SetDescription(handlerDescription);
3211 break;
3216 if (LOG_ENABLED()) {
3217 nsAutoCString type;
3218 (*_retval)->GetMIMEType(type);
3220 LOG(("MIME Info Summary: Type '%s', Primary Ext '%s'\n", type.get(),
3221 primaryExtension.get()));
3224 return NS_OK;
3227 NS_IMETHODIMP
3228 nsExternalHelperAppService::GetTypeFromExtension(const nsACString& aFileExt,
3229 nsACString& aContentType) {
3230 // OK. We want to try the following sources of mimetype information, in this
3231 // order:
3232 // 1. defaultMimeEntries array
3233 // 2. OS-provided information
3234 // 3. our "extras" array
3235 // 4. Information from plugins
3236 // 5. The "ext-to-type-mapping" category
3237 // Note that, we are intentionally not looking at the handler service, because
3238 // that can be affected by websites, which leads to undesired behavior.
3240 // Early return if called with an empty extension parameter
3241 if (aFileExt.IsEmpty()) {
3242 return NS_ERROR_NOT_AVAILABLE;
3245 // First of all, check our default entries
3246 for (auto& entry : defaultMimeEntries) {
3247 if (aFileExt.LowerCaseEqualsASCII(entry.mFileExtension)) {
3248 aContentType = entry.mMimeType;
3249 return NS_OK;
3253 // Ask OS.
3254 if (GetMIMETypeFromOSForExtension(aFileExt, aContentType)) {
3255 return NS_OK;
3258 // Check extras array.
3259 bool found = GetTypeFromExtras(aFileExt, aContentType);
3260 if (found) {
3261 return NS_OK;
3264 // Let's see if an extension added something
3265 nsCOMPtr<nsICategoryManager> catMan(
3266 do_GetService("@mozilla.org/categorymanager;1"));
3267 if (catMan) {
3268 // The extension in the category entry is always stored as lowercase
3269 nsAutoCString lowercaseFileExt(aFileExt);
3270 ToLowerCase(lowercaseFileExt);
3271 // Read the MIME type from the category entry, if available
3272 nsCString type;
3273 nsresult rv =
3274 catMan->GetCategoryEntry("ext-to-type-mapping", lowercaseFileExt, type);
3275 if (NS_SUCCEEDED(rv)) {
3276 aContentType = type;
3277 return NS_OK;
3281 return NS_ERROR_NOT_AVAILABLE;
3284 NS_IMETHODIMP nsExternalHelperAppService::GetPrimaryExtension(
3285 const nsACString& aMIMEType, const nsACString& aFileExt,
3286 nsACString& _retval) {
3287 NS_ENSURE_ARG(!aMIMEType.IsEmpty());
3289 nsCOMPtr<nsIMIMEInfo> mi;
3290 nsresult rv =
3291 GetFromTypeAndExtension(aMIMEType, aFileExt, getter_AddRefs(mi));
3292 if (NS_FAILED(rv)) return rv;
3294 return mi->GetPrimaryExtension(_retval);
3297 NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromURI(
3298 nsIURI* aURI, nsACString& aContentType) {
3299 NS_ENSURE_ARG_POINTER(aURI);
3300 nsresult rv = NS_ERROR_NOT_AVAILABLE;
3301 aContentType.Truncate();
3303 // First look for a file to use. If we have one, we just use that.
3304 nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(aURI);
3305 if (fileUrl) {
3306 nsCOMPtr<nsIFile> file;
3307 rv = fileUrl->GetFile(getter_AddRefs(file));
3308 if (NS_SUCCEEDED(rv)) {
3309 rv = GetTypeFromFile(file, aContentType);
3310 if (NS_SUCCEEDED(rv)) {
3311 // we got something!
3312 return rv;
3317 // Now try to get an nsIURL so we don't have to do our own parsing
3318 nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
3319 if (url) {
3320 nsAutoCString ext;
3321 rv = url->GetFileExtension(ext);
3322 if (NS_FAILED(rv)) return rv;
3323 if (ext.IsEmpty()) return NS_ERROR_NOT_AVAILABLE;
3325 UnescapeFragment(ext, url, ext);
3327 return GetTypeFromExtension(ext, aContentType);
3330 // no url, let's give the raw spec a shot
3331 nsAutoCString specStr;
3332 rv = aURI->GetSpec(specStr);
3333 if (NS_FAILED(rv)) return rv;
3334 UnescapeFragment(specStr, aURI, specStr);
3336 // find the file extension (if any)
3337 int32_t extLoc = specStr.RFindChar('.');
3338 int32_t specLength = specStr.Length();
3339 if (-1 != extLoc && extLoc != specLength - 1 &&
3340 // nothing over 20 chars long can be sanely considered an
3341 // extension.... Dat dere would be just data.
3342 specLength - extLoc < 20) {
3343 return GetTypeFromExtension(Substring(specStr, extLoc + 1), aContentType);
3346 // We found no information; say so.
3347 return NS_ERROR_NOT_AVAILABLE;
3350 NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromFile(
3351 nsIFile* aFile, nsACString& aContentType) {
3352 NS_ENSURE_ARG_POINTER(aFile);
3353 nsresult rv;
3355 // Get the Extension
3356 nsAutoString fileName;
3357 rv = aFile->GetLeafName(fileName);
3358 if (NS_FAILED(rv)) return rv;
3360 nsAutoCString fileExt;
3361 if (!fileName.IsEmpty()) {
3362 int32_t len = fileName.Length();
3363 for (int32_t i = len; i >= 0; i--) {
3364 if (fileName[i] == char16_t('.')) {
3365 CopyUTF16toUTF8(Substring(fileName, i + 1), fileExt);
3366 break;
3371 if (fileExt.IsEmpty()) return NS_ERROR_FAILURE;
3373 return GetTypeFromExtension(fileExt, aContentType);
3376 nsresult nsExternalHelperAppService::FillMIMEInfoForMimeTypeFromExtras(
3377 const nsACString& aContentType, bool aOverwriteDescription,
3378 nsIMIMEInfo* aMIMEInfo) {
3379 NS_ENSURE_ARG(aMIMEInfo);
3381 NS_ENSURE_ARG(!aContentType.IsEmpty());
3383 // Look for default entry with matching mime type.
3384 nsAutoCString MIMEType(aContentType);
3385 ToLowerCase(MIMEType);
3386 for (auto entry : extraMimeEntries) {
3387 if (MIMEType.Equals(entry.mMimeType)) {
3388 // This is the one. Set attributes appropriately.
3389 nsDependentCString extensions(entry.mFileExtensions);
3390 nsACString::const_iterator start, end;
3391 extensions.BeginReading(start);
3392 extensions.EndReading(end);
3393 while (start != end) {
3394 nsACString::const_iterator cursor = start;
3395 mozilla::Unused << FindCharInReadable(',', cursor, end);
3396 aMIMEInfo->AppendExtension(Substring(start, cursor));
3397 // If a comma was found, skip it for the next search.
3398 start = cursor != end ? ++cursor : cursor;
3401 nsAutoString desc;
3402 aMIMEInfo->GetDescription(desc);
3403 if (aOverwriteDescription || desc.IsEmpty()) {
3404 aMIMEInfo->SetDescription(NS_ConvertASCIItoUTF16(entry.mDescription));
3406 return NS_OK;
3410 return NS_ERROR_NOT_AVAILABLE;
3413 nsresult nsExternalHelperAppService::FillMIMEInfoForExtensionFromExtras(
3414 const nsACString& aExtension, nsIMIMEInfo* aMIMEInfo) {
3415 nsAutoCString type;
3416 bool found = GetTypeFromExtras(aExtension, type);
3417 if (!found) return NS_ERROR_NOT_AVAILABLE;
3418 return FillMIMEInfoForMimeTypeFromExtras(type, true, aMIMEInfo);
3421 bool nsExternalHelperAppService::MaybeReplacePrimaryExtension(
3422 const nsACString& aPrimaryExtension, nsIMIMEInfo* aMIMEInfo) {
3423 for (const auto& entry : sForbiddenPrimaryExtensions) {
3424 if (aPrimaryExtension.LowerCaseEqualsASCII(entry.mFileExtension)) {
3425 nsDependentCString mime(entry.mMimeType);
3426 for (const auto& extraEntry : extraMimeEntries) {
3427 if (mime.LowerCaseEqualsASCII(extraEntry.mMimeType)) {
3428 nsDependentCString goodExts(extraEntry.mFileExtensions);
3429 int32_t commaPos = goodExts.FindChar(',');
3430 commaPos = commaPos == kNotFound ? goodExts.Length() : commaPos;
3431 auto goodExt = Substring(goodExts, 0, commaPos);
3432 aMIMEInfo->SetPrimaryExtension(goodExt);
3433 return true;
3438 return false;
3441 bool nsExternalHelperAppService::GetTypeFromExtras(const nsACString& aExtension,
3442 nsACString& aMIMEType) {
3443 NS_ASSERTION(!aExtension.IsEmpty(), "Empty aExtension parameter!");
3445 // Look for default entry with matching extension.
3446 nsDependentCString::const_iterator start, end, iter;
3447 int32_t numEntries = ArrayLength(extraMimeEntries);
3448 for (int32_t index = 0; index < numEntries; index++) {
3449 nsDependentCString extList(extraMimeEntries[index].mFileExtensions);
3450 extList.BeginReading(start);
3451 extList.EndReading(end);
3452 iter = start;
3453 while (start != end) {
3454 FindCharInReadable(',', iter, end);
3455 if (Substring(start, iter)
3456 .Equals(aExtension, nsCaseInsensitiveCStringComparator)) {
3457 aMIMEType = extraMimeEntries[index].mMimeType;
3458 return true;
3460 if (iter != end) {
3461 ++iter;
3463 start = iter;
3467 return false;
3470 bool nsExternalHelperAppService::GetMIMETypeFromOSForExtension(
3471 const nsACString& aExtension, nsACString& aMIMEType) {
3472 bool found = false;
3473 nsCOMPtr<nsIMIMEInfo> mimeInfo;
3474 nsresult rv =
3475 GetMIMEInfoFromOS(""_ns, aExtension, &found, getter_AddRefs(mimeInfo));
3476 return NS_SUCCEEDED(rv) && found && mimeInfo &&
3477 NS_SUCCEEDED(mimeInfo->GetMIMEType(aMIMEType));
3480 nsresult nsExternalHelperAppService::GetMIMEInfoFromOS(
3481 const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound,
3482 nsIMIMEInfo** aMIMEInfo) {
3483 *aMIMEInfo = nullptr;
3484 *aFound = false;
3485 return NS_ERROR_NOT_IMPLEMENTED;