- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / mac / keystone_glue.mm
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.
4
5 #import "chrome/browser/mac/keystone_glue.h"
6
7 #include <sys/mount.h>
8 #include <sys/param.h>
9 #include <sys/stat.h>
10
11 #include <vector>
12
13 #include "base/bind.h"
14 #include "base/location.h"
15 #include "base/logging.h"
16 #include "base/mac/authorization_util.h"
17 #include "base/mac/bundle_locations.h"
18 #include "base/mac/mac_logging.h"
19 #include "base/mac/mac_util.h"
20 #include "base/mac/scoped_nsautorelease_pool.h"
21 #include "base/mac/scoped_nsexception_enabler.h"
22 #include "base/memory/ref_counted.h"
23 #include "base/strings/sys_string_conversions.h"
24 #include "base/threading/worker_pool.h"
25 #import "chrome/browser/mac/keystone_registration.h"
26 #include "chrome/common/chrome_constants.h"
27 #include "chrome/common/chrome_version_info.h"
28 #include "grit/chromium_strings.h"
29 #include "grit/generated_resources.h"
30 #include "ui/base/l10n/l10n_util.h"
31 #include "ui/base/l10n/l10n_util_mac.h"
32
33 namespace {
34
35 namespace ksr = keystone_registration;
36
37 // Constants for the brand file (uses an external file so it can survive
38 // updates to Chrome.)
39
40 #if defined(GOOGLE_CHROME_BUILD)
41 #define kBrandFileName @"Google Chrome Brand.plist";
42 #elif defined(CHROMIUM_BUILD)
43 #define kBrandFileName @"Chromium Brand.plist";
44 #else
45 #error Unknown branding
46 #endif
47
48 // These directories are hardcoded in Keystone promotion preflight and the
49 // Keystone install script, so NSSearchPathForDirectoriesInDomains isn't used
50 // since the scripts couldn't use anything like that.
51 NSString* kBrandUserFile = @"~/Library/Google/" kBrandFileName;
52 NSString* kBrandSystemFile = @"/Library/Google/" kBrandFileName;
53
54 NSString* UserBrandFilePath() {
55   return [kBrandUserFile stringByStandardizingPath];
56 }
57 NSString* SystemBrandFilePath() {
58   return [kBrandSystemFile stringByStandardizingPath];
59 }
60
61 // Adaptor for scheduling an Objective-C method call on a |WorkerPool|
62 // thread.
63 class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> {
64  public:
65
66   // Call |sel| on |target| with |arg| in a WorkerPool thread.
67   // |target| and |arg| are retained, |arg| may be |nil|.
68   static void PostPerform(id target, SEL sel, id arg) {
69     DCHECK(target);
70     DCHECK(sel);
71
72     scoped_refptr<PerformBridge> op = new PerformBridge(target, sel, arg);
73     base::WorkerPool::PostTask(
74         FROM_HERE, base::Bind(&PerformBridge::Run, op.get()), true);
75   }
76
77   // Convenience for the no-argument case.
78   static void PostPerform(id target, SEL sel) {
79     PostPerform(target, sel, nil);
80   }
81
82  private:
83   // Allow RefCountedThreadSafe<> to delete.
84   friend class base::RefCountedThreadSafe<PerformBridge>;
85
86   PerformBridge(id target, SEL sel, id arg)
87       : target_([target retain]),
88         sel_(sel),
89         arg_([arg retain]) {
90   }
91
92   ~PerformBridge() {}
93
94   // Happens on a WorkerPool thread.
95   void Run() {
96     base::mac::ScopedNSAutoreleasePool pool;
97     [target_ performSelector:sel_ withObject:arg_];
98   }
99
100   base::scoped_nsobject<id> target_;
101   SEL sel_;
102   base::scoped_nsobject<id> arg_;
103 };
104
105 }  // namespace
106
107 @interface KeystoneGlue (Private)
108
109 // Returns the path to the application's Info.plist file.  This returns the
110 // outer application bundle's Info.plist, not the framework's Info.plist.
111 - (NSString*)appInfoPlistPath;
112
113 // Returns a dictionary containing parameters to be used for a KSRegistration
114 // -registerWithParameters: or -promoteWithParameters:authorization: call.
115 - (NSDictionary*)keystoneParameters;
116
117 // Called when Keystone registration completes.
118 - (void)registrationComplete:(NSNotification*)notification;
119
120 // Called periodically to announce activity by pinging the Keystone server.
121 - (void)markActive:(NSTimer*)timer;
122
123 // Called when an update check or update installation is complete.  Posts the
124 // kAutoupdateStatusNotification notification to the default notification
125 // center.
126 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version;
127
128 // Returns the version of the currently-installed application on disk.
129 - (NSString*)currentlyInstalledVersion;
130
131 // These three methods are used to determine the version of the application
132 // currently installed on disk, compare that to the currently-running version,
133 // decide whether any updates have been installed, and call
134 // -updateStatus:version:.
135 //
136 // In order to check the version on disk, the installed application's
137 // Info.plist dictionary must be read; in order to see changes as updates are
138 // applied, the dictionary must be read each time, bypassing any caches such
139 // as the one that NSBundle might be maintaining.  Reading files can be a
140 // blocking operation, and blocking operations are to be avoided on the main
141 // thread.  I'm not quite sure what jank means, but I bet that a blocked main
142 // thread would cause some of it.
143 //
144 // -determineUpdateStatusAsync is called on the main thread to initiate the
145 // operation.  It performs initial set-up work that must be done on the main
146 // thread and arranges for -determineUpdateStatus to be called on a work queue
147 // thread managed by WorkerPool.
148 // -determineUpdateStatus then reads the Info.plist, gets the version from the
149 // CFBundleShortVersionString key, and performs
150 // -determineUpdateStatusForVersion: on the main thread.
151 // -determineUpdateStatusForVersion: does the actual comparison of the version
152 // on disk with the running version and calls -updateStatus:version: with the
153 // results of its analysis.
154 - (void)determineUpdateStatusAsync;
155 - (void)determineUpdateStatus;
156 - (void)determineUpdateStatusForVersion:(NSString*)version;
157
158 // Returns YES if registration_ is definitely on a user ticket.  If definitely
159 // on a system ticket, or uncertain of ticket type (due to an older version
160 // of Keystone being used), returns NO.
161 - (BOOL)isUserTicket;
162
163 // Returns YES if Keystone is definitely installed at the system level,
164 // determined by the presence of an executable ksadmin program at the expected
165 // system location.
166 - (BOOL)isSystemKeystone;
167
168 // Returns YES if on a system ticket but system Keystone is not present.
169 // Returns NO otherwise. The "doomed" condition will result in the
170 // registration framework appearing to have registered Chrome, but no updates
171 // ever actually taking place.
172 - (BOOL)isSystemTicketDoomed;
173
174 // Called when ticket promotion completes.
175 - (void)promotionComplete:(NSNotification*)notification;
176
177 // Changes the application's ownership and permissions so that all files are
178 // owned by root:wheel and all files and directories are writable only by
179 // root, but readable and executable as needed by everyone.
180 // -changePermissionsForPromotionAsync is called on the main thread by
181 // -promotionComplete.  That routine calls
182 // -changePermissionsForPromotionWithTool: on a work queue thread.  When done,
183 // -changePermissionsForPromotionComplete is called on the main thread.
184 - (void)changePermissionsForPromotionAsync;
185 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath;
186 - (void)changePermissionsForPromotionComplete;
187
188 // Returns the brand file path to use for Keystone.
189 - (NSString*)brandFilePath;
190
191 @end  // @interface KeystoneGlue (Private)
192
193 NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification";
194 NSString* const kAutoupdateStatusStatus = @"status";
195 NSString* const kAutoupdateStatusVersion = @"version";
196
197 namespace {
198
199 NSString* const kChannelKey = @"KSChannelID";
200 NSString* const kBrandKey = @"KSBrandID";
201 NSString* const kVersionKey = @"KSVersion";
202
203 }  // namespace
204
205 @implementation KeystoneGlue
206
207 + (id)defaultKeystoneGlue {
208   static bool sTriedCreatingDefaultKeystoneGlue = false;
209   // TODO(jrg): use base::SingletonObjC<KeystoneGlue>
210   static KeystoneGlue* sDefaultKeystoneGlue = nil;  // leaked
211
212   if (!sTriedCreatingDefaultKeystoneGlue) {
213     sTriedCreatingDefaultKeystoneGlue = true;
214
215     sDefaultKeystoneGlue = [[KeystoneGlue alloc] init];
216     [sDefaultKeystoneGlue loadParameters];
217     if (![sDefaultKeystoneGlue loadKeystoneRegistration]) {
218       [sDefaultKeystoneGlue release];
219       sDefaultKeystoneGlue = nil;
220     }
221   }
222   return sDefaultKeystoneGlue;
223 }
224
225 - (id)init {
226   if ((self = [super init])) {
227     NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
228
229     [center addObserver:self
230                selector:@selector(registrationComplete:)
231                    name:ksr::KSRegistrationDidCompleteNotification
232                  object:nil];
233
234     [center addObserver:self
235                selector:@selector(promotionComplete:)
236                    name:ksr::KSRegistrationPromotionDidCompleteNotification
237                  object:nil];
238
239     [center addObserver:self
240                selector:@selector(checkForUpdateComplete:)
241                    name:ksr::KSRegistrationCheckForUpdateNotification
242                  object:nil];
243
244     [center addObserver:self
245                selector:@selector(installUpdateComplete:)
246                    name:ksr::KSRegistrationStartUpdateNotification
247                  object:nil];
248   }
249
250   return self;
251 }
252
253 - (void)dealloc {
254   [productID_ release];
255   [appPath_ release];
256   [url_ release];
257   [version_ release];
258   [channel_ release];
259   [registration_ release];
260   [[NSNotificationCenter defaultCenter] removeObserver:self];
261   [super dealloc];
262 }
263
264 - (NSDictionary*)infoDictionary {
265   // Use base::mac::OuterBundle() to get the Chrome app's own bundle identifier
266   // and path, not the framework's.  For auto-update, the application is
267   // what's significant here: it's used to locate the outermost part of the
268   // application for the existence checker and other operations that need to
269   // see the entire application bundle.
270   return [base::mac::OuterBundle() infoDictionary];
271 }
272
273 - (void)loadParameters {
274   NSBundle* appBundle = base::mac::OuterBundle();
275   NSDictionary* infoDictionary = [self infoDictionary];
276
277   NSString* productID = [infoDictionary objectForKey:@"KSProductID"];
278   if (productID == nil) {
279     productID = [appBundle bundleIdentifier];
280   }
281
282   NSString* appPath = [appBundle bundlePath];
283   NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"];
284   NSString* version = [infoDictionary objectForKey:kVersionKey];
285
286   if (!productID || !appPath || !url || !version) {
287     // If parameters required for Keystone are missing, don't use it.
288     return;
289   }
290
291   NSString* channel = [infoDictionary objectForKey:kChannelKey];
292   // The stable channel has no tag.  If updating to stable, remove the
293   // dev and beta tags since we've been "promoted".
294   if (channel == nil)
295     channel = ksr::KSRegistrationRemoveExistingTag;
296
297   productID_ = [productID retain];
298   appPath_ = [appPath retain];
299   url_ = [url retain];
300   version_ = [version retain];
301   channel_ = [channel retain];
302 }
303
304 - (NSString*)brandFilePath {
305   DCHECK(version_ != nil) << "-loadParameters must be called first";
306
307   if (brandFileType_ == kBrandFileTypeNotDetermined) {
308
309     NSFileManager* fm = [NSFileManager defaultManager];
310     NSString* userBrandFile = UserBrandFilePath();
311     NSString* systemBrandFile = SystemBrandFilePath();
312
313     // Default to none.
314     brandFileType_ = kBrandFileTypeNone;
315
316     // Only the stable channel has a brand code.
317     chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
318
319     if (channel == chrome::VersionInfo::CHANNEL_DEV ||
320         channel == chrome::VersionInfo::CHANNEL_BETA) {
321
322       // If on the dev or beta channel, this installation may have replaced
323       // an older system-level installation. Check for a user brand file and
324       // nuke it if present. Don't try to remove the system brand file, there
325       // wouldn't be any permission to do so.
326       //
327       // Don't do this on the canary channel. The canary can run side-by-side
328       // with another Google Chrome installation whose brand code, if any,
329       // should remain intact.
330
331       if ([fm fileExistsAtPath:userBrandFile]) {
332         [fm removeItemAtPath:userBrandFile error:NULL];
333       }
334
335     } else if (channel == chrome::VersionInfo::CHANNEL_STABLE) {
336
337       // If there is a system brand file, use it.
338       if ([fm fileExistsAtPath:systemBrandFile]) {
339         // System
340
341         // Use the system file that is there.
342         brandFileType_ = kBrandFileTypeSystem;
343
344         // Clean up any old user level file.
345         if ([fm fileExistsAtPath:userBrandFile]) {
346           [fm removeItemAtPath:userBrandFile error:NULL];
347         }
348
349       } else {
350         // User
351
352         NSDictionary* infoDictionary = [self infoDictionary];
353         NSString* appBundleBrandID = [infoDictionary objectForKey:kBrandKey];
354
355         NSString* storedBrandID = nil;
356         if ([fm fileExistsAtPath:userBrandFile]) {
357           NSDictionary* storedBrandDict =
358               [NSDictionary dictionaryWithContentsOfFile:userBrandFile];
359           storedBrandID = [storedBrandDict objectForKey:kBrandKey];
360         }
361
362         if ((appBundleBrandID != nil) &&
363             (![storedBrandID isEqualTo:appBundleBrandID])) {
364           // App and store don't match, update store and use it.
365           NSDictionary* storedBrandDict =
366               [NSDictionary dictionaryWithObject:appBundleBrandID
367                                           forKey:kBrandKey];
368           // If Keystone hasn't been installed yet, the location the brand file
369           // is written to won't exist, so manually create the directory.
370           NSString *userBrandFileDirectory =
371               [userBrandFile stringByDeletingLastPathComponent];
372           if (![fm fileExistsAtPath:userBrandFileDirectory]) {
373             if (![fm createDirectoryAtPath:userBrandFileDirectory
374                withIntermediateDirectories:YES
375                                 attributes:nil
376                                      error:NULL]) {
377               LOG(ERROR) << "Failed to create the directory for the brand file";
378             }
379           }
380           if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) {
381             brandFileType_ = kBrandFileTypeUser;
382           }
383         } else if (storedBrandID) {
384           // Had stored brand, use it.
385           brandFileType_ = kBrandFileTypeUser;
386         }
387       }
388     }
389
390   }
391
392   NSString* result = nil;
393   switch (brandFileType_) {
394     case kBrandFileTypeUser:
395       result = UserBrandFilePath();
396       break;
397
398     case kBrandFileTypeSystem:
399       result = SystemBrandFilePath();
400       break;
401
402     case kBrandFileTypeNotDetermined:
403       NOTIMPLEMENTED();
404       // Fall through
405     case kBrandFileTypeNone:
406       // Clear the value.
407       result = @"";
408       break;
409
410   }
411   return result;
412 }
413
414 - (BOOL)loadKeystoneRegistration {
415   if (!productID_ || !appPath_ || !url_ || !version_)
416     return NO;
417
418   // Load the KeystoneRegistration framework bundle if present.  It lives
419   // inside the framework, so use base::mac::FrameworkBundle();
420   NSString* ksrPath =
421       [[base::mac::FrameworkBundle() privateFrameworksPath]
422           stringByAppendingPathComponent:@"KeystoneRegistration.framework"];
423   NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath];
424   [ksrBundle load];
425
426   // Harness the KSRegistration class.
427   Class ksrClass = [ksrBundle classNamed:@"KSRegistration"];
428   KSRegistration* ksr = [ksrClass registrationWithProductID:productID_];
429   if (!ksr)
430     return NO;
431
432   registration_ = [ksr retain];
433   return YES;
434 }
435
436 - (NSString*)appInfoPlistPath {
437   // NSBundle ought to have a way to access this path directly, but it
438   // doesn't.
439   return [[appPath_ stringByAppendingPathComponent:@"Contents"]
440              stringByAppendingPathComponent:@"Info.plist"];
441 }
442
443 - (NSDictionary*)keystoneParameters {
444   NSNumber* xcType = [NSNumber numberWithInt:ksr::kKSPathExistenceChecker];
445   NSNumber* preserveTTToken = [NSNumber numberWithBool:YES];
446   NSString* appInfoPlistPath = [self appInfoPlistPath];
447   NSString* brandKey = kBrandKey;
448   NSString* brandPath = [self brandFilePath];
449
450   if ([brandPath length] == 0) {
451     // Brand path and brand key must be cleared together or ksadmin seems
452     // to throw an error.
453     brandKey = @"";
454   }
455
456   return [NSDictionary dictionaryWithObjectsAndKeys:
457              version_, ksr::KSRegistrationVersionKey,
458              appInfoPlistPath, ksr::KSRegistrationVersionPathKey,
459              kVersionKey, ksr::KSRegistrationVersionKeyKey,
460              xcType, ksr::KSRegistrationExistenceCheckerTypeKey,
461              appPath_, ksr::KSRegistrationExistenceCheckerStringKey,
462              url_, ksr::KSRegistrationServerURLStringKey,
463              preserveTTToken, ksr::KSRegistrationPreserveTrustedTesterTokenKey,
464              channel_, ksr::KSRegistrationTagKey,
465              appInfoPlistPath, ksr::KSRegistrationTagPathKey,
466              kChannelKey, ksr::KSRegistrationTagKeyKey,
467              brandPath, ksr::KSRegistrationBrandPathKey,
468              brandKey, ksr::KSRegistrationBrandKeyKey,
469              nil];
470 }
471
472 - (void)registerWithKeystone {
473   [self updateStatus:kAutoupdateRegistering version:nil];
474
475   NSDictionary* parameters = [self keystoneParameters];
476   BOOL result;
477   {
478     // TODO(shess): Allows Keystone to throw an exception when
479     // /usr/bin/python does not exist (really!).
480     // http://crbug.com/86221 and http://crbug.com/87931
481     base::mac::ScopedNSExceptionEnabler enabler;
482     result = [registration_ registerWithParameters:parameters];
483   }
484   if (!result) {
485     [self updateStatus:kAutoupdateRegisterFailed version:nil];
486     return;
487   }
488
489   // Upon completion, ksr::KSRegistrationDidCompleteNotification will be
490   // posted, and -registrationComplete: will be called.
491
492   // Mark an active RIGHT NOW; don't wait an hour for the first one.
493   [registration_ setActive];
494
495   // Set up hourly activity pings.
496   timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60  // One hour
497                                             target:self
498                                           selector:@selector(markActive:)
499                                           userInfo:registration_
500                                            repeats:YES];
501 }
502
503 - (void)registrationComplete:(NSNotification*)notification {
504   NSDictionary* userInfo = [notification userInfo];
505   if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
506     if ([self isSystemTicketDoomed]) {
507       [self updateStatus:kAutoupdateNeedsPromotion version:nil];
508     } else {
509       [self updateStatus:kAutoupdateRegistered version:nil];
510     }
511   } else {
512     // Dump registration_?
513     [self updateStatus:kAutoupdateRegisterFailed version:nil];
514   }
515 }
516
517 - (void)stopTimer {
518   [timer_ invalidate];
519 }
520
521 - (void)markActive:(NSTimer*)timer {
522   KSRegistration* ksr = [timer userInfo];
523   [ksr setActive];
524 }
525
526 - (void)checkForUpdate {
527   DCHECK(![self asyncOperationPending]);
528
529   if (!registration_) {
530     [self updateStatus:kAutoupdateCheckFailed version:nil];
531     return;
532   }
533
534   [self updateStatus:kAutoupdateChecking version:nil];
535
536   [registration_ checkForUpdate];
537
538   // Upon completion, ksr::KSRegistrationCheckForUpdateNotification will be
539   // posted, and -checkForUpdateComplete: will be called.
540 }
541
542 - (void)checkForUpdateComplete:(NSNotification*)notification {
543   NSDictionary* userInfo = [notification userInfo];
544
545   if ([[userInfo objectForKey:ksr::KSRegistrationUpdateCheckErrorKey]
546           boolValue]) {
547     [self updateStatus:kAutoupdateCheckFailed version:nil];
548   } else if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
549     // If an update is known to be available, go straight to
550     // -updateStatus:version:.  It doesn't matter what's currently on disk.
551     NSString* version = [userInfo objectForKey:ksr::KSRegistrationVersionKey];
552     [self updateStatus:kAutoupdateAvailable version:version];
553   } else {
554     // If no updates are available, check what's on disk, because an update
555     // may have already been installed.  This check happens on another thread,
556     // and -updateStatus:version: will be called on the main thread when done.
557     [self determineUpdateStatusAsync];
558   }
559 }
560
561 - (void)installUpdate {
562   DCHECK(![self asyncOperationPending]);
563
564   if (!registration_) {
565     [self updateStatus:kAutoupdateInstallFailed version:nil];
566     return;
567   }
568
569   [self updateStatus:kAutoupdateInstalling version:nil];
570
571   [registration_ startUpdate];
572
573   // Upon completion, ksr::KSRegistrationStartUpdateNotification will be
574   // posted, and -installUpdateComplete: will be called.
575 }
576
577 - (void)installUpdateComplete:(NSNotification*)notification {
578   NSDictionary* userInfo = [notification userInfo];
579
580   // http://crbug.com/160308 and b/7517358: when using system Keystone and on
581   // a user ticket, KSUpdateCheckSuccessfulKey will be NO even when an update
582   // was installed correctly, so don't check it. It should be redudnant when
583   // KSUpdateCheckSuccessfullyInstalledKey is checked.
584   if (![[userInfo objectForKey:ksr::KSUpdateCheckSuccessfullyInstalledKey]
585           intValue]) {
586     [self updateStatus:kAutoupdateInstallFailed version:nil];
587   } else {
588     updateSuccessfullyInstalled_ = YES;
589
590     // Nothing in the notification dictionary reports the version that was
591     // installed.  Figure it out based on what's on disk.
592     [self determineUpdateStatusAsync];
593   }
594 }
595
596 - (NSString*)currentlyInstalledVersion {
597   NSString* appInfoPlistPath = [self appInfoPlistPath];
598   NSDictionary* infoPlist =
599       [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath];
600   return [infoPlist objectForKey:@"CFBundleShortVersionString"];
601 }
602
603 // Runs on the main thread.
604 - (void)determineUpdateStatusAsync {
605   DCHECK([NSThread isMainThread]);
606
607   PerformBridge::PostPerform(self, @selector(determineUpdateStatus));
608 }
609
610 // Runs on a thread managed by WorkerPool.
611 - (void)determineUpdateStatus {
612   DCHECK(![NSThread isMainThread]);
613
614   NSString* version = [self currentlyInstalledVersion];
615
616   [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:)
617                          withObject:version
618                       waitUntilDone:NO];
619 }
620
621 // Runs on the main thread.
622 - (void)determineUpdateStatusForVersion:(NSString*)version {
623   DCHECK([NSThread isMainThread]);
624
625   AutoupdateStatus status;
626   if (updateSuccessfullyInstalled_) {
627     // If an update was successfully installed and this object saw it happen,
628     // then don't even bother comparing versions.
629     status = kAutoupdateInstalled;
630   } else {
631     NSString* currentVersion =
632         [NSString stringWithUTF8String:chrome::kChromeVersion];
633     if (!version) {
634       // If the version on disk could not be determined, assume that
635       // whatever's running is current.
636       version = currentVersion;
637       status = kAutoupdateCurrent;
638     } else if ([version isEqualToString:currentVersion]) {
639       status = kAutoupdateCurrent;
640     } else {
641       // If the version on disk doesn't match what's currently running, an
642       // update must have been applied in the background, without this app's
643       // direct participation.  Leave updateSuccessfullyInstalled_ alone
644       // because there's no direct knowledge of what actually happened.
645       status = kAutoupdateInstalled;
646     }
647   }
648
649   [self updateStatus:status version:version];
650 }
651
652 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version {
653   NSNumber* statusNumber = [NSNumber numberWithInt:status];
654   NSMutableDictionary* dictionary =
655       [NSMutableDictionary dictionaryWithObject:statusNumber
656                                          forKey:kAutoupdateStatusStatus];
657   if (version) {
658     [dictionary setObject:version forKey:kAutoupdateStatusVersion];
659   }
660
661   NSNotification* notification =
662       [NSNotification notificationWithName:kAutoupdateStatusNotification
663                                     object:self
664                                   userInfo:dictionary];
665   recentNotification_.reset([notification retain]);
666
667   [[NSNotificationCenter defaultCenter] postNotification:notification];
668 }
669
670 - (NSNotification*)recentNotification {
671   return [[recentNotification_ retain] autorelease];
672 }
673
674 - (AutoupdateStatus)recentStatus {
675   NSDictionary* dictionary = [recentNotification_ userInfo];
676   return static_cast<AutoupdateStatus>(
677       [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]);
678 }
679
680 - (BOOL)asyncOperationPending {
681   AutoupdateStatus status = [self recentStatus];
682   return status == kAutoupdateRegistering ||
683          status == kAutoupdateChecking ||
684          status == kAutoupdateInstalling ||
685          status == kAutoupdatePromoting;
686 }
687
688 - (BOOL)isUserTicket {
689   return [registration_ ticketType] == ksr::kKSRegistrationUserTicket;
690 }
691
692 - (BOOL)isSystemKeystone {
693   struct stat statbuf;
694   if (stat("/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/"
695            "Contents/MacOS/ksadmin",
696            &statbuf) != 0) {
697     return NO;
698   }
699
700   if (!(statbuf.st_mode & S_IXUSR)) {
701     return NO;
702   }
703
704   return YES;
705 }
706
707 - (BOOL)isSystemTicketDoomed {
708   BOOL isSystemTicket = ![self isUserTicket];
709   return isSystemTicket && ![self isSystemKeystone];
710 }
711
712 - (BOOL)isOnReadOnlyFilesystem {
713   const char* appPathC = [appPath_ fileSystemRepresentation];
714   struct statfs statfsBuf;
715
716   if (statfs(appPathC, &statfsBuf) != 0) {
717     PLOG(ERROR) << "statfs";
718     // Be optimistic about the filesystem's writability.
719     return NO;
720   }
721
722   return (statfsBuf.f_flags & MNT_RDONLY) != 0;
723 }
724
725 - (BOOL)needsPromotion {
726   // Don't promote when on a read-only filesystem.
727   if ([self isOnReadOnlyFilesystem]) {
728     return NO;
729   }
730
731   // Promotion is required when a system ticket is present but system Keystone
732   // is not.
733   if ([self isSystemTicketDoomed]) {
734     return YES;
735   }
736
737   // If on a system ticket and system Keystone is present, promotion is not
738   // required.
739   if (![self isUserTicket]) {
740     return NO;
741   }
742
743   // Check the outermost bundle directory, the main executable path, and the
744   // framework directory.  It may be enough to just look at the outermost
745   // bundle directory, but checking an interior file and directory can be
746   // helpful in case permissions are set differently only on the outermost
747   // directory.  An interior file and directory are both checked because some
748   // file operations, such as Snow Leopard's Finder's copy operation when
749   // authenticating, may actually result in different ownership being applied
750   // to files and directories.
751   NSFileManager* fileManager = [NSFileManager defaultManager];
752   NSString* executablePath = [base::mac::OuterBundle() executablePath];
753   NSString* frameworkPath = [base::mac::FrameworkBundle() bundlePath];
754   return ![fileManager isWritableFileAtPath:appPath_] ||
755          ![fileManager isWritableFileAtPath:executablePath] ||
756          ![fileManager isWritableFileAtPath:frameworkPath];
757 }
758
759 - (BOOL)wantsPromotion {
760   if ([self needsPromotion]) {
761     return YES;
762   }
763
764   // These are the same unpromotable cases as in -needsPromotion.
765   if ([self isOnReadOnlyFilesystem] || ![self isUserTicket]) {
766     return NO;
767   }
768
769   return [appPath_ hasPrefix:@"/Applications/"];
770 }
771
772 - (void)promoteTicket {
773   if ([self asyncOperationPending] || ![self wantsPromotion]) {
774     // Because there are multiple ways of reaching promoteTicket that might
775     // not lock each other out, it may be possible to arrive here while an
776     // asynchronous operation is pending, or even after promotion has already
777     // occurred.  Just quietly return without doing anything.
778     return;
779   }
780
781   NSString* prompt = l10n_util::GetNSStringFWithFixup(
782       IDS_PROMOTE_AUTHENTICATION_PROMPT,
783       l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
784   base::mac::ScopedAuthorizationRef authorization(
785       base::mac::AuthorizationCreateToRunAsRoot(
786           base::mac::NSToCFCast(prompt)));
787   if (!authorization.get()) {
788     return;
789   }
790
791   [self promoteTicketWithAuthorization:authorization.release() synchronous:NO];
792 }
793
794 - (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg
795                            synchronous:(BOOL)synchronous {
796   base::mac::ScopedAuthorizationRef authorization(authorization_arg);
797   authorization_arg = NULL;
798
799   if ([self asyncOperationPending]) {
800     // Starting a synchronous operation while an asynchronous one is pending
801     // could be trouble.
802     return;
803   }
804   if (!synchronous && ![self wantsPromotion]) {
805     // If operating synchronously, the call came from the installer, which
806     // means that a system ticket is required.  Otherwise, only allow
807     // promotion if it's wanted.
808     return;
809   }
810
811   synchronousPromotion_ = synchronous;
812
813   [self updateStatus:kAutoupdatePromoting version:nil];
814
815   // TODO(mark): Remove when able!
816   //
817   // keystone_promote_preflight will copy the current brand information out to
818   // the system level so all users can share the data as part of the ticket
819   // promotion.
820   //
821   // It will also ensure that the Keystone system ticket store is in a usable
822   // state for all users on the system.  Ideally, Keystone's installer or
823   // another part of Keystone would handle this.  The underlying problem is
824   // http://b/2285921, and it causes http://b/2289908, which this workaround
825   // addresses.
826   //
827   // This is run synchronously, which isn't optimal, but
828   // -[KSRegistration promoteWithParameters:authorization:] is currently
829   // synchronous too, and this operation needs to happen before that one.
830   //
831   // TODO(mark): Make asynchronous.  That only makes sense if the promotion
832   // operation itself is asynchronous too.  http://b/2290009.  Hopefully,
833   // the Keystone promotion code will just be changed to do what preflight
834   // now does, and then the preflight script can be removed instead.
835   // However, preflight operation (and promotion) should only be asynchronous
836   // if the synchronous parameter is NO.
837   NSString* preflightPath =
838       [base::mac::FrameworkBundle()
839           pathForResource:@"keystone_promote_preflight"
840                    ofType:@"sh"];
841   const char* preflightPathC = [preflightPath fileSystemRepresentation];
842   const char* userBrandFile = NULL;
843   const char* systemBrandFile = NULL;
844   if (brandFileType_ == kBrandFileTypeUser) {
845     // Running with user level brand file, promote to the system level.
846     userBrandFile = [UserBrandFilePath() fileSystemRepresentation];
847     systemBrandFile = [SystemBrandFilePath() fileSystemRepresentation];
848   }
849   const char* arguments[] = {userBrandFile, systemBrandFile, NULL};
850
851   int exit_status;
852   OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
853       authorization,
854       preflightPathC,
855       kAuthorizationFlagDefaults,
856       arguments,
857       NULL,  // pipe
858       &exit_status);
859   if (status != errAuthorizationSuccess) {
860     OSSTATUS_LOG(ERROR, status)
861         << "AuthorizationExecuteWithPrivileges preflight";
862     [self updateStatus:kAutoupdatePromoteFailed version:nil];
863     return;
864   }
865   if (exit_status != 0) {
866     LOG(ERROR) << "keystone_promote_preflight status " << exit_status;
867     [self updateStatus:kAutoupdatePromoteFailed version:nil];
868     return;
869   }
870
871   // Hang on to the AuthorizationRef so that it can be used once promotion is
872   // complete.  Do this before asking Keystone to promote the ticket, because
873   // -promotionComplete: may be called from inside the Keystone promotion
874   // call.
875   authorization_.swap(authorization);
876
877   NSDictionary* parameters = [self keystoneParameters];
878
879   // If the brand file is user level, update parameters to point to the new
880   // system level file during promotion.
881   if (brandFileType_ == kBrandFileTypeUser) {
882     NSMutableDictionary* temp_parameters =
883         [[parameters mutableCopy] autorelease];
884     [temp_parameters setObject:SystemBrandFilePath()
885                         forKey:ksr::KSRegistrationBrandPathKey];
886     parameters = temp_parameters;
887   }
888
889   if (![registration_ promoteWithParameters:parameters
890                               authorization:authorization_]) {
891     [self updateStatus:kAutoupdatePromoteFailed version:nil];
892     authorization_.reset();
893     return;
894   }
895
896   // Upon completion, ksr::KSRegistrationPromotionDidCompleteNotification will
897   // be posted, and -promotionComplete: will be called.
898
899   // If synchronous, see to it that this happens immediately. Give it a
900   // 10-second deadline.
901   if (synchronous) {
902     CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
903   }
904 }
905
906 - (void)promotionComplete:(NSNotification*)notification {
907   NSDictionary* userInfo = [notification userInfo];
908   if ([[userInfo objectForKey:ksr::KSRegistrationStatusKey] boolValue]) {
909     if (synchronousPromotion_) {
910       // Short-circuit: if performing a synchronous promotion, the promotion
911       // came from the installer, which already set the permissions properly.
912       // Rather than run a duplicate permission-changing operation, jump
913       // straight to "done."
914       [self changePermissionsForPromotionComplete];
915     } else {
916       [self changePermissionsForPromotionAsync];
917     }
918   } else {
919     authorization_.reset();
920     [self updateStatus:kAutoupdatePromoteFailed version:nil];
921   }
922
923   if (synchronousPromotion_) {
924     // The run loop doesn't need to wait for this any longer.
925     CFRunLoopRef runLoop = CFRunLoopGetCurrent();
926     CFRunLoopStop(runLoop);
927     CFRunLoopWakeUp(runLoop);
928   }
929 }
930
931 - (void)changePermissionsForPromotionAsync {
932   // NSBundle is not documented as being thread-safe.  Do NSBundle operations
933   // on the main thread before jumping over to a WorkerPool-managed
934   // thread to run the tool.
935   DCHECK([NSThread isMainThread]);
936
937   SEL selector = @selector(changePermissionsForPromotionWithTool:);
938   NSString* toolPath =
939       [base::mac::FrameworkBundle()
940           pathForResource:@"keystone_promote_postflight"
941                    ofType:@"sh"];
942
943   PerformBridge::PostPerform(self, selector, toolPath);
944 }
945
946 - (void)changePermissionsForPromotionWithTool:(NSString*)toolPath {
947   const char* toolPathC = [toolPath fileSystemRepresentation];
948
949   const char* appPathC = [appPath_ fileSystemRepresentation];
950   const char* arguments[] = {appPathC, NULL};
951
952   int exit_status;
953   OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
954       authorization_,
955       toolPathC,
956       kAuthorizationFlagDefaults,
957       arguments,
958       NULL,  // pipe
959       &exit_status);
960   if (status != errAuthorizationSuccess) {
961     OSSTATUS_LOG(ERROR, status)
962         << "AuthorizationExecuteWithPrivileges postflight";
963   } else if (exit_status != 0) {
964     LOG(ERROR) << "keystone_promote_postflight status " << exit_status;
965   }
966
967   SEL selector = @selector(changePermissionsForPromotionComplete);
968   [self performSelectorOnMainThread:selector
969                          withObject:nil
970                       waitUntilDone:NO];
971 }
972
973 - (void)changePermissionsForPromotionComplete {
974   authorization_.reset();
975
976   [self updateStatus:kAutoupdatePromoted version:nil];
977 }
978
979 - (void)setAppPath:(NSString*)appPath {
980   if (appPath != appPath_) {
981     [appPath_ release];
982     appPath_ = [appPath copy];
983   }
984 }
985
986 @end  // @implementation KeystoneGlue
987
988 namespace {
989
990 std::string BrandCodeInternal() {
991   KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
992   NSString* brand_path = [keystone_glue brandFilePath];
993
994   if (![brand_path length])
995     return std::string();
996
997   NSDictionary* dict =
998       [NSDictionary dictionaryWithContentsOfFile:brand_path];
999   NSString* brand_code =
1000       base::mac::ObjCCast<NSString>([dict objectForKey:kBrandKey]);
1001   if (brand_code)
1002     return [brand_code UTF8String];
1003
1004   return std::string();
1005 }
1006
1007 }  // namespace
1008
1009 namespace keystone_glue {
1010
1011 std::string BrandCode() {
1012   // |s_brand_code| is leaked.
1013   static std::string* s_brand_code = new std::string(BrandCodeInternal());
1014   return *s_brand_code;
1015 }
1016
1017 bool KeystoneEnabled() {
1018   return [KeystoneGlue defaultKeystoneGlue] != nil;
1019 }
1020
1021 string16 CurrentlyInstalledVersion() {
1022   KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
1023   NSString* version = [keystoneGlue currentlyInstalledVersion];
1024   return base::SysNSStringToUTF16(version);
1025 }
1026
1027 }  // namespace keystone_glue