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