// Copyright 2013 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 "apps/launcher.h"

#include "apps/browser/api/app_runtime/app_runtime_api.h"
#include "apps/browser/file_handler_util.h"
#include "apps/common/api/app_runtime.h"
#include "base/command_line.h"
#include "base/file_util.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/api/file_handlers/app_file_handler_util.h"
#include "chrome/browser/extensions/api/file_system/file_system_api.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/lazy_background_task_queue.h"
#include "extensions/browser/process_manager.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_messages.h"
#include "extensions/common/manifest_handlers/kiosk_mode_info.h"
#include "net/base/filename_util.h"
#include "net/base/mime_sniffer.h"
#include "net/base/mime_util.h"
#include "net/base/net_util.h"
#include "url/gurl.h"

#if defined(OS_CHROMEOS)
#include "chrome/browser/chromeos/file_manager/filesystem_api_util.h"
#include "chrome/browser/chromeos/login/users/user_manager.h"
#endif

namespace app_runtime = apps::api::app_runtime;

using apps::file_handler_util::GrantedFileEntry;
using content::BrowserThread;
using extensions::app_file_handler_util::PrepareFilesForWritableApp;
using extensions::app_file_handler_util::FileHandlerForId;
using extensions::app_file_handler_util::FileHandlerCanHandleFile;
using extensions::app_file_handler_util::FirstFileHandlerForFile;
using extensions::app_file_handler_util::CreateFileEntry;
using extensions::app_file_handler_util::HasFileSystemWritePermission;
using extensions::EventRouter;
using extensions::Extension;
using extensions::ExtensionHost;
using extensions::ExtensionSystem;

