Bug 1496380 - stop recursion via the external protocol handler if Firefox is either the default OS handler or a configured external handler, r=mossop

This is an initial implementation of this idea that works on mac.
I've added a Windows implementation in another commit in this stack. I'll
look at a Linux one in a follow-up bug. I do not think we need them in the
child process implementation or on Android.

Effectively, this makes nsIHandlerInfo::LaunchWithURI() fall back to asking if
the handler info points to the OS default and that's us, or if it points to
a helper app and that's us. The latter is fairly easy to check, but the former,
more common case, is actually annoying - there don't seem to be APIs on the
external helper app service or the handler service that provide any information
about the app that's currently the default. So despite my belief that these
interfaces have too many methods that all do very similar things, and what we
need is fewer interfaces with fewer methods, I added another one...

For this mac implementation, I'm comparing bundle URLs and added newer API
usage for 10.10 and later to avoid deprecation warnings. I've not changed
the mac shell service as it uses bundle identifiers to check if we're the
default.

Another way of fixing these issues would be to complain about things when we
receive these URIs from external parties and our own config says that we will
just hand them to someone else. I decided not to do so because we end up with
at least one of the following problems:

- if we implement in BrowserContentHandler, that won't help for
  PWAs/Thunderbird
- if we try to implement in the external protocol handler, we'd need to start
  passing load flag information through to lots of checks.
- it wouldn't stop the recursion until we've already done one round of
  it for links that are in webpages, which seems suboptimal (ie, if you
  clicked a mailto: link on a webpage it'd go to the OS with that mailto link
  and only realize something's awry when we've gone back through the OS to us,
  rather than straightaway).

If we wanted to, we could add a fix like that in belt-and-suspenders fashion.

Differential Revision: https://phabricator.services.mozilla.com/D48742

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Gijs Kruitbosch 2019-11-26 18:51:04 +00:00
parent eb71df2dc9
commit c84bf2a64d
18 changed files with 319 additions and 58 deletions

View File

@ -39,6 +39,18 @@ nsresult nsOSHelperAppService::OSProtocolHandlerExists(const char* aScheme,
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme,
nsAString& _retval) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(
const nsACString& aScheme, bool* _retval) {
return NS_ERROR_NOT_AVAILABLE;
}
nsresult nsOSHelperAppService::GetProtocolHandlerInfoFromOS(
const nsACString& aScheme, bool* found, nsIHandlerInfo** info) {
// We don't want to get protocol handlers from the OS in GV; the app

View File

@ -24,6 +24,10 @@ class nsOSHelperAppService : public nsExternalHelperAppService {
NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
bool* found,
nsIHandlerInfo** _retval) override;
NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
nsAString& _retval) override;
NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
bool* _retval) override;
static nsIHandlerApp* CreateAndroidHandlerApp(
const nsAString& aName, const nsAString& aDescription,

View File

@ -26,6 +26,9 @@ class nsOSHelperAppService : public nsExternalHelperAppService {
NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
nsAString& _retval) override;
NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
bool* _retval) override;
nsresult GetMIMEInfoFromOS(const nsACString& aMIMEType,
const nsACString& aFileExt, bool* aFound,
nsIMIMEInfo** aMIMEInfo) override;

View File

