910e62b5创建于 1月15日历史提交
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ash/file_manager/open_with_browser.h"

#include <stddef.h>

#include "ash/constants/web_app_id_constants.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/launch_result_type.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/drive/drive_integration_service_factory.h"
#include "chrome/browser/ash/file_manager/file_tasks.h"
#include "chrome/browser/ash/file_manager/filesystem_api_util.h"
#include "chrome/browser/ash/file_manager/office_file_tasks.h"
#include "chrome/browser/ash/fileapi/external_file_url_util.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h"
#include "chrome/browser/ui/webui/ash/cloud_upload/hats_office_trigger.h"
#include "chrome/common/chrome_content_client.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chromeos/ash/components/drivefs/drivefs_util.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h"
#include "components/drive/drive_api_util.h"
#include "components/drive/file_system_core_util.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/api/file_handlers/mime_util.h"
#include "net/base/filename_util.h"
#include "pdf/buildflags.h"
#include "storage/browser/file_system/file_system_url.h"

using content::BrowserThread;

namespace file_manager::util {

namespace {

// List of file extensions viewable in the browser.
constexpr const base::FilePath::CharType* kFileExtensionsViewableInBrowser[] = {
    FILE_PATH_LITERAL(".bmp"),   FILE_PATH_LITERAL(".ico"),
    FILE_PATH_LITERAL(".jpg"),   FILE_PATH_LITERAL(".jpeg"),
    FILE_PATH_LITERAL(".png"),   FILE_PATH_LITERAL(".webp"),
    FILE_PATH_LITERAL(".gif"),   FILE_PATH_LITERAL(".txt"),
    FILE_PATH_LITERAL(".html"),  FILE_PATH_LITERAL(".htm"),
    FILE_PATH_LITERAL(".mhtml"), FILE_PATH_LITERAL(".mht"),
    FILE_PATH_LITERAL(".xhtml"), FILE_PATH_LITERAL(".xht"),
    FILE_PATH_LITERAL(".shtml"), FILE_PATH_LITERAL(".svg"),
#if BUILDFLAG(ENABLE_PDF)
    FILE_PATH_LITERAL(".pdf"),
#endif  // BUILDFLAG(ENABLE_PDF)
};

// Returns true if |file_path| is viewable in the browser (ex. HTML file).
bool IsViewableInBrowser(const base::FilePath& file_path) {
  for (size_t i = 0; i < std::size(kFileExtensionsViewableInBrowser); i++) {
    if (file_path.MatchesExtension(kFileExtensionsViewableInBrowser[i])) {
      return true;
    }
  }
  return false;
}

bool OpenNewTab(const GURL& url) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!ash::NewWindowDelegate::GetInstance()) {
    return false;
  }
  ash::NewWindowDelegate::GetInstance()->OpenUrl(
      url, ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      ash::NewWindowDelegate::Disposition::kNewForegroundTab);
  return true;
}

std::optional<std::string> GetAppIdFromFilePath(
    const GURL& url,
    const base::FilePath& file_path) {
  if (url.SchemeIsFile()) {
    return std::nullopt;
  }
  const std::string& file_extension = file_path.FinalExtension();
  if (file_extension == ".gdoc" ||
      file_tasks::WordGroupExtensions().contains(file_extension)) {
    return ash::kGoogleDocsAppId;
  } else if (file_extension == ".gsheet" ||
             file_tasks::ExcelGroupExtensions().contains(file_extension)) {
    return ash::kGoogleSheetsAppId;
  } else if (file_extension == ".gslides" ||
             file_tasks::PowerPointGroupExtensions().contains(file_extension)) {
    return ash::kGoogleSlidesAppId;
  }
  return std::nullopt;
}

// Reads the alternate URL from a GDoc file. When it fails, returns a file URL
// for |file_path| as fallback.
// Note that an alternate url is a URL to open a hosted document.
GURL ReadUrlFromGDocAsync(const base::FilePath& file_path) {
  GURL url = drive::util::ReadUrlFromGDocFile(file_path);
  if (url.is_empty()) {
    url = net::FilePathToFileURL(file_path);
  }
  return url;
}

