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