// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/file_system/browser_file_system_helper.h"

#include <memory>
#include <string>
#include <vector>

#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/scoped_refptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/null_task_runner.h"
#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
#include "content/public/common/child_process_id.h"
#include "content/public/common/content_client.h"
#include "content/public/common/drop_data.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_utils.h"
#include "net/base/filename_util.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/file_system/file_system_backend.h"
#include "storage/browser/file_system/file_system_options.h"
#include "storage/browser/file_system/file_system_url.h"
#include "storage/browser/file_system/isolated_context.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/common/file_system/file_system_types.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace content {
namespace browser_file_system_helper_unittest {

const int kRendererID = 42;
const ChildProcessId kRendererProcess(kRendererID);

TEST(BrowserFileSystemHelperTest,
     PrepareDropDataForChildProcess_FileSystemFiles) {
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

  // We need the task environment to use a separate IO thread so that the
  // ChildProcessSecurityPolicy checks which perform different logic
  // based on whether they are called on the UI thread or the IO thread do the
  // right thing.
  BrowserTaskEnvironment task_environment{
      content::BrowserTaskEnvironment::REAL_IO_THREAD};
  TestBrowserContext browser_context;
  ChildProcessSecurityPolicyImpl* p =
      ChildProcessSecurityPolicyImpl::GetInstance();
  p->AddForTesting(kRendererProcess, &browser_context);

  // Prepare |original_file| FileSystemURL that comes from a |sensitive_origin|.
  // This attempts to simulate for unit testing the drive URL from
  // https://crbug.com/705295#c23.
  const GURL kSensitiveOrigin(GetWebUIURL("hhaomjibdihmijegdhdafkllkbggdgoj"));
  const char kMountName[] = "drive-testuser%40gmail.com-hash";
  const base::FilePath kTestPath(FILE_PATH_LITERAL("root/dir/testfile.jpg"));
  base::FilePath mount_path = temp_dir.GetPath().AppendASCII(kMountName);
  scoped_refptr<storage::ExternalMountPoints> external_mount_points =
      storage::ExternalMountPoints::CreateRefCounted();
  EXPECT_TRUE(external_mount_points->RegisterFileSystem(
      kMountName, storage::FileSystemType::kFileSystemTypeTest,
      storage::FileSystemMountOption(), mount_path));
  storage::FileSystemURL original_file =
      external_mount_points->CreateExternalFileSystemURL(
          blink::StorageKey::CreateFirstParty(
              url::Origin::Create(kSensitiveOrigin)),
          kMountName, kTestPath);
  EXPECT_TRUE(original_file.is_valid());
  EXPECT_EQ(kSensitiveOrigin, original_file.origin().GetURL());

  // Prepare fake FileSystemContext to use in the test.
  storage::FileSystemOptions file_system_options(
      storage::FileSystemOptions::PROFILE_MODE_NORMAL,
      /*force_in_memory=*/false, std::vector<std::string>());
  auto test_file_system_context = storage::FileSystemContext::Create(
      /*io_task_runner=*/base::MakeRefCounted<base::NullTaskRunner>(),
      /*file_task_runner=*/base::MakeRefCounted<base::NullTaskRunner>(),
      std::move(external_mount_points), /*special_storage_policy=*/nullptr,
      /*quota_manager_proxy=*/nullptr,
      std::vector<std::unique_ptr<storage::FileSystemBackend>>(),
      std::vector<storage::URLRequestAutoMountHandler>(),
      /*partition_path=*/base::FilePath(), file_system_options);

  // Prepare content::DropData containing |file_system_url|.
  DropData::FileSystemFileInfo filesystem_file_info;
  filesystem_file_info.url = original_file.ToGURL();
  filesystem_file_info.size = 123;
  filesystem_file_info.filesystem_id = original_file.filesystem_id();
  DropData drop_data;
  drop_data.file_system_files.push_back(filesystem_file_info);

  // Verify that initially no access is be granted to the |kSensitiveOrigin|.
  EXPECT_FALSE(p->CanCommitURL(kRendererID, kSensitiveOrigin));

  // Verify that initially no access is granted to the |original_file|.
  EXPECT_FALSE(p->CanReadFileSystemFile(kRendererProcess, original_file));
  EXPECT_FALSE(p->CanWriteFileSystemFile(kRendererProcess, original_file));
  EXPECT_FALSE(p->CanCreateFileSystemFile(kRendererProcess, original_file));
  EXPECT_FALSE(p->CanCopyIntoFileSystemFile(kRendererProcess, original_file));
  EXPECT_FALSE(p->CanDeleteFileSystemFile(kRendererProcess, original_file));

  // Invoke the API under test to grant access to |drop_data|.
  PrepareDropDataForChildProcess(&drop_data, p, kRendererProcess,
                                 test_file_system_context.get());

  // Verify that |drop_data| is mostly unchanged.
  EXPECT_EQ(0u, drop_data.filenames.size());
  EXPECT_EQ(1u, drop_data.file_system_files.size());
  EXPECT_EQ(123, drop_data.file_system_files[0].size);
  // It is okay if |drop_data.file_system_files[0].url| and
  // |drop_data.file_system_files[0].filesystem_id| change (to aid in enforcing
  // proper access patterns that are verified below).

  // Verify that the URL didn't change *too* much.
  const GURL crack_url = drop_data.file_system_files[0].url;
  storage::FileSystemURL dropped_file =
      test_file_system_context->CrackURLInFirstPartyContext(crack_url);
  EXPECT_TRUE(dropped_file.is_valid());
  EXPECT_EQ(original_file.origin(), dropped_file.origin());
  EXPECT_EQ(original_file.path().BaseName(), dropped_file.path().BaseName());

  // Verify that there is still no access to |kSensitiveOrigin|.
  EXPECT_FALSE(p->CanCommitURL(kRendererID, kSensitiveOrigin));

  // Verify that there is still no access to |original_file|.
  EXPECT_FALSE(p->CanReadFileSystemFile(kRendererProcess, original_file));
  EXPECT_FALSE(p->CanWriteFileSystemFile(kRendererProcess, original_file));
  EXPECT_FALSE(p->CanCreateFileSystemFile(kRendererProcess, original_file));
  EXPECT_FALSE(p->CanCopyIntoFileSystemFile(kRendererProcess, original_file));
  EXPECT_FALSE(p->CanDeleteFileSystemFile(kRendererProcess, original_file));

  // Verify that read access (and no other access) is granted for
  // |dropped_file|.
  EXPECT_TRUE(p->CanReadFileSystemFile(kRendererProcess, dropped_file));
  EXPECT_FALSE(p->CanWriteFileSystemFile(kRendererProcess, dropped_file));
  EXPECT_FALSE(p->CanCreateFileSystemFile(kRendererProcess, dropped_file));
  EXPECT_FALSE(p->CanCopyIntoFileSystemFile(kRendererProcess, dropped_file));
  EXPECT_FALSE(p->CanDeleteFileSystemFile(kRendererProcess, dropped_file));

  p->Remove(kRendererProcess);
}

TEST(BrowserFileSystemHelperTest, PrepareDropDataForChildProcess_LocalFiles) {
  // Install a custom ContentBrowserClient that overrides IsHandledURL() to
  // return `true` for file URLs, just like regular Chromium would.  This is
  // necessary for ChildProcessSecurityPolicy::CanRequestURL() checks to work
  // more accurately, since it allows URLs not destined for the browser itself
  // (i.e., when IsHandledURL() returns false).
  class LocalFileTestBrowserClient : public ContentBrowserClient {
   public:
    LocalFileTestBrowserClient() = default;

    bool IsHandledURL(const GURL& url) override { return url.SchemeIsFile(); }
  } test_browser_client;
  auto* old_browser_client = SetBrowserClientForTesting(&test_browser_client);

  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

  // We need the task environment to use a separate IO thread so that the
  // ChildProcessSecurityPolicy checks which perform different logic
  // based on whether they are called on the UI thread or the IO thread do the
  // right thing.
  BrowserTaskEnvironment task_environment{
      content::BrowserTaskEnvironment::REAL_IO_THREAD};
  TestBrowserContext browser_context;
  ChildProcessSecurityPolicyImpl* p =
      ChildProcessSecurityPolicyImpl::GetInstance();
  p->AddForTesting(kRendererProcess, &browser_context);

  // Prepare content::DropData containing some local files.
  const base::FilePath kDraggedFile =
      temp_dir.GetPath().AppendASCII("dragged_file.txt");
  const base::FilePath kOtherFile =
      temp_dir.GetPath().AppendASCII("other_file.txt");
  DropData drop_data;
  drop_data.filenames.push_back(ui::FileInfo(kDraggedFile, base::FilePath()));

  // Verify that initially no access is granted to both |kDraggedFile| and
  // |kOtherFile|.
  EXPECT_FALSE(p->CanReadFile(kRendererProcess, kDraggedFile));
  EXPECT_FALSE(p->CanReadFile(kRendererProcess, kOtherFile));
  EXPECT_FALSE(
      p->CanRequestURL(kRendererID, net::FilePathToFileURL(kDraggedFile)));
  EXPECT_FALSE(
      p->CanCommitURL(kRendererID, net::FilePathToFileURL(kDraggedFile)));
  EXPECT_FALSE(p->CanCreateReadWriteFile(kRendererID, kDraggedFile));
  EXPECT_FALSE(p->CanCreateReadWriteFile(kRendererID, kOtherFile));
  EXPECT_FALSE(
      p->CanRequestURL(kRendererID, net::FilePathToFileURL(kOtherFile)));
  EXPECT_FALSE(
      p->CanCommitURL(kRendererID, net::FilePathToFileURL(kOtherFile)));

  // Invoke the API under test to grant access to |drop_data|.
  PrepareDropDataForChildProcess(&drop_data, p, kRendererProcess, nullptr);

  // Verify that |drop_data| is unchanged.
  EXPECT_EQ(0u, drop_data.file_system_files.size());
  EXPECT_EQ(1u, drop_data.filenames.size());
  EXPECT_EQ(kDraggedFile, drop_data.filenames[0].path);

  // Verify that read access (and no other access) is granted for
  // |kDraggedFile|.  The renderer should be allowed to request this file, but
  // not commit it.
  EXPECT_TRUE(p->CanReadFile(kRendererProcess, kDraggedFile));
  EXPECT_FALSE(p->CanCreateReadWriteFile(kRendererID, kDraggedFile));
  EXPECT_TRUE(
      p->CanRequestURL(kRendererID, net::FilePathToFileURL(kDraggedFile)));
  EXPECT_FALSE(
      p->CanCommitURL(kRendererID, net::FilePathToFileURL(kDraggedFile)));

  // Verify that there is still no access for |kOtherFile|.
  EXPECT_FALSE(p->CanReadFile(kRendererProcess, kOtherFile));
  EXPECT_FALSE(p->CanCreateReadWriteFile(kRendererID, kOtherFile));
  EXPECT_FALSE(
      p->CanRequestURL(kRendererID, net::FilePathToFileURL(kOtherFile)));
  EXPECT_FALSE(
      p->CanCommitURL(kRendererID, net::FilePathToFileURL(kOtherFile)));

  p->Remove(kRendererProcess);
  SetBrowserClientForTesting(old_browser_client);
}

#if BUILDFLAG(IS_CHROMEOS)
TEST(BrowserFileSystemHelperTest, PrepareDropDataForChildProcess_ExternalFile) {
  // Install a custom ContentBrowserClient that overrides IsHandledURL() to
  // return `true` for externalfile URLs. This is necessary for
  // ChildProcessSecurityPolicy::CanRequestURL() checks to work more accurately.
  class ExternalFileTestBrowserClient : public ContentBrowserClient {
   public:
    ExternalFileTestBrowserClient() = default;

    bool IsHandledURL(const GURL& url) override {
      return url.SchemeIs(content::kExternalFileScheme);
    }
  } test_browser_client;
  auto* old_browser_client = SetBrowserClientForTesting(&test_browser_client);

  // We need the task environment to use a separate IO thread so that the
  // ChildProcessSecurityPolicy checks do the right thing.
  BrowserTaskEnvironment task_environment{
      content::BrowserTaskEnvironment::REAL_IO_THREAD};
  TestBrowserContext browser_context;
  ChildProcessSecurityPolicyImpl* p =
      ChildProcessSecurityPolicyImpl::GetInstance();

  p->AddForTesting(kRendererProcess, &browser_context);

  // Prepare content::DropData containing an externalfile: URL.
  const GURL kExternalFileUrl(
      std::string(content::kExternalFileScheme) +
      ":arc-content/testuser%40gmail.com-hash/root/dir/testfile.jpg");

  DropData drop_data;
  drop_data.url_infos.push_back({kExternalFileUrl, std::u16string()});

  // Verify that initially no access is granted using the integer constant.
  EXPECT_FALSE(p->CanRequestURL(kRendererID, kExternalFileUrl));
  EXPECT_FALSE(p->CanCommitURL(kRendererID, kExternalFileUrl));

  // Invoke the API under test using the strongly-typed object constant.
  PrepareDropDataForChildProcess(&drop_data, p, kRendererProcess, nullptr);

  // Verify that the new fix correctly grants ONLY Request access, not Commit
  // access.
  EXPECT_TRUE(p->CanRequestURL(kRendererID, kExternalFileUrl));
  EXPECT_FALSE(p->CanCommitURL(kRendererID, kExternalFileUrl));

  p->Remove(kRendererProcess);
  SetBrowserClientForTesting(old_browser_client);
}
#endif

}  // namespace browser_file_system_helper_unittest
}  // namespace content