// Parse a local file to extract the Docs url and open this url.
void OpenGDocUrlFromFile(Profile* profile,
                         const base::FilePath& file_path,
                         LaunchAppCallback callback) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&ReadUrlFromGDocAsync, file_path),
      base::BindOnce(base::IgnoreResult(&OpenHostedFileInNewTabOrApp), profile,
                     file_path, std::move(callback)));
}

// Open a hosted GDoc, from a path hosted in DriveFS.
void OpenHostedDriveFsFile(Profile* profile,
                           const base::FilePath& file_path,
                           LaunchAppCallback callback,
                           drive::FileError error,
                           drivefs::mojom::FileMetadataPtr metadata) {
  if (error != drive::FILE_ERROR_OK) {
    return;
  }
  if (drivefs::IsLocal(metadata->type)) {
    OpenGDocUrlFromFile(profile, file_path, std::move(callback));
    return;
  }
  GURL hosted_url(metadata->alternate_url);
  if (!hosted_url.is_valid()) {
    return;
  }

  OpenHostedFileInNewTabOrApp(profile, file_path, std::move(callback),
                              hosted_url);
}

// Open an encrypted file by redirecting the user to Google Drive.
void OpenEncryptedDriveFsFile(const base::FilePath& file_path,
                              drive::FileError error,
                              drivefs::mojom::FileMetadataPtr metadata) {
  if (error != drive::FILE_ERROR_OK) {
    return;
  }
  GURL hosted_url(metadata->alternate_url);
  if (!hosted_url.is_valid()) {
    return;
  }

  OpenNewTab(hosted_url);
}

}  // namespace

bool OpenHostedFileInNewTabOrApp(Profile* profile,
                                 const base::FilePath& file_path,
                                 LaunchAppCallback callback,
                                 const GURL& hosted_url) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  std::optional<std::string> app_id =
      GetAppIdFromFilePath(hosted_url, file_path);
  if (!app_id.has_value()) {
    std::move(callback).Run(std::nullopt);
    return OpenNewTab(hosted_url);
  } else if (base::FeatureList::IsEnabled(
                 ::features::kHappinessTrackingOffice) &&
             file_tasks::IsOfficeFile(file_path)) {
    ash::cloud_upload::HatsOfficeTrigger::Get().ShowSurveyAfterAppInactive(
        app_id.value(), ash::cloud_upload::HatsOfficeLaunchingApp::kDrive);
  }
  apps::AppServiceProxy* app_service =
      apps::AppServiceProxyFactory::GetForProfile(profile);
  DCHECK(app_service);
  const apps::AppRegistryCache& cache = app_service->AppRegistryCache();
  bool is_app_available = false;
  cache.ForOneApp(
      app_id.value(), [&is_app_available](const apps::AppUpdate& update) {
        is_app_available = update.Readiness() == apps::Readiness::kReady;
      });

  if (!is_app_available) {
    std::move(callback).Run(std::nullopt);
    return OpenNewTab(hosted_url);
  }

  auto chained_callback =
      base::BindOnce([](apps::LaunchResult&& result) {
        LOG_IF(ERROR, result.state != apps::LaunchResult::State::kSuccess)
            << "Failed to launch hosted file via app despite "
               "it being ready";
        return result.state;
      }).Then(std::move(callback));

  app_service->LaunchAppWithUrl(app_id.value(), ui::EF_NONE, hosted_url,
                                apps::LaunchSource::kFromFileManager, nullptr,
                                std::move(chained_callback));
  return true;
}

