summaryrefslogtreecommitdiffstats
path: root/chrome/browser/cocoa
diff options
context:
space:
mode:
authorsail@chromium.org <sail@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-02-11 00:18:08 +0000
committersail@chromium.org <sail@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-02-11 00:18:08 +0000
commit2b78be3c1dd7cc0232aa1a24446a17299d5760e8 (patch)
tree39d8f59495b095b1857be6e908191c76ea13c386 /chrome/browser/cocoa
parent00c146f3971938025c5321cc31556ff127aebbc4 (diff)
downloadchromium_src-2b78be3c1dd7cc0232aa1a24446a17299d5760e8.zip
chromium_src-2b78be3c1dd7cc0232aa1a24446a17299d5760e8.tar.gz
chromium_src-2b78be3c1dd7cc0232aa1a24446a17299d5760e8.tar.bz2
Carnitas: Move non-ui Mac files out of browser/ui/cocoa
This is a part of the larger change to remove includes of browser/ui/cocoa/* from code that's outside of browser/ui. upgrade_detector.cc, browser_main.cc, and render_message_filter.cc were including files from browser/ui/cocoa/*. In this case all those files weren't actually UI related so I'm moving this out of browser/ui/cocoa and into browser/cocoa. BUG=None TEST=Compiling Review URL: http://codereview.chromium.org/6312165 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@74529 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/cocoa')
-rw-r--r--chrome/browser/cocoa/authorization_util.h67
-rw-r--r--chrome/browser/cocoa/authorization_util.mm183
-rw-r--r--chrome/browser/cocoa/install_from_dmg.h15
-rw-r--r--chrome/browser/cocoa/install_from_dmg.mm438
-rw-r--r--chrome/browser/cocoa/keystone_glue.h209
-rw-r--r--chrome/browser/cocoa/keystone_glue.mm957
-rw-r--r--chrome/browser/cocoa/keystone_glue_unittest.mm184
-rw-r--r--chrome/browser/cocoa/scoped_authorizationref.h80
-rw-r--r--chrome/browser/cocoa/task_helpers.h29
-rw-r--r--chrome/browser/cocoa/task_helpers.mm57
10 files changed, 2219 insertions, 0 deletions
diff --git a/chrome/browser/cocoa/authorization_util.h b/chrome/browser/cocoa/authorization_util.h
new file mode 100644
index 0000000..d5daf4a
--- /dev/null
+++ b/chrome/browser/cocoa/authorization_util.h
@@ -0,0 +1,67 @@
+// 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_
+#pragma once
+
+// 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 <CoreFoundation/CoreFoundation.h>
+#include <Security/Authorization.h>
+#include <stdio.h>
+#include <sys/types.h>
+
+namespace authorization_util {
+
+// Obtains an AuthorizationRef that can be used to run commands as root. If
+// necessary, prompts the user for authentication. If the user is prompted,
+// |prompt| will be used as the prompt string and an icon appropriate for the
+// application will be displayed in a prompt dialog. Note that the system
+// appends its own text to the prompt string. Returns NULL on failure.
+AuthorizationRef AuthorizationCreateToRunAsRoot(CFStringRef prompt);
+
+// 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/authorization_util.mm b/chrome/browser/cocoa/authorization_util.mm
new file mode 100644
index 0000000..81e9d4c
--- /dev/null
+++ b/chrome/browser/cocoa/authorization_util.mm
@@ -0,0 +1,183 @@
+// 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"
+
+#import <Foundation/Foundation.h>
+#include <sys/wait.h>
+
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/eintr_wrapper.h"
+#include "base/logging.h"
+#import "base/mac/mac_util.h"
+#include "base/string_number_conversions.h"
+#include "base/string_util.h"
+#include "chrome/browser/cocoa/scoped_authorizationref.h"
+
+namespace authorization_util {
+
+AuthorizationRef AuthorizationCreateToRunAsRoot(CFStringRef prompt) {
+ // Create an empty AuthorizationRef.
+ scoped_AuthorizationRef authorization;
+ OSStatus status = AuthorizationCreate(NULL,
+ kAuthorizationEmptyEnvironment,
+ kAuthorizationFlagDefaults,
+ &authorization);
+ if (status != errAuthorizationSuccess) {
+ LOG(ERROR) << "AuthorizationCreate: " << status;
+ return NULL;
+ }
+
+ // Specify the "system.privilege.admin" right, which allows
+ // AuthorizationExecuteWithPrivileges to run commands as root.
+ AuthorizationItem right_items[] = {
+ {kAuthorizationRightExecute, 0, NULL, 0}
+ };
+ AuthorizationRights rights = {arraysize(right_items), right_items};
+
+ // product_logo_32.png is used instead of app.icns because Authorization
+ // Services can't deal with .icns files.
+ NSString* icon_path =
+ [base::mac::MainAppBundle() pathForResource:@"product_logo_32"
+ ofType:@"png"];
+ const char* icon_path_c = [icon_path fileSystemRepresentation];
+ size_t icon_path_length = icon_path_c ? strlen(icon_path_c) : 0;
+
+ // The OS will append " Type an administrator's name and password to allow
+ // <CFBundleDisplayName> to make changes."
+ NSString* prompt_ns = base::mac::CFToNSCast(prompt);
+ const char* prompt_c = [prompt_ns UTF8String];
+ size_t prompt_length = prompt_c ? strlen(prompt_c) : 0;
+
+ AuthorizationItem environment_items[] = {
+ {kAuthorizationEnvironmentIcon, icon_path_length, (void*)icon_path_c, 0},
+ {kAuthorizationEnvironmentPrompt, prompt_length, (void*)prompt_c, 0}
+ };
+
+ AuthorizationEnvironment environment = {arraysize(environment_items),
+ environment_items};
+
+ AuthorizationFlags flags = kAuthorizationFlagDefaults |
+ kAuthorizationFlagInteractionAllowed |
+ kAuthorizationFlagExtendRights |
+ kAuthorizationFlagPreAuthorize;
+
+ status = AuthorizationCopyRights(authorization,
+ &rights,
+ &environment,
+ flags,
+ NULL);
+ if (status != errAuthorizationSuccess) {
+ if (status != errAuthorizationCanceled) {
+ LOG(ERROR) << "AuthorizationCopyRights: " << status;
+ }
+ return NULL;
+ }
+
+ return authorization.release();
+}
+
+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 (!base::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(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/install_from_dmg.h b/chrome/browser/cocoa/install_from_dmg.h
new file mode 100644
index 0000000..ec9248a
--- /dev/null
+++ b/chrome/browser/cocoa/install_from_dmg.h
@@ -0,0 +1,15 @@
+// Copyright (c) 2010 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_INSTALL_FROM_DMG_H_
+#define CHROME_BROWSER_COCOA_INSTALL_FROM_DMG_H_
+#pragma once
+
+// If the application is running from a read-only disk image, prompts the user
+// to install it to the hard drive. If the user approves, the application
+// will be installed and launched, and MaybeInstallFromDiskImage will return
+// true. In that case, the caller must exit expeditiously.
+bool MaybeInstallFromDiskImage();
+
+#endif // CHROME_BROWSER_COCOA_INSTALL_FROM_DMG_H_
diff --git a/chrome/browser/cocoa/install_from_dmg.mm b/chrome/browser/cocoa/install_from_dmg.mm
new file mode 100644
index 0000000..8af44f3
--- /dev/null
+++ b/chrome/browser/cocoa/install_from_dmg.mm
@@ -0,0 +1,438 @@
+// Copyright (c) 2010 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/install_from_dmg.h"
+
+#include <ApplicationServices/ApplicationServices.h>
+#import <AppKit/AppKit.h>
+#include <CoreFoundation/CoreFoundation.h>
+#include <CoreServices/CoreServices.h>
+#include <IOKit/IOKitLib.h>
+#include <string.h>
+#include <sys/param.h>
+#include <sys/mount.h>
+
+#include "base/basictypes.h"
+#include "base/command_line.h"
+#include "base/logging.h"
+#import "base/mac/mac_util.h"
+#include "base/mac/scoped_nsautorelease_pool.h"
+#include "chrome/browser/cocoa/authorization_util.h"
+#include "chrome/browser/cocoa/scoped_authorizationref.h"
+#import "chrome/browser/cocoa/keystone_glue.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/base/l10n/l10n_util_mac.h"
+
+// When C++ exceptions are disabled, the C++ library defines |try| and
+// |catch| so as to allow exception-expecting C++ code to build properly when
+// language support for exceptions is not present. These macros interfere
+// with the use of |@try| and |@catch| in Objective-C files such as this one.
+// Undefine these macros here, after everything has been #included, since
+// there will be no C++ uses and only Objective-C uses from this point on.
+#undef try
+#undef catch
+
+namespace {
+
+// Just like ScopedCFTypeRef but for io_object_t and subclasses.
+template<typename IOT>
+class scoped_ioobject {
+ public:
+ typedef IOT element_type;
+
+ explicit scoped_ioobject(IOT object = NULL)
+ : object_(object) {
+ }
+
+ ~scoped_ioobject() {
+ if (object_)
+ IOObjectRelease(object_);
+ }
+
+ void reset(IOT object = NULL) {
+ if (object_)
+ IOObjectRelease(object_);
+ object_ = object;
+ }
+
+ bool operator==(IOT that) const {
+ return object_ == that;
+ }
+
+ bool operator!=(IOT that) const {
+ return object_ != that;
+ }
+
+ operator IOT() const {
+ return object_;
+ }
+
+ IOT get() const {
+ return object_;
+ }
+
+ void swap(scoped_ioobject& that) {
+ IOT temp = that.object_;
+ that.object_ = object_;
+ object_ = temp;
+ }
+
+ IOT release() {
+ IOT temp = object_;
+ object_ = NULL;
+ return temp;
+ }
+
+ private:
+ IOT object_;
+
+ DISALLOW_COPY_AND_ASSIGN(scoped_ioobject);
+};
+
+// Returns true if |path| is located on a read-only filesystem of a disk
+// image. Returns false if not, or in the event of an error.
+bool IsPathOnReadOnlyDiskImage(const char path[]) {
+ struct statfs statfs_buf;
+ if (statfs(path, &statfs_buf) != 0) {
+ PLOG(ERROR) << "statfs " << path;
+ return false;
+ }
+
+ if (!(statfs_buf.f_flags & MNT_RDONLY)) {
+ // Not on a read-only filesystem.
+ return false;
+ }
+
+ const char dev_root[] = "/dev/";
+ const int dev_root_length = arraysize(dev_root) - 1;
+ if (strncmp(statfs_buf.f_mntfromname, dev_root, dev_root_length) != 0) {
+ // Not rooted at dev_root, no BSD name to search on.
+ return false;
+ }
+
+ // BSD names in IOKit don't include dev_root.
+ const char* bsd_device_name = statfs_buf.f_mntfromname + dev_root_length;
+
+ const mach_port_t master_port = kIOMasterPortDefault;
+
+ // IOBSDNameMatching gives ownership of match_dict to the caller, but
+ // IOServiceGetMatchingServices will assume that reference.
+ CFMutableDictionaryRef match_dict = IOBSDNameMatching(master_port,
+ 0,
+ bsd_device_name);
+ if (!match_dict) {
+ LOG(ERROR) << "IOBSDNameMatching " << bsd_device_name;
+ return false;
+ }
+
+ io_iterator_t iterator_ref;
+ kern_return_t kr = IOServiceGetMatchingServices(master_port,
+ match_dict,
+ &iterator_ref);
+ if (kr != KERN_SUCCESS) {
+ LOG(ERROR) << "IOServiceGetMatchingServices " << bsd_device_name
+ << ": kernel error " << kr;
+ return false;
+ }
+ scoped_ioobject<io_iterator_t> iterator(iterator_ref);
+ iterator_ref = NULL;
+
+ // There needs to be exactly one matching service.
+ scoped_ioobject<io_service_t> filesystem_service(IOIteratorNext(iterator));
+ if (!filesystem_service) {
+ LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": no service";
+ return false;
+ }
+ scoped_ioobject<io_service_t> unexpected_service(IOIteratorNext(iterator));
+ if (unexpected_service) {
+ LOG(ERROR) << "IOIteratorNext " << bsd_device_name << ": too many services";
+ return false;
+ }
+
+ iterator.reset();
+
+ const char disk_image_class[] = "IOHDIXController";
+
+ // This is highly unlikely. The filesystem service is expected to be of
+ // class IOMedia. Since the filesystem service's entire ancestor chain
+ // will be checked, though, check the filesystem service's class itself.
+ if (IOObjectConformsTo(filesystem_service, disk_image_class)) {
+ return true;
+ }
+
+ kr = IORegistryEntryCreateIterator(filesystem_service,
+ kIOServicePlane,
+ kIORegistryIterateRecursively |
+ kIORegistryIterateParents,
+ &iterator_ref);
+ if (kr != KERN_SUCCESS) {
+ LOG(ERROR) << "IORegistryEntryCreateIterator " << bsd_device_name
+ << ": kernel error " << kr;
+ return false;
+ }
+ iterator.reset(iterator_ref);
+ iterator_ref = NULL;
+
+ // Look at each of the filesystem service's ancestor services, beginning
+ // with the parent, iterating all the way up to the device tree's root. If
+ // any ancestor service matches the class used for disk images, the
+ // filesystem resides on a disk image.
+ for(scoped_ioobject<io_service_t> ancestor_service(IOIteratorNext(iterator));
+ ancestor_service;
+ ancestor_service.reset(IOIteratorNext(iterator))) {
+ if (IOObjectConformsTo(ancestor_service, disk_image_class)) {
+ return true;
+ }
+ }
+
+ // The filesystem does not reside on a disk image.
+ return false;
+}
+
+// Returns true if the application is located on a read-only filesystem of a
+// disk image. Returns false if not, or in the event of an error.
+bool IsAppRunningFromReadOnlyDiskImage() {
+ return IsPathOnReadOnlyDiskImage(
+ [[[NSBundle mainBundle] bundlePath] fileSystemRepresentation]);
+}
+
+// Shows a dialog asking the user whether or not to install from the disk
+// image. Returns true if the user approves installation.
+bool ShouldInstallDialog() {
+ NSString* title = l10n_util::GetNSStringFWithFixup(
+ IDS_INSTALL_FROM_DMG_TITLE, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ NSString* prompt = l10n_util::GetNSStringFWithFixup(
+ IDS_INSTALL_FROM_DMG_PROMPT, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ NSString* yes = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_YES);
+ NSString* no = l10n_util::GetNSStringWithFixup(IDS_INSTALL_FROM_DMG_NO);
+
+ NSAlert* alert = [[[NSAlert alloc] init] autorelease];
+
+ [alert setAlertStyle:NSInformationalAlertStyle];
+ [alert setMessageText:title];
+ [alert setInformativeText:prompt];
+ [alert addButtonWithTitle:yes];
+ NSButton* cancel_button = [alert addButtonWithTitle:no];
+ [cancel_button setKeyEquivalent:@"\e"];
+
+ NSInteger result = [alert runModal];
+
+ return result == NSAlertFirstButtonReturn;
+}
+
+// Potentially shows an authorization dialog to request authentication to
+// copy. If application_directory appears to be unwritable, attempts to
+// obtain authorization, which may result in the display of the dialog.
+// Returns NULL if authorization is not performed because it does not appear
+// to be necessary because the user has permission to write to
+// application_directory. Returns NULL if authorization fails.
+AuthorizationRef MaybeShowAuthorizationDialog(NSString* application_directory) {
+ NSFileManager* file_manager = [NSFileManager defaultManager];
+ if ([file_manager isWritableFileAtPath:application_directory]) {
+ return NULL;
+ }
+
+ NSString* prompt = l10n_util::GetNSStringFWithFixup(
+ IDS_INSTALL_FROM_DMG_AUTHENTICATION_PROMPT,
+ l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ return authorization_util::AuthorizationCreateToRunAsRoot(
+ reinterpret_cast<CFStringRef>(prompt));
+}
+
+// Invokes the installer program at installer_path to copy source_path to
+// target_path and perform any additional on-disk bookkeeping needed to be
+// able to launch target_path properly. If authorization_arg is non-NULL,
+// function will assume ownership of it, will invoke the installer with that
+// authorization reference, and will attempt Keystone ticket promotion.
+bool InstallFromDiskImage(AuthorizationRef authorization_arg,
+ NSString* installer_path,
+ NSString* source_path,
+ NSString* target_path) {
+ scoped_AuthorizationRef authorization(authorization_arg);
+ authorization_arg = NULL;
+ int exit_status;
+ if (authorization) {
+ const char* installer_path_c = [installer_path fileSystemRepresentation];
+ const char* source_path_c = [source_path fileSystemRepresentation];
+ const char* target_path_c = [target_path fileSystemRepresentation];
+ const char* arguments[] = {source_path_c, target_path_c, NULL};
+
+ OSStatus status = authorization_util::ExecuteWithPrivilegesAndWait(
+ authorization,
+ installer_path_c,
+ kAuthorizationFlagDefaults,
+ arguments,
+ NULL, // pipe
+ &exit_status);
+ if (status != errAuthorizationSuccess) {
+ LOG(ERROR) << "AuthorizationExecuteWithPrivileges install: " << status;
+ return false;
+ }
+ } else {
+ NSArray* arguments = [NSArray arrayWithObjects:source_path,
+ target_path,
+ nil];
+
+ NSTask* task;
+ @try {
+ task = [NSTask launchedTaskWithLaunchPath:installer_path
+ arguments:arguments];
+ } @catch(NSException* exception) {
+ LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: "
+ << [[exception description] UTF8String];
+ return false;
+ }
+
+ [task waitUntilExit];
+ exit_status = [task terminationStatus];
+ }
+
+ if (exit_status != 0) {
+ LOG(ERROR) << "install.sh: exit status " << exit_status;
+ return false;
+ }
+
+ if (authorization) {
+ // As long as an AuthorizationRef is available, promote the Keystone
+ // ticket. Inform KeystoneGlue of the new path to use.
+ KeystoneGlue* keystone_glue = [KeystoneGlue defaultKeystoneGlue];
+ [keystone_glue setAppPath:target_path];
+ [keystone_glue promoteTicketWithAuthorization:authorization.release()
+ synchronous:YES];
+ }
+
+ return true;
+}
+
+// Launches the application at app_path. The arguments passed to app_path
+// will be the same as the arguments used to invoke this process, except any
+// arguments beginning with -psn_ will be stripped.
+bool LaunchInstalledApp(NSString* app_path) {
+ const UInt8* app_path_c =
+ reinterpret_cast<const UInt8*>([app_path fileSystemRepresentation]);
+ FSRef app_fsref;
+ OSStatus err = FSPathMakeRef(app_path_c, &app_fsref, NULL);
+ if (err != noErr) {
+ LOG(ERROR) << "FSPathMakeRef: " << err;
+ return false;
+ }
+
+ const std::vector<std::string>& argv =
+ CommandLine::ForCurrentProcess()->argv();
+ NSMutableArray* arguments =
+ [NSMutableArray arrayWithCapacity:argv.size() - 1];
+ // Start at argv[1]. LSOpenApplication adds its own argv[0] as the path of
+ // the launched executable.
+ for (size_t index = 1; index < argv.size(); ++index) {
+ std::string argument = argv[index];
+ const char psn_flag[] = "-psn_";
+ const int psn_flag_length = arraysize(psn_flag) - 1;
+ if (argument.compare(0, psn_flag_length, psn_flag) != 0) {
+ // Strip any -psn_ arguments, as they apply to a specific process.
+ [arguments addObject:[NSString stringWithUTF8String:argument.c_str()]];
+ }
+ }
+
+ struct LSApplicationParameters parameters = {0};
+ parameters.flags = kLSLaunchDefaults;
+ parameters.application = &app_fsref;
+ parameters.argv = reinterpret_cast<CFArrayRef>(arguments);
+
+ err = LSOpenApplication(&parameters, NULL);
+ if (err != noErr) {
+ LOG(ERROR) << "LSOpenApplication: " << err;
+ return false;
+ }
+
+ return true;
+}
+
+void ShowErrorDialog() {
+ NSString* title = l10n_util::GetNSStringWithFixup(
+ IDS_INSTALL_FROM_DMG_ERROR_TITLE);
+ NSString* error = l10n_util::GetNSStringFWithFixup(
+ IDS_INSTALL_FROM_DMG_ERROR, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ NSString* ok = l10n_util::GetNSStringWithFixup(IDS_OK);
+
+ NSAlert* alert = [[[NSAlert alloc] init] autorelease];
+
+ [alert setAlertStyle:NSWarningAlertStyle];
+ [alert setMessageText:title];
+ [alert setInformativeText:error];
+ [alert addButtonWithTitle:ok];
+
+ [alert runModal];
+}
+
+} // namespace
+
+bool MaybeInstallFromDiskImage() {
+ base::mac::ScopedNSAutoreleasePool autorelease_pool;
+
+ if (!IsAppRunningFromReadOnlyDiskImage()) {
+ return false;
+ }
+
+ NSArray* application_directories =
+ NSSearchPathForDirectoriesInDomains(NSApplicationDirectory,
+ NSLocalDomainMask,
+ YES);
+ if ([application_directories count] == 0) {
+ LOG(ERROR) << "NSSearchPathForDirectoriesInDomains: "
+ << "no local application directories";
+ return false;
+ }
+ NSString* application_directory = [application_directories objectAtIndex:0];
+
+ NSFileManager* file_manager = [NSFileManager defaultManager];
+
+ BOOL is_directory;
+ if (![file_manager fileExistsAtPath:application_directory
+ isDirectory:&is_directory] ||
+ !is_directory) {
+ VLOG(1) << "No application directory at "
+ << [application_directory UTF8String];
+ return false;
+ }
+
+ NSString* source_path = [[NSBundle mainBundle] bundlePath];
+ NSString* application_name = [source_path lastPathComponent];
+ NSString* target_path =
+ [application_directory stringByAppendingPathComponent:application_name];
+
+ if ([file_manager fileExistsAtPath:target_path]) {
+ VLOG(1) << "Something already exists at " << [target_path UTF8String];
+ return false;
+ }
+
+ NSString* installer_path =
+ [base::mac::MainAppBundle() pathForResource:@"install" ofType:@"sh"];
+ if (!installer_path) {
+ VLOG(1) << "Could not locate install.sh";
+ return false;
+ }
+
+ if (!ShouldInstallDialog()) {
+ return false;
+ }
+
+ scoped_AuthorizationRef authorization(
+ MaybeShowAuthorizationDialog(application_directory));
+ // authorization will be NULL if it's deemed unnecessary or if
+ // authentication fails. In either case, try to install without privilege
+ // escalation.
+
+ if (!InstallFromDiskImage(authorization.release(),
+ installer_path,
+ source_path,
+ target_path) ||
+ !LaunchInstalledApp(target_path)) {
+ ShowErrorDialog();
+ return false;
+ }
+
+ return true;
+}
diff --git a/chrome/browser/cocoa/keystone_glue.h b/chrome/browser/cocoa/keystone_glue.h
new file mode 100644
index 0000000..69b5351
--- /dev/null
+++ b/chrome/browser/cocoa/keystone_glue.h
@@ -0,0 +1,209 @@
+// 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_
+#pragma once
+
+#include "base/string16.h"
+
+#if defined(__OBJC__)
+
+#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 NSString* const kAutoupdateStatusNotification;
+extern NSString* const kAutoupdateStatusStatus;
+extern NSString* const kAutoupdateStatusVersion;
+
+namespace {
+
+enum BrandFileType {
+ kBrandFileTypeNotDetermined = 0,
+ kBrandFileTypeNone,
+ kBrandFileTypeUser,
+ kBrandFileTypeSystem,
+};
+
+} // namespace
+
+// 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.
+ BrandFileType brandFileType_;
+
+ // 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 a synchronous promotion operation is in progress (promotion during
+ // installation).
+ BOOL synchronousPromotion_;
+
+ // 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;
+
+// Promotes the Keystone ticket into the system store. System Keystone will
+// be installed if necessary. If synchronous is NO, the promotion may occur
+// in the background. synchronous should be YES for promotion during
+// installation. The KeystoneGlue object assumes ownership of
+// authorization_arg.
+- (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg
+ synchronous:(BOOL)synchronous;
+
+// Requests authorization and calls -promoteTicketWithAuthorization: in
+// asynchronous mode.
+- (void)promoteTicket;
+
+// Sets a new value for appPath. Used during installation to point a ticket
+// at the installed copy.
+- (void)setAppPath:(NSString*)appPath;
+
+@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 // __OBJC__
+
+// Functions that may be accessed from non-Objective-C C/C++ code.
+namespace keystone_glue {
+
+// True if Keystone is enabled.
+bool KeystoneEnabled();
+
+// The version of the application currently installed on disk.
+string16 CurrentlyInstalledVersion();
+
+} // namespace keystone_glue
+
+#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..562d74a
--- /dev/null
+++ b/chrome/browser/cocoa/keystone_glue.mm
@@ -0,0 +1,957 @@
+// 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>
+
+#include "base/logging.h"
+#include "base/mac/mac_util.h"
+#include "base/mac/scoped_nsautorelease_pool.h"
+#include "base/sys_string_conversions.h"
+#include "base/ref_counted.h"
+#include "base/task.h"
+#include "base/threading/worker_pool.h"
+#include "chrome/browser/cocoa/authorization_util.h"
+#include "chrome/common/chrome_constants.h"
+#include "grit/chromium_strings.h"
+#include "grit/generated_resources.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/base/l10n/l10n_util_mac.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* const KSRegistrationVersionKey = @"Version";
+NSString* const KSRegistrationExistenceCheckerTypeKey = @"ExistenceCheckerType";
+NSString* const KSRegistrationExistenceCheckerStringKey =
+ @"ExistenceCheckerString";
+NSString* const KSRegistrationServerURLStringKey = @"URLString";
+NSString* const KSRegistrationPreserveTrustedTesterTokenKey = @"PreserveTTT";
+NSString* const KSRegistrationTagKey = @"Tag";
+NSString* const KSRegistrationTagPathKey = @"TagPath";
+NSString* const KSRegistrationTagKeyKey = @"TagKey";
+NSString* const KSRegistrationBrandPathKey = @"BrandPath";
+NSString* const KSRegistrationBrandKeyKey = @"BrandKey";
+
+NSString* const KSRegistrationDidCompleteNotification =
+ @"KSRegistrationDidCompleteNotification";
+NSString* const KSRegistrationPromotionDidCompleteNotification =
+ @"KSRegistrationPromotionDidCompleteNotification";
+
+NSString* const KSRegistrationCheckForUpdateNotification =
+ @"KSRegistrationCheckForUpdateNotification";
+NSString* KSRegistrationStatusKey = @"Status";
+NSString* KSRegistrationUpdateCheckErrorKey = @"Error";
+
+NSString* const KSRegistrationStartUpdateNotification =
+ @"KSRegistrationStartUpdateNotification";
+NSString* const KSUpdateCheckSuccessfulKey = @"CheckSuccessful";
+NSString* const KSUpdateCheckSuccessfullyInstalledKey =
+ @"SuccessfullyInstalled";
+
+NSString* const KSRegistrationRemoveExistingTag = @"";
+#define KSRegistrationPreserveExistingTag nil
+
+// Constants for the brand file (uses an external file so it can survive updates
+// to Chrome.
+
+#if defined(GOOGLE_CHROME_BUILD)
+#define kBrandFileName @"Google Chrome Brand.plist";
+#elif defined(CHROMIUM_BUILD)
+#define kBrandFileName @"Chromium Brand.plist";
+#else
+#error Unknown branding
+#endif
+
+// These directories are hardcoded in Keystone promotion preflight and the
+// Keystone install script, so NSSearchPathForDirectoriesInDomains isn't used
+// since the scripts couldn't use anything like that.
+NSString* kBrandUserFile = @"~/Library/Google/" kBrandFileName;
+NSString* kBrandSystemFile = @"/Library/Google/" kBrandFileName;
+
+NSString* UserBrandFilePath() {
+ return [kBrandUserFile stringByStandardizingPath];
+}
+NSString* SystemBrandFilePath() {
+ return [kBrandSystemFile stringByStandardizingPath];
+}
+
+// Adaptor for scheduling an Objective-C method call on a |WorkerPool|
+// thread.
+class PerformBridge : public base::RefCountedThreadSafe<PerformBridge> {
+ public:
+
+ // Call |sel| on |target| with |arg| in a WorkerPool thread.
+ // |target| and |arg| are retained, |arg| may be |nil|.
+ static void PostPerform(id target, SEL sel, id arg) {
+ DCHECK(target);
+ DCHECK(sel);
+
+ scoped_refptr<PerformBridge> op = new PerformBridge(target, sel, arg);
+ base::WorkerPool::PostTask(
+ FROM_HERE, NewRunnableMethod(op.get(), &PerformBridge::Run), true);
+ }
+
+ // Convenience for the no-argument case.
+ static void PostPerform(id target, SEL sel) {
+ PostPerform(target, sel, nil);
+ }
+
+ private:
+ // Allow RefCountedThreadSafe<> to delete.
+ friend class base::RefCountedThreadSafe<PerformBridge>;
+
+ PerformBridge(id target, SEL sel, id arg)
+ : target_([target retain]),
+ sel_(sel),
+ arg_([arg retain]) {
+ }
+
+ ~PerformBridge() {}
+
+ // Happens on a WorkerPool thread.
+ void Run() {
+ base::mac::ScopedNSAutoreleasePool pool;
+ [target_ performSelector:sel_ withObject:arg_];
+ }
+
+ scoped_nsobject<id> target_;
+ SEL sel_;
+ scoped_nsobject<id> arg_;
+};
+
+} // namespace
+
+@interface KSRegistration : NSObject
+
++ (id)registrationWithProductID:(NSString*)productID;
+
+- (BOOL)registerWithParameters:(NSDictionary*)args;
+
+- (BOOL)promoteWithParameters:(NSDictionary*)args
+ authorization:(AuthorizationRef)authorization;
+
+- (void)setActive;
+- (void)checkForUpdate;
+- (void)startUpdate;
+- (KSRegistrationTicketType)ticketType;
+
+@end // @interface KSRegistration
+
+@interface KeystoneGlue(Private)
+
+// Returns the path to the application's Info.plist file. This returns the
+// outer application bundle's Info.plist, not the framework's Info.plist.
+- (NSString*)appInfoPlistPath;
+
+// Returns a dictionary containing parameters to be used for a KSRegistration
+// -registerWithParameters: or -promoteWithParameters:authorization: call.
+- (NSDictionary*)keystoneParameters;
+
+// 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;
+
+// Returns the version of the currently-installed application on disk.
+- (NSString*)currentlyInstalledVersion;
+
+// 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 WorkerPool.
+// -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;
+
+// Returns the brand file path to use for Keystone.
+- (NSString*)brandFilePath;
+
+@end // @interface KeystoneGlue(Private)
+
+NSString* const kAutoupdateStatusNotification = @"AutoupdateStatusNotification";
+NSString* const kAutoupdateStatusStatus = @"status";
+NSString* const kAutoupdateStatusVersion = @"version";
+
+namespace {
+
+NSString* const kChannelKey = @"KSChannelID";
+NSString* const kBrandKey = @"KSBrandID";
+
+} // namespace
+
+@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 [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.
+ return [[NSBundle mainBundle] infoDictionary];
+}
+
+- (void)loadParameters {
+ NSBundle* appBundle = [NSBundle mainBundle];
+ NSDictionary* infoDictionary = [self infoDictionary];
+
+ 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:kChannelKey];
+ // 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];
+}
+
+- (NSString*)brandFilePath {
+ DCHECK(version_ != nil) << "-loadParameters must be called first";
+
+ if (brandFileType_ == kBrandFileTypeNotDetermined) {
+
+ // Default to none.
+ brandFileType_ = kBrandFileTypeNone;
+
+ // Having a channel means Dev/Beta, so there is no brand code to go with
+ // those.
+ if ([channel_ length] == 0) {
+
+ NSString* userBrandFile = UserBrandFilePath();
+ NSString* systemBrandFile = SystemBrandFilePath();
+
+ NSFileManager* fm = [NSFileManager defaultManager];
+
+ // If there is a system brand file, use it.
+ if ([fm fileExistsAtPath:systemBrandFile]) {
+ // System
+
+ // Use the system file that is there.
+ brandFileType_ = kBrandFileTypeSystem;
+
+ // Clean up any old user level file.
+ if ([fm fileExistsAtPath:userBrandFile]) {
+ [fm removeItemAtPath:userBrandFile error:NULL];
+ }
+
+ } else {
+ // User
+
+ NSDictionary* infoDictionary = [self infoDictionary];
+ NSString* appBundleBrandID = [infoDictionary objectForKey:kBrandKey];
+
+ NSString* storedBrandID = nil;
+ if ([fm fileExistsAtPath:userBrandFile]) {
+ NSDictionary* storedBrandDict =
+ [NSDictionary dictionaryWithContentsOfFile:userBrandFile];
+ storedBrandID = [storedBrandDict objectForKey:kBrandKey];
+ }
+
+ if ((appBundleBrandID != nil) &&
+ (![storedBrandID isEqualTo:appBundleBrandID])) {
+ // App and store don't match, update store and use it.
+ NSDictionary* storedBrandDict =
+ [NSDictionary dictionaryWithObject:appBundleBrandID
+ forKey:kBrandKey];
+ // If Keystone hasn't been installed yet, the location the brand file
+ // is written to won't exist, so manually create the directory.
+ NSString *userBrandFileDirectory =
+ [userBrandFile stringByDeletingLastPathComponent];
+ if (![fm fileExistsAtPath:userBrandFileDirectory]) {
+ if (![fm createDirectoryAtPath:userBrandFileDirectory
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:NULL]) {
+ LOG(ERROR) << "Failed to create the directory for the brand file";
+ }
+ }
+ if ([storedBrandDict writeToFile:userBrandFile atomically:YES]) {
+ brandFileType_ = kBrandFileTypeUser;
+ }
+ } else if (storedBrandID) {
+ // Had stored brand, use it.
+ brandFileType_ = kBrandFileTypeUser;
+ }
+ }
+ }
+
+ }
+
+ NSString* result = nil;
+ switch (brandFileType_) {
+ case kBrandFileTypeUser:
+ result = UserBrandFilePath();
+ break;
+
+ case kBrandFileTypeSystem:
+ result = SystemBrandFilePath();
+ break;
+
+ case kBrandFileTypeNotDetermined:
+ NOTIMPLEMENTED();
+ // Fall through
+ case kBrandFileTypeNone:
+ // Clear the value.
+ result = @"";
+ break;
+
+ }
+ return result;
+}
+
+- (BOOL)loadKeystoneRegistration {
+ if (!productID_ || !appPath_ || !url_ || !version_)
+ return NO;
+
+ // Load the KeystoneRegistration framework bundle if present. It lives
+ // inside the framework, so use base::mac::MainAppBundle();
+ NSString* ksrPath =
+ [[base::mac::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;
+}
+
+- (NSString*)appInfoPlistPath {
+ // NSBundle ought to have a way to access this path directly, but it
+ // doesn't.
+ return [[appPath_ stringByAppendingPathComponent:@"Contents"]
+ stringByAppendingPathComponent:@"Info.plist"];
+}
+
+- (NSDictionary*)keystoneParameters {
+ NSNumber* xcType = [NSNumber numberWithInt:kKSPathExistenceChecker];
+ NSNumber* preserveTTToken = [NSNumber numberWithBool:YES];
+ NSString* tagPath = [self appInfoPlistPath];
+
+ NSString* brandKey = kBrandKey;
+ NSString* brandPath = [self brandFilePath];
+
+ if ([brandPath length] == 0) {
+ // Brand path and brand key must be cleared together or ksadmin seems
+ // to throw an error.
+ brandKey = @"";
+ }
+
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ version_, KSRegistrationVersionKey,
+ xcType, KSRegistrationExistenceCheckerTypeKey,
+ appPath_, KSRegistrationExistenceCheckerStringKey,
+ url_, KSRegistrationServerURLStringKey,
+ preserveTTToken, KSRegistrationPreserveTrustedTesterTokenKey,
+ channel_, KSRegistrationTagKey,
+ tagPath, KSRegistrationTagPathKey,
+ kChannelKey, KSRegistrationTagKeyKey,
+ brandPath, KSRegistrationBrandPathKey,
+ brandKey, KSRegistrationBrandKeyKey,
+ nil];
+}
+
+- (void)registerWithKeystone {
+ [self updateStatus:kAutoupdateRegistering version:nil];
+
+ NSDictionary* parameters = [self keystoneParameters];
+ if (![registration_ registerWithParameters:parameters]) {
+ [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];
+ }
+}
+
+- (NSString*)currentlyInstalledVersion {
+ NSString* appInfoPlistPath = [self appInfoPlistPath];
+ NSDictionary* infoPlist =
+ [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath];
+ return [infoPlist objectForKey:@"CFBundleShortVersionString"];
+}
+
+// Runs on the main thread.
+- (void)determineUpdateStatusAsync {
+ DCHECK([NSThread isMainThread]);
+
+ PerformBridge::PostPerform(self, @selector(determineUpdateStatus));
+}
+
+// Runs on a thread managed by WorkerPool.
+- (void)determineUpdateStatus {
+ DCHECK(![NSThread isMainThread]);
+
+ NSString* version = [self currentlyInstalledVersion];
+
+ [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 = [base::mac::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;
+ }
+
+ NSString* prompt = l10n_util::GetNSStringFWithFixup(
+ IDS_PROMOTE_AUTHENTICATION_PROMPT,
+ l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
+ scoped_AuthorizationRef authorization(
+ authorization_util::AuthorizationCreateToRunAsRoot(
+ reinterpret_cast<CFStringRef>(prompt)));
+ if (!authorization.get()) {
+ return;
+ }
+
+ [self promoteTicketWithAuthorization:authorization.release() synchronous:NO];
+}
+
+- (void)promoteTicketWithAuthorization:(AuthorizationRef)authorization_arg
+ synchronous:(BOOL)synchronous {
+ scoped_AuthorizationRef authorization(authorization_arg);
+ authorization_arg = NULL;
+
+ if ([self asyncOperationPending]) {
+ // Starting a synchronous operation while an asynchronous one is pending
+ // could be trouble.
+ return;
+ }
+ if (!synchronous && ![self wantsPromotion]) {
+ // If operating synchronously, the call came from the installer, which
+ // means that a system ticket is required. Otherwise, only allow
+ // promotion if it's wanted.
+ return;
+ }
+
+ synchronousPromotion_ = synchronous;
+
+ [self updateStatus:kAutoupdatePromoting version:nil];
+
+ // TODO(mark): Remove when able!
+ //
+ // keystone_promote_preflight will copy the current brand information out to
+ // the system level so all users can share the data as part of the ticket
+ // promotion.
+ //
+ // It will also 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 promoteWithParameters:authorization:] 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.
+ // However, preflight operation (and promotion) should only be asynchronous
+ // if the synchronous parameter is NO.
+ NSString* preflightPath =
+ [base::mac::MainAppBundle() pathForResource:@"keystone_promote_preflight"
+ ofType:@"sh"];
+ const char* preflightPathC = [preflightPath fileSystemRepresentation];
+ const char* userBrandFile = NULL;
+ const char* systemBrandFile = NULL;
+ if (brandFileType_ == kBrandFileTypeUser) {
+ // Running with user level brand file, promote to the system level.
+ userBrandFile = [UserBrandFilePath() fileSystemRepresentation];
+ systemBrandFile = [SystemBrandFilePath() fileSystemRepresentation];
+ }
+ const char* arguments[] = {userBrandFile, systemBrandFile, NULL};
+
+ int exit_status;
+ OSStatus 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);
+
+ NSDictionary* parameters = [self keystoneParameters];
+
+ // If the brand file is user level, update parameters to point to the new
+ // system level file during promotion.
+ if (brandFileType_ == kBrandFileTypeUser) {
+ NSMutableDictionary* temp_parameters =
+ [[parameters mutableCopy] autorelease];
+ [temp_parameters setObject:SystemBrandFilePath()
+ forKey:KSRegistrationBrandPathKey];
+ parameters = temp_parameters;
+ }
+
+ if (![registration_ promoteWithParameters:parameters
+ 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]) {
+ if (synchronousPromotion_) {
+ // Short-circuit: if performing a synchronous promotion, the promotion
+ // came from the installer, which already set the permissions properly.
+ // Rather than run a duplicate permission-changing operation, jump
+ // straight to "done."
+ [self changePermissionsForPromotionComplete];
+ } else {
+ [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 WorkerPool-managed
+ // thread to run the tool.
+ DCHECK([NSThread isMainThread]);
+
+ SEL selector = @selector(changePermissionsForPromotionWithTool:);
+ NSString* toolPath =
+ [base::mac::MainAppBundle() pathForResource:@"keystone_promote_postflight"
+ ofType:@"sh"];
+
+ PerformBridge::PostPerform(self, selector, toolPath);
+}
+
+- (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];
+}
+
+- (void)setAppPath:(NSString*)appPath {
+ if (appPath != appPath_) {
+ [appPath_ release];
+ appPath_ = [appPath copy];
+ }
+}
+
+@end // @implementation KeystoneGlue
+
+namespace keystone_glue {
+
+bool KeystoneEnabled() {
+ return [KeystoneGlue defaultKeystoneGlue] != nil;
+}
+
+string16 CurrentlyInstalledVersion() {
+ KeystoneGlue* keystoneGlue = [KeystoneGlue defaultKeystoneGlue];
+ NSString* version = [keystoneGlue currentlyInstalledVersion];
+ return base::SysNSStringToUTF16(version);
+}
+
+} // namespace keystone_glue
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/scoped_authorizationref.h b/chrome/browser/cocoa/scoped_authorizationref.h
new file mode 100644
index 0000000..3ffa18b
--- /dev/null
+++ b/chrome/browser/cocoa/scoped_authorizationref.h
@@ -0,0 +1,80 @@
+// 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_
+#pragma once
+
+#include <Security/Authorization.h>
+
+#include "base/basictypes.h"
+#include "base/compiler_specific.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() WARN_UNUSED_RESULT {
+ AuthorizationRef temp = authorization_;
+ authorization_ = NULL;
+ return temp;
+ }
+
+ private:
+ AuthorizationRef authorization_;
+
+ DISALLOW_COPY_AND_ASSIGN(scoped_AuthorizationRef);
+};
+
+#endif // CHROME_BROWSER_COCOA_SCOPED_AUTHORIZATIONREF_H_
diff --git a/chrome/browser/cocoa/task_helpers.h b/chrome/browser/cocoa/task_helpers.h
new file mode 100644
index 0000000..ecc40f3
--- /dev/null
+++ b/chrome/browser/cocoa/task_helpers.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2010 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_TASK_HELPERS_H_
+#define CHROME_BROWSER_COCOA_TASK_HELPERS_H_
+#pragma once
+
+class Task;
+
+namespace tracked_objects {
+class Location;
+} // namespace tracked_objects
+
+namespace cocoa_utils {
+
+// This can be used in place of BrowserThread::PostTask(BrowserThread::UI, ...).
+// The purpose of this function is to be able to execute Task work alongside
+// native work when a MessageLoop is blocked by a nested run loop. This function
+// will run the Task in both NSEventTrackingRunLoopMode and NSDefaultRunLoopMode
+// for the purpose of executing work while a menu is open. See
+// http://crbug.com/48679 for the full rationale.
+bool PostTaskInEventTrackingRunLoopMode(
+ const tracked_objects::Location& from_here,
+ Task* task);
+
+} // namespace cocoa_utils
+
+#endif // CHROME_BROWSER_COCOA_TASK_HELPERS_H_
diff --git a/chrome/browser/cocoa/task_helpers.mm b/chrome/browser/cocoa/task_helpers.mm
new file mode 100644
index 0000000..7cc458b
--- /dev/null
+++ b/chrome/browser/cocoa/task_helpers.mm
@@ -0,0 +1,57 @@
+// Copyright (c) 2010 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/task_helpers.h"
+
+#import <Cocoa/Cocoa.h>
+
+#include "base/scoped_ptr.h"
+#include "base/task.h"
+
+// This is a wrapper for running Task objects from within a native run loop.
+// This can run specific tasks in that nested loop. This owns the task and will
+// delete it and itself when done.
+@interface NativeTaskRunner : NSObject {
+ @private
+ scoped_ptr<Task> task_;
+}
+- (id)initWithTask:(Task*)task;
+- (void)runTask;
+@end
+
+@implementation NativeTaskRunner
+- (id)initWithTask:(Task*)task {
+ if ((self = [super init])) {
+ task_.reset(task);
+ }
+ return self;
+}
+
+- (void)runTask {
+ task_->Run();
+ [self autorelease];
+}
+@end
+
+namespace cocoa_utils {
+
+bool PostTaskInEventTrackingRunLoopMode(
+ const tracked_objects::Location& from_here,
+ Task* task) {
+ // This deletes itself and the task after the task runs.
+ NativeTaskRunner* runner = [[NativeTaskRunner alloc] initWithTask:task];
+
+ // Schedule the selector in multiple modes in case this was called while a
+ // menu was not running.
+ NSArray* modes = [NSArray arrayWithObjects:NSEventTrackingRunLoopMode,
+ NSDefaultRunLoopMode,
+ nil];
+ [runner performSelectorOnMainThread:@selector(runTask)
+ withObject:nil
+ waitUntilDone:NO
+ modes:modes];
+ return true;
+}
+
+} // namespace cocoa_utils