| // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/cocoa/keystone_glue.h" |
| |
| #include <sys/param.h> |
| #include <sys/mount.h> |
| |
| #include <vector> |
| |
| #include "base/logging.h" |
| #include "base/mac/mac_util.h" |
| #include "base/mac/scoped_nsautorelease_pool.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/sys_string_conversions.h" |
| #include "base/task.h" |
| #include "base/threading/worker_pool.h" |
| #include "chrome/browser/cocoa/authorization_util.h" |
| #include "chrome/common/chrome_constants.h" |
| #include "grit/chromium_strings.h" |
| #include "grit/generated_resources.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| |
| namespace { |
| |
| // Provide declarations of the Keystone registration bits needed here. From |
| // KSRegistration.h. |
| typedef enum { |
| kKSPathExistenceChecker, |
| } KSExistenceCheckerType; |
| |
| typedef enum { |
| kKSRegistrationUserTicket, |
| kKSRegistrationSystemTicket, |
| kKSRegistrationDontKnowWhatKindOfTicket, |
| } KSRegistrationTicketType; |
| |
| NSString* const KSRegistrationVersionKey = @"Version"; |
| NSString* const KSRegistrationExistenceCheckerTypeKey = @"ExistenceCheckerType"; |
| NSString* const KSRegistrationExistenceCheckerStringKey = |
| @"ExistenceCheckerString"; |
| NSString* const KSRegistrationServerURLStringKey = @"URLString"; |
| NSString* const KSRegistrationPreserveTrustedTesterTokenKey = @"PreserveTTT"; |
| NSString* const KSRegistrationTagKey = @"Tag"; |
| NSString* const KSRegistrationTagPathKey = @"TagPath"; |
| NSString* const KSRegistrationTagKeyKey = @"TagKey"; |
| NSString* const KSRegistrationBrandPathKey = @"BrandPath"; |
| NSString* const KSRegistrationBrandKeyKey = @"BrandKey"; |
| |
| NSString* const KSRegistrationDidCompleteNotification = |
| @"KSRegistrationDidCompleteNotification"; |
| NSString* const KSRegistrationPromotionDidCompleteNotification = |
| @"KSRegistrationPromotionDidCompleteNotification"; |
| |
| NSString* const KSRegistrationCheckForUpdateNotification = |
| @"KSRegistrationCheckForUpdateNotification"; |
| NSString* KSRegistrationStatusKey = @"Status"; |
| NSString* KSRegistrationUpdateCheckErrorKey = @"Error"; |
| |
| NSString* const KSRegistrationStartUpdateNotification = |
| @"KSRegistrationStartUpdateNotification"; |
| NSString* const KSUpdateCheckSuccessfulKey = @"CheckSuccessful"; |
| NSString* const KSUpdateCheckSuccessfullyInstalledKey = |
| @"SuccessfullyInstalled"; |
| |
| NSString* const KSRegistrationRemoveExistingTag = @""; |
| #define KSRegistrationPreserveExistingTag nil |
| |
| // Constants for the brand file (uses an external file so it can survive updates |
| // to Chrome. |
| |
| #if defined(GOOGLE_CHROME_BUILD) |
| #define kBrandFileName @"Google Chrome Brand.plist"; |
| #elif defined(CHROMIUM_BUILD) |
| #define kBrandFileName @"Chromium Brand.plist"; |
| #else |
| #error Unknown branding |
| #endif |
| |
| // These directories are hardcoded in Keystone promotion preflight and the |
| // Keystone install script, so NSSearchPathForDirectoriesInDomains isn't used |
| // since the scripts couldn't use anything like that. |
| NSString* kBrandUserFile = @"~/Library/Google/" kBrandFileName; |
| NSString* kBrandSystemFile = @"/Library/Google/" kBrandFileName; |
| |
| NSString* UserBrandFilePath() { |
| return [kBrandUserFile stringByStandardizingPath]; |
| } |
| NSString* SystemBrandFilePath() { |
| return [kBrandSystemFile stringByStandardizingPath]; |
| } |
| |
| // Adaptor for scheduling an Objective-C method call on a |WorkerPool| |
| // thread. |
| class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> { |
| public: |
| |
| // Call |sel| on |target| with |arg| in a WorkerPool thread. |
| // |target| and |arg| are retained, |arg| may be |nil|. |
| static void PostPerform(id target, SEL sel, id arg) { |
| DCHECK(target); |
| DCHECK(sel); |
| |
| scoped_refptr<PerformBridge> op = new PerformBridge(target, sel, arg); |
| base::WorkerPool::PostTask( |
| FROM_HERE, NewRunnableMethod(op.get(), &PerformBridge::Run), true); |
| } |
| |
| // Convenience for the no-argument case. |
| static void PostPerform(id target, SEL sel) { |
| PostPerform(target, sel, nil); |
| } |
| |
| private: |
| // Allow RefCountedThreadSafe<> to delete. |
| friend class base::RefCountedThreadSafe<PerformBridge>; |
| |
| PerformBridge(id target, SEL sel, id arg) |
| : target_([target retain]), |
| sel_(sel), |
| arg_([arg retain]) { |
| } |
| |
| ~PerformBridge() {} |
| |
| // Happens on a WorkerPool thread. |
| void Run() { |
| base::mac::ScopedNSAutoreleasePool pool; |
| [target_ performSelector:sel_ withObject:arg_]; |
| } |
| |
| scoped_nsobject<id> target_; |
| SEL sel_; |
| scoped_nsobject<id> arg_; |
| }; |
| |
| } // namespace |
| |
| @interface KSRegistration : NSObject |
| |
| + (id)registrationWithProductID:(NSString*)productID; |
| |
| - (BOOL)registerWithParameters:(NSDictionary*)args; |
| |
| - (BOOL)promoteWithParameters:(NSDictionary*)args |
| authorization:(AuthorizationRef)authorization; |
| |
| - (void)setActive; |
| - (void)checkForUpdate; |
| - (void)startUpdate; |
| - (KSRegistrationTicketType)ticketType; |
| |
| @end // @interface KSRegistration |
| |
| @interface KeystoneGlue(Private) |
| |
| // Returns the path to the application's Info.plist file. This returns the |
| // outer application bundle's Info.plist, not the framework's Info.plist. |
| - (NSString*)appInfoPlistPath; |
| |
| // Returns a dictionary containing parameters to be used for a KSRegistration |
| // -registerWithParameters: or -promoteWithParameters:authorization: call. |
| - (NSDictionary*)keystoneParameters; |
| |
| // Called when Keystone registration completes. |
| - (void)registrationComplete:(NSNotification*)notification; |
| |
| // Called periodically to announce activity by pinging the Keystone server. |
| - (void)markActive:(NSTimer*)timer; |
| |
| // Called when an update check or update installation is complete. Posts the |
| // kAutoupdateStatusNotification notification to the default notification |
| // center. |
| - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version; |
| |
| // Returns the version of the currently-installed application on disk. |
| - (NSString*)currentlyInstalledVersion; |
| |
| // These three methods are used to determine the version of the application |
| // currently installed on disk, compare that to the currently-running version, |
| // decide whether any updates have been installed, and call |
| // -updateStatus:version:. |
| // |
| // In order to check the version on disk, the installed application's |
| // Info.plist dictionary must be read; in order to see changes as updates are |
| // applied, the dictionary must be read each time, bypassing any caches such |
| // as the one that NSBundle might be maintaining. Reading files can be a |
| // blocking operation, and blocking operations are to be avoided on the main |
| // thread. I'm not quite sure what jank means, but I bet that a blocked main |
| // thread would cause some of it. |
| // |
| // -determineUpdateStatusAsync is called on the main thread to initiate the |
| // operation. It performs initial set-up work that must be done on the main |
| // thread and arranges for -determineUpdateStatus to be called on a work queue |
| // thread managed by WorkerPool. |
| // -determineUpdateStatus then reads the Info.plist, gets the version from the |
| // CFBundleShortVersionString key, and performs |
| // -determineUpdateStatusForVersion: on the main thread. |
| // -determineUpdateStatusForVersion: does the actual comparison of the version |
| // on disk with the running version and calls -updateStatus:version: with the |
| // results of its analysis. |
| - (void)determineUpdateStatusAsync; |
| - (void)determineUpdateStatus; |
| - (void)determineUpdateStatusForVersion:(NSString*)version; |
| |
| // Returns YES if registration_ is definitely on a user ticket. If definitely |
| // on a system ticket, or uncertain of ticket type (due to an older version |
| // of Keystone being used), returns NO. |
| - (BOOL)isUserTicket; |
| |
| // Called when ticket promotion completes. |
| - (void)promotionComplete:(NSNotification*)notification; |
| |
| // Changes the application's ownership and permissions so that all files are |
| // owned by root:wheel and all files and directories are writable only by |
| // root, but readable and executable as needed by everyone. |
| // -changePermissionsForPromotionAsync is called on the main thread by |
| // -promotionComplete. That routine calls |
| // -changePermissionsForPromotionWithTool: on a work queue thread. When done, |
| // -changePermissionsForPromotionComplete is called on the main thread. |
| - (void)changePermissionsForPromotionAsync; |
| - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath; |
| - (void)changePermissionsForPromotionComplete; |
| |
| // Returns the brand file path to use for Keystone. |
| - (NSString*)brandFilePath; |
| |
| @end // @interface KeystoneGlue(Private) |
| |
| NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification"; |
| NSString* const kAutoupdateStatusStatus = @"status"; |
| NSString* const kAutoupdateStatusVersion = @"version"; |
| |
| namespace { |
| |
| NSString* const kChannelKey = @"KSChannelID"; |
| NSString* const kBrandKey = @"KSBrandID"; |
| |
| } // namespace |
| |
| @implementation KeystoneGlue |
| |
| + (id)defaultKeystoneGlue { |
| static bool sTriedCreatingDefaultKeystoneGlue = false; |
| // TODO(jrg): use base::SingletonObjC<KeystoneGlue> |
| static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked |
| |
| if (!sTriedCreatingDefaultKeystoneGlue) { |
| sTriedCreatingDefaultKeystoneGlue = true; |
| |
| sDefaultKeystoneGlue = [[KeystoneGlue alloc] init]; |
| [sDefaultKeystoneGlue loadParameters]; |
| if (![sDefaultKeystoneGlue loadKeystoneRegistration]) { |
| [sDefaultKeystoneGlue release]; |
| sDefaultKeystoneGlue = nil; |
| } |
| } |
| return sDefaultKeystoneGlue; |
| } |
| |
| - (id)init { |
| if ((self = [super init])) { |
| NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
| |
| [center addObserver:self |
| selector:@selector(registrationComplete:) |
| name:KSRegistrationDidCompleteNotification |
| object:nil]; |
| |
| [center addObserver:self |
| selector:@selector(promotionComplete:) |
| name:KSRegistrationPromotionDidCompleteNotification |
| object:nil]; |
| |
| [center addObserver:self |
| selector:@selector(checkForUpdateComplete:) |
| name:KSRegistrationCheckForUpdateNotification |
| object:nil]; |
| |
| [center addObserver:self |
| selector:@selector(installUpdateComplete:) |
| name:KSRegistrationStartUpdateNotification |
| object:nil]; |
| } |
| |
| return self; |
| } |
| |
| - (void)dealloc { |
| [productID_ release]; |
| [appPath_ release]; |
| [url_ release]; |
| [version_ release]; |
| [channel_ release]; |
| [registration_ release]; |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| [super dealloc]; |
| } |
| |
| - (NSDictionary*)infoDictionary { |
| // Use [NSBundle mainBundle] to get the application's own bundle identifier |
| // and path, not the framework's. For auto-update, the application is |
| // what's significant here: it's used to locate the outermost part of the |
| // application for the existence checker and other operations that need to |
| // see the entire application bundle. |
| return [[NSBundle mainBundle] infoDictionary]; |
| } |
| |
| - (void)loadParameters { |
| NSBundle* appBundle = [NSBundle mainBundle]; |
| NSDictionary* infoDictionary = [self infoDictionary]; |
| |
| NSString* productID = [infoDictionary objectForKey:@"KSProductID"]; |
| if (productID == nil) { |
| productID = [appBundle bundleIdentifier]; |
| } |
| |
| NSString* appPath = [appBundle bundlePath]; |
| NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"]; |
| NSString* version = [infoDictionary objectForKey:@"KSVersion"]; |
| |
| if (!productID || !appPath || !url || !version) { |
| // If parameters required for Keystone are missing, don't use it. |
| return; |
| } |
| |
| NSString* channel = [infoDictionary objectForKey:kChannelKey]; |
| // The stable channel has no tag. If updating to stable, remove the |
| // dev and beta tags since we've been "promoted". |
| if (channel == nil) |
| channel = KSRegistrationRemoveExistingTag; |
| |
| productID_ = [productID retain]; |
| appPath_ = [appPath retain]; |
| url_ = [url retain]; |
| version_ = [version retain]; |
| channel_ = [channel retain]; |
| } |
| |
| - (NSString*)brandFilePath { |
| DCHECK(version_ != nil) << "-loadParameters must be called first"; |
| |
| if (brandFileType_ == kBrandFileTypeNotDetermined) { |
| |
| // Default to none. |
| brandFileType_ = kBrandFileTypeNone; |
| |
| // Having a channel means Dev/Beta, so there is no brand code to go with |
| // those. |
| if ([channel_ length] == 0) { |
| |
| NSString* userBrandFile = UserBrandFilePath(); |
| NSString* systemBrandFile = SystemBrandFilePath(); |
| |
| NSFileManager* fm = [NSFileManager defaultManager]; |
| |
| // If there is a system brand file, use it. |
| if ([fm fileExistsAtPath:systemBrandFile]) { |
| // System |
| |
| // Use the system file that is there. |
| brandFileType_ = kBrandFileTypeSystem; |
| |
| // Clean up any old user level file. |
| if ([fm fileExistsAtPath:userBrandFile]) { |
| [fm removeItemAtPath:userBrandFile error:NULL]; |
| } |
| |
| } else { |
| // User |
| |
| NSDictionary* infoDictionary = [self infoDictionary]; |
| NSString* appBundleBrandID = [infoDictionary objectForKey:kBrandKey]; |
| |
| NSString* storedBrandID = nil; |
| if ([fm fileExistsAtPath:userBrandFile]) { |
| NSDictionary* storedBrandDict = |
| [NSDictionary dictionaryWithContentsOfFile:userBrandFile]; |
| storedBrandID = [storedBrandDict objectForKey:kBrandKey]; |
| } |
| |
| if ((appBundleBrandID != nil) && |
| (![storedBrandID isEqualTo:appBundleBrandID])) { |
| // App and store don't match, update store and use it. |
| NSDictionary* storedBrandDict = |
| [NSDictionary dictionaryWithObject:appBundleBrandID |
| forKey:kBrandKey]; |
| // If Keystone hasn't been installed yet, the location the brand file |
| // is written to won't exist, so manually create the directory. |
| NSString *userBrandFileDirectory = |
| [userBrandFile stringByDeletingLastPathComponent]; |
| if (![fm fileExistsAtPath:userBrandFileDirectory]) { |
| if (![fm createDirectoryAtPath:userBrandFileDirectory |
| withIntermediateDirectories:YES |
| attributes:nil |
| error:NULL]) { |
| LOG(ERROR) << "Failed to create the directory for the brand file"; |
| } |
| } |
| if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) { |
| brandFileType_ = kBrandFileTypeUser; |
| } |
| } else if (storedBrandID) { |
| // Had stored brand, use it. |
| brandFileType_ = kBrandFileTypeUser; |
| } |
| } |
| } |
| |
| } |
| |
| NSString* result = nil; |
| switch (brandFileType_) { |
| case kBrandFileTypeUser: |
| result = UserBrandFilePath(); |
| break; |
| |
| case kBrandFileTypeSystem: |
| result = SystemBrandFilePath(); |
| break; |
| |
| case kBrandFileTypeNotDetermined: |
| NOTIMPLEMENTED(); |
| // Fall through |
| case kBrandFileTypeNone: |
| // Clear the value. |
| result = @""; |
| break; |
| |
| } |
| return result; |
| } |
| |
| - (BOOL)loadKeystoneRegistration { |
| if (!productID_ || !appPath_ || !url_ || !version_) |
| return NO; |
| |
| // Load the KeystoneRegistration framework bundle if present. It lives |
| // inside the framework, so use base::mac::MainAppBundle(); |
| NSString* ksrPath = |
| [[base::mac::MainAppBundle() privateFrameworksPath] |
| stringByAppendingPathComponent:@"KeystoneRegistration.framework"]; |
| NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath]; |
| [ksrBundle load]; |
| |
| // Harness the KSRegistration class. |
| Class ksrClass = [ksrBundle classNamed:@"KSRegistration"]; |
| KSRegistration* ksr = [ksrClass registrationWithProductID:productID_]; |
| if (!ksr) |
| return NO; |
| |
| registration_ = [ksr retain]; |
| return YES; |
| } |
| |
| - (NSString*)appInfoPlistPath { |
| // NSBundle ought to have a way to access this path directly, but it |
| // doesn't. |
| return [[appPath_ stringByAppendingPathComponent:@"Contents"] |
| stringByAppendingPathComponent:@"Info.plist"]; |
| } |
| |
| - (NSDictionary*)keystoneParameters { |
| NSNumber* xcType = [NSNumber numberWithInt:kKSPathExistenceChecker]; |
| NSNumber* preserveTTToken = [NSNumber numberWithBool:YES]; |
| NSString* tagPath = [self appInfoPlistPath]; |
| |
| NSString* brandKey = kBrandKey; |
| NSString* brandPath = [self brandFilePath]; |
| |
| if ([brandPath length] == 0) { |
| // Brand path and brand key must be cleared together or ksadmin seems |
| // to throw an error. |
| brandKey = @""; |
| } |
| |
| return [NSDictionary dictionaryWithObjectsAndKeys: |
| version_, KSRegistrationVersionKey, |
| xcType, KSRegistrationExistenceCheckerTypeKey, |
| appPath_, KSRegistrationExistenceCheckerStringKey, |
| url_, KSRegistrationServerURLStringKey, |
| preserveTTToken, KSRegistrationPreserveTrustedTesterTokenKey, |
| channel_, KSRegistrationTagKey, |
| tagPath, KSRegistrationTagPathKey, |
| kChannelKey, KSRegistrationTagKeyKey, |
| brandPath, KSRegistrationBrandPathKey, |
| brandKey, KSRegistrationBrandKeyKey, |
| nil]; |
| } |
| |
| - (void)registerWithKeystone { |
| [self updateStatus:kAutoupdateRegistering version:nil]; |
| |
| NSDictionary* parameters = [self keystoneParameters]; |
| if (![registration_ registerWithParameters:parameters]) { |
| [self updateStatus:kAutoupdateRegisterFailed version:nil]; |
| return; |
| } |
| |
| // Upon completion, KSRegistrationDidCompleteNotification will be posted, |
| // and -registrationComplete: will be called. |
| |
| // Mark an active RIGHT NOW; don't wait an hour for the first one. |
| [registration_ setActive]; |
| |
| // Set up hourly activity pings. |
| timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60 // One hour |
| target:self |
| selector:@selector(markActive:) |
| userInfo:registration_ |
| repeats:YES]; |
| } |
| |
| - (void)registrationComplete:(NSNotification*)notification { |
| NSDictionary* userInfo = [notification userInfo]; |
| if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) { |
| [self updateStatus:kAutoupdateRegistered version:nil]; |
| } else { |
| // Dump registration_? |
| [self updateStatus:kAutoupdateRegisterFailed version:nil]; |
| } |
| } |
| |
| - (void)stopTimer { |
| [timer_ invalidate]; |
| } |
| |
| - (void)markActive:(NSTimer*)timer { |
| KSRegistration* ksr = [timer userInfo]; |
| [ksr setActive]; |
| } |
| |
| - (void)checkForUpdate { |
| DCHECK(![self asyncOperationPending]); |
| |
| if (!registration_) { |
| [self updateStatus:kAutoupdateCheckFailed version:nil]; |
| return; |
| } |
| |
| [self updateStatus:kAutoupdateChecking version:nil]; |
| |
| [registration_ checkForUpdate]; |
| |
| // Upon completion, KSRegistrationCheckForUpdateNotification will be posted, |
| // and -checkForUpdateComplete: will be called. |
| } |
| |
| - (void)checkForUpdateComplete:(NSNotification*)notification { |
| NSDictionary* userInfo = [notification userInfo]; |
| |
| if ([[userInfo objectForKey:KSRegistrationUpdateCheckErrorKey] boolValue]) { |
| [self updateStatus:kAutoupdateCheckFailed version:nil]; |
| } else if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) { |
| // If an update is known to be available, go straight to |
| // -updateStatus:version:. It doesn't matter what's currently on disk. |
| NSString* version = [userInfo objectForKey:KSRegistrationVersionKey]; |
| [self updateStatus:kAutoupdateAvailable version:version]; |
| } else { |
| // If no updates are available, check what's on disk, because an update |
| // may have already been installed. This check happens on another thread, |
| // and -updateStatus:version: will be called on the main thread when done. |
| [self determineUpdateStatusAsync]; |
| } |
| } |
| |
| - (void)installUpdate { |
| DCHECK(![self asyncOperationPending]); |
| |
| if (!registration_) { |
| [self updateStatus:kAutoupdateInstallFailed version:nil]; |
| return; |
| } |
| |
| [self updateStatus:kAutoupdateInstalling version:nil]; |
| |
| [registration_ startUpdate]; |
| |
| // Upon completion, KSRegistrationStartUpdateNotification will be posted, |
| // and -installUpdateComplete: will be called. |
| } |
| |
| - (void)installUpdateComplete:(NSNotification*)notification { |
| NSDictionary* userInfo = [notification userInfo]; |
| |
| if (![[userInfo objectForKey:KSUpdateCheckSuccessfulKey] boolValue] || |
| ![[userInfo objectForKey:KSUpdateCheckSuccessfullyInstalledKey] |
| intValue]) { |
| [self updateStatus:kAutoupdateInstallFailed version:nil]; |
| } else { |
| updateSuccessfullyInstalled_ = YES; |
| |
| // Nothing in the notification dictionary reports the version that was |
| // installed. Figure it out based on what's on disk. |
| [self determineUpdateStatusAsync]; |
| } |
| } |
| |
| - (NSString*)currentlyInstalledVersion { |
| NSString* appInfoPlistPath = [self appInfoPlistPath]; |
| NSDictionary* infoPlist = |
| [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath]; |
| return [infoPlist objectForKey:@"CFBundleShortVersionString"]; |
| } |
| |
| // Runs on the main thread. |
| - (void)determineUpdateStatusAsync { |
| DCHECK([NSThread isMainThread]); |
| |
| PerformBridge::PostPerform(self, @selector(determineUpdateStatus)); |
| } |
| |
| // Runs on a thread managed by WorkerPool. |
| - (void)determineUpdateStatus { |
| DCHECK(![NSThread isMainThread]); |
| |
| NSString* version = [self currentlyInstalledVersion]; |
| |
| [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:) |
| withObject:version |
| waitUntilDone:NO]; |
| } |
| |
| // Runs on the main thread. |
| - (void)determineUpdateStatusForVersion:(NSString*)version { |
| DCHECK([NSThread isMainThread]); |
| |
| AutoupdateStatus status; |
| if (updateSuccessfullyInstalled_) { |
| // If an update was successfully installed and this object saw it happen, |
| // then don't even bother comparing versions. |
| status = kAutoupdateInstalled; |
| } else { |
| NSString* currentVersion = |
| [NSString stringWithUTF8String:chrome::kChromeVersion]; |
| if (!version) { |
| // If the version on disk could not be determined, assume that |
| // whatever's running is current. |
| version = currentVersion; |
| status = kAutoupdateCurrent; |
| } else if ([version isEqualToString:currentVersion]) { |
| status = kAutoupdateCurrent; |
| } else { |
| // If the version on disk doesn't match what's currently running, an |
| // update must have been applied in the background, without this app's |
| // direct participation. Leave updateSuccessfullyInstalled_ alone |
| // because there's no direct knowledge of what actually happened. |
| status = kAutoupdateInstalled; |
| } |
| } |
| |
| [self updateStatus:status version:version]; |
| } |
| |
| - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version { |
| NSNumber* statusNumber = [NSNumber numberWithInt:status]; |
| NSMutableDictionary* dictionary = |
| [NSMutableDictionary dictionaryWithObject:statusNumber |
| forKey:kAutoupdateStatusStatus]; |
| if (version) { |
| [dictionary setObject:version forKey:kAutoupdateStatusVersion]; |
| } |
| |
| NSNotification* notification = |
| [NSNotification notificationWithName:kAutoupdateStatusNotification |
| object:self |
| userInfo:dictionary]; |
| recentNotification_.reset([notification retain]); |
| |
| [[NSNotificationCenter defaultCenter] postNotification:notification]; |
| } |
| |
| - (NSNotification*)recentNotification { |
| return [[recentNotification_ retain] autorelease]; |
| } |
| |
| - (AutoupdateStatus)recentStatus { |
| NSDictionary* dictionary = [recentNotification_ userInfo]; |
| return static_cast<AutoupdateStatus>( |
| [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); |
| } |
| |
| - (BOOL)asyncOperationPending { |
| AutoupdateStatus status = [self recentStatus]; |
| return status == kAutoupdateRegistering || |
| status == kAutoupdateChecking || |
| status == kAutoupdateInstalling || |
| status == kAutoupdatePromoting; |
| } |
| |
| - (BOOL)isUserTicket { |
| return [registration_ ticketType] == kKSRegistrationUserTicket; |
| } |
| |
| - (BOOL)isOnReadOnlyFilesystem { |
| const char* appPathC = [appPath_ fileSystemRepresentation]; |
| struct statfs statfsBuf; |
| |
| if (statfs(appPathC, &statfsBuf) != 0) { |
| PLOG(ERROR) << "statfs"; |
| // Be optimistic about the filesystem's writability. |
| return NO; |
| } |
| |
| return (statfsBuf.f_flags & MNT_RDONLY) != 0; |
| } |
| |
| - (BOOL)needsPromotion { |
| if (![self isUserTicket] || [self isOnReadOnlyFilesystem]) { |
| return NO; |
| } |
| |
| // Check the outermost bundle directory, the main executable path, and the |
| // framework directory. It may be enough to just look at the outermost |
| // bundle directory, but checking an interior file and directory can be |
| // helpful in case permissions are set differently only on the outermost |
| // directory. An interior file and directory are both checked because some |
| // file operations, such as Snow Leopard's Finder's copy operation when |
| // authenticating, may actually result in different ownership being applied |
| // to files and directories. |
| NSFileManager* fileManager = [NSFileManager defaultManager]; |
| NSString* executablePath = [[NSBundle mainBundle] executablePath]; |
| NSString* frameworkPath = [base::mac::MainAppBundle() bundlePath]; |
| return ![fileManager isWritableFileAtPath:appPath_] || |
| ![fileManager isWritableFileAtPath:executablePath] || |
| ![fileManager isWritableFileAtPath:frameworkPath]; |
| } |
| |
| - (BOOL)wantsPromotion { |
| // -needsPromotion checks these too, but this method doesn't necessarily |
| // return NO just becuase -needsPromotion returns NO, so another check is |
| // needed here. |
| if (![self isUserTicket] || [self isOnReadOnlyFilesystem]) { |
| return NO; |
| } |
| |
| if ([self needsPromotion]) { |
| return YES; |
| } |
| |
| return [appPath_ hasPrefix:@"/Applications/"]; |
| } |
| |
| - (void)promoteTicket { |
| if ([self asyncOperationPending] || ![self wantsPromotion]) { |
| // Because there are multiple ways of reaching promoteTicket that might |
| // not lock each other out, it may be possible to arrive here while an |
| // asynchronous operation is pending, or even after promotion has already |
| // occurred. Just quietly return without doing anything. |
| return; |
| } |
| |
| NSString* prompt = l10n_util::GetNSStringFWithFixup( |
| IDS_PROMOTE_AUTHENTICATION_PROMPT, |
| l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); |
| scoped_AuthorizationRef authorization( |
| authorization_util::AuthorizationCreateToRunAsRoot( |
| base::mac::NSToCFCast(prompt))); |
| if (!authorization.get()) { |
| return; |
| } |
| |
| [self promoteTicketWithAuthorization:authorization.release() synchronous:NO]; |
| } |
| |
| - (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg |
| synchronous:(BOOL)synchronous { |
| scoped_AuthorizationRef authorization(authorization_arg); |
| authorization_arg = NULL; |
| |
| if ([self asyncOperationPending]) { |
| // Starting a synchronous operation while an asynchronous one is pending |
| // could be trouble. |
| return; |
| } |
| if (!synchronous && ![self wantsPromotion]) { |
| // If operating synchronously, the call came from the installer, which |
| // means that a system ticket is required. Otherwise, only allow |
| // promotion if it's wanted. |
| return; |
| } |
| |
| synchronousPromotion_ = synchronous; |
| |
| [self updateStatus:kAutoupdatePromoting version:nil]; |
| |
| // TODO(mark): Remove when able! |
| // |
| // keystone_promote_preflight will copy the current brand information out to |
| // the system level so all users can share the data as part of the ticket |
| // promotion. |
| // |
| // It will also ensure that the Keystone system ticket store is in a usable |
| // state for all users on the system. Ideally, Keystone's installer or |
| // another part of Keystone would handle this. The underlying problem is |
| // http://b/2285921, and it causes http://b/2289908, which this workaround |
| // addresses. |
| // |
| // This is run synchronously, which isn't optimal, but |
| // -[KSRegistration promoteWithParameters:authorization:] is currently |
| // synchronous too, and this operation needs to happen before that one. |
| // |
| // TODO(mark): Make asynchronous. That only makes sense if the promotion |
| // operation itself is asynchronous too. http://b/2290009. Hopefully, |
| // the Keystone promotion code will just be changed to do what preflight |
| // now does, and then the preflight script can be removed instead. |
| // However, preflight operation (and promotion) should only be asynchronous |
| // if the synchronous parameter is NO. |
| NSString* preflightPath = |
| [base::mac::MainAppBundle() pathForResource:@"keystone_promote_preflight" |
| ofType:@"sh"]; |
| const char* preflightPathC = [preflightPath fileSystemRepresentation]; |
| const char* userBrandFile = NULL; |
| const char* systemBrandFile = NULL; |
| if (brandFileType_ == kBrandFileTypeUser) { |
| // Running with user level brand file, promote to the system level. |
| userBrandFile = [UserBrandFilePath() fileSystemRepresentation]; |
| systemBrandFile = [SystemBrandFilePath() fileSystemRepresentation]; |
| } |
| const char* arguments[] = {userBrandFile, systemBrandFile, NULL}; |
| |
| int exit_status; |
| OSStatus status = authorization_util::ExecuteWithPrivilegesAndWait( |
| authorization, |
| preflightPathC, |
| kAuthorizationFlagDefaults, |
| arguments, |
| NULL, // pipe |
| &exit_status); |
| if (status != errAuthorizationSuccess) { |
| LOG(ERROR) << "AuthorizationExecuteWithPrivileges preflight: " << status; |
| [self updateStatus:kAutoupdatePromoteFailed version:nil]; |
| return; |
| } |
| if (exit_status != 0) { |
| LOG(ERROR) << "keystone_promote_preflight status " << exit_status; |
| [self updateStatus:kAutoupdatePromoteFailed version:nil]; |
| return; |
| } |
| |
| // Hang on to the AuthorizationRef so that it can be used once promotion is |
| // complete. Do this before asking Keystone to promote the ticket, because |
| // -promotionComplete: may be called from inside the Keystone promotion |
| // call. |
| authorization_.swap(authorization); |
| |
| NSDictionary* parameters = [self keystoneParameters]; |
| |
| // If the brand file is user level, update parameters to point to the new |
| // system level file during promotion. |
| if (brandFileType_ == kBrandFileTypeUser) { |
| NSMutableDictionary* temp_parameters = |
| [[parameters mutableCopy] autorelease]; |
| [temp_parameters setObject:SystemBrandFilePath() |
| forKey:KSRegistrationBrandPathKey]; |
| parameters = temp_parameters; |
| } |
| |
| if (![registration_ promoteWithParameters:parameters |
| authorization:authorization_]) { |
| [self updateStatus:kAutoupdatePromoteFailed version:nil]; |
| authorization_.reset(); |
| return; |
| } |
| |
| // Upon completion, KSRegistrationPromotionDidCompleteNotification will be |
| // posted, and -promotionComplete: will be called. |
| } |
| |
| - (void)promotionComplete:(NSNotification*)notification { |
| NSDictionary* userInfo = [notification userInfo]; |
| if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) { |
| if (synchronousPromotion_) { |
| // Short-circuit: if performing a synchronous promotion, the promotion |
| // came from the installer, which already set the permissions properly. |
| // Rather than run a duplicate permission-changing operation, jump |
| // straight to "done." |
| [self changePermissionsForPromotionComplete]; |
| } else { |
| [self changePermissionsForPromotionAsync]; |
| } |
| } else { |
| authorization_.reset(); |
| [self updateStatus:kAutoupdatePromoteFailed version:nil]; |
| } |
| } |
| |
| - (void)changePermissionsForPromotionAsync { |
| // NSBundle is not documented as being thread-safe. Do NSBundle operations |
| // on the main thread before jumping over to a WorkerPool-managed |
| // thread to run the tool. |
| DCHECK([NSThread isMainThread]); |
| |
| SEL selector = @selector(changePermissionsForPromotionWithTool:); |
| NSString* toolPath = |
| [base::mac::MainAppBundle() pathForResource:@"keystone_promote_postflight" |
| ofType:@"sh"]; |
| |
| PerformBridge::PostPerform(self, selector, toolPath); |
| } |
| |
| - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath { |
| const char* toolPathC = [toolPath fileSystemRepresentation]; |
| |
| const char* appPathC = [appPath_ fileSystemRepresentation]; |
| const char* arguments[] = {appPathC, NULL}; |
| |
| int exit_status; |
| OSStatus status = authorization_util::ExecuteWithPrivilegesAndWait( |
| authorization_, |
| toolPathC, |
| kAuthorizationFlagDefaults, |
| arguments, |
| NULL, // pipe |
| &exit_status); |
| if (status != errAuthorizationSuccess) { |
| LOG(ERROR) << "AuthorizationExecuteWithPrivileges postflight: " << status; |
| } else if (exit_status != 0) { |
| LOG(ERROR) << "keystone_promote_postflight status " << exit_status; |
| } |
| |
| SEL selector = @selector(changePermissionsForPromotionComplete); |
| [self performSelectorOnMainThread:selector |
| withObject:nil |
| waitUntilDone:NO]; |
| } |
| |
| - (void)changePermissionsForPromotionComplete { |
| authorization_.reset(); |
| |
| [self updateStatus:kAutoupdatePromoted version:nil]; |
| } |
| |
| - (void)setAppPath:(NSString*)appPath { |
| if (appPath != appPath_) { |
| [appPath_ release]; |
| appPath_ = [appPath copy]; |
| } |
| } |
| |
| @end // @implementation KeystoneGlue |
| |
| namespace keystone_glue { |
| |
| bool KeystoneEnabled() { |
| return [KeystoneGlue defaultKeystoneGlue] != nil; |
| } |
| |
| string16 CurrentlyInstalledVersion() { |
| KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; |
| NSString* version = [keystoneGlue currentlyInstalledVersion]; |
| return base::SysNSStringToUTF16(version); |
| } |
| |
| } // namespace keystone_glue |