@ -31,6 +31,49 @@
#define HELPERAPPLAUNCHER_BUNDLE_URL "chrome://global/locale/helperAppLauncher.properties"
#define BRAND_BUNDLE_URL "chrome://branding/locale/brand.properties"
nsresult GetDefaultBundleURL(const nsACString& aScheme, CFURLRef* aBundleURL) {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
nsresult rv = NS_ERROR_NOT_AVAILABLE;
CFStringRef schemeCFString = ::CFStringCreateWithBytes(
kCFAllocatorDefault, (const UInt8*)PromiseFlatCString(aScheme).get(), aScheme.Length(),
kCFStringEncodingUTF8, false);
if (schemeCFString) {
CFStringRef lookupCFString =
::CFStringCreateWithFormat(NULL, NULL, CFSTR("%@:"), schemeCFString);
if (lookupCFString) {
CFURLRef lookupCFURL = ::CFURLCreateWithString(NULL, lookupCFString, NULL);
if (lookupCFURL) {
if (@available(macOS 10.10, *)) {
*aBundleURL = ::LSCopyDefaultApplicationURLForURL(lookupCFURL, kLSRolesAll, NULL);
if (*aBundleURL) {
rv = NS_OK;
}
} else {
OSStatus theErr = ::LSGetApplicationForURL(lookupCFURL, kLSRolesAll, NULL, aBundleURL);
if (theErr == noErr && *aBundleURL) {
rv = NS_OK;
}
}
::CFRelease(lookupCFURL);
}
::CFRelease(lookupCFString);
}
::CFRelease(schemeCFString);
}
return rv;
NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
}
using mozilla::LogLevel;
/* This is an undocumented interface (in the Foundation framework) that has
@ -91,52 +134,57 @@ NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(const nsACString&
nsresult rv = NS_ERROR_NOT_AVAILABLE;
CFStringRef schemeCFString = ::CFStringCreateWithBytes(
kCFAllocatorDefault, (const UInt8*)PromiseFlatCString(aScheme).get(), aScheme.Length(),
kCFStringEncodingUTF8, false);
CFURLRef handlerBundleURL;
rv = GetDefaultBundleURL(aScheme, &handlerBundleURL);
if (schemeCFString) {
CFStringRef lookupCFString =
::CFStringCreateWithFormat(NULL, NULL, CFSTR("%@:"), schemeCFString);
if (lookupCFString) {
CFURLRef lookupCFURL = ::CFURLCreateWithString(NULL, lookupCFString, NULL);
if (lookupCFURL) {
CFURLRef appCFURL = NULL;
OSStatus theErr = ::LSGetApplicationForURL(lookupCFURL, kLSRolesAll, NULL, &appCFURL);
if (theErr == noErr) {
CFBundleRef handlerBundle = ::CFBundleCreate(NULL, appCFURL);
if (handlerBundle) {
// Get the human-readable name of the default handler bundle
CFStringRef bundleName = (CFStringRef)::CFBundleGetValueForInfoDictionaryKey(
handlerBundle, kCFBundleNameKey);
if (bundleName) {
AutoTArray<UniChar, 255> buffer;
CFIndex bundleNameLength = ::CFStringGetLength(bundleName);
buffer.SetLength(bundleNameLength);
::CFStringGetCharacters(bundleName, CFRangeMake(0, bundleNameLength),
buffer.Elements());
_retval.Assign(reinterpret_cast<char16_t*>(buffer.Elements()), bundleNameLength);
rv = NS_OK;
}
::CFRelease(handlerBundle);
}
::CFRelease(appCFURL);
}
::CFRelease(lookupCFURL);
}
::CFRelease(lookupCFString);
if (NS_SUCCEEDED(rv) && handlerBundleURL) {
CFBundleRef handlerBundle = CFBundleCreate(NULL, handlerBundleURL);
if (!handlerBundle) {
::CFRelease(handlerBundleURL);
return NS_ERROR_OUT_OF_MEMORY;
}
::CFRelease(schemeCFString);
// Get the human-readable name of the bundle
CFStringRef bundleName =
(CFStringRef)::CFBundleGetValueForInfoDictionaryKey(handlerBundle, kCFBundleNameKey);
if (bundleName) {
AutoTArray<UniChar, 255> buffer;
CFIndex bundleNameLength = ::CFStringGetLength(bundleName);
buffer.SetLength(bundleNameLength);
::CFStringGetCharacters(bundleName, CFRangeMake(0, bundleNameLength), buffer.Elements());
_retval.Assign(reinterpret_cast<char16_t*>(buffer.Elements()), bundleNameLength);
rv = NS_OK;
}
::CFRelease(handlerBundle);
::CFRelease(handlerBundleURL);
}
return rv;
NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
}
NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
bool* _retval) {
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
nsresult rv = NS_ERROR_NOT_AVAILABLE;
CFURLRef handlerBundleURL;
rv = GetDefaultBundleURL(aScheme, &handlerBundleURL);
if (NS_SUCCEEDED(rv) && handlerBundleURL) {
// Ensure we don't accidentally return success if we can't get an app bundle.
rv = NS_ERROR_NOT_AVAILABLE;
CFBundleRef appBundle = ::CFBundleGetMainBundle();
if (appBundle) {
CFURLRef selfURL = ::CFBundleCopyBundleURL(appBundle);
*_retval = ::CFEqual(selfURL, handlerBundleURL);
rv = NS_OK;
::CFRelease(appBundle);
::CFRelease(selfURL);
}
::CFRelease(handlerBundleURL);
}
return rv;

View File

@ -1008,13 +1008,6 @@ nsExternalHelperAppService::LoadURI(nsIURI* aURI,
nsIContentDispatchChooser::REASON_CANNOT_HANDLE);
}
NS_IMETHODIMP nsExternalHelperAppService::GetApplicationDescription(
const nsACString& aScheme, nsAString& _retval) {
// this method should only be implemented by each OS specific implementation
// of this service.
return NS_ERROR_NOT_IMPLEMENTED;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
// Methods related to deleting temporary files on exit
//////////////////////////////////////////////////////////////////////////////////////////////////////
@ -1101,13 +1094,6 @@ nsExternalHelperAppService::GetProtocolHandlerInfo(
return SetProtocolHandlerDefaults(*aHandlerInfo, exists);
}
NS_IMETHODIMP
nsExternalHelperAppService::GetProtocolHandlerInfoFromOS(
const nsACString& aScheme, bool* found, nsIHandlerInfo** aHandlerInfo) {
// intended to be implemented by the subclass
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
nsExternalHelperAppService::SetProtocolHandlerDefaults(
nsIHandlerInfo* aHandlerInfo, bool aOSHandlerExists) {

View File

@ -44,6 +44,8 @@ class MaybeCloseWindowHelper;
/**
* The helper app service. Responsible for handling content that Mozilla
* itself can not handle
* Note that this is an abstract class - we depend on appropriate subclassing
* on a per-OS basis to implement some methods.
*/
class nsExternalHelperAppService : public nsIExternalHelperAppService,
public nsPIExternalAppLauncher,
@ -55,7 +57,6 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService,
NS_DECL_ISUPPORTS
NS_DECL_NSIEXTERNALHELPERAPPSERVICE
NS_DECL_NSPIEXTERNALAPPLAUNCHER
NS_DECL_NSIEXTERNALPROTOCOLSERVICE
NS_DECL_NSIMIMESERVICE
NS_DECL_NSIOBSERVER
@ -67,6 +68,21 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService,
*/
MOZ_MUST_USE nsresult Init();
/**
* nsIExternalProtocolService methods that we provide in this class. Other
* methods should be implemented by per-OS subclasses.
*/
NS_IMETHOD ExternalProtocolHandlerExists(const char* aProtocolScheme,
bool* aHandlerExists) override;
NS_IMETHOD IsExposedProtocol(const char* aProtocolScheme,
bool* aResult) override;
NS_IMETHOD GetProtocolHandlerInfo(const nsACString& aScheme,
nsIHandlerInfo** aHandlerInfo) override;
NS_IMETHOD LoadURI(nsIURI* aURI,
nsIInterfaceRequestor* aWindowContext) override;
NS_IMETHOD SetProtocolHandlerDefaults(nsIHandlerInfo* aHandlerInfo,
bool aOSHandlerExists) override;
/**
* Given a string identifying an application, create an nsIFile representing
* it. This function should look in $PATH for the application.

View File

@ -123,4 +123,11 @@ interface nsIExternalProtocolService : nsISupports
* possible to get a description for it.
*/
AString getApplicationDescription(in AUTF8String aScheme);
/**
* Check if this app is registered as the OS default for a given scheme.
*
* @param aScheme The scheme to look up. For example, "mms".
*/
bool isCurrentAppOSDefaultForProtocol(in AUTF8String aScheme);
};