bool OpenFileWithAppOrBrowser(Profile* profile,
                              const storage::FileSystemURL& file_system_url,
                              const std::string& action_id,
                              LaunchAppCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(profile);

  const base::FilePath file_path = file_system_url.path();

  if (action_id == "open-encrypted") {
    drive::DriveIntegrationService* integration_service =
        drive::DriveIntegrationServiceFactory::FindForProfile(profile);
    base::FilePath path;
    if (integration_service && integration_service->IsMounted() &&
        integration_service->GetDriveFsInterface() &&
        integration_service->GetRelativeDrivePath(file_path, &path)) {
      integration_service->GetDriveFsInterface()->GetMetadata(
          path, base::BindOnce(&OpenEncryptedDriveFsFile, file_path));
      std::move(callback).Run({apps::LaunchResult::State::kSuccess});
      return true;
    }
    LOG(WARNING) << "Failed to open file (extension): " << file_path.Extension()
                 << ": no connection to integration service";
    std::move(callback).Run({apps::LaunchResult::State::kFailed});
    return false;
  }

  if (drive::util::HasHostedDocumentExtension(file_path)) {
    if (file_manager::util::IsUnderNonNativeLocalPath(profile, file_path)) {
      // The file is on a non-native volume. Use external file URL. If the file
      // is on the drive volume, ExternalFileURLRequestJob redirects the URL to
      // drive's web interface. Otherwise (e.g. MTP, FSP), the file is just
      // downloaded in a browser tab.
      const GURL url = ash::FileSystemURLToExternalFileURL(file_system_url);
      DCHECK(!url.is_empty());
      OpenNewTab(url);
    } else {
      drive::DriveIntegrationService* integration_service =
          drive::DriveIntegrationServiceFactory::FindForProfile(profile);
      base::FilePath path;
      if (integration_service && integration_service->IsMounted() &&
          integration_service->GetDriveFsInterface() &&
          integration_service->GetRelativeDrivePath(file_path, &path)) {
        integration_service->GetDriveFsInterface()->GetMetadata(
            path, base::BindOnce(&OpenHostedDriveFsFile, profile, file_path,
                                 std::move(callback)));
        return true;
      }
      OpenGDocUrlFromFile(profile, file_path, std::move(callback));
    }
    return true;
  }

  // For things supported natively by the browser, we should open it in a tab.
  if (!(action_id == "view-pdf" || action_id == "view-in-browser")) {
    // Failed to open the file of unknown type.
    LOG(WARNING) << "Unknown file type (extension): " << file_path.Extension()
                 << " action: " << action_id;
    std::move(callback).Run({apps::LaunchResult::State::kFailed});
    return false;
  }

  // Check the MIME type to confirm that the file is a text file.
  auto* mime_type_collector =
      new extensions::app_file_handler_util::MimeTypeCollector(profile);

  auto process_mime = base::BindOnce(
      [](LaunchAppCallback callback, base::FilePath file_path,
         FileSystemURL file_system_url, std::string action_id,
         extensions::app_file_handler_util::MimeTypeCollector* mime_collector,
         std::unique_ptr<std::vector<std::string>> mimes) {
        std::string text_mime = "text/";
        std::string mime = mimes->size() > 0 ? (*mimes)[0] : "";
        bool is_text = mimes->size() > 0 && mime.starts_with(text_mime);

        if (is_text || IsViewableInBrowser(file_path) ||
            action_id == "view-pdf" ||
            (action_id == "view-in-browser" && file_path.Extension() == "")) {
          // Use external file URL if it is provided for the file system.
          GURL page_url = ash::FileSystemURLToExternalFileURL(file_system_url);
          if (page_url.is_empty()) {
            page_url = net::FilePathToFileURL(file_path);
          }
          OpenNewTab(page_url);
          std::move(callback).Run({apps::LaunchResult::State::kSuccess});
          return;
        }
        LOG(WARNING) << "Not viewable in browser: MIME: " << mime
                     << " action: " << action_id;
        std::move(callback).Run({apps::LaunchResult::State::kFailed});
      },
      std::move(callback), file_path, file_system_url, action_id,
      base::Owned(mime_type_collector));

  mime_type_collector->CollectForLocalPaths({file_path},
                                            std::move(process_mime));

  return true;
}

}  // namespace file_manager::util