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 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 GetSRGBColorSpace() { 131 // Leaked. That's OK, it's scoped to the lifetime of the application. 132 static CGColorSpaceRef g_color_space_sRGB = 133 CGColorSpaceCreateWithName(kCGColorSpaceSRGB); 134 DLOG_IF(ERROR, !g_color_space_sRGB) << "Couldn't get the sRGB color space"; 135 return g_color_space_sRGB; 136} 137 138void AddToLoginItems(const FilePath& app_bundle_file_path, 139 bool hide_on_startup) { 140 LoginItemsFileList login_items; 141 if (!login_items.Initialize()) { 142 return; 143 } 144 145 NSURL* app_bundle_url = base::apple::FilePathToNSURL(app_bundle_file_path); 146 apple::ScopedCFTypeRef<LSSharedFileListItemRef> item = 147 login_items.GetLoginItemForApp(app_bundle_url); 148 149 if (item.get() && (IsHiddenLoginItem(item.get()) == hide_on_startup)) { 150 return; // There already is a login item with required hide flag. 151 } 152 153 // Remove the old item, it has wrong hide flag, we'll create a new one. 154 if (item.get()) { 155#pragma clang diagnostic push // https://crbug.com/1154377 156#pragma clang diagnostic ignored "-Wdeprecated-declarations" 157 LSSharedFileListItemRemove(login_items.GetLoginFileList(), item.get()); 158#pragma clang diagnostic pop 159 } 160 161#pragma clang diagnostic push // https://crbug.com/1154377 162#pragma clang diagnostic ignored "-Wdeprecated-declarations" 163 BOOL hide = hide_on_startup ? YES : NO; 164 NSDictionary* properties = 165 @{apple::CFToNSPtrCast(kLSSharedFileListLoginItemHidden) : @(hide)}; 166 167 apple::ScopedCFTypeRef<LSSharedFileListItemRef> new_item( 168 LSSharedFileListInsertItemURL( 169 login_items.GetLoginFileList(), kLSSharedFileListItemLast, 170 /*inDisplayName=*/nullptr, 171 /*inIconRef=*/nullptr, apple::NSToCFPtrCast(app_bundle_url), 172 apple::NSToCFPtrCast(properties), /*inPropertiesToClear=*/nullptr)); 173#pragma clang diagnostic pop 174 175 if (!new_item.get()) { 176 DLOG(ERROR) << "Couldn't insert current app into Login Items list."; 177 } 178} 179 180void RemoveFromLoginItems(const FilePath& app_bundle_file_path) { 181 LoginItemsFileList login_items; 182 if (!login_items.Initialize()) { 183 return; 184 } 185 186 NSURL* app_bundle_url = base::apple::FilePathToNSURL(app_bundle_file_path); 187 apple::ScopedCFTypeRef<LSSharedFileListItemRef> item = 188 login_items.GetLoginItemForApp(app_bundle_url); 189 if (!item.get()) { 190 return; 191 } 192 193#pragma clang diagnostic push // https://crbug.com/1154377 194#pragma clang diagnostic ignored "-Wdeprecated-declarations" 195 LSSharedFileListItemRemove(login_items.GetLoginFileList(), item.get()); 196#pragma clang diagnostic pop 197} 198 199bool WasLaunchedAsLoginOrResumeItem() { 200 ProcessSerialNumber psn = {0, kCurrentProcess}; 201 ProcessInfoRec info = {}; 202 info.processInfoLength = sizeof(info); 203 204// GetProcessInformation has been deprecated since macOS 10.9, but there is no 205// replacement that provides the information we need. See 206// https://crbug.com/650854. 207#pragma clang diagnostic push 208#pragma clang diagnostic ignored "-Wdeprecated-declarations" 209 if (GetProcessInformation(&psn, &info) == noErr) { 210#pragma clang diagnostic pop 211 ProcessInfoRec parent_info = {}; 212 parent_info.processInfoLength = sizeof(parent_info); 213#pragma clang diagnostic push 214#pragma clang diagnostic ignored "-Wdeprecated-declarations" 215 if (GetProcessInformation(&info.processLauncher, &parent_info) == noErr) { 216#pragma clang diagnostic pop 217 return parent_info.processSignature == 'lgnw'; 218 } 219 } 220 return false; 221} 222 223bool WasLaunchedAsLoginItemRestoreState() { 224 // "Reopen windows..." option was added for 10.7. Prior OS versions should 225 // not have this behavior. 226 if (!WasLaunchedAsLoginOrResumeItem()) { 227 return false; 228 } 229 230 CFStringRef app = CFSTR("com.apple.loginwindow"); 231 CFStringRef save_state = CFSTR("TALLogoutSavesState"); 232 apple::ScopedCFTypeRef<CFPropertyListRef> plist( 233 CFPreferencesCopyAppValue(save_state, app)); 234 // According to documentation, com.apple.loginwindow.plist does not exist on a 235 // fresh installation until the user changes a login window setting. The 236 // "reopen windows" option is checked by default, so the plist would exist had 237 // the user unchecked it. 238 // https://developer.apple.com/library/mac/documentation/macosx/conceptual/bpsystemstartup/chapters/CustomLogin.html 239 if (!plist) { 240 return true; 241 } 242 243 if (CFBooleanRef restore_state = 244 base::apple::CFCast<CFBooleanRef>(plist.get())) { 245 return CFBooleanGetValue(restore_state); 246 } 247 248 return false; 249} 250 251bool WasLaunchedAsHiddenLoginItem() { 252 if (!WasLaunchedAsLoginOrResumeItem()) { 253 return false; 254 } 255 256 LoginItemsFileList login_items; 257 if (!login_items.Initialize()) { 258 return false; 259 } 260 261 apple::ScopedCFTypeRef<LSSharedFileListItemRef> item( 262 login_items.GetLoginItemForMainApp()); 263 if (!item.get()) { 264 // The OS itself can launch items, usually for the resume feature. 265 return false; 266 } 267 return IsHiddenLoginItem(item.get()); 268} 269 270bool RemoveQuarantineAttribute(const FilePath& file_path) { 271 const char kQuarantineAttrName[] = "com.apple.quarantine"; 272 int status = removexattr(file_path.value().c_str(), kQuarantineAttrName, 0); 273 return status == 0 || errno == ENOATTR; 274} 275 276void SetFileTags(const FilePath& file_path, 277 const std::vector<std::string>& file_tags) { 278 if (file_tags.empty()) { 279 return; 280 } 281 282 NSMutableArray* tag_array = [NSMutableArray array]; 283 for (const auto& tag : file_tags) { 284 [tag_array addObject:SysUTF8ToNSString(tag)]; 285 } 286 287 NSURL* file_url = apple::FilePathToNSURL(file_path); 288 [file_url setResourceValue:tag_array forKey:NSURLTagNamesKey error:nil]; 289} 290 291namespace { 292 293int ParseOSProductVersion(const std::string_view& version) { 294 int macos_version = 0; 295 296 // The number of parts that need to be a part of the return value 297 // (major/minor/bugfix). 298 int parts = 3; 299 300 // When a Rapid Security Response is applied to a system, the UI will display 301 // an additional letter (e.g. "13.4.1 (a)"). That extra letter should not be 302 // present in `version_string`; in fact, the version string should not contain 303 // any spaces. However, take the first space-delimited "word" for parsing. 304 std::vector<std::string_view> words = base::SplitStringPiece( 305 version, " ", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); 306 CHECK_GE(words.size(), 1u); 307 308 // There are expected to be either two or three numbers separated by a dot. 309 // Walk through them, and add them to the version string. 310 for (const auto& value_str : base::SplitStringPiece( 311 words[0], ".", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL)) { 312 int value; 313 bool success = base::StringToInt(value_str, &value); 314 CHECK(success); 315 macos_version *= 100; 316 macos_version += value; 317 if (--parts == 0) { 318 break; 319 } 320 } 321 322 // While historically the string has comprised exactly two or three numbers 323 // separated by a dot, it's not inconceivable that it might one day be only 324 // one number. Therefore, only check to see that at least one number was found 325 // and processed. 326 CHECK_LE(parts, 2); 327 328 // Tack on as many '00 digits as needed to be sure that exactly three version 329 // numbers are returned. 330 for (int i = 0; i < parts; ++i) { 331 macos_version *= 100; 332 } 333 334 // Checks that the value is within expected bounds corresponding to released 335 // OS version numbers. The most important bit is making sure that the "10.16" 336 // compatibility mode isn't engaged. 337 CHECK(macos_version >= 10'00'00); 338 CHECK(macos_version < 10'16'00 || macos_version >= 11'00'00); 339 340 return macos_version; 341} 342 343} // namespace 344 345int ParseOSProductVersionForTesting(const std::string_view& version) { 346 return ParseOSProductVersion(version); 347} 348 349int MacOSVersion() { 350 static int macos_version = ParseOSProductVersion( 351 StringSysctlByName("kern.osproductversion").value()); 352 353 return macos_version; 354} 355 356namespace { 357 358#if defined(ARCH_CPU_X86_64) 359// https://developer.apple.com/documentation/apple_silicon/about_the_rosetta_translation_environment#3616845 360bool ProcessIsTranslated() { 361 int ret = 0; 362 size_t size = sizeof(ret); 363 if (sysctlbyname("sysctl.proc_translated", &ret, &size, nullptr, 0) == -1) { 364 return false; 365 } 366 return ret; 367} 368#endif // ARCH_CPU_X86_64 369 370} // namespace 371 372CPUType GetCPUType() { 373#if defined(ARCH_CPU_ARM64) 374 return CPUType::kArm; 375#elif defined(ARCH_CPU_X86_64) 376 return ProcessIsTranslated() ? CPUType::kTranslatedIntel : CPUType::kIntel; 377#else 378#error Time for another chip transition? 379#endif // ARCH_CPU_* 380} 381 382std::string GetOSDisplayName() { 383 std::string version_string = base::SysNSStringToUTF8( 384 NSProcessInfo.processInfo.operatingSystemVersionString); 385 return "macOS " + version_string; 386} 387 388std::string GetPlatformSerialNumber() { 389 base::mac::ScopedIOObject<io_service_t> expert_device( 390 IOServiceGetMatchingService(kIOMasterPortDefault, 391 IOServiceMatching("IOPlatformExpertDevice"))); 392 if (!expert_device) { 393 DLOG(ERROR) << "Error retrieving the machine serial number."; 394 return std::string(); 395 } 396 397 apple::ScopedCFTypeRef<CFTypeRef> serial_number( 398 IORegistryEntryCreateCFProperty(expert_device.get(), 399 CFSTR(kIOPlatformSerialNumberKey), 400 kCFAllocatorDefault, 0)); 401 CFStringRef serial_number_cfstring = 402 base::apple::CFCast<CFStringRef>(serial_number.get()); 403 if (!serial_number_cfstring) { 404 DLOG(ERROR) << "Error retrieving the machine serial number."; 405 return std::string(); 406 } 407 408 return base::SysCFStringRefToUTF8(serial_number_cfstring); 409} 410 411void OpenSystemSettingsPane(SystemSettingsPane pane, 412 const std::string& id_param) { 413 NSString* url = nil; 414 NSString* pane_file = nil; 415 NSData* subpane_data = nil; 416 // On macOS 13 and later, System Settings are implemented with app extensions 417 // found at /System/Library/ExtensionKit/Extensions/. URLs to open them are 418 // constructed with a scheme of "x-apple.systempreferences" and a body of the 419 // the bundle ID of the app extension. (In the Info.plist there is an 420 // EXAppExtensionAttributes dictionary with legacy identifiers, but given that 421 // those are explicitly named "legacy", this code prefers to use the bundle 422 // IDs for the URLs it uses.) It is not yet known how to definitively identify 423 // the query string used to open sub-panes; the ones used below were 424 // determined from historical usage, disassembly of related code, and 425 // guessing. Clarity was requested from Apple in FB11753405. The current best 426 // guess is to analyze the method named -revealElementForKey:, but because 427 // the extensions are all written in Swift it's hard to confirm this is 428 // correct or to use this knowledge. 429 // 430 // For macOS 12 and earlier, to determine the `subpane_data`, find a method 431 // named -handleOpenParameter: which takes an AEDesc as a parameter. 432 switch (pane) { 433 case SystemSettingsPane::kAccessibility_Captions: 434 if (MacOSMajorVersion() >= 13) { 435 url = @"x-apple.systempreferences:com.apple.Accessibility-Settings." 436 @"extension?Captioning"; 437 } else { 438 url = @"x-apple.systempreferences:com.apple.preference.universalaccess?" 439 @"Captioning"; 440 } 441 break; 442 case SystemSettingsPane::kDateTime: 443 if (MacOSMajorVersion() >= 13) { 444 url = 445 @"x-apple.systempreferences:com.apple.Date-Time-Settings.extension"; 446 } else { 447 pane_file = @"/System/Library/PreferencePanes/DateAndTime.prefPane"; 448 } 449 break; 450 case SystemSettingsPane::kNetwork_Proxies: 451 if (MacOSMajorVersion() >= 13) { 452 url = @"x-apple.systempreferences:com.apple.Network-Settings.extension?" 453 @"Proxies"; 454 } else { 455 pane_file = @"/System/Library/PreferencePanes/Network.prefPane"; 456 subpane_data = [@"Proxies" dataUsingEncoding:NSASCIIStringEncoding]; 457 } 458 break; 459 case SystemSettingsPane::kNotifications: 460 if (MacOSMajorVersion() >= 13) { 461 url = @"x-apple.systempreferences:com.apple.Notifications-Settings." 462 @"extension"; 463 if (!id_param.empty()) { 464 url = [url stringByAppendingFormat:@"?id=%s", id_param.c_str()]; 465 } 466 } else { 467 pane_file = @"/System/Library/PreferencePanes/Notifications.prefPane"; 468 NSDictionary* subpane_dict = @{ 469 @"command" : @"show", 470 @"identifier" : SysUTF8ToNSString(id_param) 471 }; 472 subpane_data = [NSPropertyListSerialization 473 dataWithPropertyList:subpane_dict 474 format:NSPropertyListXMLFormat_v1_0 475 options:0 476 error:nil]; 477 } 478 break; 479 case SystemSettingsPane::kPrintersScanners: 480 if (MacOSMajorVersion() >= 13) { 481 url = @"x-apple.systempreferences:com.apple.Print-Scan-Settings." 482 @"extension"; 483 } else { 484 pane_file = @"/System/Library/PreferencePanes/PrintAndFax.prefPane"; 485 } 486 break; 487 case SystemSettingsPane::kPrivacySecurity_Accessibility: 488 if (MacOSMajorVersion() >= 13) { 489 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 490 @"extension?Privacy_Accessibility"; 491 } else { 492 url = @"x-apple.systempreferences:com.apple.preference.security?" 493 @"Privacy_Accessibility"; 494 } 495 break; 496 case SystemSettingsPane::kPrivacySecurity_Bluetooth: 497 if (MacOSMajorVersion() >= 13) { 498 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 499 @"extension?Privacy_Bluetooth"; 500 } else { 501 url = @"x-apple.systempreferences:com.apple.preference.security?" 502 @"Privacy_Bluetooth"; 503 } 504 break; 505 case SystemSettingsPane::kPrivacySecurity_Camera: 506 if (MacOSMajorVersion() >= 13) { 507 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 508 @"extension?Privacy_Camera"; 509 } else { 510 url = @"x-apple.systempreferences:com.apple.preference.security?" 511 @"Privacy_Camera"; 512 } 513 break; 514 case SystemSettingsPane::kPrivacySecurity_Extensions_Sharing: 515 if (MacOSMajorVersion() >= 13) { 516 // See ShareKit, -[SHKSharingServicePicker openAppExtensionsPrefpane]. 517 url = @"x-apple.systempreferences:com.apple.ExtensionsPreferences?" 518 @"Sharing"; 519 } else { 520 // This is equivalent to the implementation of AppKit's 521 // +[NSSharingServicePicker openAppExtensionsPrefPane]. 522 pane_file = @"/System/Library/PreferencePanes/Extensions.prefPane"; 523 NSDictionary* subpane_dict = @{ 524 @"action" : @"revealExtensionPoint", 525 @"protocol" : @"com.apple.share-services" 526 }; 527 subpane_data = [NSPropertyListSerialization 528 dataWithPropertyList:subpane_dict 529 format:NSPropertyListXMLFormat_v1_0 530 options:0 531 error:nil]; 532 } 533 break; 534 case SystemSettingsPane::kPrivacySecurity_LocationServices: 535 if (MacOSMajorVersion() >= 13) { 536 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 537 @"extension?Privacy_LocationServices"; 538 } else { 539 url = @"x-apple.systempreferences:com.apple.preference.security?" 540 @"Privacy_LocationServices"; 541 } 542 break; 543 case SystemSettingsPane::kPrivacySecurity_Microphone: 544 if (MacOSMajorVersion() >= 13) { 545 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 546 @"extension?Privacy_Microphone"; 547 } else { 548 url = @"x-apple.systempreferences:com.apple.preference.security?" 549 @"Privacy_Microphone"; 550 } 551 break; 552 case SystemSettingsPane::kPrivacySecurity_ScreenRecording: 553 if (MacOSMajorVersion() >= 13) { 554 url = @"x-apple.systempreferences:com.apple.settings.PrivacySecurity." 555 @"extension?Privacy_ScreenCapture"; 556 } else { 557 url = @"x-apple.systempreferences:com.apple.preference.security?" 558 @"Privacy_ScreenCapture"; 559 } 560 break; 561 case SystemSettingsPane::kTrackpad: 562 if (MacOSMajorVersion() >= 13) { 563 url = @"x-apple.systempreferences:com.apple.Trackpad-Settings." 564 @"extension"; 565 } else { 566 pane_file = @"/System/Library/PreferencePanes/Trackpad.prefPane"; 567 } 568 break; 569 } 570 571 DCHECK(url != nil ^ pane_file != nil); 572 573 if (url) { 574 [NSWorkspace.sharedWorkspace openURL:[NSURL URLWithString:url]]; 575 return; 576 } 577 578 NSAppleEventDescriptor* subpane_descriptor; 579 NSArray* pane_file_urls = @[ [NSURL fileURLWithPath:pane_file] ]; 580 581 LSLaunchURLSpec launchSpec = {0}; 582 launchSpec.itemURLs = apple::NSToCFPtrCast(pane_file_urls); 583 if (subpane_data) { 584 subpane_descriptor = 585 [[NSAppleEventDescriptor alloc] initWithDescriptorType:'ptru' 586 data:subpane_data]; 587 launchSpec.passThruParams = subpane_descriptor.aeDesc; 588 } 589 launchSpec.launchFlags = kLSLaunchAsync | kLSLaunchDontAddToRecents; 590 591 LSOpenFromURLSpec(&launchSpec, nullptr); 592} 593 594} // namespace base::mac 595