View File

@ -13,6 +13,42 @@
#include "nsEscape.h"
#include "nsIURILoader.h"
#include "nsCURILoader.h"
#include "nsCExternalHandlerService.h"
#include "nsIExternalProtocolService.h"
#include "mozilla/StaticPtr.h"
static bool sInitializedOurData = false;
StaticRefPtr<nsIFile> sOurAppFile;
static already_AddRefed<nsIFile> GetCanonicalExecutable(nsIFile* aFile) {
nsCOMPtr<nsIFile> binary = aFile;
#ifdef XP_MACOSX
nsAutoString leafName;
if (binary) {
binary->GetLeafName(leafName);
}
while (binary && !StringEndsWith(leafName, NS_LITERAL_STRING(".app"))) {
nsCOMPtr<nsIFile> parent;
binary->GetParent(getter_AddRefs(parent));
binary = parent;
if (binary) {
binary->GetLeafName(leafName);
}
}
#endif
return binary.forget();
}
static void EnsureAppDetailsAvailable() {
if (sInitializedOurData) {
return;
}
sInitializedOurData = true;
nsCOMPtr<nsIFile> binary;
XRE_GetBinaryPath(getter_AddRefs(binary));
sOurAppFile = GetCanonicalExecutable(binary);
ClearOnShutdown(&sOurAppFile);
}
// nsISupports methods
NS_IMPL_ADDREF(nsMIMEInfoBase)
@ -287,12 +323,45 @@ nsMIMEInfoBase::LaunchWithURI(nsIURI* aURI,
"nsMIMEInfoBase should be a protocol handler");
if (mPreferredAction == useSystemDefault) {
// First, ensure we're not accidentally going to call ourselves.
// That'd lead to an infinite loop (see bug 215554).
nsCOMPtr<nsIExternalProtocolService> extProtService =
do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID);
if (!extProtService) {
return NS_ERROR_FAILURE;
}
nsAutoCString scheme;
aURI->GetScheme(scheme);
bool isDefault = false;
nsresult rv =
extProtService->IsCurrentAppOSDefaultForProtocol(scheme, &isDefault);
if (NS_SUCCEEDED(rv) && isDefault) {
// Lie. This will trip the handler service into showing a dialog asking
// what the user wants.
return NS_ERROR_FILE_NOT_FOUND;
}
return LoadUriInternal(aURI);
}
if (mPreferredAction == useHelperApp) {
if (!mPreferredApplication) return NS_ERROR_FILE_NOT_FOUND;
EnsureAppDetailsAvailable();
nsCOMPtr<nsILocalHandlerApp> localPreferredHandler =
do_QueryInterface(mPreferredApplication);
if (localPreferredHandler) {
nsCOMPtr<nsIFile> executable;
localPreferredHandler->GetExecutable(getter_AddRefs(executable));
executable = GetCanonicalExecutable(executable);
bool isOurExecutable = false;
if (!executable ||
NS_FAILED(executable->Equals(sOurAppFile, &isOurExecutable)) ||
isOurExecutable) {
// Lie. This will trip the handler service into showing a dialog asking
// what the user wants.
return NS_ERROR_FILE_NOT_FOUND;
}
}
return mPreferredApplication->LaunchWithURI(aURI, aWindowContext);
}

