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/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 "chrome/grit/chromium_strings.h"
31 #include "chrome/grit/generated_resources.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 chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
337 if (channel == chrome::VersionInfo::CHANNEL_DEV ||
338 channel == chrome::VersionInfo::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 == chrome::VersionInfo::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 {
503 // Should never have zero profiles. Do not report this value.
505 [registration_ setActive];
509 NSError* reportingError = nil;
511 KSReportingAttribute* numAccountsAttr =
512 [ksUnsignedReportingAttributeClass_
513 reportingAttributeWithValue:numProfiles_
515 aggregationType:kKSReportingAggregationSum
516 error:&reportingError];
517 if (reportingError != nil)
518 VLOG(1) << [reportingError localizedDescription];
519 reportingError = nil;
521 KSReportingAttribute* numSignedInAccountsAttr =
522 [ksUnsignedReportingAttributeClass_
523 reportingAttributeWithValue:numSignedInProfiles_
525 aggregationType:kKSReportingAggregationSum
526 error:&reportingError];
527 if (reportingError != nil)
528 VLOG(1) << [reportingError localizedDescription];
529 reportingError = nil;
531 NSArray* profileCountsInformation =
532 [NSArray arrayWithObjects:numAccountsAttr, numSignedInAccountsAttr, nil];
534 if (![registration_ setActiveWithReportingAttributes:profileCountsInformation
535 error:&reportingError]) {
536 VLOG(1) << [reportingError localizedDescription];
540 - (void)registerWithKeystone {
541 [self updateStatus:kAutoupdateRegistering version:nil];
543 NSDictionary* parameters = [self keystoneParameters];
546 // TODO(shess): Allows Keystone to throw an exception when
547 // /usr/bin/python does not exist (really!).
548 // http://crbug.com/86221 and http://crbug.com/87931
549 base::mac::ScopedNSExceptionEnabler enabler;
550 result = [registration_ registerWithParameters:parameters];
553 [self updateStatus:kAutoupdateRegisterFailed version:nil];
557 // Upon completion, ksr::KSRegistrationDidCompleteNotification will be
558 // posted, and -registrationComplete: will be called.
560 // Mark an active RIGHT NOW; don't wait an hour for the first one.
561 [self setRegistrationActive];
563 // Set up hourly activity pings.
564 timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60 // One hour
566 selector:@selector(markActive:)
571 - (void)registrationComplete:(NSNotification*)notification {
572 NSDictionary* userInfo = [notification userInfo];
573 if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
574 if ([self isSystemTicketDoomed]) {
575 [self updateStatus:kAutoupdateNeedsPromotion version:nil];
577 [self updateStatus:kAutoupdateRegistered version:nil];
580 // Dump registration_?
581 [self updateStatus:kAutoupdateRegisterFailed version:nil];
589 - (void)markActive:(NSTimer*)timer {
590 [self setRegistrationActive];
593 - (void)checkForUpdate {
594 DCHECK(![self asyncOperationPending]);
596 if (!registration_) {
597 [self updateStatus:kAutoupdateCheckFailed version:nil];
601 [self updateStatus:kAutoupdateChecking version:nil];
603 // All checks from inside Chrome are considered user-initiated, because they
604 // only happen following a user action, such as visiting the about page.
605 // Non-user-initiated checks are the periodic checks automatically made by
606 // Keystone, which don't come through this code path (or even this process).
607 [registration_ checkForUpdateWasUserInitiated:YES];
609 // Upon completion, ksr::KSRegistrationCheckForUpdateNotification will be
610 // posted, and -checkForUpdateComplete: will be called.
613 - (void)checkForUpdateComplete:(NSNotification*)notification {
614 NSDictionary* userInfo = [notification userInfo];
616 if ([[userInfo objectForKey:ksr::KSRegistrationUpdateCheckErrorKey]
618 [self updateStatus:kAutoupdateCheckFailed version:nil];
619 } else if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
620 // If an update is known to be available, go straight to
621 // -updateStatus:version:. It doesn't matter what's currently on disk.
622 NSString* version = [userInfo objectForKey:ksr::KSRegistrationVersionKey];
623 [self updateStatus:kAutoupdateAvailable version:version];
625 // If no updates are available, check what's on disk, because an update
626 // may have already been installed. This check happens on another thread,
627 // and -updateStatus:version: will be called on the main thread when done.
628 [self determineUpdateStatusAsync];
632 - (void)installUpdate {
633 DCHECK(![self asyncOperationPending]);
635 if (!registration_) {
636 [self updateStatus:kAutoupdateInstallFailed version:nil];
640 [self updateStatus:kAutoupdateInstalling version:nil];
642 [registration_ startUpdate];
644 // Upon completion, ksr::KSRegistrationStartUpdateNotification will be
645 // posted, and -installUpdateComplete: will be called.
648 - (void)installUpdateComplete:(NSNotification*)notification {
649 NSDictionary* userInfo = [notification userInfo];
651 // http://crbug.com/160308 and b/7517358: when using system Keystone and on
652 // a user ticket, KSUpdateCheckSuccessfulKey will be NO even when an update
653 // was installed correctly, so don't check it. It should be redudnant when
654 // KSUpdateCheckSuccessfullyInstalledKey is checked.
655 if (![[userInfo objectForKey:ksr::KSUpdateCheckSuccessfullyInstalledKey]
657 [self updateStatus:kAutoupdateInstallFailed version:nil];
659 updateSuccessfullyInstalled_ = YES;
661 // Nothing in the notification dictionary reports the version that was
662 // installed. Figure it out based on what's on disk.
663 [self determineUpdateStatusAsync];
667 - (NSString*)currentlyInstalledVersion {
668 NSString* appInfoPlistPath = [self appInfoPlistPath];
669 NSDictionary* infoPlist =
670 [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath];
671 return [infoPlist objectForKey:@"CFBundleShortVersionString"];
674 // Runs on the main thread.
675 - (void)determineUpdateStatusAsync {
676 DCHECK([NSThread isMainThread]);
678 PerformBridge::PostPerform(self, @selector(determineUpdateStatus));
681 // Runs on a thread managed by WorkerPool.
682 - (void)determineUpdateStatus {
683 DCHECK(![NSThread isMainThread]);
685 NSString* version = [self currentlyInstalledVersion];
687 [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:)
692 // Runs on the main thread.
693 - (void)determineUpdateStatusForVersion:(NSString*)version {
694 DCHECK([NSThread isMainThread]);
696 AutoupdateStatus status;
697 if (updateSuccessfullyInstalled_) {
698 // If an update was successfully installed and this object saw it happen,
699 // then don't even bother comparing versions.
700 status = kAutoupdateInstalled;
702 NSString* currentVersion =
703 [NSString stringWithUTF8String:chrome::kChromeVersion];
705 // If the version on disk could not be determined, assume that
706 // whatever's running is current.
707 version = currentVersion;
708 status = kAutoupdateCurrent;
709 } else if ([version isEqualToString:currentVersion]) {
710 status = kAutoupdateCurrent;
712 // If the version on disk doesn't match what's currently running, an
713 // update must have been applied in the background, without this app's
714 // direct participation. Leave updateSuccessfullyInstalled_ alone
715 // because there's no direct knowledge of what actually happened.
716 status = kAutoupdateInstalled;
720 [self updateStatus:status version:version];
723 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version {
724 NSNumber* statusNumber = [NSNumber numberWithInt:status];
725 NSMutableDictionary* dictionary =
726 [NSMutableDictionary dictionaryWithObject:statusNumber
727 forKey:kAutoupdateStatusStatus];
729 [dictionary setObject:version forKey:kAutoupdateStatusVersion];
732 NSNotification* notification =
733 [NSNotification notificationWithName:kAutoupdateStatusNotification
735 userInfo:dictionary];
736 recentNotification_.reset([notification retain]);
738 [[NSNotificationCenter defaultCenter] postNotification:notification];
741 - (NSNotification*)recentNotification {
742 return [[recentNotification_ retain] autorelease];
745 - (AutoupdateStatus)recentStatus {
746 NSDictionary* dictionary = [recentNotification_ userInfo];
747 return static_cast<AutoupdateStatus>(
748 [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]);
751 - (BOOL)asyncOperationPending {
752 AutoupdateStatus status = [self recentStatus];
753 return status == kAutoupdateRegistering ||
754 status == kAutoupdateChecking ||
755 status == kAutoupdateInstalling ||
756 status == kAutoupdatePromoting;
759 - (BOOL)isUserTicket {
760 return [registration_ ticketType] == ksr::kKSRegistrationUserTicket;
763 - (BOOL)isSystemKeystone {
765 if (stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
766 "Contents/MacOS/ksadmin",
771 if (!(statbuf.st_mode & S_IXUSR)) {
778 - (BOOL)isSystemTicketDoomed {
779 BOOL isSystemTicket = ![self isUserTicket];
780 return isSystemTicket && ![self isSystemKeystone];
783 - (BOOL)isOnReadOnlyFilesystem {
784 const char* appPathC = [appPath_ fileSystemRepresentation];
785 struct statfs statfsBuf;
787 if (statfs(appPathC, &statfsBuf) != 0) {
788 PLOG(ERROR) << "statfs";
789 // Be optimistic about the filesystem's writability.
793 return (statfsBuf.f_flags & MNT_RDONLY) != 0;
796 - (BOOL)needsPromotion {
797 // Don't promote when on a read-only filesystem.
798 if ([self isOnReadOnlyFilesystem]) {
802 // Promotion is required when a system ticket is present but system Keystone
804 if ([self isSystemTicketDoomed]) {
808 // If on a system ticket and system Keystone is present, promotion is not
810 if (![self isUserTicket]) {
814 // Check the outermost bundle directory, the main executable path, and the
815 // framework directory. It may be enough to just look at the outermost
816 // bundle directory, but checking an interior file and directory can be
817 // helpful in case permissions are set differently only on the outermost
818 // directory. An interior file and directory are both checked because some
819 // file operations, such as Snow Leopard's Finder's copy operation when
820 // authenticating, may actually result in different ownership being applied
821 // to files and directories.
822 NSFileManager* fileManager = [NSFileManager defaultManager];
823 NSString* executablePath = [base::mac::OuterBundle() executablePath];
824 NSString* frameworkPath = [base::mac::FrameworkBundle() bundlePath];
825 return ![fileManager isWritableFileAtPath:appPath_] ||
826 ![fileManager isWritableFileAtPath:executablePath] ||
827 ![fileManager isWritableFileAtPath:frameworkPath];
830 - (BOOL)wantsPromotion {
831 if ([self needsPromotion]) {
835 // These are the same unpromotable cases as in -needsPromotion.
836 if ([self isOnReadOnlyFilesystem] || ![self isUserTicket]) {
840 return [appPath_ hasPrefix:@"/Applications/"];
843 - (void)promoteTicket {
844 if ([self asyncOperationPending] || ![self wantsPromotion]) {
845 // Because there are multiple ways of reaching promoteTicket that might
846 // not lock each other out, it may be possible to arrive here while an
847 // asynchronous operation is pending, or even after promotion has already
848 // occurred. Just quietly return without doing anything.
852 NSString* prompt = l10n_util::GetNSStringFWithFixup(
853 IDS_PROMOTE_AUTHENTICATION_PROMPT,
854 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
855 base::mac::ScopedAuthorizationRef authorization(
856 base::mac::AuthorizationCreateToRunAsRoot(
857 base::mac::NSToCFCast(prompt)));
858 if (!authorization.get()) {
862 [self promoteTicketWithAuthorization:authorization.release() synchronous:NO];
865 - (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg
866 synchronous:(BOOL)synchronous {
867 base::mac::ScopedAuthorizationRef authorization(authorization_arg);
868 authorization_arg = NULL;
870 if ([self asyncOperationPending]) {
871 // Starting a synchronous operation while an asynchronous one is pending
875 if (!synchronous && ![self wantsPromotion]) {
876 // If operating synchronously, the call came from the installer, which
877 // means that a system ticket is required. Otherwise, only allow
878 // promotion if it's wanted.
882 synchronousPromotion_ = synchronous;
884 [self updateStatus:kAutoupdatePromoting version:nil];
886 // TODO(mark): Remove when able!
888 // keystone_promote_preflight will copy the current brand information out to
889 // the system level so all users can share the data as part of the ticket
892 // It will also ensure that the Keystone system ticket store is in a usable
893 // state for all users on the system. Ideally, Keystone's installer or
894 // another part of Keystone would handle this. The underlying problem is
895 // http://b/2285921, and it causes http://b/2289908, which this workaround
898 // This is run synchronously, which isn't optimal, but
899 // -[KSRegistration promoteWithParameters:authorization:] is currently
900 // synchronous too, and this operation needs to happen before that one.
902 // TODO(mark): Make asynchronous. That only makes sense if the promotion
903 // operation itself is asynchronous too. http://b/2290009. Hopefully,
904 // the Keystone promotion code will just be changed to do what preflight
905 // now does, and then the preflight script can be removed instead.
906 // However, preflight operation (and promotion) should only be asynchronous
907 // if the synchronous parameter is NO.
908 NSString* preflightPath =
909 [base::mac::FrameworkBundle()
910 pathForResource:@"keystone_promote_preflight"
912 const char* preflightPathC = [preflightPath fileSystemRepresentation];
913 const char* userBrandFile = NULL;
914 const char* systemBrandFile = NULL;
915 if (brandFileType_ == kBrandFileTypeUser) {
916 // Running with user level brand file, promote to the system level.
917 userBrandFile = [UserBrandFilePath() fileSystemRepresentation];
918 systemBrandFile = [SystemBrandFilePath() fileSystemRepresentation];
920 const char* arguments[] = {userBrandFile, systemBrandFile, NULL};
923 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
926 kAuthorizationFlagDefaults,
930 if (status != errAuthorizationSuccess) {
931 OSSTATUS_LOG(ERROR, status)
932 << "AuthorizationExecuteWithPrivileges preflight";
933 [self updateStatus:kAutoupdatePromoteFailed version:nil];
936 if (exit_status != 0) {
937 LOG(ERROR) << "keystone_promote_preflight status " << exit_status;
938 [self updateStatus:kAutoupdatePromoteFailed version:nil];
942 // Hang on to the AuthorizationRef so that it can be used once promotion is
943 // complete. Do this before asking Keystone to promote the ticket, because
944 // -promotionComplete: may be called from inside the Keystone promotion
946 authorization_.swap(authorization);
948 NSDictionary* parameters = [self keystoneParameters];
950 // If the brand file is user level, update parameters to point to the new
951 // system level file during promotion.
952 if (brandFileType_ == kBrandFileTypeUser) {
953 NSMutableDictionary* temp_parameters =
954 [[parameters mutableCopy] autorelease];
955 [temp_parameters setObject:SystemBrandFilePath()
956 forKey:ksr::KSRegistrationBrandPathKey];
957 parameters = temp_parameters;
960 if (![registration_ promoteWithParameters:parameters
961 authorization:authorization_]) {
962 [self updateStatus:kAutoupdatePromoteFailed version:nil];
963 authorization_.reset();
967 // Upon completion, ksr::KSRegistrationPromotionDidCompleteNotification will
968 // be posted, and -promotionComplete: will be called.
970 // If synchronous, see to it that this happens immediately. Give it a
971 // 10-second deadline.
973 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
977 - (void)promotionComplete:(NSNotification*)notification {
978 NSDictionary* userInfo = [notification userInfo];
979 if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
980 if (synchronousPromotion_) {
981 // Short-circuit: if performing a synchronous promotion, the promotion
982 // came from the installer, which already set the permissions properly.
983 // Rather than run a duplicate permission-changing operation, jump
984 // straight to "done."
985 [self changePermissionsForPromotionComplete];
987 [self changePermissionsForPromotionAsync];
990 authorization_.reset();
991 [self updateStatus:kAutoupdatePromoteFailed version:nil];
994 if (synchronousPromotion_) {
995 // The run loop doesn't need to wait for this any longer.
996 CFRunLoopRef runLoop = CFRunLoopGetCurrent();
997 CFRunLoopStop(runLoop);
998 CFRunLoopWakeUp(runLoop);
1002 - (void)changePermissionsForPromotionAsync {
1003 // NSBundle is not documented as being thread-safe. Do NSBundle operations
1004 // on the main thread before jumping over to a WorkerPool-managed
1005 // thread to run the tool.
1006 DCHECK([NSThread isMainThread]);
1008 SEL selector = @selector(changePermissionsForPromotionWithTool:);
1009 NSString* toolPath =
1010 [base::mac::FrameworkBundle()
1011 pathForResource:@"keystone_promote_postflight"
1014 PerformBridge::PostPerform(self, selector, toolPath);
1017 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath {
1018 const char* toolPathC = [toolPath fileSystemRepresentation];
1020 const char* appPathC = [appPath_ fileSystemRepresentation];
1021 const char* arguments[] = {appPathC, NULL};
1024 OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
1027 kAuthorizationFlagDefaults,
1031 if (status != errAuthorizationSuccess) {
1032 OSSTATUS_LOG(ERROR, status)
1033 << "AuthorizationExecuteWithPrivileges postflight";
1034 } else if (exit_status != 0) {
1035 LOG(ERROR) << "keystone_promote_postflight status " << exit_status;
1038 SEL selector = @selector(changePermissionsForPromotionComplete);
1039 [self performSelectorOnMainThread:selector
1044 - (void)changePermissionsForPromotionComplete {
1045 authorization_.reset();
1047 [self updateStatus:kAutoupdatePromoted version:nil];
1050 - (void)setAppPath:(NSString*)appPath {
1051 if (appPath != appPath_) {
1053 appPath_ = [appPath copy];
1057 - (BOOL)wantsFullInstaller {
1058 // It's difficult to check the tag prior to Keystone registration, and
1059 // performing registration replaces the tag. keystone_install.sh
1060 // communicates a need for a full installer with Chrome in this file,
1061 // .want_full_installer.
1062 NSString* wantFullInstallerPath =
1063 [appPath_ stringByAppendingPathComponent:@".want_full_installer"];
1064 NSString* wantFullInstallerContents =
1065 [NSString stringWithContentsOfFile:wantFullInstallerPath
1066 encoding:NSUTF8StringEncoding
1068 if (!wantFullInstallerContents) {
1072 NSString* wantFullInstallerVersion =
1073 [wantFullInstallerContents stringByTrimmingCharactersInSet:
1074 [NSCharacterSet newlineCharacterSet]];
1075 return [wantFullInstallerVersion isEqualToString:version_];
1078 - (NSString*)tagSuffix {
1079 // Tag suffix components are not entirely arbitrary: all possible tag keys
1080 // must be present in the application's Info.plist, there must be
1081 // server-side agreement on the processing and meaning of tag suffix
1082 // components, and other code that manipulates tag values (such as the
1083 // Keystone update installation script) must be tag suffix-aware. To reduce
1084 // the number of tag suffix combinations that need to be listed in
1085 // Info.plist, tag suffix components should only be appended to the tag
1086 // suffix in ASCII sort order.
1087 NSString* tagSuffix = @"";
1088 if (ObsoleteSystemMac::Has32BitOnlyCPU()) {
1089 tagSuffix = [tagSuffix stringByAppendingString:@"-32bit"];
1091 if ([self wantsFullInstaller]) {
1092 tagSuffix = [tagSuffix stringByAppendingString:@"-full"];
1098 - (void)updateProfileCountsWithNumProfiles:(uint32_t)profiles
1099 numSignedInProfiles:(uint32_t)signedInProfiles {
1100 numProfiles_ = profiles;
1101 numSignedInProfiles_ = signedInProfiles;
1104 @end // @implementation KeystoneGlue
1108 std::string BrandCodeInternal() {
1109 KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
1110 NSString* brand_path = [keystone_glue brandFilePath];
1112 if (![brand_path length])
1113 return std::string();
1115 NSDictionary* dict =
1116 [NSDictionary dictionaryWithContentsOfFile:brand_path];
1117 NSString* brand_code =
1118 base::mac::ObjCCast<NSString>([dict objectForKey:kBrandKey]);
1120 return [brand_code UTF8String];
1122 return std::string();
1127 namespace keystone_glue {
1129 std::string BrandCode() {
1130 // |s_brand_code| is leaked.
1131 static std::string* s_brand_code = new std::string(BrandCodeInternal());
1132 return *s_brand_code;
1135 bool KeystoneEnabled() {
1136 return [KeystoneGlue defaultKeystoneGlue] != nil;
1139 base::string16 CurrentlyInstalledVersion() {
1140 KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
1141 NSString* version = [keystoneGlue currentlyInstalledVersion];
1142 return base::SysNSStringToUTF16(version);
1145 } // namespace keystone_glue