<commit_before>// Copyright (c) 2017 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#import "shell/browser/ui/cocoa/electron_bundle_mover.h"

#import <AppKit/AppKit.h>
#import <Security/Security.h>
#import <dlfcn.h>
#import <sys/mount.h>
#import <sys/param.h>

#include <string>
#include <utility>

#include "gin/dictionary.h"
#include "shell/browser/browser.h"
#include "shell/common/gin_converters/callback_converter.h"

namespace gin {

template <>
struct Converter<electron::BundlerMoverConflictType> {
  static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
                                   electron::BundlerMoverConflictType value) {
    switch (value) {
      case electron::BundlerMoverConflictType::kExists:
        return gin::StringToV8(isolate, "exists");
      case electron::BundlerMoverConflictType::kExistsAndRunning:
        return gin::StringToV8(isolate, "existsAndRunning");
      default:
        return gin::StringToV8(isolate, "");
    }
  }
};

}  // namespace gin

namespace electron {

bool ElectronBundleMover::ShouldContinueMove(gin_helper::ErrorThrower thrower,
                                             BundlerMoverConflictType type,
                                             gin::Arguments* args) {
  gin::Dictionary options(args->isolate());
  bool hasOptions = args->GetNext(&options);
  base::OnceCallback<v8::Local<v8::Value>(BundlerMoverConflictType)>
      conflict_cb;

  if (hasOptions && options.Get("conflictHandler", &conflict_cb)) {
    v8::Local<v8::Value> value = std::move(conflict_cb).Run(type);
    if (value->IsBoolean()) {
      if (!value.As<v8::Boolean>()->Value())
        return false;
    } else if (!value->IsUndefined()) {
      // we only want to throw an error if a user has returned a non-boolean
      // value; this allows for client-side error handling should something in
      // the handler throw
      thrower.ThrowError("Invalid conflict handler return type.");
    }
  }
  return true;
}

bool ElectronBundleMover::Move(gin_helper::ErrorThrower thrower,
                               gin::Arguments* args) {
  // Path of the current bundle
  NSString* bundlePath = [[NSBundle mainBundle] bundlePath];

  // Skip if the application is already in the Applications folder
  if (IsInApplicationsFolder(bundlePath))
    return true;

  NSFileManager* fileManager = [NSFileManager defaultManager];

  NSString* diskImageDevice = ContainingDiskImageDevice(bundlePath);

  NSString* applicationsDirectory = [[NSSearchPathForDirectoriesInDomains(
      NSApplicationDirectory, NSLocalDomainMask, true) lastObject]
      stringByResolvingSymlinksInPath];
  NSString* bundleName = [bundlePath lastPathComponent];
  NSString* destinationPath =
      [applicationsDirectory stringByAppendingPathComponent:bundleName];

  // Check if we can write to the applications directory
  // and then make sure that if the app already exists we can overwrite it
  bool needAuthorization =
      ![fileManager isWritableFileAtPath:applicationsDirectory] ||
      ([fileManager fileExistsAtPath:destinationPath] &&
       ![fileManager isWritableFileAtPath:destinationPath]);

  // Activate app -- work-around for focus issues related to "scary file from
  // internet" OS dialog.
  if (![NSApp isActive]) {
    [NSApp activateIgnoringOtherApps:true];
  }

  // Move to applications folder
  if (needAuthorization) {
    bool authorizationCanceled;

    if (!AuthorizedInstall(bundlePath, destinationPath,
                           &authorizationCanceled)) {
      if (authorizationCanceled) {
        // User rejected the authorization request
        thrower.ThrowError("User rejected the authorization request");
        return false;
      } else {
        thrower.ThrowError(
            "Failed to copy to applications directory even with authorization");
        return false;
      }
    }
  } else {
    // If a copy already exists in the Applications folder, put it in the Trash
    if ([fileManager fileExistsAtPath:destinationPath]) {
      // But first, make sure that it's not running
      if (IsApplicationAtPathRunning(destinationPath)) {
        // Check for callback handler and get user choice for open/quit
        if (!ShouldContinueMove(
                thrower, BundlerMoverConflictType::kExistsAndRunning, args))
          return false;

        // Unless explicitly denied, give running app focus and terminate self
        [[NSTask
            launchedTaskWithLaunchPath:@"/usr/bin/open"
                             arguments:[NSArray
                                           arrayWithObject:destinationPath]]
            waitUntilExit];
        electron::Browser::Get()->Quit();
        return true;
      } else {
        // Check callback handler and get user choice for app trashing
        if (!ShouldContinueMove(thrower, BundlerMoverConflictType::kExists,
                                args))
          return false;

        // Unless explicitly denied, attempt to trash old app
        if (!Trash([applicationsDirectory
                stringByAppendingPathComponent:bundleName])) {
          thrower.ThrowError("Failed to delete existing application");
          return false;
        }
      }
    }

    if (!CopyBundle(bundlePath, destinationPath)) {
      thrower.ThrowError(
          "Failed to copy current bundle to the applications folder");
      return false;
    }
  }

  // Trash the original app. It's okay if this fails.
  // NOTE: This final delete does not work if the source bundle is in a network
  // mounted volume.
  //       Calling rm or file manager's delete method doesn't work either. It's
  //       unlikely to happen but it'd be great if someone could fix this.
  if (diskImageDevice == nil && !DeleteOrTrash(bundlePath)) {
    // Could not delete original but we just don't care
  }

  // Relaunch.
  Relaunch(destinationPath);

  // Launched from within a disk image? -- unmount (if no files are open after 5
  // seconds, otherwise leave it mounted).
  if (diskImageDevice) {
    NSString* script = [NSString
        stringWithFormat:@"(/bin/sleep 5 && /usr/bin/hdiutil detach %@) &",
                         ShellQuotedString(diskImageDevice)];
    [NSTask launchedTaskWithLaunchPath:@"/bin/sh"
                             arguments:[NSArray arrayWithObjects:@"-c", script,
                                                                 nil]];
  }

  electron::Browser::Get()->Quit();

  return true;
}

bool ElectronBundleMover::IsCurrentAppInApplicationsFolder() {
  return IsInApplicationsFolder([[NSBundle mainBundle] bundlePath]);
}

bool ElectronBundleMover::IsInApplicationsFolder(NSString* bundlePath) {
  // Check all the normal Application directories
  NSArray* applicationDirs = NSSearchPathForDirectoriesInDomains(
      NSApplicationDirectory, NSAllDomainsMask, true);
  for (NSString* appDir in applicationDirs) {
    if ([bundlePath hasPrefix:appDir])
      return true;
  }

  // Also, handle the case that the user has some other Application directory
  // (perhaps on a separate data partition).
  if ([[bundlePath pathComponents] containsObject:@"Applications"])
    return true;

  return false;
}

NSString* ElectronBundleMover::ContainingDiskImageDevice(NSString* bundlePath) {
  NSString* containingPath = [bundlePath stringByDeletingLastPathComponent];

  struct statfs fs;
  if (statfs([containingPath fileSystemRepresentation], &fs) ||
      (fs.f_flags & MNT_ROOTFS))
    return nil;

  NSString* device = [[NSFileManager defaultManager]
      stringWithFileSystemRepresentation:fs.f_mntfromname
                                  length:strlen(fs.f_mntfromname)];

  NSTask* hdiutil = [[[NSTask alloc] init] autorelease];
  [hdiutil setLaunchPath:@"/usr/bin/hdiutil"];
  [hdiutil setArguments:[NSArray arrayWithObjects:@"info", @"-plist", nil]];
  [hdiutil setStandardOutput:[NSPipe pipe]];
  [hdiutil launch];
  [hdiutil waitUntilExit];
<commit_msg>fix: edge case in app.isInApplicationsFolder() (#35636)<commit_after>// Copyright (c) 2017 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#import "shell/browser/ui/cocoa/electron_bundle_mover.h"

#import <AppKit/AppKit.h>
#import <Security/Security.h>
#import <dlfcn.h>
#import <sys/mount.h>
#import <sys/param.h>

#include <string>
#include <utility>

#include "gin/dictionary.h"
#include "shell/browser/browser.h"
#include "shell/common/gin_converters/callback_converter.h"

namespace gin {

template <>
struct Converter<electron::BundlerMoverConflictType> {
  static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
                                   electron::BundlerMoverConflictType value) {
    switch (value) {
      case electron::BundlerMoverConflictType::kExists:
        return gin::StringToV8(isolate, "exists");
      case electron::BundlerMoverConflictType::kExistsAndRunning:
        return gin::StringToV8(isolate, "existsAndRunning");
      default:
        return gin::StringToV8(isolate, "");
    }
  }
};

}  // namespace gin

namespace electron {

bool ElectronBundleMover::ShouldContinueMove(gin_helper::ErrorThrower thrower,
                                             BundlerMoverConflictType type,
                                             gin::Arguments* args) {
  gin::Dictionary options(args->isolate());
  bool hasOptions = args->GetNext(&options);
  base::OnceCallback<v8::Local<v8::Value>(BundlerMoverConflictType)>
      conflict_cb;

  if (hasOptions && options.Get("conflictHandler", &conflict_cb)) {
    v8::Local<v8::Value> value = std::move(conflict_cb).Run(type);
    if (value->IsBoolean()) {
      if (!value.As<v8::Boolean>()->Value())
        return false;
    } else if (!value->IsUndefined()) {
      // we only want to throw an error if a user has returned a non-boolean
      // value; this allows for client-side error handling should something in
      // the handler throw
      thrower.ThrowError("Invalid conflict handler return type.");
    }
  }
  return true;
}

bool ElectronBundleMover::Move(gin_helper::ErrorThrower thrower,
                               gin::Arguments* args) {
  // Path of the current bundle
  NSString* bundlePath = [[NSBundle mainBundle] bundlePath];

  // Skip if the application is already in the Applications folder
  if (IsInApplicationsFolder(bundlePath))
    return true;

  NSFileManager* fileManager = [NSFileManager defaultManager];

  NSString* diskImageDevice = ContainingDiskImageDevice(bundlePath);

  NSString* applicationsDirectory = [[NSSearchPathForDirectoriesInDomains(
      NSApplicationDirectory, NSLocalDomainMask, true) lastObject]
      stringByResolvingSymlinksInPath];
  NSString* bundleName = [bundlePath lastPathComponent];
  NSString* destinationPath =
      [applicationsDirectory stringByAppendingPathComponent:bundleName];

  // Check if we can write to the applications directory
  // and then make sure that if the app already exists we can overwrite it
  bool needAuthorization =
      ![fileManager isWritableFileAtPath:applicationsDirectory] ||
      ([fileManager fileExistsAtPath:destinationPath] &&
       ![fileManager isWritableFileAtPath:destinationPath]);

  // Activate app -- work-around for focus issues related to "scary file from
  // internet" OS dialog.
  if (![NSApp isActive]) {
    [NSApp activateIgnoringOtherApps:true];
  }

  // Move to applications folder
  if (needAuthorization) {
    bool authorizationCanceled;

    if (!AuthorizedInstall(bundlePath, destinationPath,
                           &authorizationCanceled)) {
      if (authorizationCanceled) {
        // User rejected the authorization request
        thrower.ThrowError("User rejected the authorization request");
        return false;
      } else {
        thrower.ThrowError(
            "Failed to copy to applications directory even with authorization");
        return false;
      }
    }
  } else {
    // If a copy already exists in the Applications folder, put it in the Trash
    if ([fileManager fileExistsAtPath:destinationPath]) {
      // But first, make sure that it's not running
      if (IsApplicationAtPathRunning(destinationPath)) {
        // Check for callback handler and get user choice for open/quit
        if (!ShouldContinueMove(
                thrower, BundlerMoverConflictType::kExistsAndRunning, args))
          return false;

        // Unless explicitly denied, give running app focus and terminate self
        [[NSTask
            launchedTaskWithLaunchPath:@"/usr/bin/open"
                             arguments:[NSArray
                                           arrayWithObject:destinationPath]]
            waitUntilExit];
        electron::Browser::Get()->Quit();
        return true;
      } else {
        // Check callback handler and get user choice for app trashing
        if (!ShouldContinueMove(thrower, BundlerMoverConflictType::kExists,
                                args))
          return false;

        // Unless explicitly denied, attempt to trash old app
        if (!Trash([applicationsDirectory
                stringByAppendingPathComponent:bundleName])) {
          thrower.ThrowError("Failed to delete existing application");
          return false;
        }
      }
    }

    if (!CopyBundle(bundlePath, destinationPath)) {
      thrower.ThrowError(
          "Failed to copy current bundle to the applications folder");
      return false;
    }
  }

  // Trash the original app. It's okay if this fails.
  // NOTE: This final delete does not work if the source bundle is in a network
  // mounted volume.
  //       Calling rm or file manager's delete method doesn't work either. It's
  //       unlikely to happen but it'd be great if someone could fix this.
  if (diskImageDevice == nil && !DeleteOrTrash(bundlePath)) {
    // Could not delete original but we just don't care
  }

  // Relaunch.
  Relaunch(destinationPath);

  // Launched from within a disk image? -- unmount (if no files are open after 5
  // seconds, otherwise leave it mounted).
  if (diskImageDevice) {
    NSString* script = [NSString
        stringWithFormat:@"(/bin/sleep 5 && /usr/bin/hdiutil detach %@) &",
                         ShellQuotedString(diskImageDevice)];
    [NSTask launchedTaskWithLaunchPath:@"/bin/sh"
                             arguments:[NSArray arrayWithObjects:@"-c", script,
                                                                 nil]];
  }

  electron::Browser::Get()->Quit();

  return true;
}

bool ElectronBundleMover::IsCurrentAppInApplicationsFolder() {
  return IsInApplicationsFolder([[NSBundle mainBundle] bundlePath]);
}

NSString* resolvePath(NSString* path) {
  NSString* standardizedPath = [path stringByStandardizingPath];
  char resolved[PATH_MAX];
  if (realpath([standardizedPath UTF8String], resolved) == NULL)
    return path;
  return @(resolved);
}

bool ElectronBundleMover::IsInApplicationsFolder(NSString* bundlePath) {
  // Check all the normal Application directories
  NSArray* applicationDirs = NSSearchPathForDirectoriesInDomains(
      NSApplicationDirectory, NSAllDomainsMask, true);
  NSString* resolvedBundlePath = resolvePath(bundlePath);
  for (NSString* appDir in applicationDirs) {
    if ([resolvedBundlePath hasPrefix:appDir])
      return true;
  }

  // Also, handle the case that the user has some other Application directory
  // (perhaps on a separate data partition).
  if ([[resolvedBundlePath pathComponents] containsObject:@"Applications"])
    return true;

  return false;
}

NSString* ElectronBundleMover::ContainingDiskImageDevice(NSString* bundlePath) {
  NSString* containingPath = [bundlePath stringByDeletingLastPathComponent];

  struct statfs fs;
  if (statfs([containingPath fileSystemRepresentation], &fs) ||
      (fs.f_flags & MNT_ROOTFS))
    return nil;

  NSString* device = [[NSFileManager defaultManager]
      stringWithFileSystemRepresentation:fs.f_mntfromname
                                  length:strlen(fs.f_mntfromname)];

  NSTask* hdiutil = [[[NSTask alloc] init] autorelease];
  [hdiutil setLaunchPath:@"/usr/bin/hdiutil"];
  [hdiutil setArguments:[NSArray arrayWithObjects:@"info", @"-plist", nil]];
  [hdiutil setStandardOutput:[NSPipe pipe]];
  [hdiutil launch];
  [hdiutil waitUntilExit];

{'fraction_non_alphanumeric': 0.09746827683442653, 'fraction_numerical': 0.002574633727701833, 'mean_word_length': 3.016248153618907, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 20, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7142656', 'n_tokens_mistral': 4578, 'n_tokens_neox': 4317, 'n_words': 1222}