View File

@ -120,6 +120,12 @@ nsOSHelperAppServiceChild::GetProtocolHandlerInfoFromOS(
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
nsOSHelperAppServiceChild::IsCurrentAppOSDefaultForProtocol(
const nsACString& aScheme, bool* aRetVal) {
return NS_ERROR_NOT_IMPLEMENTED;
}
nsresult nsOSHelperAppServiceChild::GetFileTokenForPath(
const char16_t* platformAppPath, nsIFile** aFile) {
return NS_ERROR_NOT_IMPLEMENTED;

View File

@ -37,6 +37,8 @@ class nsOSHelperAppServiceChild : public nsExternalHelperAppService {
NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
nsAString& aRetVal) override;
NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
bool* _retval) override;
NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType,
const nsACString& aFileExt, bool* aFound,

View File

@ -11,5 +11,7 @@ support-files =
download.sjs
[browser_download_always_ask_preferred_app.js]
[browser_download_privatebrowsing.js]
[browser_protocolhandler_loop.js]
skip-if = fission # Bug 1597154
[browser_remember_download_option.js]
[browser_web_protocol_handlers.js]

View File

@ -0,0 +1,82 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_helperapp() {
// Set up the test infrastructure:
const kProt = "foopydoopydoo";
const extProtocolSvc = Cc[
"@mozilla.org/uriloader/external-protocol-service;1"
].getService(Ci.nsIExternalProtocolService);
const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
Ci.nsIHandlerService
);
let handlerInfo = extProtocolSvc.getProtocolHandlerInfo(kProt);
if (handlerSvc.exists(handlerInfo)) {
handlerSvc.fillHandlerInfo(handlerInfo, "");
}
// Say we want to use a specific app:
handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
handlerInfo.alwaysAskBeforeHandling = false;
// Say it's us:
let selfFile = Services.dirsvc.get("XREExeF", Ci.nsIFile);
// Make sure it's the .app
if (AppConstants.platform == "macosx") {
while (
!selfFile.leafName.endsWith(".app") &&
!selfFile.leafName.endsWith(".app/")
) {
selfFile = selfFile.parent;
}
}
let selfHandlerApp = Cc[
"@mozilla.org/uriloader/local-handler-app;1"
].createInstance(Ci.nsILocalHandlerApp);
selfHandlerApp.executable = selfFile;
handlerInfo.possibleApplicationHandlers.appendElement(selfHandlerApp);
handlerInfo.preferredApplicationHandler = selfHandlerApp;
handlerSvc.store(handlerInfo);
await BrowserTestUtils.withNewTab("about:blank", async browser => {
// Now, do some safety stubbing. If we do end up recursing we spawn
// infinite tabs. We definitely don't want that. Avoid it by stubbing
// our external URL handling bits:
let oldAddTab = gBrowser.addTab;
registerCleanupFunction(
() => (gBrowser.addTab = gBrowser.loadOneTab = oldAddTab)
);
let wrongThingHappenedPromise = new Promise(resolve => {
gBrowser.addTab = gBrowser.loadOneTab = function(aURI) {
ok(false, "Tried to open unexpected URL in a tab: " + aURI);
resolve(null);
// Pass a dummy object to avoid upsetting BrowserContentHandler -
// if it thinks opening the tab failed, it tries to open a window instead,
// which we can't prevent as easily, and at which point we still end up
// with runaway tabs.
return {};
};
});
// We can't use TestUtils.topicObserved because it leaks.
let askedUserPromise = new Promise(r => {
let obs = () => {
r("yes");
Services.obs.removeObserver(obs, "domwindowopened");
};
Services.obs.addObserver(obs, "domwindowopened");
});
BrowserTestUtils.loadURI(browser, kProt + ":test");
let win = await Promise.race([wrongThingHappenedPromise, askedUserPromise]);
ok(win, "Should have gotten a window");
// This is really annoying. Hanging on to the window from the observer
// leaks for some reason. Just close it now. It has no window type, so use
// the lack of one to distinguish it from the browser and the harness.
for (let openWin of Services.wm.getEnumerator("")) {
if (!openWin.document.documentElement.getAttribute("windowtype")) {
openWin.close();
}
}
askedUserPromise = null;
});
});

