// Copyright 2020 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/web_contents/file_chooser_impl.h"

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/test/test_future.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/common/content_paths.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h"
#include "url/gurl.h"
#include "url/url_constants.h"

namespace content {

using FileChooserImplBrowserTest = ContentBrowserTest;

IN_PROC_BROWSER_TEST_F(FileChooserImplBrowserTest, FileChooserAfterRfhDeath) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
  auto* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  mojo::Remote<blink::mojom::FileChooser> chooser =
      FileChooserImpl::CreateBoundForTesting(rfh);

  // Kill the renderer process.
  RenderProcessHostWatcher crash_observer(
      rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  rfh->GetProcess()->Shutdown(0);
  crash_observer.Wait();

  auto quit_run_loop = [](base::RunLoop* run_loop,
                          blink::mojom::FileChooserResultPtr result) {
    run_loop->Quit();
  };

  // Call FileChooser methods.  The browser process should not crash.
  base::RunLoop run_loop1;
  chooser->OpenFileChooser(blink::mojom::FileChooserParams::New(),
                           base::BindOnce(quit_run_loop, &run_loop1));
  run_loop1.Run();

  base::RunLoop run_loop2;
  chooser->EnumerateChosenDirectory(base::FilePath(),
                                    base::BindOnce(quit_run_loop, &run_loop2));
  run_loop2.Run();

  // Pass if this didn't crash.
}

class FileChooserImplBrowserTestWebContentsDelegate
    : public WebContentsDelegate {
 public:
  void RunFileChooser(RenderFrameHost* render_frame_host,
                      scoped_refptr<FileSelectListener> listener,
                      const blink::mojom::FileChooserParams& params) override {
    listener_ = listener;
  }

  FileSelectListener* listener() { return listener_.get(); }

  void ClearListener() { listener_ = nullptr; }

 private:
  scoped_refptr<FileSelectListener> listener_;
};

// This test makes sure that if we kill the renderer after opening the file
// picker, then the cancelation callback is still called in order to prevent
// the parent renderer from thinking that the file picker is still open in the
// case of an iframe.
IN_PROC_BROWSER_TEST_F(FileChooserImplBrowserTest,
                       FileChooserCallbackAfterRfhDeathCancel) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  auto delegate =
      std::make_unique<FileChooserImplBrowserTestWebContentsDelegate>();

  shell()->web_contents()->SetDelegate(delegate.get());

  auto* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  auto chooser_and_remote = FileChooserImpl::CreateForTesting(rfh);
  auto* chooser = chooser_and_remote.first;

  auto quit_run_loop = [](base::RunLoop* run_loop,
                          blink::mojom::FileChooserResultPtr result) {
    run_loop->Quit();
  };

  base::RunLoop run_loop;
  chooser->OpenFileChooser(blink::mojom::FileChooserParams::New(),
                           base::BindOnce(quit_run_loop, &run_loop));

  // Kill the renderer process
  RenderProcessHostWatcher crash_observer(
      rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  rfh->GetProcess()->Shutdown(0);
  crash_observer.Wait();

  static_cast<FileChooserImpl::FileSelectListenerImpl*>(delegate->listener())
      ->SetListenerFunctionCalledTrueForTesting();
  chooser->FileSelectionCanceled();

  // Test passes if this run_loop.Run() returns instead of timing out.
  run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(FileChooserImplBrowserTest, ListenerOutlivingChooser) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  auto delegate =
      std::make_unique<FileChooserImplBrowserTestWebContentsDelegate>();
  shell()->web_contents()->SetDelegate(delegate.get());

  auto* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  auto chooser_and_remote = FileChooserImpl::CreateForTesting(rfh);
  auto* chooser = chooser_and_remote.first;

  chooser->OpenFileChooser(blink::mojom::FileChooserParams::New(),
                           base::DoNothing());

  static_cast<FileChooserImpl::FileSelectListenerImpl*>(delegate->listener())
      ->SetListenerFunctionCalledTrueForTesting();
  // After this, `chooser` will no longer have a reference to the listener.
  chooser->FileSelectionCanceled();

  // Destroy `chooser`.
  chooser_and_remote.second.reset();
  // The destruction happens asynchronously after the disconnection.
  base::RunLoop await_chooser_destruction_run_loop;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, await_chooser_destruction_run_loop.QuitClosure());
  await_chooser_destruction_run_loop.Run();

