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