Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / installer / gcapi_mac / gcapi.mm
blobbef6d50e2dded3d54a2f86db4c496a3605de30e0
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>
8 #include <grp.h>
9 #include <pwd.h>
10 #include <sys/stat.h>
11 #include <sys/types.h>
12 #include <sys/utsname.h>
14 namespace {
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)
43     return false;
44   if (strcmp(uname_info.sysname, "Darwin") != 0)
45     return false;
47   char* dot = strchr(uname_info.release, '.');
48   if (!dot)
49     return false;
51   int darwin_major_version = atoi(uname_info.release);
52   if (darwin_major_version < 6)
53     return false;
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
57   // 10.2.
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,
74                                                          kCGSessionUserIDKey);
75   if (CFGetTypeID(ns_uid) != CFNumberGetTypeID())
76     return NULL;
78   uid_t uid;
79   BOOL success = CFNumberGetValue(ns_uid, kCFNumberSInt32Type, &uid);
80   if (!success)
81     return NULL;
83   return getpwuid(uid);
86 enum TicketKind {
87   kSystemTicket, kUserTicket
90 // Replaces "~~" with |home_dir|.
91 NSString* AdjustHomedir(NSString* s, const char* home_dir) {
92   if (![s hasPrefix:@"~~"])
93     return s;
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) {
102   if (chrome_path)
103     *chrome_path = nil;
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)
110                         atIndex:0];
111   }
112   NSEnumerator* e = [keystone_paths objectEnumerator];
113   id ks_path;
114   while ((ks_path = [e nextObject])) {
115     if (![[NSFileManager defaultManager] fileExistsAtPath:ks_path])
116       continue;
118     NSTask* task = nil;
119     NSString* string = nil;
120     bool ksadmin_ran_successfully = false;
122     @try {
123       task = [[NSTask alloc] init];
124       [task setLaunchPath:ks_path];
126       NSArray* arguments = @[
127           kind == kUserTicket ? @"--user-store" : @"--system-store",
128           @"--print-tickets",
129           @"--productid",
130           @"com.google.Chrome",
131       ];
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];
137       }
138       [task setArguments:arguments];
140       NSPipe* pipe = [NSPipe pipe];
141       [task setStandardOutput:pipe];
143       NSFileHandle* file = [pipe fileHandleForReading];
145       [task launch];
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];
153     }
154     @catch (id exception) {
155       // Most likely, ks_path didn't exist.
156     }
157     [task release];
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
166       // chrome.
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)
179         return YES;
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
188       // exist on disk.
189       if (kind == kSystemTicket)
190         return YES;
191       return exists;
192     }
193   }
195   return NO;
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];
215   if (user) {
216     [attributes setObject:[NSNumber numberWithShort:kUserPermissions]
217                    forKey:NSFilePosixPermissions];
218     [attributes setObject:[NSNumber numberWithInt:user->pw_uid]
219                    forKey:NSFileOwnerAccountID];
220   } else {
221     [attributes setObject:[NSNumber numberWithShort:kAdminPermissions]
222                    forKey:NSFilePosixPermissions];
223     [attributes setObject:@"admin" forKey:NSFileGroupOwnerAccountName];
224   }
226   NSFileManager* manager = [NSFileManager defaultManager];
227   return [manager createDirectoryAtPath:path
228             withIntermediateDirectories:YES
229                              attributes:attributes
230                                   error:nil];
233 // Tries to write |data| at |user_path|.
234 // Returns the path where it wrote, or nil on failure.
235 NSString* WriteUserData(NSData* data,
236                         NSString* user_path,
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);
243     return user_path;
244   }
245   return nil;
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,
252                     NSString* user_path,
253                     const passwd* user) {
254   // Try system first.
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);
261     return system_path;
262   }
264   // Failed, try user.
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],
271   };
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];
285   return WriteData(
286       contents, kSystemMasterPrefsPath, kUserMasterPrefsPath, user) != nil;
289 NSString* PathToFramework(NSString* app_path, NSDictionary* info_plist) {
290   NSString* version = [info_plist objectForKey:@"CFBundleShortVersionString"];
291   if (!version)
292     return nil;
293   return [[[app_path
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';
309 }  // namespace
311 int GoogleChromeCompatibilityCheck(unsigned* reasons) {
312   unsigned local_reasons = 0;
313   @autoreleasepool {
314     passwd* user = GetRealUserId();
315     if (!user)
316       return GCCC_ERROR_ACCESSDENIED;
318     if (!IsOSXVersionSupported())
319       local_reasons |= GCCC_ERROR_OSNOTSUPPORTED;
321     NSString* path;
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;
326     }
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;
338     }
340   }
341   if (reasons != NULL)
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))
351     return 0;
353   @autoreleasepool {
354     passwd* user = GetRealUserId();
355     if (!user)
356       return 0;
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
365     // destination.
366     NSString* install_script = PathToInstallScript(app_path, info_plist);
367     if (!install_script) {
368       return 0;
369     }
371     @try {
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
380         // dropped groups).
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,
399                                        app_path_quoted,
400                                        install_path_quoted];
401         [task setArguments:
402             @[run_as, @"-c", install_script_execution]];
403       } else {
404         [task setLaunchPath:install_script];
405         [task setArguments:@[app_path, kChromeInstallPath]];
406       }
408       [task launch];
409       [task waitUntilExit];
410       if ([task terminationStatus] != 0) {
411         return 0;
412       }
413     }
414     @catch (id exception) {
415       return 0;
416     }
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.
437   }
438   return 1;
441 int LaunchGoogleChrome() {
442   @autoreleasepool {
443     passwd* user = GetRealUserId();
444     if (!user)
445       return 0;
447     NSString* app_path;
449     NSString* path;
450     if (FindChromeTicket(kUserTicket, user, &path) && path)
451       app_path = path;
452     else if (FindChromeTicket(kSystemTicket, NULL, &path) && path)
453       app_path = path;
454     else
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];
460   }