/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include "nsOSHelperAppService.h" #include "nsMIMEInfoUnix.h" #ifdef MOZ_WIDGET_GTK # include "nsGNOMERegistry.h" # ifdef MOZ_BUILD_APP_IS_BROWSER # include "nsIToolkitShellService.h" # include "nsIGNOMEShellService.h" # endif #endif #include "nsISupports.h" #include "nsString.h" #include "nsReadableUtils.h" #include "nsUnicharUtils.h" #include "nsIFileStreams.h" #include "nsILineInputStream.h" #include "nsIFile.h" #include "nsIProcess.h" #include "nsNetCID.h" #include "nsXPCOM.h" #include "nsComponentManagerUtils.h" #include "nsCRT.h" #include "nsDirectoryServiceDefs.h" #include "nsDirectoryServiceUtils.h" #include "nsXULAppAPI.h" #include "ContentHandlerService.h" #include "prenv.h" // for PR_GetEnv() #include "mozilla/EnumeratedRange.h" #include "mozilla/Preferences.h" #include "mozilla/ClearOnShutdown.h" #include "nsMimeTypes.h" #include using namespace mozilla; #define LOG(...) \ MOZ_LOG(nsOSHelperAppService::sLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) #define LOG_ENABLED() \ MOZ_LOG_TEST(nsOSHelperAppService::sLog, mozilla::LogLevel::Debug) static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter, const nsAString::const_iterator& aEnd_iter); static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter, nsAString::const_iterator& aMajorTypeStart, nsAString::const_iterator& aMajorTypeEnd, nsAString::const_iterator& aMinorTypeStart, nsAString::const_iterator& aMinorTypeEnd, const nsAString::const_iterator& aEnd_iter); inline bool IsNetscapeFormat(const nsACString& aBuffer); nsOSHelperAppService::~nsOSHelperAppService() {} /* * Take a command with all the mailcap escapes in it and unescape it * Ideally this needs the mime type, mime type options, and location of the * temporary file, but this last can't be got from here */ // static nsresult nsOSHelperAppService::UnescapeCommand(const nsAString& aEscapedCommand, const nsAString& aMajorType, const nsAString& aMinorType, nsACString& aUnEscapedCommand) { LOG("-- UnescapeCommand"); LOG("Command to escape: '%s'\n", NS_LossyConvertUTF16toASCII(aEscapedCommand).get()); // XXX This function will need to get the mime type and various stuff like // that being passed in to work properly LOG( ("UnescapeCommand really needs some work -- it should actually do some " "unescaping\n")); CopyUTF16toUTF8(aEscapedCommand, aUnEscapedCommand); LOG("Escaped command: '%s'\n", PromiseFlatCString(aUnEscapedCommand).get()); return NS_OK; } /* Put aSemicolon_iter at the first non-escaped semicolon after * aStart_iter but before aEnd_iter */ static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter, const nsAString::const_iterator& aEnd_iter) { bool semicolonFound = false; while (aSemicolon_iter != aEnd_iter && !semicolonFound) { switch (*aSemicolon_iter) { case '\\': aSemicolon_iter.advance(2); break; case ';': semicolonFound = true; break; default: ++aSemicolon_iter; break; } } return NS_OK; } static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter, nsAString::const_iterator& aMajorTypeStart, nsAString::const_iterator& aMajorTypeEnd, nsAString::const_iterator& aMinorTypeStart, nsAString::const_iterator& aMinorTypeEnd, const nsAString::const_iterator& aEnd_iter) { nsAString::const_iterator iter(aStart_iter); // skip leading whitespace while (iter != aEnd_iter && nsCRT::IsAsciiSpace(*iter)) { ++iter; } if (iter == aEnd_iter) { return NS_ERROR_INVALID_ARG; } aMajorTypeStart = iter; // find major/minor separator ('/') while (iter != aEnd_iter && *iter != '/') { ++iter; } if (iter == aEnd_iter) { return NS_ERROR_INVALID_ARG; } aMajorTypeEnd = iter; // skip '/' ++iter; if (iter == aEnd_iter) { return NS_ERROR_INVALID_ARG; } aMinorTypeStart = iter; // find end of minor type, delimited by whitespace or ';' while (iter != aEnd_iter && !nsCRT::IsAsciiSpace(*iter) && *iter != ';') { ++iter; } aMinorTypeEnd = iter; return NS_OK; } // TODO: We should consider not only caching the file location but maybe the // file contents too? enum class FileKind { PrivateMimeTypes = 0, GlobalMimeTypes, PrivateMailCap, GlobalMailCap, Count, }; struct FileLocationCache { struct Entry { bool mIsCached = false; nsresult mResult = NS_OK; nsString mLocation; void Clear() { *this = {}; } }; EnumeratedArray mEntries; static const char* PrefFor(FileKind aKind) { switch (aKind) { case FileKind::GlobalMimeTypes: return "helpers.global_mime_types_file"; case FileKind::PrivateMimeTypes: return "helpers.private_mime_types_file"; case FileKind::GlobalMailCap: return "helpers.global_mailcap_file"; case FileKind::PrivateMailCap: return "helpers.private_mailcap_file"; case FileKind::Count: break; } MOZ_ASSERT_UNREACHABLE("Unknown file kind"); return ""; } void Clear() { for (auto& entry : mEntries) { entry.Clear(); } } static void PrefChangeCallback(const char*, void*) { Get().Clear(); } static FileLocationCache& Get() { static FileLocationCache sCache; static std::once_flag flag; std::call_once(flag, [&] { for (auto kind : MakeEnumeratedRange(FileKind::PrivateMimeTypes, FileKind::Count)) { Preferences::RegisterCallback(PrefChangeCallback, nsDependentCString(PrefFor(kind))); } RunOnShutdown([] { sCache.Clear(); for (auto kind : MakeEnumeratedRange(FileKind::PrivateMimeTypes, FileKind::Count)) { Preferences::UnregisterCallback(PrefChangeCallback, nsDependentCString(PrefFor(kind))); } }); }); return sCache; } Entry& EntryFor(FileKind aKind) { return mEntries[aKind]; } }; // The lookup order is: // 1) user pref // 2) env var (if any) // 3) pref static nsresult DoGetFileLocation(FileKind aKind, nsAString& aFileLocation) { LOG("-- GetFileLocation(%d)\n", int(aKind)); aFileLocation.Truncate(); const char* envVar = [&]() -> const char* { switch (aKind) { case FileKind::PrivateMailCap: return "PERSONAL_MAILCAP"; case FileKind::GlobalMailCap: return "MAILCAP"; default: return nullptr; } }(); const char* prefName = FileLocationCache::PrefFor(aKind); if (envVar) { // If we have an env var we should check whether the pref is a user pref. If // we do not, we don't care. if (Preferences::HasUserValue(prefName) && NS_SUCCEEDED(Preferences::GetString(prefName, aFileLocation))) { return NS_OK; } char* envValue = PR_GetEnv(envVar); if (envValue && *envValue) { // the pref is in the system charset and it's a filepath... The // natural way to do the charset conversion is by just initing // an nsIFile with the native path and asking it for the Unicode // version. nsresult rv; nsCOMPtr file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); NS_ENSURE_SUCCESS(rv, rv); rv = file->InitWithNativePath(nsDependentCString(envValue)); NS_ENSURE_SUCCESS(rv, rv); return file->GetPath(aFileLocation); } } return Preferences::GetString(prefName, aFileLocation); } static nsresult GetFileLocation(FileKind aKind, nsAString& aFileLocation) { MOZ_ASSERT(NS_IsMainThread()); auto& entry = FileLocationCache::Get().EntryFor(aKind); if (!entry.mIsCached) { entry.mIsCached = true; entry.mResult = DoGetFileLocation(aKind, entry.mLocation); } aFileLocation = entry.mLocation; return entry.mResult; } /* Get the mime.types file names from prefs and look up info in them based on extension */ // static nsresult nsOSHelperAppService::LookUpTypeAndDescription( const nsAString& aFileExtension, nsAString& aMajorType, nsAString& aMinorType, nsAString& aDescription, bool aUserData) { LOG("-- LookUpTypeAndDescription for extension '%s'\n", NS_LossyConvertUTF16toASCII(aFileExtension).get()); nsAutoString mimeFileName; auto kind = aUserData ? FileKind::PrivateMimeTypes : FileKind::GlobalMimeTypes; nsresult rv = GetFileLocation(kind, mimeFileName); if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { rv = GetTypeAndDescriptionFromMimetypesFile( mimeFileName, aFileExtension, aMajorType, aMinorType, aDescription); } else { rv = NS_ERROR_NOT_AVAILABLE; } return rv; } inline bool IsNetscapeFormat(const nsACString& aBuffer) { return StringBeginsWith( aBuffer, nsLiteralCString( "#--Netscape Communications Corporation MIME Information")) || StringBeginsWith(aBuffer, "#--MCOM MIME Information"_ns); } /* * Create a file stream and line input stream for the filename. * Leaves the first line of the file in aBuffer and sets the format to * true for netscape files and false for normail ones */ // static nsresult nsOSHelperAppService::CreateInputStream( const nsAString& aFilename, nsIFileInputStream** aFileInputStream, nsILineInputStream** aLineInputStream, nsACString& aBuffer, bool* aNetscapeFormat, bool* aMore) { LOG("-- CreateInputStream"); nsresult rv = NS_OK; nsCOMPtr file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); if (NS_FAILED(rv)) return rv; rv = file->InitWithPath(aFilename); if (NS_FAILED(rv)) return rv; nsCOMPtr fileStream( do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv)); if (NS_FAILED(rv)) return rv; rv = fileStream->Init(file, -1, -1, false); if (NS_FAILED(rv)) return rv; nsCOMPtr lineStream(do_QueryInterface(fileStream, &rv)); if (NS_FAILED(rv)) { LOG("Interface trouble in stream land!"); return rv; } rv = lineStream->ReadLine(aBuffer, aMore); if (NS_FAILED(rv)) { fileStream->Close(); return rv; } *aNetscapeFormat = IsNetscapeFormat(aBuffer); *aFileInputStream = fileStream; NS_ADDREF(*aFileInputStream); *aLineInputStream = lineStream; NS_ADDREF(*aLineInputStream); return NS_OK; } /* Open the file, read the first line, decide what type of file it is, then get info based on extension */ // static nsresult nsOSHelperAppService::GetTypeAndDescriptionFromMimetypesFile( const nsAString& aFilename, const nsAString& aFileExtension, nsAString& aMajorType, nsAString& aMinorType, nsAString& aDescription) { LOG("-- GetTypeAndDescriptionFromMimetypesFile\n"); LOG("Getting type and description from types file '%s'\n", NS_LossyConvertUTF16toASCII(aFilename).get()); LOG("Using extension '%s'\n", NS_LossyConvertUTF16toASCII(aFileExtension).get()); nsCOMPtr mimeFile; nsCOMPtr mimeTypes; bool netscapeFormat; nsAutoString buf; nsAutoCString cBuf; bool more = false; nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), getter_AddRefs(mimeTypes), cBuf, &netscapeFormat, &more); if (NS_FAILED(rv)) { return rv; } nsAutoString extensions; nsAutoStringN<101> entry; nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, descriptionStart, descriptionEnd; do { CopyASCIItoUTF16(cBuf, buf); // read through, building up an entry. If we finish an entry, check for // a match and return out of the loop if we match // skip comments and empty lines if (!buf.IsEmpty() && buf.First() != '#') { entry.Append(buf); if (entry.Last() == '\\') { entry.Truncate(entry.Length() - 1); entry.Append(char16_t( ' ')); // in case there is no trailing whitespace on this line } else { // we have a full entry LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get()); if (netscapeFormat) { rv = ParseNetscapeMIMETypesEntry( entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, extensions, descriptionStart, descriptionEnd); if (NS_FAILED(rv)) { // We sometimes get things like RealPlayer appending // "normal" entries to "Netscape" .mime.types files. Try // to handle that. Bug 106381. LOG("Bogus entry; trying 'normal' mode\n"); rv = ParseNormalMIMETypesEntry( entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, extensions, descriptionStart, descriptionEnd); } } else { rv = ParseNormalMIMETypesEntry( entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, extensions, descriptionStart, descriptionEnd); if (NS_FAILED(rv)) { // We sometimes get things like StarOffice prepending // "normal" entries to "Netscape" .mime.types files. Try // to handle that. Bug 136670. LOG("Bogus entry; trying 'Netscape' mode\n"); rv = ParseNetscapeMIMETypesEntry( entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, extensions, descriptionStart, descriptionEnd); } } if (NS_SUCCEEDED(rv)) { // entry parses nsAString::const_iterator start, end; extensions.BeginReading(start); extensions.EndReading(end); nsAString::const_iterator iter(start); while (start != end) { FindCharInReadable(',', iter, end); if (Substring(start, iter) .Equals(aFileExtension, nsCaseInsensitiveStringComparator)) { // it's a match. Assign the type and description and run aMajorType.Assign(Substring(majorTypeStart, majorTypeEnd)); aMinorType.Assign(Substring(minorTypeStart, minorTypeEnd)); aDescription.Assign(Substring(descriptionStart, descriptionEnd)); mimeFile->Close(); return NS_OK; } if (iter != end) { ++iter; } start = iter; } } else { LOG("Failed to parse entry: %s\n", NS_LossyConvertUTF16toASCII(entry).get()); } // truncate the entry for the next iteration entry.Truncate(); } } if (!more) { rv = NS_ERROR_NOT_AVAILABLE; break; } // read the next line rv = mimeTypes->ReadLine(cBuf, &more); } while (NS_SUCCEEDED(rv)); mimeFile->Close(); return rv; } /* Get the mime.types file names from prefs and look up info in them based on mimetype */ // static nsresult nsOSHelperAppService::LookUpExtensionsAndDescription( const nsAString& aMajorType, const nsAString& aMinorType, nsAString& aFileExtensions, nsAString& aDescription) { LOG("-- LookUpExtensionsAndDescription for type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(), NS_LossyConvertUTF16toASCII(aMinorType).get()); nsAutoString mimeFileName; nsresult rv = GetFileLocation(FileKind::PrivateMimeTypes, mimeFileName); if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { rv = GetExtensionsAndDescriptionFromMimetypesFile( mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription); } else { rv = NS_ERROR_NOT_AVAILABLE; } if (NS_FAILED(rv) || aFileExtensions.IsEmpty()) { rv = GetFileLocation(FileKind::GlobalMimeTypes, mimeFileName); if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { rv = GetExtensionsAndDescriptionFromMimetypesFile( mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription); } else { rv = NS_ERROR_NOT_AVAILABLE; } } return rv; } /* Open the file, read the first line, decide what type of file it is, then get info based on extension */ // static nsresult nsOSHelperAppService::GetExtensionsAndDescriptionFromMimetypesFile( const nsAString& aFilename, const nsAString& aMajorType, const nsAString& aMinorType, nsAString& aFileExtensions, nsAString& aDescription) { LOG("-- GetExtensionsAndDescriptionFromMimetypesFile\n"); LOG("Getting extensions and description from types file '%s'\n", NS_LossyConvertUTF16toASCII(aFilename).get()); LOG("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(), NS_LossyConvertUTF16toASCII(aMinorType).get()); nsCOMPtr mimeFile; nsCOMPtr mimeTypes; bool netscapeFormat; nsAutoCString cBuf; nsAutoString buf; bool more = false; nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), getter_AddRefs(mimeTypes), cBuf, &netscapeFormat, &more); if (NS_FAILED(rv)) { return rv; } nsAutoString extensions; nsAutoStringN<101> entry; nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, descriptionStart, descriptionEnd; do { CopyASCIItoUTF16(cBuf, buf); // read through, building up an entry. If we finish an entry, check for // a match and return out of the loop if we match // skip comments and empty lines if (!buf.IsEmpty() && buf.First() != '#') { entry.Append(buf); if (entry.Last() == '\\') { entry.Truncate(entry.Length() - 1); entry.Append(char16_t( ' ')); // in case there is no trailing whitespace on this line } else { // we have a full entry LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get()); if (netscapeFormat) { rv = ParseNetscapeMIMETypesEntry( entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, extensions, descriptionStart, descriptionEnd); if (NS_FAILED(rv)) { // We sometimes get things like RealPlayer appending // "normal" entries to "Netscape" .mime.types files. Try // to handle that. Bug 106381. LOG("Bogus entry; trying 'normal' mode\n"); rv = ParseNormalMIMETypesEntry( entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, extensions, descriptionStart, descriptionEnd); } } else { rv = ParseNormalMIMETypesEntry( entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, extensions, descriptionStart, descriptionEnd); if (NS_FAILED(rv)) { // We sometimes get things like StarOffice prepending // "normal" entries to "Netscape" .mime.types files. Try // to handle that. Bug 136670. LOG("Bogus entry; trying 'Netscape' mode\n"); rv = ParseNetscapeMIMETypesEntry( entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, extensions, descriptionStart, descriptionEnd); } } if (NS_SUCCEEDED(rv) && Substring(majorTypeStart, majorTypeEnd) .Equals(aMajorType, nsCaseInsensitiveStringComparator) && Substring(minorTypeStart, minorTypeEnd) .Equals(aMinorType, nsCaseInsensitiveStringComparator)) { // it's a match aFileExtensions.Assign(extensions); aDescription.Assign(Substring(descriptionStart, descriptionEnd)); mimeFile->Close(); return NS_OK; } if (NS_FAILED(rv)) { LOG("Failed to parse entry: %s\n", NS_LossyConvertUTF16toASCII(entry).get()); } entry.Truncate(); } } if (!more) { rv = NS_ERROR_NOT_AVAILABLE; break; } // read the next line rv = mimeTypes->ReadLine(cBuf, &more); } while (NS_SUCCEEDED(rv)); mimeFile->Close(); return rv; } /* * This parses a Netscape format mime.types entry. There are two * possible formats: * * type=foo/bar; options exts="baz" description="Some type" * * and * * type=foo/bar; options description="Some type" exts="baz" */ // static nsresult nsOSHelperAppService::ParseNetscapeMIMETypesEntry( const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, nsAString::const_iterator& aMajorTypeEnd, nsAString::const_iterator& aMinorTypeStart, nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, nsAString::const_iterator& aDescriptionStart, nsAString::const_iterator& aDescriptionEnd) { LOG("-- ParseNetscapeMIMETypesEntry\n"); NS_ASSERTION(!aEntry.IsEmpty(), "Empty Netscape MIME types entry being parsed."); nsAString::const_iterator start_iter, end_iter, match_start, match_end; aEntry.BeginReading(start_iter); aEntry.EndReading(end_iter); // skip trailing whitespace do { --end_iter; } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter)); // if we're pointing to a quote, don't advance -- we don't want to // include the quote.... if (*end_iter != '"') ++end_iter; match_start = start_iter; match_end = end_iter; // Get the major and minor types // First the major type if (!FindInReadable(u"type="_ns, match_start, match_end)) { return NS_ERROR_FAILURE; } match_start = match_end; while (match_end != end_iter && *match_end != '/') { ++match_end; } if (match_end == end_iter) { return NS_ERROR_FAILURE; } aMajorTypeStart = match_start; aMajorTypeEnd = match_end; // now the minor type if (++match_end == end_iter) { return NS_ERROR_FAILURE; } match_start = match_end; while (match_end != end_iter && !nsCRT::IsAsciiSpace(*match_end) && *match_end != ';') { ++match_end; } if (match_end == end_iter) { return NS_ERROR_FAILURE; } aMinorTypeStart = match_start; aMinorTypeEnd = match_end; // ignore everything up to the end of the mime type from here on start_iter = match_end; // get the extensions match_start = match_end; match_end = end_iter; if (FindInReadable(u"exts="_ns, match_start, match_end)) { nsAString::const_iterator extStart, extEnd; if (match_end == end_iter || (*match_end == '"' && ++match_end == end_iter)) { return NS_ERROR_FAILURE; } extStart = match_end; match_start = extStart; match_end = end_iter; if (FindInReadable(u"desc=\""_ns, match_start, match_end)) { // exts= before desc=, so we have to find the actual end of the extensions extEnd = match_start; if (extEnd == extStart) { return NS_ERROR_FAILURE; } do { --extEnd; } while (extEnd != extStart && nsCRT::IsAsciiSpace(*extEnd)); if (extEnd != extStart && *extEnd == '"') { --extEnd; } } else { // desc= before exts=, so we can use end_iter as the end of the extensions extEnd = end_iter; } aExtensions = Substring(extStart, extEnd); } else { // no extensions aExtensions.Truncate(); } // get the description match_start = start_iter; match_end = end_iter; if (FindInReadable(u"desc=\""_ns, match_start, match_end)) { aDescriptionStart = match_end; match_start = aDescriptionStart; match_end = end_iter; if (FindInReadable(u"exts="_ns, match_start, match_end)) { // exts= after desc=, so have to find actual end of description aDescriptionEnd = match_start; if (aDescriptionEnd == aDescriptionStart) { return NS_ERROR_FAILURE; } do { --aDescriptionEnd; } while (aDescriptionEnd != aDescriptionStart && nsCRT::IsAsciiSpace(*aDescriptionEnd)); } else { // desc= after exts=, so use end_iter for the description end aDescriptionEnd = end_iter; } } else { // no description aDescriptionStart = start_iter; aDescriptionEnd = start_iter; } return NS_OK; } /* * This parses a normal format mime.types entry. The format is: * * major/minor ext1 ext2 ext3 */ // static nsresult nsOSHelperAppService::ParseNormalMIMETypesEntry( const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, nsAString::const_iterator& aMajorTypeEnd, nsAString::const_iterator& aMinorTypeStart, nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, nsAString::const_iterator& aDescriptionStart, nsAString::const_iterator& aDescriptionEnd) { LOG("-- ParseNormalMIMETypesEntry\n"); NS_ASSERTION(!aEntry.IsEmpty(), "Empty Normal MIME types entry being parsed."); nsAString::const_iterator start_iter, end_iter, iter; aEntry.BeginReading(start_iter); aEntry.EndReading(end_iter); // no description aDescriptionStart = start_iter; aDescriptionEnd = start_iter; // skip leading whitespace while (start_iter != end_iter && nsCRT::IsAsciiSpace(*start_iter)) { ++start_iter; } if (start_iter == end_iter) { return NS_ERROR_FAILURE; } // skip trailing whitespace do { --end_iter; } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter)); ++end_iter; // point to first whitespace char (or to end of string) iter = start_iter; // get the major type if (!FindCharInReadable('/', iter, end_iter)) return NS_ERROR_FAILURE; nsAString::const_iterator equals_sign_iter(start_iter); if (FindCharInReadable('=', equals_sign_iter, iter)) return NS_ERROR_FAILURE; // see bug 136670 aMajorTypeStart = start_iter; aMajorTypeEnd = iter; // get the minor type if (++iter == end_iter) { return NS_ERROR_FAILURE; } start_iter = iter; while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) { ++iter; } aMinorTypeStart = start_iter; aMinorTypeEnd = iter; // get the extensions aExtensions.Truncate(); while (iter != end_iter) { while (iter != end_iter && nsCRT::IsAsciiSpace(*iter)) { ++iter; } start_iter = iter; while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) { ++iter; } aExtensions.Append(Substring(start_iter, iter)); if (iter != end_iter) { // not the last extension aExtensions.Append(char16_t(',')); } } return NS_OK; } // static nsresult nsOSHelperAppService::LookUpHandlerAndDescription( const nsAString& aMajorType, const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags) { // The mailcap lookup is two-pass to handle the case of mailcap files // that have something like: // // text/*; emacs %s // text/rtf; soffice %s // // in that order. We want to pick up "soffice" for text/rtf in such cases nsresult rv = DoLookUpHandlerAndDescription( aMajorType, aMinorType, aHandler, aDescription, aMozillaFlags, true); if (NS_FAILED(rv)) { rv = DoLookUpHandlerAndDescription(aMajorType, aMinorType, aHandler, aDescription, aMozillaFlags, false); } // maybe we have an entry for "aMajorType/*"? if (NS_FAILED(rv)) { rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler, aDescription, aMozillaFlags, true); } if (NS_FAILED(rv)) { rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler, aDescription, aMozillaFlags, false); } return rv; } // static nsresult nsOSHelperAppService::DoLookUpHandlerAndDescription( const nsAString& aMajorType, const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags, bool aUserData) { LOG("-- LookUpHandlerAndDescription for type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(), NS_LossyConvertUTF16toASCII(aMinorType).get()); nsAutoString mailcapFileName; const auto kind = aUserData ? FileKind::PrivateMailCap : FileKind::GlobalMailCap; nsresult rv = GetFileLocation(kind, mailcapFileName); if (NS_FAILED(rv) || mailcapFileName.IsEmpty()) { return NS_ERROR_NOT_AVAILABLE; } return GetHandlerAndDescriptionFromMailcapFile(mailcapFileName, aMajorType, aMinorType, aHandler, aDescription, aMozillaFlags); } // static nsresult nsOSHelperAppService::GetHandlerAndDescriptionFromMailcapFile( const nsAString& aFilename, const nsAString& aMajorType, const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags) { LOG("-- GetHandlerAndDescriptionFromMailcapFile\n"); LOG("Getting handler and description from mailcap file '%s'\n", NS_LossyConvertUTF16toASCII(aFilename).get()); LOG("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(), NS_LossyConvertUTF16toASCII(aMinorType).get()); nsresult rv = NS_OK; bool more = false; nsCOMPtr file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); if (NS_FAILED(rv)) return rv; rv = file->InitWithPath(aFilename); if (NS_FAILED(rv)) return rv; nsCOMPtr mailcapFile( do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv)); if (NS_FAILED(rv)) return rv; rv = mailcapFile->Init(file, -1, -1, false); if (NS_FAILED(rv)) return rv; nsCOMPtr mailcap(do_QueryInterface(mailcapFile, &rv)); if (NS_FAILED(rv)) { LOG("Interface trouble in stream land!"); return rv; } nsAutoStringN<129> entry; nsAutoStringN<81> buffer; nsAutoCStringN<81> cBuffer; rv = mailcap->ReadLine(cBuffer, &more); if (NS_FAILED(rv)) { mailcapFile->Close(); return rv; } do { // return on end-of-file in the loop CopyASCIItoUTF16(cBuffer, buffer); if (!buffer.IsEmpty() && buffer.First() != '#') { entry.Append(buffer); if (entry.Last() == '\\') { // entry continues on next line entry.Truncate(entry.Length() - 1); entry.Append(char16_t( ' ')); // in case there is no trailing whitespace on this line } else { // we have a full entry in entry. Check it for the type LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get()); nsAString::const_iterator semicolon_iter, start_iter, end_iter, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd; entry.BeginReading(start_iter); entry.EndReading(end_iter); semicolon_iter = start_iter; FindSemicolon(semicolon_iter, end_iter); if (semicolon_iter != end_iter) { // we have something resembling a valid entry rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, semicolon_iter); if (NS_SUCCEEDED(rv) && Substring(majorTypeStart, majorTypeEnd) .Equals(aMajorType, nsCaseInsensitiveStringComparator) && Substring(minorTypeStart, minorTypeEnd) .Equals(aMinorType, nsCaseInsensitiveStringComparator)) { // we have a match bool match = true; ++semicolon_iter; // point at the first char past the semicolon start_iter = semicolon_iter; // handler string starts here FindSemicolon(semicolon_iter, end_iter); while (start_iter != semicolon_iter && nsCRT::IsAsciiSpace(*start_iter)) { ++start_iter; } LOG("The real handler is: '%s'\n", NS_LossyConvertUTF16toASCII( Substring(start_iter, semicolon_iter)) .get()); // XXX ugly hack. Just grab the executable name nsAString::const_iterator end_handler_iter = semicolon_iter; nsAString::const_iterator end_executable_iter = start_iter; while (end_executable_iter != end_handler_iter && !nsCRT::IsAsciiSpace(*end_executable_iter)) { ++end_executable_iter; } // XXX End ugly hack aHandler = Substring(start_iter, end_executable_iter); nsAString::const_iterator start_option_iter, end_optionname_iter, equal_sign_iter; bool equalSignFound; while (match && semicolon_iter != end_iter && ++semicolon_iter != end_iter) { // there are options left and we still match start_option_iter = semicolon_iter; // skip over leading whitespace while (start_option_iter != end_iter && nsCRT::IsAsciiSpace(*start_option_iter)) { ++start_option_iter; } if (start_option_iter == end_iter) { // nothing actually here break; } semicolon_iter = start_option_iter; FindSemicolon(semicolon_iter, end_iter); equal_sign_iter = start_option_iter; equalSignFound = false; while (equal_sign_iter != semicolon_iter && !equalSignFound) { switch (*equal_sign_iter) { case '\\': equal_sign_iter.advance(2); break; case '=': equalSignFound = true; break; default: ++equal_sign_iter; break; } } end_optionname_iter = start_option_iter; // find end of option name while (end_optionname_iter != equal_sign_iter && !nsCRT::IsAsciiSpace(*end_optionname_iter)) { ++end_optionname_iter; } nsDependentSubstring optionName(start_option_iter, end_optionname_iter); if (equalSignFound) { // This is an option that has a name and value if (optionName.EqualsLiteral("description")) { aDescription = Substring(++equal_sign_iter, semicolon_iter); } else if (optionName.EqualsLiteral("x-mozilla-flags")) { aMozillaFlags = Substring(++equal_sign_iter, semicolon_iter); } else if (optionName.EqualsLiteral("test")) { nsAutoCString testCommand; rv = UnescapeCommand( Substring(++equal_sign_iter, semicolon_iter), aMajorType, aMinorType, testCommand); if (NS_FAILED(rv)) continue; nsCOMPtr process = do_CreateInstance(NS_PROCESS_CONTRACTID, &rv); if (NS_FAILED(rv)) continue; nsCOMPtr file( do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); if (NS_FAILED(rv)) continue; rv = file->InitWithNativePath("/bin/sh"_ns); if (NS_FAILED(rv)) continue; rv = process->Init(file); if (NS_FAILED(rv)) continue; const char* args[] = {"-c", testCommand.get()}; LOG("Running Test: %s\n", testCommand.get()); rv = process->Run(true, args, 2); if (NS_FAILED(rv)) continue; int32_t exitValue; rv = process->GetExitValue(&exitValue); if (NS_FAILED(rv)) continue; LOG("Exit code: %d\n", exitValue); if (exitValue) { match = false; } } } else { // This is an option that just has a name but no value (eg // "copiousoutput") if (optionName.EqualsLiteral("needsterminal")) { match = false; } } } if (match) { // we did not fail any test clauses; all is good // get out of here mailcapFile->Close(); return NS_OK; } // pretend that this match never happened aDescription.Truncate(); aMozillaFlags.Truncate(); aHandler.Truncate(); } } // zero out the entry for the next cycle entry.Truncate(); } } if (!more) { rv = NS_ERROR_NOT_AVAILABLE; break; } rv = mailcap->ReadLine(cBuffer, &more); } while (NS_SUCCEEDED(rv)); mailcapFile->Close(); return rv; } nsresult nsOSHelperAppService::OSProtocolHandlerExists( const char* aProtocolScheme, bool* aHandlerExists) { nsresult rv = NS_OK; if (!XRE_IsContentProcess()) { #ifdef MOZ_WIDGET_GTK // Check the GNOME registry for a protocol handler *aHandlerExists = nsGNOMERegistry::HandlerExists(aProtocolScheme); #else *aHandlerExists = false; #endif } else { *aHandlerExists = false; nsCOMPtr handlerSvc = do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv); if (NS_SUCCEEDED(rv) && handlerSvc) { rv = handlerSvc->ExistsForProtocolOS(nsCString(aProtocolScheme), aHandlerExists); } } return rv; } NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription( const nsACString& aScheme, nsAString& _retval) { #ifdef MOZ_WIDGET_GTK nsGNOMERegistry::GetAppDescForScheme(aScheme, _retval); return _retval.IsEmpty() ? NS_ERROR_NOT_AVAILABLE : NS_OK; #else return NS_ERROR_NOT_AVAILABLE; #endif } NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol( const nsACString& aScheme, bool* _retval) { *_retval = false; #if defined(MOZ_BUILD_APP_IS_BROWSER) && defined(MOZ_WIDGET_GTK) if (nsCOMPtr shell = do_GetService(NS_TOOLKITSHELLSERVICE_CONTRACTID)) { return shell->IsDefaultForScheme(aScheme, _retval); } #endif return NS_OK; } nsresult nsOSHelperAppService::GetFileTokenForPath( const char16_t* platformAppPath, nsIFile** aFile) { LOG("-- nsOSHelperAppService::GetFileTokenForPath: '%s'\n", NS_LossyConvertUTF16toASCII(platformAppPath).get()); if (!*platformAppPath) { // empty filename--return error NS_WARNING("Empty filename passed in."); return NS_ERROR_INVALID_ARG; } // first check if the base class implementation finds anything nsresult rv = nsExternalHelperAppService::GetFileTokenForPath(platformAppPath, aFile); if (NS_SUCCEEDED(rv)) return rv; // If the reason for failure was that the file doesn't exist, return too // (because it means the path was absolute, and so that we shouldn't search in // the path) if (rv == NS_ERROR_FILE_NOT_FOUND) return rv; // If we get here, we really should have a relative path. NS_ASSERTION(*platformAppPath != char16_t('/'), "Unexpected absolute path"); nsCOMPtr localFile(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID)); if (!localFile) return NS_ERROR_NOT_INITIALIZED; bool exists = false; // ugly hack. Walk the PATH variable... char* unixpath = PR_GetEnv("PATH"); nsAutoCString path(unixpath); const char* start_iter = path.BeginReading(start_iter); const char* colon_iter = start_iter; const char* end_iter = path.EndReading(end_iter); while (start_iter != end_iter && !exists) { while (colon_iter != end_iter && *colon_iter != ':') { ++colon_iter; } localFile->InitWithNativePath(Substring(start_iter, colon_iter)); rv = localFile->AppendRelativePath(nsDependentString(platformAppPath)); // Failing AppendRelativePath is a bad thing - it should basically always // succeed given a relative path. Show a warning if it does fail. // To prevent infinite loops when it does fail, return at this point. NS_ENSURE_SUCCESS(rv, rv); localFile->Exists(&exists); if (!exists) { if (colon_iter == end_iter) { break; } ++colon_iter; start_iter = colon_iter; } } if (exists) { rv = NS_OK; } else { rv = NS_ERROR_NOT_AVAILABLE; } *aFile = localFile; NS_IF_ADDREF(*aFile); return rv; } already_AddRefed nsOSHelperAppService::GetFromExtension( const nsCString& aFileExt) { // if the extension is empty, return immediately if (aFileExt.IsEmpty()) { return nullptr; } LOG("Here we do an extension lookup for '%s'\n", aFileExt.get()); nsAutoString majorType, minorType, mime_types_description, mailcap_description, handler, mozillaFlags; nsresult rv = LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType, minorType, mime_types_description, true); if (NS_FAILED(rv) || majorType.IsEmpty()) { #ifdef MOZ_WIDGET_GTK LOG("Looking in GNOME registry\n"); RefPtr gnomeInfo = nsGNOMERegistry::GetFromExtension(aFileExt); if (gnomeInfo) { LOG("Got MIMEInfo from GNOME registry\n"); return gnomeInfo.forget(); } #endif rv = LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType, minorType, mime_types_description, false); } if (NS_FAILED(rv)) { return nullptr; } NS_LossyConvertUTF16toASCII asciiMajorType(majorType); NS_LossyConvertUTF16toASCII asciiMinorType(minorType); LOG("Type/Description results: majorType='%s', minorType='%s', " "description='%s'\n", asciiMajorType.get(), asciiMinorType.get(), NS_LossyConvertUTF16toASCII(mime_types_description).get()); if (majorType.IsEmpty() && minorType.IsEmpty()) { // we didn't get a type mapping, so we can't do anything useful return nullptr; } nsAutoCString mimeType(asciiMajorType + "/"_ns + asciiMinorType); RefPtr mimeInfo = new nsMIMEInfoUnix(mimeType); mimeInfo->AppendExtension(aFileExt); rv = LookUpHandlerAndDescription(majorType, minorType, handler, mailcap_description, mozillaFlags); LOG("Handler/Description results: handler='%s', description='%s', " "mozillaFlags='%s'\n", NS_LossyConvertUTF16toASCII(handler).get(), NS_LossyConvertUTF16toASCII(mailcap_description).get(), NS_LossyConvertUTF16toASCII(mozillaFlags).get()); mailcap_description.Trim(" \t\""); mozillaFlags.Trim(" \t"); if (!mime_types_description.IsEmpty()) { mimeInfo->SetDescription(mime_types_description); } else { mimeInfo->SetDescription(mailcap_description); } if (NS_SUCCEEDED(rv) && handler.IsEmpty()) { rv = NS_ERROR_NOT_AVAILABLE; } if (NS_SUCCEEDED(rv)) { nsCOMPtr handlerFile; rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile)); if (NS_SUCCEEDED(rv)) { mimeInfo->SetDefaultApplication(handlerFile); mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); mimeInfo->SetDefaultDescription(handler); } } if (NS_FAILED(rv)) { mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); } return mimeInfo.forget(); } already_AddRefed nsOSHelperAppService::GetFromType( const nsCString& aMIMEType) { // if the type is empty, return immediately if (aMIMEType.IsEmpty()) { return nullptr; } LOG("Here we do a mimetype lookup for '%s'\n", aMIMEType.get()); // extract the major and minor types NS_ConvertASCIItoUTF16 mimeType(aMIMEType); nsAString::const_iterator start_iter, end_iter, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd; mimeType.BeginReading(start_iter); mimeType.EndReading(end_iter); // XXX FIXME: add typeOptions parsing in here nsresult rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, end_iter); if (NS_FAILED(rv)) { return nullptr; } nsDependentSubstring majorType(majorTypeStart, majorTypeEnd); nsDependentSubstring minorType(minorTypeStart, minorTypeEnd); // First check the user's private mailcap file nsAutoString mailcap_description, handler, mozillaFlags; DoLookUpHandlerAndDescription(majorType, minorType, handler, mailcap_description, mozillaFlags, true); LOG("Private Handler/Description results: handler='%s', description='%s'\n", NS_LossyConvertUTF16toASCII(handler).get(), NS_LossyConvertUTF16toASCII(mailcap_description).get()); // Now look up our extensions nsAutoString extensions, mime_types_description; LookUpExtensionsAndDescription(majorType, minorType, extensions, mime_types_description); #ifdef MOZ_WIDGET_GTK if (handler.IsEmpty()) { RefPtr gnomeInfo = nsGNOMERegistry::GetFromType(aMIMEType); if (gnomeInfo) { LOG("Got MIMEInfo from GNOME registry without extensions; setting them " "to %s\n", NS_LossyConvertUTF16toASCII(extensions).get()); NS_ASSERTION(!gnomeInfo->HasExtensions(), "How'd that happen?"); gnomeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions)); return gnomeInfo.forget(); } } #endif if (handler.IsEmpty()) { DoLookUpHandlerAndDescription(majorType, minorType, handler, mailcap_description, mozillaFlags, false); } if (handler.IsEmpty()) { DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler, mailcap_description, mozillaFlags, true); } if (handler.IsEmpty()) { DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler, mailcap_description, mozillaFlags, false); } LOG("Handler/Description results: handler='%s', description='%s', " "mozillaFlags='%s'\n", NS_LossyConvertUTF16toASCII(handler).get(), NS_LossyConvertUTF16toASCII(mailcap_description).get(), NS_LossyConvertUTF16toASCII(mozillaFlags).get()); mailcap_description.Trim(" \t\""); mozillaFlags.Trim(" \t"); if (handler.IsEmpty() && extensions.IsEmpty() && mailcap_description.IsEmpty() && mime_types_description.IsEmpty()) { // No real useful info return nullptr; } RefPtr mimeInfo = new nsMIMEInfoUnix(aMIMEType); mimeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions)); if (!mime_types_description.IsEmpty()) { mimeInfo->SetDescription(mime_types_description); } else { mimeInfo->SetDescription(mailcap_description); } rv = NS_ERROR_NOT_AVAILABLE; nsCOMPtr handlerFile; if (!handler.IsEmpty()) { rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile)); } if (NS_SUCCEEDED(rv)) { mimeInfo->SetDefaultApplication(handlerFile); mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); mimeInfo->SetDefaultDescription(handler); } else { mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); } return mimeInfo.forget(); } nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aType, const nsACString& aFileExt, bool* aFound, nsIMIMEInfo** aMIMEInfo) { *aFound = true; RefPtr retval; // Fallback to lookup by extension when generic 'application/octet-stream' // content type is received. if (!aType.EqualsLiteral(APPLICATION_OCTET_STREAM)) { retval = GetFromType(PromiseFlatCString(aType)); } bool hasDefault = false; if (retval) retval->GetHasDefaultHandler(&hasDefault); if (!retval || !hasDefault) { RefPtr miByExt = GetFromExtension(PromiseFlatCString(aFileExt)); // If we had no extension match, but a type match, use that if (!miByExt && retval) { retval.forget(aMIMEInfo); return NS_OK; } // If we had an extension match but no type match, set the mimetype and use // it if (!retval && miByExt) { if (!aType.IsEmpty()) miByExt->SetMIMEType(aType); miByExt.swap(retval); retval.forget(aMIMEInfo); return NS_OK; } // If we got nothing, make a new mimeinfo if (!retval) { *aFound = false; retval = new nsMIMEInfoUnix(aType); if (retval) { if (!aFileExt.IsEmpty()) retval->AppendExtension(aFileExt); } retval.forget(aMIMEInfo); return NS_OK; } // Copy the attributes of retval (mimeinfo from type) onto miByExt, to // return it // but reset to just collected mDefaultAppDescription (from ext) nsAutoString byExtDefault; miByExt->GetDefaultDescription(byExtDefault); retval->SetDefaultDescription(byExtDefault); retval->CopyBasicDataTo(miByExt); miByExt.swap(retval); } retval.forget(aMIMEInfo); return NS_OK; } NS_IMETHODIMP nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, bool* found, nsIHandlerInfo** _retval) { NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!"); nsresult rv = OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found); if (NS_FAILED(rv)) return rv; nsMIMEInfoUnix* handlerInfo = new nsMIMEInfoUnix(aScheme, nsMIMEInfoBase::eProtocolInfo); NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY); NS_ADDREF(*_retval = handlerInfo); if (!*found) { // Code that calls this requires an object regardless if the OS has // something for us, so we return the empty object. return NS_OK; } nsAutoString desc; GetApplicationDescription(aScheme, desc); handlerInfo->SetDefaultDescription(desc); return NS_OK; }