| // 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 "base/mac/mac_util.h" | |
| #import <Cocoa/Cocoa.h> | |
| #include <CoreServices/CoreServices.h> | |
| #import <IOKit/IOKitLib.h> | |
| #include <errno.h> | |
| #include <stddef.h> | |
| #include <string.h> | |
| #include <sys/sysctl.h> | |
| #include <sys/types.h> | |
| #include <sys/utsname.h> | |
| #include <sys/xattr.h> | |
| #include <string> | |
| #include <string_view> | |
| #include <vector> | |
| #include "base/apple/bridging.h" | |
| #include "base/apple/bundle_locations.h" | |
| #include "base/apple/foundation_util.h" | |
| #include "base/apple/osstatus_logging.h" | |
| #include "base/apple/scoped_cftyperef.h" | |
| #include "base/check.h" | |
| #include "base/files/file_path.h" | |
| #include "base/logging.h" | |
| #include "base/mac/scoped_aedesc.h" | |
| #include "base/mac/scoped_ioobject.h" | |
| #include "base/posix/sysctl.h" | |
| #include "base/strings/string_number_conversions.h" | |
| #include "base/strings/string_split.h" | |
| #include "base/strings/string_util.h" | |
| #include "base/strings/sys_string_conversions.h" | |
| #include "base/threading/scoped_blocking_call.h" | |
| #include "build/build_config.h" | |
| namespace base::mac { | |
| namespace { | |
| class LoginItemsFileList { | |
| public: | |
| LoginItemsFileList() = default; | |
| LoginItemsFileList(const LoginItemsFileList&) = delete; | |
| LoginItemsFileList& operator=(const LoginItemsFileList&) = delete; | |
| ~LoginItemsFileList() = default; | |
| [[nodiscard]] bool Initialize() { | |
| DCHECK(!login_items_) << __func__ << " called more than once."; | |
| // The LSSharedFileList suite of functions has been deprecated. Instead, | |
| // a LoginItems helper should be registered with SMLoginItemSetEnabled() | |
| // https://crbug.com/1154377. | |
| #pragma clang diagnostic push | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | |
| login_items_.reset(LSSharedFileListCreate( | |
| nullptr, kLSSharedFileListSessionLoginItems, nullptr)); | |
| #pragma clang diagnostic pop | |
| DLOG_IF(ERROR, !login_items_.get()) << "Couldn't get a Login Items list."; | |
| return login_items_.get(); | |
| } | |
| LSSharedFileListRef GetLoginFileList() { | |
| DCHECK(login_items_) << "Initialize() failed or not called."; | |
| return login_items_.get(); | |
| } | |
| // Looks into Shared File Lists corresponding to Login Items for the item | |
| // representing the specified bundle. If such an item is found, returns a | |
| // retained reference to it. Caller is responsible for releasing the | |
| // reference. | |
| apple::ScopedCFTypeRef<LSSharedFileListItemRef> GetLoginItemForApp( | |
| NSURL* url) { | |
| DCHECK(login_items_) << "Initialize() failed or not called."; | |
| #pragma clang diagnostic push // https://crbug.com/1154377 | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | |
| apple::ScopedCFTypeRef<CFArrayRef> login_items_array( | |
| LSSharedFileListCopySnapshot(login_items_.get(), /*inList=*/nullptr)); | |
| #pragma clang diagnostic pop | |
| for (CFIndex i = 0; i < CFArrayGetCount(login_items_array.get()); ++i) { | |
| LSSharedFileListItemRef item = | |
| (LSSharedFileListItemRef)CFArrayGetValueAtIndex( | |
| login_items_array.get(), i); | |
| #pragma clang diagnostic push // https://crbug.com/1154377 | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | |
| // kLSSharedFileListDoNotMountVolumes is used so that we don't trigger | |
| // mounting when it's not expected by a user. Just listing the login | |
| // items should not cause any side-effects. | |
| NSURL* item_url = | |
| apple::CFToNSOwnershipCast(LSSharedFileListItemCopyResolvedURL( | |
| item, kLSSharedFileListDoNotMountVolumes, /*outError=*/nullptr)); | |
| #pragma clang diagnostic pop | |
| if (item_url && [item_url isEqual:url]) { | |
| return apple::ScopedCFTypeRef<LSSharedFileListItemRef>( | |
| item, base::scoped_policy::RETAIN); | |
| } | |
| } | |
| return apple::ScopedCFTypeRef<LSSharedFileListItemRef>(); | |
| } | |
| apple::ScopedCFTypeRef<LSSharedFileListItemRef> GetLoginItemForMainApp() { | |
| NSURL* url = [NSURL fileURLWithPath:base::apple::MainBundle().bundlePath]; | |
| return GetLoginItemForApp(url); | |
| } | |
| private: | |
| apple::ScopedCFTypeRef<LSSharedFileListRef> login_items_; | |
| }; | |
| bool IsHiddenLoginItem(LSSharedFileListItemRef item) { | |
| #pragma clang diagnostic push // https://crbug.com/1154377 | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | |
| apple::ScopedCFTypeRef<CFBooleanRef> hidden( | |
| reinterpret_cast<CFBooleanRef>(LSSharedFileListItemCopyProperty( | |
| item, kLSSharedFileListLoginItemHidden))); | |
| #pragma clang diagnostic pop | |
| return hidden && hidden.get() == kCFBooleanTrue; | |
| } | |
| } // namespace | |
| CGColorSpaceRef GetSRGBColorSpace() { | |
| // Leaked. That's OK, it's scoped to the lifetime of the application. | |
| static CGColorSpaceRef g_color_space_sRGB = | |
| CGColorSpaceCreateWithName(kCGColorSpaceSRGB); | |
| DLOG_IF(ERROR, !g_color_space_sRGB) << "Couldn't get the sRGB color space"; | |
| return g_color_space_sRGB; | |
| } | |
| void AddToLoginItems(const FilePath& app_bundle_file_path, | |
| bool hide_on_startup) { | |
| LoginItemsFileList login_items; | |
| if (!login_items.Initialize()) { | |
| return; | |
| } | |
| NSURL* app_bundle_url = base::apple::FilePathToNSURL(app_bundle_file_path); | |
| apple::ScopedCFTypeRef<LSSharedFileListItemRef> item = | |
| login_items.GetLoginItemForApp(app_bundle_url); | |
| if (item.get() && (IsHiddenLoginItem(item.get()) == hide_on_startup)) { | |
| return; // There already is a login item with required hide flag. | |
| } | |
| // Remove the old item, it has wrong hide flag, we'll create a new one. | |
| if (item.get()) { | |
| #pragma clang diagnostic push // https://crbug.com/1154377 | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | |
| LSSharedFileListItemRemove(login_items.GetLoginFileList(), item.get()); | |
| #pragma clang diagnostic pop | |
| } | |
| #pragma clang diagnostic push // https://crbug.com/1154377 | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | |
| BOOL hide = hide_on_startup ? YES : NO; | |
| NSDictionary* properties = | |
| @{apple::CFToNSPtrCast(kLSSharedFileListLoginItemHidden) : @(hide)}; | |
| apple::ScopedCFTypeRef<LSSharedFileListItemRef> new_item( | |
| LSSharedFileListInsertItemURL( | |
| login_items.GetLoginFileList(), kLSSharedFileListItemLast, | |
| /*inDisplayName=*/nullptr, | |
| /*inIconRef=*/nullptr, apple::NSToCFPtrCast(app_bundle_url), | |
| apple::NSToCFPtrCast(properties), /*inPropertiesToClear=*/nullptr)); | |
| #pragma clang diagnostic pop | |
| if (!new_item.get()) { | |
| DLOG(ERROR) << "Couldn't insert current app into Login Items list."; | |
| } | |
| } | |
| void RemoveFromLoginItems(const FilePath& app_bundle_file_path) { | |
| LoginItemsFileList login_items; | |
| if (!login_items.Initialize()) { | |
| return; | |
| } | |
| NSURL* app_bundle_url = base::apple::FilePathToNSURL(app_bundle_file_path); | |
| apple::ScopedCFTypeRef<LSSharedFileListItemRef> item = | |
| login_items.GetLoginItemForApp(app_bundle_url); | |
| if (!item.get()) { | |
| return; | |
| } | |
| #pragma clang diagnostic push // https://crbug.com/1154377 | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | |
| LSSharedFileListItemRemove(login_items.GetLoginFileList(), item.get()); | |
| #pragma clang diagnostic pop | |
| } | |
| bool WasLaunchedAsLoginOrResumeItem() { | |
| ProcessSerialNumber psn = {0, kCurrentProcess}; | |
| ProcessInfoRec info = {}; | |
| info.processInfoLength = sizeof(info); | |
| // GetProcessInformation has been deprecated since macOS 10.9, but there is no | |
| // replacement that provides the information we need. See | |
| // https://crbug.com/650854. | |
| #pragma clang diagnostic push | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | |
| if (GetProcessInformation(&psn, &info) == noErr) { | |
| #pragma clang diagnostic pop | |
| ProcessInfoRec parent_info = {}; | |
| parent_info.processInfoLength = sizeof(parent_info); | |
| #pragma clang diagnostic push | |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" | |
| if (GetProcessInformation(&info.processLauncher, &parent_info) == noErr) { | |
| #pragma clang diagnostic pop | |
| return parent_info.processSignature == 'lgnw'; | |
| } | |
| } | |
| return false; | |
| } | |
| bool WasLaunchedAsLoginItemRestoreState() { | |
| // "Reopen windows..." option was added for 10.7. Prior OS versions should | |
| // not have this behavior. | |
| if (!WasLaunchedAsLoginOrResumeItem()) { | |
| return false; | |
| } | |
| CFStringRef app = CFSTR("com.apple.loginwindow"); | |
| CFStringRef save_state = CFSTR("TALLogoutSavesState"); | |
| apple::ScopedCFTypeRef<CFPropertyListRef> plist( | |
| CFPreferencesCopyAppValue(save_state, app)); | |
| // According to documentation, com.apple.loginwindow.plist does not exist on a | |
| // fresh installation until the user changes a login window setting. The | |
| // "reopen windows" option is checked by default, so the plist would exist had | |
| // the user unchecked it. | |
| // https://developer.apple.com/library/mac/documentation/macosx/conceptual/bpsystemstartup/chapters/CustomLogin.html | |
| if (!plist) { | |
| return true; | |
| } | |
| if (CFBooleanRef restore_state = | |
| base::apple::CFCast<CFBooleanRef>(plist.get())) { | |
| return CFBooleanGetValue(restore_state); | |
| } | |
| return false; | |
| } | |
| bool WasLaunchedAsHiddenLoginItem() { | |
| if (!WasLaunchedAsLoginOrResumeItem()) { | |
| return false; | |
| } | |
| LoginItemsFileList login_items; | |
| if (!login_items.Initialize()) { | |
| return false; | |
| } | |
| apple::ScopedCFTypeRef<LSSharedFileListItemRef> item( | |
| login_items.GetLoginItemForMainApp()); | |
| if (!item.get()) { | |
| // The OS itself can launch items, usually for the resume feature. | |
| return false; | |
| } | |
| return IsHiddenLoginItem(item.get()); | |
| } | |
| bool RemoveQuarantineAttribute(const FilePath& file_path) { | |
| // It is critical that NSURLQuarantinePropertiesKey not be used here; it is | |
| // irredeemably broken. | |
| // | |
| // Before macOS 26.1, attempting to remove a quarantine attribute using | |
| // NSURLQuarantinePropertiesKey would fail if the file didn't already have a | |
| // quarantine attribute. But even after that, while | |
| // NSURLQuarantinePropertiesKey appears to successfully remove the quarantine | |
| // attribute in testing, in deployment it fails to reliably do so, and in the | |
| // process that breaks the updater (no bug filed as it was caught early in a | |
| // panic) and app links (https://crbug.com/488020336). | |
| // | |
| // It's not clear how to test NSURLQuarantinePropertiesKey for any further | |
| // attempt to use it. The previous land/revert cycle | |
| // (https://crrev.com/c/7551491 and https://crrev.com/c/7602582) came with | |
| // extensive testing that passed, and in informal experience, things would | |
| // work fine for, say, seven times before failing on the eighth. | |
| // | |
| // Given the criticality of the updater, it's hard to find an incentive to do | |
| // experimentation in this area. Stay away. | |
| const char kQuarantineAttrName[] = "com.apple.quarantine"; | |
| int status = removexattr(file_path.value().c_str(), kQuarantineAttrName, 0); | |
| return status == 0 || errno == ENOATTR; | |
| } | |
| void SetFileTags(const FilePath& file_path, | |
| const std::vector<std::string>& file_tags) { | |
| if (file_tags.empty()) { | |
| return; | |
| } | |
| NSMutableArray* tag_array = [NSMutableArray array]; | |
| for (const auto& tag : file_tags) { | |
| [tag_array addObject:SysUTF8ToNSString(tag)]; | |
| } | |
| NSURL* file_url = apple::FilePathToNSURL(file_path); | |
| [file_url setResourceValue:tag_array forKey:NSURLTagNamesKey error:nil]; | |
| } | |
| namespace { | |
| int ParseOSProductVersion(const std::string_view& version) { | |
| int macos_version = 0; | |
| // The number of parts that need to be a part of the return value | |
| // (major/minor/bugfix). | |
| int parts = 3; | |
| // When a Rapid Security Response is applied to a system, the UI will display | |
| // an additional letter (e.g. "13.4.1 (a)"). That extra letter should not be | |
| // present in `version_string`; in fact, the version string should not contain | |
| // any spaces. However, take the first space-delimited "word" for parsing. | |
| std::vector<std::string_view> words = base::SplitStringPiece( | |
| version, " ", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); | |
| CHECK_GE(words.size(), 1u); | |
| // There are expected to be either two or three numbers separated by a dot. | |
| // Walk through them, and add them to the version string. | |
| for (const auto& value_str : base::SplitStringPiece( | |
| words[0], ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL)) { | |
| int value; | |
| bool success = base::StringToInt(value_str, &value); | |
| CHECK(success); | |
| macos_version *= 100; | |
| macos_version += value; | |
| if (--parts == 0) { | |
| break; | |
| } | |
| } | |
| // While historically the string has comprised exactly two or three numbers | |
| // separated by a dot, it's not inconceivable that it might one day be only | |
| // one number. Therefore, only check to see that at least one number was found | |
| // and processed. | |
| CHECK_LE(parts, 2); | |
| // Tack on as many '00 digits as needed to be sure that exactly three version | |
| // numbers are returned. | |
| for (int i = 0; i < parts; ++i) { | |
| macos_version *= 100; | |
| } | |
| // Check that the value is within expected bounds corresponding to released | |
| // OS version numbers. Specifically, ensure that neither the "macOS 10.16" nor | |
| // the "macOS 16" compatibility modes are engaged. | |
| CHECK(macos_version >= 10'00'00); | |
| CHECK(macos_version < 10'16'00 || macos_version >= 11'00'00); | |
| CHECK(macos_version < 16'00'00 || macos_version >= 26'00'00); | |
| return macos_version; | |
| } | |
| } // namespace | |
| int ParseOSProductVersionForTesting(const std::string_view& version) { | |
| return ParseOSProductVersion(version); | |
| } | |
| int MacOSVersion() { | |
| static int macos_version = ParseOSProductVersion( | |
| StringSysctlByName("kern.osproductversion").value()); | |
| return macos_version; | |
| } | |
| bool IsVirtualMachine() { | |
| int ret; | |
| size_t size = sizeof(ret); | |
| PCHECK(sysctlbyname("kern.hv_vmm_present", &ret, &size, nullptr, 0) != -1); | |
| return ret; | |
| } | |
| MacOS26LiquidGlassPreferredLook GetMacOS26LiquidGlassPreferredLook() { | |
| CHECK_EQ(MacOSMajorVersion(), 26); | |
| NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; | |
| NSNumber* setting = base::apple::ObjCCast<NSNumber>( | |
| [defaults objectForKey:@"NSGlassDiffusionSetting"]); | |
| if (!setting) { | |
| setting = base::apple::ObjCCast<NSNumber>( | |
| [defaults objectForKey:@"NSLiquidGlassSetting"]); | |
| } | |
| if (setting) { | |
| return [setting boolValue] ? MacOS26LiquidGlassPreferredLook::kTint | |
| : MacOS26LiquidGlassPreferredLook::kClear; | |
| } | |
| return MacOS26LiquidGlassPreferredLook::kDefault; | |
| } | |
| namespace { | |
| #if defined(ARCH_CPU_X86_64) | |
| // https://developer.apple.com/documentation/apple_silicon/about_the_rosetta_translation_environment#3616845 | |
| bool ProcessIsTranslated() { | |
| int ret = 0; | |
| size_t size = sizeof(ret); | |
| if (sysctlbyname("sysctl.proc_translated", &ret, &size, nullptr, 0) == -1) { | |
| return false; | |
| } | |
| return ret; | |
| } | |
| #endif // ARCH_CPU_X86_64 | |
| } // namespace | |
| CPUType GetCPUType() { | |
| #if defined(ARCH_CPU_ARM64) | |
| return CPUType::kArm; | |
| #elif defined(ARCH_CPU_X86_64) | |
| return ProcessIsTranslated() ? CPUType::kTranslatedIntel : CPUType::kIntel; | |
| #else | |
| #error Time for another chip transition? | |
| #endif // ARCH_CPU_* | |
| } | |
| std::string GetOSDisplayName() { | |
| std::string version_string = base::SysNSStringToUTF8( | |
| NSProcessInfo.processInfo.operatingSystemVersionString); | |
| return "macOS " + version_string; | |
| } | |
| std::string GetPlatformSerialNumber() { | |
| base::mac::ScopedIOObject<io_service_t> expert_device( | |
| IOServiceGetMatchingService(kIOMainPortDefault, | |
| IOServiceMatching("IOPlatformExpertDevice"))); | |
| if (!expert_device) { | |
| DLOG(ERROR) << "Error retrieving the machine serial number."; | |
| return std::string(); | |
| } | |
| apple::ScopedCFTypeRef<CFTypeRef> serial_number( | |
| IORegistryEntryCreateCFProperty(expert_device.get(), | |
| CFSTR(kIOPlatformSerialNumberKey), | |
| kCFAllocatorDefault, 0)); | |
| CFStringRef serial_number_cfstring = | |
| base::apple::CFCast<CFStringRef>(serial_number.get()); | |
| if (!serial_number_cfstring) { | |
| DLOG(ERROR) << "Error retrieving the machine serial number."; | |
| return std::string(); | |
| } | |
| return base::SysCFStringRefToUTF8(serial_number_cfstring); | |
| } | |
| void OpenSystemSettingsPane(SystemSettingsPane pane, | |
| const std::string& id_param) { | |
| NSString* url = nil; | |
| // System Settings are implemented with app extensions found at | |
| // /System/Library/ExtensionKit/Extensions/. URLs to open them are constructed | |
| // with a scheme of "x-apple.systempreferences" and a body of the the bundle | |
| // ID of the app extension. (In the Info.plist there is an | |
| // EXAppExtensionAttributes dictionary with legacy identifiers, but given that | |
| // those are explicitly named "legacy", this code prefers to use the bundle | |
| // IDs for the URLs it uses.) It is not yet known how to definitively identify | |
| // the query string used to open sub-panes; the ones used below were | |
| // determined from historical usage, disassembly of related code, and | |
| // guessing. Clarity was requested from Apple in FB11753405. The current best | |
| // guess is to analyze the method named -revealElementForKey:, but because the | |
| // extensions are all written in Swift it's hard to confirm this is correct or | |
| // to use this knowledge. | |
| // | |
| // These values have been tested on macOS 13, 14, 15, and 26. Be sure to | |
| // verify them on new releases of macOS. | |
| switch (pane) { | |
| case SystemSettingsPane::kAccessibility_Captions: | |
| url = @"x-apple.systempreferences:com.apple.Accessibility-Settings." | |
| @"extension?Captioning"; | |
| break; | |
| case SystemSettingsPane::kGeneral_DateTime: | |
| url = @"x-apple.systempreferences:com.apple.Date-Time-Settings.extension"; | |
| break; | |
| case SystemSettingsPane::kGeneral_LoginItems_Extensions_Sharing: | |
| if (MacOSMajorVersion() >= 15) { | |
| // See ShareKit, -[SHKSharingServicePicker openAppExtensionsPrefpane]. | |
| url = @"x-apple.systempreferences:com.apple.ExtensionsPreferences?" | |
| @"extensionPointIdentifier=com.apple.share-services"; | |
| } else { | |
| // See ShareKit, -[SHKSharingServicePicker openAppExtensionsPrefpane]. | |
| url = @"x-apple.systempreferences:com.apple.ExtensionsPreferences?" | |
| @"Sharing"; | |
| } | |
| break; | |
| case SystemSettingsPane::kNetwork_Proxies: | |
| url = @"x-apple.systempreferences:com.apple.Network-Settings.extension?" | |
| @"Proxies"; | |
| break; | |
| case SystemSettingsPane::kNotifications: | |
| url = @"x-apple.systempreferences:com.apple.Notifications-Settings." | |
| @"extension"; | |
| if (!id_param.empty()) { | |
| url = [url stringByAppendingFormat:@"?id=%s", id_param.c_str()]; | |
| } | |
| break; | |
| case SystemSettingsPane::kPrintersScanners: | |
| url = @"x-apple.systempreferences:com.apple.Print-Scan-Settings." | |
| @"extension"; | |
| break; | |
| case SystemSettingsPane::kPrivacySecurity: | |
| url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." | |
| @"extension?Privacy"; | |
| break; | |
| case SystemSettingsPane::kPrivacySecurity_Accessibility: | |
| url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." | |
| @"extension?Privacy_Accessibility"; | |
| break; | |
| case SystemSettingsPane::kPrivacySecurity_Bluetooth: | |
| url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." | |
| @"extension?Privacy_Bluetooth"; | |
| break; | |
| case SystemSettingsPane::kPrivacySecurity_Camera: | |
| url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." | |
| @"extension?Privacy_Camera"; | |
| break; | |
| case SystemSettingsPane::kPrivacySecurity_LocationServices: | |
| url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." | |
| @"extension?Privacy_LocationServices"; | |
| break; | |
| case SystemSettingsPane::kPrivacySecurity_Microphone: | |
| url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." | |
| @"extension?Privacy_Microphone"; | |
| break; | |
| case SystemSettingsPane::kPrivacySecurity_PasteFromOtherApps: | |
| // Pasteboard permissions were added in macOS 15. | |
| DCHECK_GE(MacOSMajorVersion(), 15); | |
| url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." | |
| @"extension?Privacy_Pasteboard"; | |
| break; | |
| case SystemSettingsPane::kPrivacySecurity_ScreenRecording: | |
| url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." | |
| @"extension?Privacy_ScreenCapture"; | |
| break; | |
| case SystemSettingsPane::kTrackpad: | |
| url = @"x-apple.systempreferences:com.apple.Trackpad-Settings." | |
| @"extension"; | |
| break; | |
| default: | |
| NOTREACHED(); | |
| } | |
| [NSWorkspace.sharedWorkspace | |
| openURL:[NSURL URLWithString:url] | |
| configuration:[NSWorkspaceOpenConfiguration configuration] | |
| completionHandler:nil]; | |
| } | |
| } // namespace base::mac |