// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/shell_integration.h"
#include <AppKit/AppKit.h>
#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include "base/apple/bridging.h"
#include "base/apple/bundle_locations.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/mac/mac_util.h"
#include "base/strings/sys_string_conversions.h"
#include "build/branding_buildflags.h"
#include "chrome/common/channel_info.h"
#include "components/version_info/version_info.h"
#import "net/base/apple/url_conversions.h"
namespace shell_integration {
namespace {
// Returns the bundle id of the default client application for the given
// scheme or nil on failure.
NSString* GetBundleIdForDefaultAppForScheme(NSString* scheme) {
NSURL* scheme_url =
[NSURL URLWithString:[scheme stringByAppendingString:@":"]];
NSURL* default_app_url =
[NSWorkspace.sharedWorkspace URLForApplicationToOpenURL:scheme_url];
if (!default_app_url) {
return nil;
}
NSBundle* default_app_bundle = [NSBundle bundleWithURL:default_app_url];
return default_app_bundle.bundleIdentifier;
}
} // namespace
bool SetAsDefaultBrowser() {
if (@available(macOS 12, *)) {
// We really do want the outer bundle here, not the main bundle since
// setting a shortcut to Chrome as the default browser doesn't make sense.
NSURL* app_bundle = base::apple::OuterBundleURL();
if (!app_bundle) {
return false;
}
[NSWorkspace.sharedWorkspace setDefaultApplicationAtURL:app_bundle
toOpenURLsWithScheme:@"http"
completionHandler:^(NSError*){
}];
[NSWorkspace.sharedWorkspace setDefaultApplicationAtURL:app_bundle
toOpenURLsWithScheme:@"https"
completionHandler:^(NSError*){
}];
[NSWorkspace.sharedWorkspace setDefaultApplicationAtURL:app_bundle
toOpenContentType:UTTypeHTML
completionHandler:^(NSError*){
}];
// TODO(crbug.com/40248220): Passing empty completion handlers,
// above, is kinda broken, but given that this API is synchronous, nothing
// better can be done. This entire API should be rebuilt.
} else {
// We really do want the outer bundle here, not the main bundle since
// setting a shortcut to Chrome as the default browser doesn't make sense.
CFStringRef identifier =
base::apple::NSToCFPtrCast(base::apple::OuterBundle().bundleIdentifier);
if (!identifier) {
return false;
}
if (LSSetDefaultHandlerForURLScheme(CFSTR("http"), identifier) != noErr) {
return false;
}
if (LSSetDefaultHandlerForURLScheme(CFSTR("https"), identifier) != noErr) {
return false;
}
if (LSSetDefaultRoleHandlerForContentType(kUTTypeHTML, kLSRolesViewer,
identifier) != noErr) {
return false;
}
}
// The CoreServicesUIAgent presents a dialog asking the user to confirm their
// new default browser choice, but the agent sometimes orders the dialog
// behind the Chrome window. The user never sees the dialog, and therefore
// never confirms the change. Make the CoreServicesUIAgent active so the
// confirmation dialog comes to the front.
NSString* const kCoreServicesUIAgentBundleID =
@"com.apple.coreservices.uiagent";
for (NSRunningApplication* application in NSWorkspace.sharedWorkspace
.runningApplications) {
if ([application.bundleIdentifier
isEqualToString:kCoreServicesUIAgentBundleID]) {
[application activateWithOptions:NSApplicationActivateAllWindows];
break;
}
}
return true;
}
bool SetAsDefaultClientForScheme(const std::string& scheme) {
if (scheme.empty()) {
return false;
}
if (GetDefaultSchemeClientSetPermission() != SET_DEFAULT_UNATTENDED) {
return false;
}
if (@available(macOS 12, *)) {
// We really do want the main bundle here since it makes sense to set an
// app shortcut as a default scheme handler.
NSURL* app_bundle = base::apple::MainBundleURL();
if (!app_bundle) {
return false;
}
[NSWorkspace.sharedWorkspace
setDefaultApplicationAtURL:app_bundle
toOpenURLsWithScheme:base::SysUTF8ToNSString(scheme)
completionHandler:^(NSError*){
}];
// TODO(crbug.com/40248220): Passing empty completion handlers,
// above, is kinda broken, but given that this API is synchronous, nothing
// better can be done. This entire API should be rebuilt.
return true;
} else {
// We really do want the main bundle here since it makes sense to set an
// app shortcut as a default scheme handler.
NSString* identifier = base::apple::MainBundle().bundleIdentifier;
if (!identifier) {
return false;
}
NSString* scheme_ns = base::SysUTF8ToNSString(scheme);
OSStatus return_code =
LSSetDefaultHandlerForURLScheme(base::apple::NSToCFPtrCast(scheme_ns),
base::apple::NSToCFPtrCast(identifier));
return return_code == noErr;
}
}
std::u16string GetApplicationNameForScheme(const GURL& url) {
NSURL* ns_url = net::NSURLWithGURL(url);
if (!ns_url) {
return {};
}
NSURL* app_url =
[NSWorkspace.sharedWorkspace URLForApplicationToOpenURL:ns_url];
if (!app_url) {
return std::u16string();
}
NSString* app_display_name =
[NSFileManager.defaultManager displayNameAtPath:app_url.path];
return base::SysNSStringToUTF16(app_display_name);
}
std::vector<base::FilePath> GetAllApplicationPathsForURL(const GURL& url) {
NSURL* ns_url = net::NSURLWithGURL(url);
if (!ns_url) {
return {};
}
NSArray* app_urls = nil;
if (@available(macos 12.0, *)) {
app_urls =
[NSWorkspace.sharedWorkspace URLsForApplicationsToOpenURL:ns_url];
} else {
app_urls = base::apple::CFToNSOwnershipCast(LSCopyApplicationURLsForURL(
base::apple::NSToCFPtrCast(ns_url), kLSRolesAll));
}
if (app_urls.count == 0) {
return {};
}
std::vector<base::FilePath> app_paths;
app_paths.reserve(app_urls.count);
for (NSURL* app_url in app_urls) {
app_paths.push_back(base::apple::NSURLToFilePath(app_url));
}
return app_paths;
}
bool CanApplicationHandleURL(const base::FilePath& app_path, const GURL& url) {
NSURL* ns_item_url = net::NSURLWithGURL(url);
NSURL* ns_app_url = base::apple::FilePathToNSURL(app_path);
Boolean result = FALSE;
LSCanURLAcceptURL(base::apple::NSToCFPtrCast(ns_item_url),
base::apple::NSToCFPtrCast(ns_app_url), kLSRolesAll,
kLSAcceptDefault, &result);
return result;
}
// Attempt to determine if this instance of Chrome is the default browser and
// return the appropriate state. (Defined as being the handler for HTTP/HTTPS
// schemes; we don't want to report "no" here if the user has simply chosen
// to open HTML files in a text editor and FTP links with an FTP client.)
DefaultWebClientState GetDefaultBrowser() {
// We really do want the outer bundle here, since this we want to know the
// status of the main Chrome bundle and not a shortcut.
NSString* my_identifier = base::apple::OuterBundle().bundleIdentifier;
if (!my_identifier) {
return UNKNOWN_DEFAULT;
}
NSString* default_browser = GetBundleIdForDefaultAppForScheme(@"http");
if ([default_browser isEqualToString:my_identifier]) {
return IS_DEFAULT;
}
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Flavors of Chrome are of the constructions "com.google.Chrome" and
// "com.google.Chrome.beta". If the first three components match, then these
// are variant flavors.
auto three_components_only_lopper = [](NSString* bundle_id) {
NSMutableArray<NSString*>* parts =
[[bundle_id componentsSeparatedByString:@"."] mutableCopy];
while (parts.count > 3) {
[parts removeLastObject];
}
return [parts componentsJoinedByString:@"."];
};
NSString* my_identifier_lopped = three_components_only_lopper(my_identifier);
NSString* default_browser_lopped =
three_components_only_lopper(default_browser);
if ([my_identifier_lopped isEqualToString:default_browser_lopped]) {
return OTHER_MODE_IS_DEFAULT;
}
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
return NOT_DEFAULT;
}
// Returns true if Firefox is the default browser for the current user.
bool IsFirefoxDefaultBrowser() {
return [GetBundleIdForDefaultAppForScheme(@"http")
isEqualToString:@"org.mozilla.firefox"];
}
// Attempt to determine if this instance of Chrome is the default client
// application for the given scheme and return the appropriate state.
DefaultWebClientState IsDefaultClientForScheme(const std::string& scheme) {
if (scheme.empty()) {
return UNKNOWN_DEFAULT;
}
// We really do want the main bundle here since it makes sense to set an
// app shortcut as a default scheme handler.
NSString* my_identifier = base::apple::MainBundle().bundleIdentifier;
if (!my_identifier) {
return UNKNOWN_DEFAULT;
}
NSString* default_browser =
GetBundleIdForDefaultAppForScheme(base::SysUTF8ToNSString(scheme));
return [default_browser isEqualToString:my_identifier] ? IS_DEFAULT
: NOT_DEFAULT;
}
namespace internal {
DefaultWebClientSetPermission GetPlatformSpecificDefaultWebClientSetPermission(
WebClientSetMethod method) {
// This should be `SET_DEFAULT_INTERACTIVE`, but that changes how
// `DefaultBrowserWorker` and `DefaultSchemeClientWorker` work.
// TODO(crbug.com/40248220): Migrate all callers to the new API,
// migrate all the Mac code to integrate with it, and change this to return
// the correct value.
return SET_DEFAULT_UNATTENDED;
}
} // namespace internal
} // namespace shell_integration