1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/installer/gcapi_mac/gcapi.h"
7 #import <Cocoa/Cocoa.h>
11 #include <sys/types.h>
12 #include <sys/utsname.h>
16 // The "~~" prefixes are replaced with the home directory of the
17 // console owner (i.e. not the home directory of the euid).
18 NSString* const kChromeInstallPath = @"/Applications/Google Chrome.app";
20 NSString* const kBrandKey = @"KSBrandID";
21 NSString* const kUserBrandPath = @"~~/Library/Google/Google Chrome Brand.plist";
23 NSString* const kSystemKsadminPath =
24 @"/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
25 "Contents/MacOS/ksadmin";
27 NSString* const kUserKsadminPath =
28 @"~~/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
29 "Contents/MacOS/ksadmin";
31 NSString* const kSystemMasterPrefsPath =
32 @"/Library/Google/Google Chrome Master Preferences";
33 NSString* const kUserMasterPrefsPath =
34 @"~~/Library/Application Support/Google/Chrome/"
35 "Google Chrome Master Preferences";
37 // Condensed from chromium's base/mac/mac_util.mm.
38 bool IsOSXVersionSupported() {
39 // On 10.6, Gestalt() was observed to be able to spawn threads (see
40 // http://crbug.com/53200). Don't call Gestalt().
41 struct utsname uname_info;
42 if (uname(&uname_info) != 0)
44 if (strcmp(uname_info.sysname, "Darwin") != 0)
47 char* dot = strchr(uname_info.release, '.');
51 int darwin_major_version = atoi(uname_info.release);
52 if (darwin_major_version < 6)
55 // The Darwin major version is always 4 greater than the Mac OS X minor
56 // version for Darwin versions beginning with 6, corresponding to Mac OS X
58 int mac_os_x_minor_version = darwin_major_version - 4;
60 // Chrome is known to work on 10.6 - 10.10.
61 return mac_os_x_minor_version >= 6 && mac_os_x_minor_version <= 10;
64 // Returns the pid/gid of the logged-in user, even if getuid() claims that the
65 // current user is root.
66 // Returns NULL on error.
67 passwd* GetRealUserId() {
68 CFDictionaryRef session_info_dict = CGSessionCopyCurrentDictionary();
69 [NSMakeCollectable(session_info_dict) autorelease];
70 if (!session_info_dict)
71 return NULL; // Possibly no screen plugged in.
73 CFNumberRef ns_uid = (CFNumberRef)CFDictionaryGetValue(session_info_dict,
75 if (CFGetTypeID(ns_uid) != CFNumberGetTypeID())
79 BOOL success = CFNumberGetValue(ns_uid, kCFNumberSInt32Type, &uid);
87 kSystemTicket, kUserTicket
90 // Replaces "~~" with |home_dir|.
91 NSString* AdjustHomedir(NSString* s, const char* home_dir) {
92 if (![s hasPrefix:@"~~"])
94 NSString* ns_home_dir = [NSString stringWithUTF8String:home_dir];
95 return [ns_home_dir stringByAppendingString:[s substringFromIndex:2]];
98 // If |chrome_path| is not 0, |*chrome_path| is set to the path where chrome
99 // is according to keystone. It's only set if that path exists on disk.
100 BOOL FindChromeTicket(TicketKind kind, const passwd* user,
101 NSString** chrome_path) {
105 // Don't use Objective-C 2 loop syntax, in case an installer runs on 10.4.
106 NSMutableArray* keystone_paths =
107 [NSMutableArray arrayWithObject:kSystemKsadminPath];
108 if (kind == kUserTicket) {
109 [keystone_paths insertObject:AdjustHomedir(kUserKsadminPath, user->pw_dir)
112 NSEnumerator* e = [keystone_paths objectEnumerator];
114 while ((ks_path = [e nextObject])) {
115 if (![[NSFileManager defaultManager] fileExistsAtPath:ks_path])
119 NSString* string = nil;
120 bool ksadmin_ran_successfully = false;
123 task = [[NSTask alloc] init];
124 [task setLaunchPath:ks_path];
126 NSArray* arguments = @[
127 kind == kUserTicket ? @"--user-store" : @"--system-store",
130 @"com.google.Chrome",
132 if (geteuid() == 0 && kind == kUserTicket) {
133 NSString* run_as = [NSString stringWithUTF8String:user->pw_name];
134 [task setLaunchPath:@"/usr/bin/sudo"];
135 arguments = [@[@"-u", run_as, ks_path]
136 arrayByAddingObjectsFromArray:arguments];
138 [task setArguments:arguments];
140 NSPipe* pipe = [NSPipe pipe];
141 [task setStandardOutput:pipe];
143 NSFileHandle* file = [pipe fileHandleForReading];
147 NSData* data = [file readDataToEndOfFile];
148 [task waitUntilExit];
150 ksadmin_ran_successfully = [task terminationStatus] == 0;
151 string = [[[NSString alloc] initWithData:data
152 encoding:NSUTF8StringEncoding] autorelease];
154 @catch (id exception) {
155 // Most likely, ks_path didn't exist.
159 if (ksadmin_ran_successfully && [string length] > 0) {
160 // If the user deleted chrome, it doesn't get unregistered in keystone.
161 // Check if the path keystone thinks chrome is at still exists, and if not
162 // treat this as "chrome isn't installed". Sniff for
163 // xc=<KSPathExistenceChecker:1234 path=/Applications/Google Chrome.app>
164 // in the output. But don't mess with system tickets, since reinstalling
165 // a user chrome on top of a system ticket produces a non-autoupdating
167 NSRange start = [string rangeOfString:@"\n\txc=<KSPathExistenceChecker:"];
168 if (start.location == NSNotFound && start.length == 0)
169 return YES; // Err on the cautious side.
170 string = [string substringFromIndex:start.location];
172 start = [string rangeOfString:@"path="];
173 if (start.location == NSNotFound && start.length == 0)
174 return YES; // Err on the cautious side.
175 string = [string substringFromIndex:start.location];
177 NSRange end = [string rangeOfString:@".app>\n\t"];
178 if (end.location == NSNotFound && end.length == 0)
181 string = [string substringToIndex:NSMaxRange(end) - [@">\n\t" length]];
182 string = [string substringFromIndex:start.length];
184 BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:string];
185 if (exists && chrome_path)
186 *chrome_path = string;
187 // Don't allow reinstallation over a system ticket, even if chrome doesn't
189 if (kind == kSystemTicket)
198 // File permission mask for files created by gcapi.
199 const mode_t kUserPermissions = 0755;
200 const mode_t kAdminPermissions = 0775;
202 BOOL CreatePathToFile(NSString* path, const passwd* user) {
203 path = [path stringByDeletingLastPathComponent];
205 // Default owner, group, permissions:
206 // * Permissions are set according to the umask of the current process. For
207 // more information, see umask.
208 // * The owner ID is set to the effective user ID of the process.
209 // * The group ID is set to that of the parent directory.
210 // The default group ID is fine. Owner ID is fine if creating a system path,
211 // but when creating a user path explicitly set the owner in case euid is 0.
212 // Do set permissions explicitly; for admin paths all admins can write, for
213 // user paths just the owner may.
214 NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
216 [attributes setObject:[NSNumber numberWithShort:kUserPermissions]
217 forKey:NSFilePosixPermissions];
218 [attributes setObject:[NSNumber numberWithInt:user->pw_uid]
219 forKey:NSFileOwnerAccountID];
221 [attributes setObject:[NSNumber numberWithShort:kAdminPermissions]
222 forKey:NSFilePosixPermissions];
223 [attributes setObject:@"admin" forKey:NSFileGroupOwnerAccountName];
226 NSFileManager* manager = [NSFileManager defaultManager];
227 return [manager createDirectoryAtPath:path
228 withIntermediateDirectories:YES
229 attributes:attributes
233 // Tries to write |data| at |user_path|.
234 // Returns the path where it wrote, or nil on failure.
235 NSString* WriteUserData(NSData* data,
237 const passwd* user) {
238 user_path = AdjustHomedir(user_path, user->pw_dir);
239 if (CreatePathToFile(user_path, user) &&
240 [data writeToFile:user_path atomically:YES]) {
241 chmod([user_path fileSystemRepresentation], kUserPermissions & ~0111);
242 chown([user_path fileSystemRepresentation], user->pw_uid, user->pw_gid);
248 // Tries to write |data| at |system_path| or if that fails at |user_path|.
249 // Returns the path where it wrote, or nil on failure.
250 NSString* WriteData(NSData* data,
251 NSString* system_path,
253 const passwd* user) {
255 if (CreatePathToFile(system_path, NULL) &&
256 [data writeToFile:system_path atomically:YES]) {
257 chmod([system_path fileSystemRepresentation], kAdminPermissions & ~0111);
258 // Make sure the file is owned by group admin.
259 if (group* group = getgrnam("admin"))
260 chown([system_path fileSystemRepresentation], 0, group->gr_gid);
265 return WriteUserData(data, user_path, user);
268 NSString* WriteBrandCode(const char* brand_code, const passwd* user) {
269 NSDictionary* brand_dict = @{
270 kBrandKey: [NSString stringWithUTF8String:brand_code],
272 NSData* contents = [NSPropertyListSerialization
273 dataFromPropertyList:brand_dict
274 format:NSPropertyListBinaryFormat_v1_0
275 errorDescription:nil];
277 return WriteUserData(contents, kUserBrandPath, user);
280 BOOL WriteMasterPrefs(const char* master_prefs_contents,
281 size_t master_prefs_contents_size,
282 const passwd* user) {
283 NSData* contents = [NSData dataWithBytes:master_prefs_contents
284 length:master_prefs_contents_size];
286 contents, kSystemMasterPrefsPath, kUserMasterPrefsPath, user) != nil;
289 NSString* PathToFramework(NSString* app_path, NSDictionary* info_plist) {
290 NSString* version = [info_plist objectForKey:@"CFBundleShortVersionString"];
294 stringByAppendingPathComponent:@"Contents/Versions"]
295 stringByAppendingPathComponent:version]
296 stringByAppendingPathComponent:@"Google Chrome Framework.framework"];
299 NSString* PathToInstallScript(NSString* app_path, NSDictionary* info_plist) {
300 return [PathToFramework(app_path, info_plist) stringByAppendingPathComponent:
301 @"Resources/install.sh"];
304 bool isbrandchar(int c) {
305 // Always four upper-case alpha chars.
306 return c >= 'A' && c <= 'Z';
311 int GoogleChromeCompatibilityCheck(unsigned* reasons) {
312 unsigned local_reasons = 0;
314 passwd* user = GetRealUserId();
316 return GCCC_ERROR_ACCESSDENIED;
318 if (!IsOSXVersionSupported())
319 local_reasons |= GCCC_ERROR_OSNOTSUPPORTED;
322 if (FindChromeTicket(kSystemTicket, NULL, &path)) {
323 local_reasons |= GCCC_ERROR_ALREADYPRESENT;
324 if (!path) // Ticket points to nothingness.
325 local_reasons |= GCCC_ERROR_ACCESSDENIED;
328 if (FindChromeTicket(kUserTicket, user, NULL))
329 local_reasons |= GCCC_ERROR_ALREADYPRESENT;
331 if ([[NSFileManager defaultManager] fileExistsAtPath:kChromeInstallPath])
332 local_reasons |= GCCC_ERROR_ALREADYPRESENT;
334 if ((local_reasons & GCCC_ERROR_ALREADYPRESENT) == 0) {
335 if (![[NSFileManager defaultManager]
336 isWritableFileAtPath:@"/Applications"])
337 local_reasons |= GCCC_ERROR_ACCESSDENIED;
342 *reasons = local_reasons;
343 return local_reasons == 0;
346 int InstallGoogleChrome(const char* source_path,
347 const char* brand_code,
348 const char* master_prefs_contents,
349 unsigned master_prefs_contents_size) {
350 if (!GoogleChromeCompatibilityCheck(NULL))
354 passwd* user = GetRealUserId();
358 NSString* app_path = [NSString stringWithUTF8String:source_path];
359 NSString* info_plist_path =
360 [app_path stringByAppendingPathComponent:@"Contents/Info.plist"];
361 NSDictionary* info_plist =
362 [NSDictionary dictionaryWithContentsOfFile:info_plist_path];
364 // Use install.sh from the Chrome app bundle to copy Chrome to its
366 NSString* install_script = PathToInstallScript(app_path, info_plist);
367 if (!install_script) {
372 NSTask* task = [[[NSTask alloc] init] autorelease];
374 // install.sh tries to make the installed app admin-writable, but
375 // only when it's not run as root.
376 if (geteuid() == 0) {
377 // Use |su $(whoami)| instead of sudo -u. If the current user is in more
378 // than 16 groups, |sudo -u $(whoami)| will drop all but the first 16
379 // groups, which can lead to problems (e.g. if "admin" is one of the
381 // Since geteuid() is 0, su won't prompt for a password.
382 NSString* run_as = [NSString stringWithUTF8String:user->pw_name];
383 [task setLaunchPath:@"/usr/bin/su"];
385 NSString* single_quote_escape = @"'\"'\"'";
386 NSString* install_script_quoted = [install_script
387 stringByReplacingOccurrencesOfString:@"'"
388 withString:single_quote_escape];
389 NSString* app_path_quoted =
390 [app_path stringByReplacingOccurrencesOfString:@"'"
391 withString:single_quote_escape];
392 NSString* install_path_quoted = [kChromeInstallPath
393 stringByReplacingOccurrencesOfString:@"'"
394 withString:single_quote_escape];
396 NSString* install_script_execution =
397 [NSString stringWithFormat:@"exec '%@' '%@' '%@'",
398 install_script_quoted,
400 install_path_quoted];
402 @[run_as, @"-c", install_script_execution]];
404 [task setLaunchPath:install_script];
405 [task setArguments:@[app_path, kChromeInstallPath]];
409 [task waitUntilExit];
410 if ([task terminationStatus] != 0) {
414 @catch (id exception) {
418 // Set brand code. If Chrome's Info.plist contains a brand code, use that.
419 NSString* info_plist_brand = [info_plist objectForKey:kBrandKey];
420 if (info_plist_brand &&
421 [info_plist_brand respondsToSelector:@selector(UTF8String)])
422 brand_code = [info_plist_brand UTF8String];
424 BOOL valid_brand_code = brand_code && strlen(brand_code) == 4 &&
425 isbrandchar(brand_code[0]) && isbrandchar(brand_code[1]) &&
426 isbrandchar(brand_code[2]) && isbrandchar(brand_code[3]);
428 NSString* brand_path = nil;
429 if (valid_brand_code)
430 brand_path = WriteBrandCode(brand_code, user);
432 // Write master prefs.
433 if (master_prefs_contents)
434 WriteMasterPrefs(master_prefs_contents, master_prefs_contents_size, user);
436 // TODO Set default browser if requested.
441 int LaunchGoogleChrome() {
443 passwd* user = GetRealUserId();
450 if (FindChromeTicket(kUserTicket, user, &path) && path)
452 else if (FindChromeTicket(kSystemTicket, NULL, &path) && path)
455 app_path = kChromeInstallPath;
457 // NSWorkspace launches processes as the current console owner,
458 // even when running with euid of 0.
459 return [[NSWorkspace sharedWorkspace] launchApplication:app_path];