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