Check USB device path access when prompting users to select a device.
[chromium-blink-merge.git] / chrome / browser / mac / keystone_glue.mm
blob42b6b49e2c4b3db0135f78da0cad7ef3b8731d46
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/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"
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     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.
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 == chrome::VersionInfo::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;
503   // Should never have zero profiles. Do not report this value.
504   if (!numProfiles_) {
505     [registration_ setActive];
506     return;
507   }
509   NSError* reportingError = nil;
511   KSReportingAttribute* numAccountsAttr =
512       [ksUnsignedReportingAttributeClass_
513           reportingAttributeWithValue:numProfiles_
514                                  name:@"_NumAccounts"
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_
524                                  name:@"_NumSignedIn"
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];
537   }
540 - (void)registerWithKeystone {
541   [self updateStatus:kAutoupdateRegistering version:nil];
543   NSDictionary* parameters = [self keystoneParameters];
544   BOOL result;
545   {
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];
551   }
552   if (!result) {
553     [self updateStatus:kAutoupdateRegisterFailed version:nil];
554     return;
555   }
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
565                                             target:self
566                                           selector:@selector(markActive:)
567                                           userInfo:nil
568                                            repeats:YES];
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];
576     } else {
577       [self updateStatus:kAutoupdateRegistered version:nil];
578     }
579   } else {
580     // Dump registration_?
581     [self updateStatus:kAutoupdateRegisterFailed version:nil];
582   }
585 - (void)stopTimer {
586   [timer_ invalidate];
589 - (void)markActive:(NSTimer*)timer {
590   [self setRegistrationActive];
593 - (void)checkForUpdate {
594   DCHECK(![self asyncOperationPending]);
596   if (!registration_) {
597     [self updateStatus:kAutoupdateCheckFailed version:nil];
598     return;
599   }
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]
617           boolValue]) {
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];
624   } else {
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];
629   }
632 - (void)installUpdate {
633   DCHECK(![self asyncOperationPending]);
635   if (!registration_) {
636     [self updateStatus:kAutoupdateInstallFailed version:nil];
637     return;
638   }
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]
656           intValue]) {
657     [self updateStatus:kAutoupdateInstallFailed version:nil];
658   } else {
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];
664   }
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:)
688                          withObject:version
689                       waitUntilDone:NO];
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;
701   } else {
702     NSString* currentVersion =
703         [NSString stringWithUTF8String:chrome::kChromeVersion];
704     if (!version) {
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;
711     } else {
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;
717     }
718   }
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];
728   if (version) {
729     [dictionary setObject:version forKey:kAutoupdateStatusVersion];
730   }
732   NSNotification* notification =
733       [NSNotification notificationWithName:kAutoupdateStatusNotification
734                                     object:self
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 {
764   struct stat statbuf;
765   if (stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
766            "Contents/MacOS/ksadmin",
767            &statbuf) != 0) {
768     return NO;
769   }
771   if (!(statbuf.st_mode & S_IXUSR)) {
772     return NO;
773   }
775   return YES;
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.
790     return NO;
791   }
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]) {
799     return NO;
800   }
802   // Promotion is required when a system ticket is present but system Keystone
803   // is not.
804   if ([self isSystemTicketDoomed]) {
805     return YES;
806   }
808   // If on a system ticket and system Keystone is present, promotion is not
809   // required.
810   if (![self isUserTicket]) {
811     return NO;
812   }
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]) {
832     return YES;
833   }
835   // These are the same unpromotable cases as in -needsPromotion.
836   if ([self isOnReadOnlyFilesystem] || ![self isUserTicket]) {
837     return NO;
838   }
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.
849     return;
850   }
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()) {
859     return;
860   }
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
872     // could be trouble.
873     return;
874   }
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.
879     return;
880   }
882   synchronousPromotion_ = synchronous;
884   [self updateStatus:kAutoupdatePromoting version:nil];
886   // TODO(mark): Remove when able!
887   //
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
890   // promotion.
891   //
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
896   // addresses.
897   //
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.
901   //
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"
911                    ofType:@"sh"];
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];
919   }
920   const char* arguments[] = {userBrandFile, systemBrandFile, NULL};
922   int exit_status;
923   OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
924       authorization,
925       preflightPathC,
926       kAuthorizationFlagDefaults,
927       arguments,
928       NULL,  // pipe
929       &exit_status);
930   if (status != errAuthorizationSuccess) {
931     OSSTATUS_LOG(ERROR, status)
932         << "AuthorizationExecuteWithPrivileges preflight";
933     [self updateStatus:kAutoupdatePromoteFailed version:nil];
934     return;
935   }
936   if (exit_status != 0) {
937     LOG(ERROR) << "keystone_promote_preflight status " << exit_status;
938     [self updateStatus:kAutoupdatePromoteFailed version:nil];
939     return;
940   }
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
945   // call.
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;
958   }
960   if (![registration_ promoteWithParameters:parameters
961                               authorization:authorization_]) {
962     [self updateStatus:kAutoupdatePromoteFailed version:nil];
963     authorization_.reset();
964     return;
965   }
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.
972   if (synchronous) {
973     CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
974   }
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];
986     } else {
987       [self changePermissionsForPromotionAsync];
988     }
989   } else {
990     authorization_.reset();
991     [self updateStatus:kAutoupdatePromoteFailed version:nil];
992   }
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);
999   }
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"
1012                    ofType:@"sh"];
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};
1023   int exit_status;
1024   OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
1025       authorization_,
1026       toolPathC,
1027       kAuthorizationFlagDefaults,
1028       arguments,
1029       NULL,  // pipe
1030       &exit_status);
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;
1036   }
1038   SEL selector = @selector(changePermissionsForPromotionComplete);
1039   [self performSelectorOnMainThread:selector
1040                          withObject:nil
1041                       waitUntilDone:NO];
1044 - (void)changePermissionsForPromotionComplete {
1045   authorization_.reset();
1047   [self updateStatus:kAutoupdatePromoted version:nil];
1050 - (void)setAppPath:(NSString*)appPath {
1051   if (appPath != appPath_) {
1052     [appPath_ release];
1053     appPath_ = [appPath copy];
1054   }
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
1067                                    error:NULL];
1068   if (!wantFullInstallerContents) {
1069     return NO;
1070   }
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"];
1090   }
1091   if ([self wantsFullInstaller]) {
1092     tagSuffix = [tagSuffix stringByAppendingString:@"-full"];
1093   }
1094   return tagSuffix;
1098 - (void)updateProfileCountsWithNumProfiles:(uint32_t)profiles
1099                        numSignedInProfiles:(uint32_t)signedInProfiles {
1100   numProfiles_ = profiles;
1101   numSignedInProfiles_ = signedInProfiles;
1104 @end  // @implementation KeystoneGlue
1106 namespace {
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]);
1119   if (brand_code)
1120     return [brand_code UTF8String];
1122   return std::string();
1125 }  // namespace
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