summaryrefslogtreecommitdiffstats
path: root/chrome/browser/cocoa/install_from_dmg.mm
blob: d6e28f02b240e5399ca5fe20a0c5e11aab7f5b7d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/cocoa/install_from_dmg.h"

#include <ApplicationServices/ApplicationServices.h>
#import <AppKit/AppKit.h>
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
#include <IOKit/IOKitLib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/mount.h>

#import "app/l10n_util_mac.h"
#include "base/basictypes.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/scoped_nsautorelease_pool.h"
#include "base/sys_info.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.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 scoped_cftyperef from base/scoped_cftyperef.h, 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;
}

// Copies source_path to target_path and performs any additional on-disk
// bookkeeping needed to be able to launch target_path properly.
bool InstallFromDiskImage(NSString* source_path, NSString* target_path) {
  NSFileManager* file_manager = [NSFileManager defaultManager];

  // For the purposes of this copy, the file manager's delegate shouldn't be
  // consulted at all.  Clear the delegate and restore it after the copy is
  // done.
  id file_manager_delegate = [file_manager delegate];
  [file_manager setDelegate:nil];

  NSError* copy_error;
  bool copy_result = [file_manager copyItemAtPath:source_path
                                           toPath:target_path
                                            error:&copy_error];

  [file_manager setDelegate:file_manager_delegate];

  if (!copy_result) {
    LOG(ERROR) << "-[NSFileManager copyItemAtPath:toPath:error:]: "
               << [[copy_error description] UTF8String];
    return false;
  }

  // Since the application performed the copy, and the application has the
  // quarantine bit (LSFileQuarantineEnabled) set, the installed copy will
  // be quarantined.  That's bad, because it will cause the quarantine dialog
  // to be displayed, possibly after a long delay, when the application is
  // relaunched.  Use xattr to drop the quarantine attribute.
  //
  // There are three reasons not to use MDItemRemoveAttribute directly:
  // 1. MDItemRemoveAttribute is a private API.
  // 2. The operation needs to be recursive, and writing a bunch of code to
  //    handle the recursion just to call a private API is annoying.
  // 3. All of this stuff will likely move into a shell script anyway, and
  //    the shell script will have no choice but to use xattr.

  int32 os_major, os_minor, os_patch;
  base::SysInfo::OperatingSystemVersionNumbers(&os_major, &os_minor, &os_patch);

  const NSString* xattr_path = @"/usr/bin/xattr";
  const NSString* quarantine_attribute = @"com.apple.quarantine";
  NSString* launch_path;
  NSArray* arguments;

  if (os_major > 10 || (os_major == 10 && os_minor >= 6)) {
    // On 10.6, xattr supports -r for recursive operation.
    launch_path = xattr_path;
    arguments = [NSArray arrayWithObjects:@"-r",
                                          @"-d",
                                          quarantine_attribute,
                                          target_path,
                                          nil];
  } else {
    // On earlier systems, xattr doesn't support -r, so run xattr via find.
    launch_path = @"/usr/bin/find";
    arguments = [NSArray arrayWithObjects:target_path,
                                          @"-exec",
                                          xattr_path,
                                          @"-d",
                                          quarantine_attribute,
                                          @"{}",
                                          @"+",
                                          nil];
  }

  NSTask* task;
  @try {
    task = [NSTask launchedTaskWithLaunchPath:launch_path
                                    arguments:arguments];
  } @catch(NSException* exception) {
    LOG(ERROR) << "+[NSTask launchedTaskWithLaunchPath:arguments:]: "
               << [[exception description] UTF8String];
    return false;
  }

  [task waitUntilExit];
  int status = [task terminationStatus];
  if (status != 0) {
    LOG(ERROR) << "/usr/bin/xattr: status " << status;
    return false;
  }

  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;
  }

  // Use an empty dictionary for the environment.
  NSDictionary* environment = [NSDictionary dictionary];

  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.environment = reinterpret_cast<CFDictionaryRef>(environment);
  parameters.argv = reinterpret_cast<CFArrayRef>(arguments);

  err = LSOpenApplication(&parameters, NULL);
  if (err != noErr) {
    LOG(ERROR) << "LSOpenApplication: " << err;
    return false;
  }

  return true;
}

void ShowErrorDialog() {
  NSString* title = l10n_util::GetNSStringWithFixup(
      IDS_INSTALL_FROM_DMG_ERROR_TITLE);
  NSString* error = l10n_util::GetNSStringFWithFixup(
      IDS_INSTALL_FROM_DMG_ERROR, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME));
  NSString* ok = l10n_util::GetNSStringWithFixup(IDS_OK);

  NSAlert* alert = [[[NSAlert alloc] init] autorelease];

  [alert setAlertStyle:NSWarningAlertStyle];
  [alert setMessageText:title];
  [alert setInformativeText:error];
  [alert addButtonWithTitle:ok];

  [alert runModal];
}

}  // namespace

bool MaybeInstallFromDiskImage() {
  base::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) {
    LOG(INFO) << "No application directory at "
              << [application_directory UTF8String];
    return false;
  }

  // TODO(mark): When this happens, prompt for authentication.
  if (![file_manager isWritableFileAtPath:application_directory]) {
    LOG(INFO) << "Non-writable 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]) {
    LOG(INFO) << "Something already exists at " << [target_path UTF8String];
    return false;
  }

  if (!ShouldInstallDialog()) {
    return false;
  }

  if (!InstallFromDiskImage(source_path, target_path) ||
      !LaunchInstalledApp(target_path)) {
    ShowErrorDialog();
    return false;
  }

  return true;
}