Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / mac / keystone_glue.mm
blob959eac28004f85bab192f0cc75642e2472e49c7b
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.
5 #import "chrome/browser/mac/keystone_glue.h"
7 #include <sys/mount.h>
8 #include <sys/param.h>
9 #include <sys/stat.h>
11 #include <vector>
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"
35 namespace {
37 namespace ksr = keystone_registration;
39 // Constants for the brand file (uses an external file so it can survive
40 // updates to Chrome.)
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
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;
56 NSString* UserBrandFilePath() {
57   return [kBrandUserFile stringByStandardizingPath];
59 NSString* SystemBrandFilePath() {
60   return [kBrandSystemFile stringByStandardizingPath];
63 // Adaptor for scheduling an Objective-C method call on a |WorkerPool|
64 // thread.
65 class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> {
66  public:
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);
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   }
79   // Convenience for the no-argument case.
80   static void PostPerform(id target, SEL sel) {
81     PostPerform(target, sel, nil);
82   }
84  private:
85   // Allow RefCountedThreadSafe<> to delete.
86   friend class base::RefCountedThreadSafe<PerformBridge>;
88   PerformBridge(id target, SEL sel, id arg)
89       : target_([target retain]),
90         sel_(sel),
91         arg_([arg retain]) {
92   }
94   ~PerformBridge() {}
96   // Happens on a WorkerPool thread.
97   void Run() {
98     base::mac::ScopedNSAutoreleasePool pool;
99     [target_ performSelector:sel_ withObject:arg_];
100   }
102   base::scoped_nsobject<id> target_;
103   SEL sel_;
104   base::scoped_nsobject<id> arg_;
107 }  // namespace
109 @interface KeystoneGlue (Private)
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;
115 // Returns a dictionary containing parameters to be used for a KSRegistration
116 // -registerWithParameters: or -promoteWithParameters:authorization: call.
117 - (NSDictionary*)keystoneParameters;
119 // Called when Keystone registration completes.
120 - (void)registrationComplete:(NSNotification*)notification;
122 // Called periodically to announce activity by pinging the Keystone server.
123 - (void)markActive:(NSTimer*)timer;
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;
130 // Returns the version of the currently-installed application on disk.
131 - (NSString*)currentlyInstalledVersion;
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:.
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.
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;
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;
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;
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;
176 // Called when ticket promotion completes.
177 - (void)promotionComplete:(NSNotification*)notification;
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;
190 // Returns the brand file path to use for Keystone.
191 - (NSString*)brandFilePath;
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;
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;
206 @end  // @interface KeystoneGlue (Private)
208 NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification";
209 NSString* const kAutoupdateStatusStatus = @"status";
210 NSString* const kAutoupdateStatusVersion = @"version";
212 namespace {
214 NSString* const kChannelKey = @"KSChannelID";
215 NSString* const kBrandKey = @"KSBrandID";
216 NSString* const kVersionKey = @"KSVersion";
218 }  // namespace
220 @implementation KeystoneGlue
222 + (id)defaultKeystoneGlue {
223   static bool sTriedCreatingDefaultKeystoneGlue = false;
224   // TODO(jrg): use base::SingletonObjC<KeystoneGlue>
225   static KeystoneGlue* sDefaultKeystoneGlue = nil;  // leaked
227   if (!sTriedCreatingDefaultKeystoneGlue) {
228     sTriedCreatingDefaultKeystoneGlue = true;
230     sDefaultKeystoneGlue = [[KeystoneGlue alloc] init];
231     [sDefaultKeystoneGlue loadParameters];
232     if (![sDefaultKeystoneGlue loadKeystoneRegistration]) {
233       [sDefaultKeystoneGlue release];
234       sDefaultKeystoneGlue = nil;
235     }
236   }
237   return sDefaultKeystoneGlue;
240 - (id)init {
241   if ((self = [super init])) {
242     NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
244     [center addObserver:self
245                selector:@selector(registrationComplete:)
246                    name:ksr::KSRegistrationDidCompleteNotification
247                  object:nil];
249     [center addObserver:self
250                selector:@selector(promotionComplete:)
251                    name:ksr::KSRegistrationPromotionDidCompleteNotification
252                  object:nil];
254     [center addObserver:self
255                selector:@selector(checkForUpdateComplete:)
256                    name:ksr::KSRegistrationCheckForUpdateNotification
257                  object:nil];
259     [center addObserver:self
260                selector:@selector(installUpdateComplete:)
261                    name:ksr::KSRegistrationStartUpdateNotification
262                  object:nil];
263   }
265   return self;
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];
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];
288 - (void)loadParameters {
289   NSBundle* appBundle = base::mac::OuterBundle();
290   NSDictionary* infoDictionary = [self infoDictionary];
292   NSString* productID = [infoDictionary objectForKey:@"KSProductID"];
293   if (productID == nil) {
294     productID = [appBundle bundleIdentifier];
295   }
297   NSString* appPath = [appBundle bundlePath];
298   NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"];
299   NSString* version = [infoDictionary objectForKey:kVersionKey];
301   if (!productID || !appPath || !url || !version) {
302     // If parameters required for Keystone are missing, don't use it.
303     return;
304   }
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;
312   productID_ = [productID retain];
313   appPath_ = [appPath retain];
314   url_ = [url retain];
315   version_ = [version retain];
316   channel_ = [channel retain];
319 - (NSString*)brandFilePath {
320   DCHECK(version_ != nil) << "-loadParameters must be called first";
322   if (brandFileType_ == kBrandFileTypeNotDetermined) {
324     NSFileManager* fm = [NSFileManager defaultManager];
325     NSString* userBrandFile = UserBrandFilePath();
326     NSString* systemBrandFile = SystemBrandFilePath();
328     // Default to none.
329     brandFileType_ = kBrandFileTypeNone;
331     // Only the stable channel has a brand code.
332     chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
334     if (channel == chrome::VersionInfo::CHANNEL_DEV ||
335         channel == chrome::VersionInfo::CHANNEL_BETA) {
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.
346       if ([fm fileExistsAtPath:userBrandFile]) {
347         [fm removeItemAtPath:userBrandFile error:NULL];
348       }
350     } else if (channel == chrome::VersionInfo::CHANNEL_STABLE) {
352       // If there is a system brand file, use it.
353       if ([fm fileExistsAtPath:systemBrandFile]) {
354         // System
356         // Use the system file that is there.
357         brandFileType_ = kBrandFileTypeSystem;
359         // Clean up any old user level file.
360         if ([fm fileExistsAtPath:userBrandFile]) {
361           [fm removeItemAtPath:userBrandFile error:NULL];
362         }
364       } else {
365         // User
367         NSDictionary* infoDictionary = [self infoDictionary];
368         NSString* appBundleBrandID = [infoDictionary objectForKey:kBrandKey];
370         NSString* storedBrandID = nil;
371         if ([fm fileExistsAtPath:userBrandFile]) {
372           NSDictionary* storedBrandDict =
373               [NSDictionary dictionaryWithContentsOfFile:userBrandFile];
374           storedBrandID = [storedBrandDict objectForKey:kBrandKey];
375         }
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     }
405   }
407   NSString* result = nil;
408   switch (brandFileType_) {
409     case kBrandFileTypeUser:
410       result = UserBrandFilePath();
411       break;
413     case kBrandFileTypeSystem:
414       result = SystemBrandFilePath();
415       break;
417     case kBrandFileTypeNotDetermined:
418       NOTIMPLEMENTED();
419       // Fall through
420     case kBrandFileTypeNone:
421       // Clear the value.
422       result = @"";
423       break;
425   }
426   return result;
429 - (BOOL)loadKeystoneRegistration {
430   if (!productID_ || !appPath_ || !url_ || !version_)
431     return NO;
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];
441   // Harness the KSRegistration class.
442   Class ksrClass = [ksrBundle classNamed:@"KSRegistration"];
443   KSRegistration* ksr = [ksrClass registrationWithProductID:productID_];
444   if (!ksr)
445     return NO;
447   registration_ = [ksr retain];
448   return YES;
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"];
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];
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   }
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];
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];
494 - (void)registerWithKeystone {
495   [self updateStatus:kAutoupdateRegistering version:nil];
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   }
511   // Upon completion, ksr::KSRegistrationDidCompleteNotification will be
512   // posted, and -registrationComplete: will be called.
514   // Mark an active RIGHT NOW; don't wait an hour for the first one.
515   [registration_ setActive];
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];
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   }
539 - (void)stopTimer {
540   [timer_ invalidate];
543 - (void)markActive:(NSTimer*)timer {
544   KSRegistration* ksr = [timer userInfo];
545   [ksr setActive];
548 - (void)checkForUpdate {
549   DCHECK(![self asyncOperationPending]);
551   if (!registration_) {
552     [self updateStatus:kAutoupdateCheckFailed version:nil];
553     return;
554   }
556   [self updateStatus:kAutoupdateChecking version:nil];
558   [registration_ checkForUpdate];
560   // Upon completion, ksr::KSRegistrationCheckForUpdateNotification will be
561   // posted, and -checkForUpdateComplete: will be called.
564 - (void)checkForUpdateComplete:(NSNotification*)notification {
565   NSDictionary* userInfo = [notification userInfo];
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   }
583 - (void)installUpdate {
584   DCHECK(![self asyncOperationPending]);
586   if (!registration_) {
587     [self updateStatus:kAutoupdateInstallFailed version:nil];
588     return;
589   }
591   [self updateStatus:kAutoupdateInstalling version:nil];
593   [registration_ startUpdate];
595   // Upon completion, ksr::KSRegistrationStartUpdateNotification will be
596   // posted, and -installUpdateComplete: will be called.
599 - (void)installUpdateComplete:(NSNotification*)notification {
600   NSDictionary* userInfo = [notification userInfo];
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;
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   }
618 - (NSString*)currentlyInstalledVersion {
619   NSString* appInfoPlistPath = [self appInfoPlistPath];
620   NSDictionary* infoPlist =
621       [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath];
622   return [infoPlist objectForKey:@"CFBundleShortVersionString"];
625 // Runs on the main thread.
626 - (void)determineUpdateStatusAsync {
627   DCHECK([NSThread isMainThread]);
629   PerformBridge::PostPerform(self, @selector(determineUpdateStatus));
632 // Runs on a thread managed by WorkerPool.
633 - (void)determineUpdateStatus {
634   DCHECK(![NSThread isMainThread]);
636   NSString* version = [self currentlyInstalledVersion];
638   [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:)
639                          withObject:version
640                       waitUntilDone:NO];
643 // Runs on the main thread.
644 - (void)determineUpdateStatusForVersion:(NSString*)version {
645   DCHECK([NSThread isMainThread]);
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   }
671   [self updateStatus:status version:version];
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   }
683   NSNotification* notification =
684       [NSNotification notificationWithName:kAutoupdateStatusNotification
685                                     object:self
686                                   userInfo:dictionary];
687   recentNotification_.reset([notification retain]);
689   [[NSNotificationCenter defaultCenter] postNotification:notification];
692 - (NSNotification*)recentNotification {
693   return [[recentNotification_ retain] autorelease];
696 - (AutoupdateStatus)recentStatus {
697   NSDictionary* dictionary = [recentNotification_ userInfo];
698   return static_cast<AutoupdateStatus>(
699       [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]);
702 - (BOOL)asyncOperationPending {
703   AutoupdateStatus status = [self recentStatus];
704   return status == kAutoupdateRegistering ||
705          status == kAutoupdateChecking ||
706          status == kAutoupdateInstalling ||
707          status == kAutoupdatePromoting;
710 - (BOOL)isUserTicket {
711   return [registration_ ticketType] == ksr::kKSRegistrationUserTicket;
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   }
722   if (!(statbuf.st_mode & S_IXUSR)) {
723     return NO;
724   }
726   return YES;
729 - (BOOL)isSystemTicketDoomed {
730   BOOL isSystemTicket = ![self isUserTicket];
731   return isSystemTicket && ![self isSystemKeystone];
734 - (BOOL)isOnReadOnlyFilesystem {
735   const char* appPathC = [appPath_ fileSystemRepresentation];
736   struct statfs statfsBuf;
738   if (statfs(appPathC, &statfsBuf) != 0) {
739     PLOG(ERROR) << "statfs";
740     // Be optimistic about the filesystem's writability.
741     return NO;
742   }
744   return (statfsBuf.f_flags & MNT_RDONLY) != 0;
747 - (BOOL)needsPromotion {
748   // Don't promote when on a read-only filesystem.
749   if ([self isOnReadOnlyFilesystem]) {
750     return NO;
751   }
753   // Promotion is required when a system ticket is present but system Keystone
754   // is not.
755   if ([self isSystemTicketDoomed]) {
756     return YES;
757   }
759   // If on a system ticket and system Keystone is present, promotion is not
760   // required.
761   if (![self isUserTicket]) {
762     return NO;
763   }
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];
781 - (BOOL)wantsPromotion {
782   if ([self needsPromotion]) {
783     return YES;
784   }
786   // These are the same unpromotable cases as in -needsPromotion.
787   if ([self isOnReadOnlyFilesystem] || ![self isUserTicket]) {
788     return NO;
789   }
791   return [appPath_ hasPrefix:@"/Applications/"];
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   }
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   }
813   [self promoteTicketWithAuthorization:authorization.release() synchronous:NO];
816 - (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg
817                            synchronous:(BOOL)synchronous {
818   base::mac::ScopedAuthorizationRef authorization(authorization_arg);
819   authorization_arg = NULL;
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   }
833   synchronousPromotion_ = synchronous;
835   [self updateStatus:kAutoupdatePromoting version:nil];
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};
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   }
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);
899   NSDictionary* parameters = [self keystoneParameters];
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   }
911   if (![registration_ promoteWithParameters:parameters
912                               authorization:authorization_]) {
913     [self updateStatus:kAutoupdatePromoteFailed version:nil];
914     authorization_.reset();
915     return;
916   }
918   // Upon completion, ksr::KSRegistrationPromotionDidCompleteNotification will
919   // be posted, and -promotionComplete: will be called.
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   }
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   }
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   }
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]);
959   SEL selector = @selector(changePermissionsForPromotionWithTool:);
960   NSString* toolPath =
961       [base::mac::FrameworkBundle()
962           pathForResource:@"keystone_promote_postflight"
963                    ofType:@"sh"];
965   PerformBridge::PostPerform(self, selector, toolPath);
968 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath {
969   const char* toolPathC = [toolPath fileSystemRepresentation];
971   const char* appPathC = [appPath_ fileSystemRepresentation];
972   const char* arguments[] = {appPathC, NULL};
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   }
989   SEL selector = @selector(changePermissionsForPromotionComplete);
990   [self performSelectorOnMainThread:selector
991                          withObject:nil
992                       waitUntilDone:NO];
995 - (void)changePermissionsForPromotionComplete {
996   authorization_.reset();
998   [self updateStatus:kAutoupdatePromoted version:nil];
1001 - (void)setAppPath:(NSString*)appPath {
1002   if (appPath != appPath_) {
1003     [appPath_ release];
1004     appPath_ = [appPath copy];
1005   }
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   }
1023   NSString* wantFullInstallerVersion =
1024       [wantFullInstallerContents stringByTrimmingCharactersInSet:
1025           [NSCharacterSet newlineCharacterSet]];
1026   return [wantFullInstallerVersion isEqualToString:version_];
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;
1048 @end  // @implementation KeystoneGlue
1050 namespace {
1052 std::string BrandCodeInternal() {
1053   KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
1054   NSString* brand_path = [keystone_glue brandFilePath];
1056   if (![brand_path length])
1057     return std::string();
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];
1066   return std::string();
1069 }  // namespace
1071 namespace keystone_glue {
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;
1079 bool KeystoneEnabled() {
1080   return [KeystoneGlue defaultKeystoneGlue] != nil;
1083 base::string16 CurrentlyInstalledVersion() {
1084   KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
1085   NSString* version = [keystoneGlue currentlyInstalledVersion];
1086   return base::SysNSStringToUTF16(version);
1089 }  // namespace keystone_glue