namespace apps {

namespace {

const char kFallbackMimeType[] = "application/octet-stream";

bool DoMakePathAbsolute(const base::FilePath& current_directory,
                        base::FilePath* file_path) {
  DCHECK(file_path);
  if (file_path->IsAbsolute())
    return true;

  if (current_directory.empty()) {
    *file_path = base::MakeAbsoluteFilePath(*file_path);
    return !file_path->empty();
  }

  if (!current_directory.IsAbsolute())
    return false;

  *file_path = current_directory.Append(*file_path);
  return true;
}

// Helper method to launch the platform app |extension| with no data. This
// should be called in the fallback case, where it has been impossible to
// load or obtain file launch data.
void LaunchPlatformAppWithNoData(Profile* profile, const Extension* extension) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  AppEventRouter::DispatchOnLaunchedEvent(profile, extension);
}

// Class to handle launching of platform apps to open specific paths.
// An instance of this class is created for each launch. The lifetime of these
// instances is managed by reference counted pointers. As long as an instance
// has outstanding tasks on a message queue it will be retained; once all
// outstanding tasks are completed it will be deleted.
class PlatformAppPathLauncher
    : public base::RefCountedThreadSafe<PlatformAppPathLauncher> {
 public:
  PlatformAppPathLauncher(Profile* profile,
                          const Extension* extension,
                          const std::vector<base::FilePath>& file_paths)
      : profile_(profile), extension_(extension), file_paths_(file_paths) {}

  PlatformAppPathLauncher(Profile* profile,
                          const Extension* extension,
                          const base::FilePath& file_path)
      : profile_(profile), extension_(extension) {
    if (!file_path.empty())
      file_paths_.push_back(file_path);
  }

  void Launch() {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (file_paths_.empty()) {
      LaunchPlatformAppWithNoData(profile_, extension_);
      return;
    }

    for (size_t i = 0; i < file_paths_.size(); ++i) {
      DCHECK(file_paths_[i].IsAbsolute());
    }

    if (HasFileSystemWritePermission(extension_)) {
      PrepareFilesForWritableApp(
          file_paths_,
          profile_,
          false,
          base::Bind(&PlatformAppPathLauncher::OnFileValid, this),
          base::Bind(&PlatformAppPathLauncher::OnFileInvalid, this));
      return;
    }

    OnFileValid();
  }

  void LaunchWithHandler(const std::string& handler_id) {
    handler_id_ = handler_id;
    Launch();
  }

  void LaunchWithRelativePath(const base::FilePath& current_directory) {
    BrowserThread::PostTask(
        BrowserThread::FILE,
        FROM_HERE,
        base::Bind(&PlatformAppPathLauncher::MakePathAbsolute,
                   this,
                   current_directory));
  }

 private:
  friend class base::RefCountedThreadSafe<PlatformAppPathLauncher>;

  virtual ~PlatformAppPathLauncher() {}

  void MakePathAbsolute(const base::FilePath& current_directory) {
    DCHECK_CURRENTLY_ON(BrowserThread::FILE);

    for (std::vector<base::FilePath>::iterator it = file_paths_.begin();
         it != file_paths_.end();
         ++it) {
      if (!DoMakePathAbsolute(current_directory, &*it)) {
        LOG(WARNING) << "Cannot make absolute path from " << it->value();
        BrowserThread::PostTask(
            BrowserThread::UI,
            FROM_HERE,
            base::Bind(&PlatformAppPathLauncher::LaunchWithNoLaunchData, this));
        return;
      }
    }

    BrowserThread::PostTask(BrowserThread::UI,
                            FROM_HERE,
                            base::Bind(&PlatformAppPathLauncher::Launch, this));
  }

  void OnFileValid() {
    mime_types_.resize(file_paths_.size());
#if defined(OS_CHROMEOS)
    GetNextNonNativeMimeType();
#else
    BrowserThread::PostTask(
        BrowserThread::FILE,
        FROM_HERE,
        base::Bind(&PlatformAppPathLauncher::GetMimeTypesAndLaunch, this));
#endif
  }

  void OnFileInvalid(const base::FilePath& /* error_path */) {
    LaunchWithNoLaunchData();
  }

#if defined(OS_CHROMEOS)
  void GetNextNonNativeMimeType() {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);

    bool any_native_files = false;
    for (size_t i = 0; i < mime_types_.size(); ++i) {
      if (!mime_types_[i].empty())
        continue;
      const base::FilePath& file_path = file_paths_[i];
      if (file_manager::util::IsUnderNonNativeLocalPath(profile_, file_path)) {
        file_manager::util::GetNonNativeLocalPathMimeType(
            profile_,
            file_path,
            base::Bind(&PlatformAppPathLauncher::OnGotMimeType, this, i));
        return;
      }
      any_native_files = true;
    }

    // If there are any native files, we need to call GetMimeTypesAndLaunch to
    // obtain mime types for the files.
    if (any_native_files) {
      BrowserThread::PostTask(
          BrowserThread::FILE,
          FROM_HERE,
          base::Bind(&PlatformAppPathLauncher::GetMimeTypesAndLaunch, this));
      return;
    }

    // Otherwise, we can call LaunchWithMimeTypes directly.
    LaunchWithMimeTypes();
  }

  void OnGotMimeType(size_t index, bool success, const std::string& mime_type) {
    if (!success) {
      LaunchWithNoLaunchData();
      return;
    }
    mime_types_[index] = mime_type.empty() ? kFallbackMimeType : mime_type;
    GetNextNonNativeMimeType();
  }
#endif

  void GetMimeTypesAndLaunch() {
    DCHECK_CURRENTLY_ON(BrowserThread::FILE);

    for (size_t i = 0; i < mime_types_.size(); ++i) {
      if (!this->mime_types_[i].empty())
        continue;
      const base::FilePath& file_path = file_paths_[i];

      // If the file doesn't exist, or is a directory, launch with no launch
      // data.
      if (!base::PathExists(file_path) || base::DirectoryExists(file_path)) {
        LOG(WARNING) << "No file exists with path " << file_path.value();
        BrowserThread::PostTask(
            BrowserThread::UI,
            FROM_HERE,
            base::Bind(&PlatformAppPathLauncher::LaunchWithNoLaunchData, this));
        return;
      }

      std::string mime_type;
      if (!net::GetMimeTypeFromFile(file_path, &mime_type)) {
        // If MIME type of the file can't be determined by its path,
        // try to sniff it by its content.
        std::vector<char> content(net::kMaxBytesToSniff);
        int bytes_read = base::ReadFile(file_path, &content[0], content.size());
        if (bytes_read >= 0) {
          net::SniffMimeType(&content[0],
                             bytes_read,
                             net::FilePathToFileURL(file_path),
                             std::string(),  // type_hint (passes no hint)
                             &mime_type);
        }
        if (mime_type.empty())
          mime_type = kFallbackMimeType;
      }
      mime_types_[i] = mime_type;
    }

    BrowserThread::PostTask(
        BrowserThread::UI,
        FROM_HERE,
        base::Bind(&PlatformAppPathLauncher::LaunchWithMimeTypes, this));
  }