View File

@ -24,6 +24,8 @@ class nsOSHelperAppService final : public nsExternalHelperAppService {
// override nsIExternalProtocolService methods
NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
nsAString& _retval);
NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
bool* _retval);
// method overrides --> used to hook the mime service into internet config....
NS_IMETHOD GetFromTypeAndExtension(const nsACString& aType,

View File

@ -21,6 +21,11 @@ nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, nsASt
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, bool* _retval) {
return NS_ERROR_NOT_AVAILABLE;
}
nsresult nsOSHelperAppService::GetFileTokenForPath(const char16_t* aPlatformAppPath,
nsIFile** aFile) {
return NS_ERROR_NOT_IMPLEMENTED;

View File

@ -1057,6 +1057,12 @@ NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(
#endif
}
NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(
const nsACString& aScheme, bool* _retval) {
*_retval = false;
return NS_OK;
}
nsresult nsOSHelperAppService::GetFileTokenForPath(
const char16_t* platformAppPath, nsIFile** aFile) {
LOG(("-- nsOSHelperAppService::GetFileTokenForPath: '%s'\n",

View File

@ -36,6 +36,8 @@ class nsOSHelperAppService : public nsExternalHelperAppService {
bool* aHandlerExists) override;
NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
nsAString& _retval) override;
NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
bool* _retval) override;
// GetFileTokenForPath must be implemented by each platform.
// platformAppPath --> a platform specific path to an application that we got

View File

@ -152,6 +152,12 @@ NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(
const nsACString& aScheme, bool* _retval) {
*_retval = false;
return NS_OK;
}
// GetMIMEInfoFromRegistry: This function obtains the values of some of the
// nsIMIMEInfo attributes for the mimeType/extension associated with the input
// registry key. The default entry for that key is the name of a registry key

View File

@ -37,6 +37,9 @@ class nsOSHelperAppService : public nsExternalHelperAppService {
NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
nsAString& _retval) override;
NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
bool* _retval) override;
// method overrides for windows registry look up steps....
NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType,
const nsACString& aFileExt, bool* aFound,