diff options
author | thakis@chromium.org <thakis@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-24 16:34:37 +0000 |
---|---|---|
committer | thakis@chromium.org <thakis@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-24 16:34:37 +0000 |
commit | 4a80767ed12331099fe3b1b464875d318d1da895 (patch) | |
tree | e3771f06cc76ff4cfa61726da2b8fa589aec86ed /chrome/installer/gcapi_mac | |
parent | ed2e8624d702924934de4c180fbe555450ecdfd8 (diff) | |
download | chromium_src-4a80767ed12331099fe3b1b464875d318d1da895.zip chromium_src-4a80767ed12331099fe3b1b464875d318d1da895.tar.gz chromium_src-4a80767ed12331099fe3b1b464875d318d1da895.tar.bz2 |
mac gcapi: Add a InstallGoogleChrome() function.
BUG=128462
TEST=none
Review URL: https://chromiumcodereview.appspot.com/10407047
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@138813 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/installer/gcapi_mac')
-rw-r--r-- | chrome/installer/gcapi_mac/gcapi.h | 32 | ||||
-rw-r--r-- | chrome/installer/gcapi_mac/gcapi.mm | 260 |
2 files changed, 269 insertions, 23 deletions
diff --git a/chrome/installer/gcapi_mac/gcapi.h b/chrome/installer/gcapi_mac/gcapi.h index 28a23aa..ec454b1 100644 --- a/chrome/installer/gcapi_mac/gcapi.h +++ b/chrome/installer/gcapi_mac/gcapi.h @@ -18,19 +18,31 @@ extern "C" { #endif -// This function returns YES if Google Chrome should be offered. -// If the return is NO, |reasons| explains why. If you don't care for the +// This function returns nonzero if Google Chrome should be offered. +// If the return value is 0, |reasons| explains why. If you don't care for the // reason, you can pass NULL for |reasons|. -// |set_flag| indicates whether a flag should be set indicating that Chrome was -// offered within the last six months; if passed NO, this method will not -// set the flag even if Chrome can be offered. If passed TRUE, this method -// will set the flag only if Chrome can be offered. -// |shell_mode| should be set to one of GCAPI_INVOKED_STANDARD_SHELL or -// GCAPI_INVOKED_UAC_ELEVATION depending on whether this method is invoked -// from an elevated or non-elevated process. TODO(thakis): This doesn't make -// sense on mac, change comment. int GoogleChromeCompatibilityCheck(unsigned* reasons); +// This function installs Google Chrome in the application folder and optionally +// sets up the brand code and master prefs. +// |source_path| Path to an uninstalled Google Chrome.app directory, for example +// in a mounted dmg, in file system representation. +// |brand_code| If not NULL, a string containing the brand code Google Chrome +// should use. Has no effect if Google Chrome has an embedded brand +// code. Overwrites existing brand files. +// |master_prefs_contents| If not NULL, the _contents_ of a master prefs file +// Google Chrome should use. This is not a path. +// Overwrites existing master pref files. +// Returns nonzero if Google Chrome was successfully copied. If copying +// succeeded but writing of master prefs, brand code, or other noncrucial +// setup tasks fail, this still returns nonzero. +// Returns 0 if the installation failed, for example if Google Chrome was +// already installed, or no disk space was left. +int InstallGoogleChrome(const char* source_path, + const char* brand_code, + const char* master_prefs_contents, + unsigned master_prefs_contents_size); + // This function launches Google Chrome after a successful install. int LaunchGoogleChrome(); diff --git a/chrome/installer/gcapi_mac/gcapi.mm b/chrome/installer/gcapi_mac/gcapi.mm index 12413f5..226fb46 100644 --- a/chrome/installer/gcapi_mac/gcapi.mm +++ b/chrome/installer/gcapi_mac/gcapi.mm @@ -5,10 +5,34 @@ #include "chrome/installer/gcapi_mac/gcapi.h" #import <Cocoa/Cocoa.h> +#include <sys/stat.h> +#include <sys/types.h> #include <sys/utsname.h> namespace { +NSString* const kChromeInstallPath = @"/Applications/Google Chrome.app"; + +NSString* const kBrandKey = @"KSBrandID"; +NSString* const kSystemBrandPath = @"/Library/Google/Google Chrome Brand.plist"; +NSString* const kUserBrandPath = @"~/Library/Google/Google Chrome Brand.plist"; + +NSString* const kSystemKsadminPath = + @"/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/" + "Contents/MacOS/ksadmin"; + +NSString* const kUserKsadminPath = + @"~/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/" + "Contents/MacOS/ksadmin"; + +NSString* const kSystemMasterPrefsPath = + @"/Library/Google/Google Chrome Master Preferences"; +NSString* const kUserMasterPrefsPath = + @"~/Library/Application Support/Google/Google Chrome Master Preferences"; + +NSString* const kChannelKey = @"KSChannelID"; +NSString* const kVersionKey = @"KSVersion"; + // Condensed from chromium's base/mac/mac_util.mm. bool IsOSXLeopardOrLater() { // On 10.6, Gestalt() was observed to be able to spawn threads (see @@ -35,14 +59,6 @@ bool IsOSXLeopardOrLater() { return mac_os_x_minor_version >= 5; } -NSString* const kSystemKsadminPath = - @"/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/" - "Contents/MacOS/ksadmin"; - -NSString* const kUserKsadminPath = - @"~/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/" - "Contents/MacOS/ksadmin"; - enum TicketKind { kSystemTicket, kUserTicket }; @@ -50,9 +66,9 @@ enum TicketKind { BOOL HasChromeTicket(TicketKind kind) { // Don't use Objective-C 2 loop syntax, in case an installer runs on 10.4. NSMutableArray* keystonePaths = - [NSMutableArray arrayWithObject:kUserKsadminPath]; - if (kind == kSystemTicket) - [keystonePaths insertObject:kSystemKsadminPath atIndex:0]; + [NSMutableArray arrayWithObject:kSystemKsadminPath]; + if (kind == kUserTicket && geteuid() != 0) + [keystonePaths insertObject:kUserKsadminPath atIndex:0]; NSEnumerator* e = [keystonePaths objectEnumerator]; id ksPath; while ((ksPath = [e nextObject])) { @@ -98,6 +114,133 @@ BOOL HasChromeTicket(TicketKind kind) { return NO; } +// Returns the file permission mask for files created by gcapi. +mode_t Permissions() { + return 0755; +} + +BOOL CreatePathToFile(NSString* path) { + path = [path stringByDeletingLastPathComponent]; + + // Default owner, group, permissions: + // * Permissions are set according to the umask of the current process. For + // more information, see umask. + // * The owner ID is set to the effective user ID of the process. + // * The group ID is set to that of the parent directory. + // The default group ID is fine. Owner ID is fine too, since user directory + // paths won't be created if euid is 0. Do set permissions explicitly; for + // admin paths all admins can write, for user paths just the owner may. + NSMutableDictionary* attributes = [NSMutableDictionary + dictionaryWithObject:[NSNumber numberWithShort:Permissions()] + forKey:NSFilePosixPermissions]; + if (geteuid() == 0) + [attributes setObject:@"wheel" forKey:NSFileGroupOwnerAccountName]; + + NSFileManager* manager = [NSFileManager defaultManager]; + return [manager createDirectoryAtPath:path + withIntermediateDirectories:YES + attributes:attributes + error:nil]; +} + +// Tries to write |data| at |system_path| or if that fails and geteuid() is not +// 0 at |user_path|. Returns the path where it wrote, or nil on failure. +NSString* WriteData(NSData* data, NSString* system_path, NSString* user_path) { + // Try system first. + if (CreatePathToFile(system_path) && + [data writeToFile:system_path atomically:YES]) { + // files are created with group of parent dir (good), owner of euid (good). + chmod([system_path fileSystemRepresentation], Permissions() & ~0111); + return system_path; + } + + // Failed, try user. + // -stringByExpandingTildeInPath returns root's home directory if this is run + // setuid root, and in that case the kSystemBrandPath path above should have + // worked anyway. So only try user if geteuid() isn't root. + if (geteuid() != 0) { + NSString* user_path = [user_path stringByExpandingTildeInPath]; + if (CreatePathToFile(user_path) && + [data writeToFile:user_path atomically:YES]) { + chmod([user_path fileSystemRepresentation], Permissions() & ~0111); + return user_path; + } + } + return nil; +} + +NSString* WriteBrandCode(const char* brand_code) { + NSDictionary* brand_dict = @{ + kBrandKey: [NSString stringWithUTF8String:brand_code], + }; + NSData* contents = [NSPropertyListSerialization + dataFromPropertyList:brand_dict + format:NSPropertyListBinaryFormat_v1_0 + errorDescription:nil]; + + return WriteData(contents, kSystemBrandPath, kUserBrandPath); +} + +BOOL WriteMasterPrefs(const char* master_prefs_contents, + size_t master_prefs_contents_size) { + NSData* contents = [NSData dataWithBytes:master_prefs_contents + length:master_prefs_contents_size]; + return + WriteData(contents, kSystemMasterPrefsPath, kUserMasterPrefsPath) != nil; +} + +NSString* PathToFramework(NSString* app_path, NSDictionary* info_plist) { + NSString* version = [info_plist objectForKey:(NSString*)kCFBundleVersionKey]; + if (!version) + return nil; + return [[[app_path + stringByAppendingPathComponent:@"Contents/Versions"] + stringByAppendingPathComponent:version] + stringByAppendingPathComponent:@"Google Chrome Framework.framework"]; +} + +NSString* PathToInstallScript(NSString* app_path, NSDictionary* info_plist) { + return [PathToFramework(app_path, info_plist) stringByAppendingPathComponent: + @"Contents/Resources/install.sh"]; +} + +NSString* PathToKeystoneResources( + NSString* app_path, NSDictionary* info_plist) { + return [PathToFramework(app_path, info_plist) stringByAppendingPathComponent: + @"Frameworks/KeystoneRegistration.framework/Resources"]; +} + +NSString* FindOrInstallKeystone(NSString* app_path, NSDictionary* info_plist) { + NSString* ks_path = geteuid() == 0 ? + kSystemKsadminPath : [kUserKsadminPath stringByExpandingTildeInPath]; + + // Always run install.py. It won't overwrite an existing keystone, but + // it might update it or repair a broken existing installation. + NSString* ks_resources = PathToKeystoneResources(app_path, info_plist); + NSString* ks_install = + [ks_resources stringByAppendingPathComponent:@"install.py"]; + NSString* ks_tbz = + [ks_resources stringByAppendingPathComponent:@"Keystone.tbz"]; + @try { + NSTask* task = [[[NSTask alloc] init] autorelease]; + [task setLaunchPath:ks_install]; + [task setArguments:@[ks_tbz]]; + [task launch]; + [task waitUntilExit]; + if ([task terminationStatus] == 0) + return ks_path; + } + @catch (id exception) { + // Ignore. + } + return nil; +} + +bool isbrandchar(int c) { + // Always four upper-case alpha chars. + return c >= 'A' && c <= 'Z'; +} + } // namespace int GoogleChromeCompatibilityCheck(unsigned* reasons) { @@ -114,6 +257,9 @@ int GoogleChromeCompatibilityCheck(unsigned* reasons) { if (HasChromeTicket(kUserTicket)) local_reasons |= GCCC_ERROR_USERLEVELALREADYPRESENT; + if (![[NSFileManager defaultManager] isWritableFileAtPath:@"/Applications"]) + local_reasons |= GCCC_ERROR_ACCESSDENIED; + [pool drain]; // Done. Copy/return results. @@ -123,9 +269,97 @@ int GoogleChromeCompatibilityCheck(unsigned* reasons) { return local_reasons == 0; } +int InstallGoogleChrome(const char* source_path, + const char* brand_code, + const char* master_prefs_contents, + unsigned master_prefs_contents_size) { + if (!GoogleChromeCompatibilityCheck(NULL)) + return 0; + + @autoreleasepool { + NSString* app_path = [NSString stringWithUTF8String:source_path]; + NSString* info_plist_path = + [app_path stringByAppendingPathComponent:@"Contents/Info.plist"]; + NSDictionary* info_plist = + [NSDictionary dictionaryWithContentsOfFile:info_plist_path]; + + // Use install.sh from the Chrome app bundle to copy Chrome to its + // destination. + NSString* install_script = PathToInstallScript(app_path, info_plist); + if (!install_script) + return 0; + + @try { + NSTask* task = [[[NSTask alloc] init] autorelease]; + [task setLaunchPath:install_script]; + [task setArguments:@[app_path, kChromeInstallPath]]; + [task launch]; + [task waitUntilExit]; + if ([task terminationStatus] != 0) + return 0; + } + @catch (id exception) { + return 0; + } + + // Set brand code. If Chrome's Info.plist contains a brand code, use that. + NSString* info_plist_brand = [info_plist objectForKey:kBrandKey]; + if (info_plist_brand && + [info_plist_brand respondsToSelector:@selector(UTF8String)]) + brand_code = [info_plist_brand UTF8String]; + + BOOL valid_brand_code = strlen(brand_code) == 4 && + isbrandchar(brand_code[0]) && isbrandchar(brand_code[1]) && + isbrandchar(brand_code[2]) && isbrandchar(brand_code[3]); + + NSString* brand_path = nil; + if (brand_code && valid_brand_code) + brand_path = WriteBrandCode(brand_code); + + // Write master prefs. + if (master_prefs_contents) + WriteMasterPrefs(master_prefs_contents, master_prefs_contents_size); + + // Install Keystone if necessary. + if (NSString* keystone = FindOrInstallKeystone(app_path, info_plist)) { + // Register Chrome with Keystone. + @try { + NSTask* task = [[[NSTask alloc] init] autorelease]; + [task setLaunchPath:keystone]; + NSString* tag = [info_plist objectForKey:kChannelKey]; + [task setArguments:@[ + @"--register", + @"--productid", [info_plist objectForKey:@"KSProductID"], + @"--version", [info_plist objectForKey:kVersionKey], + @"--xcpath", kChromeInstallPath, + @"--url", [info_plist objectForKey:@"KSUpdateURL"], + + @"--tag", tag ? tag : @"", // Stable channel + @"--tag-path", info_plist_path, + @"--tag-key", kChannelKey, + + @"--brand-path", brand_path ? brand_path : @"", + @"--brand-key", brand_path ? kBrandKey: @"", + + @"--version-path", info_plist_path, + @"--version-key", kVersionKey, + ]]; + [task launch]; + [task waitUntilExit]; + } + @catch (id exception) { + // Chrome will try to register keystone on launch. + } + } + + // TODO Set default browser if requested. Will be tricky when running as + // root. + } + return 1; +} + int LaunchGoogleChrome() { @autoreleasepool { - return [[NSWorkspace sharedWorkspace] - launchApplication:@"/Applications/Google Chrome.app"]; + return [[NSWorkspace sharedWorkspace] launchApplication:kChromeInstallPath]; } } |