diff options
author | sail@chromium.org <sail@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-02-11 00:18:08 +0000 |
---|---|---|
committer | sail@chromium.org <sail@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-02-11 00:18:08 +0000 |
commit | 2b78be3c1dd7cc0232aa1a24446a17299d5760e8 (patch) | |
tree | 39d8f59495b095b1857be6e908191c76ea13c386 /chrome/browser/cocoa | |
parent | 00c146f3971938025c5321cc31556ff127aebbc4 (diff) | |
download | chromium_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.h | 67 | ||||
-rw-r--r-- | chrome/browser/cocoa/authorization_util.mm | 183 | ||||
-rw-r--r-- | chrome/browser/cocoa/install_from_dmg.h | 15 | ||||
-rw-r--r-- | chrome/browser/cocoa/install_from_dmg.mm | 438 | ||||
-rw-r--r-- | chrome/browser/cocoa/keystone_glue.h | 209 | ||||
-rw-r--r-- | chrome/browser/cocoa/keystone_glue.mm | 957 | ||||
-rw-r--r-- | chrome/browser/cocoa/keystone_glue_unittest.mm | 184 | ||||
-rw-r--r-- | chrome/browser/cocoa/scoped_authorizationref.h | 80 | ||||
-rw-r--r-- | chrome/browser/cocoa/task_helpers.h | 29 | ||||
-rw-r--r-- | chrome/browser/cocoa/task_helpers.mm | 57 |
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(¶meters, 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 |