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