  void LaunchWithNoLaunchData() {
    // This method is required as an entry point on the UI thread.
    LaunchPlatformAppWithNoData(profile_, extension_);
  }

  void LaunchWithMimeTypes() {
    DCHECK(file_paths_.size() == mime_types_.size());

    // Find file handler from the platform app for the file being opened.
    const extensions::FileHandlerInfo* handler = NULL;
    if (!handler_id_.empty()) {
      handler = FileHandlerForId(*extension_, handler_id_);
      if (handler) {
        for (size_t i = 0; i < file_paths_.size(); ++i) {
          if (!FileHandlerCanHandleFile(
                  *handler, mime_types_[i], file_paths_[i])) {
            LOG(WARNING)
                << "Extension does not provide a valid file handler for "
                << file_paths_[i].value();
            handler = NULL;
            break;
          }
        }
      }
    } else {
      std::set<std::pair<base::FilePath, std::string> > path_and_file_type_set;
      for (size_t i = 0; i < file_paths_.size(); ++i) {
        path_and_file_type_set.insert(
            std::make_pair(file_paths_[i], mime_types_[i]));
      }
      const std::vector<const extensions::FileHandlerInfo*>& handlers =
          extensions::app_file_handler_util::FindFileHandlersForFiles(
              *extension_, path_and_file_type_set);
      if (!handlers.empty())
        handler = handlers[0];
    }

    // If this app doesn't have a file handler that supports the file, launch
    // with no launch data.
    if (!handler) {
      LOG(WARNING) << "Extension does not provide a valid file handler.";
      LaunchWithNoLaunchData();
      return;
    }

    if (handler_id_.empty())
      handler_id_ = handler->id;

    // Access needs to be granted to the file for the process associated with
    // the extension. To do this the ExtensionHost is needed. This might not be
    // available, or it might be in the process of being unloaded, in which case
    // the lazy background task queue is used to load the extension and then
    // call back to us.
    extensions::LazyBackgroundTaskQueue* const queue =
        ExtensionSystem::Get(profile_)->lazy_background_task_queue();
    if (queue->ShouldEnqueueTask(profile_, extension_)) {
      queue->AddPendingTask(
          profile_,
          extension_->id(),
          base::Bind(&PlatformAppPathLauncher::GrantAccessToFilesAndLaunch,
                     this));
      return;
    }

    extensions::ProcessManager* const process_manager =
        ExtensionSystem::Get(profile_)->process_manager();
    ExtensionHost* const host =
        process_manager->GetBackgroundHostForExtension(extension_->id());
    DCHECK(host);
    GrantAccessToFilesAndLaunch(host);
  }

  void GrantAccessToFilesAndLaunch(ExtensionHost* host) {
    // If there was an error loading the app page, |host| will be NULL.
    if (!host) {
      LOG(ERROR) << "Could not load app page for " << extension_->id();
      return;
    }

    std::vector<GrantedFileEntry> file_entries;
    for (size_t i = 0; i < file_paths_.size(); ++i) {
      file_entries.push_back(
          CreateFileEntry(profile_,
                          extension_,
                          host->render_process_host()->GetID(),
                          file_paths_[i],
                          false));
    }

    AppEventRouter::DispatchOnLaunchedEventWithFileEntries(
        profile_, extension_, handler_id_, mime_types_, file_entries);
  }

  // The profile the app should be run in.
  Profile* profile_;
  // The extension providing the app.
  // TODO(benwells): Hold onto the extension ID instead of a pointer as it
  // is possible the extension will be unloaded while we're doing our thing.
  // See http://crbug.com/372270 for details.
  const Extension* extension_;
  // The path to be passed through to the app.
  std::vector<base::FilePath> file_paths_;
  std::vector<std::string> mime_types_;
  // The ID of the file handler used to launch the app.
  std::string handler_id_;

