diff options
author | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-11-28 22:05:11 +0000 |
---|---|---|
committer | mark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-11-28 22:05:11 +0000 |
commit | eee9f55299e1223c156f1bcf86f557446009cc5e (patch) | |
tree | 5eb3b23a1754afca47a4a8e5b74078224d0c122e /chrome/browser | |
parent | 110086c1a303bdcf3829a289efebef7e9378ab7b (diff) | |
download | chromium_src-eee9f55299e1223c156f1bcf86f557446009cc5e.zip chromium_src-eee9f55299e1223c156f1bcf86f557446009cc5e.tar.gz chromium_src-eee9f55299e1223c156f1bcf86f557446009cc5e.tar.bz2 |
In-application Keystone ticket promotion.
The concept of "ticket promotion" is added to the application when Keystone is
in use. Ticket promotion is used to turn a user Keystone ticket, which Chrome
normally establishes when it launches, into a system Keystone ticket, after
successful user authentication and authorization. Having a system Keystone
with a system ticket means that updates are applied with root privileges
instead of user privileges, essentially eliminating the possibility that a
user will fall off of the auto-update train because they can read and execute
but not write the application.
Two principles of promotion apply:
- An application on a user ticket NEEDS promotion if it determines that it
doesn't have permission to write to itself. Being on a user ticket, an
update attempt would fail.
- An application on a user ticket WANTS promotion if it already NEEDS
promotion. Additionally, if it is installed in a system-wide location
such as /Applications, it will WANT promotion, even if it does not NEED it.
If promotion is needed, an info bar will show up on launch requesting it.
This info bar works similarly to the default browser info bar: it has a "don't
bother me again" button, it will only show up after the first launch, it won't
disappear on navigation if the navigation happens very quickly, and it won't
show itself if another info bar is up. This means that if both the default
browser info bar and the promotion info bar have a shot at showing, only one
will win. In my experience, each wins about half of the time.
If promotion is needed, the update UI in the About window will be hidden.
Checking for updates and offering to apply them doesn't make much sense when
the update won't be able to install successfully. All of the auto-update
machinery is still working in the background, but the About window UI is
hidden.
If promotion is wanted, the About window will contain a new button allowing
the user to enter promotion. This gives access to the same promotion routine
as the promotion info bar. It can be used even from an administrative account
that is able to update the application without promotion. It's intended to be
used by the system administrator of the family without requiring them to
switch to one of the kids' accounts.
BUG=16360
TEST=Exhaustively, please.
Review URL: http://codereview.chromium.org/437053
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@33241 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser')
-rw-r--r-- | chrome/browser/app_controller_mac.mm | 3 | ||||
-rw-r--r-- | chrome/browser/browser.cc | 1 | ||||
-rw-r--r-- | chrome/browser/browser_init.cc | 20 | ||||
-rw-r--r-- | chrome/browser/browser_main_mac.mm | 2 | ||||
-rw-r--r-- | chrome/browser/cocoa/about_window_controller.h | 19 | ||||
-rw-r--r-- | chrome/browser/cocoa/about_window_controller.mm | 389 | ||||
-rw-r--r-- | chrome/browser/cocoa/about_window_controller_unittest.mm | 2 | ||||
-rw-r--r-- | chrome/browser/cocoa/authorization_util.cc | 118 | ||||
-rw-r--r-- | chrome/browser/cocoa/authorization_util.h | 58 | ||||
-rw-r--r-- | chrome/browser/cocoa/keystone_glue.h | 163 | ||||
-rw-r--r-- | chrome/browser/cocoa/keystone_glue.mm | 728 | ||||
-rw-r--r-- | chrome/browser/cocoa/keystone_glue_unittest.mm | 184 | ||||
-rw-r--r-- | chrome/browser/cocoa/keystone_infobar.h | 23 | ||||
-rw-r--r-- | chrome/browser/cocoa/keystone_infobar.mm | 211 | ||||
-rwxr-xr-x | chrome/browser/cocoa/keystone_promote_postflight.sh | 50 | ||||
-rwxr-xr-x | chrome/browser/cocoa/keystone_promote_preflight.sh | 59 | ||||
-rw-r--r-- | chrome/browser/cocoa/scoped_authorizationref.h | 78 |
17 files changed, 2047 insertions, 61 deletions
diff --git a/chrome/browser/app_controller_mac.mm b/chrome/browser/app_controller_mac.mm index b80780a..5a93c8d 100644 --- a/chrome/browser/app_controller_mac.mm +++ b/chrome/browser/app_controller_mac.mm @@ -780,9 +780,6 @@ static bool g_is_opening_new_window = false; object:[aboutController_ window]]; } - if (![[aboutController_ window] isVisible]) - [[aboutController_ window] center]; - [aboutController_ showWindow:self]; } diff --git a/chrome/browser/browser.cc b/chrome/browser/browser.cc index 821b901..521d15a 100644 --- a/chrome/browser/browser.cc +++ b/chrome/browser/browser.cc @@ -1350,6 +1350,7 @@ void Browser::RegisterUserPrefs(PrefService* prefs) { // to hard-code pref registration in the several unit tests that use // this preference. prefs->RegisterBooleanPref(prefs::kShowPageOptionsButtons, false); + prefs->RegisterBooleanPref(prefs::kShowUpdatePromotionInfoBar, true); #endif prefs->RegisterStringPref(prefs::kRecentlySelectedEncoding, L""); prefs->RegisterBooleanPref(prefs::kDeleteBrowsingHistory, true); diff --git a/chrome/browser/browser_init.cc b/chrome/browser/browser_init.cc index 847e966..ee192b4 100644 --- a/chrome/browser/browser_init.cc +++ b/chrome/browser/browser_init.cc @@ -43,6 +43,10 @@ #include "net/base/net_util.h" #include "webkit/glue/webkit_glue.h" +#if defined(OS_MACOSX) +#include "chrome/browser/cocoa/keystone_infobar.h" +#endif + #if defined(OS_WIN) #include "app/win_util.h" #endif @@ -450,10 +454,18 @@ bool BrowserInit::LaunchWithProfile::Launch(Profile* profile, browser = BrowserList::GetLastActive(); OpenURLsInBrowser(browser, process_startup, urls_to_open); } - // Check whether we are the default browser. - if (process_startup && browser_defaults::kOSSupportsOtherBrowsers && - !command_line_.HasSwitch(switches::kNoDefaultBrowserCheck)) - CheckDefaultBrowser(profile); + if (process_startup) { + if (browser_defaults::kOSSupportsOtherBrowsers && + !command_line_.HasSwitch(switches::kNoDefaultBrowserCheck)) { + // Check whether we are the default browser. + CheckDefaultBrowser(profile); + } +#if defined(OS_MACOSX) + // Check whether the auto-update system needs to be promoted from user + // to system. + KeystoneInfoBar::PromotionInfoBar(profile); +#endif + } } else { RecordLaunchModeHistogram(LM_AS_WEBAPP); } diff --git a/chrome/browser/browser_main_mac.mm b/chrome/browser/browser_main_mac.mm index 27024f06..c621b97 100644 --- a/chrome/browser/browser_main_mac.mm +++ b/chrome/browser/browser_main_mac.mm @@ -12,10 +12,10 @@ #include "base/command_line.h" #include "base/debug_util.h" #include "chrome/app/breakpad_mac.h" -#import "chrome/app/keystone_glue.h" #import "chrome/browser/app_controller_mac.h" #include "chrome/browser/browser_main_win.h" #import "chrome/browser/chrome_browser_application_mac.h" +#import "chrome/browser/cocoa/keystone_glue.h" #include "chrome/browser/metrics/metrics_service.h" #include "chrome/common/main_function_params.h" #include "chrome/common/result_codes.h" diff --git a/chrome/browser/cocoa/about_window_controller.h b/chrome/browser/cocoa/about_window_controller.h index 99e175e..fd1bd4b 100644 --- a/chrome/browser/cocoa/about_window_controller.h +++ b/chrome/browser/cocoa/about_window_controller.h @@ -23,14 +23,25 @@ class Profile; IBOutlet NSImageView* logoView_; IBOutlet NSView* legalBlock_; IBOutlet AboutLegalTextView* legalText_; - IBOutlet NSView* updateBlock_; // Holds everything related to updates + + // updateBlock_ holds the update image or throbber, update text, and update + // button. + IBOutlet NSView* updateBlock_; + + // promotionBlock_ holds the Keystone ticket promotion text and button. + IBOutlet NSView* promotionBlock_; + IBOutlet NSProgressIndicator* spinner_; IBOutlet NSImageView* updateStatusIndicator_; IBOutlet NSTextField* updateText_; IBOutlet NSButton* updateNowButton_; + IBOutlet NSButton* promoteButton_; - BOOL updateTriggered_; // Has an update ever been triggered? Profile* profile_; // Weak, probably the default profile. + + // The window frame height. During an animation, this will contain the + // height being animated to. + CGFloat windowHeight_; } // Initialize the controller with the given profile, but does not show it. @@ -40,6 +51,10 @@ class Profile; // Trigger an update right now, as initiated by a button. - (IBAction)updateNow:(id)sender; +// Install a system Keystone if necessary and promote the ticket to a system +// ticket. +- (IBAction)promoteUpdater:(id)sender; + @end // @interface AboutWindowController @interface AboutWindowController(JustForTesting) diff --git a/chrome/browser/cocoa/about_window_controller.mm b/chrome/browser/cocoa/about_window_controller.mm index 1cce20c9..416da37 100644 --- a/chrome/browser/cocoa/about_window_controller.mm +++ b/chrome/browser/cocoa/about_window_controller.mm @@ -2,16 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import "chrome/browser/cocoa/about_window_controller.h" + #include "app/l10n_util_mac.h" #include "app/resource_bundle.h" #include "base/logging.h" #include "base/mac_util.h" #include "base/string_util.h" #include "base/sys_string_conversions.h" -#import "chrome/app/keystone_glue.h" #include "chrome/browser/browser_list.h" -#import "chrome/browser/cocoa/about_window_controller.h" #import "chrome/browser/cocoa/background_tile_view.h" +#import "chrome/browser/cocoa/keystone_glue.h" #include "chrome/browser/cocoa/restart_browser.h" #include "grit/chromium_strings.h" #include "grit/generated_resources.h" @@ -59,6 +60,15 @@ void AttributedStringAppendHyperlink(NSMutableAttributedString* attr_str, // Launches a check for available updates. - (void)checkForUpdate; +// Turns the update and promotion blocks on and off as needed based on whether +// updates are possible and promotion is desired or required. +- (void)adjustUpdateUIVisibility; + +// Maintains the update and promotion block visibility and window sizing. +// This uses bool instead of BOOL for the convenience of the internal +// implementation. +- (void)setAllowsUpdate:(bool)update allowsPromotion:(bool)promotion; + // Notification callback, called with the status of asynchronous // -checkForUpdate and -updateNow: operations. - (void)updateStatus:(NSNotification*)notification; @@ -106,11 +116,11 @@ void AttributedStringAppendHyperlink(NSMutableAttributedString* attr_str, // YES when an About box is currently showing the kAutoupdateInstallFailed // status, or if no About box is visible, if the most recent About box to be // closed was closed while showing this status. When an About box opens, if -// the recent status is kAutoupdateInstallFailed and -// recentShownInstallFailedStatus is NO, the failure needs to be shown instead -// of launching a new update check. recentShownInstallFailedStatus is +// the recent status is kAutoupdateInstallFailed or kAutoupdatePromoteFailed +// and recentShownUserActionFailedStatus is NO, the failure needs to be shown +// instead of launching a new update check. recentShownInstallFailedStatus is // maintained by -updateStatus:. -static BOOL recentShownInstallFailedStatus = NO; +static BOOL recentShownUserActionFailedStatus = NO; - (void)awakeFromNib { NSBundle* bundle = mac_util::MainAppBundle(); @@ -146,7 +156,7 @@ static BOOL recentShownInstallFailedStatus = NO; // difference in |legalShift|. We do something similar with |updateShift| // below, which is either 0, or the amount of space to offset the window size // because the view that contains the update button has been removed because - // this build doesn't have KeyStone. + // this build doesn't have Keystone. NSRect oldLegalRect = [legalBlock_ frame]; [legalText_ sizeToFit]; NSRect newRect = oldLegalRect; @@ -154,19 +164,39 @@ static BOOL recentShownInstallFailedStatus = NO; [legalBlock_ setFrame:newRect]; CGFloat legalShift = newRect.size.height - oldLegalRect.size.height; - KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; - CGFloat updateShift; - if (keystoneGlue) { + NSRect backgroundFrame = [backgroundView_ frame]; + backgroundFrame.origin.y += legalShift; + [backgroundView_ setFrame:backgroundFrame]; + + NSSize windowDelta = NSMakeSize(0.0, legalShift); + [GTMUILocalizerAndLayoutTweaker + resizeWindowWithoutAutoResizingSubViews:[self window] + delta:windowDelta]; + + windowHeight_ = [[self window] frame].size.height; + + [self adjustUpdateUIVisibility]; + + // Don't do anything update-related if adjustUpdateUIVisibility decided that + // updates aren't possible. + if (![updateBlock_ isHidden]) { + KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; + AutoupdateStatus recentStatus = [keystoneGlue recentStatus]; if ([keystoneGlue asyncOperationPending] || - ([keystoneGlue recentStatus] == kAutoupdateInstallFailed && - !recentShownInstallFailedStatus)) { + recentStatus == kAutoupdateRegisterFailed || + ((recentStatus == kAutoupdateInstallFailed || + recentStatus == kAutoupdatePromoteFailed) && + !recentShownUserActionFailedStatus)) { // If an asynchronous update operation is currently pending, such as a // check for updates or an update installation attempt, set the status // up correspondingly without launching a new update check. // - // If a previous update attempt was unsuccessful but no About box was - // around to report the error, show it now, and allow another chance to - // install the update. + // If registration failed, no other operations make sense, so just go + // straight to the error. + // + // If a previous update or promotion attempt was unsuccessful but no + // About box was around to report the error, show it now, and allow + // another chance to perform the action. [self updateStatus:[keystoneGlue recentNotification]]; } else { // Launch a new update check, even if one was already completed, because @@ -174,34 +204,174 @@ static BOOL recentShownInstallFailedStatus = NO; // in the background since the last time an About box was displayed. [self checkForUpdate]; } + } + + [[self window] center]; +} + +- (void)windowWillClose:(NSNotification*)notification { + [self autorelease]; +} - updateShift = 0.0; +- (void)adjustUpdateUIVisibility { + bool allowUpdate; + bool allowPromotion; + + KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; + if (keystoneGlue && ![keystoneGlue isOnReadOnlyFilesystem]) { + AutoupdateStatus recentStatus = [keystoneGlue recentStatus]; + if (recentStatus == kAutoupdateRegistering || + recentStatus == kAutoupdateRegisterFailed || + recentStatus == kAutoupdatePromoted) { + // Show the update block while registering so that there's a progress + // spinner, and if registration failed so that there's an error message. + // Show it following a promotion because updates should be possible + // after promotion successfully completes. + allowUpdate = true; + + // Promotion isn't possible at this point. + allowPromotion = false; + } else if (recentStatus == kAutoupdatePromoteFailed) { + // TODO(mark): Add kAutoupdatePromoting to this block. KSRegistration + // currently handles the promotion synchronously, meaning that the main + // thread's loop doesn't spin, meaning that animations and other updates + // to the window won't occur until KSRegistration is done with + // promotion. This looks laggy and bad and probably qualifies as + // "jank." For now, there just won't be any visual feedback while + // promotion is in progress, but it should complete (or fail) very + // quickly. http://b/2290009. + // + // Also see the TODO for kAutoupdatePromoting in -updateStatus:version:. + // + // Show the update block so that there's some visual feedback that + // promotion is under way or that it's failed. Show the promotion block + // because the user either just clicked that button or because the user + // should be able to click it again. + allowUpdate = true; + allowPromotion = true; + } else { + // Show the update block only if a promotion is not absolutely required. + allowUpdate = ![keystoneGlue needsPromotion]; + + // Show the promotion block if promotion is a possibility. + allowPromotion = [keystoneGlue wantsPromotion]; + } } else { - // Hide all the update UI - [updateBlock_ setHidden:YES]; + // There is no glue, or the application is on a read-only filesystem. + // Updates and promotions are impossible. + allowUpdate = false; + allowPromotion = false; + } + + [self setAllowsUpdate:allowUpdate allowsPromotion:allowPromotion]; +} - // Figure out the amount being removed by taking out the update block - // and its spacing. - updateShift = NSMinY([legalBlock_ frame]) - NSMinY([updateBlock_ frame]); +- (void)setAllowsUpdate:(bool)update allowsPromotion:(bool)promotion { + bool oldUpdate = ![updateBlock_ isHidden]; + bool oldPromotion = ![promotionBlock_ isHidden]; - NSRect legalFrame = [legalBlock_ frame]; - legalFrame.origin.y -= updateShift; - [legalBlock_ setFrame:legalFrame]; + if (promotion == oldPromotion && update == oldUpdate) { + return; } - NSRect backgroundFrame = [backgroundView_ frame]; - backgroundFrame.origin.y += legalShift - updateShift; - [backgroundView_ setFrame:backgroundFrame]; + NSRect updateFrame = [updateBlock_ frame]; + NSRect promotionFrame = [promotionBlock_ frame]; + + // This routine assumes no space between the update and promotion blocks. + DCHECK_EQ(NSMinY(updateFrame), NSMaxY(promotionFrame)); + + bool promotionOnly = !update && promotion; + bool oldPromotionOnly = !oldUpdate && oldPromotion; + if (promotionOnly != oldPromotionOnly) { + // The window is transitioning from having a promotion block and no update + // block to any other state, or the other way around. Move the promotion + // frame up so that its top edge is in the same position as the update + // frame's top edge, or move it back down to its original location. + promotionFrame.origin.y += (promotionOnly ? 1.0 : -1.0) * + NSHeight(updateFrame); + } - NSSize windowDelta = NSMakeSize(0.0, legalShift - updateShift); + CGFloat delta = 0.0; - [GTMUILocalizerAndLayoutTweaker - resizeWindowWithoutAutoResizingSubViews:[self window] - delta:windowDelta]; -} + if (update != oldUpdate) { + [updateBlock_ setHidden:!update]; + delta += (update ? 1.0 : -1.0) * NSHeight(updateFrame); + } -- (void)windowWillClose:(NSNotification*)notification { - [self autorelease]; + if (promotion != oldPromotion) { + [promotionBlock_ setHidden:!promotion]; + delta += (promotion ? 1.0 : -1.0) * NSHeight(promotionFrame); + } + + NSRect legalFrame = [legalBlock_ frame]; + + bool updateOrPromotion = update || promotion; + bool oldUpdateOrPromotion = oldUpdate || oldPromotion; + if (updateOrPromotion != oldUpdateOrPromotion) { + // The window is transitioning from having an update or promotion block or + // both to not having either, or the other way around. Adjust delta to + // account for the space between the legal block and the update or + // promotion block. When the update and promotion blocks are not visible, + // this extra spacing is not used. + delta += (updateOrPromotion ? 1.0 : -1.0) * + (NSMinY(legalFrame) - NSMaxY(updateFrame)); + } + + // The promotion frame may have changed even if delta is 0, so always reset + // its frame. + promotionFrame.origin.y += delta; + [promotionBlock_ setFrame:promotionFrame]; + + if (delta) { + updateFrame.origin.y += delta; + [updateBlock_ setFrame:updateFrame]; + + legalFrame.origin.y += delta; + [legalBlock_ setFrame:legalFrame]; + + NSRect backgroundFrame = [backgroundView_ frame]; + backgroundFrame.origin.y += delta; + [backgroundView_ setFrame:backgroundFrame]; + + // GTMUILocalizerAndLayoutTweaker resizes the window without any + // opportunity for animation. In order to animate, disable window + // updates, save the current frame, let GTMUILocalizerAndLayoutTweaker do + // its thing, save the desired frame, restore the original frame, and then + // animate. + NSWindow* window = [self window]; + [window disableScreenUpdatesUntilFlush]; + + NSRect oldFrame = [window frame]; + + // GTMUILocalizerAndLayoutTweaker applies its delta to the window's + // current size (like oldFrame.size), but oldFrame isn't trustworthy if + // an animation is in progress. Set the window's frame to + // intermediateFrame, which is a frame of the size that an existing + // animation is animating to, so that GTM can apply the delta to the right + // size. + NSRect intermediateFrame = oldFrame; + intermediateFrame.origin.y -= intermediateFrame.size.height - windowHeight_; + intermediateFrame.size.height = windowHeight_; + [window setFrame:intermediateFrame display:NO]; + + NSSize windowDelta = NSMakeSize(0.0, delta); + [GTMUILocalizerAndLayoutTweaker + resizeWindowWithoutAutoResizingSubViews:window + delta:windowDelta]; + [window setFrameTopLeftPoint:NSMakePoint(NSMinX(intermediateFrame), + NSMaxY(intermediateFrame))]; + NSRect newFrame = [window frame]; + + windowHeight_ += delta; + + if (![[self window] isVisible]) { + // Don't animate if the window isn't on screen yet. + [window setFrame:newFrame display:NO]; + } else { + [window setFrame:oldFrame display:NO]; + [window setFrame:newFrame display:YES animate:YES]; + } + } } - (void)setUpdateThrobberMessage:(NSString*)message { @@ -238,8 +408,6 @@ static BOOL recentShownInstallFailedStatus = NO; } - (IBAction)updateNow:(id)sender { - updateTriggered_ = YES; - [[KeystoneGlue defaultKeystoneGlue] installUpdate]; // Immediately, kAutoupdateStatusNotification will be posted, and @@ -250,8 +418,23 @@ static BOOL recentShownInstallFailedStatus = NO; // installation attempt. } +- (IBAction)promoteUpdater:(id)sender { + [[KeystoneGlue defaultKeystoneGlue] promoteTicket]; + + // Immediately, kAutoupdateStatusNotification will be posted, and + // -updateStatus: will be called with status kAutoupdatePromoting. + // + // Upon completion, kAutoupdateStatusNotification will be posted, and + // -updateStatus: will be called with a status indicating a result of the + // installation attempt. + // + // If the promotion was successful, KeystoneGlue will re-register the ticket + // and -updateStatus: will be called again indicating first that + // registration is in progress and subsequently that it has completed. +} + - (void)updateStatus:(NSNotification*)notification { - recentShownInstallFailedStatus = NO; + recentShownUserActionFailedStatus = NO; NSDictionary* dictionary = [notification userInfo]; AutoupdateStatus status = static_cast<AutoupdateStatus>( @@ -260,14 +443,48 @@ static BOOL recentShownInstallFailedStatus = NO; // Don't assume |version| is a real string. It may be nil. NSString* version = [dictionary objectForKey:kAutoupdateStatusVersion]; + bool updateMessage = true; bool throbber = false; int imageID = 0; NSString* message; + bool enableUpdateButton = false; + bool enablePromoteButton = true; switch (status) { + case kAutoupdateRegistering: + // When registering, use the "checking" message. The check will be + // launched if appropriate immediately after registration. + throbber = true; + message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED); + enablePromoteButton = false; + + break; + + case kAutoupdateRegistered: + // Once registered, the ability to update and promote is known. + [self adjustUpdateUIVisibility]; + + if (![updateBlock_ isHidden]) { + // If registration completes while the window is visible, go straight + // into an update check. Return immediately, this routine will be + // re-entered shortly with kAutoupdateChecking. + [self checkForUpdate]; + return; + } + + // Nothing actually failed, but updates aren't possible. The throbber + // and message are hidden, but they'll be reset to these dummy values + // just to get the throbber to stop spinning if it's running. + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + IntToString16(status)); + + break; + case kAutoupdateChecking: throbber = true; message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED); + enablePromoteButton = false; break; @@ -284,16 +501,14 @@ static BOOL recentShownInstallFailedStatus = NO; imageID = IDR_UPDATE_AVAILABLE; message = l10n_util::GetNSStringFWithFixup( IDS_UPGRADE_AVAILABLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); - [updateNowButton_ setEnabled:YES]; + enableUpdateButton = true; break; case kAutoupdateInstalling: - // Don't let anyone click "Update Now" twice. - [updateNowButton_ setEnabled:NO]; - throbber = true; message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_STARTED); + enablePromoteButton = false; break; @@ -320,15 +535,80 @@ static BOOL recentShownInstallFailedStatus = NO; break; + case kAutoupdatePromoting: +#if 1 + // TODO(mark): See the TODO in -adjustUpdateUIVisibility for an + // explanation of why nothing can be done here at the moment. When + // KSRegistration handles promotion asynchronously, this dummy block can + // be replaced with the #else block. For now, just leave the messaging + // alone. http://b/2290009. + updateMessage = false; +#else + // The visibility may be changing. + [self adjustUpdateUIVisibility]; + + // This is not a terminal state, and kAutoupdatePromoted or + // kAutoupdatePromoteFailed will follow. Use the throbber and + // "checking" message so that it looks like something's happening. + throbber = true; + message = l10n_util::GetNSStringWithFixup(IDS_UPGRADE_CHECK_STARTED); +#endif + + enablePromoteButton = false; + + break; + + case kAutoupdatePromoted: + // The visibility may be changing. + [self adjustUpdateUIVisibility]; + + if (![updateBlock_ isHidden]) { + // If promotion completes while the window is visible, go straight + // into an update check. Return immediately, this routine will be + // re-entered shortly with kAutoupdateChecking. + [self checkForUpdate]; + return; + } + + // Nothing actually failed, but updates aren't possible. The throbber + // and message are hidden, but they'll be reset to these dummy values + // just to get the throbber to stop spinning if it's running. + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + IntToString16(status)); + + break; + + case kAutoupdateRegisterFailed: + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + IntToString16(status)); + enablePromoteButton = false; + + break; + + case kAutoupdateCheckFailed: + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + IntToString16(status)); + + break; + case kAutoupdateInstallFailed: - recentShownInstallFailedStatus = YES; + recentShownUserActionFailedStatus = YES; + + imageID = IDR_UPDATE_FAIL; + message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, + IntToString16(status)); // Allow another chance. - [updateNowButton_ setEnabled:YES]; + enableUpdateButton = true; + + break; - // Fall through. + case kAutoupdatePromoteFailed: + recentShownUserActionFailedStatus = YES; - case kAutoupdateCheckFailed: imageID = IDR_UPDATE_FAIL; message = l10n_util::GetNSStringFWithFixup(IDS_UPGRADE_ERROR, IntToString16(status)); @@ -337,15 +617,24 @@ static BOOL recentShownInstallFailedStatus = NO; default: NOTREACHED(); + return; } - if (throbber) { - [self setUpdateThrobberMessage:message]; - } else { - DCHECK_NE(imageID, 0); - [self setUpdateImage:imageID message:message]; + if (updateMessage) { + if (throbber) { + [self setUpdateThrobberMessage:message]; + } else { + DCHECK_NE(imageID, 0); + [self setUpdateImage:imageID message:message]; + } } + + // Note that these buttons may be hidden depending on what + // -adjustUpdateUIVisibility did. Their enabled/disabled status doesn't + // necessarily have anything to do with their visibility. + [updateNowButton_ setEnabled:enableUpdateButton]; + [promoteButton_ setEnabled:enablePromoteButton]; } - (BOOL)textView:(NSTextView *)aTextView diff --git a/chrome/browser/cocoa/about_window_controller_unittest.mm b/chrome/browser/cocoa/about_window_controller_unittest.mm index 71ce7c7..64a4f6d 100644 --- a/chrome/browser/cocoa/about_window_controller_unittest.mm +++ b/chrome/browser/cocoa/about_window_controller_unittest.mm @@ -5,10 +5,10 @@ #import <Cocoa/Cocoa.h> #import "base/scoped_nsobject.h" -#import "chrome/app/keystone_glue.h" #import "chrome/browser/cocoa/about_window_controller.h" #include "chrome/browser/cocoa/browser_test_helper.h" #include "chrome/browser/cocoa/cocoa_test_helper.h" +#import "chrome/browser/cocoa/keystone_glue.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/platform_test.h" diff --git a/chrome/browser/cocoa/authorization_util.cc b/chrome/browser/cocoa/authorization_util.cc new file mode 100644 index 0000000..434f1a3 --- /dev/null +++ b/chrome/browser/cocoa/authorization_util.cc @@ -0,0 +1,118 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/cocoa/authorization_util.h" + +#include <sys/wait.h> + +#include <string> + +#include "base/eintr_wrapper.h" +#include "base/logging.h" +#include "base/string_util.h" + +namespace authorization_util { + +OSStatus ExecuteWithPrivilegesAndGetPID(AuthorizationRef authorization, + const char* tool_path, + AuthorizationFlags options, + const char** arguments, + FILE** pipe, + pid_t* pid) { + // pipe may be NULL, but this function needs one. In that case, use a local + // pipe. + FILE* local_pipe; + FILE** pipe_pointer; + if (pipe) { + pipe_pointer = pipe; + } else { + pipe_pointer = &local_pipe; + } + + // AuthorizationExecuteWithPrivileges wants |char* const*| for |arguments|, + // but it doesn't actually modify the arguments, and that type is kind of + // silly and callers probably aren't dealing with that. Put the cast here + // to make things a little easier on callers. + OSStatus status = AuthorizationExecuteWithPrivileges(authorization, + tool_path, + options, + (char* const*)arguments, + pipe_pointer); + if (status != errAuthorizationSuccess) { + return status; + } + + int line_pid = -1; + size_t line_length = 0; + char* line_c = fgetln(*pipe_pointer, &line_length); + if (line_c) { + if (line_length > 0 && line_c[line_length - 1] == '\n') { + // line_c + line_length is the start of the next line if there is one. + // Back up one character. + --line_length; + } + std::string line(line_c, line_length); + if (!StringToInt(line, &line_pid)) { + // StringToInt may have set line_pid to something, but if the conversion + // was imperfect, use -1. + LOG(ERROR) << "ExecuteWithPrivilegesAndGetPid: funny line: " << line; + line_pid = -1; + } + } else { + LOG(ERROR) << "ExecuteWithPrivilegesAndGetPid: no line"; + } + + if (!pipe) { + fclose(*pipe_pointer); + } + + if (pid) { + *pid = line_pid; + } + + return status; +} + +OSStatus ExecuteWithPrivilegesAndWait(AuthorizationRef authorization, + const char* tool_path, + AuthorizationFlags options, + const char** arguments, + FILE** pipe, + int* exit_status) { + pid_t pid; + OSStatus status = ExecuteWithPrivilegesAndGetPID(authorization, + tool_path, + options, + arguments, + pipe, + &pid); + if (status != errAuthorizationSuccess) { + return status; + } + + // exit_status may be NULL, but this function needs it. In that case, use a + // local version. + int local_exit_status; + int* exit_status_pointer; + if (exit_status) { + exit_status_pointer = exit_status; + } else { + exit_status_pointer = &local_exit_status; + } + + if (pid != -1) { + pid_t wait_result; + HANDLE_EINTR(wait_result = waitpid(pid, exit_status_pointer, 0)); + if (wait_result != pid) { + PLOG(ERROR) << "waitpid"; + *exit_status_pointer = -1; + } + } else { + *exit_status_pointer = -1; + } + + return status; +} + +} // namespace authorization_util diff --git a/chrome/browser/cocoa/authorization_util.h b/chrome/browser/cocoa/authorization_util.h new file mode 100644 index 0000000..2d7c09d --- /dev/null +++ b/chrome/browser/cocoa/authorization_util.h @@ -0,0 +1,58 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_COCOA_AUTHORIZATION_UTIL_H_ +#define CHROME_BROWSER_COCOA_AUTHORIZATION_UTIL_H_ + +// AuthorizationExecuteWithPrivileges fork()s and exec()s the tool, but it +// does not wait() for it. It also doesn't provide the caller with access to +// the forked pid. If used irresponsibly, zombie processes will accumulate. +// +// Apple's really gotten us between a rock and a hard place, here. +// +// Fortunately, AuthorizationExecuteWithPrivileges does give access to the +// tool's stdout (and stdin) via a FILE* pipe. The tool can output its pid +// to this pipe, and the main program can read it, and then have something +// that it can wait() for. +// +// The contract is that any tool executed by the wrappers declared in this +// file must print its pid to stdout on a line by itself before doing anything +// else. +// +// http://developer.apple.com/mac/library/samplecode/BetterAuthorizationSample/listing1.html +// (Look for "What's This About Zombies?") + +#include <Security/Authorization.h> +#include <stdio.h> +#include <sys/types.h> + +namespace authorization_util { + +// Calls straight through to AuthorizationExecuteWithPrivileges. If that +// call succeeds, |pid| will be set to the pid of the executed tool. If the +// pid can't be determined, |pid| will be set to -1. |pid| must not be NULL. +// |pipe| may be NULL, but the tool will always be executed with a pipe in +// order to read the pid from its stdout. +OSStatus ExecuteWithPrivilegesAndGetPID(AuthorizationRef authorization, + const char* tool_path, + AuthorizationFlags options, + const char** arguments, + FILE** pipe, + pid_t* pid); + +// Calls ExecuteWithPrivilegesAndGetPID, and if that call succeeds, calls +// waitpid() to wait for the process to exit. If waitpid() succeeds, the +// exit status is placed in |exit_status|, otherwise, -1 is stored. +// |exit_status| may be NULL and this function will still wait for the process +// to exit. +OSStatus ExecuteWithPrivilegesAndWait(AuthorizationRef authorization, + const char* tool_path, + AuthorizationFlags options, + const char** arguments, + FILE** pipe, + int* exit_status); + +} // namespace authorization_util + +#endif // CHROME_BROWSER_COCOA_AUTHORIZATION_UTIL_H_ diff --git a/chrome/browser/cocoa/keystone_glue.h b/chrome/browser/cocoa/keystone_glue.h new file mode 100644 index 0000000..81d74a3 --- /dev/null +++ b/chrome/browser/cocoa/keystone_glue.h @@ -0,0 +1,163 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_COCOA_KEYSTONE_GLUE_H_ +#define CHROME_BROWSER_COCOA_KEYSTONE_GLUE_H_ + +#import <Foundation/Foundation.h> +#import <base/scoped_nsobject.h> + +#include "chrome/browser/cocoa/scoped_authorizationref.h" + +// Possible outcomes of various operations. A version may accompany some of +// these, but beware: a version is never required. For statuses that can be +// accompanied by a version, the comment indicates what version is referenced. +// A notification posted containing an asynchronous status will always be +// followed by a notification with a terminal status. +enum AutoupdateStatus { + kAutoupdateNone = 0, // no version (initial state only) + kAutoupdateRegistering, // no version (asynchronous operation in progress) + kAutoupdateRegistered, // no version + kAutoupdateChecking, // no version (asynchronous operation in progress) + kAutoupdateCurrent, // version of the running application + kAutoupdateAvailable, // version of the update that is available + kAutoupdateInstalling, // no version (asynchronous operation in progress) + kAutoupdateInstalled, // version of the update that was installed + kAutoupdatePromoting, // no version (asynchronous operation in progress) + kAutoupdatePromoted, // no version + kAutoupdateRegisterFailed, // no version + kAutoupdateCheckFailed, // no version + kAutoupdateInstallFailed, // no version + kAutoupdatePromoteFailed, // no version +}; + +// kAutoupdateStatusNotification is the name of the notification posted when +// -checkForUpdate and -installUpdate complete. This notification will be +// sent with with its sender object set to the KeystoneGlue instance sending +// the notification. Its userInfo dictionary will contain an AutoupdateStatus +// value as an intValue at key kAutoupdateStatusStatus. If a version is +// available (see AutoupdateStatus), it will be present at key +// kAutoupdateStatusVersion. +extern const NSString* const kAutoupdateStatusNotification; +extern const NSString* const kAutoupdateStatusStatus; +extern const NSString* const kAutoupdateStatusVersion; + +// KeystoneGlue is an adapter around the KSRegistration class, allowing it to +// be used without linking directly against its containing KeystoneRegistration +// framework. This is used in an environment where most builds (such as +// developer builds) don't want or need Keystone support and might not even +// have the framework available. Enabling Keystone support in an application +// that uses KeystoneGlue is as simple as dropping +// KeystoneRegistration.framework in the application's Frameworks directory +// and providing the relevant information in its Info.plist. KeystoneGlue +// requires that the KSUpdateURL key be set in the application's Info.plist, +// and that it contain a string identifying the update URL to be used by +// Keystone. + +@class KSRegistration; + +@interface KeystoneGlue : NSObject { + @protected + + // Data for Keystone registration + NSString* productID_; + NSString* appPath_; + NSString* url_; + NSString* version_; + NSString* channel_; // Logically: Dev, Beta, or Stable. + + // And the Keystone registration itself, with the active timer + KSRegistration* registration_; // strong + NSTimer* timer_; // strong + + // The most recent kAutoupdateStatusNotification notification posted. + scoped_nsobject<NSNotification> recentNotification_; + + // The authorization object, when it needs to persist because it's being + // carried across threads. + scoped_AuthorizationRef authorization_; + + // YES if an update was ever successfully installed by -installUpdate. + BOOL updateSuccessfullyInstalled_; +} + +// Return the default Keystone Glue object. ++ (id)defaultKeystoneGlue; + +// Load KeystoneRegistration.framework if present, call into it to register +// with Keystone, and set up periodic activity pings. +- (void)registerWithKeystone; + +// -checkForUpdate launches a check for updates, and -installUpdate begins +// installing an available update. For each, status will be communicated via +// a kAutoupdateStatusNotification notification, and will also be available +// through -recentNotification. +- (void)checkForUpdate; +- (void)installUpdate; + +// Accessor for recentNotification_. Returns an autoreleased NSNotification. +- (NSNotification*)recentNotification; + +// Accessor for the kAutoupdateStatusStatus field of recentNotification_'s +// userInfo dictionary. +- (AutoupdateStatus)recentStatus; + +// Returns YES if an asynchronous operation is pending: if an update check or +// installation attempt is currently in progress. +- (BOOL)asyncOperationPending; + +// Returns YES if the application is running from a read-only filesystem, +// such as a disk image. +- (BOOL)isOnReadOnlyFilesystem; + +// -needsPromotion is YES if the application needs its ticket promoted to +// a system ticket. This will be YES when the application is on a user +// ticket and determines that the current user does not have sufficient +// permission to perform the update. +// +// -wantsPromotion is YES if the application wants its ticket promoted to +// a system ticket, even if it doesn't need it as determined by +// -needsPromotion. -wantsPromotion will always be YES if -needsPromotion is, +// and it will additionally be YES when the application is on a user ticket +// and appears to be installed in a system-wide location such as +// /Applications. +// +// Use -needsPromotion to decide whether to show any update UI at all. If +// it's YES, there's no sense in asking the user to "update now" because it +// will fail given the rights and permissions involved. On the other hand, +// when -needsPromotion is YES, the application can encourage the user to +// promote the ticket so that updates will work properly. +// +// Use -wantsPromotion to decide whether to allow the user to promote. The +// user shouldn't be nagged about promotion on the basis of -wantsPromotion, +// but if it's YES, the user should be allowed to promote the ticket. +- (BOOL)needsPromotion; +- (BOOL)wantsPromotion; + +// Requests authorization and promotes the Keystone ticket into the system +// store. System Keystone will be installed if necessary. +- (void)promoteTicket; + +@end // @interface KeystoneGlue + +@interface KeystoneGlue(ExposedForTesting) + +// Load any params we need for configuring Keystone. +- (void)loadParameters; + +// Load the Keystone registration object. +// Return NO on failure. +- (BOOL)loadKeystoneRegistration; + +- (void)stopTimer; + +// Called when a checkForUpdate: notification completes. +- (void)checkForUpdateComplete:(NSNotification*)notification; + +// Called when an installUpdate: notification completes. +- (void)installUpdateComplete:(NSNotification*)notification; + +@end // @interface KeystoneGlue(ExposedForTesting) + +#endif // CHROME_BROWSER_COCOA_KEYSTONE_GLUE_H_ diff --git a/chrome/browser/cocoa/keystone_glue.mm b/chrome/browser/cocoa/keystone_glue.mm new file mode 100644 index 0000000..3ef10bc --- /dev/null +++ b/chrome/browser/cocoa/keystone_glue.mm @@ -0,0 +1,728 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/cocoa/keystone_glue.h" + +#include <sys/param.h> +#include <sys/mount.h> + +#include <vector> + +#import "app/l10n_util_mac.h" +#include "base/logging.h" +#include "base/mac_util.h" +#import "base/worker_pool_mac.h" +#include "chrome/browser/cocoa/authorization_util.h" +#include "chrome/common/chrome_constants.h" +#include "grit/chromium_strings.h" +#include "grit/generated_resources.h" + +namespace { + +// Provide declarations of the Keystone registration bits needed here. From +// KSRegistration.h. +typedef enum { + kKSPathExistenceChecker, +} KSExistenceCheckerType; + +typedef enum { + kKSRegistrationUserTicket, + kKSRegistrationSystemTicket, + kKSRegistrationDontKnowWhatKindOfTicket, +} KSRegistrationTicketType; + +NSString *KSRegistrationDidCompleteNotification = + @"KSRegistrationDidCompleteNotification"; +NSString *KSRegistrationPromotionDidCompleteNotification = + @"KSRegistrationPromotionDidCompleteNotification"; + +NSString *KSRegistrationCheckForUpdateNotification = + @"KSRegistrationCheckForUpdateNotification"; +NSString *KSRegistrationStatusKey = @"Status"; +NSString *KSRegistrationVersionKey = @"Version"; +NSString *KSRegistrationUpdateCheckErrorKey = @"Error"; + +NSString *KSRegistrationStartUpdateNotification = + @"KSRegistrationStartUpdateNotification"; +NSString *KSUpdateCheckSuccessfulKey = @"CheckSuccessful"; +NSString *KSUpdateCheckSuccessfullyInstalledKey = @"SuccessfullyInstalled"; + +NSString *KSRegistrationRemoveExistingTag = @""; +#define KSRegistrationPreserveExistingTag nil + +} // namespace + +@interface KSRegistration : NSObject + ++ (id)registrationWithProductID:(NSString*)productID; + +- (BOOL)registerWithVersion:(NSString*)version + existenceCheckerType:(KSExistenceCheckerType)xctype + existenceCheckerString:(NSString*)xc + serverURLString:(NSString*)serverURLString + preserveTTToken:(BOOL)preserveToken + tag:(NSString*)tag; + +- (BOOL)promoteWithVersion:(NSString*)version + existenceCheckerType:(KSExistenceCheckerType)xctype + existenceCheckerString:(NSString*)xc + serverURLString:(NSString*)serverURLString + preserveTTToken:(BOOL)preserveToken + tag:(NSString*)tag + authorization:(AuthorizationRef)authorization; + +- (void)setActive; +- (void)checkForUpdate; +- (void)startUpdate; +- (KSRegistrationTicketType)ticketType; + +@end // @interface KSRegistration + +@interface KeystoneGlue(Private) + +// Called when Keystone registration completes. +- (void)registrationComplete:(NSNotification*)notification; + +// Called periodically to announce activity by pinging the Keystone server. +- (void)markActive:(NSTimer*)timer; + +// Called when an update check or update installation is complete. Posts the +// kAutoupdateStatusNotification notification to the default notification +// center. +- (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version; + +// These three methods are used to determine the version of the application +// currently installed on disk, compare that to the currently-running version, +// decide whether any updates have been installed, and call +// -updateStatus:version:. +// +// In order to check the version on disk, the installed application's +// Info.plist dictionary must be read; in order to see changes as updates are +// applied, the dictionary must be read each time, bypassing any caches such +// as the one that NSBundle might be maintaining. Reading files can be a +// blocking operation, and blocking operations are to be avoided on the main +// thread. I'm not quite sure what jank means, but I bet that a blocked main +// thread would cause some of it. +// +// -determineUpdateStatusAsync is called on the main thread to initiate the +// operation. It performs initial set-up work that must be done on the main +// thread and arranges for -determineUpdateStatus to be called on a work queue +// thread managed by NSOperationQueue. +// -determineUpdateStatus then reads the Info.plist, gets the version from the +// CFBundleShortVersionString key, and performs +// -determineUpdateStatusForVersion: on the main thread. +// -determineUpdateStatusForVersion: does the actual comparison of the version +// on disk with the running version and calls -updateStatus:version: with the +// results of its analysis. +- (void)determineUpdateStatusAsync; +- (void)determineUpdateStatus; +- (void)determineUpdateStatusForVersion:(NSString*)version; + +// Returns YES if registration_ is definitely on a user ticket. If definitely +// on a system ticket, or uncertain of ticket type (due to an older version +// of Keystone being used), returns NO. +- (BOOL)isUserTicket; + +// Called when ticket promotion completes. +- (void)promotionComplete:(NSNotification*)notification; + +// Changes the application's ownership and permissions so that all files are +// owned by root:wheel and all files and directories are writable only by +// root, but readable and executable as needed by everyone. +// -changePermissionsForPromotionAsync is called on the main thread by +// -promotionComplete. That routine calls +// -changePermissionsForPromotionWithTool: on a work queue thread. When done, +// -changePermissionsForPromotionComplete is called on the main thread. +- (void)changePermissionsForPromotionAsync; +- (void)changePermissionsForPromotionWithTool:(NSString*)toolPath; +- (void)changePermissionsForPromotionComplete; + +@end // @interface KeystoneGlue(Private) + +const NSString* const kAutoupdateStatusNotification = + @"AutoupdateStatusNotification"; +const NSString* const kAutoupdateStatusStatus = @"status"; +const NSString* const kAutoupdateStatusVersion = @"version"; + +@implementation KeystoneGlue + ++ (id)defaultKeystoneGlue { + static bool sTriedCreatingDefaultKeystoneGlue = false; + // TODO(jrg): use base::SingletonObjC<KeystoneGlue> + static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked + + if (!sTriedCreatingDefaultKeystoneGlue) { + sTriedCreatingDefaultKeystoneGlue = true; + + sDefaultKeystoneGlue = [[KeystoneGlue alloc] init]; + [sDefaultKeystoneGlue loadParameters]; + if (![sDefaultKeystoneGlue loadKeystoneRegistration]) { + [sDefaultKeystoneGlue release]; + sDefaultKeystoneGlue = nil; + } + } + return sDefaultKeystoneGlue; +} + +- (id)init { + if ((self = [super init])) { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + + [center addObserver:self + selector:@selector(registrationComplete:) + name:KSRegistrationDidCompleteNotification + object:nil]; + + [center addObserver:self + selector:@selector(promotionComplete:) + name:KSRegistrationPromotionDidCompleteNotification + object:nil]; + + [center addObserver:self + selector:@selector(checkForUpdateComplete:) + name:KSRegistrationCheckForUpdateNotification + object:nil]; + + [center addObserver:self + selector:@selector(installUpdateComplete:) + name:KSRegistrationStartUpdateNotification + object:nil]; + } + + return self; +} + +- (void)dealloc { + [productID_ release]; + [appPath_ release]; + [url_ release]; + [version_ release]; + [channel_ release]; + [registration_ release]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +- (NSDictionary*)infoDictionary { + // Use mac_util::MainAppBundle() to get the app framework's dictionary. + return [mac_util::MainAppBundle() infoDictionary]; +} + +- (void)loadParameters { + NSDictionary* infoDictionary = [self infoDictionary]; + + // Use [NSBundle mainBundle] to get the application's own bundle identifier + // and path, not the framework's. For auto-update, the application is + // what's significant here: it's used to locate the outermost part of the + // application for the existence checker and other operations that need to + // see the entire application bundle. + NSBundle* appBundle = [NSBundle mainBundle]; + + NSString* productID = [infoDictionary objectForKey:@"KSProductID"]; + if (productID == nil) { + productID = [appBundle bundleIdentifier]; + } + + NSString* appPath = [appBundle bundlePath]; + NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"]; + NSString* version = [infoDictionary objectForKey:@"KSVersion"]; + + if (!productID || !appPath || !url || !version) { + // If parameters required for Keystone are missing, don't use it. + return; + } + + NSString* channel = [infoDictionary objectForKey:@"KSChannelID"]; + // The stable channel has no tag. If updating to stable, remove the + // dev and beta tags since we've been "promoted". + if (channel == nil) + channel = KSRegistrationRemoveExistingTag; + + productID_ = [productID retain]; + appPath_ = [appPath retain]; + url_ = [url retain]; + version_ = [version retain]; + channel_ = [channel retain]; +} + +- (BOOL)loadKeystoneRegistration { + if (!productID_ || !appPath_ || !url_ || !version_) + return NO; + + // Load the KeystoneRegistration framework bundle if present. It lives + // inside the framework, so use mac_util::MainAppBundle(); + NSString* ksrPath = + [[mac_util::MainAppBundle() privateFrameworksPath] + stringByAppendingPathComponent:@"KeystoneRegistration.framework"]; + NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath]; + [ksrBundle load]; + + // Harness the KSRegistration class. + Class ksrClass = [ksrBundle classNamed:@"KSRegistration"]; + KSRegistration* ksr = [ksrClass registrationWithProductID:productID_]; + if (!ksr) + return NO; + + registration_ = [ksr retain]; + return YES; +} + +- (void)registerWithKeystone { + [self updateStatus:kAutoupdateRegistering version:nil]; + + if (![registration_ registerWithVersion:version_ + existenceCheckerType:kKSPathExistenceChecker + existenceCheckerString:appPath_ + serverURLString:url_ + preserveTTToken:YES + tag:channel_]) { + [self updateStatus:kAutoupdateRegisterFailed version:nil]; + return; + } + + // Upon completion, KSRegistrationDidCompleteNotification will be posted, + // and -registrationComplete: will be called. + + // Mark an active RIGHT NOW; don't wait an hour for the first one. + [registration_ setActive]; + + // Set up hourly activity pings. + timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60 // One hour + target:self + selector:@selector(markActive:) + userInfo:registration_ + repeats:YES]; +} + +- (void)registrationComplete:(NSNotification*)notification { + NSDictionary* userInfo = [notification userInfo]; + if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) { + [self updateStatus:kAutoupdateRegistered version:nil]; + } else { + // Dump registration_? + [self updateStatus:kAutoupdateRegisterFailed version:nil]; + } +} + +- (void)stopTimer { + [timer_ invalidate]; +} + +- (void)markActive:(NSTimer*)timer { + KSRegistration* ksr = [timer userInfo]; + [ksr setActive]; +} + +- (void)checkForUpdate { + DCHECK(![self asyncOperationPending]); + + if (!registration_) { + [self updateStatus:kAutoupdateCheckFailed version:nil]; + return; + } + + [self updateStatus:kAutoupdateChecking version:nil]; + + [registration_ checkForUpdate]; + + // Upon completion, KSRegistrationCheckForUpdateNotification will be posted, + // and -checkForUpdateComplete: will be called. +} + +- (void)checkForUpdateComplete:(NSNotification*)notification { + NSDictionary* userInfo = [notification userInfo]; + + if ([[userInfo objectForKey:KSRegistrationUpdateCheckErrorKey] boolValue]) { + [self updateStatus:kAutoupdateCheckFailed version:nil]; + } else if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) { + // If an update is known to be available, go straight to + // -updateStatus:version:. It doesn't matter what's currently on disk. + NSString* version = [userInfo objectForKey:KSRegistrationVersionKey]; + [self updateStatus:kAutoupdateAvailable version:version]; + } else { + // If no updates are available, check what's on disk, because an update + // may have already been installed. This check happens on another thread, + // and -updateStatus:version: will be called on the main thread when done. + [self determineUpdateStatusAsync]; + } +} + +- (void)installUpdate { + DCHECK(![self asyncOperationPending]); + + if (!registration_) { + [self updateStatus:kAutoupdateInstallFailed version:nil]; + return; + } + + [self updateStatus:kAutoupdateInstalling version:nil]; + + [registration_ startUpdate]; + + // Upon completion, KSRegistrationStartUpdateNotification will be posted, + // and -installUpdateComplete: will be called. +} + +- (void)installUpdateComplete:(NSNotification*)notification { + NSDictionary* userInfo = [notification userInfo]; + + if (![[userInfo objectForKey:KSUpdateCheckSuccessfulKey] boolValue] || + ![[userInfo objectForKey:KSUpdateCheckSuccessfullyInstalledKey] + intValue]) { + [self updateStatus:kAutoupdateInstallFailed version:nil]; + } else { + updateSuccessfullyInstalled_ = YES; + + // Nothing in the notification dictionary reports the version that was + // installed. Figure it out based on what's on disk. + [self determineUpdateStatusAsync]; + } +} + +// Runs on the main thread. +- (void)determineUpdateStatusAsync { + DCHECK([NSThread isMainThread]); + + SEL selector = @selector(determineUpdateStatus); + NSInvocationOperation* operation = + [[[NSInvocationOperation alloc] initWithTarget:self + selector:selector + object:nil] autorelease]; + + NSOperationQueue* operationQueue = [WorkerPoolObjC sharedOperationQueue]; + [operationQueue addOperation:operation]; +} + +// Runs on a thread managed by NSOperationQueue. +- (void)determineUpdateStatus { + DCHECK(![NSThread isMainThread]); + + NSString* appInfoPlistPath = + [[appPath_ stringByAppendingPathComponent:@"Contents"] + stringByAppendingPathComponent:@"Info.plist"]; + NSDictionary* infoPlist = + [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath]; + NSString* version = [infoPlist objectForKey:@"CFBundleShortVersionString"]; + + [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:) + withObject:version + waitUntilDone:NO]; +} + +// Runs on the main thread. +- (void)determineUpdateStatusForVersion:(NSString*)version { + DCHECK([NSThread isMainThread]); + + AutoupdateStatus status; + if (updateSuccessfullyInstalled_) { + // If an update was successfully installed and this object saw it happen, + // then don't even bother comparing versions. + status = kAutoupdateInstalled; + } else { + NSString* currentVersion = + [NSString stringWithUTF8String:chrome::kChromeVersion]; + if (!version) { + // If the version on disk could not be determined, assume that + // whatever's running is current. + version = currentVersion; + status = kAutoupdateCurrent; + } else if ([version isEqualToString:currentVersion]) { + status = kAutoupdateCurrent; + } else { + // If the version on disk doesn't match what's currently running, an + // update must have been applied in the background, without this app's + // direct participation. Leave updateSuccessfullyInstalled_ alone + // because there's no direct knowledge of what actually happened. + status = kAutoupdateInstalled; + } + } + + [self updateStatus:status version:version]; +} + +- (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version { + NSNumber* statusNumber = [NSNumber numberWithInt:status]; + NSMutableDictionary* dictionary = + [NSMutableDictionary dictionaryWithObject:statusNumber + forKey:kAutoupdateStatusStatus]; + if (version) { + [dictionary setObject:version forKey:kAutoupdateStatusVersion]; + } + + NSNotification* notification = + [NSNotification notificationWithName:kAutoupdateStatusNotification + object:self + userInfo:dictionary]; + recentNotification_.reset([notification retain]); + + [[NSNotificationCenter defaultCenter] postNotification:notification]; +} + +- (NSNotification*)recentNotification { + return [[recentNotification_ retain] autorelease]; +} + +- (AutoupdateStatus)recentStatus { + NSDictionary* dictionary = [recentNotification_ userInfo]; + return static_cast<AutoupdateStatus>( + [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); +} + +- (BOOL)asyncOperationPending { + AutoupdateStatus status = [self recentStatus]; + return status == kAutoupdateRegistering || + status == kAutoupdateChecking || + status == kAutoupdateInstalling || + status == kAutoupdatePromoting; +} + +- (BOOL)isUserTicket { + return [registration_ ticketType] == kKSRegistrationUserTicket; +} + +- (BOOL)isOnReadOnlyFilesystem { + const char* appPathC = [appPath_ fileSystemRepresentation]; + struct statfs statfsBuf; + + if (statfs(appPathC, &statfsBuf) != 0) { + PLOG(ERROR) << "statfs"; + // Be optimistic about the filesystem's writability. + return NO; + } + + return (statfsBuf.f_flags & MNT_RDONLY) != 0; +} + +- (BOOL)needsPromotion { + if (![self isUserTicket] || [self isOnReadOnlyFilesystem]) { + return NO; + } + + // Check the outermost bundle directory, the main executable path, and the + // framework directory. It may be enough to just look at the outermost + // bundle directory, but checking an interior file and directory can be + // helpful in case permissions are set differently only on the outermost + // directory. An interior file and directory are both checked because some + // file operations, such as Snow Leopard's Finder's copy operation when + // authenticating, may actually result in different ownership being applied + // to files and directories. + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSString* executablePath = [[NSBundle mainBundle] executablePath]; + NSString* frameworkPath = [mac_util::MainAppBundle() bundlePath]; + return ![fileManager isWritableFileAtPath:appPath_] || + ![fileManager isWritableFileAtPath:executablePath] || + ![fileManager isWritableFileAtPath:frameworkPath]; +} + +- (BOOL)wantsPromotion { + // -needsPromotion checks these too, but this method doesn't necessarily + // return NO just becuase -needsPromotion returns NO, so another check is + // needed here. + if (![self isUserTicket] || [self isOnReadOnlyFilesystem]) { + return NO; + } + + if ([self needsPromotion]) { + return YES; + } + + return [appPath_ hasPrefix:@"/Applications/"]; +} + +- (void)promoteTicket { + if ([self asyncOperationPending] || ![self wantsPromotion]) { + // Because there are multiple ways of reaching promoteTicket that might + // not lock each other out, it may be possible to arrive here while an + // asynchronous operation is pending, or even after promotion has already + // occurred. Just quietly return without doing anything. + return; + } + + // Create an empty AuthorizationRef. + scoped_AuthorizationRef authorization; + OSStatus status = AuthorizationCreate(NULL, + kAuthorizationEmptyEnvironment, + kAuthorizationFlagDefaults, + &authorization); + if (status != errAuthorizationSuccess) { + LOG(ERROR) << "AuthorizationCreate: " << status; + return; + } + + // Specify the "system.privilege.admin" right, which allows + // AuthorizationExecuteWithPrivileges to run commands as root. + AuthorizationItem rightItems[] = { + {kAuthorizationRightExecute, 0, NULL, 0} + }; + AuthorizationRights rights = {arraysize(rightItems), rightItems}; + + // product_logo_32.png is used instead of app.icns because Authorization + // Services requires an image that NSImage can read. + NSString* iconPath = + [mac_util::MainAppBundle() pathForResource:@"product_logo_32" + ofType:@"png"]; + const char* iconPathC = [iconPath fileSystemRepresentation]; + size_t iconPathLength = iconPathC ? strlen(iconPathC) : 0; + + // The OS will append " Type an administrator's name and password to allow + // <CFBundleDisplayName> to make changes." + NSString* prompt = l10n_util::GetNSStringFWithFixup( + IDS_PROMOTE_AUTHENTICATION_PROMPT, + l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); + const char* promptC = [prompt UTF8String]; + size_t promptLength = promptC ? strlen(promptC) : 0; + + AuthorizationItem environmentItems[] = { + {kAuthorizationEnvironmentIcon, iconPathLength, (void*)iconPathC, 0}, + {kAuthorizationEnvironmentPrompt, promptLength, (void*)promptC, 0} + }; + + AuthorizationEnvironment environment = {arraysize(environmentItems), + environmentItems}; + + AuthorizationFlags flags = kAuthorizationFlagDefaults | + kAuthorizationFlagInteractionAllowed | + kAuthorizationFlagExtendRights | + kAuthorizationFlagPreAuthorize; + + status = AuthorizationCopyRights(authorization, + &rights, + &environment, + flags, + NULL); + if (status != errAuthorizationSuccess) { + if (status != errAuthorizationCanceled) { + LOG(ERROR) << "AuthorizationCopyRights: " << status; + } + return; + } + + [self updateStatus:kAutoupdatePromoting version:nil]; + + // TODO(mark): Remove when able! + // + // keystone_promote_preflight is hopefully temporary. It's here to ensure + // that the Keystone system ticket store is in a usable state for all users + // on the system. Ideally, Keystone's installer or another part of Keystone + // would handle this. The underlying problem is http://b/2285921, and it + // causes http://b/2289908, which this workaround addresses. + // + // This is run synchronously, which isn't optimal, but + // -[KSRegistration promoteWithVersion:...] is currently synchronous too, + // and this operation needs to happen before that one. + // + // TODO(mark): Make asynchronous. That only makes sense if the promotion + // operation itself is asynchronous too. http://b/2290009. Hopefully, + // the Keystone promotion code will just be changed to do what preflight + // now does, and then the preflight script can be removed instead. + NSString* preflightPath = + [mac_util::MainAppBundle() pathForResource:@"keystone_promote_preflight" + ofType:@"sh"]; + const char* preflightPathC = [preflightPath fileSystemRepresentation]; + const char* arguments[] = {NULL}; + + int exit_status; + status = authorization_util::ExecuteWithPrivilegesAndWait( + authorization, + preflightPathC, + kAuthorizationFlagDefaults, + arguments, + NULL, // pipe + &exit_status); + if (status != errAuthorizationSuccess) { + LOG(ERROR) << "AuthorizationExecuteWithPrivileges preflight: " << status; + [self updateStatus:kAutoupdatePromoteFailed version:nil]; + return; + } + if (exit_status != 0) { + LOG(ERROR) << "keystone_promote_preflight status " << exit_status; + [self updateStatus:kAutoupdatePromoteFailed version:nil]; + return; + } + + // Hang on to the AuthorizationRef so that it can be used once promotion is + // complete. Do this before asking Keystone to promote the ticket, because + // -promotionComplete: may be called from inside the Keystone promotion + // call. + authorization_.swap(authorization); + + if (![registration_ promoteWithVersion:version_ + existenceCheckerType:kKSPathExistenceChecker + existenceCheckerString:appPath_ + serverURLString:url_ + preserveTTToken:YES + tag:channel_ + authorization:authorization_]) { + [self updateStatus:kAutoupdatePromoteFailed version:nil]; + authorization_.reset(); + return; + } + + // Upon completion, KSRegistrationPromotionDidCompleteNotification will be + // posted, and -promotionComplete: will be called. +} + +- (void)promotionComplete:(NSNotification*)notification { + NSDictionary* userInfo = [notification userInfo]; + if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) { + [self changePermissionsForPromotionAsync]; + } else { + authorization_.reset(); + [self updateStatus:kAutoupdatePromoteFailed version:nil]; + } +} + +- (void)changePermissionsForPromotionAsync { + // NSBundle is not documented as being thread-safe. Do NSBundle operations + // on the main thread before jumping over to a NSOperationQueue-managed + // thread to run the tool. + DCHECK([NSThread isMainThread]); + + SEL selector = @selector(changePermissionsForPromotionWithTool:); + NSString* toolPath = + [mac_util::MainAppBundle() pathForResource:@"keystone_promote_postflight" + ofType:@"sh"]; + + NSInvocationOperation* operation = + [[[NSInvocationOperation alloc] initWithTarget:self + selector:selector + object:toolPath] autorelease]; + + NSOperationQueue* operationQueue = [WorkerPoolObjC sharedOperationQueue]; + [operationQueue addOperation:operation]; +} + +- (void)changePermissionsForPromotionWithTool:(NSString*)toolPath { + const char* toolPathC = [toolPath fileSystemRepresentation]; + + const char* appPathC = [appPath_ fileSystemRepresentation]; + const char* arguments[] = {appPathC, NULL}; + + int exit_status; + OSStatus status = authorization_util::ExecuteWithPrivilegesAndWait( + authorization_, + toolPathC, + kAuthorizationFlagDefaults, + arguments, + NULL, // pipe + &exit_status); + if (status != errAuthorizationSuccess) { + LOG(ERROR) << "AuthorizationExecuteWithPrivileges postflight: " << status; + } else if (exit_status != 0) { + LOG(ERROR) << "keystone_promote_postflight status " << exit_status; + } + + SEL selector = @selector(changePermissionsForPromotionComplete); + [self performSelectorOnMainThread:selector + withObject:nil + waitUntilDone:NO]; +} + +- (void)changePermissionsForPromotionComplete { + authorization_.reset(); + + [self updateStatus:kAutoupdatePromoted version:nil]; +} + +@end // @implementation KeystoneGlue diff --git a/chrome/browser/cocoa/keystone_glue_unittest.mm b/chrome/browser/cocoa/keystone_glue_unittest.mm new file mode 100644 index 0000000..9d49a09 --- /dev/null +++ b/chrome/browser/cocoa/keystone_glue_unittest.mm @@ -0,0 +1,184 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Foundation/Foundation.h> +#import <objc/objc-class.h> + +#import "chrome/browser/cocoa/keystone_glue.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +@interface FakeGlueRegistration : NSObject +@end + + +@implementation FakeGlueRegistration + +// Send the notifications that a real KeystoneGlue object would send. + +- (void)checkForUpdate { + NSNumber* yesNumber = [NSNumber numberWithBool:YES]; + NSString* statusKey = @"Status"; + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:yesNumber + forKey:statusKey]; + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:@"KSRegistrationCheckForUpdateNotification" + object:nil + userInfo:dictionary]; +} + +- (void)startUpdate { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:@"KSRegistrationStartUpdateNotification" + object:nil]; +} + +@end + + +@interface FakeKeystoneGlue : KeystoneGlue { + @public + BOOL upToDate_; + NSString *latestVersion_; + BOOL successful_; + int installs_; +} + +- (void)fakeAboutWindowCallback:(NSNotification*)notification; +@end + + +@implementation FakeKeystoneGlue + +- (id)init { + if ((self = [super init])) { + // some lies + upToDate_ = YES; + latestVersion_ = @"foo bar"; + successful_ = YES; + installs_ = 1010101010; + + // Set up an observer that takes the notification that the About window + // listens for. + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(fakeAboutWindowCallback:) + name:kAutoupdateStatusNotification + object:nil]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +// For mocking +- (NSDictionary*)infoDictionary { + NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys: + @"http://foo.bar", @"KSUpdateURL", + @"com.google.whatever", @"KSProductID", + @"0.0.0.1", @"KSVersion", + nil]; + return dict; +} + +// For mocking +- (BOOL)loadKeystoneRegistration { + return YES; +} + +// Confirms certain things are happy +- (BOOL)dictReadCorrectly { + return ([url_ isEqual:@"http://foo.bar"] && + [productID_ isEqual:@"com.google.whatever"] && + [version_ isEqual:@"0.0.0.1"]); +} + +// Confirms certain things are happy +- (BOOL)hasATimer { + return timer_ ? YES : NO; +} + +- (void)addFakeRegistration { + registration_ = [[FakeGlueRegistration alloc] init]; +} + +- (void)fakeAboutWindowCallback:(NSNotification*)notification { + NSDictionary* dictionary = [notification userInfo]; + AutoupdateStatus status = static_cast<AutoupdateStatus>( + [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); + + if (status == kAutoupdateAvailable) { + upToDate_ = NO; + latestVersion_ = [dictionary objectForKey:kAutoupdateStatusVersion]; + } else if (status == kAutoupdateInstallFailed) { + successful_ = NO; + installs_ = 0; + } +} + +// Confirm we look like callbacks with nil NSNotifications +- (BOOL)confirmCallbacks { + return (!upToDate_ && + (latestVersion_ == nil) && + !successful_ && + (installs_ == 0)); +} + +@end + + +namespace { + +class KeystoneGlueTest : public PlatformTest { +}; + +// DISABLED because the mocking isn't currently working. +TEST_F(KeystoneGlueTest, DISABLED_BasicGlobalCreate) { + // Allow creation of a KeystoneGlue by mocking out a few calls + SEL ids = @selector(infoDictionary); + IMP oldInfoImp_ = [[KeystoneGlue class] instanceMethodForSelector:ids]; + IMP newInfoImp_ = [[FakeKeystoneGlue class] instanceMethodForSelector:ids]; + Method infoMethod_ = class_getInstanceMethod([KeystoneGlue class], ids); + method_setImplementation(infoMethod_, newInfoImp_); + + SEL lks = @selector(loadKeystoneRegistration); + IMP oldLoadImp_ = [[KeystoneGlue class] instanceMethodForSelector:lks]; + IMP newLoadImp_ = [[FakeKeystoneGlue class] instanceMethodForSelector:lks]; + Method loadMethod_ = class_getInstanceMethod([KeystoneGlue class], lks); + method_setImplementation(loadMethod_, newLoadImp_); + + KeystoneGlue *glue = [KeystoneGlue defaultKeystoneGlue]; + ASSERT_TRUE(glue); + + // Fix back up the class to the way we found it. + method_setImplementation(infoMethod_, oldInfoImp_); + method_setImplementation(loadMethod_, oldLoadImp_); +} + +// DISABLED because the mocking isn't currently working. +TEST_F(KeystoneGlueTest, DISABLED_BasicUse) { + FakeKeystoneGlue* glue = [[[FakeKeystoneGlue alloc] init] autorelease]; + [glue loadParameters]; + ASSERT_TRUE([glue dictReadCorrectly]); + + // Likely returns NO in the unit test, but call it anyway to make + // sure it doesn't crash. + [glue loadKeystoneRegistration]; + + // Confirm we start up an active timer + [glue registerWithKeystone]; + ASSERT_TRUE([glue hasATimer]); + [glue stopTimer]; + + // Brief exercise of callbacks + [glue addFakeRegistration]; + [glue checkForUpdate]; + [glue installUpdate]; + ASSERT_TRUE([glue confirmCallbacks]); +} + +} // namespace diff --git a/chrome/browser/cocoa/keystone_infobar.h b/chrome/browser/cocoa/keystone_infobar.h new file mode 100644 index 0000000..60083b9 --- /dev/null +++ b/chrome/browser/cocoa/keystone_infobar.h @@ -0,0 +1,23 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_COCOA_KEYSTONE_INFOBAR_H_ +#define CHROME_BROWSER_COCOA_KEYSTONE_INFOBAR_H_ + +class Profile; + +class KeystoneInfoBar { + public: + // If the application is Keystone-enabled and not on a read-only filesystem + // (capable of being auto-updated), and Keystone indicates that it needs + // ticket promotion, PromotionInfoBar displays an info bar asking the user + // to promote the ticket. The user will need to authenticate in order to + // gain authorization to perform the promotion. The info bar is not shown + // if its "don't ask" button was ever clicked, if the "don't check default + // browser" command-line flag is present, on the very first launch, or if + // another info bar is already showing in the active tab. + static void PromotionInfoBar(Profile* profile); +}; + +#endif // CHROME_BROWSER_COCOA_KEYSTONE_INFOBAR_H_ diff --git a/chrome/browser/cocoa/keystone_infobar.mm b/chrome/browser/cocoa/keystone_infobar.mm new file mode 100644 index 0000000..689a1ad --- /dev/null +++ b/chrome/browser/cocoa/keystone_infobar.mm @@ -0,0 +1,211 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/cocoa/keystone_infobar.h" + +#import <AppKit/AppKit.h> + +#include <string> + +#include "app/l10n_util.h" +#include "app/resource_bundle.h" +#include "base/command_line.h" +#include "base/message_loop.h" +#include "base/task.h" +#import "chrome/browser/cocoa/keystone_glue.h" +#include "chrome/browser/browser.h" +#include "chrome/browser/browser_list.h" +#include "chrome/browser/first_run.h" +#include "chrome/browser/tab_contents/infobar_delegate.h" +#include "chrome/browser/tab_contents/navigation_controller.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/profile.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/pref_names.h" +#include "grit/chromium_strings.h" +#include "grit/generated_resources.h" +#include "grit/theme_resources.h" + +class SkBitmap; + +namespace { + +class KeystonePromotionInfoBarDelegate : public ConfirmInfoBarDelegate { + public: + KeystonePromotionInfoBarDelegate(TabContents* tab_contents) + : ConfirmInfoBarDelegate(tab_contents), + profile_(tab_contents->profile()), + can_expire_(false), + ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) { + const int kCanExpireOnNavigationAfterMilliseconds = 8 * 1000; + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + method_factory_.NewRunnableMethod( + &KeystonePromotionInfoBarDelegate::SetCanExpire), + kCanExpireOnNavigationAfterMilliseconds); + } + + virtual ~KeystonePromotionInfoBarDelegate() {} + + // Inherited from InfoBarDelegate and overridden. + + virtual bool ShouldExpire( + const NavigationController::LoadCommittedDetails& details) { + return can_expire_; + } + + virtual void InfoBarClosed() { + delete this; + } + + // Inherited from AlertInfoBarDelegate and overridden. + + virtual std::wstring GetMessageText() const { + return l10n_util::GetStringF(IDS_PROMOTE_INFOBAR_TEXT, + l10n_util::GetString(IDS_PRODUCT_NAME)); + } + + virtual SkBitmap* GetIcon() const { + return ResourceBundle::GetSharedInstance().GetBitmapNamed( + IDR_PRODUCT_ICON_32); + } + + // Inherited from ConfirmInfoBarDelegate and overridden. + + virtual int GetButtons() const { + return BUTTON_OK | BUTTON_CANCEL | BUTTON_OK_DEFAULT; + } + + virtual std::wstring GetButtonLabel(InfoBarButton button) const { + return button == BUTTON_OK ? + l10n_util::GetString(IDS_PROMOTE_INFOBAR_PROMOTE_BUTTON) : + l10n_util::GetString(IDS_PROMOTE_INFOBAR_DONT_ASK_BUTTON); + } + + virtual bool Accept() { + [[KeystoneGlue defaultKeystoneGlue] promoteTicket]; + return true; + } + + virtual bool Cancel() { + profile_->GetPrefs()->SetBoolean(prefs::kShowUpdatePromotionInfoBar, false); + return true; + } + + private: + // Sets this info bar to be able to expire. Called a predetermined amount + // of time after this object is created. + void SetCanExpire() { + can_expire_ = true; + } + + // The TabContents' profile. + Profile* profile_; // weak + + // Whether the info bar should be dismissed on the next navigation. + bool can_expire_; + + // Used to delay the expiration of the info bar. + ScopedRunnableMethodFactory<KeystonePromotionInfoBarDelegate> method_factory_; + + DISALLOW_COPY_AND_ASSIGN(KeystonePromotionInfoBarDelegate); +}; + +} // namespace + +@interface KeystonePromotionInfoBar : NSObject +- (void)checkAndShowInfoBarForProfile:(Profile*)profile; +- (void)updateStatus:(NSNotification*)notification; +- (void)removeObserver; +@end // @interface KeystonePromotionInfoBar + +@implementation KeystonePromotionInfoBar + +- (void)dealloc { + [self removeObserver]; + [super dealloc]; +} + +- (void)checkAndShowInfoBarForProfile:(Profile*)profile { + // If this is the first run, the user clicked the "don't ask again" button + // at some point in the past, or if the "don't ask about the default + // browser" command-line switch is present, bail out. That command-line + // switch is recycled here because it's likely that the set of users that + // don't want to be nagged about the default browser also don't want to be + // nagged about the update check. (Automated testers, I'm thinking of + // you...) + CommandLine* commandLine = CommandLine::ForCurrentProcess(); + if (FirstRun::IsChromeFirstRun() || + !profile->GetPrefs()->GetBoolean(prefs::kShowUpdatePromotionInfoBar) || + commandLine->HasSwitch(switches::kNoDefaultBrowserCheck)) { + return; + } + + // If there is no Keystone glue (maybe because this application isn't + // Keystone-enabled) or the application is on a read-only filesystem, + // doing anything related to auto-update is pointless. Bail out. + KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue]; + if (!keystoneGlue || [keystoneGlue isOnReadOnlyFilesystem]) { + return; + } + + // Stay alive as long as needed. This is balanced by a release in + // -updateStatus:. + [self retain]; + + AutoupdateStatus recentStatus = [keystoneGlue recentStatus]; + if (recentStatus == kAutoupdateNone || + recentStatus == kAutoupdateRegistering) { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(updateStatus:) + name:kAutoupdateStatusNotification + object:nil]; + } else { + [self updateStatus:[keystoneGlue recentNotification]]; + } +} + +- (void)updateStatus:(NSNotification*)notification { + NSDictionary* dictionary = [notification userInfo]; + AutoupdateStatus status = static_cast<AutoupdateStatus>( + [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); + + if (status == kAutoupdateNone || status == kAutoupdateRegistering) { + return; + } + + [self removeObserver]; + + if (status != kAutoupdateRegisterFailed && + [[KeystoneGlue defaultKeystoneGlue] needsPromotion]) { + Browser* browser = BrowserList::GetLastActive(); + if (browser) { + TabContents* tabContents = browser->GetSelectedTabContents(); + + // Only show if no other info bars are showing, because that's how the + // default browser info bar works. + if (tabContents && tabContents->infobar_delegate_count() == 0) { + tabContents->AddInfoBar( + new KeystonePromotionInfoBarDelegate(tabContents)); + } + } + } + + [self release]; +} + +- (void)removeObserver { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +@end // @implementation KeystonePromotionInfoBar + +// static +void KeystoneInfoBar::PromotionInfoBar(Profile* profile) { + KeystonePromotionInfoBar* promotionInfoBar = + [[[KeystonePromotionInfoBar alloc] init] autorelease]; + + [promotionInfoBar checkAndShowInfoBarForProfile:profile]; +} diff --git a/chrome/browser/cocoa/keystone_promote_postflight.sh b/chrome/browser/cocoa/keystone_promote_postflight.sh new file mode 100755 index 0000000..5eb85d2 --- /dev/null +++ b/chrome/browser/cocoa/keystone_promote_postflight.sh @@ -0,0 +1,50 @@ +#!/bin/bash -p + +# Copyright (c) 2009 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Called as root after Keystone ticket promotion to change the owner, group, +# and permissions on the application. The application bundle and its contents +# are set to owner root, group wheel, and to be writable only by root, but +# readable and executable (when appropriate) by everyone. +# +# Note that this script will be invoked with the real user ID set to the +# user's ID, but the effective user ID set to 0 (root). bash -p is used on +# the first line to prevent bash from setting the effective user ID to the +# real user ID (dropping root privileges). + +set -e + +# This script runs as root, so be paranoid about things like ${PATH}. +export PATH="/usr/bin:/usr/sbin:/bin:/sbin" + +# Output the pid to stdout before doing anything else. See +# chrome/browser/cocoa/authorization_util.h. +echo "${$}" + +if [ ${#} -ne 1 ] ; then + echo "usage: ${0} APP" >& 2 + exit 2 +fi + +APP="${1}" + +# Make sure that APP is an absolute path and that it exists. +if [ -z "${APP}" ] || [ "${APP:0:1}" != "/" ] || [ ! -d "${APP}" ] ; then + echo "${0}: must provide an absolute path naming an extant directory" >& 2 + exit 3 +fi + +OWNER_GROUP="root:wheel" +chown -Rh "${OWNER_GROUP}" "${APP}" >& /dev/null + +CHMOD_MODE="a+rX,u+w,go-w" +chmod -R "${CHMOD_MODE}" "${APP}" >& /dev/null + +# On the Mac, or at least on HFS+, symbolic link permissions are significant, +# but chmod -R and -h can't be used together. Do another pass to fix the +# permissions on any symbolic links. +find "${APP}" -type l -exec chmod -h "${CHMOD_MODE}" {} + >& /dev/null + +exit 0 diff --git a/chrome/browser/cocoa/keystone_promote_preflight.sh b/chrome/browser/cocoa/keystone_promote_preflight.sh new file mode 100755 index 0000000..d1c4c59 --- /dev/null +++ b/chrome/browser/cocoa/keystone_promote_preflight.sh @@ -0,0 +1,59 @@ +#!/bin/bash -p + +# Copyright (c) 2009 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Called as root before Keystone ticket promotion to ensure a suitable +# environment for Keystone installation. Ultimately, these features should be +# integrated directly into the Keystone installation. +# +# Note that this script will be invoked with the real user ID set to the +# user's ID, but the effective user ID set to 0 (root). bash -p is used on +# the first line to prevent bash from setting the effective user ID to the +# real user ID (dropping root privileges). +# +# TODO(mark): Remove this script when able. See http://b/2285921 and +# http://b/2289908. + +set -e + +# This script runs as root, so be paranoid about things like ${PATH}. +export PATH="/usr/bin:/usr/sbin:/bin:/sbin" + +# Output the pid to stdout before doing anything else. See +# chrome/browser/cocoa/authorization_util.h. +echo "${$}" + +if [ ${#} -ne 0 ] ; then + echo "usage: ${0}" >& 2 + exit 2 +fi + +OWNER_GROUP="root:admin" +CHMOD_MODE="a+rX,u+w,go-w" + +LIB_GOOG="/Library/Google" +if [ -d "${LIB_GOOG}" ] ; then + # Just work with the directory. Don't do anything recursively here, so as + # to leave other things in /Library/Google alone. + chown -h "${OWNER_GROUP}" "${LIB_GOOG}" >& /dev/null + chmod -h "${CHMOD_MODE}" "${LIB_GOOG}" >& /dev/null + + LIB_GOOG_GSU="${LIB_GOOG}/GoogleSoftwareUpdate" + if [ -d "${LIB_GOOG_GSU}" ] ; then + chown -Rh "${OWNER_GROUP}" "${LIB_GOOG_GSU}" >& /dev/null + chmod -R "${CHMOD_MODE}" "${LIB_GOOG_GSU}" >& /dev/null + + # On the Mac, or at least on HFS+, symbolic link permissions are + # significant, but chmod -R and -h can't be used together. Do another + # pass to fix the permissions on any symbolic links. + find "${LIB_GOOG_GSU}" -type l -exec chmod -h "${CHMOD_MODE}" {} + >& \ + /dev/null + + # TODO(mark): If GoogleSoftwareUpdate.bundle is missing, dump TicketStore + # too? + fi +fi + +exit 0 diff --git a/chrome/browser/cocoa/scoped_authorizationref.h b/chrome/browser/cocoa/scoped_authorizationref.h new file mode 100644 index 0000000..5e6cff2 --- /dev/null +++ b/chrome/browser/cocoa/scoped_authorizationref.h @@ -0,0 +1,78 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_COCOA_SCOPED_AUTHORIZATIONREF_H_ +#define CHROME_BROWSER_COCOA_SCOPED_AUTHORIZATIONREF_H_ + +#include <Security/Authorization.h> + +#include "base/basictypes.h" + +// scoped_AuthorizationRef maintains ownership of an AuthorizationRef. It is +// patterned after the scoped_ptr interface. + +class scoped_AuthorizationRef { + public: + explicit scoped_AuthorizationRef(AuthorizationRef authorization = NULL) + : authorization_(authorization) { + } + + ~scoped_AuthorizationRef() { + if (authorization_) { + AuthorizationFree(authorization_, kAuthorizationFlagDestroyRights); + } + } + + void reset(AuthorizationRef authorization = NULL) { + if (authorization_ != authorization) { + if (authorization_) { + AuthorizationFree(authorization_, kAuthorizationFlagDestroyRights); + } + authorization_ = authorization; + } + } + + bool operator==(AuthorizationRef that) const { + return authorization_ == that; + } + + bool operator!=(AuthorizationRef that) const { + return authorization_ != that; + } + + operator AuthorizationRef() const { + return authorization_; + } + + AuthorizationRef* operator&() { + return &authorization_; + } + + AuthorizationRef get() const { + return authorization_; + } + + void swap(scoped_AuthorizationRef& that) { + AuthorizationRef temp = that.authorization_; + that.authorization_ = authorization_; + authorization_ = temp; + } + + // scoped_AuthorizationRef::release() is like scoped_ptr<>::release. It is + // NOT a wrapper for AuthorizationFree(). To force a + // scoped_AuthorizationRef object to call AuthorizationFree(), use + // scoped_AuthorizaitonRef::reset(). + AuthorizationRef release() { + AuthorizationRef temp = authorization_; + authorization_ = NULL; + return temp; + } + + private: + AuthorizationRef authorization_; + + DISALLOW_COPY_AND_ASSIGN(scoped_AuthorizationRef); +}; + +#endif // CHROME_BROWSER_COCOA_SCOPED_AUTHORIZATIONREF_H_ |