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/. */
6 
7 #include "base/basictypes.h"
8 
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"
13 
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/StaticPrefs_security.h"
19 #include "nsXULAppAPI.h"
20 
21 #include "nsExternalHelperAppService.h"
22 #include "nsCExternalHandlerService.h"
23 #include "nsIURI.h"
24 #include "nsIURL.h"
25 #include "nsIFile.h"
26 #include "nsIFileURL.h"
27 #include "nsIChannel.h"
28 #include "nsAppDirectoryServiceDefs.h"
29 #include "nsICategoryManager.h"
30 #include "nsDependentSubstring.h"
31 #include "nsString.h"
32 #include "nsUnicharUtils.h"
33 #include "nsIStringEnumerator.h"
34 #include "nsMemory.h"
35 #include "nsIStreamListener.h"
36 #include "nsIMIMEService.h"
37 #include "nsILoadGroup.h"
38 #include "nsIWebProgressListener.h"
39 #include "nsITransfer.h"
40 #include "nsReadableUtils.h"
41 #include "nsIRequest.h"
42 #include "nsDirectoryServiceDefs.h"
43 #include "nsIInterfaceRequestor.h"
44 #include "nsThreadUtils.h"
45 #include "nsIMutableArray.h"
46 #include "nsIRedirectHistoryEntry.h"
47 #include "nsOSHelperAppService.h"
48 #include "nsOSHelperAppServiceChild.h"
49 #include "nsContentSecurityUtils.h"
50 
51 // used to access our datastore of user-configured helper applications
52 #include "nsIHandlerService.h"
53 #include "nsIMIMEInfo.h"
54 #include "nsIHelperAppLauncherDialog.h"
55 #include "nsIContentDispatchChooser.h"
56 #include "nsNetUtil.h"
57 #include "nsIPrivateBrowsingChannel.h"
58 #include "nsIIOService.h"
59 #include "nsNetCID.h"
60 
61 #include "nsIApplicationReputation.h"
62 
63 #include "nsDSURIContentListener.h"
64 #include "nsMimeTypes.h"
65 // used for header disposition information.
66 #include "nsIHttpChannel.h"
67 #include "nsIHttpChannelInternal.h"
68 #include "nsIEncodedChannel.h"
69 #include "nsIMultiPartChannel.h"
70 #include "nsIFileChannel.h"
71 #include "nsIObserverService.h"  // so we can be a profile change observer
72 #include "nsIPropertyBag2.h"     // for the 64-bit content length
73 
74 #ifdef XP_MACOSX
75 #  include "nsILocalFileMac.h"
76 #endif
77 
78 #include "nsPluginHost.h"
79 #include "nsEscape.h"
80 
81 #include "nsIStringBundle.h"  // XXX needed to localize error msgs
82 #include "nsIPrompt.h"
83 
84 #include "nsITextToSubURI.h"  // to unescape the filename
85 
86 #include "nsDocShellCID.h"
87 
88 #include "nsCRT.h"
89 #include "nsLocalHandlerApp.h"
90 
91 #include "nsIRandomGenerator.h"
92 
93 #include "ContentChild.h"
94 #include "nsXULAppAPI.h"
95 #include "nsPIDOMWindow.h"
96 #include "ExternalHelperAppChild.h"
97 
98 #ifdef XP_WIN
99 #  include "nsWindowsHelpers.h"
100 #endif
101 
102 #include "mozilla/Components.h"
103 #include "mozilla/ClearOnShutdown.h"
104 #include "mozilla/Preferences.h"
105 #include "mozilla/ipc/URIUtils.h"
106 
107 using namespace mozilla;
108 using namespace mozilla::ipc;
109 using namespace mozilla::dom;
110 
111 // Download Folder location constants
112 #define NS_PREF_DOWNLOAD_DIR "browser.download.dir"
113 #define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList"
114 enum {
115   NS_FOLDER_VALUE_DESKTOP = 0,
116   NS_FOLDER_VALUE_DOWNLOADS = 1,
117   NS_FOLDER_VALUE_CUSTOM = 2
118 };
119 
120 LazyLogModule nsExternalHelperAppService::mLog("HelperAppService");
121 
122 // Using level 3 here because the OSHelperAppServices use a log level
123 // of LogLevel::Debug (4), and we want less detailed output here
124 // Using 3 instead of LogLevel::Warning because we don't output warnings
125 #undef LOG
126 #define LOG(args) \
127   MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info, args)
128 #define LOG_ENABLED() \
129   MOZ_LOG_TEST(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info)
130 
131 static const char NEVER_ASK_FOR_SAVE_TO_DISK_PREF[] =
132     "browser.helperApps.neverAsk.saveToDisk";
133 static const char NEVER_ASK_FOR_OPEN_FILE_PREF[] =
134     "browser.helperApps.neverAsk.openFile";
135 
136 // Helper functions for Content-Disposition headers
137 
138 /**
139  * Given a URI fragment, unescape it
140  * @param aFragment The string to unescape
141  * @param aURI The URI from which this fragment is taken. Only its character set
142  *             will be used.
143  * @param aResult [out] Unescaped string.
144  */
UnescapeFragment(const nsACString & aFragment,nsIURI * aURI,nsAString & aResult)145 static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI,
146                                  nsAString& aResult) {
147   // We need the unescaper
148   nsresult rv;
149   nsCOMPtr<nsITextToSubURI> textToSubURI =
150       do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv);
151   NS_ENSURE_SUCCESS(rv, rv);
152 
153   return textToSubURI->UnEscapeURIForUI(aFragment, aResult);
154 }
155 
156 /**
157  * UTF-8 version of UnescapeFragment.
158  * @param aFragment The string to unescape
159  * @param aURI The URI from which this fragment is taken. Only its character set
160  *             will be used.
161  * @param aResult [out] Unescaped string, UTF-8 encoded.
162  * @note It is safe to pass the same string for aFragment and aResult.
163  * @note When this function fails, aResult will not be modified.
164  */
UnescapeFragment(const nsACString & aFragment,nsIURI * aURI,nsACString & aResult)165 static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI,
166                                  nsACString& aResult) {
167   nsAutoString result;
168   nsresult rv = UnescapeFragment(aFragment, aURI, result);
169   if (NS_SUCCEEDED(rv)) CopyUTF16toUTF8(result, aResult);
170   return rv;
171 }
172 
173 /**
174  * Given a channel, returns the filename and extension the channel has.
175  * This uses the URL and other sources (nsIMultiPartChannel).
176  * Also gives back whether the channel requested external handling (i.e.
177  * whether Content-Disposition: attachment was sent)
178  * @param aChannel The channel to extract the filename/extension from
179  * @param aFileName [out] Reference to the string where the filename should be
180  *        stored. Empty if it could not be retrieved.
181  *        WARNING - this filename may contain characters which the OS does not
182  *        allow as part of filenames!
183  * @param aExtension [out] Reference to the string where the extension should
184  *        be stored. Empty if it could not be retrieved. Stored in UTF-8.
185  * @param aAllowURLExtension (optional) Get the extension from the URL if no
186  *        Content-Disposition header is present. Default is true.
187  * @retval true The server sent Content-Disposition:attachment or equivalent
188  * @retval false Content-Disposition: inline or no content-disposition header
189  *         was sent.
190  */
GetFilenameAndExtensionFromChannel(nsIChannel * aChannel,nsString & aFileName,nsCString & aExtension,bool aAllowURLExtension=true)191 static bool GetFilenameAndExtensionFromChannel(nsIChannel* aChannel,
192                                                nsString& aFileName,
193                                                nsCString& aExtension,
194                                                bool aAllowURLExtension = true) {
195   aExtension.Truncate();
196   /*
197    * If the channel is an http or part of a multipart channel and we
198    * have a content disposition header set, then use the file name
199    * suggested there as the preferred file name to SUGGEST to the
200    * user.  we shouldn't actually use that without their
201    * permission... otherwise just use our temp file
202    */
203   bool handleExternally = false;
204   uint32_t disp;
205   nsresult rv = aChannel->GetContentDisposition(&disp);
206   bool gotFileNameFromURI = false;
207   if (NS_SUCCEEDED(rv)) {
208     aChannel->GetContentDispositionFilename(aFileName);
209     if (disp == nsIChannel::DISPOSITION_ATTACHMENT) handleExternally = true;
210   }
211 
212   // If the disposition header didn't work, try the filename from nsIURL
213   nsCOMPtr<nsIURI> uri;
214   aChannel->GetURI(getter_AddRefs(uri));
215   nsCOMPtr<nsIURL> url(do_QueryInterface(uri));
216   if (url && aFileName.IsEmpty()) {
217     if (aAllowURLExtension) {
218       url->GetFileExtension(aExtension);
219       UnescapeFragment(aExtension, url, aExtension);
220 
221       // Windows ignores terminating dots. So we have to as well, so
222       // that our security checks do "the right thing"
223       // In case the aExtension consisted only of the dot, the code below will
224       // extract an aExtension from the filename
225       aExtension.Trim(".", false);
226     }
227 
228     // try to extract the file name from the url and use that as a first pass as
229     // the leaf name of our temp file...
230     nsAutoCString leafName;
231     url->GetFileName(leafName);
232     if (!leafName.IsEmpty()) {
233       gotFileNameFromURI = true;
234       rv = UnescapeFragment(leafName, url, aFileName);
235       if (NS_FAILED(rv)) {
236         CopyUTF8toUTF16(leafName, aFileName);  // use escaped name
237       }
238     }
239   }
240 
241   // If we have a filename and no extension, remove trailing dots from the
242   // filename and extract the extension if that is possible.
243   if (aExtension.IsEmpty() && !aFileName.IsEmpty()) {
244     // Windows ignores terminating dots. So we have to as well, so
245     // that our security checks do "the right thing"
246     aFileName.Trim(".", false);
247     // We can get an extension if the filename is from a header, or if getting
248     // it from the URL was allowed.
249     bool canGetExtensionFromFilename =
250         !gotFileNameFromURI || aAllowURLExtension;
251     // ... , or if the mimetype is meaningless and we have nothing to go on:
252     if (!canGetExtensionFromFilename) {
253       nsAutoCString contentType;
254       if (NS_SUCCEEDED(aChannel->GetContentType(contentType))) {
255         canGetExtensionFromFilename =
256             contentType.EqualsIgnoreCase(APPLICATION_OCTET_STREAM) ||
257             contentType.EqualsIgnoreCase("binary/octet-stream") ||
258             contentType.EqualsIgnoreCase("application/x-msdownload");
259       }
260     }
261 
262     if (canGetExtensionFromFilename) {
263       // XXX RFindCharInReadable!!
264       nsAutoString fileNameStr(aFileName);
265       int32_t idx = fileNameStr.RFindChar(char16_t('.'));
266       if (idx != kNotFound)
267         CopyUTF16toUTF8(StringTail(fileNameStr, fileNameStr.Length() - idx - 1),
268                         aExtension);
269     }
270   }
271 
272   return handleExternally;
273 }
274 
275 /**
276  * Obtains the directory to use.  This tends to vary per platform, and
277  * needs to be consistent throughout our codepaths. For platforms where
278  * helper apps use the downloads directory, this should be kept in
279  * sync with DownloadIntegration.jsm.
280  *
281  * Optionally skip availability of the directory and storage.
282  */
GetDownloadDirectory(nsIFile ** _directory,bool aSkipChecks=false)283 static nsresult GetDownloadDirectory(nsIFile** _directory,
284                                      bool aSkipChecks = false) {
285   nsCOMPtr<nsIFile> dir;
286 #ifdef XP_MACOSX
287   // On OS X, we first try to get the users download location, if it's set.
288   switch (Preferences::GetInt(NS_PREF_DOWNLOAD_FOLDERLIST, -1)) {
289     case NS_FOLDER_VALUE_DESKTOP:
290       (void)NS_GetSpecialDirectory(NS_OS_DESKTOP_DIR, getter_AddRefs(dir));
291       break;
292     case NS_FOLDER_VALUE_CUSTOM: {
293       Preferences::GetComplex(NS_PREF_DOWNLOAD_DIR, NS_GET_IID(nsIFile),
294                               getter_AddRefs(dir));
295       if (!dir) break;
296 
297       // If we're not checking for availability we're done.
298       if (aSkipChecks) {
299         dir.forget(_directory);
300         return NS_OK;
301       }
302 
303       // We have the directory, and now we need to make sure it exists
304       bool dirExists = false;
305       (void)dir->Exists(&dirExists);
306       if (dirExists) break;
307 
308       nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755);
309       if (NS_FAILED(rv)) {
310         dir = nullptr;
311         break;
312       }
313     } break;
314     case NS_FOLDER_VALUE_DOWNLOADS:
315       // This is just the OS default location, so fall out
316       break;
317   }
318 
319   if (!dir) {
320     // If not, we default to the OS X default download location.
321     nsresult rv = NS_GetSpecialDirectory(NS_OSX_DEFAULT_DOWNLOAD_DIR,
322                                          getter_AddRefs(dir));
323     NS_ENSURE_SUCCESS(rv, rv);
324   }
325 #elif defined(ANDROID)
326   return NS_ERROR_FAILURE;
327 #else
328   // On all other platforms, we default to the systems temporary directory.
329   nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dir));
330   NS_ENSURE_SUCCESS(rv, rv);
331 
332 #  if defined(XP_UNIX)
333   // Ensuring that only the current user can read the file names we end up
334   // creating. Note that Creating directories with specified permission only
335   // supported on Unix platform right now. That's why above if exists.
336 
337   uint32_t permissions;
338   rv = dir->GetPermissions(&permissions);
339   NS_ENSURE_SUCCESS(rv, rv);
340 
341   if (permissions != PR_IRWXU) {
342     const char* userName = PR_GetEnv("USERNAME");
343     if (!userName || !*userName) {
344       userName = PR_GetEnv("USER");
345     }
346     if (!userName || !*userName) {
347       userName = PR_GetEnv("LOGNAME");
348     }
349     if (!userName || !*userName) {
350       userName = "mozillaUser";
351     }
352 
353     nsAutoString userDir;
354     userDir.AssignLiteral("mozilla_");
355     userDir.AppendASCII(userName);
356     userDir.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_');
357 
358     int counter = 0;
359     bool pathExists;
360     nsCOMPtr<nsIFile> finalPath;
361 
362     while (true) {
363       nsAutoString countedUserDir(userDir);
364       countedUserDir.AppendInt(counter, 10);
365       dir->Clone(getter_AddRefs(finalPath));
366       finalPath->Append(countedUserDir);
367 
368       rv = finalPath->Exists(&pathExists);
369       NS_ENSURE_SUCCESS(rv, rv);
370 
371       if (pathExists) {
372         // If this path has the right permissions, use it.
373         rv = finalPath->GetPermissions(&permissions);
374         NS_ENSURE_SUCCESS(rv, rv);
375 
376         // Ensuring the path is writable by the current user.
377         bool isWritable;
378         rv = finalPath->IsWritable(&isWritable);
379         NS_ENSURE_SUCCESS(rv, rv);
380 
381         if (permissions == PR_IRWXU && isWritable) {
382           dir = finalPath;
383           break;
384         }
385       }
386 
387       rv = finalPath->Create(nsIFile::DIRECTORY_TYPE, PR_IRWXU);
388       if (NS_SUCCEEDED(rv)) {
389         dir = finalPath;
390         break;
391       } else if (rv != NS_ERROR_FILE_ALREADY_EXISTS) {
392         // Unexpected error.
393         return rv;
394       }
395 
396       counter++;
397     }
398   }
399 
400 #  endif
401 #endif
402 
403   NS_ASSERTION(dir, "Somehow we didn't get a download directory!");
404   dir.forget(_directory);
405   return NS_OK;
406 }
407 
408 /**
409  * Structure for storing extension->type mappings.
410  * @see defaultMimeEntries
411  */
412 struct nsDefaultMimeTypeEntry {
413   const char* mMimeType;
414   const char* mFileExtension;
415 };
416 
417 /**
418  * Default extension->mimetype mappings. These are not overridable.
419  * If you add types here, make sure they are lowercase, or you'll regret it.
420  */
421 static const nsDefaultMimeTypeEntry defaultMimeEntries[] = {
422     // The following are those extensions that we're asked about during startup,
423     // sorted by order used
424     {IMAGE_GIF, "gif"},
425     {TEXT_XML, "xml"},
426     {APPLICATION_RDF, "rdf"},
427     {IMAGE_PNG, "png"},
428     // -- end extensions used during startup
429     {TEXT_CSS, "css"},
430     {IMAGE_JPEG, "jpeg"},
431     {IMAGE_JPEG, "jpg"},
432     {IMAGE_SVG_XML, "svg"},
433     {TEXT_HTML, "html"},
434     {TEXT_HTML, "htm"},
435     {APPLICATION_XPINSTALL, "xpi"},
436     {"application/xhtml+xml", "xhtml"},
437     {"application/xhtml+xml", "xht"},
438     {TEXT_PLAIN, "txt"},
439     {APPLICATION_JSON, "json"},
440     {APPLICATION_XJAVASCRIPT, "js"},
441     {APPLICATION_XJAVASCRIPT, "jsm"},
442     {VIDEO_OGG, "ogv"},
443     {VIDEO_OGG, "ogg"},
444     {APPLICATION_OGG, "ogg"},
445     {AUDIO_OGG, "oga"},
446     {AUDIO_OGG, "opus"},
447     {APPLICATION_PDF, "pdf"},
448     {VIDEO_WEBM, "webm"},
449     {AUDIO_WEBM, "webm"},
450     {IMAGE_ICO, "ico"},
451     {TEXT_PLAIN, "properties"},
452     {TEXT_PLAIN, "locale"},
453     {TEXT_PLAIN, "ftl"},
454 #if defined(MOZ_WMF)
455     {VIDEO_MP4, "mp4"},
456     {AUDIO_MP4, "m4a"},
457     {AUDIO_MP3, "mp3"},
458 #endif
459 #ifdef MOZ_RAW
460     {VIDEO_RAW, "yuv"}
461 #endif
462 };
463 
464 /**
465  * This is a small private struct used to help us initialize some
466  * default mime types.
467  */
468 struct nsExtraMimeTypeEntry {
469   const char* mMimeType;
470   const char* mFileExtensions;
471   const char* mDescription;
472 };
473 
474 /**
475  * This table lists all of the 'extra' content types that we can deduce from
476  * particular file extensions.  These entries also ensure that we provide a good
477  * descriptive name when we encounter files with these content types and/or
478  * extensions.  These can be overridden by user helper app prefs. If you add
479  * types here, make sure they are lowercase, or you'll regret it.
480  */
481 static const nsExtraMimeTypeEntry extraMimeEntries[] = {
482 #if defined(XP_MACOSX)  // don't define .bin on the mac...use internet config to
483                         // look that up...
484     {APPLICATION_OCTET_STREAM, "exe,com", "Binary File"},
485 #else
486     {APPLICATION_OCTET_STREAM, "exe,com,bin", "Binary File"},
487 #endif
488     {APPLICATION_GZIP2, "gz", "gzip"},
489     {"application/x-arj", "arj", "ARJ file"},
490     {"application/rtf", "rtf", "Rich Text Format File"},
491     {APPLICATION_ZIP, "zip", "ZIP Archive"},
492     {APPLICATION_XPINSTALL, "xpi", "XPInstall Install"},
493     {APPLICATION_PDF, "pdf", "Portable Document Format"},
494     {APPLICATION_POSTSCRIPT, "ps,eps,ai", "Postscript File"},
495     {APPLICATION_XJAVASCRIPT, "js", "Javascript Source File"},
496     {APPLICATION_XJAVASCRIPT, "jsm,mjs", "Javascript Module Source File"},
497 #ifdef MOZ_WIDGET_ANDROID
498     {"application/vnd.android.package-archive", "apk", "Android Package"},
499 #endif
500 
501     // OpenDocument formats
502     {"application/vnd.oasis.opendocument.text", "odt", "OpenDocument Text"},
503     {"application/vnd.oasis.opendocument.presentation", "odp",
504      "OpenDocument Presentation"},
505     {"application/vnd.oasis.opendocument.spreadsheet", "ods",
506      "OpenDocument Spreadsheet"},
507     {"application/vnd.oasis.opendocument.graphics", "odg",
508      "OpenDocument Graphics"},
509 
510     // Legacy Microsoft Office
511     {"application/msword", "doc", "Microsoft Word"},
512     {"application/vnd.ms-powerpoint", "ppt", "Microsoft PowerPoint"},
513     {"application/vnd.ms-excel", "xls", "Microsoft Excel"},
514 
515     // Office Open XML
516     {"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
517      "docx", "Microsoft Word (Open XML)"},
518     {"application/"
519      "vnd.openxmlformats-officedocument.presentationml.presentation",
520      "pptx", "Microsoft PowerPoint (Open XML)"},
521     {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
522      "xlsx", "Microsoft Excel (Open XML)"},
523 
524     // Note: if you add new image types, please also update the list in
525     // contentAreaUtils.js to match.
526     {IMAGE_ART, "art", "ART Image"},
527     {IMAGE_BMP, "bmp", "BMP Image"},
528     {IMAGE_GIF, "gif", "GIF Image"},
529     {IMAGE_ICO, "ico,cur", "ICO Image"},
530     {IMAGE_JPEG, "jpg,jpeg,jfif,pjpeg,pjp", "JPEG Image"},
531     {IMAGE_PNG, "png", "PNG Image"},
532     {IMAGE_APNG, "apng", "APNG Image"},
533     {IMAGE_TIFF, "tiff,tif", "TIFF Image"},
534     {IMAGE_XBM, "xbm", "XBM Image"},
535     {IMAGE_SVG_XML, "svg", "Scalable Vector Graphics"},
536     {IMAGE_WEBP, "webp", "WebP Image"},
537     {IMAGE_AVIF, "avif", "AV1 Image File"},
538     {IMAGE_JXL, "jxl", "JPEG XL Image File"},
539 
540     {MESSAGE_RFC822, "eml", "RFC-822 data"},
541     {TEXT_PLAIN, "txt,text", "Text File"},
542     {APPLICATION_JSON, "json", "JavaScript Object Notation"},
543     {TEXT_VTT, "vtt", "Web Video Text Tracks"},
544     {TEXT_CACHE_MANIFEST, "appcache", "Application Cache Manifest"},
545     {TEXT_HTML, "html,htm,shtml,ehtml", "HyperText Markup Language"},
546     {"application/xhtml+xml", "xhtml,xht",
547      "Extensible HyperText Markup Language"},
548     {APPLICATION_MATHML_XML, "mml", "Mathematical Markup Language"},
549     {APPLICATION_RDF, "rdf", "Resource Description Framework"},
550     {"text/csv", "csv", "CSV File"},
551     {TEXT_XML, "xml,xsl,xbl", "Extensible Markup Language"},
552     {TEXT_CSS, "css", "Style Sheet"},
553     {TEXT_VCARD, "vcf,vcard", "Contact Information"},
554     {TEXT_CALENDAR, "ics,ical,ifb,icalendar", "iCalendar"},
555     {VIDEO_OGG, "ogv", "Ogg Video"},
556     {VIDEO_OGG, "ogg", "Ogg Video"},
557     {APPLICATION_OGG, "ogg", "Ogg Video"},
558     {AUDIO_OGG, "oga", "Ogg Audio"},
559     {AUDIO_OGG, "opus", "Opus Audio"},
560     {VIDEO_WEBM, "webm", "Web Media Video"},
561     {AUDIO_WEBM, "webm", "Web Media Audio"},
562     {AUDIO_MP3, "mp3", "MPEG Audio"},
563     {VIDEO_MP4, "mp4", "MPEG-4 Video"},
564     {AUDIO_MP4, "m4a", "MPEG-4 Audio"},
565     {VIDEO_RAW, "yuv", "Raw YUV Video"},
566     {AUDIO_WAV, "wav", "Waveform Audio"},
567     {VIDEO_3GPP, "3gpp,3gp", "3GPP Video"},
568     {VIDEO_3GPP2, "3g2", "3GPP2 Video"},
569     {AUDIO_AAC, "aac", "AAC Audio"},
570     {AUDIO_FLAC, "flac", "FLAC Audio"},
571     {AUDIO_MIDI, "mid", "Standard MIDI Audio"},
572     {APPLICATION_WASM, "wasm", "WebAssembly Module"}};
573 
574 static const nsDefaultMimeTypeEntry sForbiddenPrimaryExtensions[] = {
575     {IMAGE_JPEG, "jfif"}};
576 
577 /**
578  * File extensions for which decoding should be disabled.
579  * NOTE: These MUST be lower-case and ASCII.
580  */
581 static const nsDefaultMimeTypeEntry nonDecodableExtensions[] = {
582     {APPLICATION_GZIP, "gz"},
583     {APPLICATION_GZIP, "tgz"},
584     {APPLICATION_ZIP, "zip"},
585     {APPLICATION_COMPRESS, "z"},
586     {APPLICATION_GZIP, "svgz"}};
587 
588 /**
589  * Mimetypes for which we enforce using a known extension.
590  *
591  * In addition to this list, we do this for all audio/, video/ and
592  * image/ mimetypes.
593  */
594 static const char* forcedExtensionMimetypes[] = {
595     // Note: zip and json mimetypes are commonly used with a variety of
596     // extensions; don't add them here. It's a similar story for text/xml,
597     // but slightly worse because we can use it when sniffing for a mimetype
598     // if one hasn't been provided, so don't re-add that here either.
599     APPLICATION_PDF, APPLICATION_OGG, APPLICATION_WASM,
600     TEXT_CALENDAR,   TEXT_CSS,        TEXT_VCARD};
601 
602 /**
603  * Primary extensions of types whose descriptions should be overwritten.
604  * This extension is concatenated with "ExtHandlerDescription" to look up the
605  * description in unknownContentType.properties.
606  * NOTE: These MUST be lower-case and ASCII.
607  */
608 static const char* descriptionOverwriteExtensions[] = {
609     "avif", "jxl", "pdf", "svg", "webp", "xml",
610 };
611 
612 static StaticRefPtr<nsExternalHelperAppService> sExtHelperAppSvcSingleton;
613 
614 /**
615  * On Mac child processes, return an nsOSHelperAppServiceChild for remoting
616  * OS calls to the parent process. On all other platforms use
617  * nsOSHelperAppService.
618  */
619 /* static */
620 already_AddRefed<nsExternalHelperAppService>
GetSingleton()621 nsExternalHelperAppService::GetSingleton() {
622   if (!sExtHelperAppSvcSingleton) {
623 #ifdef XP_MACOSX
624     if (XRE_IsParentProcess()) {
625       sExtHelperAppSvcSingleton = new nsOSHelperAppService();
626     } else {
627       sExtHelperAppSvcSingleton = new nsOSHelperAppServiceChild();
628     }
629 #else
630     sExtHelperAppSvcSingleton = new nsOSHelperAppService();
631 #endif /* XP_MACOSX */
632     ClearOnShutdown(&sExtHelperAppSvcSingleton);
633   }
634 
635   return do_AddRef(sExtHelperAppSvcSingleton);
636 }
637 
NS_IMPL_ISUPPORTS(nsExternalHelperAppService,nsIExternalHelperAppService,nsPIExternalAppLauncher,nsIExternalProtocolService,nsIMIMEService,nsIObserver,nsISupportsWeakReference)638 NS_IMPL_ISUPPORTS(nsExternalHelperAppService, nsIExternalHelperAppService,
639                   nsPIExternalAppLauncher, nsIExternalProtocolService,
640                   nsIMIMEService, nsIObserver, nsISupportsWeakReference)
641 
642 nsExternalHelperAppService::nsExternalHelperAppService() {}
Init()643 nsresult nsExternalHelperAppService::Init() {
644   // Add an observer for profile change
645   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
646   if (!obs) return NS_ERROR_FAILURE;
647 
648   nsresult rv = obs->AddObserver(this, "profile-before-change", true);
649   NS_ENSURE_SUCCESS(rv, rv);
650   return obs->AddObserver(this, "last-pb-context-exited", true);
651 }
652 
~nsExternalHelperAppService()653 nsExternalHelperAppService::~nsExternalHelperAppService() {}
654 
DoContentContentProcessHelper(const nsACString & aMimeContentType,nsIRequest * aRequest,BrowsingContext * aContentContext,bool aForceSave,nsIInterfaceRequestor * aWindowContext,nsIStreamListener ** aStreamListener)655 nsresult nsExternalHelperAppService::DoContentContentProcessHelper(
656     const nsACString& aMimeContentType, nsIRequest* aRequest,
657     BrowsingContext* aContentContext, bool aForceSave,
658     nsIInterfaceRequestor* aWindowContext,
659     nsIStreamListener** aStreamListener) {
660   // We need to get a hold of a ContentChild so that we can begin forwarding
661   // this data to the parent.  In the HTTP case, this is unfortunate, since
662   // we're actually passing data from parent->child->parent wastefully, but
663   // the Right Fix will eventually be to short-circuit those channels on the
664   // parent side based on some sort of subscription concept.
665   using mozilla::dom::ContentChild;
666   using mozilla::dom::ExternalHelperAppChild;
667   ContentChild* child = ContentChild::GetSingleton();
668   if (!child) {
669     return NS_ERROR_FAILURE;
670   }
671 
672   nsCString disp;
673   nsCOMPtr<nsIURI> uri;
674   int64_t contentLength = -1;
675   bool wasFileChannel = false;
676   uint32_t contentDisposition = -1;
677   nsAutoString fileName;
678   nsCOMPtr<nsILoadInfo> loadInfo;
679 
680   nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
681   if (channel) {
682     channel->GetURI(getter_AddRefs(uri));
683     channel->GetContentLength(&contentLength);
684     channel->GetContentDisposition(&contentDisposition);
685     channel->GetContentDispositionFilename(fileName);
686     channel->GetContentDispositionHeader(disp);
687     loadInfo = channel->LoadInfo();
688 
689     nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aRequest));
690     wasFileChannel = fileChan != nullptr;
691   }
692 
693   nsCOMPtr<nsIURI> referrer;
694   NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer));
695 
696   Maybe<mozilla::net::LoadInfoArgs> loadInfoArgs;
697   MOZ_ALWAYS_SUCCEEDS(LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs));
698 
699   nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aRequest));
700   // Determine whether a new window was opened specifically for this request
701   bool shouldCloseWindow = false;
702   if (props) {
703     props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns,
704                              &shouldCloseWindow);
705   }
706 
707   // Now we build a protocol for forwarding our data to the parent.  The
708   // protocol will act as a listener on the child-side and create a "real"
709   // helperAppService listener on the parent-side, via another call to
710   // DoContent.
711   RefPtr<ExternalHelperAppChild> childListener = new ExternalHelperAppChild();
712   MOZ_ALWAYS_TRUE(child->SendPExternalHelperAppConstructor(
713       childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp,
714       contentDisposition, fileName, aForceSave, contentLength, wasFileChannel,
715       referrer, aContentContext, shouldCloseWindow));
716 
717   NS_ADDREF(*aStreamListener = childListener);
718 
719   uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
720 
721   RefPtr<nsExternalAppHandler> handler =
722       new nsExternalAppHandler(nullptr, ""_ns, aContentContext, aWindowContext,
723                                this, fileName, reason, aForceSave);
724   if (!handler) {
725     return NS_ERROR_OUT_OF_MEMORY;
726   }
727 
728   childListener->SetHandler(handler);
729   return NS_OK;
730 }
731 
CreateListener(const nsACString & aMimeContentType,nsIRequest * aRequest,BrowsingContext * aContentContext,bool aForceSave,nsIInterfaceRequestor * aWindowContext,nsIStreamListener ** aStreamListener)732 NS_IMETHODIMP nsExternalHelperAppService::CreateListener(
733     const nsACString& aMimeContentType, nsIRequest* aRequest,
734     BrowsingContext* aContentContext, bool aForceSave,
735     nsIInterfaceRequestor* aWindowContext,
736     nsIStreamListener** aStreamListener) {
737   MOZ_ASSERT(!XRE_IsContentProcess());
738 
739   nsAutoString fileName;
740   nsAutoCString fileExtension;
741   uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
742   uint32_t contentDisposition = -1;
743 
744   // Get the file extension and name that we will need later
745   nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
746   nsCOMPtr<nsIURI> uri;
747   int64_t contentLength = -1;
748   if (channel) {
749     channel->GetURI(getter_AddRefs(uri));
750     channel->GetContentLength(&contentLength);
751     channel->GetContentDisposition(&contentDisposition);
752     channel->GetContentDispositionFilename(fileName);
753 
754     // Check if we have a POST request, in which case we don't want to use
755     // the url's extension
756     bool allowURLExt = !net::ChannelIsPost(channel);
757 
758     // Check if we had a query string - we don't want to check the URL
759     // extension if a query is present in the URI
760     // If we already know we don't want to check the URL extension, don't
761     // bother checking the query
762     if (uri && allowURLExt) {
763       nsCOMPtr<nsIURL> url = do_QueryInterface(uri);
764 
765       if (url) {
766         nsAutoCString query;
767 
768         // We only care about the query for HTTP and HTTPS URLs
769         if (uri->SchemeIs("http") || uri->SchemeIs("https")) {
770           url->GetQuery(query);
771         }
772 
773         // Only get the extension if the query is empty; if it isn't, then the
774         // extension likely belongs to a cgi script and isn't helpful
775         allowURLExt = query.IsEmpty();
776       }
777     }
778     // Extract name & extension
779     bool isAttachment = GetFilenameAndExtensionFromChannel(
780         channel, fileName, fileExtension, allowURLExt);
781     LOG(("Found extension '%s' (filename is '%s', handling attachment: %i)",
782          fileExtension.get(), NS_ConvertUTF16toUTF8(fileName).get(),
783          isAttachment));
784     if (isAttachment) {
785       reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST;
786     }
787   }
788 
789   LOG(("HelperAppService::DoContent: mime '%s', extension '%s'\n",
790        PromiseFlatCString(aMimeContentType).get(), fileExtension.get()));
791 
792   // We get the mime service here even though we're the default implementation
793   // of it, so it's possible to override only the mime service and not need to
794   // reimplement the whole external helper app service itself.
795   nsCOMPtr<nsIMIMEService> mimeSvc(do_GetService(NS_MIMESERVICE_CONTRACTID));
796   NS_ENSURE_TRUE(mimeSvc, NS_ERROR_FAILURE);
797 
798   // Try to find a mime object by looking at the mime type/extension
799   nsCOMPtr<nsIMIMEInfo> mimeInfo;
800   if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT,
801                               nsCaseInsensitiveCStringComparator)) {
802     nsAutoCString mimeType;
803     if (!fileExtension.IsEmpty()) {
804       mimeSvc->GetFromTypeAndExtension(""_ns, fileExtension,
805                                        getter_AddRefs(mimeInfo));
806       if (mimeInfo) {
807         mimeInfo->GetMIMEType(mimeType);
808 
809         LOG(("OS-Provided mime type '%s' for extension '%s'\n", mimeType.get(),
810              fileExtension.get()));
811       }
812     }
813 
814     if (fileExtension.IsEmpty() || mimeType.IsEmpty()) {
815       // Extension lookup gave us no useful match
816       mimeSvc->GetFromTypeAndExtension(
817           nsLiteralCString(APPLICATION_OCTET_STREAM), fileExtension,
818           getter_AddRefs(mimeInfo));
819       mimeType.AssignLiteral(APPLICATION_OCTET_STREAM);
820     }
821 
822     if (channel) {
823       channel->SetContentType(mimeType);
824     }
825 
826     // Don't overwrite SERVERREQUEST
827     if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
828       reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
829     }
830   } else {
831     mimeSvc->GetFromTypeAndExtension(aMimeContentType, fileExtension,
832                                      getter_AddRefs(mimeInfo));
833   }
834   LOG(("Type/Ext lookup found 0x%p\n", mimeInfo.get()));
835 
836   // No mimeinfo -> we can't continue. probably OOM.
837   if (!mimeInfo) {
838     return NS_ERROR_OUT_OF_MEMORY;
839   }
840 
841   *aStreamListener = nullptr;
842   // We want the mimeInfo's primary extension to pass it to
843   // nsExternalAppHandler
844   nsAutoCString buf;
845   mimeInfo->GetPrimaryExtension(buf);
846 
847   // NB: ExternalHelperAppParent depends on this listener always being an
848   // nsExternalAppHandler. If this changes, make sure to update that code.
849   nsExternalAppHandler* handler =
850       new nsExternalAppHandler(mimeInfo, buf, aContentContext, aWindowContext,
851                                this, fileName, reason, aForceSave);
852   if (!handler) {
853     return NS_ERROR_OUT_OF_MEMORY;
854   }
855 
856   NS_ADDREF(*aStreamListener = handler);
857   return NS_OK;
858 }
859 
DoContent(const nsACString & aMimeContentType,nsIRequest * aRequest,nsIInterfaceRequestor * aContentContext,bool aForceSave,nsIInterfaceRequestor * aWindowContext,nsIStreamListener ** aStreamListener)860 NS_IMETHODIMP nsExternalHelperAppService::DoContent(
861     const nsACString& aMimeContentType, nsIRequest* aRequest,
862     nsIInterfaceRequestor* aContentContext, bool aForceSave,
863     nsIInterfaceRequestor* aWindowContext,
864     nsIStreamListener** aStreamListener) {
865   // Scripted interface requestors cannot return an instance of the
866   // (non-scriptable) nsPIDOMWindowOuter or nsPIDOMWindowInner interfaces, so
867   // get to the window via `nsIDOMWindow`.  Unfortunately, at that point we
868   // don't know whether the thing we got is an inner or outer window, so have to
869   // work with either one.
870   RefPtr<BrowsingContext> bc;
871   nsCOMPtr<nsIDOMWindow> domWindow = do_GetInterface(aContentContext);
872   if (nsCOMPtr<nsPIDOMWindowOuter> outerWindow = do_QueryInterface(domWindow)) {
873     bc = outerWindow->GetBrowsingContext();
874   } else if (nsCOMPtr<nsPIDOMWindowInner> innerWindow =
875                  do_QueryInterface(domWindow)) {
876     bc = innerWindow->GetBrowsingContext();
877   }
878 
879   if (XRE_IsContentProcess()) {
880     return DoContentContentProcessHelper(aMimeContentType, aRequest, bc,
881                                          aForceSave, aWindowContext,
882                                          aStreamListener);
883   }
884 
885   nsresult rv = CreateListener(aMimeContentType, aRequest, bc, aForceSave,
886                                aWindowContext, aStreamListener);
887   return rv;
888 }
889 
ApplyDecodingForExtension(const nsACString & aExtension,const nsACString & aEncodingType,bool * aApplyDecoding)890 NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension(
891     const nsACString& aExtension, const nsACString& aEncodingType,
892     bool* aApplyDecoding) {
893   *aApplyDecoding = true;
894   uint32_t i;
895   for (i = 0; i < ArrayLength(nonDecodableExtensions); ++i) {
896     if (aExtension.LowerCaseEqualsASCII(
897             nonDecodableExtensions[i].mFileExtension) &&
898         aEncodingType.LowerCaseEqualsASCII(
899             nonDecodableExtensions[i].mMimeType)) {
900       *aApplyDecoding = false;
901       break;
902     }
903   }
904   return NS_OK;
905 }
906 
GetFileTokenForPath(const char16_t * aPlatformAppPath,nsIFile ** aFile)907 nsresult nsExternalHelperAppService::GetFileTokenForPath(
908     const char16_t* aPlatformAppPath, nsIFile** aFile) {
909   nsDependentString platformAppPath(aPlatformAppPath);
910   // First, check if we have an absolute path
911   nsIFile* localFile = nullptr;
912   nsresult rv = NS_NewLocalFile(platformAppPath, true, &localFile);
913   if (NS_SUCCEEDED(rv)) {
914     *aFile = localFile;
915     bool exists;
916     if (NS_FAILED((*aFile)->Exists(&exists)) || !exists) {
917       NS_RELEASE(*aFile);
918       return NS_ERROR_FILE_NOT_FOUND;
919     }
920     return NS_OK;
921   }
922 
923   // Second, check if file exists in mozilla program directory
924   rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, aFile);
925   if (NS_SUCCEEDED(rv)) {
926     rv = (*aFile)->Append(platformAppPath);
927     if (NS_SUCCEEDED(rv)) {
928       bool exists = false;
929       rv = (*aFile)->Exists(&exists);
930       if (NS_SUCCEEDED(rv) && exists) return NS_OK;
931     }
932     NS_RELEASE(*aFile);
933   }
934 
935   return NS_ERROR_NOT_AVAILABLE;
936 }
937 
938 //////////////////////////////////////////////////////////////////////////////////////////////////////
939 // begin external protocol service default implementation...
940 //////////////////////////////////////////////////////////////////////////////////////////////////////
ExternalProtocolHandlerExists(const char * aProtocolScheme,bool * aHandlerExists)941 NS_IMETHODIMP nsExternalHelperAppService::ExternalProtocolHandlerExists(
942     const char* aProtocolScheme, bool* aHandlerExists) {
943   nsCOMPtr<nsIHandlerInfo> handlerInfo;
944   nsresult rv = GetProtocolHandlerInfo(nsDependentCString(aProtocolScheme),
945                                        getter_AddRefs(handlerInfo));
946   if (NS_SUCCEEDED(rv)) {
947     // See if we have any known possible handler apps for this
948     nsCOMPtr<nsIMutableArray> possibleHandlers;
949     handlerInfo->GetPossibleApplicationHandlers(
950         getter_AddRefs(possibleHandlers));
951 
952     uint32_t length;
953     possibleHandlers->GetLength(&length);
954     if (length) {
955       *aHandlerExists = true;
956       return NS_OK;
957     }
958   }
959 
960   // if not, fall back on an os-based handler
961   return OSProtocolHandlerExists(aProtocolScheme, aHandlerExists);
962 }
963 
IsExposedProtocol(const char * aProtocolScheme,bool * aResult)964 NS_IMETHODIMP nsExternalHelperAppService::IsExposedProtocol(
965     const char* aProtocolScheme, bool* aResult) {
966   // check the per protocol setting first.  it always takes precedence.
967   // if not set, then use the global setting.
968 
969   nsAutoCString prefName("network.protocol-handler.expose.");
970   prefName += aProtocolScheme;
971   bool val;
972   if (NS_SUCCEEDED(Preferences::GetBool(prefName.get(), &val))) {
973     *aResult = val;
974     return NS_OK;
975   }
976 
977   // by default, no protocol is exposed.  i.e., by default all link clicks must
978   // go through the external protocol service.  most applications override this
979   // default behavior.
980   *aResult = Preferences::GetBool("network.protocol-handler.expose-all", false);
981 
982   return NS_OK;
983 }
984 
985 static const char kExternalProtocolPrefPrefix[] =
986     "network.protocol-handler.external.";
987 static const char kExternalProtocolDefaultPref[] =
988     "network.protocol-handler.external-default";
989 
990 // static
EscapeURI(nsIURI * aURI,nsIURI ** aResult)991 nsresult nsExternalHelperAppService::EscapeURI(nsIURI* aURI, nsIURI** aResult) {
992   MOZ_ASSERT(aURI);
993   MOZ_ASSERT(aResult);
994 
995   nsAutoCString spec;
996   aURI->GetSpec(spec);
997 
998   if (spec.Find("%00") != -1) return NS_ERROR_MALFORMED_URI;
999 
1000   nsAutoCString escapedSpec;
1001   nsresult rv = NS_EscapeURL(spec, esc_AlwaysCopy | esc_ExtHandler, escapedSpec,
1002                              fallible);
1003   NS_ENSURE_SUCCESS(rv, rv);
1004 
1005   nsCOMPtr<nsIIOService> ios(do_GetIOService());
1006   return ios->NewURI(escapedSpec, nullptr, nullptr, aResult);
1007 }
1008 
1009 NS_IMETHODIMP
LoadURI(nsIURI * aURI,nsIPrincipal * aTriggeringPrincipal,nsIPrincipal * aRedirectPrincipal,BrowsingContext * aBrowsingContext,bool aTriggeredExternally)1010 nsExternalHelperAppService::LoadURI(nsIURI* aURI,
1011                                     nsIPrincipal* aTriggeringPrincipal,
1012                                     nsIPrincipal* aRedirectPrincipal,
1013                                     BrowsingContext* aBrowsingContext,
1014                                     bool aTriggeredExternally) {
1015   NS_ENSURE_ARG_POINTER(aURI);
1016 
1017   if (XRE_IsContentProcess()) {
1018     mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal(
1019         aURI, aTriggeringPrincipal, aRedirectPrincipal, aBrowsingContext,
1020         aTriggeredExternally);
1021     return NS_OK;
1022   }
1023 
1024   nsCOMPtr<nsIURI> escapedURI;
1025   nsresult rv = EscapeURI(aURI, getter_AddRefs(escapedURI));
1026   NS_ENSURE_SUCCESS(rv, rv);
1027 
1028   nsAutoCString scheme;
1029   escapedURI->GetScheme(scheme);
1030   if (scheme.IsEmpty()) return NS_OK;  // must have a scheme
1031 
1032   // Deny load if the prefs say to do so
1033   nsAutoCString externalPref(kExternalProtocolPrefPrefix);
1034   externalPref += scheme;
1035   bool allowLoad = false;
1036   if (NS_FAILED(Preferences::GetBool(externalPref.get(), &allowLoad))) {
1037     // no scheme-specific value, check the default
1038     if (NS_FAILED(
1039             Preferences::GetBool(kExternalProtocolDefaultPref, &allowLoad))) {
1040       return NS_OK;  // missing default pref
1041     }
1042   }
1043 
1044   if (!allowLoad) {
1045     return NS_OK;  // explicitly denied
1046   }
1047 
1048   // Now check if the principal is allowed to access the navigated context.
1049   // We allow navigating subframes, even if not same-origin - non-external
1050   // links can always navigate everywhere, so this is a minor additional
1051   // restriction, only aiming to prevent some types of spoofing attacks
1052   // from otherwise disjoint browsingcontext trees.
1053   if (aBrowsingContext && aTriggeringPrincipal &&
1054       !StaticPrefs::security_allow_disjointed_external_uri_loads() &&
1055       // Add-on principals are always allowed:
1056       !BasePrincipal::Cast(aTriggeringPrincipal)->AddonPolicy() &&
1057       // As is chrome code:
1058       !aTriggeringPrincipal->IsSystemPrincipal()) {
1059     RefPtr<BrowsingContext> bc = aBrowsingContext;
1060     WindowGlobalParent* wgp = bc->Canonical()->GetCurrentWindowGlobal();
1061     bool foundAccessibleFrame = false;
1062 
1063     // Also allow this load if the target is a toplevel BC and contains a
1064     // non-web-controlled about:blank document
1065     if (bc->IsTop() && !bc->HadOriginalOpener() && wgp) {
1066       RefPtr<nsIURI> uri = wgp->GetDocumentURI();
1067       foundAccessibleFrame =
1068           uri && uri->GetSpecOrDefault().EqualsLiteral("about:blank");
1069     }
1070 
1071     while (!foundAccessibleFrame) {
1072       if (wgp) {
1073         foundAccessibleFrame =
1074             aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal());
1075       }
1076       // We have to get the parent via the bc, because there may not
1077       // be a window global for the innermost bc; see bug 1650162.
1078       BrowsingContext* parent = bc->GetParent();
1079       if (!parent) {
1080         break;
1081       }
1082       bc = parent;
1083       wgp = parent->Canonical()->GetCurrentWindowGlobal();
1084     }
1085 
1086     if (!foundAccessibleFrame) {
1087       // See if this navigation could have come from a subframe.
1088       nsTArray<RefPtr<BrowsingContext>> contexts;
1089       aBrowsingContext->GetAllBrowsingContextsInSubtree(contexts);
1090       for (const auto& kid : contexts) {
1091         wgp = kid->Canonical()->GetCurrentWindowGlobal();
1092         if (wgp && aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal())) {
1093           foundAccessibleFrame = true;
1094           break;
1095         }
1096       }
1097     }
1098 
1099     if (!foundAccessibleFrame) {
1100       return NS_OK;  // deny the load.
1101     }
1102   }
1103 
1104   nsCOMPtr<nsIHandlerInfo> handler;
1105   rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler));
1106   NS_ENSURE_SUCCESS(rv, rv);
1107 
1108   nsCOMPtr<nsIContentDispatchChooser> chooser =
1109       do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv);
1110   NS_ENSURE_SUCCESS(rv, rv);
1111 
1112   return chooser->HandleURI(
1113       handler, escapedURI,
1114       aRedirectPrincipal ? aRedirectPrincipal : aTriggeringPrincipal,
1115       aBrowsingContext, aTriggeredExternally);
1116 }
1117 
1118 //////////////////////////////////////////////////////////////////////////////////////////////////////
1119 // Methods related to deleting temporary files on exit
1120 //////////////////////////////////////////////////////////////////////////////////////////////////////
1121 
1122 /* static */
DeleteTemporaryFileHelper(nsIFile * aTemporaryFile,nsCOMArray<nsIFile> & aFileList)1123 nsresult nsExternalHelperAppService::DeleteTemporaryFileHelper(
1124     nsIFile* aTemporaryFile, nsCOMArray<nsIFile>& aFileList) {
1125   bool isFile = false;
1126 
1127   // as a safety measure, make sure the nsIFile is really a file and not a
1128   // directory object.
1129   aTemporaryFile->IsFile(&isFile);
1130   if (!isFile) return NS_OK;
1131 
1132   aFileList.AppendObject(aTemporaryFile);
1133 
1134   return NS_OK;
1135 }
1136 
1137 NS_IMETHODIMP
DeleteTemporaryFileOnExit(nsIFile * aTemporaryFile)1138 nsExternalHelperAppService::DeleteTemporaryFileOnExit(nsIFile* aTemporaryFile) {
1139   return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryFilesList);
1140 }
1141 
1142 NS_IMETHODIMP
DeleteTemporaryPrivateFileWhenPossible(nsIFile * aTemporaryFile)1143 nsExternalHelperAppService::DeleteTemporaryPrivateFileWhenPossible(
1144     nsIFile* aTemporaryFile) {
1145   return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryPrivateFilesList);
1146 }
1147 
ExpungeTemporaryFilesHelper(nsCOMArray<nsIFile> & fileList)1148 void nsExternalHelperAppService::ExpungeTemporaryFilesHelper(
1149     nsCOMArray<nsIFile>& fileList) {
1150   int32_t numEntries = fileList.Count();
1151   nsIFile* localFile;
1152   for (int32_t index = 0; index < numEntries; index++) {
1153     localFile = fileList[index];
1154     if (localFile) {
1155       // First make the file writable, since the temp file is probably readonly.
1156       localFile->SetPermissions(0600);
1157       localFile->Remove(false);
1158     }
1159   }
1160 
1161   fileList.Clear();
1162 }
1163 
ExpungeTemporaryFiles()1164 void nsExternalHelperAppService::ExpungeTemporaryFiles() {
1165   ExpungeTemporaryFilesHelper(mTemporaryFilesList);
1166 }
1167 
ExpungeTemporaryPrivateFiles()1168 void nsExternalHelperAppService::ExpungeTemporaryPrivateFiles() {
1169   ExpungeTemporaryFilesHelper(mTemporaryPrivateFilesList);
1170 }
1171 
1172 static const char kExternalWarningPrefPrefix[] =
1173     "network.protocol-handler.warn-external.";
1174 static const char kExternalWarningDefaultPref[] =
1175     "network.protocol-handler.warn-external-default";
1176 
1177 NS_IMETHODIMP
GetProtocolHandlerInfo(const nsACString & aScheme,nsIHandlerInfo ** aHandlerInfo)1178 nsExternalHelperAppService::GetProtocolHandlerInfo(
1179     const nsACString& aScheme, nsIHandlerInfo** aHandlerInfo) {
1180   // XXX enterprise customers should be able to turn this support off with a
1181   // single master pref (maybe use one of the "exposed" prefs here?)
1182 
1183   bool exists;
1184   nsresult rv = GetProtocolHandlerInfoFromOS(aScheme, &exists, aHandlerInfo);
1185   if (NS_FAILED(rv)) {
1186     // Either it knows nothing, or we ran out of memory
1187     return NS_ERROR_FAILURE;
1188   }
1189 
1190   nsCOMPtr<nsIHandlerService> handlerSvc =
1191       do_GetService(NS_HANDLERSERVICE_CONTRACTID);
1192   if (handlerSvc) {
1193     bool hasHandler = false;
1194     (void)handlerSvc->Exists(*aHandlerInfo, &hasHandler);
1195     if (hasHandler) {
1196       rv = handlerSvc->FillHandlerInfo(*aHandlerInfo, ""_ns);
1197       if (NS_SUCCEEDED(rv)) return NS_OK;
1198     }
1199   }
1200 
1201   return SetProtocolHandlerDefaults(*aHandlerInfo, exists);
1202 }
1203 
1204 NS_IMETHODIMP
SetProtocolHandlerDefaults(nsIHandlerInfo * aHandlerInfo,bool aOSHandlerExists)1205 nsExternalHelperAppService::SetProtocolHandlerDefaults(
1206     nsIHandlerInfo* aHandlerInfo, bool aOSHandlerExists) {
1207   // this type isn't in our database, so we've only got an OS default handler,
1208   // if one exists
1209 
1210   if (aOSHandlerExists) {
1211     // we've got a default, so use it
1212     aHandlerInfo->SetPreferredAction(nsIHandlerInfo::useSystemDefault);
1213 
1214     // whether or not to ask the user depends on the warning preference
1215     nsAutoCString scheme;
1216     aHandlerInfo->GetType(scheme);
1217 
1218     nsAutoCString warningPref(kExternalWarningPrefPrefix);
1219     warningPref += scheme;
1220     bool warn;
1221     if (NS_FAILED(Preferences::GetBool(warningPref.get(), &warn))) {
1222       // no scheme-specific value, check the default
1223       warn = Preferences::GetBool(kExternalWarningDefaultPref, true);
1224     }
1225     aHandlerInfo->SetAlwaysAskBeforeHandling(warn);
1226   } else {
1227     // If no OS default existed, we set the preferred action to alwaysAsk.
1228     // This really means not initialized (i.e. there's no available handler)
1229     // to all the code...
1230     aHandlerInfo->SetPreferredAction(nsIHandlerInfo::alwaysAsk);
1231   }
1232 
1233   return NS_OK;
1234 }
1235 
1236 // XPCOM profile change observer
1237 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * someData)1238 nsExternalHelperAppService::Observe(nsISupports* aSubject, const char* aTopic,
1239                                     const char16_t* someData) {
1240   if (!strcmp(aTopic, "profile-before-change")) {
1241     ExpungeTemporaryFiles();
1242   } else if (!strcmp(aTopic, "last-pb-context-exited")) {
1243     ExpungeTemporaryPrivateFiles();
1244   }
1245   return NS_OK;
1246 }
1247 
1248 //////////////////////////////////////////////////////////////////////////////////////////////////////
1249 // begin external app handler implementation
1250 //////////////////////////////////////////////////////////////////////////////////////////////////////
1251 
1252 NS_IMPL_ADDREF(nsExternalAppHandler)
NS_IMPL_RELEASE(nsExternalAppHandler)1253 NS_IMPL_RELEASE(nsExternalAppHandler)
1254 
1255 NS_INTERFACE_MAP_BEGIN(nsExternalAppHandler)
1256   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener)
1257   NS_INTERFACE_MAP_ENTRY(nsIStreamListener)
1258   NS_INTERFACE_MAP_ENTRY(nsIRequestObserver)
1259   NS_INTERFACE_MAP_ENTRY(nsIHelperAppLauncher)
1260   NS_INTERFACE_MAP_ENTRY(nsICancelable)
1261   NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver)
1262   NS_INTERFACE_MAP_ENTRY(nsINamed)
1263   NS_INTERFACE_MAP_ENTRY_CONCRETE(nsExternalAppHandler)
1264 NS_INTERFACE_MAP_END
1265 
1266 nsExternalAppHandler::nsExternalAppHandler(
1267     nsIMIMEInfo* aMIMEInfo, const nsACString& aTempFileExtension,
1268     BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext,
1269     nsExternalHelperAppService* aExtProtSvc,
1270     const nsAString& aSuggestedFilename, uint32_t aReason, bool aForceSave)
1271     : mMimeInfo(aMIMEInfo),
1272       mBrowsingContext(aBrowsingContext),
1273       mWindowContext(aWindowContext),
1274       mSuggestedFileName(aSuggestedFilename),
1275       mForceSave(aForceSave),
1276       mCanceled(false),
1277       mStopRequestIssued(false),
1278       mIsFileChannel(false),
1279       mShouldCloseWindow(false),
1280       mHandleInternally(false),
1281       mReason(aReason),
1282       mTempFileIsExecutable(false),
1283       mTimeDownloadStarted(0),
1284       mContentLength(-1),
1285       mProgress(0),
1286       mSaver(nullptr),
1287       mDialogProgressListener(nullptr),
1288       mTransfer(nullptr),
1289       mRequest(nullptr),
1290       mExtProtSvc(aExtProtSvc) {
1291   // make sure the extention includes the '.'
1292   if (!aTempFileExtension.IsEmpty() && aTempFileExtension.First() != '.')
1293     mTempFileExtension = char16_t('.');
1294   AppendUTF8toUTF16(aTempFileExtension, mTempFileExtension);
1295 
1296   // Get mSuggestedFileName's current file extension.
1297   nsAutoString originalFileExt;
1298   int32_t pos = mSuggestedFileName.RFindChar('.');
1299   if (pos != kNotFound) {
1300     mSuggestedFileName.Right(originalFileExt,
1301                              mSuggestedFileName.Length() - pos);
1302   }
1303 
1304   // replace platform specific path separator and illegal characters to avoid
1305   // any confusion.
1306   // Try to keep the use of spaces or underscores in sync with the Downloads
1307   // code sanitization in DownloadPaths.jsm
1308   mSuggestedFileName.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
1309   mSuggestedFileName.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' ');
1310   mSuggestedFileName.ReplaceChar(char16_t(0), '_');
1311   mTempFileExtension.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
1312   mTempFileExtension.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' ');
1313 
1314   // Remove unsafe bidi characters which might have spoofing implications (bug
1315   // 511521).
1316   const char16_t unsafeBidiCharacters[] = {
1317       char16_t(0x061c),  // Arabic Letter Mark
1318       char16_t(0x200e),  // Left-to-Right Mark
1319       char16_t(0x200f),  // Right-to-Left Mark
1320       char16_t(0x202a),  // Left-to-Right Embedding
1321       char16_t(0x202b),  // Right-to-Left Embedding
1322       char16_t(0x202c),  // Pop Directional Formatting
1323       char16_t(0x202d),  // Left-to-Right Override
1324       char16_t(0x202e),  // Right-to-Left Override
1325       char16_t(0x2066),  // Left-to-Right Isolate
1326       char16_t(0x2067),  // Right-to-Left Isolate
1327       char16_t(0x2068),  // First Strong Isolate
1328       char16_t(0x2069),  // Pop Directional Isolate
1329       char16_t(0)};
1330   mSuggestedFileName.ReplaceChar(unsafeBidiCharacters, '_');
1331   mTempFileExtension.ReplaceChar(unsafeBidiCharacters, '_');
1332 
1333   // Remove trailing or leading spaces that we may have generated while
1334   // sanitizing.
1335   mSuggestedFileName.CompressWhitespace();
1336   mTempFileExtension.CompressWhitespace();
1337 
1338   EnsureCorrectExtension(originalFileExt);
1339 
1340   mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096);
1341 }
1342 
~nsExternalAppHandler()1343 nsExternalAppHandler::~nsExternalAppHandler() {
1344   MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted");
1345 }
1346 
ShouldForceExtension(const nsString & aFileExt)1347 bool nsExternalAppHandler::ShouldForceExtension(const nsString& aFileExt) {
1348   nsAutoCString MIMEType;
1349   if (!mMimeInfo || NS_FAILED(mMimeInfo->GetMIMEType(MIMEType))) {
1350     return false;
1351   }
1352 
1353   bool canForce = StringBeginsWith(MIMEType, "image/"_ns) ||
1354                   StringBeginsWith(MIMEType, "audio/"_ns) ||
1355                   StringBeginsWith(MIMEType, "video/"_ns);
1356 
1357   if (!canForce &&
1358       StaticPrefs::browser_download_sanitize_non_media_extensions()) {
1359     for (const char* mime : forcedExtensionMimetypes) {
1360       if (MIMEType.Equals(mime)) {
1361         canForce = true;
1362         break;
1363       }
1364     }
1365   }
1366   if (!canForce) {
1367     return false;
1368   }
1369 
1370   // If we get here, we know for sure the mimetype allows us to overwrite the
1371   // existing extension, if it's wrong. Return whether the extension is wrong:
1372 
1373   bool knownExtension = false;
1374   // Note that aFileExt is either empty or consists of an extension
1375   // *including the dot* which we remove for ExtensionExists().
1376   return (
1377       aFileExt.IsEmpty() || aFileExt.EqualsLiteral(".") ||
1378       (NS_SUCCEEDED(mMimeInfo->ExtensionExists(
1379            Substring(NS_ConvertUTF16toUTF8(aFileExt), 1), &knownExtension)) &&
1380        !knownExtension));
1381 }
1382 
EnsureCorrectExtension(const nsString & aFileExt)1383 void nsExternalAppHandler::EnsureCorrectExtension(const nsString& aFileExt) {
1384   // If we don't have an extension (which will include the .),
1385   // just short-circuit.
1386   if (mTempFileExtension.Length() <= 1) {
1387     return;
1388   }
1389 
1390   // After removing trailing whitespaces from the name, if we have a
1391   // temp file extension, there are broadly 2 cases where we want to
1392   // replace the extension.
1393   // First, if the file extension contains invalid characters.
1394   // Second, for document type mimetypes, if the extension is either
1395   // missing or not valid for this mimetype.
1396   bool replaceExtension =
1397       (aFileExt.FindCharInSet(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS) !=
1398        kNotFound) ||
1399       ShouldForceExtension(aFileExt);
1400 
1401   if (replaceExtension) {
1402     int32_t pos = mSuggestedFileName.RFindChar('.');
1403     if (pos != kNotFound) {
1404       mSuggestedFileName =
1405           Substring(mSuggestedFileName, 0, pos) + mTempFileExtension;
1406     } else {
1407       mSuggestedFileName.Append(mTempFileExtension);
1408     }
1409   }
1410 
1411   /*
1412    * Ensure we don't double-append the file extension if it matches:
1413    */
1414   if (replaceExtension ||
1415       aFileExt.Equals(mTempFileExtension, nsCaseInsensitiveStringComparator)) {
1416     // Matches -> mTempFileExtension can be empty
1417     mTempFileExtension.Truncate();
1418   }
1419 }
1420 
DidDivertRequest(nsIRequest * request)1421 void nsExternalAppHandler::DidDivertRequest(nsIRequest* request) {
1422   MOZ_ASSERT(XRE_IsContentProcess(), "in child process");
1423   // Remove our request from the child loadGroup
1424   RetargetLoadNotifications(request);
1425 }
1426 
SetWebProgressListener(nsIWebProgressListener2 * aWebProgressListener)1427 NS_IMETHODIMP nsExternalAppHandler::SetWebProgressListener(
1428     nsIWebProgressListener2* aWebProgressListener) {
1429   // This is always called by nsHelperDlg.js. Go ahead and register the
1430   // progress listener. At this point, we don't have mTransfer.
1431   mDialogProgressListener = aWebProgressListener;
1432   return NS_OK;
1433 }
1434 
GetTargetFile(nsIFile ** aTarget)1435 NS_IMETHODIMP nsExternalAppHandler::GetTargetFile(nsIFile** aTarget) {
1436   if (mFinalFileDestination)
1437     *aTarget = mFinalFileDestination;
1438   else
1439     *aTarget = mTempFile;
1440 
1441   NS_IF_ADDREF(*aTarget);
1442   return NS_OK;
1443 }
1444 
GetTargetFileIsExecutable(bool * aExec)1445 NS_IMETHODIMP nsExternalAppHandler::GetTargetFileIsExecutable(bool* aExec) {
1446   // Use the real target if it's been set
1447   if (mFinalFileDestination) return mFinalFileDestination->IsExecutable(aExec);
1448 
1449   // Otherwise, use the stored executable-ness of the temporary
1450   *aExec = mTempFileIsExecutable;
1451   return NS_OK;
1452 }
1453 
GetTimeDownloadStarted(PRTime * aTime)1454 NS_IMETHODIMP nsExternalAppHandler::GetTimeDownloadStarted(PRTime* aTime) {
1455   *aTime = mTimeDownloadStarted;
1456   return NS_OK;
1457 }
1458 
GetContentLength(int64_t * aContentLength)1459 NS_IMETHODIMP nsExternalAppHandler::GetContentLength(int64_t* aContentLength) {
1460   *aContentLength = mContentLength;
1461   return NS_OK;
1462 }
1463 
GetBrowsingContextId(uint64_t * aBrowsingContextId)1464 NS_IMETHODIMP nsExternalAppHandler::GetBrowsingContextId(
1465     uint64_t* aBrowsingContextId) {
1466   *aBrowsingContextId = mBrowsingContext ? mBrowsingContext->Id() : 0;
1467   return NS_OK;
1468 }
1469 
RetargetLoadNotifications(nsIRequest * request)1470 void nsExternalAppHandler::RetargetLoadNotifications(nsIRequest* request) {
1471   // we are going to run the downloading of the helper app in our own little
1472   // docloader / load group context. so go ahead and force the creation of a
1473   // load group and doc loader for us to use...
1474   nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
1475   if (!aChannel) return;
1476 
1477   bool isPrivate = NS_UsePrivateBrowsing(aChannel);
1478 
1479   nsCOMPtr<nsILoadGroup> oldLoadGroup;
1480   aChannel->GetLoadGroup(getter_AddRefs(oldLoadGroup));
1481 
1482   if (oldLoadGroup) {
1483     oldLoadGroup->RemoveRequest(request, nullptr, NS_BINDING_RETARGETED);
1484   }
1485 
1486   aChannel->SetLoadGroup(nullptr);
1487   aChannel->SetNotificationCallbacks(nullptr);
1488 
1489   nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel);
1490   if (pbChannel) {
1491     pbChannel->SetPrivate(isPrivate);
1492   }
1493 }
1494 
SetUpTempFile(nsIChannel * aChannel)1495 nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) {
1496   // First we need to try to get the destination directory for the temporary
1497   // file.
1498   nsresult rv = GetDownloadDirectory(getter_AddRefs(mTempFile));
1499   NS_ENSURE_SUCCESS(rv, rv);
1500 
1501   // At this point, we do not have a filename for the temp file.  For security
1502   // purposes, this cannot be predictable, so we must use a cryptographic
1503   // quality PRNG to generate one.
1504   // We will request raw random bytes, and transform that to a base64 string,
1505   // as all characters from the base64 set are acceptable for filenames.  For
1506   // each three bytes of random data, we will get four bytes of ASCII.  Request
1507   // a bit more, to be safe, and truncate to the length we want in the end.
1508 
1509   const uint32_t wantedFileNameLength = 8;
1510   const uint32_t requiredBytesLength =
1511       static_cast<uint32_t>((wantedFileNameLength + 1) / 4 * 3);
1512 
1513   nsCOMPtr<nsIRandomGenerator> rg =
1514       do_GetService("@mozilla.org/security/random-generator;1", &rv);
1515   NS_ENSURE_SUCCESS(rv, rv);
1516 
1517   uint8_t* buffer;
1518   rv = rg->GenerateRandomBytes(requiredBytesLength, &buffer);
1519   NS_ENSURE_SUCCESS(rv, rv);
1520 
1521   nsAutoCString tempLeafName;
1522   nsDependentCSubstring randomData(reinterpret_cast<const char*>(buffer),
1523                                    requiredBytesLength);
1524   rv = Base64Encode(randomData, tempLeafName);
1525   free(buffer);
1526   buffer = nullptr;
1527   NS_ENSURE_SUCCESS(rv, rv);
1528 
1529   tempLeafName.Truncate(wantedFileNameLength);
1530 
1531   // Base64 characters are alphanumeric (a-zA-Z0-9) and '+' and '/', so we need
1532   // to replace illegal characters -- notably '/'
1533   tempLeafName.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
1534 
1535   // now append our extension.
1536   nsAutoCString ext;
1537   mMimeInfo->GetPrimaryExtension(ext);
1538   if (!ext.IsEmpty()) {
1539     ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
1540     if (ext.First() != '.') tempLeafName.Append('.');
1541     tempLeafName.Append(ext);
1542   }
1543 
1544   // We need to temporarily create a dummy file with the correct
1545   // file extension to determine the executable-ness, so do this before adding
1546   // the extra .part extension.
1547   nsCOMPtr<nsIFile> dummyFile;
1548   rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dummyFile));
1549   NS_ENSURE_SUCCESS(rv, rv);
1550 
1551   // Set the file name without .part
1552   rv = dummyFile->Append(NS_ConvertUTF8toUTF16(tempLeafName));
1553   NS_ENSURE_SUCCESS(rv, rv);
1554   rv = dummyFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
1555   NS_ENSURE_SUCCESS(rv, rv);
1556 
1557   // Store executable-ness then delete
1558   dummyFile->IsExecutable(&mTempFileIsExecutable);
1559   dummyFile->Remove(false);
1560 
1561   // Add an additional .part to prevent the OS from running this file in the
1562   // default application.
1563   tempLeafName.AppendLiteral(".part");
1564 
1565   rv = mTempFile->Append(NS_ConvertUTF8toUTF16(tempLeafName));
1566   // make this file unique!!!
1567   NS_ENSURE_SUCCESS(rv, rv);
1568   rv = mTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
1569   NS_ENSURE_SUCCESS(rv, rv);
1570 
1571   // Now save the temp leaf name, minus the ".part" bit, so we can use it later.
1572   // This is a bit broken in the case when createUnique actually had to append
1573   // some numbers, because then we now have a filename like foo.bar-1.part and
1574   // we'll end up with foo.bar-1.bar as our final filename if we end up using
1575   // this.  But the other options are all bad too....  Ideally we'd have a way
1576   // to tell createUnique to put its unique marker before the extension that
1577   // comes before ".part" or something.
1578   rv = mTempFile->GetLeafName(mTempLeafName);
1579   NS_ENSURE_SUCCESS(rv, rv);
1580 
1581   NS_ENSURE_TRUE(StringEndsWith(mTempLeafName, u".part"_ns),
1582                  NS_ERROR_UNEXPECTED);
1583 
1584   // Strip off the ".part" from mTempLeafName
1585   mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1);
1586 
1587   MOZ_ASSERT(!mSaver, "Output file initialization called more than once!");
1588   mSaver =
1589       do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv);
1590   NS_ENSURE_SUCCESS(rv, rv);
1591 
1592   rv = mSaver->SetObserver(this);
1593   if (NS_FAILED(rv)) {
1594     mSaver = nullptr;
1595     return rv;
1596   }
1597 
1598   rv = mSaver->EnableSha256();
1599   NS_ENSURE_SUCCESS(rv, rv);
1600 
1601   rv = mSaver->EnableSignatureInfo();
1602   NS_ENSURE_SUCCESS(rv, rv);
1603   LOG(("Enabled hashing and signature verification"));
1604 
1605   rv = mSaver->SetTarget(mTempFile, false);
1606   NS_ENSURE_SUCCESS(rv, rv);
1607 
1608   return rv;
1609 }
1610 
MaybeApplyDecodingForExtension(nsIRequest * aRequest)1611 void nsExternalAppHandler::MaybeApplyDecodingForExtension(
1612     nsIRequest* aRequest) {
1613   MOZ_ASSERT(aRequest);
1614 
1615   nsCOMPtr<nsIEncodedChannel> encChannel = do_QueryInterface(aRequest);
1616   if (!encChannel) {
1617     return;
1618   }
1619 
1620   // Turn off content encoding conversions if needed
1621   bool applyConversion = true;
1622 
1623   // First, check to see if conversion is already disabled.  If so, we
1624   // have nothing to do here.
1625   encChannel->GetApplyConversion(&applyConversion);
1626   if (!applyConversion) {
1627     return;
1628   }
1629 
1630   nsCOMPtr<nsIURL> sourceURL(do_QueryInterface(mSourceUrl));
1631   if (sourceURL) {
1632     nsAutoCString extension;
1633     sourceURL->GetFileExtension(extension);
1634     if (!extension.IsEmpty()) {
1635       nsCOMPtr<nsIUTF8StringEnumerator> encEnum;
1636       encChannel->GetContentEncodings(getter_AddRefs(encEnum));
1637       if (encEnum) {
1638         bool hasMore;
1639         nsresult rv = encEnum->HasMore(&hasMore);
1640         if (NS_SUCCEEDED(rv) && hasMore) {
1641           nsAutoCString encType;
1642           rv = encEnum->GetNext(encType);
1643           if (NS_SUCCEEDED(rv) && !encType.IsEmpty()) {
1644             MOZ_ASSERT(mExtProtSvc);
1645             mExtProtSvc->ApplyDecodingForExtension(extension, encType,
1646                                                    &applyConversion);
1647           }
1648         }
1649       }
1650     }
1651   }
1652 
1653   encChannel->SetApplyConversion(applyConversion);
1654 }
1655 
1656 already_AddRefed<nsIInterfaceRequestor>
GetDialogParent()1657 nsExternalAppHandler::GetDialogParent() {
1658   nsCOMPtr<nsIInterfaceRequestor> dialogParent = mWindowContext;
1659 
1660   if (!dialogParent && mBrowsingContext) {
1661     dialogParent = do_QueryInterface(mBrowsingContext->GetDOMWindow());
1662   }
1663   if (!dialogParent && mBrowsingContext && XRE_IsParentProcess()) {
1664     RefPtr<Element> element = mBrowsingContext->Top()->GetEmbedderElement();
1665     if (element) {
1666       dialogParent = do_QueryInterface(element->OwnerDoc()->GetWindow());
1667     }
1668   }
1669   return dialogParent.forget();
1670 }
1671 
OnStartRequest(nsIRequest * request)1672 NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
1673   MOZ_ASSERT(request, "OnStartRequest without request?");
1674 
1675   // Set mTimeDownloadStarted here as the download has already started and
1676   // we want to record the start time before showing the filepicker.
1677   mTimeDownloadStarted = PR_Now();
1678 
1679   mRequest = request;
1680 
1681   nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
1682 
1683   nsresult rv;
1684   nsAutoCString MIMEType;
1685   if (mMimeInfo) {
1686     mMimeInfo->GetMIMEType(MIMEType);
1687   }
1688   // Now get the URI
1689   if (aChannel) {
1690     aChannel->GetURI(getter_AddRefs(mSourceUrl));
1691   }
1692 
1693   mDownloadClassification =
1694       nsContentSecurityUtils::ClassifyDownload(aChannel, MIMEType);
1695 
1696   if (mDownloadClassification == nsITransfer::DOWNLOAD_FORBIDDEN) {
1697     // If the download is rated as forbidden,
1698     // cancel the request so no ui knows about this.
1699     mCanceled = true;
1700     request->Cancel(NS_ERROR_ABORT);
1701     return NS_OK;
1702   }
1703 
1704   nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(request));
1705   mIsFileChannel = fileChan != nullptr;
1706   if (!mIsFileChannel) {
1707     // It's possible that this request came from the child process and the
1708     // file channel actually lives there. If this returns true, then our
1709     // mSourceUrl will be an nsIFileURL anyway.
1710     nsCOMPtr<dom::nsIExternalHelperAppParent> parent(
1711         do_QueryInterface(request));
1712     mIsFileChannel = parent && parent->WasFileChannel();
1713   }
1714 
1715   // Get content length
1716   if (aChannel) {
1717     aChannel->GetContentLength(&mContentLength);
1718   }
1719 
1720   if (mBrowsingContext) {
1721     mMaybeCloseWindowHelper = new MaybeCloseWindowHelper(mBrowsingContext);
1722     mMaybeCloseWindowHelper->SetShouldCloseWindow(mShouldCloseWindow);
1723     nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(request, &rv));
1724     // Determine whether a new window was opened specifically for this request
1725     if (props) {
1726       bool tmp = false;
1727       if (NS_SUCCEEDED(
1728               props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns, &tmp))) {
1729         mMaybeCloseWindowHelper->SetShouldCloseWindow(tmp);
1730       }
1731     }
1732   }
1733 
1734   // retarget all load notifications to our docloader instead of the original
1735   // window's docloader...
1736   RetargetLoadNotifications(request);
1737 
1738   // Close the underlying DOMWindow if it was opened specifically for the
1739   // download. We don't run this in the content process, since we have
1740   // an instance running in the parent as well, which will handle this
1741   // if needed.
1742   if (!XRE_IsContentProcess() && mMaybeCloseWindowHelper) {
1743     mBrowsingContext = mMaybeCloseWindowHelper->MaybeCloseWindow();
1744   }
1745 
1746   // In an IPC setting, we're allowing the child process, here, to make
1747   // decisions about decoding the channel (e.g. decompression).  It will
1748   // still forward the decoded (uncompressed) data back to the parent.
1749   // Con: Uncompressed data means more IPC overhead.
1750   // Pros: ExternalHelperAppParent doesn't need to implement nsIEncodedChannel.
1751   //       Parent process doesn't need to expect CPU time on decompression.
1752   MaybeApplyDecodingForExtension(aChannel);
1753 
1754   // At this point, the child process has done everything it can usefully do
1755   // for OnStartRequest.
1756   if (XRE_IsContentProcess()) {
1757     return NS_OK;
1758   }
1759 
1760   rv = SetUpTempFile(aChannel);
1761   if (NS_FAILED(rv)) {
1762     nsresult transferError = rv;
1763 
1764     rv = CreateFailedTransfer();
1765     if (NS_FAILED(rv)) {
1766       LOG(
1767           ("Failed to create transfer to report failure."
1768            "Will fallback to prompter!"));
1769     }
1770 
1771     mCanceled = true;
1772     request->Cancel(transferError);
1773 
1774     nsAutoString path;
1775     if (mTempFile) mTempFile->GetPath(path);
1776 
1777     SendStatusChange(kWriteError, transferError, request, path);
1778 
1779     return NS_OK;
1780   }
1781 
1782   // Inform channel it is open on behalf of a download to throttle it during
1783   // page loads and prevent its caching.
1784   nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(aChannel);
1785   if (httpInternal) {
1786     rv = httpInternal->SetChannelIsForDownload(true);
1787     MOZ_ASSERT(NS_SUCCEEDED(rv));
1788   }
1789 
1790   if (mSourceUrl->SchemeIs("data")) {
1791     // In case we're downloading a data:// uri
1792     // we don't want to apply AllowTopLevelNavigationToDataURI.
1793     nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
1794     loadInfo->SetForceAllowDataURI(true);
1795   }
1796 
1797   // now that the temp file is set up, find out if we need to invoke a dialog
1798   // asking the user what they want us to do with this content...
1799 
1800   // We can get here for three reasons: "can't handle", "sniffed type", or
1801   // "server sent content-disposition:attachment".  In the first case we want
1802   // to honor the user's "always ask" pref; in the other two cases we want to
1803   // honor it only if the default action is "save".  Opening attachments in
1804   // helper apps by default breaks some websites (especially if the attachment
1805   // is one part of a multipart document).  Opening sniffed content in helper
1806   // apps by default introduces security holes that we'd rather not have.
1807 
1808   // So let's find out whether the user wants to be prompted.  If he does not,
1809   // check mReason and the preferred action to see what we should do.
1810 
1811   bool alwaysAsk = true;
1812 
1813   // Skip showing UnknownContentType dialog, unless it is explicitly
1814   // set in preferences.
1815   bool skipShowingDialog =
1816       Preferences::GetBool("browser.download.useDownloadDir") &&
1817       StaticPrefs::browser_download_improvements_to_download_panel();
1818 
1819   if (skipShowingDialog) {
1820     alwaysAsk = false;
1821   } else {
1822     mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk);
1823   }
1824 
1825   if (alwaysAsk) {
1826     // But we *don't* ask if this mimeInfo didn't come from
1827     // our user configuration datastore and the user has said
1828     // at some point in the distant past that they don't
1829     // want to be asked.  The latter fact would have been
1830     // stored in pref strings back in the old days.
1831 
1832     bool mimeTypeIsInDatastore = false;
1833     nsCOMPtr<nsIHandlerService> handlerSvc =
1834         do_GetService(NS_HANDLERSERVICE_CONTRACTID);
1835     if (handlerSvc) {
1836       handlerSvc->Exists(mMimeInfo, &mimeTypeIsInDatastore);
1837     }
1838     if (!handlerSvc || !mimeTypeIsInDatastore) {
1839       if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_SAVE_TO_DISK_PREF,
1840                                    MIMEType.get())) {
1841         // Don't need to ask after all.
1842         alwaysAsk = false;
1843         // Make sure action matches pref (save to disk).
1844         mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
1845       } else if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_OPEN_FILE_PREF,
1846                                           MIMEType.get())) {
1847         // Don't need to ask after all.
1848         alwaysAsk = false;
1849       }
1850     }
1851   } else if (MIMEType.EqualsLiteral("text/plain")) {
1852     nsAutoCString ext;
1853     mMimeInfo->GetPrimaryExtension(ext);
1854     // If people are sending us ApplicationReputation-eligible files with
1855     // text/plain mimetypes, enforce asking the user what to do.
1856     if (!ext.IsEmpty()) {
1857       nsAutoCString dummyFileName("f");
1858       if (ext.First() != '.') {
1859         dummyFileName.Append(".");
1860       }
1861       ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
1862       nsCOMPtr<nsIApplicationReputationService> appRep =
1863           components::ApplicationReputation::Service();
1864       appRep->IsBinary(dummyFileName + ext, &alwaysAsk);
1865     }
1866   }
1867 
1868   int32_t action = nsIMIMEInfo::saveToDisk;
1869   mMimeInfo->GetPreferredAction(&action);
1870 
1871   bool forcePrompt =
1872       mReason == nsIHelperAppLauncherDialog::REASON_TYPESNIFFED ||
1873       (mReason == nsIHelperAppLauncherDialog::REASON_SERVERREQUEST &&
1874        !skipShowingDialog);
1875 
1876   // OK, now check why we're here
1877   if (!alwaysAsk && forcePrompt) {
1878     // Force asking if we're not saving.  See comment back when we fetched the
1879     // alwaysAsk boolean for details.
1880     alwaysAsk = (action != nsIMIMEInfo::saveToDisk);
1881   }
1882 
1883   bool shouldAutomaticallyHandleInternally =
1884       action == nsIMIMEInfo::handleInternally &&
1885       StaticPrefs::browser_download_improvements_to_download_panel();
1886 
1887   // If we're not asking, check we actually know what to do:
1888   if (!alwaysAsk) {
1889     alwaysAsk = action != nsIMIMEInfo::saveToDisk &&
1890                 action != nsIMIMEInfo::useHelperApp &&
1891                 action != nsIMIMEInfo::useSystemDefault &&
1892                 !shouldAutomaticallyHandleInternally;
1893   }
1894 
1895   // if we were told that we _must_ save to disk without asking, all the stuff
1896   // before this is irrelevant; override it
1897   if (mForceSave) {
1898     alwaysAsk = false;
1899     action = nsIMIMEInfo::saveToDisk;
1900   }
1901   // Additionally, if we are asked by the OS to open a local file,
1902   // automatically downloading it to create a second copy of that file doesn't
1903   // really make sense. We should ask the user what they want to do.
1904   if (mSourceUrl->SchemeIs("file") && !alwaysAsk &&
1905       action == nsIMIMEInfo::saveToDisk) {
1906     alwaysAsk = true;
1907   }
1908   if (alwaysAsk) {
1909     // Display the dialog
1910     mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv);
1911     NS_ENSURE_SUCCESS(rv, rv);
1912 
1913     // this will create a reference cycle (the dialog holds a reference to us as
1914     // nsIHelperAppLauncher), which will be broken in Cancel or CreateTransfer.
1915     nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
1916     rv = mDialog->Show(this, dialogParent, mReason);
1917 
1918     // what do we do if the dialog failed? I guess we should call Cancel and
1919     // abort the load....
1920   } else {
1921     // We need to do the save/open immediately, then.
1922 #ifdef XP_WIN
1923     /* We need to see whether the file we've got here could be
1924      * executable.  If it could, we had better not try to open it!
1925      * We can skip this check, though, if we have a setting to open in a
1926      * helper app.
1927      * This code mirrors the code in
1928      * nsExternalAppHandler::LaunchWithApplication so that what we
1929      * test here is as close as possible to what will really be
1930      * happening if we decide to execute
1931      */
1932     nsCOMPtr<nsIHandlerApp> prefApp;
1933     mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(prefApp));
1934     if (action != nsIMIMEInfo::useHelperApp || !prefApp) {
1935       nsCOMPtr<nsIFile> fileToTest;
1936       GetTargetFile(getter_AddRefs(fileToTest));
1937       if (fileToTest) {
1938         bool isExecutable;
1939         rv = fileToTest->IsExecutable(&isExecutable);
1940         if (NS_FAILED(rv) ||
1941             isExecutable) {  // checking NS_FAILED, because paranoia is good
1942           action = nsIMIMEInfo::saveToDisk;
1943         }
1944       } else {  // Paranoia is good here too, though this really should not
1945                 // happen
1946         NS_WARNING(
1947             "GetDownloadInfo returned a null file after the temp file has been "
1948             "set up! ");
1949         action = nsIMIMEInfo::saveToDisk;
1950       }
1951     }
1952 
1953 #endif
1954     if (action == nsIMIMEInfo::useHelperApp ||
1955         action == nsIMIMEInfo::useSystemDefault ||
1956         shouldAutomaticallyHandleInternally) {
1957       rv = LaunchWithApplication(shouldAutomaticallyHandleInternally);
1958     } else {
1959       rv = PromptForSaveDestination();
1960     }
1961   }
1962   return NS_OK;
1963 }
1964 
1965 // Convert error info into proper message text and send OnStatusChange
1966 // notification to the dialog progress listener or nsITransfer implementation.
SendStatusChange(ErrorType type,nsresult rv,nsIRequest * aRequest,const nsString & path)1967 void nsExternalAppHandler::SendStatusChange(ErrorType type, nsresult rv,
1968                                             nsIRequest* aRequest,
1969                                             const nsString& path) {
1970   const char* msgId = nullptr;
1971   switch (rv) {
1972     case NS_ERROR_OUT_OF_MEMORY:
1973       // No memory
1974       msgId = "noMemory";
1975       break;
1976 
1977     case NS_ERROR_FILE_NO_DEVICE_SPACE:
1978       // Out of space on target volume.
1979       msgId = "diskFull";
1980       break;
1981 
1982     case NS_ERROR_FILE_READ_ONLY:
1983       // Attempt to write to read/only file.
1984       msgId = "readOnly";
1985       break;
1986 
1987     case NS_ERROR_FILE_ACCESS_DENIED:
1988       if (type == kWriteError) {
1989         // Attempt to write without sufficient permissions.
1990 #if defined(ANDROID)
1991         // On Android this means the SD card is present but
1992         // unavailable (read-only).
1993         msgId = "SDAccessErrorCardReadOnly";
1994 #else
1995         msgId = "accessError";
1996 #endif
1997       } else {
1998         msgId = "launchError";
1999       }
2000       break;
2001 
2002     case NS_ERROR_FILE_NOT_FOUND:
2003     case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
2004     case NS_ERROR_FILE_UNRECOGNIZED_PATH:
2005       // Helper app not found, let's verify this happened on launch
2006       if (type == kLaunchError) {
2007         msgId = "helperAppNotFound";
2008         break;
2009       }
2010 #if defined(ANDROID)
2011       else if (type == kWriteError) {
2012         // On Android this means the SD card is missing (not in
2013         // SD slot).
2014         msgId = "SDAccessErrorCardMissing";
2015         break;
2016       }
2017 #endif
2018       [[fallthrough]];
2019 
2020     default:
2021       // Generic read/write/launch error message.
2022       switch (type) {
2023         case kReadError:
2024           msgId = "readError";
2025           break;
2026         case kWriteError:
2027           msgId = "writeError";
2028           break;
2029         case kLaunchError:
2030           msgId = "launchError";
2031           break;
2032       }
2033       break;
2034   }
2035 
2036   MOZ_LOG(
2037       nsExternalHelperAppService::mLog, LogLevel::Error,
2038       ("Error: %s, type=%i, listener=0x%p, transfer=0x%p, rv=0x%08" PRIX32 "\n",
2039        msgId, type, mDialogProgressListener.get(), mTransfer.get(),
2040        static_cast<uint32_t>(rv)));
2041 
2042   MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error,
2043           ("       path='%s'\n", NS_ConvertUTF16toUTF8(path).get()));
2044 
2045   // Get properties file bundle and extract status string.
2046   nsCOMPtr<nsIStringBundleService> stringService =
2047       mozilla::components::StringBundle::Service();
2048   if (stringService) {
2049     nsCOMPtr<nsIStringBundle> bundle;
2050     if (NS_SUCCEEDED(stringService->CreateBundle(
2051             "chrome://global/locale/nsWebBrowserPersist.properties",
2052             getter_AddRefs(bundle)))) {
2053       nsAutoString msgText;
2054       AutoTArray<nsString, 1> strings = {path};
2055       if (NS_SUCCEEDED(bundle->FormatStringFromName(msgId, strings, msgText))) {
2056         if (mDialogProgressListener) {
2057           // We have a listener, let it handle the error.
2058           mDialogProgressListener->OnStatusChange(
2059               nullptr, (type == kReadError) ? aRequest : nullptr, rv,
2060               msgText.get());
2061         } else if (mTransfer) {
2062           mTransfer->OnStatusChange(nullptr,
2063                                     (type == kReadError) ? aRequest : nullptr,
2064                                     rv, msgText.get());
2065         } else if (XRE_IsParentProcess()) {
2066           // We don't have a listener.  Simply show the alert ourselves.
2067           nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
2068           nsresult qiRv;
2069           nsCOMPtr<nsIPrompt> prompter(do_GetInterface(dialogParent, &qiRv));
2070           nsAutoString title;
2071           bundle->FormatStringFromName("title", strings, title);
2072 
2073           MOZ_LOG(
2074               nsExternalHelperAppService::mLog, LogLevel::Debug,
2075               ("mBrowsingContext=0x%p, prompter=0x%p, qi rv=0x%08" PRIX32
2076                ", title='%s', msg='%s'",
2077                mBrowsingContext.get(), prompter.get(),
2078                static_cast<uint32_t>(qiRv), NS_ConvertUTF16toUTF8(title).get(),
2079                NS_ConvertUTF16toUTF8(msgText).get()));
2080 
2081           // If we didn't have a prompter we will try and get a window
2082           // instead, get it's docshell and use it to alert the user.
2083           if (!prompter) {
2084             nsCOMPtr<nsPIDOMWindowOuter> window(do_GetInterface(dialogParent));
2085             if (!window || !window->GetDocShell()) {
2086               return;
2087             }
2088 
2089             prompter = do_GetInterface(window->GetDocShell(), &qiRv);
2090 
2091             MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Debug,
2092                     ("No prompter from mBrowsingContext, using DocShell, "
2093                      "window=0x%p, docShell=0x%p, "
2094                      "prompter=0x%p, qi rv=0x%08" PRIX32,
2095                      window.get(), window->GetDocShell(), prompter.get(),
2096                      static_cast<uint32_t>(qiRv)));
2097 
2098             // If we still don't have a prompter, there's nothing else we
2099             // can do so just return.
2100             if (!prompter) {
2101               MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error,
2102                       ("No prompter from DocShell, no way to alert user"));
2103               return;
2104             }
2105           }
2106 
2107           // We should always have a prompter at this point.
2108           prompter->Alert(title.get(), msgText.get());
2109         }
2110       }
2111     }
2112   }
2113 }
2114 
2115 NS_IMETHODIMP
OnDataAvailable(nsIRequest * request,nsIInputStream * inStr,uint64_t sourceOffset,uint32_t count)2116 nsExternalAppHandler::OnDataAvailable(nsIRequest* request,
2117                                       nsIInputStream* inStr,
2118                                       uint64_t sourceOffset, uint32_t count) {
2119   nsresult rv = NS_OK;
2120   // first, check to see if we've been canceled....
2121   if (mCanceled || !mSaver) {
2122     // then go cancel our underlying channel too
2123     return request->Cancel(NS_BINDING_ABORTED);
2124   }
2125 
2126   // read the data out of the stream and write it to the temp file.
2127   if (count > 0) {
2128     mProgress += count;
2129 
2130     nsCOMPtr<nsIStreamListener> saver = do_QueryInterface(mSaver);
2131     rv = saver->OnDataAvailable(request, inStr, sourceOffset, count);
2132     if (NS_SUCCEEDED(rv)) {
2133       // Send progress notification.
2134       if (mTransfer) {
2135         mTransfer->OnProgressChange64(nullptr, request, mProgress,
2136                                       mContentLength, mProgress,
2137                                       mContentLength);
2138       }
2139     } else {
2140       // An error occurred, notify listener.
2141       nsAutoString tempFilePath;
2142       if (mTempFile) {
2143         mTempFile->GetPath(tempFilePath);
2144       }
2145       SendStatusChange(kReadError, rv, request, tempFilePath);
2146 
2147       // Cancel the download.
2148       Cancel(rv);
2149     }
2150   }
2151   return rv;
2152 }
2153 
OnStopRequest(nsIRequest * request,nsresult aStatus)2154 NS_IMETHODIMP nsExternalAppHandler::OnStopRequest(nsIRequest* request,
2155                                                   nsresult aStatus) {
2156   LOG(
2157       ("nsExternalAppHandler::OnStopRequest\n"
2158        "  mCanceled=%d, mTransfer=0x%p, aStatus=0x%08" PRIX32 "\n",
2159        mCanceled, mTransfer.get(), static_cast<uint32_t>(aStatus)));
2160 
2161   mStopRequestIssued = true;
2162 
2163   // Cancel if the request did not complete successfully.
2164   if (!mCanceled && NS_FAILED(aStatus)) {
2165     // Send error notification.
2166     nsAutoString tempFilePath;
2167     if (mTempFile) mTempFile->GetPath(tempFilePath);
2168     SendStatusChange(kReadError, aStatus, request, tempFilePath);
2169 
2170     Cancel(aStatus);
2171   }
2172 
2173   // first, check to see if we've been canceled....
2174   if (mCanceled || !mSaver) {
2175     return NS_OK;
2176   }
2177 
2178   return mSaver->Finish(NS_OK);
2179 }
2180 
2181 NS_IMETHODIMP
OnTargetChange(nsIBackgroundFileSaver * aSaver,nsIFile * aTarget)2182 nsExternalAppHandler::OnTargetChange(nsIBackgroundFileSaver* aSaver,
2183                                      nsIFile* aTarget) {
2184   return NS_OK;
2185 }
2186 
2187 NS_IMETHODIMP
OnSaveComplete(nsIBackgroundFileSaver * aSaver,nsresult aStatus)2188 nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver,
2189                                      nsresult aStatus) {
2190   LOG(
2191       ("nsExternalAppHandler::OnSaveComplete\n"
2192        "  aSaver=0x%p, aStatus=0x%08" PRIX32 ", mCanceled=%d, mTransfer=0x%p\n",
2193        aSaver, static_cast<uint32_t>(aStatus), mCanceled, mTransfer.get()));
2194 
2195   if (!mCanceled) {
2196     // Save the hash and signature information
2197     (void)mSaver->GetSha256Hash(mHash);
2198     (void)mSaver->GetSignatureInfo(mSignatureInfo);
2199 
2200     // Free the reference that the saver keeps on us, even if we couldn't get
2201     // the hash.
2202     mSaver = nullptr;
2203 
2204     // Save the redirect information.
2205     nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2206     if (channel) {
2207       nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
2208       nsresult rv = NS_OK;
2209       nsCOMPtr<nsIMutableArray> redirectChain =
2210           do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
2211       NS_ENSURE_SUCCESS(rv, rv);
2212       LOG(("nsExternalAppHandler: Got %zu redirects\n",
2213            loadInfo->RedirectChain().Length()));
2214       for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) {
2215         redirectChain->AppendElement(entry);
2216       }
2217       mRedirects = redirectChain;
2218     }
2219 
2220     if (NS_FAILED(aStatus)) {
2221       nsAutoString path;
2222       mTempFile->GetPath(path);
2223 
2224       // It may happen when e10s is enabled that there will be no transfer
2225       // object available to communicate status as expected by the system.
2226       // Let's try and create a temporary transfer object to take care of this
2227       // for us, we'll fall back to using the prompt service if we absolutely
2228       // have to.
2229       if (!mTransfer) {
2230         // We don't care if this fails.
2231         CreateFailedTransfer();
2232       }
2233 
2234       SendStatusChange(kWriteError, aStatus, nullptr, path);
2235       if (!mCanceled) Cancel(aStatus);
2236       return NS_OK;
2237     }
2238   }
2239 
2240   // Notify the transfer object that we are done if the user has chosen an
2241   // action. If the user hasn't chosen an action, the progress listener
2242   // (nsITransfer) will be notified in CreateTransfer.
2243   if (mTransfer) {
2244     NotifyTransfer(aStatus);
2245   }
2246 
2247   return NS_OK;
2248 }
2249 
NotifyTransfer(nsresult aStatus)2250 void nsExternalAppHandler::NotifyTransfer(nsresult aStatus) {
2251   MOZ_ASSERT(NS_IsMainThread(), "Must notify on main thread");
2252   MOZ_ASSERT(mTransfer, "We must have an nsITransfer");
2253 
2254   LOG(("Notifying progress listener"));
2255 
2256   if (NS_SUCCEEDED(aStatus)) {
2257     (void)mTransfer->SetSha256Hash(mHash);
2258     (void)mTransfer->SetSignatureInfo(mSignatureInfo);
2259     (void)mTransfer->SetRedirects(mRedirects);
2260     (void)mTransfer->OnProgressChange64(
2261         nullptr, nullptr, mProgress, mContentLength, mProgress, mContentLength);
2262   }
2263 
2264   (void)mTransfer->OnStateChange(nullptr, nullptr,
2265                                  nsIWebProgressListener::STATE_STOP |
2266                                      nsIWebProgressListener::STATE_IS_REQUEST |
2267                                      nsIWebProgressListener::STATE_IS_NETWORK,
2268                                  aStatus);
2269 
2270   // This nsITransfer object holds a reference to us (we are its observer), so
2271   // we need to release the reference to break a reference cycle (and therefore
2272   // to prevent leaking).  We do this even if the previous calls failed.
2273   mTransfer = nullptr;
2274 }
2275 
GetMIMEInfo(nsIMIMEInfo ** aMIMEInfo)2276 NS_IMETHODIMP nsExternalAppHandler::GetMIMEInfo(nsIMIMEInfo** aMIMEInfo) {
2277   *aMIMEInfo = mMimeInfo;
2278   NS_ADDREF(*aMIMEInfo);
2279   return NS_OK;
2280 }
2281 
GetSource(nsIURI ** aSourceURI)2282 NS_IMETHODIMP nsExternalAppHandler::GetSource(nsIURI** aSourceURI) {
2283   NS_ENSURE_ARG(aSourceURI);
2284   *aSourceURI = mSourceUrl;
2285   NS_IF_ADDREF(*aSourceURI);
2286   return NS_OK;
2287 }
2288 
GetSuggestedFileName(nsAString & aSuggestedFileName)2289 NS_IMETHODIMP nsExternalAppHandler::GetSuggestedFileName(
2290     nsAString& aSuggestedFileName) {
2291   aSuggestedFileName = mSuggestedFileName;
2292   return NS_OK;
2293 }
2294 
CreateTransfer()2295 nsresult nsExternalAppHandler::CreateTransfer() {
2296   LOG(("nsExternalAppHandler::CreateTransfer"));
2297 
2298   MOZ_ASSERT(NS_IsMainThread(), "Must create transfer on main thread");
2299   // We are back from the helper app dialog (where the user chooses to save or
2300   // open), but we aren't done processing the load. in this case, throw up a
2301   // progress dialog so the user can see what's going on.
2302   // Also, release our reference to mDialog. We don't need it anymore, and we
2303   // need to break the reference cycle.
2304   mDialog = nullptr;
2305   if (!mDialogProgressListener) {
2306     NS_WARNING("The dialog should nullify the dialog progress listener");
2307   }
2308   // In case of a non acceptable download, we need to cancel the request and
2309   // pass a FailedTransfer for the Download UI.
2310   if (mDownloadClassification != nsITransfer::DOWNLOAD_ACCEPTABLE) {
2311     mCanceled = true;
2312     mRequest->Cancel(NS_ERROR_ABORT);
2313     return CreateFailedTransfer();
2314   }
2315   nsresult rv;
2316 
2317   // We must be able to create an nsITransfer object. If not, it doesn't matter
2318   // much that we can't launch the helper application or save to disk. Work on
2319   // a local copy rather than mTransfer until we know we succeeded, to make it
2320   // clearer that this function is re-entrant.
2321   nsCOMPtr<nsITransfer> transfer =
2322       do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
2323   NS_ENSURE_SUCCESS(rv, rv);
2324 
2325   // Initialize the download
2326   nsCOMPtr<nsIURI> target;
2327   rv = NS_NewFileURI(getter_AddRefs(target), mFinalFileDestination);
2328   NS_ENSURE_SUCCESS(rv, rv);
2329 
2330   nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2331   if (mBrowsingContext) {
2332     rv = transfer->InitWithBrowsingContext(
2333         mSourceUrl, target, u""_ns, mMimeInfo, mTimeDownloadStarted, mTempFile,
2334         this, channel && NS_UsePrivateBrowsing(channel),
2335         mDownloadClassification, mBrowsingContext, mHandleInternally);
2336   } else {
2337     rv = transfer->Init(mSourceUrl, target, u""_ns, mMimeInfo,
2338                         mTimeDownloadStarted, mTempFile, this,
2339                         channel && NS_UsePrivateBrowsing(channel),
2340                         mDownloadClassification);
2341   }
2342 
2343   NS_ENSURE_SUCCESS(rv, rv);
2344 
2345   // If we were cancelled since creating the transfer, just return. It is
2346   // always ok to return NS_OK if we are cancelled. Callers of this function
2347   // must call Cancel if CreateTransfer fails, but there's no need to cancel
2348   // twice.
2349   if (mCanceled) {
2350     return NS_OK;
2351   }
2352   rv = transfer->OnStateChange(nullptr, mRequest,
2353                                nsIWebProgressListener::STATE_START |
2354                                    nsIWebProgressListener::STATE_IS_REQUEST |
2355                                    nsIWebProgressListener::STATE_IS_NETWORK,
2356                                NS_OK);
2357   NS_ENSURE_SUCCESS(rv, rv);
2358 
2359   if (mCanceled) {
2360     return NS_OK;
2361   }
2362 
2363   mRequest = nullptr;
2364   // Finally, save the transfer to mTransfer.
2365   mTransfer = transfer;
2366   transfer = nullptr;
2367 
2368   // While we were bringing up the progress dialog, we actually finished
2369   // processing the url. If that's the case then mStopRequestIssued will be
2370   // true and OnSaveComplete has been called.
2371   if (mStopRequestIssued && !mSaver && mTransfer) {
2372     NotifyTransfer(NS_OK);
2373   }
2374 
2375   return rv;
2376 }
2377 
CreateFailedTransfer()2378 nsresult nsExternalAppHandler::CreateFailedTransfer() {
2379   nsresult rv;
2380   nsCOMPtr<nsITransfer> transfer =
2381       do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
2382   NS_ENSURE_SUCCESS(rv, rv);
2383 
2384   // We won't pass the temp file to the transfer, so if we have one it needs to
2385   // get deleted now.
2386   if (mTempFile) {
2387     if (mSaver) {
2388       mSaver->Finish(NS_BINDING_ABORTED);
2389       mSaver = nullptr;
2390     }
2391     mTempFile->Remove(false);
2392   }
2393 
2394   nsCOMPtr<nsIURI> pseudoTarget;
2395   if (!mFinalFileDestination) {
2396     // If we don't have a download directory we're kinda screwed but it's OK
2397     // we'll still report the error via the prompter.
2398     nsCOMPtr<nsIFile> pseudoFile;
2399     rv = GetDownloadDirectory(getter_AddRefs(pseudoFile), true);
2400     NS_ENSURE_SUCCESS(rv, rv);
2401 
2402     // Append the default suggested filename. If the user restarts the transfer
2403     // we will re-trigger a filename check anyway to ensure that it is unique.
2404     rv = pseudoFile->Append(mSuggestedFileName);
2405     NS_ENSURE_SUCCESS(rv, rv);
2406 
2407     rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), pseudoFile);
2408     NS_ENSURE_SUCCESS(rv, rv);
2409   } else {
2410     // Initialize the target, if present
2411     rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), mFinalFileDestination);
2412     NS_ENSURE_SUCCESS(rv, rv);
2413   }
2414 
2415   nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
2416   if (mBrowsingContext) {
2417     rv = transfer->InitWithBrowsingContext(
2418         mSourceUrl, pseudoTarget, u""_ns, mMimeInfo, mTimeDownloadStarted,
2419         nullptr, this, channel && NS_UsePrivateBrowsing(channel),
2420         mDownloadClassification, mBrowsingContext, mHandleInternally);
2421   } else {
2422     rv = transfer->Init(mSourceUrl, pseudoTarget, u""_ns, mMimeInfo,
2423                         mTimeDownloadStarted, nullptr, this,
2424                         channel && NS_UsePrivateBrowsing(channel),
2425                         mDownloadClassification);
2426   }
2427   NS_ENSURE_SUCCESS(rv, rv);
2428 
2429   // Our failed transfer is ready.
2430   mTransfer = std::move(transfer);
2431 
2432   return NS_OK;
2433 }
2434 
SaveDestinationAvailable(nsIFile * aFile)2435 nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile* aFile) {
2436   if (aFile)
2437     ContinueSave(aFile);
2438   else
2439     Cancel(NS_BINDING_ABORTED);
2440 
2441   return NS_OK;
2442 }
2443 
RequestSaveDestination(const nsString & aDefaultFile,const nsString & aFileExtension)2444 void nsExternalAppHandler::RequestSaveDestination(
2445     const nsString& aDefaultFile, const nsString& aFileExtension) {
2446   // Display the dialog
2447   // XXX Convert to use file picker? No, then embeddors could not do any sort of
2448   // "AutoDownload" w/o showing a prompt
2449   nsresult rv = NS_OK;
2450   if (!mDialog) {
2451     // Get helper app launcher dialog.
2452     mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv);
2453     if (rv != NS_OK) {
2454       Cancel(NS_BINDING_ABORTED);
2455       return;
2456     }
2457   }
2458 
2459   // we want to explicitly unescape aDefaultFile b4 passing into the dialog. we
2460   // can't unescape it because the dialog is implemented by a JS component which
2461   // doesn't have a window so no unescape routine is defined...
2462 
2463   // Now, be sure to keep |this| alive, and the dialog
2464   // If we don't do this, users that close the helper app dialog while the file
2465   // picker is up would cause Cancel() to be called, and the dialog would be
2466   // released, which would release this object too, which would crash.
2467   // See Bug 249143
2468   RefPtr<nsExternalAppHandler> kungFuDeathGrip(this);
2469   nsCOMPtr<nsIHelperAppLauncherDialog> dlg(mDialog);
2470   nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
2471 
2472   rv = dlg->PromptForSaveToFileAsync(this, dialogParent, aDefaultFile.get(),
2473                                      aFileExtension.get(), mForceSave);
2474   if (NS_FAILED(rv)) {
2475     Cancel(NS_BINDING_ABORTED);
2476   }
2477 }
2478 
2479 // PromptForSaveDestination should only be called by the helper app dialog which
2480 // allows the user to say launch with application or save to disk.
PromptForSaveDestination()2481 NS_IMETHODIMP nsExternalAppHandler::PromptForSaveDestination() {
2482   if (mCanceled) return NS_OK;
2483 
2484   mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
2485 
2486   if (mSuggestedFileName.IsEmpty()) {
2487     RequestSaveDestination(mTempLeafName, mTempFileExtension);
2488   } else {
2489     nsAutoString fileExt;
2490     int32_t pos = mSuggestedFileName.RFindChar('.');
2491     if (pos >= 0) {
2492       mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos);
2493     }
2494     if (fileExt.IsEmpty()) {
2495       fileExt = mTempFileExtension;
2496     }
2497 
2498     RequestSaveDestination(mSuggestedFileName, fileExt);
2499   }
2500 
2501   return NS_OK;
2502 }
ContinueSave(nsIFile * aNewFileLocation)2503 nsresult nsExternalAppHandler::ContinueSave(nsIFile* aNewFileLocation) {
2504   if (mCanceled) return NS_OK;
2505 
2506   MOZ_ASSERT(aNewFileLocation, "Must be called with a non-null file");
2507 
2508   nsresult rv = NS_OK;
2509   nsCOMPtr<nsIFile> fileToUse = aNewFileLocation;
2510   mFinalFileDestination = fileToUse;
2511 
2512   // Move what we have in the final directory, but append .part
2513   // to it, to indicate that it's unfinished.  Do not call SetTarget on the
2514   // saver if we are done (Finish has been called) but OnSaverComplete has not
2515   // been called.
2516   if (mFinalFileDestination && mSaver && !mStopRequestIssued) {
2517     nsCOMPtr<nsIFile> movedFile;
2518     mFinalFileDestination->Clone(getter_AddRefs(movedFile));
2519     if (movedFile) {
2520       // Get the old leaf name and append .part to it
2521       nsAutoString name;
2522       mFinalFileDestination->GetLeafName(name);
2523       name.AppendLiteral(".part");
2524       movedFile->SetLeafName(name);
2525 
2526       rv = mSaver->SetTarget(movedFile, true);
2527       if (NS_FAILED(rv)) {
2528         nsAutoString path;
2529         mTempFile->GetPath(path);
2530         SendStatusChange(kWriteError, rv, nullptr, path);
2531         Cancel(rv);
2532         return NS_OK;
2533       }
2534 
2535       mTempFile = movedFile;
2536     }
2537   }
2538 
2539   // The helper app dialog has told us what to do and we have a final file
2540   // destination.
2541   rv = CreateTransfer();
2542   // If we fail to create the transfer, Cancel.
2543   if (NS_FAILED(rv)) {
2544     Cancel(rv);
2545     return rv;
2546   }
2547 
2548   return NS_OK;
2549 }
2550 
2551 // LaunchWithApplication should only be called by the helper app dialog which
2552 // allows the user to say launch with application or save to disk.
LaunchWithApplication(bool aHandleInternally)2553 NS_IMETHODIMP nsExternalAppHandler::LaunchWithApplication(
2554     bool aHandleInternally) {
2555   if (mCanceled) return NS_OK;
2556 
2557   mHandleInternally = aHandleInternally;
2558 
2559   // Now check if the file is local, in which case we won't bother with saving
2560   // it to a temporary directory and just launch it from where it is
2561   nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(mSourceUrl));
2562   if (fileUrl && mIsFileChannel) {
2563     Cancel(NS_BINDING_ABORTED);
2564     nsCOMPtr<nsIFile> file;
2565     nsresult rv = fileUrl->GetFile(getter_AddRefs(file));
2566 
2567     if (NS_SUCCEEDED(rv)) {
2568       rv = mMimeInfo->LaunchWithFile(file);
2569       if (NS_SUCCEEDED(rv)) return NS_OK;
2570     }
2571     nsAutoString path;
2572     if (file) file->GetPath(path);
2573     // If we get here, an error happened
2574     SendStatusChange(kLaunchError, rv, nullptr, path);
2575     return rv;
2576   }
2577 
2578   // Now that the user has elected to launch the downloaded file with a helper
2579   // app, we're justified in removing the 'salted' name.  We'll rename to what
2580   // was specified in mSuggestedFileName after the download is done prior to
2581   // launching the helper app.  So that any existing file of that name won't be
2582   // overwritten we call CreateUnique().  Also note that we use the same
2583   // directory as originally downloaded so the download can be renamed in place
2584   // later.
2585   nsCOMPtr<nsIFile> fileToUse;
2586   (void)GetDownloadDirectory(getter_AddRefs(fileToUse));
2587 
2588   if (mSuggestedFileName.IsEmpty()) {
2589     // Keep using the leafname of the temp file, since we're just starting a
2590     // helper
2591     mSuggestedFileName = mTempLeafName;
2592   }
2593 
2594 #ifdef XP_WIN
2595   fileToUse->Append(mSuggestedFileName + mTempFileExtension);
2596 #else
2597   fileToUse->Append(mSuggestedFileName);
2598 #endif
2599 
2600   nsresult rv = fileToUse->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
2601   if (NS_SUCCEEDED(rv)) {
2602     mFinalFileDestination = fileToUse;
2603     // launch the progress window now that the user has picked the desired
2604     // action.
2605     rv = CreateTransfer();
2606     if (NS_FAILED(rv)) {
2607       Cancel(rv);
2608     }
2609   } else {
2610     // Cancel the download and report an error.  We do not want to end up in
2611     // a state where it appears that we have a normal download that is
2612     // pointing to a file that we did not actually create.
2613     nsAutoString path;
2614     mTempFile->GetPath(path);
2615     SendStatusChange(kWriteError, rv, nullptr, path);
2616     Cancel(rv);
2617   }
2618   return rv;
2619 }
2620 
Cancel(nsresult aReason)2621 NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) {
2622   NS_ENSURE_ARG(NS_FAILED(aReason));
2623 
2624   if (mCanceled) {
2625     return NS_OK;
2626   }
2627   mCanceled = true;
2628 
2629   if (mSaver) {
2630     // We are still writing to the target file.  Give the saver a chance to
2631     // close the target file, then notify the transfer object if necessary in
2632     // the OnSaveComplete callback.
2633     mSaver->Finish(aReason);
2634     mSaver = nullptr;
2635   } else {
2636     if (mStopRequestIssued && mTempFile) {
2637       // This branch can only happen when the user cancels the helper app dialog
2638       // when the request has completed. The temp file has to be removed here,
2639       // because mSaver has been released at that time with the temp file left.
2640       (void)mTempFile->Remove(false);
2641     }
2642 
2643     // Notify the transfer object that the download has been canceled, if the
2644     // user has already chosen an action and we didn't notify already.
2645     if (mTransfer) {
2646       NotifyTransfer(aReason);
2647     }
2648   }
2649 
2650   // Break our reference cycle with the helper app dialog (set up in
2651   // OnStartRequest)
2652   mDialog = nullptr;
2653 
2654   mRequest = nullptr;
2655 
2656   // Release the listener, to break the reference cycle with it (we are the
2657   // observer of the listener).
2658   mDialogProgressListener = nullptr;
2659 
2660   return NS_OK;
2661 }
2662 
GetNeverAskFlagFromPref(const char * prefName,const char * aContentType)2663 bool nsExternalAppHandler::GetNeverAskFlagFromPref(const char* prefName,
2664                                                    const char* aContentType) {
2665   // Search the obsolete pref strings.
2666   nsAutoCString prefCString;
2667   Preferences::GetCString(prefName, prefCString);
2668   if (prefCString.IsEmpty()) {
2669     // Default is true, if not found in the pref string.
2670     return true;
2671   }
2672 
2673   NS_UnescapeURL(prefCString);
2674   nsACString::const_iterator start, end;
2675   prefCString.BeginReading(start);
2676   prefCString.EndReading(end);
2677   return !CaseInsensitiveFindInReadable(nsDependentCString(aContentType), start,
2678                                         end);
2679 }
2680 
2681 NS_IMETHODIMP
GetName(nsACString & aName)2682 nsExternalAppHandler::GetName(nsACString& aName) {
2683   aName.AssignLiteral("nsExternalAppHandler");
2684   return NS_OK;
2685 }
2686 
2687 //////////////////////////////////////////////////////////////////////////////////////////////////////////////
2688 // The following section contains our nsIMIMEService implementation and related
2689 // methods.
2690 //
2691 //////////////////////////////////////////////////////////////////////////////////////////////////////////////
2692 
2693 // nsIMIMEService methods
GetFromTypeAndExtension(const nsACString & aMIMEType,const nsACString & aFileExt,nsIMIMEInfo ** _retval)2694 NS_IMETHODIMP nsExternalHelperAppService::GetFromTypeAndExtension(
2695     const nsACString& aMIMEType, const nsACString& aFileExt,
2696     nsIMIMEInfo** _retval) {
2697   MOZ_ASSERT(!aMIMEType.IsEmpty() || !aFileExt.IsEmpty(),
2698              "Give me something to work with");
2699   MOZ_DIAGNOSTIC_ASSERT(aFileExt.FindChar('\0') == kNotFound,
2700                         "The extension should never contain null characters");
2701   LOG(("Getting mimeinfo from type '%s' ext '%s'\n",
2702        PromiseFlatCString(aMIMEType).get(),
2703        PromiseFlatCString(aFileExt).get()));
2704 
2705   *_retval = nullptr;
2706 
2707   // OK... we need a type. Get one.
2708   nsAutoCString typeToUse(aMIMEType);
2709   if (typeToUse.IsEmpty()) {
2710     nsresult rv = GetTypeFromExtension(aFileExt, typeToUse);
2711     if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE;
2712   }
2713 
2714   // We promise to only send lower case mime types to the OS
2715   ToLowerCase(typeToUse);
2716 
2717   // First, ask the OS for a mime info
2718   bool found;
2719   nsresult rv = GetMIMEInfoFromOS(typeToUse, aFileExt, &found, _retval);
2720   if (NS_WARN_IF(NS_FAILED(rv))) {
2721     return rv;
2722   }
2723 
2724   LOG(("OS gave back 0x%p - found: %i\n", *_retval, found));
2725   // If we got no mimeinfo, something went wrong. Probably lack of memory.
2726   if (!*_retval) return NS_ERROR_OUT_OF_MEMORY;
2727 
2728   // The handler service can make up for bad mime types by checking the file
2729   // extension. If the mime type is known (in extras or in the handler
2730   // service), we stop it doing so by flipping this bool to true.
2731   bool trustMIMEType = false;
2732 
2733   // Check extras - not everything we support will be known by the OS store,
2734   // unfortunately, and it may even miss some extensions that we know should
2735   // be accepted. We only do this for non-octet-stream mimetypes, because
2736   // our information for octet-stream would lead to us trying to open all such
2737   // files as Binary file with exe, com or bin extension regardless of the
2738   // real extension.
2739   if (!typeToUse.Equals(APPLICATION_OCTET_STREAM,
2740                         nsCaseInsensitiveCStringComparator)) {
2741     rv = FillMIMEInfoForMimeTypeFromExtras(typeToUse, !found, *_retval);
2742     LOG(("Searched extras (by type), rv 0x%08" PRIX32 "\n",
2743          static_cast<uint32_t>(rv)));
2744     trustMIMEType = NS_SUCCEEDED(rv);
2745     found = found || NS_SUCCEEDED(rv);
2746   }
2747 
2748   // Now, let's see if we can find something in our datastore.
2749   // This will not overwrite the OS information that interests us
2750   // (i.e. default application, default app. description)
2751   nsCOMPtr<nsIHandlerService> handlerSvc =
2752       do_GetService(NS_HANDLERSERVICE_CONTRACTID);
2753   if (handlerSvc) {
2754     bool hasHandler = false;
2755     (void)handlerSvc->Exists(*_retval, &hasHandler);
2756     if (hasHandler) {
2757       rv = handlerSvc->FillHandlerInfo(*_retval, ""_ns);
2758       LOG(("Data source: Via type: retval 0x%08" PRIx32 "\n",
2759            static_cast<uint32_t>(rv)));
2760       trustMIMEType = trustMIMEType || NS_SUCCEEDED(rv);
2761     } else {
2762       rv = NS_ERROR_NOT_AVAILABLE;
2763     }
2764 
2765     found = found || NS_SUCCEEDED(rv);
2766   }
2767 
2768   // If we still haven't found anything, try finding a match for
2769   // an extension in extras first:
2770   if (!found && !aFileExt.IsEmpty()) {
2771     rv = FillMIMEInfoForExtensionFromExtras(aFileExt, *_retval);
2772     LOG(("Searched extras (by ext), rv 0x%08" PRIX32 "\n",
2773          static_cast<uint32_t>(rv)));
2774   }
2775 
2776   // Then check the handler service - but only do so if we really do not know
2777   // the mimetype. This avoids overwriting good mimetype info with bad file
2778   // extension info.
2779   if ((!found || !trustMIMEType) && handlerSvc && !aFileExt.IsEmpty()) {
2780     nsAutoCString overrideType;
2781     rv = handlerSvc->GetTypeFromExtension(aFileExt, overrideType);
2782     if (NS_SUCCEEDED(rv) && !overrideType.IsEmpty()) {
2783       // We can't check handlerSvc->Exists() here, because we have a
2784       // overideType. That's ok, it just results in some console noise.
2785       // (If there's no handler for the override type, it throws)
2786       rv = handlerSvc->FillHandlerInfo(*_retval, overrideType);
2787       LOG(("Data source: Via ext: retval 0x%08" PRIx32 "\n",
2788            static_cast<uint32_t>(rv)));
2789       found = found || NS_SUCCEEDED(rv);
2790     }
2791   }
2792 
2793   // If we still don't have a match, at least set the file description
2794   // to `${aFileExt} File` if it's empty:
2795   if (!found && !aFileExt.IsEmpty()) {
2796     // XXXzpao This should probably be localized
2797     nsAutoCString desc(aFileExt);
2798     desc.AppendLiteral(" File");
2799     (*_retval)->SetDescription(NS_ConvertASCIItoUTF16(desc));
2800     LOG(("Falling back to 'File' file description\n"));
2801   }
2802 
2803   // Sometimes, OSes give us bad data. We have a set of forbidden extensions
2804   // for some MIME types. If the primary extension is forbidden,
2805   // overwrite it with a known-good one. See bug 1571247 for context.
2806   nsAutoCString primaryExtension;
2807   (*_retval)->GetPrimaryExtension(primaryExtension);
2808   if (!primaryExtension.EqualsIgnoreCase(PromiseFlatCString(aFileExt).get())) {
2809     if (MaybeReplacePrimaryExtension(primaryExtension, *_retval)) {
2810       (*_retval)->GetPrimaryExtension(primaryExtension);
2811     }
2812   }
2813 
2814   // Finally, check if we got a file extension and if yes, if it is an
2815   // extension on the mimeinfo, in which case we want it to be the primary one
2816   if (!aFileExt.IsEmpty()) {
2817     bool matches = false;
2818     (*_retval)->ExtensionExists(aFileExt, &matches);
2819     LOG(("Extension '%s' matches mime info: %i\n",
2820          PromiseFlatCString(aFileExt).get(), matches));
2821     if (matches) {
2822       nsAutoCString fileExt;
2823       ToLowerCase(aFileExt, fileExt);
2824       (*_retval)->SetPrimaryExtension(fileExt);
2825       primaryExtension = fileExt;
2826     }
2827   }
2828 
2829   // Overwrite with a generic description if the primary extension for the
2830   // type is in our list; these are file formats supported by Firefox and
2831   // we don't want other brands positioning themselves as the sole viewer
2832   // for a system.
2833   if (!primaryExtension.IsEmpty()) {
2834     for (const char* ext : descriptionOverwriteExtensions) {
2835       if (primaryExtension.Equals(ext)) {
2836         nsCOMPtr<nsIStringBundleService> bundleService =
2837             do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
2838         NS_ENSURE_SUCCESS(rv, rv);
2839         nsCOMPtr<nsIStringBundle> unknownContentTypeBundle;
2840         rv = bundleService->CreateBundle(
2841             "chrome://mozapps/locale/downloads/unknownContentType.properties",
2842             getter_AddRefs(unknownContentTypeBundle));
2843         if (NS_SUCCEEDED(rv)) {
2844           nsAutoCString stringName(ext);
2845           stringName.AppendLiteral("ExtHandlerDescription");
2846           nsAutoString handlerDescription;
2847           rv = unknownContentTypeBundle->GetStringFromName(stringName.get(),
2848                                                            handlerDescription);
2849           if (NS_SUCCEEDED(rv)) {
2850             (*_retval)->SetDescription(handlerDescription);
2851           }
2852         }
2853         break;
2854       }
2855     }
2856   }
2857 
2858   if (LOG_ENABLED()) {
2859     nsAutoCString type;
2860     (*_retval)->GetMIMEType(type);
2861 
2862     LOG(("MIME Info Summary: Type '%s', Primary Ext '%s'\n", type.get(),
2863          primaryExtension.get()));
2864   }
2865 
2866   return NS_OK;
2867 }
2868 
2869 NS_IMETHODIMP
GetTypeFromExtension(const nsACString & aFileExt,nsACString & aContentType)2870 nsExternalHelperAppService::GetTypeFromExtension(const nsACString& aFileExt,
2871                                                  nsACString& aContentType) {
2872   // OK. We want to try the following sources of mimetype information, in this
2873   // order:
2874   // 1. defaultMimeEntries array
2875   // 2. OS-provided information
2876   // 3. our "extras" array
2877   // 4. Information from plugins
2878   // 5. The "ext-to-type-mapping" category
2879   // Note that, we are intentionally not looking at the handler service, because
2880   // that can be affected by websites, which leads to undesired behavior.
2881 
2882   // Early return if called with an empty extension parameter
2883   if (aFileExt.IsEmpty()) {
2884     return NS_ERROR_NOT_AVAILABLE;
2885   }
2886 
2887   // First of all, check our default entries
2888   for (auto& entry : defaultMimeEntries) {
2889     if (aFileExt.LowerCaseEqualsASCII(entry.mFileExtension)) {
2890       aContentType = entry.mMimeType;
2891       return NS_OK;
2892     }
2893   }
2894 
2895   // Ask OS.
2896   if (GetMIMETypeFromOSForExtension(aFileExt, aContentType)) {
2897     return NS_OK;
2898   }
2899 
2900   // Check extras array.
2901   bool found = GetTypeFromExtras(aFileExt, aContentType);
2902   if (found) {
2903     return NS_OK;
2904   }
2905 
2906   // Let's see if an extension added something
2907   nsCOMPtr<nsICategoryManager> catMan(
2908       do_GetService("@mozilla.org/categorymanager;1"));
2909   if (catMan) {
2910     // The extension in the category entry is always stored as lowercase
2911     nsAutoCString lowercaseFileExt(aFileExt);
2912     ToLowerCase(lowercaseFileExt);
2913     // Read the MIME type from the category entry, if available
2914     nsCString type;
2915     nsresult rv =
2916         catMan->GetCategoryEntry("ext-to-type-mapping", lowercaseFileExt, type);
2917     if (NS_SUCCEEDED(rv)) {
2918       aContentType = type;
2919       return NS_OK;
2920     }
2921   }
2922 
2923   return NS_ERROR_NOT_AVAILABLE;
2924 }
2925 
GetPrimaryExtension(const nsACString & aMIMEType,const nsACString & aFileExt,nsACString & _retval)2926 NS_IMETHODIMP nsExternalHelperAppService::GetPrimaryExtension(
2927     const nsACString& aMIMEType, const nsACString& aFileExt,
2928     nsACString& _retval) {
2929   NS_ENSURE_ARG(!aMIMEType.IsEmpty());
2930 
2931   nsCOMPtr<nsIMIMEInfo> mi;
2932   nsresult rv =
2933       GetFromTypeAndExtension(aMIMEType, aFileExt, getter_AddRefs(mi));
2934   if (NS_FAILED(rv)) return rv;
2935 
2936   return mi->GetPrimaryExtension(_retval);
2937 }
2938 
GetTypeFromURI(nsIURI * aURI,nsACString & aContentType)2939 NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromURI(
2940     nsIURI* aURI, nsACString& aContentType) {
2941   NS_ENSURE_ARG_POINTER(aURI);
2942   nsresult rv = NS_ERROR_NOT_AVAILABLE;
2943   aContentType.Truncate();
2944 
2945   // First look for a file to use.  If we have one, we just use that.
2946   nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(aURI);
2947   if (fileUrl) {
2948     nsCOMPtr<nsIFile> file;
2949     rv = fileUrl->GetFile(getter_AddRefs(file));
2950     if (NS_SUCCEEDED(rv)) {
2951       rv = GetTypeFromFile(file, aContentType);
2952       if (NS_SUCCEEDED(rv)) {
2953         // we got something!
2954         return rv;
2955       }
2956     }
2957   }
2958 
2959   // Now try to get an nsIURL so we don't have to do our own parsing
2960   nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
2961   if (url) {
2962     nsAutoCString ext;
2963     rv = url->GetFileExtension(ext);
2964     if (NS_FAILED(rv)) return rv;
2965     if (ext.IsEmpty()) return NS_ERROR_NOT_AVAILABLE;
2966 
2967     UnescapeFragment(ext, url, ext);
2968 
2969     return GetTypeFromExtension(ext, aContentType);
2970   }
2971 
2972   // no url, let's give the raw spec a shot
2973   nsAutoCString specStr;
2974   rv = aURI->GetSpec(specStr);
2975   if (NS_FAILED(rv)) return rv;
2976   UnescapeFragment(specStr, aURI, specStr);
2977 
2978   // find the file extension (if any)
2979   int32_t extLoc = specStr.RFindChar('.');
2980   int32_t specLength = specStr.Length();
2981   if (-1 != extLoc && extLoc != specLength - 1 &&
2982       // nothing over 20 chars long can be sanely considered an
2983       // extension.... Dat dere would be just data.
2984       specLength - extLoc < 20) {
2985     return GetTypeFromExtension(Substring(specStr, extLoc + 1), aContentType);
2986   }
2987 
2988   // We found no information; say so.
2989   return NS_ERROR_NOT_AVAILABLE;
2990 }
2991 
GetTypeFromFile(nsIFile * aFile,nsACString & aContentType)2992 NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromFile(
2993     nsIFile* aFile, nsACString& aContentType) {
2994   NS_ENSURE_ARG_POINTER(aFile);
2995   nsresult rv;
2996 
2997   // Get the Extension
2998   nsAutoString fileName;
2999   rv = aFile->GetLeafName(fileName);
3000   if (NS_FAILED(rv)) return rv;
3001 
3002   nsAutoCString fileExt;
3003   if (!fileName.IsEmpty()) {
3004     int32_t len = fileName.Length();
3005     for (int32_t i = len; i >= 0; i--) {
3006       if (fileName[i] == char16_t('.')) {
3007         CopyUTF16toUTF8(Substring(fileName, i + 1), fileExt);
3008         break;
3009       }
3010     }
3011   }
3012 
3013   if (fileExt.IsEmpty()) return NS_ERROR_FAILURE;
3014 
3015   return GetTypeFromExtension(fileExt, aContentType);
3016 }
3017 
FillMIMEInfoForMimeTypeFromExtras(const nsACString & aContentType,bool aOverwriteDescription,nsIMIMEInfo * aMIMEInfo)3018 nsresult nsExternalHelperAppService::FillMIMEInfoForMimeTypeFromExtras(
3019     const nsACString& aContentType, bool aOverwriteDescription,
3020     nsIMIMEInfo* aMIMEInfo) {
3021   NS_ENSURE_ARG(aMIMEInfo);
3022 
3023   NS_ENSURE_ARG(!aContentType.IsEmpty());
3024 
3025   // Look for default entry with matching mime type.
3026   nsAutoCString MIMEType(aContentType);
3027   ToLowerCase(MIMEType);
3028   for (auto entry : extraMimeEntries) {
3029     if (MIMEType.Equals(entry.mMimeType)) {
3030       // This is the one. Set attributes appropriately.
3031       nsDependentCString extensions(entry.mFileExtensions);
3032       nsACString::const_iterator start, end;
3033       extensions.BeginReading(start);
3034       extensions.EndReading(end);
3035       while (start != end) {
3036         nsACString::const_iterator cursor = start;
3037         mozilla::Unused << FindCharInReadable(',', cursor, end);
3038         aMIMEInfo->AppendExtension(Substring(start, cursor));
3039         // If a comma was found, skip it for the next search.
3040         start = cursor != end ? ++cursor : cursor;
3041       }
3042 
3043       nsAutoString desc;
3044       aMIMEInfo->GetDescription(desc);
3045       if (aOverwriteDescription || desc.IsEmpty()) {
3046         aMIMEInfo->SetDescription(NS_ConvertASCIItoUTF16(entry.mDescription));
3047       }
3048       return NS_OK;
3049     }
3050   }
3051 
3052   return NS_ERROR_NOT_AVAILABLE;
3053 }
3054 
FillMIMEInfoForExtensionFromExtras(const nsACString & aExtension,nsIMIMEInfo * aMIMEInfo)3055 nsresult nsExternalHelperAppService::FillMIMEInfoForExtensionFromExtras(
3056     const nsACString& aExtension, nsIMIMEInfo* aMIMEInfo) {
3057   nsAutoCString type;
3058   bool found = GetTypeFromExtras(aExtension, type);
3059   if (!found) return NS_ERROR_NOT_AVAILABLE;
3060   return FillMIMEInfoForMimeTypeFromExtras(type, true, aMIMEInfo);
3061 }
3062 
MaybeReplacePrimaryExtension(const nsACString & aPrimaryExtension,nsIMIMEInfo * aMIMEInfo)3063 bool nsExternalHelperAppService::MaybeReplacePrimaryExtension(
3064     const nsACString& aPrimaryExtension, nsIMIMEInfo* aMIMEInfo) {
3065   for (const auto& entry : sForbiddenPrimaryExtensions) {
3066     if (aPrimaryExtension.LowerCaseEqualsASCII(entry.mFileExtension)) {
3067       nsDependentCString mime(entry.mMimeType);
3068       for (const auto& extraEntry : extraMimeEntries) {
3069         if (mime.LowerCaseEqualsASCII(extraEntry.mMimeType)) {
3070           nsDependentCString goodExts(extraEntry.mFileExtensions);
3071           int32_t commaPos = goodExts.FindChar(',');
3072           commaPos = commaPos == kNotFound ? goodExts.Length() : commaPos;
3073           auto goodExt = Substring(goodExts, 0, commaPos);
3074           aMIMEInfo->SetPrimaryExtension(goodExt);
3075           return true;
3076         }
3077       }
3078     }
3079   }
3080   return false;
3081 }
3082 
GetTypeFromExtras(const nsACString & aExtension,nsACString & aMIMEType)3083 bool nsExternalHelperAppService::GetTypeFromExtras(const nsACString& aExtension,
3084                                                    nsACString& aMIMEType) {
3085   NS_ASSERTION(!aExtension.IsEmpty(), "Empty aExtension parameter!");
3086 
3087   // Look for default entry with matching extension.
3088   nsDependentCString::const_iterator start, end, iter;
3089   int32_t numEntries = ArrayLength(extraMimeEntries);
3090   for (int32_t index = 0; index < numEntries; index++) {
3091     nsDependentCString extList(extraMimeEntries[index].mFileExtensions);
3092     extList.BeginReading(start);
3093     extList.EndReading(end);
3094     iter = start;
3095     while (start != end) {
3096       FindCharInReadable(',', iter, end);
3097       if (Substring(start, iter)
3098               .Equals(aExtension, nsCaseInsensitiveCStringComparator)) {
3099         aMIMEType = extraMimeEntries[index].mMimeType;
3100         return true;
3101       }
3102       if (iter != end) {
3103         ++iter;
3104       }
3105       start = iter;
3106     }
3107   }
3108 
3109   return false;
3110 }
3111 
GetMIMETypeFromOSForExtension(const nsACString & aExtension,nsACString & aMIMEType)3112 bool nsExternalHelperAppService::GetMIMETypeFromOSForExtension(
3113     const nsACString& aExtension, nsACString& aMIMEType) {
3114   bool found = false;
3115   nsCOMPtr<nsIMIMEInfo> mimeInfo;
3116   nsresult rv =
3117       GetMIMEInfoFromOS(""_ns, aExtension, &found, getter_AddRefs(mimeInfo));
3118   return NS_SUCCEEDED(rv) && found && mimeInfo &&
3119          NS_SUCCEEDED(mimeInfo->GetMIMEType(aMIMEType));
3120 }
3121 
GetMIMEInfoFromOS(const nsACString & aMIMEType,const nsACString & aFileExt,bool * aFound,nsIMIMEInfo ** aMIMEInfo)3122 nsresult nsExternalHelperAppService::GetMIMEInfoFromOS(
3123     const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound,
3124     nsIMIMEInfo** aMIMEInfo) {
3125   *aMIMEInfo = nullptr;
3126   *aFound = false;
3127   return NS_ERROR_NOT_IMPLEMENTED;
3128 }
3129