  DISALLOW_COPY_AND_ASSIGN(PlatformAppPathLauncher);
};

}  // namespace

void LaunchPlatformAppWithCommandLine(Profile* profile,
                                      const Extension* extension,
                                      const CommandLine& command_line,
                                      const base::FilePath& current_directory) {
  // An app with "kiosk_only" should not be installed and launched
  // outside of ChromeOS kiosk mode in the first place. This is a defensive
  // check in case this scenario does occur.
  if (extensions::KioskModeInfo::IsKioskOnly(extension)) {
    bool in_kiosk_mode = false;
#if defined(OS_CHROMEOS)
    chromeos::UserManager* user_manager = chromeos::UserManager::Get();
    in_kiosk_mode = user_manager && user_manager->IsLoggedInAsKioskApp();
#endif
    if (!in_kiosk_mode) {
      LOG(ERROR) << "App with 'kiosk_only' attribute must be run in "
          << " ChromeOS kiosk mode.";
      NOTREACHED();
      return;
    }
  }

#if defined(OS_WIN)
  base::CommandLine::StringType about_blank_url(
      base::ASCIIToWide(url::kAboutBlankURL));
#else
  base::CommandLine::StringType about_blank_url(url::kAboutBlankURL);
#endif
  CommandLine::StringVector args = command_line.GetArgs();
  // Browser tests will add about:blank to the command line. This should
  // never be interpreted as a file to open, as doing so with an app that
  // has write access will result in a file 'about' being created, which
  // causes problems on the bots.
  if (args.empty() || (command_line.HasSwitch(switches::kTestType) &&
                       args[0] == about_blank_url)) {
    LaunchPlatformAppWithNoData(profile, extension);
    return;
  }

  base::FilePath file_path(command_line.GetArgs()[0]);
  scoped_refptr<PlatformAppPathLauncher> launcher =
      new PlatformAppPathLauncher(profile, extension, file_path);
  launcher->LaunchWithRelativePath(current_directory);
}

void LaunchPlatformAppWithPath(Profile* profile,
                               const Extension* extension,
                               const base::FilePath& file_path) {
  scoped_refptr<PlatformAppPathLauncher> launcher =
      new PlatformAppPathLauncher(profile, extension, file_path);
  launcher->Launch();
}

void LaunchPlatformApp(Profile* profile, const Extension* extension) {
  LaunchPlatformAppWithCommandLine(profile,
                                   extension,
                                   CommandLine(CommandLine::NO_PROGRAM),
                                   base::FilePath());
}

void LaunchPlatformAppWithFileHandler(
    Profile* profile,
    const Extension* extension,
    const std::string& handler_id,
    const std::vector<base::FilePath>& file_paths) {
  scoped_refptr<PlatformAppPathLauncher> launcher =
      new PlatformAppPathLauncher(profile, extension, file_paths);
  launcher->LaunchWithHandler(handler_id);
}

void RestartPlatformApp(Profile* profile, const Extension* extension) {
  EventRouter* event_router = EventRouter::Get(profile);
  bool listening_to_restart = event_router->
      ExtensionHasEventListener(extension->id(),
                                app_runtime::OnRestarted::kEventName);

  if (listening_to_restart) {
    AppEventRouter::DispatchOnRestartedEvent(profile, extension);
    return;
  }

  extensions::ExtensionPrefs* extension_prefs =
      extensions::ExtensionPrefs::Get(profile);
  bool had_windows = extension_prefs->IsActive(extension->id());
  extension_prefs->SetIsActive(extension->id(), false);
  bool listening_to_launch = event_router->
      ExtensionHasEventListener(extension->id(),
                                app_runtime::OnLaunched::kEventName);

  if (listening_to_launch && had_windows)
    LaunchPlatformAppWithNoData(profile, extension);
}

void LaunchPlatformAppWithUrl(Profile* profile,
                              const Extension* extension,
                              const std::string& handler_id,
                              const GURL& url,
                              const GURL& referrer_url) {
  AppEventRouter::DispatchOnLaunchedEventWithUrl(
      profile, extension, handler_id, url, referrer_url);
}

}  // namespace apps