  // Clear the last reference to the listener, which will destroy it. It
  // should gracefully handle being destroyed after the chooser.
  delegate->ClearListener();
}

// Same as FileChooserCallbackAfterRfhDeathCancel but with a file selected from
// the file picker.
IN_PROC_BROWSER_TEST_F(FileChooserImplBrowserTest,
                       FileChooserCallbackAfterRfhDeathSelected) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  auto delegate =
      std::make_unique<FileChooserImplBrowserTestWebContentsDelegate>();

  shell()->web_contents()->SetDelegate(delegate.get());

  auto* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  auto chooser_and_remote = FileChooserImpl::CreateForTesting(rfh);
  auto* chooser = chooser_and_remote.first;

  auto quit_run_loop = [](base::RunLoop* run_loop,
                          blink::mojom::FileChooserResultPtr result) {
    run_loop->Quit();
  };

  base::RunLoop run_loop;
  chooser->OpenFileChooser(blink::mojom::FileChooserParams::New(),
                           base::BindOnce(quit_run_loop, &run_loop));

  // Kill the renderer process
  RenderProcessHostWatcher crash_observer(
      rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  rfh->GetProcess()->Shutdown(0);
  crash_observer.Wait();

  static_cast<FileChooserImpl::FileSelectListenerImpl*>(delegate->listener())
      ->SetListenerFunctionCalledTrueForTesting();
  std::vector<blink::mojom::FileChooserFileInfoPtr> files;
  files.emplace_back(blink::mojom::FileChooserFileInfoPtr(nullptr));
  chooser->FileSelected(base::FilePath(),
                        blink::mojom::FileChooserParams::Mode::kOpen,
                        std::move(files));

  // Test passes if this run_loop.Run() returns instead of timing out.
  run_loop.Run();
}

// https://crbug.com/1345275
IN_PROC_BROWSER_TEST_F(FileChooserImplBrowserTest, UploadFolderWithSymlink) {
  EXPECT_TRUE(NavigateToURL(
      shell(), GetTestUrl(".", "file_input_webkitdirectory.html")));

  // The folder contains a regular file and a symbolic link.
  // When uploading the folder, the symbolic link should be excluded.
  base::FilePath dir_test_data;
  ASSERT_TRUE(base::PathService::Get(DIR_TEST_DATA, &dir_test_data));
  base::FilePath folder_to_upload =
      dir_test_data.AppendASCII("file_chooser").AppendASCII("dir_with_symlink");

  base::FilePath text_file = folder_to_upload.AppendASCII("text_file.txt");
  base::FilePath symlink_file = folder_to_upload.AppendASCII("symlink");

  // Skip the test if symbolic links are not supported.
  {
    base::ScopedAllowBlockingForTesting allow_blocking;
    if (!base::IsLink(symlink_file))
      return;
  }

  std::unique_ptr<FileChooserDelegate> delegate(new FileChooserDelegate(
      {text_file, symlink_file}, folder_to_upload, base::OnceClosure()));
  shell()->web_contents()->SetDelegate(delegate.get());
  EXPECT_TRUE(ExecJs(shell(),
                     "(async () => {"
                     "  let listener = new Promise("
                     "      resolve => fileinput.onchange = resolve);"
                     "  fileinput.click();"
                     "  await listener;"
                     "})()"));

  EXPECT_EQ(
      1, EvalJs(shell(), "document.getElementById('fileinput').files.length;"));
  EXPECT_EQ(
      "text_file.txt",
      EvalJs(shell(), "document.getElementById('fileinput').files[0].name;"));
}

