Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / mac / keystone_glue.mm
blob61261a45f5e18e962cc4c2bf4e7c6026923e3be8
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/foundation_util.h"
19 #include "base/mac/mac_logging.h"
20 #include "base/mac/scoped_nsautorelease_pool.h"
21 #include "base/memory/ref_counted.h"
22 #include "base/strings/sys_string_conversions.h"
23 #include "base/threading/worker_pool.h"
24 #include "build/build_config.h"
25 #import "chrome/browser/mac/keystone_registration.h"
26 #include "chrome/browser/mac/obsolete_system.h"
27 #include "chrome/common/channel_info.h"
28 #include "chrome/common/chrome_constants.h"
29 #include "chrome/grit/chromium_strings.h"
30 #include "chrome/grit/generated_resources.h"
31 #include "components/version_info/version_info.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 // Set the registration active and pass profile count parameters.
123 - (void)setRegistrationActive;
125 // Called periodically to announce activity by pinging the Keystone server.
126 - (void)markActive:(NSTimer*)timer;
128 // Called when an update check or update installation is complete.  Posts the
129 // kAutoupdateStatusNotification notification to the default notification
130 // center.
131 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version;
133 // Returns the version of the currently-installed application on disk.
134 - (NSString*)currentlyInstalledVersion;
136 // These three methods are used to determine the version of the application
137 // currently installed on disk, compare that to the currently-running version,
138 // decide whether any updates have been installed, and call
139 // -updateStatus:version:.
141 // In order to check the version on disk, the installed application's
142 // Info.plist dictionary must be read; in order to see changes as updates are
143 // applied, the dictionary must be read each time, bypassing any caches such
144 // as the one that NSBundle might be maintaining.  Reading files can be a
145 // blocking operation, and blocking operations are to be avoided on the main
146 // thread.  I'm not quite sure what jank means, but I bet that a blocked main
147 // thread would cause some of it.
149 // -determineUpdateStatusAsync is called on the main thread to initiate the
150 // operation.  It performs initial set-up work that must be done on the main
151 // thread and arranges for -determineUpdateStatus to be called on a work queue
152 // thread managed by WorkerPool.
153 // -determineUpdateStatus then reads the Info.plist, gets the version from the
154 // CFBundleShortVersionString key, and performs
155 // -determineUpdateStatusForVersion: on the main thread.
156 // -determineUpdateStatusForVersion: does the actual comparison of the version
157 // on disk with the running version and calls -updateStatus:version: with the
158 // results of its analysis.
159 - (void)determineUpdateStatusAsync;
160 - (void)determineUpdateStatus;
161 - (void)determineUpdateStatusForVersion:(NSString*)version;
163 // Returns YES if registration_ is definitely on a user ticket.  If definitely
164 // on a system ticket, or uncertain of ticket type (due to an older version
165 // of Keystone being used), returns NO.
166 - (BOOL)isUserTicket;
168 // Returns YES if Keystone is definitely installed at the system level,
169 // determined by the presence of an executable ksadmin program at the expected
170 // system location.
171 - (BOOL)isSystemKeystone;
173 // Returns YES if on a system ticket but system Keystone is not present.
174 // Returns NO otherwise. The "doomed" condition will result in the
175 // registration framework appearing to have registered Chrome, but no updates
176 // ever actually taking place.
177 - (BOOL)isSystemTicketDoomed;
179 // Called when ticket promotion completes.
180 - (void)promotionComplete:(NSNotification*)notification;
182 // Changes the application's ownership and permissions so that all files are
183 // owned by root:wheel and all files and directories are writable only by
184 // root, but readable and executable as needed by everyone.
185 // -changePermissionsForPromotionAsync is called on the main thread by
186 // -promotionComplete.  That routine calls
187 // -changePermissionsForPromotionWithTool: on a work queue thread.  When done,
188 // -changePermissionsForPromotionComplete is called on the main thread.
189 - (void)changePermissionsForPromotionAsync;
190 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath;
191 - (void)changePermissionsForPromotionComplete;
193 // Returns the brand file path to use for Keystone.
194 - (NSString*)brandFilePath;
196 // YES if no update installation has succeeded since a binary diff patch
197 // installation failed. This signals the need to attempt a full installer
198 // which does not depend on applying a patch to existing files.
199 - (BOOL)wantsFullInstaller;
201 // Returns an NSString* suitable for appending to a Chrome Keystone tag value
202 // or tag key. If the system has a 32-bit-only CPU, the tag suffix will
203 // contain the string "-32bit". If a full installer (as opposed to a binary
204 // diff/delta patch) is required, the tag suffix will contain the string
205 // "-full". If no special treatment is required, the tag suffix will be an
206 // empty string.
207 - (NSString*)tagSuffix;
209 @end  // @interface KeystoneGlue (Private)
211 NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification";
212 NSString* const kAutoupdateStatusStatus = @"status";
213 NSString* const kAutoupdateStatusVersion = @"version";
215 namespace {
217 NSString* const kChannelKey = @"KSChannelID";
218 NSString* const kBrandKey = @"KSBrandID";
219 NSString* const kVersionKey = @"KSVersion";
221 }  // namespace
223 @implementation KeystoneGlue
225 + (id)defaultKeystoneGlue {
226   static bool sTriedCreatingDefaultKeystoneGlue = false;
227   // TODO(jrg): use base::SingletonObjC<KeystoneGlue>
228   static KeystoneGlue* sDefaultKeystoneGlue = nil;  // leaked
230   if (!sTriedCreatingDefaultKeystoneGlue) {
231     sTriedCreatingDefaultKeystoneGlue = true;
233     sDefaultKeystoneGlue = [[KeystoneGlue alloc] init];
234     [sDefaultKeystoneGlue loadParameters];
235     if (![sDefaultKeystoneGlue loadKeystoneRegistration]) {
236       [sDefaultKeystoneGlue release];
237       sDefaultKeystoneGlue = nil;
238     }
239   }
240   return sDefaultKeystoneGlue;
243 - (id)init {
244   if ((self = [super init])) {
245     NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
247     [center addObserver:self
248                selector:@selector(registrationComplete:)
249                    name:ksr::KSRegistrationDidCompleteNotification
250                  object:nil];
252     [center addObserver:self
253                selector:@selector(promotionComplete:)
254                    name:ksr::KSRegistrationPromotionDidCompleteNotification
255                  object:nil];
257     [center addObserver:self
258                selector:@selector(checkForUpdateComplete:)
259                    name:ksr::KSRegistrationCheckForUpdateNotification
260                  object:nil];
262     [center addObserver:self
263                selector:@selector(installUpdateComplete:)
264                    name:ksr::KSRegistrationStartUpdateNotification
265                  object:nil];
266   }
268   return self;
271 - (void)dealloc {
272   [productID_ release];
273   [appPath_ release];
274   [url_ release];
275   [version_ release];
276   [channel_ release];
277   [registration_ release];
278   [[NSNotificationCenter defaultCenter] removeObserver:self];
279   [super dealloc];
282 - (NSDictionary*)infoDictionary {
283   // Use base::mac::OuterBundle() to get the Chrome app's own bundle identifier
284   // and path, not the framework's.  For auto-update, the application is
285   // what's significant here: it's used to locate the outermost part of the
286   // application for the existence checker and other operations that need to
287   // see the entire application bundle.
288   return [base::mac::OuterBundle() infoDictionary];
291 - (void)loadParameters {
292   NSBundle* appBundle = base::mac::OuterBundle();
293   NSDictionary* infoDictionary = [self infoDictionary];
295   NSString* productID = [infoDictionary objectForKey:@"KSProductID"];
296   if (productID == nil) {
297     productID = [appBundle bundleIdentifier];
298   }
300   NSString* appPath = [appBundle bundlePath];
301   NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"];
302   NSString* version = [infoDictionary objectForKey:kVersionKey];
304   if (!productID || !appPath || !url || !version) {
305     // If parameters required for Keystone are missing, don't use it.
306     return;
307   }
309   NSString* channel = [infoDictionary objectForKey:kChannelKey];
310   // The stable channel has no tag.  If updating to stable, remove the
311   // dev and beta tags since we've been "promoted".
312   if (channel == nil)
313     channel = ksr::KSRegistrationRemoveExistingTag;
315   productID_ = [productID retain];
316   appPath_ = [appPath retain];
317   url_ = [url retain];
318   version_ = [version retain];
319   channel_ = [channel retain];
322 - (NSString*)brandFilePath {
323   DCHECK(version_ != nil) << "-loadParameters must be called first";
325   if (brandFileType_ == kBrandFileTypeNotDetermined) {
327     NSFileManager* fm = [NSFileManager defaultManager];
328     NSString* userBrandFile = UserBrandFilePath();
329     NSString* systemBrandFile = SystemBrandFilePath();
331     // Default to none.
332     brandFileType_ = kBrandFileTypeNone;
334     // Only the stable channel has a brand code.
335     version_info::Channel channel = chrome::GetChannel();
337     if (channel == version_info::Channel::DEV ||
338         channel == version_info::Channel::BETA) {
340       // If on the dev or beta channel, this installation may have replaced
341       // an older system-level installation. Check for a user brand file and
342       // nuke it if present. Don't try to remove the system brand file, there
343       // wouldn't be any permission to do so.
344       //
345       // Don't do this on the canary channel. The canary can run side-by-side
346       // with another Google Chrome installation whose brand code, if any,
347       // should remain intact.
349       if ([fm fileExistsAtPath:userBrandFile]) {
350         [fm removeItemAtPath:userBrandFile error:NULL];
351       }
353     } else if (channel == version_info::Channel::STABLE) {
355       // If there is a system brand file, use it.
356       if ([fm fileExistsAtPath:systemBrandFile]) {
357         // System
359         // Use the system file that is there.
360         brandFileType_ = kBrandFileTypeSystem;
362         // Clean up any old user level file.
363         if ([fm fileExistsAtPath:userBrandFile]) {
364           [fm removeItemAtPath:userBrandFile error:NULL];
365         }
367       } else {
368         // User
370         NSDictionary* infoDictionary = [self infoDictionary];
371         NSString* appBundleBrandID = [infoDictionary objectForKey:kBrandKey];
373         NSString* storedBrandID = nil;
374         if ([fm fileExistsAtPath:userBrandFile]) {
375           NSDictionary* storedBrandDict =
376               [NSDictionary dictionaryWithContentsOfFile:userBrandFile];
377           storedBrandID = [storedBrandDict objectForKey:kBrandKey];
378         }
380         if ((appBundleBrandID != nil) &&
381             (![storedBrandID isEqualTo:appBundleBrandID])) {
382           // App and store don't match, update store and use it.
383           NSDictionary* storedBrandDict =
384               [NSDictionary dictionaryWithObject:appBundleBrandID
385                                           forKey:kBrandKey];
386           // If Keystone hasn't been installed yet, the location the brand file
387           // is written to won't exist, so manually create the directory.
388           NSString *userBrandFileDirectory =
389               [userBrandFile stringByDeletingLastPathComponent];
390           if (![fm fileExistsAtPath:userBrandFileDirectory]) {
391             if (![fm createDirectoryAtPath:userBrandFileDirectory
392                withIntermediateDirectories:YES
393                                 attributes:nil
394                                      error:NULL]) {
395               LOG(ERROR) << "Failed to create the directory for the brand file";
396             }
397           }
398           if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) {
399             brandFileType_ = kBrandFileTypeUser;
400           }
401         } else if (storedBrandID) {
402           // Had stored brand, use it.
403           brandFileType_ = kBrandFileTypeUser;
404         }
405       }
406     }
408   }
410   NSString* result = nil;
411   switch (brandFileType_) {
412     case kBrandFileTypeUser:
413       result = UserBrandFilePath();
414       break;
416     case kBrandFileTypeSystem:
417       result = SystemBrandFilePath();
418       break;
420     case kBrandFileTypeNotDetermined:
421       NOTIMPLEMENTED();
422       // Fall through
423     case kBrandFileTypeNone:
424       // Clear the value.
425       result = @"";
426       break;
428   }
429   return result;
432 - (BOOL)loadKeystoneRegistration {
433   if (!productID_ || !appPath_ || !url_ || !version_)
434     return NO;
436   // Load the KeystoneRegistration framework bundle if present.  It lives
437   // inside the framework, so use base::mac::FrameworkBundle();
438   NSString* ksrPath =
439       [[base::mac::FrameworkBundle() privateFrameworksPath]
440           stringByAppendingPathComponent:@"KeystoneRegistration.framework"];
441   NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath];
442   [ksrBundle load];
444   // Harness the KSRegistration class.
445   Class ksrClass = [ksrBundle classNamed:@"KSRegistration"];
446   KSRegistration* ksr = [ksrClass registrationWithProductID:productID_];
447   if (!ksr)
448     return NO;
450   registration_ = [ksr retain];
451   ksUnsignedReportingAttributeClass_ =
452       [ksrBundle classNamed:@"KSUnsignedReportingAttribute"];
453   return YES;
456 - (NSString*)appInfoPlistPath {
457   // NSBundle ought to have a way to access this path directly, but it
458   // doesn't.
459   return [[appPath_ stringByAppendingPathComponent:@"Contents"]
460              stringByAppendingPathComponent:@"Info.plist"];
463 - (NSDictionary*)keystoneParameters {
464   NSNumber* xcType = [NSNumber numberWithInt:ksr::kKSPathExistenceChecker];
465   NSNumber* preserveTTToken = [NSNumber numberWithBool:YES];
466   NSString* appInfoPlistPath = [self appInfoPlistPath];
467   NSString* brandKey = kBrandKey;
468   NSString* brandPath = [self brandFilePath];
470   if ([brandPath length] == 0) {
471     // Brand path and brand key must be cleared together or ksadmin seems
472     // to throw an error.
473     brandKey = @"";
474   }
476   // Note that channel_ is permitted to be an empty string, but it must not be
477   // nil.
478   DCHECK(channel_);
479   NSString* tagSuffix = [self tagSuffix];
480   NSString* tagValue = [channel_ stringByAppendingString:tagSuffix];
481   NSString* tagKey = [kChannelKey stringByAppendingString:tagSuffix];
483   return [NSDictionary dictionaryWithObjectsAndKeys:
484              version_, ksr::KSRegistrationVersionKey,
485              appInfoPlistPath, ksr::KSRegistrationVersionPathKey,
486              kVersionKey, ksr::KSRegistrationVersionKeyKey,
487              xcType, ksr::KSRegistrationExistenceCheckerTypeKey,
488              appPath_, ksr::KSRegistrationExistenceCheckerStringKey,
489              url_, ksr::KSRegistrationServerURLStringKey,
490              preserveTTToken, ksr::KSRegistrationPreserveTrustedTesterTokenKey,
491              tagValue, ksr::KSRegistrationTagKey,
492              appInfoPlistPath, ksr::KSRegistrationTagPathKey,
493              tagKey, ksr::KSRegistrationTagKeyKey,
494              brandPath, ksr::KSRegistrationBrandPathKey,
495              brandKey, ksr::KSRegistrationBrandKeyKey,
496              nil];
499 - (void)setRegistrationActive {
500   if (!registration_)
501     return;
502   registrationActive_ = YES;
504   // Should never have zero profiles. Do not report this value.
505   if (!numProfiles_) {
506     [registration_ setActive];
507     return;
508   }
510   NSError* reportingError = nil;
512   KSReportingAttribute* numAccountsAttr =
513       [ksUnsignedReportingAttributeClass_
514           reportingAttributeWithValue:numProfiles_
515                                  name:@"_NumAccounts"
516                       aggregationType:kKSReportingAggregationSum
517                                 error:&reportingError];
518   if (reportingError != nil)
519     VLOG(1) << [reportingError localizedDescription];
520   reportingError = nil;
522   KSReportingAttribute* numSignedInAccountsAttr =
523       [ksUnsignedReportingAttributeClass_
524           reportingAttributeWithValue:numSignedInProfiles_
525                                  name:@"_NumSignedIn"
526                       aggregationType:kKSReportingAggregationSum
527                                 error:&reportingError];
528   if (reportingError != nil)
529     VLOG(1) << [reportingError localizedDescription];
530   reportingError = nil;
532   NSArray* profileCountsInformation =
533       [NSArray arrayWithObjects:numAccountsAttr, numSignedInAccountsAttr, nil];
535   if (![registration_ setActiveWithReportingAttributes:profileCountsInformation
536                                                  error:&reportingError]) {
537     VLOG(1) << [reportingError localizedDescription];
538   }
541 - (void)registerWithKeystone {
542   [self updateStatus:kAutoupdateRegistering version:nil];
544   NSDictionary* parameters = [self keystoneParameters];
545   BOOL result = [registration_ registerWithParameters:parameters];
546   if (!result) {
547     [self updateStatus:kAutoupdateRegisterFailed version:nil];
548     return;
549   }
551   // Upon completion, ksr::KSRegistrationDidCompleteNotification will be
552   // posted, and -registrationComplete: will be called.
554   // Set up hourly activity pings.
555   timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60  // One hour
556                                             target:self
557                                           selector:@selector(markActive:)
558                                           userInfo:nil
559                                            repeats:YES];
562 - (BOOL)isRegisteredAndActive {
563   return registrationActive_;
566 - (void)registrationComplete:(NSNotification*)notification {
567   NSDictionary* userInfo = [notification userInfo];
568   if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
569     if ([self isSystemTicketDoomed]) {
570       [self updateStatus:kAutoupdateNeedsPromotion version:nil];
571     } else {
572       [self updateStatus:kAutoupdateRegistered version:nil];
573     }
574   } else {
575     // Dump registration_?
576     [self updateStatus:kAutoupdateRegisterFailed version:nil];
577   }
580 - (void)stopTimer {
581   [timer_ invalidate];
584 - (void)markActive:(NSTimer*)timer {
585   [self setRegistrationActive];
588 - (void)checkForUpdate {
589   DCHECK(![self asyncOperationPending]);
591   if (!registration_) {
592     [self updateStatus:kAutoupdateCheckFailed version:nil];
593     return;
594   }
596   [self updateStatus:kAutoupdateChecking version:nil];
598   // All checks from inside Chrome are considered user-initiated, because they
599   // only happen following a user action, such as visiting the about page.
600   // Non-user-initiated checks are the periodic checks automatically made by
601   // Keystone, which don't come through this code path (or even this process).
602   [registration_ checkForUpdateWasUserInitiated:YES];
604   // Upon completion, ksr::KSRegistrationCheckForUpdateNotification will be
605   // posted, and -checkForUpdateComplete: will be called.
608 - (void)checkForUpdateComplete:(NSNotification*)notification {
609   NSDictionary* userInfo = [notification userInfo];
611   if ([[userInfo objectForKey:ksr::KSRegistrationUpdateCheckErrorKey]
612           boolValue]) {
613     [self updateStatus:kAutoupdateCheckFailed version:nil];
614   } else if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
615     // If an update is known to be available, go straight to
616     // -updateStatus:version:.  It doesn't matter what's currently on disk.
617     NSString* version = [userInfo objectForKey:ksr::KSRegistrationVersionKey];
618     [self updateStatus:kAutoupdateAvailable version:version];
619   } else {
620     // If no updates are available, check what's on disk, because an update
621     // may have already been installed.  This check happens on another thread,
622     // and -updateStatus:version: will be called on the main thread when done.
623     [self determineUpdateStatusAsync];
624   }
627 - (void)installUpdate {
628   DCHECK(![self asyncOperationPending]);
630   if (!registration_) {
631     [self updateStatus:kAutoupdateInstallFailed version:nil];
632     return;
633   }
635   [self updateStatus:kAutoupdateInstalling version:nil];
637   [registration_ startUpdate];
639   // Upon completion, ksr::KSRegistrationStartUpdateNotification will be
640   // posted, and -installUpdateComplete: will be called.
643 - (void)installUpdateComplete:(NSNotification*)notification {
644   NSDictionary* userInfo = [notification userInfo];
646   // http://crbug.com/160308 and b/7517358: when using system Keystone and on
647   // a user ticket, KSUpdateCheckSuccessfulKey will be NO even when an update
648   // was installed correctly, so don't check it. It should be redudnant when
649   // KSUpdateCheckSuccessfullyInstalledKey is checked.
650   if (![[userInfo objectForKey:ksr::KSUpdateCheckSuccessfullyInstalledKey]
651           intValue]) {
652     [self updateStatus:kAutoupdateInstallFailed version:nil];
653   } else {
654     updateSuccessfullyInstalled_ = YES;
656     // Nothing in the notification dictionary reports the version that was
657     // installed.  Figure it out based on what's on disk.
658     [self determineUpdateStatusAsync];
659   }
662 - (NSString*)currentlyInstalledVersion {
663   NSString* appInfoPlistPath = [self appInfoPlistPath];
664   NSDictionary* infoPlist =
665       [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath];
666   return [infoPlist objectForKey:@"CFBundleShortVersionString"];
669 // Runs on the main thread.
670 - (void)determineUpdateStatusAsync {
671   DCHECK([NSThread isMainThread]);
673   PerformBridge::PostPerform(self, @selector(determineUpdateStatus));
676 // Runs on a thread managed by WorkerPool.
677 - (void)determineUpdateStatus {
678   DCHECK(![NSThread isMainThread]);
680   NSString* version = [self currentlyInstalledVersion];
682   [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:)
683                          withObject:version
684                       waitUntilDone:NO];
687 // Runs on the main thread.
688 - (void)determineUpdateStatusForVersion:(NSString*)version {
689   DCHECK([NSThread isMainThread]);
691   AutoupdateStatus status;
692   if (updateSuccessfullyInstalled_) {
693     // If an update was successfully installed and this object saw it happen,
694     // then don't even bother comparing versions.
695     status = kAutoupdateInstalled;
696   } else {
697     NSString* currentVersion =
698         [NSString stringWithUTF8String:chrome::kChromeVersion];
699     if (!version) {
700       // If the version on disk could not be determined, assume that
701       // whatever's running is current.
702       version = currentVersion;
703       status = kAutoupdateCurrent;
704     } else if ([version isEqualToString:currentVersion]) {
705       status = kAutoupdateCurrent;
706     } else {
707       // If the version on disk doesn't match what's currently running, an
708       // update must have been applied in the background, without this app's
709       // direct participation.  Leave updateSuccessfullyInstalled_ alone
710       // because there's no direct knowledge of what actually happened.
711       status = kAutoupdateInstalled;
712     }
713   }
715   [self updateStatus:status version:version];
718 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version {
719   NSNumber* statusNumber = [NSNumber numberWithInt:status];
720   NSMutableDictionary* dictionary =
721       [NSMutableDictionary dictionaryWithObject:statusNumber
722                                          forKey:kAutoupdateStatusStatus];
723   if (version) {
724     [dictionary setObject:version forKey:kAutoupdateStatusVersion];
725   }
727   NSNotification* notification =
728       [NSNotification notificationWithName:kAutoupdateStatusNotification
729                                     object:self
730                                   userInfo:dictionary];
731   recentNotification_.reset([notification retain]);
733   [[NSNotificationCenter defaultCenter] postNotification:notification];
736 - (NSNotification*)recentNotification {
737   return [[recentNotification_ retain] autorelease];
740 - (AutoupdateStatus)recentStatus {
741   NSDictionary* dictionary = [recentNotification_ userInfo];
742   return static_cast<AutoupdateStatus>(
743       [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]);
746 - (BOOL)asyncOperationPending {
747   AutoupdateStatus status = [self recentStatus];
748   return status == kAutoupdateRegistering ||
749          status == kAutoupdateChecking ||
750          status == kAutoupdateInstalling ||
751          status == kAutoupdatePromoting;
754 - (BOOL)isUserTicket {
755   return [registration_ ticketType] == ksr::kKSRegistrationUserTicket;
758 - (BOOL)isSystemKeystone {
759   struct stat statbuf;
760   if (stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
761            "Contents/MacOS/ksadmin",
762            &statbuf) != 0) {
763     return NO;
764   }
766   if (!(statbuf.st_mode & S_IXUSR)) {
767     return NO;
768   }
770   return YES;
773 - (BOOL)isSystemTicketDoomed {
774   BOOL isSystemTicket = ![self isUserTicket];
775   return isSystemTicket && ![self isSystemKeystone];
778 - (BOOL)isOnReadOnlyFilesystem {
779   const char* appPathC = [appPath_ fileSystemRepresentation];
780   struct statfs statfsBuf;
782   if (statfs(appPathC, &statfsBuf) != 0) {
783     PLOG(ERROR) << "statfs";
784     // Be optimistic about the filesystem's writability.
785     return NO;
786   }
788   return (statfsBuf.f_flags & MNT_RDONLY) != 0;
791 - (BOOL)needsPromotion {
792   // Don't promote when on a read-only filesystem.
793   if ([self isOnReadOnlyFilesystem]) {
794     return NO;
795   }
797   // Promotion is required when a system ticket is present but system Keystone
798   // is not.
799   if ([self isSystemTicketDoomed]) {
800     return YES;
801   }
803   // If on a system ticket and system Keystone is present, promotion is not
804   // required.
805   if (![self isUserTicket]) {
806     return NO;
807   }
809   // Check the outermost bundle directory, the main executable path, and the
810   // framework directory.  It may be enough to just look at the outermost
811   // bundle directory, but checking an interior file and directory can be
812   // helpful in case permissions are set differently only on the outermost
813   // directory.  An interior file and directory are both checked because some
814   // file operations, such as Snow Leopard's Finder's copy operation when
815   // authenticating, may actually result in different ownership being applied
816   // to files and directories.
817   NSFileManager* fileManager = [NSFileManager defaultManager];
818   NSString* executablePath = [base::mac::OuterBundle() executablePath];
819   NSString* frameworkPath = [base::mac::FrameworkBundle() bundlePath];
820   return ![fileManager isWritableFileAtPath:appPath_] ||
821          ![fileManager isWritableFileAtPath:executablePath] ||
822          ![fileManager isWritableFileAtPath:frameworkPath];
825 - (BOOL)wantsPromotion {
826   if ([self needsPromotion]) {
827     return YES;
828   }
830   // These are the same unpromotable cases as in -needsPromotion.
831   if ([self isOnReadOnlyFilesystem] || ![self isUserTicket]) {
832     return NO;
833   }
835   return [appPath_ hasPrefix:@"/Applications/"];
838 - (void)promoteTicket {
839   if ([self asyncOperationPending] || ![self wantsPromotion]) {
840     // Because there are multiple ways of reaching promoteTicket that might
841     // not lock each other out, it may be possible to arrive here while an
842     // asynchronous operation is pending, or even after promotion has already
843     // occurred.  Just quietly return without doing anything.
844     return;
845   }
847   NSString* prompt = l10n_util::GetNSStringFWithFixup(
848       IDS_PROMOTE_AUTHENTICATION_PROMPT,
849       l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
850   base::mac::ScopedAuthorizationRef authorization(
851       base::mac::AuthorizationCreateToRunAsRoot(
852           base::mac::NSToCFCast(prompt)));
853   if (!authorization.get()) {
854     return;
855   }
857   [self promoteTicketWithAuthorization:authorization.release() synchronous:NO];
860 - (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg
861                            synchronous:(BOOL)synchronous {
862   base::mac::ScopedAuthorizationRef authorization(authorization_arg);
863   authorization_arg = NULL;
865   if ([self asyncOperationPending]) {
866     // Starting a synchronous operation while an asynchronous one is pending
867     // could be trouble.
868     return;
869   }
870   if (!synchronous && ![self wantsPromotion]) {
871     // If operating synchronously, the call came from the installer, which
872     // means that a system ticket is required.  Otherwise, only allow
873     // promotion if it's wanted.
874     return;
875   }
877   synchronousPromotion_ = synchronous;
879   [self updateStatus:kAutoupdatePromoting version:nil];
881   // TODO(mark): Remove when able!
882   //
883   // keystone_promote_preflight will copy the current brand information out to
884   // the system level so all users can share the data as part of the ticket
885   // promotion.
886   //
887   // It will also ensure that the Keystone system ticket store is in a usable
888   // state for all users on the system.  Ideally, Keystone's installer or
889   // another part of Keystone would handle this.  The underlying problem is
890   // http://b/2285921, and it causes http://b/2289908, which this workaround
891   // addresses.
892   //
893   // This is run synchronously, which isn't optimal, but
894   // -[KSRegistration promoteWithParameters:authorization:] is currently
895   // synchronous too, and this operation needs to happen before that one.
896   //
897   // TODO(mark): Make asynchronous.  That only makes sense if the promotion
898   // operation itself is asynchronous too.  http://b/2290009.  Hopefully,
899   // the Keystone promotion code will just be changed to do what preflight
900   // now does, and then the preflight script can be removed instead.
901   // However, preflight operation (and promotion) should only be asynchronous
902   // if the synchronous parameter is NO.
903   NSString* preflightPath =
904       [base::mac::FrameworkBundle()
905           pathForResource:@"keystone_promote_preflight"
906                    ofType:@"sh"];
907   const char* preflightPathC = [preflightPath fileSystemRepresentation];
908   const char* userBrandFile = NULL;
909   const char* systemBrandFile = NULL;
910   if (brandFileType_ == kBrandFileTypeUser) {
911     // Running with user level brand file, promote to the system level.
912     userBrandFile = [UserBrandFilePath() fileSystemRepresentation];
913     systemBrandFile = [SystemBrandFilePath() fileSystemRepresentation];
914   }
915   const char* arguments[] = {userBrandFile, systemBrandFile, NULL};
917   int exit_status;
918   OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
919       authorization,
920       preflightPathC,
921       kAuthorizationFlagDefaults,
922       arguments,
923       NULL,  // pipe
924       &exit_status);
925   if (status != errAuthorizationSuccess) {
926     OSSTATUS_LOG(ERROR, status)
927         << "AuthorizationExecuteWithPrivileges preflight";
928     [self updateStatus:kAutoupdatePromoteFailed version:nil];
929     return;
930   }
931   if (exit_status != 0) {
932     LOG(ERROR) << "keystone_promote_preflight status " << exit_status;
933     [self updateStatus:kAutoupdatePromoteFailed version:nil];
934     return;
935   }
937   // Hang on to the AuthorizationRef so that it can be used once promotion is
938   // complete.  Do this before asking Keystone to promote the ticket, because
939   // -promotionComplete: may be called from inside the Keystone promotion
940   // call.
941   authorization_.swap(authorization);
943   NSDictionary* parameters = [self keystoneParameters];
945   // If the brand file is user level, update parameters to point to the new
946   // system level file during promotion.
947   if (brandFileType_ == kBrandFileTypeUser) {
948     NSMutableDictionary* temp_parameters =
949         [[parameters mutableCopy] autorelease];
950     [temp_parameters setObject:SystemBrandFilePath()
951                         forKey:ksr::KSRegistrationBrandPathKey];
952     parameters = temp_parameters;
953   }
955   if (![registration_ promoteWithParameters:parameters
956                               authorization:authorization_]) {
957     [self updateStatus:kAutoupdatePromoteFailed version:nil];
958     authorization_.reset();
959     return;
960   }
962   // Upon completion, ksr::KSRegistrationPromotionDidCompleteNotification will
963   // be posted, and -promotionComplete: will be called.
965   // If synchronous, see to it that this happens immediately. Give it a
966   // 10-second deadline.
967   if (synchronous) {
968     CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
969   }
972 - (void)promotionComplete:(NSNotification*)notification {
973   NSDictionary* userInfo = [notification userInfo];
974   if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
975     if (synchronousPromotion_) {
976       // Short-circuit: if performing a synchronous promotion, the promotion
977       // came from the installer, which already set the permissions properly.
978       // Rather than run a duplicate permission-changing operation, jump
979       // straight to "done."
980       [self changePermissionsForPromotionComplete];
981     } else {
982       [self changePermissionsForPromotionAsync];
983     }
984   } else {
985     authorization_.reset();
986     [self updateStatus:kAutoupdatePromoteFailed version:nil];
987   }
989   if (synchronousPromotion_) {
990     // The run loop doesn't need to wait for this any longer.
991     CFRunLoopRef runLoop = CFRunLoopGetCurrent();
992     CFRunLoopStop(runLoop);
993     CFRunLoopWakeUp(runLoop);
994   }
997 - (void)changePermissionsForPromotionAsync {
998   // NSBundle is not documented as being thread-safe.  Do NSBundle operations
999   // on the main thread before jumping over to a WorkerPool-managed
1000   // thread to run the tool.
1001   DCHECK([NSThread isMainThread]);
1003   SEL selector = @selector(changePermissionsForPromotionWithTool:);
1004   NSString* toolPath =
1005       [base::mac::FrameworkBundle()
1006           pathForResource:@"keystone_promote_postflight"
1007                    ofType:@"sh"];
1009   PerformBridge::PostPerform(self, selector, toolPath);
1012 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath {
1013   const char* toolPathC = [toolPath fileSystemRepresentation];
1015   const char* appPathC = [appPath_ fileSystemRepresentation];
1016   const char* arguments[] = {appPathC, NULL};
1018   int exit_status;
1019   OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
1020       authorization_,
1021       toolPathC,
1022       kAuthorizationFlagDefaults,
1023       arguments,
1024       NULL,  // pipe
1025       &exit_status);
1026   if (status != errAuthorizationSuccess) {
1027     OSSTATUS_LOG(ERROR, status)
1028         << "AuthorizationExecuteWithPrivileges postflight";
1029   } else if (exit_status != 0) {
1030     LOG(ERROR) << "keystone_promote_postflight status " << exit_status;
1031   }
1033   SEL selector = @selector(changePermissionsForPromotionComplete);
1034   [self performSelectorOnMainThread:selector
1035                          withObject:nil
1036                       waitUntilDone:NO];
1039 - (void)changePermissionsForPromotionComplete {
1040   authorization_.reset();
1042   [self updateStatus:kAutoupdatePromoted version:nil];
1045 - (void)setAppPath:(NSString*)appPath {
1046   if (appPath != appPath_) {
1047     [appPath_ release];
1048     appPath_ = [appPath copy];
1049   }
1052 - (BOOL)wantsFullInstaller {
1053   // It's difficult to check the tag prior to Keystone registration, and
1054   // performing registration replaces the tag. keystone_install.sh
1055   // communicates a need for a full installer with Chrome in this file,
1056   // .want_full_installer.
1057   NSString* wantFullInstallerPath =
1058       [appPath_ stringByAppendingPathComponent:@".want_full_installer"];
1059   NSString* wantFullInstallerContents =
1060       [NSString stringWithContentsOfFile:wantFullInstallerPath
1061                                 encoding:NSUTF8StringEncoding
1062                                    error:NULL];
1063   if (!wantFullInstallerContents) {
1064     return NO;
1065   }
1067   NSString* wantFullInstallerVersion =
1068       [wantFullInstallerContents stringByTrimmingCharactersInSet:
1069           [NSCharacterSet newlineCharacterSet]];
1070   return [wantFullInstallerVersion isEqualToString:version_];
1073 - (NSString*)tagSuffix {
1074   // Tag suffix components are not entirely arbitrary: all possible tag keys
1075   // must be present in the application's Info.plist, there must be
1076   // server-side agreement on the processing and meaning of tag suffix
1077   // components, and other code that manipulates tag values (such as the
1078   // Keystone update installation script) must be tag suffix-aware. To reduce
1079   // the number of tag suffix combinations that need to be listed in
1080   // Info.plist, tag suffix components should only be appended to the tag
1081   // suffix in ASCII sort order.
1082   NSString* tagSuffix = @"";
1083   if (ObsoleteSystemMac::Has32BitOnlyCPU()) {
1084     tagSuffix = [tagSuffix stringByAppendingString:@"-32bit"];
1085   }
1086   if ([self wantsFullInstaller]) {
1087     tagSuffix = [tagSuffix stringByAppendingString:@"-full"];
1088   }
1089   return tagSuffix;
1093 - (void)updateProfileCountsWithNumProfiles:(uint32_t)profiles
1094                        numSignedInProfiles:(uint32_t)signedInProfiles {
1095   BOOL activate = numProfiles_ == 0;
1096   numProfiles_ = profiles;
1097   numSignedInProfiles_ = signedInProfiles;
1098   if (activate) {
1099     // During startup, numProfiles_ defaults to 0 so this is called when the
1100     // very first update to profile-counts is made.  http://crbug/487807
1101     [self setRegistrationActive];
1102   }
1105 @end  // @implementation KeystoneGlue
1107 namespace {
1109 std::string BrandCodeInternal() {
1110   KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
1111   NSString* brand_path = [keystone_glue brandFilePath];
1113   if (![brand_path length])
1114     return std::string();
1116   NSDictionary* dict =
1117       [NSDictionary dictionaryWithContentsOfFile:brand_path];
1118   NSString* brand_code =
1119       base::mac::ObjCCast<NSString>([dict objectForKey:kBrandKey]);
1120   if (brand_code)
1121     return [brand_code UTF8String];
1123   return std::string();
1126 }  // namespace
1128 namespace keystone_glue {
1130 std::string BrandCode() {
1131   // |s_brand_code| is leaked.
1132   static std::string* s_brand_code = new std::string(BrandCodeInternal());
1133   return *s_brand_code;
1136 bool KeystoneEnabled() {
1137   return [KeystoneGlue defaultKeystoneGlue] != nil;
1140 base::string16 CurrentlyInstalledVersion() {
1141   KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
1142   NSString* version = [keystoneGlue currentlyInstalledVersion];
1143   return base::SysNSStringToUTF16(version);
1146 }  // namespace keystone_glue