1// Copyright 2012 The Chromium Authors 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "base/mac/mac_util.h" 6 7#import <Cocoa/Cocoa.h> 8#include <CoreServices/CoreServices.h> 9#import <IOKit/IOKitLib.h> 10#include <errno.h> 11#include <stddef.h> 12#include <string.h> 13#include <sys/sysctl.h> 14#include <sys/types.h> 15#include <sys/utsname.h> 16#include <sys/xattr.h> 17 18#include <string> 19#include <string_view> 20#include <vector> 21 22#include "base/apple/bridging.h" 23#include "base/apple/bundle_locations.h" 24#include "base/apple/foundation_util.h" 25#include "base/apple/osstatus_logging.h" 26#include "base/apple/scoped_cftyperef.h" 27#include "base/check.h" 28#include "base/files/file_path.h" 29#include "base/logging.h" 30#include "base/mac/scoped_aedesc.h" 31#include "base/mac/scoped_ioobject.h" 32#include "base/posix/sysctl.h" 33#include "base/strings/string_number_conversions.h" 34#include "base/strings/string_piece.h" 35#include "base/strings/string_split.h" 36#include "base/strings/string_util.h" 37#include "base/strings/sys_string_conversions.h" 38#include "base/threading/scoped_blocking_call.h" 39#include "build/build_config.h" 40 41namespace base::mac { 42 43namespace { 44 45class LoginItemsFileList { 46 public: 47 LoginItemsFileList() = default; 48 LoginItemsFileList(const LoginItemsFileList&) = delete; 49 LoginItemsFileList& operator=(const LoginItemsFileList&) = delete; 50 ~LoginItemsFileList() = default; 51 52 [[nodiscard]] bool Initialize() { 53 DCHECK(!login_items_) << __func__ << " called more than once."; 54 // The LSSharedFileList suite of functions has been deprecated. Instead, 55 // a LoginItems helper should be registered with SMLoginItemSetEnabled() 56 // https://crbug.com/1154377. 57#pragma clang diagnostic push 58#pragma clang diagnostic ignored "-Wdeprecated-declarations" 59 login_items_.reset(LSSharedFileListCreate( 60 nullptr, kLSSharedFileListSessionLoginItems, nullptr)); 61#pragma clang diagnostic pop 62 DLOG_IF(ERROR, !login_items_.get()) << "Couldn't get a Login Items list."; 63 return login_items_.get(); 64 } 65 66 LSSharedFileListRef GetLoginFileList() { 67 DCHECK(login_items_) << "Initialize() failed or not called."; 68 return login_items_.get(); 69 } 70 71 // Looks into Shared File Lists corresponding to Login Items for the item 72 // representing the specified bundle. If such an item is found, returns a 73 // retained reference to it. Caller is responsible for releasing the 74 // reference. 75 apple::ScopedCFTypeRef<LSSharedFileListItemRef> GetLoginItemForApp( 76 NSURL* url) { 77 DCHECK(login_items_) << "Initialize() failed or not called."; 78 79#pragma clang diagnostic push // https://crbug.com/1154377 80#pragma clang diagnostic ignored "-Wdeprecated-declarations" 81 apple::ScopedCFTypeRef<CFArrayRef> login_items_array( 82 LSSharedFileListCopySnapshot(login_items_.get(), /*inList=*/nullptr)); 83#pragma clang diagnostic pop 84 85 for (CFIndex i = 0; i < CFArrayGetCount(login_items_array.get()); ++i) { 86 LSSharedFileListItemRef item = 87 (LSSharedFileListItemRef)CFArrayGetValueAtIndex( 88 login_items_array.get(), i); 89#pragma clang diagnostic push // https://crbug.com/1154377 90#pragma clang diagnostic ignored "-Wdeprecated-declarations" 91 // kLSSharedFileListDoNotMountVolumes is used so that we don't trigger 92 // mounting when it's not expected by a user. Just listing the login 93 // items should not cause any side-effects. 94 NSURL* item_url = 95 apple::CFToNSOwnershipCast(LSSharedFileListItemCopyResolvedURL( 96 item, kLSSharedFileListDoNotMountVolumes, /*outError=*/nullptr)); 97#pragma clang diagnostic pop 98 99 if (item_url && [item_url isEqual:url]) { 100 return apple::ScopedCFTypeRef<LSSharedFileListItemRef>( 101 item, base::scoped_policy::RETAIN); 102 } 103 } 104 105 return apple::ScopedCFTypeRef<LSSharedFileListItemRef>(); 106 } 107 108 apple::ScopedCFTypeRef<LSSharedFileListItemRef> GetLoginItemForMainApp() { 109 NSURL* url = [NSURL fileURLWithPath:base::apple::MainBundle().bundlePath]; 110 return GetLoginItemForApp(url); 111 } 112 113 private: 114 apple::ScopedCFTypeRef<LSSharedFileListRef> login_items_; 115}; 116 117bool IsHiddenLoginItem(LSSharedFileListItemRef item) { 118#pragma clang diagnostic push // https://crbug.com/1154377 119#pragma clang diagnostic ignored "-Wdeprecated-declarations" 120 apple::ScopedCFTypeRef<CFBooleanRef> hidden( 121 reinterpret_cast<CFBooleanRef>(LSSharedFileListItemCopyProperty( 122 item, kLSSharedFileListLoginItemHidden))); 123#pragma clang diagnostic pop 124 125 return hidden && hidden.get() == kCFBooleanTrue; 126} 127 128} // namespace 129 130CGColorSpaceRef GetGenericRGBColorSpace() { 131 // Leaked. That's OK, it's scoped to the lifetime of the application. 132 static CGColorSpaceRef g_color_space_generic_rgb( 133 CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB)); 134 DLOG_IF(ERROR, !g_color_space_generic_rgb) << 135 "Couldn't get the generic RGB color space"; 136 return g_color_space_generic_rgb; 137} 138 139CGColorSpaceRef GetSRGBColorSpace() { 140 // Leaked. That's OK, it's scoped to the lifetime of the application. 141 static CGColorSpaceRef g_color_space_sRGB = 142 CGColorSpaceCreateWithName(kCGColorSpaceSRGB); 143 DLOG_IF(ERROR, !g_color_space_sRGB) << "Couldn't get the sRGB color space"; 144 return g_color_space_sRGB; 145} 146 147CGColorSpaceRef GetSystemColorSpace() { 148 // Leaked. That's OK, it's scoped to the lifetime of the application. 149 // Try to get the main display's color space. 150 static CGColorSpaceRef g_system_color_space = 151 CGDisplayCopyColorSpace(CGMainDisplayID()); 152 153 if (!g_system_color_space) { 154 // Use a generic RGB color space. This is better than nothing. 155 g_system_color_space = CGColorSpaceCreateDeviceRGB(); 156 157 if (g_system_color_space) { 158 DLOG(WARNING) << 159 "Couldn't get the main display's color space, using generic"; 160 } else { 161 DLOG(ERROR) << "Couldn't get any color space"; 162 } 163 } 164 165 return g_system_color_space; 166} 167 168void AddToLoginItems(const FilePath& app_bundle_file_path, 169 bool hide_on_startup) { 170 LoginItemsFileList login_items; 171 if (!login_items.Initialize()) { 172 return; 173 } 174 175 NSURL* app_bundle_url = base::apple::FilePathToNSURL(app_bundle_file_path); 176 apple::ScopedCFTypeRef<LSSharedFileListItemRef> item = 177 login_items.GetLoginItemForApp(app_bundle_url); 178 179 if (item.get() && (IsHiddenLoginItem(item.get()) == hide_on_startup)) { 180 return; // There already is a login item with required hide flag. 181 } 182 183 // Remove the old item, it has wrong hide flag, we'll create a new one. 184 if (item.get()) { 185#pragma clang diagnostic push // https://crbug.com/1154377 186#pragma clang diagnostic ignored "-Wdeprecated-declarations" 187 LSSharedFileListItemRemove(login_items.GetLoginFileList(), item.get()); 188#pragma clang diagnostic pop 189 } 190 191#pragma clang diagnostic push // https://crbug.com/1154377 192#pragma clang diagnostic ignored "-Wdeprecated-declarations" 193 BOOL hide = hide_on_startup ? YES : NO; 194 NSDictionary* properties = 195 @{apple::CFToNSPtrCast(kLSSharedFileListLoginItemHidden) : @(hide)}; 196 197 apple::ScopedCFTypeRef<LSSharedFileListItemRef> new_item( 198 LSSharedFileListInsertItemURL( 199 login_items.GetLoginFileList(), kLSSharedFileListItemLast, 200 /*inDisplayName=*/nullptr, 201 /*inIconRef=*/nullptr, apple::NSToCFPtrCast(app_bundle_url), 202 apple::NSToCFPtrCast(properties), /*inPropertiesToClear=*/nullptr)); 203#pragma clang diagnostic pop 204 205 if (!new_item.get()) { 206 DLOG(ERROR) << "Couldn't insert current app into Login Items list."; 207 } 208} 209 210void RemoveFromLoginItems(const FilePath& app_bundle_file_path) { 211 LoginItemsFileList login_items; 212 if (!login_items.Initialize()) { 213 return; 214 } 215 216 NSURL* app_bundle_url = base::apple::FilePathToNSURL(app_bundle_file_path); 217 apple::ScopedCFTypeRef<LSSharedFileListItemRef> item = 218 login_items.GetLoginItemForApp(app_bundle_url); 219 if (!item.get()) { 220 return; 221 } 222 223#pragma clang diagnostic push // https://crbug.com/1154377 224#pragma clang diagnostic ignored "-Wdeprecated-declarations" 225 LSSharedFileListItemRemove(login_items.GetLoginFileList(), item.get()); 226#pragma clang diagnostic pop 227} 228 229bool WasLaunchedAsLoginOrResumeItem() { 230 ProcessSerialNumber psn = {0, kCurrentProcess}; 231 ProcessInfoRec info = {}; 232 info.processInfoLength = sizeof(info); 233 234// GetProcessInformation has been deprecated since macOS 10.9, but there is no 235// replacement that provides the information we need. See 236// https://crbug.com/650854. 237#pragma clang diagnostic push 238#pragma clang diagnostic ignored "-Wdeprecated-declarations" 239 if (GetProcessInformation(&psn, &info) == noErr) { 240#pragma clang diagnostic pop 241 ProcessInfoRec parent_info = {}; 242 parent_info.processInfoLength = sizeof(parent_info); 243#pragma clang diagnostic push 244#pragma clang diagnostic ignored "-Wdeprecated-declarations" 245 if (GetProcessInformation(&info.processLauncher, &parent_info) == noErr) { 246#pragma clang diagnostic pop 247 return parent_info.processSignature == 'lgnw'; 248 } 249 } 250 return false; 251} 252 253bool WasLaunchedAsLoginItemRestoreState() { 254 // "Reopen windows..." option was added for 10.7. Prior OS versions should 255 // not have this behavior. 256 if (!WasLaunchedAsLoginOrResumeItem()) { 257 return false; 258 } 259 260 CFStringRef app = CFSTR("com.apple.loginwindow"); 261 CFStringRef save_state = CFSTR("TALLogoutSavesState"); 262 apple::ScopedCFTypeRef<CFPropertyListRef> plist( 263 CFPreferencesCopyAppValue(save_state, app)); 264 // According to documentation, com.apple.loginwindow.plist does not exist on a 265 // fresh installation until the user changes a login window setting. The 266 // "reopen windows" option is checked by default, so the plist would exist had 267 // the user unchecked it. 268 // https://developer.apple.com/library/mac/documentation/macosx/conceptual/bpsystemstartup/chapters/CustomLogin.html 269 if (!plist) { 270 return true; 271 } 272 273 if (CFBooleanRef restore_state = 274 base::apple::CFCast<CFBooleanRef>(plist.get())) { 275 return CFBooleanGetValue(restore_state); 276 } 277 278 return false; 279} 280 281bool WasLaunchedAsHiddenLoginItem() { 282 if (!WasLaunchedAsLoginOrResumeItem()) { 283 return false; 284 } 285 286 LoginItemsFileList login_items; 287 if (!login_items.Initialize()) { 288 return false; 289 } 290 291 apple::ScopedCFTypeRef<LSSharedFileListItemRef> item( 292 login_items.GetLoginItemForMainApp()); 293 if (!item.get()) { 294 // The OS itself can launch items, usually for the resume feature. 295 return false; 296 } 297 return IsHiddenLoginItem(item.get()); 298} 299 300bool RemoveQuarantineAttribute(const FilePath& file_path) { 301 const char kQuarantineAttrName[] = "com.apple.quarantine"; 302 int status = removexattr(file_path.value().c_str(), kQuarantineAttrName, 0); 303 return status == 0 || errno == ENOATTR; 304} 305 306namespace { 307 308int ParseOSProductVersion(const std::string_view& version) { 309 int macos_version = 0; 310 311 // The number of parts that need to be a part of the return value 312 // (major/minor/bugfix). 313 int parts = 3; 314 315 // When a Rapid Security Response is applied to a system, the UI will display 316 // an additional letter (e.g. "13.4.1 (a)"). That extra letter should not be 317 // present in `version_string`; in fact, the version string should not contain 318 // any spaces. However, take the first string-delimited "word" for parsing. 319 std::vector<std::string_view> words = base::SplitStringPiece( 320 version, " ", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); 321 CHECK_GE(words.size(), 1u); 322 323 // There are expected to be either two or three numbers separated by a dot. 324 // Walk through them, and add them to the version string. 325 for (const auto& value_str : base::SplitStringPiece( 326 words[0], ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL)) { 327 int value; 328 bool success = base::StringToInt(value_str, &value); 329 CHECK(success); 330 macos_version *= 100; 331 macos_version += value; 332 if (--parts == 0) { 333 break; 334 } 335 } 336 337 // While historically the string has comprised exactly two or three numbers 338 // separated by a dot, it's not inconceivable that it might one day be only 339 // one number. Therefore, only check to see that at least one number was found 340 // and processed. 341 CHECK_LE(parts, 2); 342 343 // Tack on as many '00 digits as needed to be sure that exactly three version 344 // numbers are returned. 345 for (int i = 0; i < parts; ++i) { 346 macos_version *= 100; 347 } 348 349 // Checks that the value is within expected bounds corresponding to released 350 // OS version numbers. The most important bit is making sure that the "10.16" 351 // compatibility mode isn't engaged. 352 CHECK(macos_version >= 10'00'00); 353 CHECK(macos_version < 10'16'00 || macos_version >= 11'00'00); 354 355 return macos_version; 356} 357 358} // namespace 359 360int ParseOSProductVersionForTesting(const std::string_view& version) { 361 return ParseOSProductVersion(version); 362} 363 364int MacOSVersion() { 365 static int macos_version = ParseOSProductVersion( 366 StringSysctlByName("kern.osproductversion").value()); 367 368 return macos_version; 369} 370 371namespace { 372 373#if defined(ARCH_CPU_X86_64) 374// https://developer.apple.com/documentation/apple_silicon/about_the_rosetta_translation_environment#3616845 375bool ProcessIsTranslated() { 376 int ret = 0; 377 size_t size = sizeof(ret); 378 if (sysctlbyname("sysctl.proc_translated", &ret, &size, nullptr, 0) == -1) { 379 return false; 380 } 381 return ret; 382} 383#endif // ARCH_CPU_X86_64 384 385} // namespace 386 387CPUType GetCPUType() { 388#if defined(ARCH_CPU_ARM64) 389 return CPUType::kArm; 390#elif defined(ARCH_CPU_X86_64) 391 return ProcessIsTranslated() ? CPUType::kTranslatedIntel : CPUType::kIntel; 392#else 393#error Time for another chip transition? 394#endif // ARCH_CPU_* 395} 396 397std::string GetOSDisplayName() { 398 std::string version_string = base::SysNSStringToUTF8( 399 NSProcessInfo.processInfo.operatingSystemVersionString); 400 return "macOS " + version_string; 401} 402 403std::string GetPlatformSerialNumber() { 404 base::mac::ScopedIOObject<io_service_t> expert_device( 405 IOServiceGetMatchingService(kIOMasterPortDefault, 406 IOServiceMatching("IOPlatformExpertDevice"))); 407 if (!expert_device) { 408 DLOG(ERROR) << "Error retrieving the machine serial number."; 409 return std::string(); 410 } 411 412 apple::ScopedCFTypeRef<CFTypeRef> serial_number( 413 IORegistryEntryCreateCFProperty(expert_device.get(), 414 CFSTR(kIOPlatformSerialNumberKey), 415 kCFAllocatorDefault, 0)); 416 CFStringRef serial_number_cfstring = 417 base::apple::CFCast<CFStringRef>(serial_number.get()); 418 if (!serial_number_cfstring) { 419 DLOG(ERROR) << "Error retrieving the machine serial number."; 420 return std::string(); 421 } 422 423 return base::SysCFStringRefToUTF8(serial_number_cfstring); 424} 425 426void OpenSystemSettingsPane(SystemSettingsPane pane) { 427 NSString* url = nil; 428 NSString* pane_file = nil; 429 NSData* subpane_data = nil; 430 // Note: On macOS 13 and later, System Settings are implemented with app 431 // extensions found at /System/Library/ExtensionKit/Extensions/. URLs to open 432 // them are constructed with a scheme of "x-apple.systempreferences" and a 433 // body of the the bundle ID of the app extension. (In the Info.plist there is 434 // an EXAppExtensionAttributes dictionary with legacy identifiers, but given 435 // that those are explicitly named "legacy", this code prefers to use the 436 // bundle IDs for the URLs it uses.) It is not yet known how to definitively 437 // identify the query string used to open sub-panes; the ones used below were 438 // determined from historical usage, disassembly of related code, and 439 // guessing. Clarity was requested from Apple in FB11753405. 440 switch (pane) { 441 case SystemSettingsPane::kAccessibility_Captions: 442 if (MacOSMajorVersion() >= 13) { 443 url = @"x-apple.systempreferences:com.apple.Accessibility-Settings." 444 @"extension?Captioning"; 445 } else { 446 url = @"x-apple.systempreferences:com.apple.preference.universalaccess?" 447 @"Captioning"; 448 } 449 break; 450 case SystemSettingsPane::kDateTime: 451 if (MacOSMajorVersion() >= 13) { 452 url = 453 @"x-apple.systempreferences:com.apple.Date-Time-Settings.extension"; 454 } else { 455 pane_file = @"/System/Library/PreferencePanes/DateAndTime.prefPane"; 456 } 457 break; 458 case SystemSettingsPane::kNetwork_Proxies: 459 if (MacOSMajorVersion() >= 13) { 460 url = @"x-apple.systempreferences:com.apple.Network-Settings.extension?" 461 @"Proxies"; 462 } else { 463 pane_file = @"/System/Library/PreferencePanes/Network.prefPane"; 464 subpane_data = [@"Proxies" dataUsingEncoding:NSASCIIStringEncoding]; 465 } 466 break; 467 case SystemSettingsPane::kPrintersScanners: 468 if (MacOSMajorVersion() >= 13) { 469 url = @"x-apple.systempreferences:com.apple.Print-Scan-Settings." 470 @"extension"; 471 } else { 472 pane_file = @"/System/Library/PreferencePanes/PrintAndFax.prefPane"; 473 } 474 break; 475 case SystemSettingsPane::kPrivacySecurity_Accessibility: 476 if (MacOSMajorVersion() >= 13) { 477 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 478 @"extension?Privacy_Accessibility"; 479 } else { 480 url = @"x-apple.systempreferences:com.apple.preference.security?" 481 @"Privacy_Accessibility"; 482 } 483 break; 484 case SystemSettingsPane::kPrivacySecurity_Bluetooth: 485 if (MacOSMajorVersion() >= 13) { 486 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 487 @"extension?Privacy_Bluetooth"; 488 } else { 489 url = @"x-apple.systempreferences:com.apple.preference.security?" 490 @"Privacy_Bluetooth"; 491 } 492 break; 493 case SystemSettingsPane::kPrivacySecurity_Camera: 494 if (MacOSMajorVersion() >= 13) { 495 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 496 @"extension?Privacy_Camera"; 497 } else { 498 url = @"x-apple.systempreferences:com.apple.preference.security?" 499 @"Privacy_Camera"; 500 } 501 break; 502 case SystemSettingsPane::kPrivacySecurity_Extensions_Sharing: 503 if (MacOSMajorVersion() >= 13) { 504 // See ShareKit, -[SHKSharingServicePicker openAppExtensionsPrefpane]. 505 url = @"x-apple.systempreferences:com.apple.ExtensionsPreferences?" 506 @"Sharing"; 507 } else { 508 // This is equivalent to the implementation of AppKit's 509 // +[NSSharingServicePicker openAppExtensionsPrefPane]. 510 pane_file = @"/System/Library/PreferencePanes/Extensions.prefPane"; 511 NSDictionary* subpane_dict = @{ 512 @"action" : @"revealExtensionPoint", 513 @"protocol" : @"com.apple.share-services" 514 }; 515 subpane_data = [NSPropertyListSerialization 516 dataWithPropertyList:subpane_dict 517 format:NSPropertyListXMLFormat_v1_0 518 options:0 519 error:nil]; 520 } 521 break; 522 case SystemSettingsPane::kPrivacySecurity_LocationServices: 523 if (MacOSMajorVersion() >= 13) { 524 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 525 @"extension?Privacy_LocationServices"; 526 } else { 527 url = @"x-apple.systempreferences:com.apple.preference.security?" 528 @"Privacy_LocationServices"; 529 } 530 break; 531 case SystemSettingsPane::kPrivacySecurity_Microphone: 532 if (MacOSMajorVersion() >= 13) { 533 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 534 @"extension?Privacy_Microphone"; 535 } else { 536 url = @"x-apple.systempreferences:com.apple.preference.security?" 537 @"Privacy_Microphone"; 538 } 539 break; 540 case SystemSettingsPane::kPrivacySecurity_ScreenRecording: 541 if (MacOSMajorVersion() >= 13) { 542 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 543 @"extension?Privacy_ScreenCapture"; 544 } else { 545 url = @"x-apple.systempreferences:com.apple.preference.security?" 546 @"Privacy_ScreenCapture"; 547 } 548 break; 549 case SystemSettingsPane::kTrackpad: 550 if (MacOSMajorVersion() >= 13) { 551 url = @"x-apple.systempreferences:com.apple.Trackpad-Settings." 552 @"extension"; 553 } else { 554 pane_file = @"/System/Library/PreferencePanes/Trackpad.prefPane"; 555 } 556 break; 557 } 558 559 DCHECK(url != nil ^ pane_file != nil); 560 561 if (url) { 562 [NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:url]]; 563 return; 564 } 565 566 NSAppleEventDescriptor* subpane_descriptor; 567 NSArray* pane_file_urls = @[ [NSURL fileURLWithPath:pane_file] ]; 568 569 LSLaunchURLSpec launchSpec = {0}; 570 launchSpec.itemURLs = apple::NSToCFPtrCast(pane_file_urls); 571 if (subpane_data) { 572 subpane_descriptor = 573 [[NSAppleEventDescriptor alloc] initWithDescriptorType:'ptru' 574 data:subpane_data]; 575 launchSpec.passThruParams = subpane_descriptor.aeDesc; 576 } 577 launchSpec.launchFlags = kLSLaunchAsync | kLSLaunchDontAddToRecents; 578 579 LSOpenFromURLSpec(&launchSpec, nullptr); 580} 581 582} // namespace base::mac 583