[Android] Added UMA for search by image context menu.
[chromium-blink-merge.git] / chrome / installer / gcapi_mac / gcapi.mm
blob57f0d02d58077880e2c25dc335eee4394f600ca3
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 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)
46     return false;
47   if (strcmp(uname_info.sysname, "Darwin") != 0)
48     return false;
50   char* dot = strchr(uname_info.release, '.');
51   if (!dot)
52     return false;
54   int darwin_major_version = atoi(uname_info.release);
55   if (darwin_major_version < 6)
56     return false;
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
60   // 10.2.
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,
77                                                          kCGSessionUserIDKey);
78   if (CFGetTypeID(ns_uid) != CFNumberGetTypeID())
79     return NULL;
81   uid_t uid;
82   BOOL success = CFNumberGetValue(ns_uid, kCFNumberSInt32Type, &uid);
83   if (!success)
84     return NULL;
86   return getpwuid(uid);
89 enum TicketKind {
90   kSystemTicket, kUserTicket
93 // Replaces "~~" with |home_dir|.
94 NSString* AdjustHomedir(NSString* s, const char* home_dir) {
95   if (![s hasPrefix:@"~~"])
96     return s;
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) {
105   if (chrome_path)
106     *chrome_path = nil;
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)
113                         atIndex:0];
114   }
115   NSEnumerator* e = [keystone_paths objectEnumerator];
116   id ks_path;
117   while ((ks_path = [e nextObject])) {
118     if (![[NSFileManager defaultManager] fileExistsAtPath:ks_path])
119       continue;
121     NSTask* task = nil;
122     NSString* string = nil;
123     bool ksadmin_ran_successfully = false;
125     @try {
126       task = [[NSTask alloc] init];
127       [task setLaunchPath:ks_path];
129       NSArray* arguments = @[
130           kind == kUserTicket ? @"--user-store" : @"--system-store",
131           @"--print-tickets",
132           @"--productid",
133           @"com.google.Chrome",
134       ];
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];
140       }
141       [task setArguments:arguments];
143       NSPipe* pipe = [NSPipe pipe];
144       [task setStandardOutput:pipe];
146       NSFileHandle* file = [pipe fileHandleForReading];
148       [task launch];
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];
156     }
157     @catch (id exception) {
158       // Most likely, ks_path didn't exist.
159     }
160     [task release];
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
169       // chrome.
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)
182         return YES;
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
191       // exist on disk.
192       if (kind == kSystemTicket)
193         return YES;
194       return exists;
195     }
196   }
198   return NO;
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];
218   if (user) {
219     [attributes setObject:[NSNumber numberWithShort:kUserPermissions]
220                    forKey:NSFilePosixPermissions];
221     [attributes setObject:[NSNumber numberWithInt:user->pw_uid]
222                    forKey:NSFileOwnerAccountID];
223   } else {
224     [attributes setObject:[NSNumber numberWithShort:kAdminPermissions]
225                    forKey:NSFilePosixPermissions];
226     [attributes setObject:@"admin" forKey:NSFileGroupOwnerAccountName];
227   }
229   NSFileManager* manager = [NSFileManager defaultManager];
230   return [manager createDirectoryAtPath:path
231             withIntermediateDirectories:YES
232                              attributes:attributes
233                                   error:nil];
236 // Tries to write |data| at |user_path|.
237 // Returns the path where it wrote, or nil on failure.
238 NSString* WriteUserData(NSData* data,
239                         NSString* user_path,
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);
246     return user_path;
247   }
248   return nil;
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,
255                     NSString* user_path,
256                     const passwd* user) {
257   // Try system first.
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);
264     return system_path;
265   }
267   // Failed, try user.
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],
274   };
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];
288   return WriteData(
289       contents, kSystemMasterPrefsPath, kUserMasterPrefsPath, user) != nil;
292 NSString* PathToFramework(NSString* app_path, NSDictionary* info_plist) {
293   NSString* version = [info_plist objectForKey:@"CFBundleShortVersionString"];
294   if (!version)
295     return nil;
296   return [[[app_path
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';
312 }  // namespace
314 int GoogleChromeCompatibilityCheck(unsigned* reasons) {
315   unsigned local_reasons = 0;
316   @autoreleasepool {
317     passwd* user = GetRealUserId();
318     if (!user)
319       return GCCC_ERROR_ACCESSDENIED;
321     if (!IsOSXVersionSupported())
322       local_reasons |= GCCC_ERROR_OSNOTSUPPORTED;
324     NSString* path;
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;
329     }
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;
341     }
343   }
344   if (reasons != NULL)
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))
354     return 0;
356   @autoreleasepool {
357     passwd* user = GetRealUserId();
358     if (!user)
359       return 0;
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
368     // destination.
369     NSString* install_script = PathToInstallScript(app_path, info_plist);
370     if (!install_script) {
371       return 0;
372     }
374     @try {
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
383         // dropped groups).
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,
402                                        app_path_quoted,
403                                        install_path_quoted];
404         [task setArguments:
405             @[run_as, @"-c", install_script_execution]];
406       } else {
407         [task setLaunchPath:install_script];
408         [task setArguments:@[app_path, kChromeInstallPath]];
409       }
411       [task launch];
412       [task waitUntilExit];
413       if ([task terminationStatus] != 0) {
414         return 0;
415       }
416     }
417     @catch (id exception) {
418       return 0;
419     }
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.
440   }
441   return 1;
444 int LaunchGoogleChrome() {
445   @autoreleasepool {
446     passwd* user = GetRealUserId();
447     if (!user)
448       return 0;
450     NSString* app_path;
452     NSString* path;
453     if (FindChromeTicket(kUserTicket, user, &path) && path)
454       app_path = path;
455     else if (FindChromeTicket(kSystemTicket, NULL, &path) && path)
456       app_path = path;
457     else
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];
463   }