// https://crbug.com/1378997
IN_PROC_BROWSER_TEST_F(FileChooserImplBrowserTest, UploadFolderWithDirSymlink) {
  EXPECT_TRUE(NavigateToURL(
      shell(), GetTestUrl(".", "file_input_webkitdirectory.html")));

  // The folder contains a regular file and a directory symbolic link.
  // When uploading the folder, the symbolic link should not be followed.
  base::FilePath dir_test_data;
  ASSERT_TRUE(base::PathService::Get(DIR_TEST_DATA, &dir_test_data));
  base::FilePath folder_to_upload = dir_test_data.AppendASCII("file_chooser")
                                        .AppendASCII("dir_with_dir_symlink");

  base::FilePath foo_file = folder_to_upload.AppendASCII("foo.txt");
  base::FilePath dir_symlink = folder_to_upload.AppendASCII("symlink");
  base::FilePath bar_file = dir_symlink.AppendASCII("bar.txt");

  // Skip the test if symbolic links are not supported.
  {
    base::ScopedAllowBlockingForTesting allow_blocking;
    if (!base::IsLink(dir_symlink))
      return;
  }

  std::unique_ptr<FileChooserDelegate> delegate(new FileChooserDelegate(
      {foo_file, bar_file}, folder_to_upload, base::OnceClosure()));
  shell()->web_contents()->SetDelegate(delegate.get());
  EXPECT_TRUE(ExecJs(shell(),
                     "(async () => {"
                     "  let listener = new Promise("
                     "      resolve => fileinput.onchange = resolve);"
                     "  fileinput.click();"
                     "  await listener;"
                     "})()"));

  EXPECT_EQ(
      1, EvalJs(shell(), "document.getElementById('fileinput').files.length;"));
  EXPECT_EQ(
      "foo.txt",
      EvalJs(shell(), "document.getElementById('fileinput').files[0].name;"));
}

// Ensure that FileChooserImpl::FileSelected does not grant permissions if
// invoked with Mode::kSave, as a defense against compromised renderers.
// See https://crbug.com/435684924.
IN_PROC_BROWSER_TEST_F(FileChooserImplBrowserTest,
                       FileSelectedWithSaveModeDoesNotGrantPermission) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  auto* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  auto chooser_and_remote = FileChooserImpl::CreateForTesting(rfh);
  auto* chooser = chooser_and_remote.first;

  base::FilePath test_file;
  EXPECT_TRUE(base::PathService::Get(base::DIR_TEMP, &test_file));
  test_file = test_file.AppendASCII("sensitive_file.txt");

  // Ensure renderer doesn't have access initially.
  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  EXPECT_FALSE(policy->CanReadFile(rfh->GetProcess()->GetID(), test_file));

  std::vector<blink::mojom::FileChooserFileInfoPtr> files;
  files.emplace_back(blink::mojom::FileChooserFileInfo::NewNativeFile(
      blink::mojom::NativeFileInfo::New(test_file, std::u16string(),
                                        std::vector<std::u16string>())));

  // Call FileSelected with kSave.
  chooser->FileSelected(base::FilePath(),
                        blink::mojom::FileChooserParams::Mode::kSave,
                        std::move(files));

  // Verify renderer STILL doesn't have access.
  EXPECT_FALSE(policy->CanReadFile(rfh->GetProcess()->GetID(), test_file));
}

#if BUILDFLAG(IS_ANDROID)
class DisallowSystemUiPopupsContentBrowserClient
    : public ContentBrowserTestContentBrowserClient {
 public:
  bool ShouldAllowSystemUiPopups(WebContents* web_contents) override {
    return false;
  }
};

IN_PROC_BROWSER_TEST_F(FileChooserImplBrowserTest, DisallowSystemUiPopups) {
  DisallowSystemUiPopupsContentBrowserClient test_client;
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  auto* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  auto chooser_and_remote = FileChooserImpl::CreateForTesting(rfh);
  auto* chooser = chooser_and_remote.first;

  base::test::TestFuture<blink::mojom::FileChooserResultPtr> future;
  chooser->OpenFileChooser(blink::mojom::FileChooserParams::New(),
                           future.GetCallback());
  EXPECT_TRUE(future.Get().is_null());
}
#endif

}  // namespace content
