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 NSString* const kChannelKey = @"KSChannelID";
38 NSString* const kVersionKey = @"KSVersion";
40 // Condensed from chromium's base/mac/mac_util.mm.
41 bool IsOSXVersionSupported() {
42 // On 10.6, Gestalt() was observed to be able to spawn threads (see
43 // http://crbug.com/53200). Don't call Gestalt().
44 struct utsname uname_info;
45 if (uname(&uname_info) != 0)
47 if (strcmp(uname_info.sysname, "Darwin") != 0)
50 char* dot = strchr(uname_info.release, '.');
54 int darwin_major_version = atoi(uname_info.release);
55 if (darwin_major_version < 6)
58 // The Darwin major version is always 4 greater than the Mac OS X minor
59 // version for Darwin versions beginning with 6, corresponding to Mac OS X
61 int mac_os_x_minor_version = darwin_major_version - 4;
63 // Chrome is known to work on 10.6 - 10.8.
64 return mac_os_x_minor_version >= 6 && mac_os_x_minor_version <= 8;
67 // Returns the pid/gid of the logged-in user, even if getuid() claims that the
68 // current user is root.
69 // Returns NULL on error.
70 passwd* GetRealUserId() {
71 CFDictionaryRef session_info_dict = CGSessionCopyCurrentDictionary();
72 [NSMakeCollectable(session_info_dict) autorelease];
73 if (!session_info_dict)
74 return NULL; // Possibly no screen plugged in.
76 CFNumberRef ns_uid = (CFNumberRef)CFDictionaryGetValue(session_info_dict,
78 if (CFGetTypeID(ns_uid) != CFNumberGetTypeID())
82 BOOL success = CFNumberGetValue(ns_uid, kCFNumberSInt32Type, &uid);
90 kSystemTicket, kUserTicket
93 // Replaces "~~" with |home_dir|.
94 NSString* AdjustHomedir(NSString* s, const char* home_dir) {
95 if (![s hasPrefix:@"~~"])
97 NSString* ns_home_dir = [NSString stringWithUTF8String:home_dir];
98 return [ns_home_dir stringByAppendingString:[s substringFromIndex:2]];
101 // If |chrome_path| is not 0, |*chrome_path| is set to the path where chrome
102 // is according to keystone. It's only set if that path exists on disk.
103 BOOL FindChromeTicket(TicketKind kind, const passwd* user,
104 NSString** chrome_path) {
108 // Don't use Objective-C 2 loop syntax, in case an installer runs on 10.4.
109 NSMutableArray* keystone_paths =
110 [NSMutableArray arrayWithObject:kSystemKsadminPath];
111 if (kind == kUserTicket) {
112 [keystone_paths insertObject:AdjustHomedir(kUserKsadminPath, user->pw_dir)
115 NSEnumerator* e = [keystone_paths objectEnumerator];
117 while ((ks_path = [e nextObject])) {
118 if (![[NSFileManager defaultManager] fileExistsAtPath:ks_path])
122 NSString* string = nil;
123 bool ksadmin_ran_successfully = false;
126 task = [[NSTask alloc] init];
127 [task setLaunchPath:ks_path];
129 NSArray* arguments = @[
130 kind == kUserTicket ? @"--user-store" : @"--system-store",
133 @"com.google.Chrome",
135 if (geteuid() == 0 && kind == kUserTicket) {
136 NSString* run_as = [NSString stringWithUTF8String:user->pw_name];
137 [task setLaunchPath:@"/usr/bin/sudo"];
138 arguments = [@[@"-u", run_as, ks_path]
139 arrayByAddingObjectsFromArray:arguments];
141 [task setArguments:arguments];
143 NSPipe* pipe = [NSPipe pipe];
144 [task setStandardOutput:pipe];
146 NSFileHandle* file = [pipe fileHandleForReading];
150 NSData* data = [file readDataToEndOfFile];
151 [task waitUntilExit];
153 ksadmin_ran_successfully = [task terminationStatus] == 0;
154 string = [[[NSString alloc] initWithData:data
155 encoding:NSUTF8StringEncoding] autorelease];
157 @catch (id exception) {
158 // Most likely, ks_path didn't exist.
162 if (ksadmin_ran_successfully && [string length] > 0) {
163 // If the user deleted chrome, it doesn't get unregistered in keystone.
164 // Check if the path keystone thinks chrome is at still exists, and if not
165 // treat this as "chrome isn't installed". Sniff for
166 // xc=<KSPathExistenceChecker:1234 path=/Applications/Google Chrome.app>
167 // in the output. But don't mess with system tickets, since reinstalling
168 // a user chrome on top of a system ticket produces a non-autoupdating
170 NSRange start = [string rangeOfString:@"\n\txc=<KSPathExistenceChecker:"];
171 if (start.location == NSNotFound && start.length == 0)
172 return YES; // Err on the cautious side.
173 string = [string substringFromIndex:start.location];
175 start = [string rangeOfString:@"path="];
176 if (start.location == NSNotFound && start.length == 0)
177 return YES; // Err on the cautious side.
178 string = [string substringFromIndex:start.location];
180 NSRange end = [string rangeOfString:@".app>\n\t"];
181 if (end.location == NSNotFound && end.length == 0)
184 string = [string substringToIndex:NSMaxRange(end) - [@">\n\t" length]];
185 string = [string substringFromIndex:start.length];
187 BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:string];
188 if (exists && chrome_path)
189 *chrome_path = string;
190 // Don't allow reinstallation over a system ticket, even if chrome doesn't
192 if (kind == kSystemTicket)
201 // File permission mask for files created by gcapi.
202 const mode_t kUserPermissions = 0755;
203 const mode_t kAdminPermissions = 0775;
205 BOOL CreatePathToFile(NSString* path, const passwd* user) {
206 path = [path stringByDeletingLastPathComponent];
208 // Default owner, group, permissions:
209 // * Permissions are set according to the umask of the current process. For
210 // more information, see umask.
211 // * The owner ID is set to the effective user ID of the process.
212 // * The group ID is set to that of the parent directory.
213 // The default group ID is fine. Owner ID is fine if creating a system path,
214 // but when creating a user path explicitly set the owner in case euid is 0.
215 // Do set permissions explicitly; for admin paths all admins can write, for
216 // user paths just the owner may.
217 NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
219 [attributes setObject:[NSNumber numberWithShort:kUserPermissions]
220 forKey:NSFilePosixPermissions];
221 [attributes setObject:[NSNumber numberWithInt:user->pw_uid]
222 forKey:NSFileOwnerAccountID];
224 [attributes setObject:[NSNumber numberWithShort:kAdminPermissions]
225 forKey:NSFilePosixPermissions];
226 [attributes setObject:@"admin" forKey:NSFileGroupOwnerAccountName];
229 NSFileManager* manager = [NSFileManager defaultManager];
230 return [manager createDirectoryAtPath:path
231 withIntermediateDirectories:YES
232 attributes:attributes
236 // Tries to write |data| at |user_path|.
237 // Returns the path where it wrote, or nil on failure.
238 NSString* WriteUserData(NSData* data,
240 const passwd* user) {
241 user_path = AdjustHomedir(user_path, user->pw_dir);
242 if (CreatePathToFile(user_path, user) &&
243 [data writeToFile:user_path atomically:YES]) {
244 chmod([user_path fileSystemRepresentation], kUserPermissions & ~0111);
245 chown([user_path fileSystemRepresentation], user->pw_uid, user->pw_gid);
251 // Tries to write |data| at |system_path| or if that fails at |user_path|.
252 // Returns the path where it wrote, or nil on failure.
253 NSString* WriteData(NSData* data,
254 NSString* system_path,
256 const passwd* user) {
258 if (CreatePathToFile(system_path, NULL) &&
259 [data writeToFile:system_path atomically:YES]) {
260 chmod([system_path fileSystemRepresentation], kAdminPermissions & ~0111);
261 // Make sure the file is owned by group admin.
262 if (group* group = getgrnam("admin"))
263 chown([system_path fileSystemRepresentation], 0, group->gr_gid);
268 return WriteUserData(data, user_path, user);
271 NSString* WriteBrandCode(const char* brand_code, const passwd* user) {
272 NSDictionary* brand_dict = @{
273 kBrandKey: [NSString stringWithUTF8String:brand_code],
275 NSData* contents = [NSPropertyListSerialization
276 dataFromPropertyList:brand_dict
277 format:NSPropertyListBinaryFormat_v1_0
278 errorDescription:nil];
280 return WriteUserData(contents, kUserBrandPath, user);
283 BOOL WriteMasterPrefs(const char* master_prefs_contents,
284 size_t master_prefs_contents_size,
285 const passwd* user) {
286 NSData* contents = [NSData dataWithBytes:master_prefs_contents
287 length:master_prefs_contents_size];
289 contents, kSystemMasterPrefsPath, kUserMasterPrefsPath, user) != nil;
292 NSString* PathToFramework(NSString* app_path, NSDictionary* info_plist) {
293 NSString* version = [info_plist objectForKey:@"CFBundleShortVersionString"];
297 stringByAppendingPathComponent:@"Contents/Versions"]
298 stringByAppendingPathComponent:version]
299 stringByAppendingPathComponent:@"Google Chrome Framework.framework"];
302 NSString* PathToInstallScript(NSString* app_path, NSDictionary* info_plist) {
303 return [PathToFramework(app_path, info_plist) stringByAppendingPathComponent:
304 @"Resources/install.sh"];
307 bool isbrandchar(int c) {
308 // Always four upper-case alpha chars.
309 return c >= 'A' && c <= 'Z';
314 int GoogleChromeCompatibilityCheck(unsigned* reasons) {
315 unsigned local_reasons = 0;
317 passwd* user = GetRealUserId();
319 return GCCC_ERROR_ACCESSDENIED;
321 if (!IsOSXVersionSupported())
322 local_reasons |= GCCC_ERROR_OSNOTSUPPORTED;
325 if (FindChromeTicket(kSystemTicket, NULL, &path)) {
326 local_reasons |= GCCC_ERROR_ALREADYPRESENT;
327 if (!path) // Ticket points to nothingness.
328 local_reasons |= GCCC_ERROR_ACCESSDENIED;
331 if (FindChromeTicket(kUserTicket, user, NULL))
332 local_reasons |= GCCC_ERROR_ALREADYPRESENT;
334 if ([[NSFileManager defaultManager] fileExistsAtPath:kChromeInstallPath])
335 local_reasons |= GCCC_ERROR_ALREADYPRESENT;
337 if ((local_reasons & GCCC_ERROR_ALREADYPRESENT) == 0) {
338 if (![[NSFileManager defaultManager]
339 isWritableFileAtPath:@"/Applications"])
340 local_reasons |= GCCC_ERROR_ACCESSDENIED;
345 *reasons = local_reasons;
346 return local_reasons == 0;
349 int InstallGoogleChrome(const char* source_path,
350 const char* brand_code,
351 const char* master_prefs_contents,
352 unsigned master_prefs_contents_size) {
353 if (!GoogleChromeCompatibilityCheck(NULL))
357 passwd* user = GetRealUserId();
361 NSString* app_path = [NSString stringWithUTF8String:source_path];
362 NSString* info_plist_path =
363 [app_path stringByAppendingPathComponent:@"Contents/Info.plist"];
364 NSDictionary* info_plist =
365 [NSDictionary dictionaryWithContentsOfFile:info_plist_path];
367 // Use install.sh from the Chrome app bundle to copy Chrome to its
369 NSString* install_script = PathToInstallScript(app_path, info_plist);
370 if (!install_script) {
375 NSTask* task = [[[NSTask alloc] init] autorelease];
377 // install.sh tries to make the installed app admin-writable, but
378 // only when it's not run as root.
379 if (geteuid() == 0) {
380 // Use |su $(whoami)| instead of sudo -u. If the current user is in more
381 // than 16 groups, |sudo -u $(whoami)| will drop all but the first 16
382 // groups, which can lead to problems (e.g. if "admin" is one of the
384 // Since geteuid() is 0, su won't prompt for a password.
385 NSString* run_as = [NSString stringWithUTF8String:user->pw_name];
386 [task setLaunchPath:@"/usr/bin/su"];
388 NSString* single_quote_escape = @"'\"'\"'";
389 NSString* install_script_quoted = [install_script
390 stringByReplacingOccurrencesOfString:@"'"
391 withString:single_quote_escape];
392 NSString* app_path_quoted =
393 [app_path stringByReplacingOccurrencesOfString:@"'"
394 withString:single_quote_escape];
395 NSString* install_path_quoted = [kChromeInstallPath
396 stringByReplacingOccurrencesOfString:@"'"
397 withString:single_quote_escape];
399 NSString* install_script_execution =
400 [NSString stringWithFormat:@"exec '%@' '%@' '%@'",
401 install_script_quoted,
403 install_path_quoted];
405 @[run_as, @"-c", install_script_execution]];
407 [task setLaunchPath:install_script];
408 [task setArguments:@[app_path, kChromeInstallPath]];
412 [task waitUntilExit];
413 if ([task terminationStatus] != 0) {
417 @catch (id exception) {
421 // Set brand code. If Chrome's Info.plist contains a brand code, use that.
422 NSString* info_plist_brand = [info_plist objectForKey:kBrandKey];
423 if (info_plist_brand &&
424 [info_plist_brand respondsToSelector:@selector(UTF8String)])
425 brand_code = [info_plist_brand UTF8String];
427 BOOL valid_brand_code = brand_code && strlen(brand_code) == 4 &&
428 isbrandchar(brand_code[0]) && isbrandchar(brand_code[1]) &&
429 isbrandchar(brand_code[2]) && isbrandchar(brand_code[3]);
431 NSString* brand_path = nil;
432 if (valid_brand_code)
433 brand_path = WriteBrandCode(brand_code, user);
435 // Write master prefs.
436 if (master_prefs_contents)
437 WriteMasterPrefs(master_prefs_contents, master_prefs_contents_size, user);
439 // TODO Set default browser if requested.
444 int LaunchGoogleChrome() {
446 passwd* user = GetRealUserId();
453 if (FindChromeTicket(kUserTicket, user, &path) && path)
455 else if (FindChromeTicket(kSystemTicket, NULL, &path) && path)
458 app_path = kChromeInstallPath;
460 // NSWorkspace launches processes as the current console owner,
461 // even when running with euid of 0.
462 return [[NSWorkspace sharedWorkspace] launchApplication:app_path];