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"
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"
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";
47 #error Unknown branding
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|
65 class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> {
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) {
74 scoped_refptr<PerformBridge> op = new PerformBridge(target, sel, arg);
75 base::WorkerPool::PostTask(
76 FROM_HERE, base::Bind(&PerformBridge::Run, op.get()), true);
79 // Convenience for the no-argument case.
80 static void PostPerform(id target, SEL sel) {
81 PostPerform(target, sel, nil);
85 // Allow RefCountedThreadSafe<> to delete.
86 friend class base::RefCountedThreadSafe<PerformBridge>;
88 PerformBridge(id target, SEL sel, id arg)
89 : target_([target retain]),
96 // Happens on a WorkerPool thread.
98 base::mac::ScopedNSAutoreleasePool pool;
99 [target_ performSelector:sel_ withObject:arg_];
102 base::scoped_nsobject<id> target_;
104 base::scoped_nsobject<id> arg_;
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
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
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
207 - (NSString*)tagSuffix;
209 @end // @interface KeystoneGlue (Private)
211 NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification";
212 NSString* const kAutoupdateStatusStatus = @"status";
213 NSString* const kAutoupdateStatusVersion = @"version";
217 NSString* const kChannelKey = @"KSChannelID";
218 NSString* const kBrandKey = @"KSBrandID";
219 NSString* const kVersionKey = @"KSVersion";
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;
240 return sDefaultKeystoneGlue;
244 if ((self = [super init])) {
245 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
247 [center addObserver:self
248 selector:@selector(registrationComplete:)
249 name:ksr::KSRegistrationDidCompleteNotification
252 [center addObserver:self
253 selector:@selector(promotionComplete:)
254 name:ksr::KSRegistrationPromotionDidCompleteNotification
257 [center addObserver:self
258 selector:@selector(checkForUpdateComplete:)
259 name:ksr::KSRegistrationCheckForUpdateNotification
262 [center addObserver:self
263 selector:@selector(installUpdateComplete:)
264 name:ksr::KSRegistrationStartUpdateNotification
272 [productID_ release];
277 [registration_ release];
278 [[NSNotificationCenter defaultCenter] removeObserver:self];
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];
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.
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".
313 channel = ksr::KSRegistrationRemoveExistingTag;
315 productID_ = [productID retain];
316 appPath_ = [appPath 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();
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.
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];
353 } else if (channel == version_info::Channel::STABLE) {
355 // If there is a system brand file, use it.
356 if ([fm fileExistsAtPath:systemBrandFile]) {
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];
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];
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
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
395 LOG(ERROR) << "Failed to create the directory for the brand file";
398 if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) {
399 brandFileType_ = kBrandFileTypeUser;
401 } else if (storedBrandID) {
402 // Had stored brand, use it.
403 brandFileType_ = kBrandFileTypeUser;
410 NSString* result = nil;
411 switch (brandFileType_) {
412 case kBrandFileTypeUser:
413 result = UserBrandFilePath();
416 case kBrandFileTypeSystem:
417 result = SystemBrandFilePath();
420 case kBrandFileTypeNotDetermined:
423 case kBrandFileTypeNone:
432 - (BOOL)loadKeystoneRegistration {
433 if (!productID_ || !appPath_ || !url_ || !version_)
436 // Load the KeystoneRegistration framework bundle if present. It lives
437 // inside the framework, so use base::mac::FrameworkBundle();
439 [[base::mac::FrameworkBundle() privateFrameworksPath]
440 stringByAppendingPathComponent:@"KeystoneRegistration.framework"];
441 NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath];
444 // Harness the KSRegistration class.
445 Class ksrClass = [ksrBundle classNamed:@"KSRegistration"];
446 KSRegistration* ksr = [ksrClass registrationWithProductID:productID_];
450 registration_ = [ksr retain];
451 ksUnsignedReportingAttributeClass_ =
452 [ksrBundle classNamed:@"KSUnsignedReportingAttribute"];
456 - (NSString*)appInfoPlistPath {
457 // NSBundle ought to have a way to access this path directly, but it
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.
476 // Note that channel_ is permitted to be an empty string, but it must not be
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,
499 - (void)setRegistrationActive {
502 registrationActive_ = YES;
504 // Should never have zero profiles. Do not report this value.
506 [registration_ setActive];
510 NSError* reportingError = nil;
512 KSReportingAttribute* numAccountsAttr =
513 [ksUnsignedReportingAttributeClass_
514 reportingAttributeWithValue:numProfiles_
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_
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];
541 - (void)registerWithKeystone {
542 [self updateStatus:kAutoupdateRegistering version:nil];
544 NSDictionary* parameters = [self keystoneParameters];
545 BOOL result = [registration_ registerWithParameters:parameters];
547 [self updateStatus:kAutoupdateRegisterFailed version:nil];
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
557 selector:@selector(markActive:)
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];
572 [self updateStatus:kAutoupdateRegistered version:nil];
575 // Dump registration_?
576 [self updateStatus:kAutoupdateRegisterFailed version:nil];
584 - (void)markActive:(NSTimer*)timer {
585 [self setRegistrationActive];
588 - (void)checkForUpdate {
589 DCHECK(![self asyncOperationPending]);
591 if (!registration_) {
592 [self updateStatus:kAutoupdateCheckFailed version:nil];
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]
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];
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];
627 - (void)installUpdate {
628 DCHECK(![self asyncOperationPending]);
630 if (!registration_) {
631 [self updateStatus:kAutoupdateInstallFailed version:nil];
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]
652 [self updateStatus:kAutoupdateInstallFailed version:nil];
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];
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:)
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;
697 NSString* currentVersion =
698 [NSString stringWithUTF8String:chrome::kChromeVersion];
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;
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;
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];
724 [dictionary setObject:version forKey:kAutoupdateStatusVersion];
727 NSNotification* notification =
728 [NSNotification notificationWithName:kAutoupdateStatusNotification
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 {
760 if (stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
761 "Contents/MacOS/ksadmin",
766 if (!(statbuf.st_mode & S_IXUSR)) {
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.
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]) {
797 // Promotion is required when a system ticket is present but system Keystone
799 if ([self isSystemTicketDoomed]) {
803 // If on a system ticket and system Keystone is present, promotion is not
805 if (![self isUserTicket]) {
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]) {
830 // These are the same unpromotable cases as in -needsPromotion.
831 if ([self isOnReadOnlyFilesystem] || ![self isUserTicket]) {
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.
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()) {
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
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.
877 synchronousPromotion_ = synchronous;
879 [self updateStatus:kAutoupdatePromoting version:nil];
881 // TODO(mark): Remove when able!
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
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
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.
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"
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];
915 const char* arguments[] = {userBrandFile, systemBrandFile, NULL};
918 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
921 kAuthorizationFlagDefaults,
925 if (status != errAuthorizationSuccess) {
926 OSSTATUS_LOG(ERROR, status)
927 << "AuthorizationExecuteWithPrivileges preflight";
928 [self updateStatus:kAutoupdatePromoteFailed version:nil];
931 if (exit_status != 0) {
932 LOG(ERROR) << "keystone_promote_preflight status " << exit_status;
933 [self updateStatus:kAutoupdatePromoteFailed version:nil];
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
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;
955 if (![registration_ promoteWithParameters:parameters
956 authorization:authorization_]) {
957 [self updateStatus:kAutoupdatePromoteFailed version:nil];
958 authorization_.reset();
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.
968 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
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];
982 [self changePermissionsForPromotionAsync];
985 authorization_.reset();
986 [self updateStatus:kAutoupdatePromoteFailed version:nil];
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);
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"
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};
1019 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
1022 kAuthorizationFlagDefaults,
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;
1033 SEL selector = @selector(changePermissionsForPromotionComplete);
1034 [self performSelectorOnMainThread:selector
1039 - (void)changePermissionsForPromotionComplete {
1040 authorization_.reset();
1042 [self updateStatus:kAutoupdatePromoted version:nil];
1045 - (void)setAppPath:(NSString*)appPath {
1046 if (appPath != appPath_) {
1048 appPath_ = [appPath copy];
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
1063 if (!wantFullInstallerContents) {
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"];
1086 if ([self wantsFullInstaller]) {
1087 tagSuffix = [tagSuffix stringByAppendingString:@"-full"];
1093 - (void)updateProfileCountsWithNumProfiles:(uint32_t)profiles
1094 numSignedInProfiles:(uint32_t)signedInProfiles {
1095 BOOL activate = numProfiles_ == 0;
1096 numProfiles_ = profiles;
1097 numSignedInProfiles_ = signedInProfiles;
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];
1105 @end // @implementation KeystoneGlue
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]);
1121 return [brand_code UTF8String];
1123 return std::string();
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