Home | History | Annotate | Download | only in mac
      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.
      4 
      5 #import "chrome/browser/mac/keystone_glue.h"
      6 
      7 #include <sys/mount.h>
      8 #include <sys/param.h>
      9 #include <sys/stat.h>
     10 
     11 #include <vector>
     12 
     13 #include "base/bind.h"
     14 #include "base/location.h"
     15 #include "base/logging.h"
     16 #include "base/mac/authorization_util.h"
     17 #include "base/mac/bundle_locations.h"
     18 #include "base/mac/mac_logging.h"
     19 #include "base/mac/mac_util.h"
     20 #include "base/mac/scoped_nsautorelease_pool.h"
     21 #include "base/mac/scoped_nsexception_enabler.h"
     22 #include "base/memory/ref_counted.h"
     23 #include "base/strings/sys_string_conversions.h"
     24 #include "base/threading/worker_pool.h"
     25 #import "chrome/browser/mac/keystone_registration.h"
     26 #include "chrome/common/chrome_constants.h"
     27 #include "chrome/common/chrome_version_info.h"
     28 #include "grit/chromium_strings.h"
     29 #include "grit/generated_resources.h"
     30 #include "ui/base/l10n/l10n_util.h"
     31 #include "ui/base/l10n/l10n_util_mac.h"
     32 
     33 namespace {
     34 
     35 namespace ksr = keystone_registration;
     36 
     37 // Constants for the brand file (uses an external file so it can survive
     38 // updates to Chrome.)
     39 
     40 #if defined(GOOGLE_CHROME_BUILD)
     41 #define kBrandFileName @"Google Chrome Brand.plist";
     42 #elif defined(CHROMIUM_BUILD)
     43 #define kBrandFileName @"Chromium Brand.plist";
     44 #else
     45 #error Unknown branding
     46 #endif
     47 
     48 // These directories are hardcoded in Keystone promotion preflight and the
     49 // Keystone install script, so NSSearchPathForDirectoriesInDomains isn't used
     50 // since the scripts couldn't use anything like that.
     51 NSString* kBrandUserFile = @"~/Library/Google/" kBrandFileName;
     52 NSString* kBrandSystemFile = @"/Library/Google/" kBrandFileName;
     53 
     54 NSString* UserBrandFilePath() {
     55   return [kBrandUserFile stringByStandardizingPath];
     56 }
     57 NSString* SystemBrandFilePath() {
     58   return [kBrandSystemFile stringByStandardizingPath];
     59 }
     60 
     61 // Adaptor for scheduling an Objective-C method call on a |WorkerPool|
     62 // thread.
     63 class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> {
     64  public:
     65 
     66   // Call |sel| on |target| with |arg| in a WorkerPool thread.
     67   // |target| and |arg| are retained, |arg| may be |nil|.
     68   static void PostPerform(id target, SEL sel, id arg) {
     69     DCHECK(target);
     70     DCHECK(sel);
     71 
     72     scoped_refptr<PerformBridge> op = new PerformBridge(target, sel, arg);
     73     base::WorkerPool::PostTask(
     74         FROM_HERE, base::Bind(&PerformBridge::Run, op.get()), true);
     75   }
     76 
     77   // Convenience for the no-argument case.
     78   static void PostPerform(id target, SEL sel) {
     79     PostPerform(target, sel, nil);
     80   }
     81 
     82  private:
     83   // Allow RefCountedThreadSafe<> to delete.
     84   friend class base::RefCountedThreadSafe<PerformBridge>;
     85 
     86   PerformBridge(id target, SEL sel, id arg)
     87       : target_([target retain]),
     88         sel_(sel),
     89         arg_([arg retain]) {
     90   }
     91 
     92   ~PerformBridge() {}
     93 
     94   // Happens on a WorkerPool thread.
     95   void Run() {
     96     base::mac::ScopedNSAutoreleasePool pool;
     97     [target_ performSelector:sel_ withObject:arg_];
     98   }
     99 
    100   base::scoped_nsobject<id> target_;
    101   SEL sel_;
    102   base::scoped_nsobject<id> arg_;
    103 };
    104 
    105 }  // namespace
    106 
    107 @interface KeystoneGlue (Private)
    108 
    109 // Returns the path to the application's Info.plist file.  This returns the
    110 // outer application bundle's Info.plist, not the framework's Info.plist.
    111 - (NSString*)appInfoPlistPath;
    112 
    113 // Returns a dictionary containing parameters to be used for a KSRegistration
    114 // -registerWithParameters: or -promoteWithParameters:authorization: call.
    115 - (NSDictionary*)keystoneParameters;
    116 
    117 // Called when Keystone registration completes.
    118 - (void)registrationComplete:(NSNotification*)notification;
    119 
    120 // Called periodically to announce activity by pinging the Keystone server.
    121 - (void)markActive:(NSTimer*)timer;
    122 
    123 // Called when an update check or update installation is complete.  Posts the
    124 // kAutoupdateStatusNotification notification to the default notification
    125 // center.
    126 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version;
    127 
    128 // Returns the version of the currently-installed application on disk.
    129 - (NSString*)currentlyInstalledVersion;
    130 
    131 // These three methods are used to determine the version of the application
    132 // currently installed on disk, compare that to the currently-running version,
    133 // decide whether any updates have been installed, and call
    134 // -updateStatus:version:.
    135 //
    136 // In order to check the version on disk, the installed application's
    137 // Info.plist dictionary must be read; in order to see changes as updates are
    138 // applied, the dictionary must be read each time, bypassing any caches such
    139 // as the one that NSBundle might be maintaining.  Reading files can be a
    140 // blocking operation, and blocking operations are to be avoided on the main
    141 // thread.  I'm not quite sure what jank means, but I bet that a blocked main
    142 // thread would cause some of it.
    143 //
    144 // -determineUpdateStatusAsync is called on the main thread to initiate the
    145 // operation.  It performs initial set-up work that must be done on the main
    146 // thread and arranges for -determineUpdateStatus to be called on a work queue
    147 // thread managed by WorkerPool.
    148 // -determineUpdateStatus then reads the Info.plist, gets the version from the
    149 // CFBundleShortVersionString key, and performs
    150 // -determineUpdateStatusForVersion: on the main thread.
    151 // -determineUpdateStatusForVersion: does the actual comparison of the version
    152 // on disk with the running version and calls -updateStatus:version: with the
    153 // results of its analysis.
    154 - (void)determineUpdateStatusAsync;
    155 - (void)determineUpdateStatus;
    156 - (void)determineUpdateStatusForVersion:(NSString*)version;
    157 
    158 // Returns YES if registration_ is definitely on a user ticket.  If definitely
    159 // on a system ticket, or uncertain of ticket type (due to an older version
    160 // of Keystone being used), returns NO.
    161 - (BOOL)isUserTicket;
    162 
    163 // Returns YES if Keystone is definitely installed at the system level,
    164 // determined by the presence of an executable ksadmin program at the expected
    165 // system location.
    166 - (BOOL)isSystemKeystone;
    167 
    168 // Returns YES if on a system ticket but system Keystone is not present.
    169 // Returns NO otherwise. The "doomed" condition will result in the
    170 // registration framework appearing to have registered Chrome, but no updates
    171 // ever actually taking place.
    172 - (BOOL)isSystemTicketDoomed;
    173 
    174 // Called when ticket promotion completes.
    175 - (void)promotionComplete:(NSNotification*)notification;
    176 
    177 // Changes the application's ownership and permissions so that all files are
    178 // owned by root:wheel and all files and directories are writable only by
    179 // root, but readable and executable as needed by everyone.
    180 // -changePermissionsForPromotionAsync is called on the main thread by
    181 // -promotionComplete.  That routine calls
    182 // -changePermissionsForPromotionWithTool: on a work queue thread.  When done,
    183 // -changePermissionsForPromotionComplete is called on the main thread.
    184 - (void)changePermissionsForPromotionAsync;
    185 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath;
    186 - (void)changePermissionsForPromotionComplete;
    187 
    188 // Returns the brand file path to use for Keystone.
    189 - (NSString*)brandFilePath;
    190 
    191 @end  // @interface KeystoneGlue (Private)
    192 
    193 NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification";
    194 NSString* const kAutoupdateStatusStatus = @"status";
    195 NSString* const kAutoupdateStatusVersion = @"version";
    196 
    197 namespace {
    198 
    199 NSString* const kChannelKey = @"KSChannelID";
    200 NSString* const kBrandKey = @"KSBrandID";
    201 NSString* const kVersionKey = @"KSVersion";
    202 
    203 }  // namespace
    204 
    205 @implementation KeystoneGlue
    206 
    207 + (id)defaultKeystoneGlue {
    208   static bool sTriedCreatingDefaultKeystoneGlue = false;
    209   // TODO(jrg): use base::SingletonObjC<KeystoneGlue>
    210   static KeystoneGlue* sDefaultKeystoneGlue = nil;  // leaked
    211 
    212   if (!sTriedCreatingDefaultKeystoneGlue) {
    213     sTriedCreatingDefaultKeystoneGlue = true;
    214 
    215     sDefaultKeystoneGlue = [[KeystoneGlue alloc] init];
    216     [sDefaultKeystoneGlue loadParameters];
    217     if (![sDefaultKeystoneGlue loadKeystoneRegistration]) {
    218       [sDefaultKeystoneGlue release];
    219       sDefaultKeystoneGlue = nil;
    220     }
    221   }
    222   return sDefaultKeystoneGlue;
    223 }
    224 
    225 - (id)init {
    226   if ((self = [super init])) {
    227     NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
    228 
    229     [center addObserver:self
    230                selector:@selector(registrationComplete:)
    231                    name:ksr::KSRegistrationDidCompleteNotification
    232                  object:nil];
    233 
    234     [center addObserver:self
    235                selector:@selector(promotionComplete:)
    236                    name:ksr::KSRegistrationPromotionDidCompleteNotification
    237                  object:nil];
    238 
    239     [center addObserver:self
    240                selector:@selector(checkForUpdateComplete:)
    241                    name:ksr::KSRegistrationCheckForUpdateNotification
    242                  object:nil];
    243 
    244     [center addObserver:self
    245                selector:@selector(installUpdateComplete:)
    246                    name:ksr::KSRegistrationStartUpdateNotification
    247                  object:nil];
    248   }
    249 
    250   return self;
    251 }
    252 
    253 - (void)dealloc {
    254   [productID_ release];
    255   [appPath_ release];
    256   [url_ release];
    257   [version_ release];
    258   [channel_ release];
    259   [registration_ release];
    260   [[NSNotificationCenter defaultCenter] removeObserver:self];
    261   [super dealloc];
    262 }
    263 
    264 - (NSDictionary*)infoDictionary {
    265   // Use base::mac::OuterBundle() to get the Chrome app's own bundle identifier
    266   // and path, not the framework's.  For auto-update, the application is
    267   // what's significant here: it's used to locate the outermost part of the
    268   // application for the existence checker and other operations that need to
    269   // see the entire application bundle.
    270   return [base::mac::OuterBundle() infoDictionary];
    271 }
    272 
    273 - (void)loadParameters {
    274   NSBundle* appBundle = base::mac::OuterBundle();
    275   NSDictionary* infoDictionary = [self infoDictionary];
    276 
    277   NSString* productID = [infoDictionary objectForKey:@"KSProductID"];
    278   if (productID == nil) {
    279     productID = [appBundle bundleIdentifier];
    280   }
    281 
    282   NSString* appPath = [appBundle bundlePath];
    283   NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"];
    284   NSString* version = [infoDictionary objectForKey:kVersionKey];
    285 
    286   if (!productID || !appPath || !url || !version) {
    287     // If parameters required for Keystone are missing, don't use it.
    288     return;
    289   }
    290 
    291   NSString* channel = [infoDictionary objectForKey:kChannelKey];
    292   // The stable channel has no tag.  If updating to stable, remove the
    293   // dev and beta tags since we've been "promoted".
    294   if (channel == nil)
    295     channel = ksr::KSRegistrationRemoveExistingTag;
    296 
    297   productID_ = [productID retain];
    298   appPath_ = [appPath retain];
    299   url_ = [url retain];
    300   version_ = [version retain];
    301   channel_ = [channel retain];
    302 }
    303 
    304 - (NSString*)brandFilePath {
    305   DCHECK(version_ != nil) << "-loadParameters must be called first";
    306 
    307   if (brandFileType_ == kBrandFileTypeNotDetermined) {
    308 
    309     NSFileManager* fm = [NSFileManager defaultManager];
    310     NSString* userBrandFile = UserBrandFilePath();
    311     NSString* systemBrandFile = SystemBrandFilePath();
    312 
    313     // Default to none.
    314     brandFileType_ = kBrandFileTypeNone;
    315 
    316     // Only the stable channel has a brand code.
    317     chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
    318 
    319     if (channel == chrome::VersionInfo::CHANNEL_DEV ||
    320         channel == chrome::VersionInfo::CHANNEL_BETA) {
    321 
    322       // If on the dev or beta channel, this installation may have replaced
    323       // an older system-level installation. Check for a user brand file and
    324       // nuke it if present. Don't try to remove the system brand file, there
    325       // wouldn't be any permission to do so.
    326       //
    327       // Don't do this on the canary channel. The canary can run side-by-side
    328       // with another Google Chrome installation whose brand code, if any,
    329       // should remain intact.
    330 
    331       if ([fm fileExistsAtPath:userBrandFile]) {
    332         [fm removeItemAtPath:userBrandFile error:NULL];
    333       }
    334 
    335     } else if (channel == chrome::VersionInfo::CHANNEL_STABLE) {
    336 
    337       // If there is a system brand file, use it.
    338       if ([fm fileExistsAtPath:systemBrandFile]) {
    339         // System
    340 
    341         // Use the system file that is there.
    342         brandFileType_ = kBrandFileTypeSystem;
    343 
    344         // Clean up any old user level file.
    345         if ([fm fileExistsAtPath:userBrandFile]) {
    346           [fm removeItemAtPath:userBrandFile error:NULL];
    347         }
    348 
    349       } else {
    350         // User
    351 
    352         NSDictionary* infoDictionary = [self infoDictionary];
    353         NSString* appBundleBrandID = [infoDictionary objectForKey:kBrandKey];
    354 
    355         NSString* storedBrandID = nil;
    356         if ([fm fileExistsAtPath:userBrandFile]) {
    357           NSDictionary* storedBrandDict =
    358               [NSDictionary dictionaryWithContentsOfFile:userBrandFile];
    359           storedBrandID = [storedBrandDict objectForKey:kBrandKey];
    360         }
    361 
    362         if ((appBundleBrandID != nil) &&
    363             (![storedBrandID isEqualTo:appBundleBrandID])) {
    364           // App and store don't match, update store and use it.
    365           NSDictionary* storedBrandDict =
    366               [NSDictionary dictionaryWithObject:appBundleBrandID
    367                                           forKey:kBrandKey];
    368           // If Keystone hasn't been installed yet, the location the brand file
    369           // is written to won't exist, so manually create the directory.
    370           NSString *userBrandFileDirectory =
    371               [userBrandFile stringByDeletingLastPathComponent];
    372           if (![fm fileExistsAtPath:userBrandFileDirectory]) {
    373             if (![fm createDirectoryAtPath:userBrandFileDirectory
    374                withIntermediateDirectories:YES
    375                                 attributes:nil
    376                                      error:NULL]) {
    377               LOG(ERROR) << "Failed to create the directory for the brand file";
    378             }
    379           }
    380           if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) {
    381             brandFileType_ = kBrandFileTypeUser;
    382           }
    383         } else if (storedBrandID) {
    384           // Had stored brand, use it.
    385           brandFileType_ = kBrandFileTypeUser;
    386         }
    387       }
    388     }
    389 
    390   }
    391 
    392   NSString* result = nil;
    393   switch (brandFileType_) {
    394     case kBrandFileTypeUser:
    395       result = UserBrandFilePath();
    396       break;
    397 
    398     case kBrandFileTypeSystem:
    399       result = SystemBrandFilePath();
    400       break;
    401 
    402     case kBrandFileTypeNotDetermined:
    403       NOTIMPLEMENTED();
    404       // Fall through
    405     case kBrandFileTypeNone:
    406       // Clear the value.
    407       result = @"";
    408       break;
    409 
    410   }
    411   return result;
    412 }
    413 
    414 - (BOOL)loadKeystoneRegistration {
    415   if (!productID_ || !appPath_ || !url_ || !version_)
    416     return NO;
    417 
    418   // Load the KeystoneRegistration framework bundle if present.  It lives
    419   // inside the framework, so use base::mac::FrameworkBundle();
    420   NSString* ksrPath =
    421       [[base::mac::FrameworkBundle() privateFrameworksPath]
    422           stringByAppendingPathComponent:@"KeystoneRegistration.framework"];
    423   NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath];
    424   [ksrBundle load];
    425 
    426   // Harness the KSRegistration class.
    427   Class ksrClass = [ksrBundle classNamed:@"KSRegistration"];
    428   KSRegistration* ksr = [ksrClass registrationWithProductID:productID_];
    429   if (!ksr)
    430     return NO;
    431 
    432   registration_ = [ksr retain];
    433   return YES;
    434 }
    435 
    436 - (NSString*)appInfoPlistPath {
    437   // NSBundle ought to have a way to access this path directly, but it
    438   // doesn't.
    439   return [[appPath_ stringByAppendingPathComponent:@"Contents"]
    440              stringByAppendingPathComponent:@"Info.plist"];
    441 }
    442 
    443 - (NSDictionary*)keystoneParameters {
    444   NSNumber* xcType = [NSNumber numberWithInt:ksr::kKSPathExistenceChecker];
    445   NSNumber* preserveTTToken = [NSNumber numberWithBool:YES];
    446   NSString* appInfoPlistPath = [self appInfoPlistPath];
    447   NSString* brandKey = kBrandKey;
    448   NSString* brandPath = [self brandFilePath];
    449 
    450   if ([brandPath length] == 0) {
    451     // Brand path and brand key must be cleared together or ksadmin seems
    452     // to throw an error.
    453     brandKey = @"";
    454   }
    455 
    456   return [NSDictionary dictionaryWithObjectsAndKeys:
    457              version_, ksr::KSRegistrationVersionKey,
    458              appInfoPlistPath, ksr::KSRegistrationVersionPathKey,
    459              kVersionKey, ksr::KSRegistrationVersionKeyKey,
    460              xcType, ksr::KSRegistrationExistenceCheckerTypeKey,
    461              appPath_, ksr::KSRegistrationExistenceCheckerStringKey,
    462              url_, ksr::KSRegistrationServerURLStringKey,
    463              preserveTTToken, ksr::KSRegistrationPreserveTrustedTesterTokenKey,
    464              channel_, ksr::KSRegistrationTagKey,
    465              appInfoPlistPath, ksr::KSRegistrationTagPathKey,
    466              kChannelKey, ksr::KSRegistrationTagKeyKey,
    467              brandPath, ksr::KSRegistrationBrandPathKey,
    468              brandKey, ksr::KSRegistrationBrandKeyKey,
    469              nil];
    470 }
    471 
    472 - (void)registerWithKeystone {
    473   [self updateStatus:kAutoupdateRegistering version:nil];
    474 
    475   NSDictionary* parameters = [self keystoneParameters];
    476   BOOL result;
    477   {
    478     // TODO(shess): Allows Keystone to throw an exception when
    479     // /usr/bin/python does not exist (really!).
    480     // http://crbug.com/86221 and http://crbug.com/87931
    481     base::mac::ScopedNSExceptionEnabler enabler;
    482     result = [registration_ registerWithParameters:parameters];
    483   }
    484   if (!result) {
    485     [self updateStatus:kAutoupdateRegisterFailed version:nil];
    486     return;
    487   }
    488 
    489   // Upon completion, ksr::KSRegistrationDidCompleteNotification will be
    490   // posted, and -registrationComplete: will be called.
    491 
    492   // Mark an active RIGHT NOW; don't wait an hour for the first one.
    493   [registration_ setActive];
    494 
    495   // Set up hourly activity pings.
    496   timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60  // One hour
    497                                             target:self
    498                                           selector:@selector(markActive:)
    499                                           userInfo:registration_
    500                                            repeats:YES];
    501 }
    502 
    503 - (void)registrationComplete:(NSNotification*)notification {
    504   NSDictionary* userInfo = [notification userInfo];
    505   if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
    506     if ([self isSystemTicketDoomed]) {
    507       [self updateStatus:kAutoupdateNeedsPromotion version:nil];
    508     } else {
    509       [self updateStatus:kAutoupdateRegistered version:nil];
    510     }
    511   } else {
    512     // Dump registration_?
    513     [self updateStatus:kAutoupdateRegisterFailed version:nil];
    514   }
    515 }
    516 
    517 - (void)stopTimer {
    518   [timer_ invalidate];
    519 }
    520 
    521 - (void)markActive:(NSTimer*)timer {
    522   KSRegistration* ksr = [timer userInfo];
    523   [ksr setActive];
    524 }
    525 
    526 - (void)checkForUpdate {
    527   DCHECK(![self asyncOperationPending]);
    528 
    529   if (!registration_) {
    530     [self updateStatus:kAutoupdateCheckFailed version:nil];
    531     return;
    532   }
    533 
    534   [self updateStatus:kAutoupdateChecking version:nil];
    535 
    536   [registration_ checkForUpdate];
    537 
    538   // Upon completion, ksr::KSRegistrationCheckForUpdateNotification will be
    539   // posted, and -checkForUpdateComplete: will be called.
    540 }
    541 
    542 - (void)checkForUpdateComplete:(NSNotification*)notification {
    543   NSDictionary* userInfo = [notification userInfo];
    544 
    545   if ([[userInfo objectForKey:ksr::KSRegistrationUpdateCheckErrorKey]
    546           boolValue]) {
    547     [self updateStatus:kAutoupdateCheckFailed version:nil];
    548   } else if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
    549     // If an update is known to be available, go straight to
    550     // -updateStatus:version:.  It doesn't matter what's currently on disk.
    551     NSString* version = [userInfo objectForKey:ksr::KSRegistrationVersionKey];
    552     [self updateStatus:kAutoupdateAvailable version:version];
    553   } else {
    554     // If no updates are available, check what's on disk, because an update
    555     // may have already been installed.  This check happens on another thread,
    556     // and -updateStatus:version: will be called on the main thread when done.
    557     [self determineUpdateStatusAsync];
    558   }
    559 }
    560 
    561 - (void)installUpdate {
    562   DCHECK(![self asyncOperationPending]);
    563 
    564   if (!registration_) {
    565     [self updateStatus:kAutoupdateInstallFailed version:nil];
    566     return;
    567   }
    568 
    569   [self updateStatus:kAutoupdateInstalling version:nil];
    570 
    571   [registration_ startUpdate];
    572 
    573   // Upon completion, ksr::KSRegistrationStartUpdateNotification will be
    574   // posted, and -installUpdateComplete: will be called.
    575 }
    576 
    577 - (void)installUpdateComplete:(NSNotification*)notification {
    578   NSDictionary* userInfo = [notification userInfo];
    579 
    580   // http://crbug.com/160308 and b/7517358: when using system Keystone and on
    581   // a user ticket, KSUpdateCheckSuccessfulKey will be NO even when an update
    582   // was installed correctly, so don't check it. It should be redudnant when
    583   // KSUpdateCheckSuccessfullyInstalledKey is checked.
    584   if (![[userInfo objectForKey:ksr::KSUpdateCheckSuccessfullyInstalledKey]
    585           intValue]) {
    586     [self updateStatus:kAutoupdateInstallFailed version:nil];
    587   } else {
    588     updateSuccessfullyInstalled_ = YES;
    589 
    590     // Nothing in the notification dictionary reports the version that was
    591     // installed.  Figure it out based on what's on disk.
    592     [self determineUpdateStatusAsync];
    593   }
    594 }
    595 
    596 - (NSString*)currentlyInstalledVersion {
    597   NSString* appInfoPlistPath = [self appInfoPlistPath];
    598   NSDictionary* infoPlist =
    599       [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath];
    600   return [infoPlist objectForKey:@"CFBundleShortVersionString"];
    601 }
    602 
    603 // Runs on the main thread.
    604 - (void)determineUpdateStatusAsync {
    605   DCHECK([NSThread isMainThread]);
    606 
    607   PerformBridge::PostPerform(self, @selector(determineUpdateStatus));
    608 }
    609 
    610 // Runs on a thread managed by WorkerPool.
    611 - (void)determineUpdateStatus {
    612   DCHECK(![NSThread isMainThread]);
    613 
    614   NSString* version = [self currentlyInstalledVersion];
    615 
    616   [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:)
    617                          withObject:version
    618                       waitUntilDone:NO];
    619 }
    620 
    621 // Runs on the main thread.
    622 - (void)determineUpdateStatusForVersion:(NSString*)version {
    623   DCHECK([NSThread isMainThread]);
    624 
    625   AutoupdateStatus status;
    626   if (updateSuccessfullyInstalled_) {
    627     // If an update was successfully installed and this object saw it happen,
    628     // then don't even bother comparing versions.
    629     status = kAutoupdateInstalled;
    630   } else {
    631     NSString* currentVersion =
    632         [NSString stringWithUTF8String:chrome::kChromeVersion];
    633     if (!version) {
    634       // If the version on disk could not be determined, assume that
    635       // whatever's running is current.
    636       version = currentVersion;
    637       status = kAutoupdateCurrent;
    638     } else if ([version isEqualToString:currentVersion]) {
    639       status = kAutoupdateCurrent;
    640     } else {
    641       // If the version on disk doesn't match what's currently running, an
    642       // update must have been applied in the background, without this app's
    643       // direct participation.  Leave updateSuccessfullyInstalled_ alone
    644       // because there's no direct knowledge of what actually happened.
    645       status = kAutoupdateInstalled;
    646     }
    647   }
    648 
    649   [self updateStatus:status version:version];
    650 }
    651 
    652 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version {
    653   NSNumber* statusNumber = [NSNumber numberWithInt:status];
    654   NSMutableDictionary* dictionary =
    655       [NSMutableDictionary dictionaryWithObject:statusNumber
    656                                          forKey:kAutoupdateStatusStatus];
    657   if (version) {
    658     [dictionary setObject:version forKey:kAutoupdateStatusVersion];
    659   }
    660 
    661   NSNotification* notification =
    662       [NSNotification notificationWithName:kAutoupdateStatusNotification
    663                                     object:self
    664                                   userInfo:dictionary];
    665   recentNotification_.reset([notification retain]);
    666 
    667   [[NSNotificationCenter defaultCenter] postNotification:notification];
    668 }
    669 
    670 - (NSNotification*)recentNotification {
    671   return [[recentNotification_ retain] autorelease];
    672 }
    673 
    674 - (AutoupdateStatus)recentStatus {
    675   NSDictionary* dictionary = [recentNotification_ userInfo];
    676   return static_cast<AutoupdateStatus>(
    677       [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]);
    678 }
    679 
    680 - (BOOL)asyncOperationPending {
    681   AutoupdateStatus status = [self recentStatus];
    682   return status == kAutoupdateRegistering ||
    683          status == kAutoupdateChecking ||
    684          status == kAutoupdateInstalling ||
    685          status == kAutoupdatePromoting;
    686 }
    687 
    688 - (BOOL)isUserTicket {
    689   return [registration_ ticketType] == ksr::kKSRegistrationUserTicket;
    690 }
    691 
    692 - (BOOL)isSystemKeystone {
    693   struct stat statbuf;
    694   if (stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
    695            "Contents/MacOS/ksadmin",
    696            &statbuf) != 0) {
    697     return NO;
    698   }
    699 
    700   if (!(statbuf.st_mode & S_IXUSR)) {
    701     return NO;
    702   }
    703 
    704   return YES;
    705 }
    706 
    707 - (BOOL)isSystemTicketDoomed {
    708   BOOL isSystemTicket = ![self isUserTicket];
    709   return isSystemTicket && ![self isSystemKeystone];
    710 }
    711 
    712 - (BOOL)isOnReadOnlyFilesystem {
    713   const char* appPathC = [appPath_ fileSystemRepresentation];
    714   struct statfs statfsBuf;
    715 
    716   if (statfs(appPathC, &statfsBuf) != 0) {
    717     PLOG(ERROR) << "statfs";
    718     // Be optimistic about the filesystem's writability.
    719     return NO;
    720   }
    721 
    722   return (statfsBuf.f_flags & MNT_RDONLY) != 0;
    723 }
    724 
    725 - (BOOL)needsPromotion {
    726   // Don't promote when on a read-only filesystem.
    727   if ([self isOnReadOnlyFilesystem]) {
    728     return NO;
    729   }
    730 
    731   // Promotion is required when a system ticket is present but system Keystone
    732   // is not.
    733   if ([self isSystemTicketDoomed]) {
    734     return YES;
    735   }
    736 
    737   // If on a system ticket and system Keystone is present, promotion is not
    738   // required.
    739   if (![self isUserTicket]) {
    740     return NO;
    741   }
    742 
    743   // Check the outermost bundle directory, the main executable path, and the
    744   // framework directory.  It may be enough to just look at the outermost
    745   // bundle directory, but checking an interior file and directory can be
    746   // helpful in case permissions are set differently only on the outermost
    747   // directory.  An interior file and directory are both checked because some
    748   // file operations, such as Snow Leopard's Finder's copy operation when
    749   // authenticating, may actually result in different ownership being applied
    750   // to files and directories.
    751   NSFileManager* fileManager = [NSFileManager defaultManager];
    752   NSString* executablePath = [base::mac::OuterBundle() executablePath];
    753   NSString* frameworkPath = [base::mac::FrameworkBundle() bundlePath];
    754   return ![fileManager isWritableFileAtPath:appPath_] ||
    755          ![fileManager isWritableFileAtPath:executablePath] ||
    756          ![fileManager isWritableFileAtPath:frameworkPath];
    757 }
    758 
    759 - (BOOL)wantsPromotion {
    760   if ([self needsPromotion]) {
    761     return YES;
    762   }
    763 
    764   // These are the same unpromotable cases as in -needsPromotion.
    765   if ([self isOnReadOnlyFilesystem] || ![self isUserTicket]) {
    766     return NO;
    767   }
    768 
    769   return [appPath_ hasPrefix:@"/Applications/"];
    770 }
    771 
    772 - (void)promoteTicket {
    773   if ([self asyncOperationPending] || ![self wantsPromotion]) {
    774     // Because there are multiple ways of reaching promoteTicket that might
    775     // not lock each other out, it may be possible to arrive here while an
    776     // asynchronous operation is pending, or even after promotion has already
    777     // occurred.  Just quietly return without doing anything.
    778     return;
    779   }
    780 
    781   NSString* prompt = l10n_util::GetNSStringFWithFixup(
    782       IDS_PROMOTE_AUTHENTICATION_PROMPT,
    783       l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
    784   base::mac::ScopedAuthorizationRef authorization(
    785       base::mac::AuthorizationCreateToRunAsRoot(
    786           base::mac::NSToCFCast(prompt)));
    787   if (!authorization.get()) {
    788     return;
    789   }
    790 
    791   [self promoteTicketWithAuthorization:authorization.release() synchronous:NO];
    792 }
    793 
    794 - (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg
    795                            synchronous:(BOOL)synchronous {
    796   base::mac::ScopedAuthorizationRef authorization(authorization_arg);
    797   authorization_arg = NULL;
    798 
    799   if ([self asyncOperationPending]) {
    800     // Starting a synchronous operation while an asynchronous one is pending
    801     // could be trouble.
    802     return;
    803   }
    804   if (!synchronous && ![self wantsPromotion]) {
    805     // If operating synchronously, the call came from the installer, which
    806     // means that a system ticket is required.  Otherwise, only allow
    807     // promotion if it's wanted.
    808     return;
    809   }
    810 
    811   synchronousPromotion_ = synchronous;
    812 
    813   [self updateStatus:kAutoupdatePromoting version:nil];
    814 
    815   // TODO(mark): Remove when able!
    816   //
    817   // keystone_promote_preflight will copy the current brand information out to
    818   // the system level so all users can share the data as part of the ticket
    819   // promotion.
    820   //
    821   // It will also ensure that the Keystone system ticket store is in a usable
    822   // state for all users on the system.  Ideally, Keystone's installer or
    823   // another part of Keystone would handle this.  The underlying problem is
    824   // http://b/2285921, and it causes http://b/2289908, which this workaround
    825   // addresses.
    826   //
    827   // This is run synchronously, which isn't optimal, but
    828   // -[KSRegistration promoteWithParameters:authorization:] is currently
    829   // synchronous too, and this operation needs to happen before that one.
    830   //
    831   // TODO(mark): Make asynchronous.  That only makes sense if the promotion
    832   // operation itself is asynchronous too.  http://b/2290009.  Hopefully,
    833   // the Keystone promotion code will just be changed to do what preflight
    834   // now does, and then the preflight script can be removed instead.
    835   // However, preflight operation (and promotion) should only be asynchronous
    836   // if the synchronous parameter is NO.
    837   NSString* preflightPath =
    838       [base::mac::FrameworkBundle()
    839           pathForResource:@"keystone_promote_preflight"
    840                    ofType:@"sh"];
    841   const char* preflightPathC = [preflightPath fileSystemRepresentation];
    842   const char* userBrandFile = NULL;
    843   const char* systemBrandFile = NULL;
    844   if (brandFileType_ == kBrandFileTypeUser) {
    845     // Running with user level brand file, promote to the system level.
    846     userBrandFile = [UserBrandFilePath() fileSystemRepresentation];
    847     systemBrandFile = [SystemBrandFilePath() fileSystemRepresentation];
    848   }
    849   const char* arguments[] = {userBrandFile, systemBrandFile, NULL};
    850 
    851   int exit_status;
    852   OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
    853       authorization,
    854       preflightPathC,
    855       kAuthorizationFlagDefaults,
    856       arguments,
    857       NULL,  // pipe
    858       &exit_status);
    859   if (status != errAuthorizationSuccess) {
    860     OSSTATUS_LOG(ERROR, status)
    861         << "AuthorizationExecuteWithPrivileges preflight";
    862     [self updateStatus:kAutoupdatePromoteFailed version:nil];
    863     return;
    864   }
    865   if (exit_status != 0) {
    866     LOG(ERROR) << "keystone_promote_preflight status " << exit_status;
    867     [self updateStatus:kAutoupdatePromoteFailed version:nil];
    868     return;
    869   }
    870 
    871   // Hang on to the AuthorizationRef so that it can be used once promotion is
    872   // complete.  Do this before asking Keystone to promote the ticket, because
    873   // -promotionComplete: may be called from inside the Keystone promotion
    874   // call.
    875   authorization_.swap(authorization);
    876 
    877   NSDictionary* parameters = [self keystoneParameters];
    878 
    879   // If the brand file is user level, update parameters to point to the new
    880   // system level file during promotion.
    881   if (brandFileType_ == kBrandFileTypeUser) {
    882     NSMutableDictionary* temp_parameters =
    883         [[parameters mutableCopy] autorelease];
    884     [temp_parameters setObject:SystemBrandFilePath()
    885                         forKey:ksr::KSRegistrationBrandPathKey];
    886     parameters = temp_parameters;
    887   }
    888 
    889   if (![registration_ promoteWithParameters:parameters
    890                               authorization:authorization_]) {
    891     [self updateStatus:kAutoupdatePromoteFailed version:nil];
    892     authorization_.reset();
    893     return;
    894   }
    895 
    896   // Upon completion, ksr::KSRegistrationPromotionDidCompleteNotification will
    897   // be posted, and -promotionComplete: will be called.
    898 
    899   // If synchronous, see to it that this happens immediately. Give it a
    900   // 10-second deadline.
    901   if (synchronous) {
    902     CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
    903   }
    904 }
    905 
    906 - (void)promotionComplete:(NSNotification*)notification {
    907   NSDictionary* userInfo = [notification userInfo];
    908   if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
    909     if (synchronousPromotion_) {
    910       // Short-circuit: if performing a synchronous promotion, the promotion
    911       // came from the installer, which already set the permissions properly.
    912       // Rather than run a duplicate permission-changing operation, jump
    913       // straight to "done."
    914       [self changePermissionsForPromotionComplete];
    915     } else {
    916       [self changePermissionsForPromotionAsync];
    917     }
    918   } else {
    919     authorization_.reset();
    920     [self updateStatus:kAutoupdatePromoteFailed version:nil];
    921   }
    922 
    923   if (synchronousPromotion_) {
    924     // The run loop doesn't need to wait for this any longer.
    925     CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    926     CFRunLoopStop(runLoop);
    927     CFRunLoopWakeUp(runLoop);
    928   }
    929 }
    930 
    931 - (void)changePermissionsForPromotionAsync {
    932   // NSBundle is not documented as being thread-safe.  Do NSBundle operations
    933   // on the main thread before jumping over to a WorkerPool-managed
    934   // thread to run the tool.
    935   DCHECK([NSThread isMainThread]);
    936 
    937   SEL selector = @selector(changePermissionsForPromotionWithTool:);
    938   NSString* toolPath =
    939       [base::mac::FrameworkBundle()
    940           pathForResource:@"keystone_promote_postflight"
    941                    ofType:@"sh"];
    942 
    943   PerformBridge::PostPerform(self, selector, toolPath);
    944 }
    945 
    946 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath {
    947   const char* toolPathC = [toolPath fileSystemRepresentation];
    948 
    949   const char* appPathC = [appPath_ fileSystemRepresentation];
    950   const char* arguments[] = {appPathC, NULL};
    951 
    952   int exit_status;
    953   OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
    954       authorization_,
    955       toolPathC,
    956       kAuthorizationFlagDefaults,
    957       arguments,
    958       NULL,  // pipe
    959       &exit_status);
    960   if (status != errAuthorizationSuccess) {
    961     OSSTATUS_LOG(ERROR, status)
    962         << "AuthorizationExecuteWithPrivileges postflight";
    963   } else if (exit_status != 0) {
    964     LOG(ERROR) << "keystone_promote_postflight status " << exit_status;
    965   }
    966 
    967   SEL selector = @selector(changePermissionsForPromotionComplete);
    968   [self performSelectorOnMainThread:selector
    969                          withObject:nil
    970                       waitUntilDone:NO];
    971 }
    972 
    973 - (void)changePermissionsForPromotionComplete {
    974   authorization_.reset();
    975 
    976   [self updateStatus:kAutoupdatePromoted version:nil];
    977 }
    978 
    979 - (void)setAppPath:(NSString*)appPath {
    980   if (appPath != appPath_) {
    981     [appPath_ release];
    982     appPath_ = [appPath copy];
    983   }
    984 }
    985 
    986 @end  // @implementation KeystoneGlue
    987 
    988 namespace {
    989 
    990 std::string BrandCodeInternal() {
    991   KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
    992   NSString* brand_path = [keystone_glue brandFilePath];
    993 
    994   if (![brand_path length])
    995     return std::string();
    996 
    997   NSDictionary* dict =
    998       [NSDictionary dictionaryWithContentsOfFile:brand_path];
    999   NSString* brand_code =
   1000       base::mac::ObjCCast<NSString>([dict objectForKey:kBrandKey]);
   1001   if (brand_code)
   1002     return [brand_code UTF8String];
   1003 
   1004   return std::string();
   1005 }
   1006 
   1007 }  // namespace
   1008 
   1009 namespace keystone_glue {
   1010 
   1011 std::string BrandCode() {
   1012   // |s_brand_code| is leaked.
   1013   static std::string* s_brand_code = new std::string(BrandCodeInternal());
   1014   return *s_brand_code;
   1015 }
   1016 
   1017 bool KeystoneEnabled() {
   1018   return [KeystoneGlue defaultKeystoneGlue] != nil;
   1019 }
   1020 
   1021 string16 CurrentlyInstalledVersion() {
   1022   KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
   1023   NSString* version = [keystoneGlue currentlyInstalledVersion];
   1024   return base::SysNSStringToUTF16(version);
   1025 }
   1026 
   1027 }  // namespace keystone_glue
   1028