summaryrefslogtreecommitdiffstats
path: root/chrome/browser/mac
diff options
context:
space:
mode:
authormark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-06-20 22:15:52 +0000
committermark@chromium.org <mark@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-06-20 22:15:52 +0000
commit5950f5716a2b5b074d2762185fca7f42885bac06 (patch)
tree79ff42bb202ea8fb4e32714199ffe10f3630dff6 /chrome/browser/mac
parentab376cae3cd176f4aae8d67a5274891b66d9beda (diff)
downloadchromium_src-5950f5716a2b5b074d2762185fca7f42885bac06.zip
chromium_src-5950f5716a2b5b074d2762185fca7f42885bac06.tar.gz
chromium_src-5950f5716a2b5b074d2762185fca7f42885bac06.tar.bz2
Rename chrome/browser/cocoa to chrome/browser/mac.
chrome/browser/cocoa was created in r74529 under Carnitas to break non-UI files from chrome/browser/ui/cocoa out of that directory. These non-UI files for the most part have nothing to do with Cocoa. It would be more appropriate to name this directory "mac". Three shell scripts used only by files that now live in this directory never moved along with their associated files. I'm moving them to the proper location now. BUG=none, or whatever bug tracks Carnitas cleanups TEST=none Review URL: http://codereview.chromium.org/7204034 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@89742 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/mac')
-rw-r--r--chrome/browser/mac/OWNERS2
-rw-r--r--chrome/browser/mac/authorization_util.h67
-rw-r--r--chrome/browser/mac/authorization_util.mm183
-rw-r--r--chrome/browser/mac/file_metadata.h29
-rw-r--r--chrome/browser/mac/file_metadata.mm167
-rwxr-xr-xchrome/browser/mac/install.sh123
-rw-r--r--chrome/browser/mac/install_from_dmg.h15
-rw-r--r--chrome/browser/mac/install_from_dmg.mm438
-rw-r--r--chrome/browser/mac/keystone_glue.h209
-rw-r--r--chrome/browser/mac/keystone_glue.mm921
-rw-r--r--chrome/browser/mac/keystone_glue_unittest.mm222
-rwxr-xr-xchrome/browser/mac/keystone_promote_postflight.sh55
-rwxr-xr-xchrome/browser/mac/keystone_promote_preflight.sh97
-rw-r--r--chrome/browser/mac/keystone_registration.h72
-rw-r--r--chrome/browser/mac/keystone_registration.mm42
-rw-r--r--chrome/browser/mac/scoped_authorizationref.h80
16 files changed, 2722 insertions, 0 deletions
diff --git a/chrome/browser/mac/OWNERS b/chrome/browser/mac/OWNERS
new file mode 100644
index 0000000..c56e89d
--- /dev/null
+++ b/chrome/browser/mac/OWNERS
@@ -0,0 +1,2 @@
+mark@chromium.org
+thomasvl@chromium.org
diff --git a/chrome/browser/mac/authorization_util.h b/chrome/browser/mac/authorization_util.h
new file mode 100644
index 0000000..c25f0cb
--- /dev/null
+++ b/chrome/browser/mac/authorization_util.h
@@ -0,0 +1,67 @@
+// Copyright (c) 2011 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_MAC_AUTHORIZATION_UTIL_H_
+#define CHROME_BROWSER_MAC_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/library/mac/#samplecode/BetterAuthorizationSample/Listings/BetterAuthorizationSampleLib_c.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_MAC_AUTHORIZATION_UTIL_H_
diff --git a/chrome/browser/mac/authorization_util.mm b/chrome/browser/mac/authorization_util.mm
new file mode 100644
index 0000000..4db58b9
--- /dev/null
+++ b/chrome/browser/mac/authorization_util.mm
@@ -0,0 +1,183 @@
+// Copyright (c) 2011 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/mac/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/mac/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/mac/file_metadata.h b/chrome/browser/mac/file_metadata.h
new file mode 100644
index 0000000..17a59e8
--- /dev/null
+++ b/chrome/browser/mac/file_metadata.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2011 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_MAC_FILE_METADATA_H_
+#define CHROME_BROWSER_MAC_FILE_METADATA_H_
+#pragma once
+
+class FilePath;
+class GURL;
+
+namespace file_metadata {
+
+// Adds origin metadata to the file.
+// |source| should be the source URL for the download, and |referrer| should be
+// the URL the user initiated the download from.
+void AddOriginMetadataToFile(const FilePath& file, const GURL& source,
+ const GURL& referrer);
+
+// Adds quarantine metadata to the file, assuming it has already been
+// quarantined by the OS.
+// |source| should be the source URL for the download, and |referrer| should be
+// the URL the user initiated the download from.
+void AddQuarantineMetadataToFile(const FilePath& file, const GURL& source,
+ const GURL& referrer);
+
+} // namespace file_metadata
+
+#endif // CHROME_BROWSER_MAC_FILE_METADATA_H_
diff --git a/chrome/browser/mac/file_metadata.mm b/chrome/browser/mac/file_metadata.mm
new file mode 100644
index 0000000..2775259
--- /dev/null
+++ b/chrome/browser/mac/file_metadata.mm
@@ -0,0 +1,167 @@
+// Copyright (c) 2011 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/mac/file_metadata.h"
+
+#include <ApplicationServices/ApplicationServices.h>
+#include <Foundation/Foundation.h>
+
+#include "base/file_path.h"
+#include "base/logging.h"
+#include "base/mac/mac_util.h"
+#include "base/mac/scoped_cftyperef.h"
+#include "googleurl/src/gurl.h"
+
+namespace file_metadata {
+
+// As of Mac OS X 10.4 ("Tiger"), files can be tagged with metadata describing
+// various attributes. Metadata is integrated with the system's Spotlight
+// feature and is searchable. Ordinarily, metadata can only be set by
+// Spotlight importers, which requires that the importer own the target file.
+// However, there's an attribute intended to describe the origin of a
+// file, that can store the source URL and referrer of a downloaded file.
+// It's stored as a "com.apple.metadata:kMDItemWhereFroms" extended attribute,
+// structured as a binary1-format plist containing a list of sources. This
+// attribute can only be populated by the downloader, not a Spotlight importer.
+// Safari on 10.4 and later populates this attribute.
+//
+// With this metadata set, you can locate downloads by performing a Spotlight
+// search for their source or referrer URLs, either from within the Spotlight
+// UI or from the command line:
+// mdfind 'kMDItemWhereFroms == "http://releases.mozilla.org/*"'
+//
+// There is no documented API to set metadata on a file directly as of the
+// 10.5 SDK. The MDSetItemAttribute function does exist to perform this task,
+// but it's undocumented.
+void AddOriginMetadataToFile(const FilePath& file, const GURL& source,
+ const GURL& referrer) {
+ // There's no declaration for MDItemSetAttribute in any known public SDK.
+ // It exists in the 10.4 and 10.5 runtimes. To play it safe, do the lookup
+ // at runtime instead of declaring it ourselves and linking against what's
+ // provided. This has two benefits:
+ // - If Apple relents and declares the function in a future SDK (it's
+ // happened before), our build won't break.
+ // - If Apple removes or renames the function in a future runtime, the
+ // loader won't refuse to let the application launch. Instead, we'll
+ // silently fail to set any metadata.
+ typedef OSStatus (*MDItemSetAttribute_type)(MDItemRef, CFStringRef,
+ CFTypeRef);
+ static MDItemSetAttribute_type md_item_set_attribute_func = NULL;
+
+ static bool did_symbol_lookup = false;
+ if (!did_symbol_lookup) {
+ did_symbol_lookup = true;
+ CFBundleRef metadata_bundle =
+ CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Metadata"));
+ if (!metadata_bundle)
+ return;
+
+ md_item_set_attribute_func = (MDItemSetAttribute_type)
+ CFBundleGetFunctionPointerForName(metadata_bundle,
+ CFSTR("MDItemSetAttribute"));
+ }
+ if (!md_item_set_attribute_func)
+ return;
+
+ NSString* file_path =
+ [NSString stringWithUTF8String:file.value().c_str()];
+ if (!file_path)
+ return;
+
+ base::mac::ScopedCFTypeRef<MDItemRef> md_item(
+ MDItemCreate(NULL, base::mac::NSToCFCast(file_path)));
+ if (!md_item)
+ return;
+
+ // We won't put any more than 2 items into the attribute.
+ NSMutableArray* list = [NSMutableArray arrayWithCapacity:2];
+
+ // Follow Safari's lead: the first item in the list is the source URL of
+ // the downloaded file. If the referrer is known, store that, too.
+ NSString* origin_url = [NSString stringWithUTF8String:source.spec().c_str()];
+ if (origin_url)
+ [list addObject:origin_url];
+ NSString* referrer_url =
+ [NSString stringWithUTF8String:referrer.spec().c_str()];
+ if (referrer_url)
+ [list addObject:referrer_url];
+
+ md_item_set_attribute_func(md_item, kMDItemWhereFroms,
+ base::mac::NSToCFCast(list));
+}
+
+// The OS will automatically quarantine files due to the
+// LSFileQuarantineEnabled entry in our Info.plist, but it knows relatively
+// little about the files. We add more information about the download to
+// improve the UI shown by the OS when the users tries to open the file.
+void AddQuarantineMetadataToFile(const FilePath& file, const GURL& source,
+ const GURL& referrer) {
+ FSRef file_ref;
+ if (!base::mac::FSRefFromPath(file.value(), &file_ref))
+ return;
+
+ NSMutableDictionary* quarantine_properties = nil;
+ CFTypeRef quarantine_properties_base = NULL;
+ if (LSCopyItemAttribute(&file_ref, kLSRolesAll, kLSItemQuarantineProperties,
+ &quarantine_properties_base) == noErr) {
+ if (CFGetTypeID(quarantine_properties_base) ==
+ CFDictionaryGetTypeID()) {
+ // Quarantine properties will already exist if LSFileQuarantineEnabled
+ // is on and the file doesn't match an exclusion.
+ quarantine_properties =
+ [[(NSDictionary*)quarantine_properties_base mutableCopy] autorelease];
+ } else {
+ LOG(WARNING) << "kLSItemQuarantineProperties is not a dictionary on file "
+ << file.value();
+ }
+ CFRelease(quarantine_properties_base);
+ }
+
+ if (!quarantine_properties) {
+ // If there are no quarantine properties, then the file isn't quarantined
+ // (e.g., because the user has set up exclusions for certain file types).
+ // We don't want to add any metadata, because that will cause the file to
+ // be quarantined against the user's wishes.
+ return;
+ }
+
+ // kLSQuarantineAgentNameKey, kLSQuarantineAgentBundleIdentifierKey, and
+ // kLSQuarantineTimeStampKey are set for us (see LSQuarantine.h), so we only
+ // need to set the values that the OS can't infer.
+
+ if (![quarantine_properties valueForKey:(NSString*)kLSQuarantineTypeKey]) {
+ CFStringRef type = (source.SchemeIs("http") || source.SchemeIs("https"))
+ ? kLSQuarantineTypeWebDownload
+ : kLSQuarantineTypeOtherDownload;
+ [quarantine_properties setValue:(NSString*)type
+ forKey:(NSString*)kLSQuarantineTypeKey];
+ }
+
+ if (![quarantine_properties
+ valueForKey:(NSString*)kLSQuarantineOriginURLKey] &&
+ referrer.is_valid()) {
+ NSString* referrer_url =
+ [NSString stringWithUTF8String:referrer.spec().c_str()];
+ [quarantine_properties setValue:referrer_url
+ forKey:(NSString*)kLSQuarantineOriginURLKey];
+ }
+
+ if (![quarantine_properties valueForKey:(NSString*)kLSQuarantineDataURLKey] &&
+ source.is_valid()) {
+ NSString* origin_url =
+ [NSString stringWithUTF8String:source.spec().c_str()];
+ [quarantine_properties setValue:origin_url
+ forKey:(NSString*)kLSQuarantineDataURLKey];
+ }
+
+ OSStatus os_error = LSSetItemAttribute(&file_ref, kLSRolesAll,
+ kLSItemQuarantineProperties,
+ quarantine_properties);
+ if (os_error != noErr) {
+ LOG(WARNING) << "Unable to set quarantine attributes on file "
+ << file.value();
+ }
+}
+
+} // namespace file_metadata
diff --git a/chrome/browser/mac/install.sh b/chrome/browser/mac/install.sh
new file mode 100755
index 0000000..6cf110e
--- /dev/null
+++ b/chrome/browser/mac/install.sh
@@ -0,0 +1,123 @@
+#!/bin/bash -p
+
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Called by the application to install in a new location. Generally, this
+# means that the application is running from a disk image and wants to be
+# copied to /Applications. The application, when running from the disk image,
+# will call this script to perform the copy.
+#
+# This script will be run as root if the application determines that it would
+# not otherwise have permission to perform the copy.
+#
+# When running as root, this script will be invoked with the real user ID set
+# to the user's ID, but the effective user ID set to 0 (root). bash -p is
+# used on the first line to prevent bash from setting the effective user ID to
+# the real user ID (dropping root privileges).
+
+set -e
+
+# This script may run as root, so be paranoid about things like ${PATH}.
+export PATH="/usr/bin:/usr/sbin:/bin:/sbin"
+
+# If running as root, output the pid to stdout before doing anything else.
+# See chrome/browser/mac/authorization_util.h.
+if [ ${EUID} -eq 0 ] ; then
+ echo "${$}"
+fi
+
+if [ ${#} -ne 2 ] ; then
+ echo "usage: ${0} SRC DEST" >& 2
+ exit 2
+fi
+
+SRC=${1}
+DEST=${2}
+
+# Make sure that SRC is an absolute path and that it exists.
+if [ -z "${SRC}" ] || [ "${SRC:0:1}" != "/" ] || [ ! -d "${SRC}" ] ; then
+ echo "${0}: source ${SRC} sanity check failed" >& 2
+ exit 3
+fi
+
+# Make sure that DEST is an absolute path and that it doesn't yet exist.
+if [ -z "${DEST}" ] || [ "${DEST:0:1}" != "/" ] || [ -e "${DEST}" ] ; then
+ echo "${0}: destination ${DEST} sanity check failed" >& 2
+ exit 4
+fi
+
+# Do the copy.
+rsync -lrpt "${SRC}/" "${DEST}"
+
+# The remaining steps are not considered critical.
+set +e
+
+# Notify LaunchServices.
+/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister "${DEST}"
+
+# If this script is not running as root and the application is installed
+# somewhere under /Applications, try to make it writable by all admin users.
+# This will allow other admin users to update the application from their own
+# user Keystone instances even if the Keystone ticket is not promoted to
+# system level.
+#
+# If the script is not running as root and the application is not installed
+# under /Applications, it might not be in a system-wide location, and it
+# probably won't be something that other users on the system are running, so
+# err on the side of safety and don't make it group-writable.
+#
+# If this script is running as root, a Keystone ticket promotion is expected,
+# and future updates can be expected to be applied as root, so
+# admin-writeability is not a concern. Set the entire thing to be owned by
+# root in that case, regardless of where it's installed, and drop any group
+# and other write permission.
+#
+# If this script is running as a user that is not a member of the admin group,
+# the chgrp operation will not succeed. Tolerate that case, because it's
+# better than the alternative, which is to make the application
+# world-writable.
+CHMOD_MODE="a+rX,u+w,go-w"
+if [ ${EUID} -ne 0 ] ; then
+ if [ "${DEST:0:14}" = "/Applications/" ] &&
+ chgrp -Rh admin "${DEST}" >& /dev/null ; then
+ CHMOD_MODE="a+rX,ug+w,o-w"
+ fi
+else
+ chown -Rh root:wheel "${DEST}" >& /dev/null
+fi
+
+chmod -R "${CHMOD_MODE}" "${DEST}" >& /dev/null
+
+# On the Mac, or at least on HFS+, symbolic link permissions are significant,
+# but chmod -R and -h can't be used together. Do another pass to fix the
+# permissions on any symbolic links.
+find "${DEST}" -type l -exec chmod -h "${CHMOD_MODE}" {} + >& /dev/null
+
+# Host OS version check, to be able to take advantage of features on newer
+# systems and fall back to slow ways of doing things on older systems.
+OS_VERSION=$(sw_vers -productVersion)
+OS_MAJOR=$(sed -Ene 's/^([0-9]+).*/\1/p' <<< ${OS_VERSION})
+OS_MINOR=$(sed -Ene 's/^([0-9]+)\.([0-9]+).*/\2/p' <<< ${OS_VERSION})
+
+# Because this script is launched by the application itself, the installation
+# process inherits the quarantine bit (LSFileQuarantineEnabled). Any files or
+# directories created during the update will be quarantined in that case,
+# which may cause Launch Services to display quarantine UI. That's bad,
+# especially if it happens when the outer .app launches a quarantined inner
+# helper. Since the user approved the application launch if quarantined, it
+# it can be assumed that the installed copy should not be quarantined. Use
+# xattr to drop the quarantine attribute.
+QUARANTINE_ATTR=com.apple.quarantine
+if [ ${OS_MAJOR} -gt 10 ] ||
+ ([ ${OS_MAJOR} -eq 10 ] && [ ${OS_MINOR} -ge 6 ]) ; then
+ # On 10.6, xattr supports -r for recursive operation.
+ xattr -d -r "${QUARANTINE_ATTR}" "${DEST}" >& /dev/null
+else
+ # On earlier systems, xattr doesn't support -r, so run xattr via find.
+ find "${DEST}" -exec xattr -d "${QUARANTINE_ATTR}" {} + >& /dev/null
+fi
+
+# Great success!
+exit 0
diff --git a/chrome/browser/mac/install_from_dmg.h b/chrome/browser/mac/install_from_dmg.h
new file mode 100644
index 0000000..34d66d5
--- /dev/null
+++ b/chrome/browser/mac/install_from_dmg.h
@@ -0,0 +1,15 @@
+// Copyright (c) 2011 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_MAC_INSTALL_FROM_DMG_H_
+#define CHROME_BROWSER_MAC_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_MAC_INSTALL_FROM_DMG_H_
diff --git a/chrome/browser/mac/install_from_dmg.mm b/chrome/browser/mac/install_from_dmg.mm
new file mode 100644
index 0000000..4361f5f
--- /dev/null
+++ b/chrome/browser/mac/install_from_dmg.mm
@@ -0,0 +1,438 @@
+// Copyright (c) 2011 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/mac/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/mac/authorization_util.h"
+#include "chrome/browser/mac/scoped_authorizationref.h"
+#import "chrome/browser/mac/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(
+ base::mac::NSToCFCast(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 = base::mac::NSToCFCast(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/mac/keystone_glue.h b/chrome/browser/mac/keystone_glue.h
new file mode 100644
index 0000000..58b5705
--- /dev/null
+++ b/chrome/browser/mac/keystone_glue.h
@@ -0,0 +1,209 @@
+// Copyright (c) 2011 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_MAC_KEYSTONE_GLUE_H_
+#define CHROME_BROWSER_MAC_KEYSTONE_GLUE_H_
+#pragma once
+
+#include "base/string16.h"
+
+#if defined(__OBJC__)
+
+#import <Foundation/Foundation.h>
+
+#import "base/memory/scoped_nsobject.h"
+#include "chrome/browser/mac/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_MAC_KEYSTONE_GLUE_H_
diff --git a/chrome/browser/mac/keystone_glue.mm b/chrome/browser/mac/keystone_glue.mm
new file mode 100644
index 0000000..ec84eab
--- /dev/null
+++ b/chrome/browser/mac/keystone_glue.mm
@@ -0,0 +1,921 @@
+// Copyright (c) 2011 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/mac/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/memory/ref_counted.h"
+#include "base/sys_string_conversions.h"
+#include "base/task.h"
+#include "base/threading/worker_pool.h"
+#include "chrome/browser/mac/authorization_util.h"
+#import "chrome/browser/mac/keystone_registration.h"
+#include "chrome/browser/platform_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 {
+
+namespace ksr = keystone_registration;
+
+// 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 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";
+NSString* const kVersionKey = @"KSVersion";
+
+} // 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:ksr::KSRegistrationDidCompleteNotification
+ object:nil];
+
+ [center addObserver:self
+ selector:@selector(promotionComplete:)
+ name:ksr::KSRegistrationPromotionDidCompleteNotification
+ object:nil];
+
+ [center addObserver:self
+ selector:@selector(checkForUpdateComplete:)
+ name:ksr::KSRegistrationCheckForUpdateNotification
+ object:nil];
+
+ [center addObserver:self
+ selector:@selector(installUpdateComplete:)
+ name:ksr::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:kVersionKey];
+
+ 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 = ksr::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) {
+
+ NSFileManager* fm = [NSFileManager defaultManager];
+ NSString* userBrandFile = UserBrandFilePath();
+ NSString* systemBrandFile = SystemBrandFilePath();
+
+ // Default to none.
+ brandFileType_ = kBrandFileTypeNone;
+
+ // Only the stable channel has a brand code.
+ platform_util::Channel channel = platform_util::GetChannel();
+
+ if (channel == platform_util::CHANNEL_DEV ||
+ channel == platform_util::CHANNEL_BETA) {
+
+ // If on the dev or beta channel, this installation may have replaced
+ // an older system-level installation. Check for a user brand file and
+ // nuke it if present. Don't try to remove the system brand file, there
+ // wouldn't be any permission to do so.
+ //
+ // Don't do this on the canary channel. The canary can run side-by-side
+ // with another Google Chrome installation whose brand code, if any,
+ // should remain intact.
+
+ if ([fm fileExistsAtPath:userBrandFile]) {
+ [fm removeItemAtPath:userBrandFile error:NULL];
+ }
+
+ } else if (channel == platform_util::CHANNEL_STABLE) {
+
+ // 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:ksr::kKSPathExistenceChecker];
+ NSNumber* preserveTTToken = [NSNumber numberWithBool:YES];
+ NSString* appInfoPlistPath = [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_, ksr::KSRegistrationVersionKey,
+ appInfoPlistPath, ksr::KSRegistrationVersionPathKey,
+ kVersionKey, ksr::KSRegistrationVersionKeyKey,
+ xcType, ksr::KSRegistrationExistenceCheckerTypeKey,
+ appPath_, ksr::KSRegistrationExistenceCheckerStringKey,
+ url_, ksr::KSRegistrationServerURLStringKey,
+ preserveTTToken, ksr::KSRegistrationPreserveTrustedTesterTokenKey,
+ channel_, ksr::KSRegistrationTagKey,
+ appInfoPlistPath, ksr::KSRegistrationTagPathKey,
+ kChannelKey, ksr::KSRegistrationTagKeyKey,
+ brandPath, ksr::KSRegistrationBrandPathKey,
+ brandKey, ksr::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, ksr::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:ksr::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, ksr::KSRegistrationCheckForUpdateNotification will be
+ // posted, and -checkForUpdateComplete: will be called.
+}
+
+- (void)checkForUpdateComplete:(NSNotification*)notification {
+ NSDictionary* userInfo = [notification userInfo];
+
+ if ([[userInfo objectForKey:ksr::KSRegistrationUpdateCheckErrorKey]
+ boolValue]) {
+ [self updateStatus:kAutoupdateCheckFailed version:nil];
+ } else if ([[userInfo objectForKey:ksr::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:ksr::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, ksr::KSRegistrationStartUpdateNotification will be
+ // posted, and -installUpdateComplete: will be called.
+}
+
+- (void)installUpdateComplete:(NSNotification*)notification {
+ NSDictionary* userInfo = [notification userInfo];
+
+ if (![[userInfo objectForKey:ksr::KSUpdateCheckSuccessfulKey] boolValue] ||
+ ![[userInfo objectForKey:ksr::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] == ksr::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(
+ base::mac::NSToCFCast(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:ksr::KSRegistrationBrandPathKey];
+ parameters = temp_parameters;
+ }
+
+ if (![registration_ promoteWithParameters:parameters
+ authorization:authorization_]) {
+ [self updateStatus:kAutoupdatePromoteFailed version:nil];
+ authorization_.reset();
+ return;
+ }
+
+ // Upon completion, ksr::KSRegistrationPromotionDidCompleteNotification will
+ // be posted, and -promotionComplete: will be called.
+}
+
+- (void)promotionComplete:(NSNotification*)notification {
+ NSDictionary* userInfo = [notification userInfo];
+ if ([[userInfo objectForKey:ksr::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/mac/keystone_glue_unittest.mm b/chrome/browser/mac/keystone_glue_unittest.mm
new file mode 100644
index 0000000..5b9c2ca0
--- /dev/null
+++ b/chrome/browser/mac/keystone_glue_unittest.mm
@@ -0,0 +1,222 @@
+// Copyright (c) 2011 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/mac/keystone_glue.h"
+#import "chrome/browser/mac/keystone_registration.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace ksr = keystone_registration;
+
+
+@interface FakeKeystoneRegistration : KSRegistration
+@end
+
+
+// This unit test implements FakeKeystoneRegistration as a KSRegistration
+// subclass. It won't be linked against KSRegistration, so provide a stub
+// KSRegistration class on which to base FakeKeystoneRegistration.
+@implementation KSRegistration
+
++ (id)registrationWithProductID:(NSString*)productID {
+ return nil;
+}
+
+- (BOOL)registerWithParameters:(NSDictionary*)args {
+ return NO;
+}
+
+- (BOOL)promoteWithParameters:(NSDictionary*)args
+ authorization:(AuthorizationRef)authorization {
+ return NO;
+}
+
+- (void)setActive {
+}
+
+- (void)checkForUpdate {
+}
+
+- (void)startUpdate {
+}
+
+- (ksr::KSRegistrationTicketType)ticketType {
+ return ksr::kKSRegistrationDontKnowWhatKindOfTicket;
+}
+
+@end
+
+
+@implementation FakeKeystoneRegistration
+
+// 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:ksr::KSRegistrationCheckForUpdateNotification
+ object:nil
+ userInfo:dictionary];
+}
+
+- (void)startUpdate {
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
+ [center postNotificationName:ksr::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_ = [[FakeKeystoneRegistration 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/mac/keystone_promote_postflight.sh b/chrome/browser/mac/keystone_promote_postflight.sh
new file mode 100755
index 0000000..6344cd3
--- /dev/null
+++ b/chrome/browser/mac/keystone_promote_postflight.sh
@@ -0,0 +1,55 @@
+#!/bin/bash -p
+
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Called as root after Keystone ticket promotion to change the owner, group,
+# and permissions on the application. The application bundle and its contents
+# are set to owner root, group wheel, and to be writable only by root, but
+# readable and executable (when appropriate) by everyone.
+#
+# Note that this script will be invoked with the real user ID set to the
+# user's ID, but the effective user ID set to 0 (root). bash -p is used on
+# the first line to prevent bash from setting the effective user ID to the
+# real user ID (dropping root privileges).
+#
+# WARNING: This script is NOT currently run when the Keystone ticket is
+# promoted during application installation directly from the disk image,
+# because the installation process itself handles the same permission fix-ups
+# that this script normally would.
+
+set -e
+
+# This script runs as root, so be paranoid about things like ${PATH}.
+export PATH="/usr/bin:/usr/sbin:/bin:/sbin"
+
+# Output the pid to stdout before doing anything else. See
+# chrome/browser/mac/authorization_util.h.
+echo "${$}"
+
+if [ ${#} -ne 1 ] ; then
+ echo "usage: ${0} APP" >& 2
+ exit 2
+fi
+
+APP="${1}"
+
+# Make sure that APP is an absolute path and that it exists.
+if [ -z "${APP}" ] || [ "${APP:0:1}" != "/" ] || [ ! -d "${APP}" ] ; then
+ echo "${0}: must provide an absolute path naming an extant directory" >& 2
+ exit 3
+fi
+
+OWNER_GROUP="root:wheel"
+chown -Rh "${OWNER_GROUP}" "${APP}" >& /dev/null
+
+CHMOD_MODE="a+rX,u+w,go-w"
+chmod -R "${CHMOD_MODE}" "${APP}" >& /dev/null
+
+# On the Mac, or at least on HFS+, symbolic link permissions are significant,
+# but chmod -R and -h can't be used together. Do another pass to fix the
+# permissions on any symbolic links.
+find "${APP}" -type l -exec chmod -h "${CHMOD_MODE}" {} + >& /dev/null
+
+exit 0
diff --git a/chrome/browser/mac/keystone_promote_preflight.sh b/chrome/browser/mac/keystone_promote_preflight.sh
new file mode 100755
index 0000000..e054ff0
--- /dev/null
+++ b/chrome/browser/mac/keystone_promote_preflight.sh
@@ -0,0 +1,97 @@
+#!/bin/bash -p
+
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Called as root before Keystone ticket promotion to ensure a suitable
+# environment for Keystone installation. Ultimately, these features should be
+# integrated directly into the Keystone installation.
+#
+# If the two branding paths are given, then the branding information is also
+# copied and the permissions on the system branding file are set to be owned by
+# root, but readable by anyone.
+#
+# Note that this script will be invoked with the real user ID set to the
+# user's ID, but the effective user ID set to 0 (root). bash -p is used on
+# the first line to prevent bash from setting the effective user ID to the
+# real user ID (dropping root privileges).
+#
+# TODO(mark): Remove this script when able. See http://b/2285921 and
+# http://b/2289908.
+
+set -e
+
+# This script runs as root, so be paranoid about things like ${PATH}.
+export PATH="/usr/bin:/usr/sbin:/bin:/sbin"
+
+# Output the pid to stdout before doing anything else. See
+# chrome/browser/mac/authorization_util.h.
+echo "${$}"
+
+if [ ${#} -ne 0 ] && [ ${#} -ne 2 ] ; then
+ echo "usage: ${0} [USER_BRAND SYSTEM_BRAND]" >& 2
+ exit 2
+fi
+
+if [ ${#} -eq 2 ] ; then
+ USER_BRAND="${1}"
+ SYSTEM_BRAND="${2}"
+
+ # Make sure that USER_BRAND is an absolute path and that it exists.
+ if [ -z "${USER_BRAND}" ] || \
+ [ "${USER_BRAND:0:1}" != "/" ] || \
+ [ ! -f "${USER_BRAND}" ] ; then
+ echo "${0}: must provide an absolute path naming an existing user file" >& 2
+ exit 3
+ fi
+
+ # Make sure that SYSTEM_BRAND is an absolute path.
+ if [ -z "${SYSTEM_BRAND}" ] || [ "${SYSTEM_BRAND:0:1}" != "/" ] ; then
+ echo "${0}: must provide an absolute path naming a system file" >& 2
+ exit 4
+ fi
+
+ # Make sure the directory for the system brand file exists.
+ SYSTEM_BRAND_DIR=$(dirname "${SYSTEM_BRAND}")
+ if [ ! -e "${SYSTEM_BRAND_DIR}" ] ; then
+ mkdir -p "${SYSTEM_BRAND_DIR}"
+ # Permissions on this directory will be fixed up at the end of this script.
+ fi
+
+ # Copy the brand file
+ cp "${USER_BRAND}" "${SYSTEM_BRAND}" >& /dev/null
+
+ # Ensure the right ownership and permissions
+ chown "root:wheel" "${SYSTEM_BRAND}" >& /dev/null
+ chmod "a+r,u+w,go-w" "${SYSTEM_BRAND}" >& /dev/null
+
+fi
+
+OWNER_GROUP="root:admin"
+CHMOD_MODE="a+rX,u+w,go-w"
+
+LIB_GOOG="/Library/Google"
+if [ -d "${LIB_GOOG}" ] ; then
+ # Just work with the directory. Don't do anything recursively here, so as
+ # to leave other things in /Library/Google alone.
+ chown -h "${OWNER_GROUP}" "${LIB_GOOG}" >& /dev/null
+ chmod -h "${CHMOD_MODE}" "${LIB_GOOG}" >& /dev/null
+
+ LIB_GOOG_GSU="${LIB_GOOG}/GoogleSoftwareUpdate"
+ if [ -d "${LIB_GOOG_GSU}" ] ; then
+ chown -Rh "${OWNER_GROUP}" "${LIB_GOOG_GSU}" >& /dev/null
+ chmod -R "${CHMOD_MODE}" "${LIB_GOOG_GSU}" >& /dev/null
+
+ # On the Mac, or at least on HFS+, symbolic link permissions are
+ # significant, but chmod -R and -h can't be used together. Do another
+ # pass to fix the permissions on any symbolic links.
+ find "${LIB_GOOG_GSU}" -type l -exec chmod -h "${CHMOD_MODE}" {} + >& \
+ /dev/null
+
+ # TODO(mark): If GoogleSoftwareUpdate.bundle is missing, dump TicketStore
+ # too?
+ fi
+fi
+
+exit 0
diff --git a/chrome/browser/mac/keystone_registration.h b/chrome/browser/mac/keystone_registration.h
new file mode 100644
index 0000000..4d0e0cf
--- /dev/null
+++ b/chrome/browser/mac/keystone_registration.h
@@ -0,0 +1,72 @@
+// Copyright (c) 2011 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_MAC_KEYSTONE_REGISTRATION_H_
+#define CHROME_BROWSER_MAC_KEYSTONE_REGISTRATION_H_
+#pragma once
+
+#import <Foundation/Foundation.h>
+#include <Security/Authorization.h>
+
+// Declarations of the Keystone registration bits needed here. From
+// KSRegistration.h.
+
+namespace keystone_registration {
+
+typedef enum {
+ kKSPathExistenceChecker,
+} KSExistenceCheckerType;
+
+typedef enum {
+ kKSRegistrationUserTicket,
+ kKSRegistrationSystemTicket,
+ kKSRegistrationDontKnowWhatKindOfTicket,
+} KSRegistrationTicketType;
+
+extern NSString* KSRegistrationVersionKey;
+extern NSString* KSRegistrationExistenceCheckerTypeKey;
+extern NSString* KSRegistrationExistenceCheckerStringKey;
+extern NSString* KSRegistrationServerURLStringKey;
+extern NSString* KSRegistrationPreserveTrustedTesterTokenKey;
+extern NSString* KSRegistrationTagKey;
+extern NSString* KSRegistrationTagPathKey;
+extern NSString* KSRegistrationTagKeyKey;
+extern NSString* KSRegistrationBrandPathKey;
+extern NSString* KSRegistrationBrandKeyKey;
+extern NSString* KSRegistrationVersionPathKey;
+extern NSString* KSRegistrationVersionKeyKey;
+
+extern NSString* KSRegistrationDidCompleteNotification;
+extern NSString* KSRegistrationPromotionDidCompleteNotification;
+
+extern NSString* KSRegistrationCheckForUpdateNotification;
+extern NSString* KSRegistrationStatusKey;
+extern NSString* KSRegistrationUpdateCheckErrorKey;
+
+extern NSString* KSRegistrationStartUpdateNotification;
+extern NSString* KSUpdateCheckSuccessfulKey;
+extern NSString* KSUpdateCheckSuccessfullyInstalledKey;
+
+extern NSString* KSRegistrationRemoveExistingTag;
+#define KSRegistrationPreserveExistingTag nil
+
+} // namespace keystone_registration
+
+@interface KSRegistration : NSObject
+
++ (id)registrationWithProductID:(NSString*)productID;
+
+- (BOOL)registerWithParameters:(NSDictionary*)args;
+
+- (BOOL)promoteWithParameters:(NSDictionary*)args
+ authorization:(AuthorizationRef)authorization;
+
+- (void)setActive;
+- (void)checkForUpdate;
+- (void)startUpdate;
+- (keystone_registration::KSRegistrationTicketType)ticketType;
+
+@end // @interface KSRegistration
+
+#endif // CHROME_BROWSER_MAC_KEYSTONE_REGISTRATION_H_
diff --git a/chrome/browser/mac/keystone_registration.mm b/chrome/browser/mac/keystone_registration.mm
new file mode 100644
index 0000000..779ec94
--- /dev/null
+++ b/chrome/browser/mac/keystone_registration.mm
@@ -0,0 +1,42 @@
+// Copyright (c) 2011 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/mac/keystone_registration.h"
+
+namespace keystone_registration {
+
+// Definitions of the Keystone registration constants needed here. From
+// KSRegistration.m.
+
+NSString* KSRegistrationVersionKey = @"Version";
+NSString* KSRegistrationExistenceCheckerTypeKey = @"ExistenceCheckerType";
+NSString* KSRegistrationExistenceCheckerStringKey = @"ExistenceCheckerString";
+NSString* KSRegistrationServerURLStringKey = @"URLString";
+NSString* KSRegistrationPreserveTrustedTesterTokenKey = @"PreserveTTT";
+NSString* KSRegistrationTagKey = @"Tag";
+NSString* KSRegistrationTagPathKey = @"TagPath";
+NSString* KSRegistrationTagKeyKey = @"TagKey";
+NSString* KSRegistrationBrandPathKey = @"BrandPath";
+NSString* KSRegistrationBrandKeyKey = @"BrandKey";
+NSString* KSRegistrationVersionPathKey = @"VersionPath";
+NSString* KSRegistrationVersionKeyKey = @"VersionKey";
+
+NSString* KSRegistrationDidCompleteNotification =
+ @"KSRegistrationDidCompleteNotification";
+NSString* KSRegistrationPromotionDidCompleteNotification =
+ @"KSRegistrationPromotionDidCompleteNotification";
+
+NSString* KSRegistrationCheckForUpdateNotification =
+ @"KSRegistrationCheckForUpdateNotification";
+NSString* KSRegistrationStatusKey = @"Status";
+NSString* KSRegistrationUpdateCheckErrorKey = @"Error";
+
+NSString* KSRegistrationStartUpdateNotification =
+ @"KSRegistrationStartUpdateNotification";
+NSString* KSUpdateCheckSuccessfulKey = @"CheckSuccessful";
+NSString* KSUpdateCheckSuccessfullyInstalledKey = @"SuccessfullyInstalled";
+
+NSString* KSRegistrationRemoveExistingTag = @"";
+
+} // namespace keystone_registration
diff --git a/chrome/browser/mac/scoped_authorizationref.h b/chrome/browser/mac/scoped_authorizationref.h
new file mode 100644
index 0000000..2a4c6fe
--- /dev/null
+++ b/chrome/browser/mac/scoped_authorizationref.h
@@ -0,0 +1,80 @@
+// Copyright (c) 2011 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_MAC_SCOPED_AUTHORIZATIONREF_H_
+#define CHROME_BROWSER_MAC_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_MAC_SCOPED_AUTHORIZATIONREF_H_