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

#include "base/memory/raw_ptr.h"

// This must be before Windows headers
#include "base/functional/callback_helpers.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"

#if BUILDFLAG(IS_WIN)
#include <objbase.h>

#include <windows.h>

#include <shlobj.h>
#include <wrl/client.h>
#endif

#include <string>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_restrictions.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.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/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "net/base/filename_util.h"
#include "net/base/net_errors.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/gtest_util.h"
#include "url/gurl.h"

namespace content {
namespace {

const char16_t kSuccessTitle[] = u"Title Of Awesomeness";
const char16_t kErrorTitle[] = u"Error";

base::FilePath TestFilePath() {
  base::ScopedAllowBlockingForTesting allow_blocking;
  return GetTestFilePath("", "title2.html");
}

base::FilePath AbsoluteFilePath(const base::FilePath& file_path) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  return base::MakeAbsoluteFilePath(file_path);
}

class TestFileAccessContentBrowserClient
    : public ContentBrowserTestContentBrowserClient {
 public:
  struct FileAccessAllowedArgs {
    base::FilePath path;
    base::FilePath absolute_path;
    base::FilePath profile_path;
  };

  TestFileAccessContentBrowserClient() = default;

  void set_blocked_path(const base::FilePath& blocked_path) {
    blocked_path_ = AbsoluteFilePath(blocked_path);
  }

  TestFileAccessContentBrowserClient(
      const TestFileAccessContentBrowserClient&) = delete;
  TestFileAccessContentBrowserClient& operator=(
      const TestFileAccessContentBrowserClient&) = delete;

  ~TestFileAccessContentBrowserClient() override = default;

  bool IsFileAccessAllowed(const base::FilePath& path,
                           const base::FilePath& absolute_path,
                           const base::FilePath& profile_path) override {
    access_allowed_args_.push_back(
        FileAccessAllowedArgs{path, absolute_path, profile_path});
    return blocked_path_ != absolute_path;
  }

  // Returns a vector of arguments passed to each invocation of
  // IsFileAccessAllowed().
  const std::vector<FileAccessAllowedArgs>& access_allowed_args() const {
    return access_allowed_args_;
  }

  void ClearAccessAllowedArgs() { access_allowed_args_.clear(); }

 private:
  base::FilePath blocked_path_;

  std::vector<FileAccessAllowedArgs> access_allowed_args_;
};

// This class contains integration tests for file URLs.
class FileURLLoaderFactoryBrowserTest : public ContentBrowserTest {
 public:
  FileURLLoaderFactoryBrowserTest() = default;

  // ContentBrowserTest implementation:
  void SetUpOnMainThread() override {
    base::FilePath test_data_path;
    EXPECT_TRUE(base::PathService::Get(DIR_TEST_DATA, &test_data_path));
    embedded_test_server()->ServeFilesFromDirectory(test_data_path);
    EXPECT_TRUE(embedded_test_server()->Start());
  }

  base::FilePath ProfilePath() const {
    return shell()
        ->web_contents()
        ->GetSiteInstance()
        ->GetBrowserContext()
        ->GetPath();
  }

  GURL RedirectToFileURL() const {
    return embedded_test_server()->GetURL(
        "/server-redirect?" + net::FilePathToFileURL(TestFilePath()).spec());
  }
};

IN_PROC_BROWSER_TEST_F(FileURLLoaderFactoryBrowserTest, Basic) {
  TestFileAccessContentBrowserClient test_browser_client;
  EXPECT_TRUE(NavigateToURL(shell(), net::FilePathToFileURL(TestFilePath())));
  EXPECT_EQ(kSuccessTitle, shell()->web_contents()->GetTitle());

  ASSERT_EQ(1u, test_browser_client.access_allowed_args().size());
  EXPECT_EQ(TestFilePath(), test_browser_client.access_allowed_args()[0].path);
  EXPECT_EQ(AbsoluteFilePath(TestFilePath()),
            test_browser_client.access_allowed_args()[0].absolute_path);
  EXPECT_EQ(ProfilePath(),
            test_browser_client.access_allowed_args()[0].profile_path);
}

IN_PROC_BROWSER_TEST_F(FileURLLoaderFactoryBrowserTest, FileAccessNotAllowed) {
  TestFileAccessContentBrowserClient test_browser_client;
  test_browser_client.set_blocked_path(TestFilePath());

  TestNavigationObserver navigation_observer(shell()->web_contents());
  EXPECT_FALSE(NavigateToURL(shell(), net::FilePathToFileURL(TestFilePath())));
  EXPECT_FALSE(navigation_observer.last_navigation_succeeded());
  EXPECT_THAT(navigation_observer.last_net_error_code(),
              net::test::IsError(net::ERR_ACCESS_DENIED));
  EXPECT_EQ(net::FilePathToFileURL(TestFilePath()),
            shell()->web_contents()->GetURL());
  EXPECT_EQ(kErrorTitle, shell()->web_contents()->GetTitle());

  ASSERT_EQ(1u, test_browser_client.access_allowed_args().size());
  EXPECT_EQ(TestFilePath(), test_browser_client.access_allowed_args()[0].path);
  EXPECT_EQ(AbsoluteFilePath(TestFilePath()),
            test_browser_client.access_allowed_args()[0].absolute_path);
  EXPECT_EQ(ProfilePath(),
            test_browser_client.access_allowed_args()[0].profile_path);
}

#if BUILDFLAG(IS_POSIX)

// Test symbolic links on POSIX platforms. These act like the contents of
// the symbolic link are the same as the contents of the file it links to.
IN_PROC_BROWSER_TEST_F(FileURLLoaderFactoryBrowserTest, SymlinksToFiles) {
  TestFileAccessContentBrowserClient test_browser_client;

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

  // Get an absolute path since |temp_dir| can contain a symbolic link.
  base::FilePath absolute_temp_dir = AbsoluteFilePath(temp_dir.GetPath());

  // MIME sniffing should use the symbolic link's destination path, so despite
  // not having a file extension of "html", this should be rendered as HTML.
  base::FilePath sym_link = absolute_temp_dir.AppendASCII("link.bin");
  ASSERT_TRUE(
      base::CreateSymbolicLink(AbsoluteFilePath(TestFilePath()), sym_link));

  EXPECT_TRUE(NavigateToURL(shell(), net::FilePathToFileURL(sym_link)));
  EXPECT_EQ(kSuccessTitle, shell()->web_contents()->GetTitle());

  ASSERT_EQ(1u, test_browser_client.access_allowed_args().size());
  EXPECT_EQ(sym_link, test_browser_client.access_allowed_args()[0].path);
  EXPECT_EQ(AbsoluteFilePath(sym_link),
            test_browser_client.access_allowed_args()[0].absolute_path);
  EXPECT_EQ(ProfilePath(),
            test_browser_client.access_allowed_args()[0].profile_path);

  // Test the case where access to the destination URL is blocked. Note that
  // this is the same as blocking the symbolic link URL - the
  // IsFileAccessAllowed() is passed both the symbolic link path and the
  // absolute path, so rejecting on looks just like rejecting the other.

  test_browser_client.ClearAccessAllowedArgs();
  test_browser_client.set_blocked_path(TestFilePath());

  TestNavigationObserver navigation_observer3(shell()->web_contents());
  EXPECT_FALSE(NavigateToURL(shell(), net::FilePathToFileURL(sym_link)));
  EXPECT_FALSE(navigation_observer3.last_navigation_succeeded());
  EXPECT_THAT(navigation_observer3.last_net_error_code(),
              net::test::IsError(net::ERR_ACCESS_DENIED));
  EXPECT_EQ(net::FilePathToFileURL(sym_link),
            shell()->web_contents()->GetURL());
  EXPECT_EQ(kErrorTitle, shell()->web_contents()->GetTitle());

  ASSERT_EQ(1u, test_browser_client.access_allowed_args().size());
  EXPECT_EQ(sym_link, test_browser_client.access_allowed_args()[0].path);
  EXPECT_EQ(AbsoluteFilePath(sym_link),
            test_browser_client.access_allowed_args()[0].absolute_path);
  EXPECT_EQ(ProfilePath(),
            test_browser_client.access_allowed_args()[0].profile_path);
}

#elif BUILDFLAG(IS_WIN)

// Test shortcuts on Windows. These are treated as redirects.
IN_PROC_BROWSER_TEST_F(FileURLLoaderFactoryBrowserTest, ResolveShortcutTest) {
  TestFileAccessContentBrowserClient test_browser_client;

  // Create an empty temp directory, to be sure there's no file in it.
  base::ScopedAllowBlockingForTesting allow_blocking;
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

  base::FilePath lnk_path =
      temp_dir.GetPath().Append(FILE_PATH_LITERAL("foo.lnk"));

  base::FilePath test = TestFilePath();

  // Create a shortcut for the test.
  {
    Microsoft::WRL::ComPtr<IShellLink> shell;
    ASSERT_TRUE(SUCCEEDED(::CoCreateInstance(
        CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shell))));
    Microsoft::WRL::ComPtr<IPersistFile> persist;
    ASSERT_TRUE(SUCCEEDED(shell.As<IPersistFile>(&persist)));
    EXPECT_TRUE(SUCCEEDED(shell->SetPath(TestFilePath().value().c_str())));
    EXPECT_TRUE(SUCCEEDED(shell->SetDescription(L"ResolveShortcutTest")));
    std::wstring lnk_string = lnk_path.value();
    EXPECT_TRUE(SUCCEEDED(persist->Save(lnk_string.c_str(), TRUE)));
  }

  EXPECT_TRUE(NavigateToURL(
      shell(), net::FilePathToFileURL(lnk_path),
      net::FilePathToFileURL(TestFilePath()) /* expect_commit_url */));
  EXPECT_EQ(kSuccessTitle, shell()->web_contents()->GetTitle());

  ASSERT_EQ(2u, test_browser_client.access_allowed_args().size());
  EXPECT_EQ(lnk_path, test_browser_client.access_allowed_args()[0].path);
  EXPECT_EQ(AbsoluteFilePath(lnk_path),
            test_browser_client.access_allowed_args()[0].absolute_path);
  EXPECT_EQ(ProfilePath(),
            test_browser_client.access_allowed_args()[0].profile_path);

  EXPECT_EQ(TestFilePath(), test_browser_client.access_allowed_args()[1].path);
  EXPECT_EQ(AbsoluteFilePath(TestFilePath()),
            test_browser_client.access_allowed_args()[1].absolute_path);
  EXPECT_EQ(ProfilePath(),
            test_browser_client.access_allowed_args()[1].profile_path);

  // Test the case where access to the shortcut URL is blocked. Should display
  // an error page at the shortcut's file URL.

  test_browser_client.ClearAccessAllowedArgs();
  test_browser_client.set_blocked_path(lnk_path);

  TestNavigationObserver navigation_observer2(shell()->web_contents());
  EXPECT_FALSE(NavigateToURL(shell(), net::FilePathToFileURL(lnk_path)));
  EXPECT_FALSE(navigation_observer2.last_navigation_succeeded());
  EXPECT_THAT(navigation_observer2.last_net_error_code(),
              net::test::IsError(net::ERR_ACCESS_DENIED));
  EXPECT_EQ(net::FilePathToFileURL(lnk_path),
            shell()->web_contents()->GetURL());
  EXPECT_EQ(kErrorTitle, shell()->web_contents()->GetTitle());

  ASSERT_EQ(1u, test_browser_client.access_allowed_args().size());
  EXPECT_EQ(lnk_path, test_browser_client.access_allowed_args()[0].path);
  EXPECT_EQ(AbsoluteFilePath(lnk_path),
            test_browser_client.access_allowed_args()[0].absolute_path);
  EXPECT_EQ(ProfilePath(),
            test_browser_client.access_allowed_args()[0].profile_path);

  // Test the case where access to the destination URL is blocked. The redirect
  // is followed, so this should end up at the shortcut destination, but
  // displaying an error.

  test_browser_client.ClearAccessAllowedArgs();
  test_browser_client.set_blocked_path(TestFilePath());

  TestNavigationObserver navigation_observer3(shell()->web_contents());
  EXPECT_FALSE(NavigateToURL(shell(), net::FilePathToFileURL(lnk_path)));
  EXPECT_FALSE(navigation_observer3.last_navigation_succeeded());
  EXPECT_THAT(navigation_observer3.last_net_error_code(),
              net::test::IsError(net::ERR_ACCESS_DENIED));
  EXPECT_EQ(net::FilePathToFileURL(TestFilePath()),
            shell()->web_contents()->GetURL());
  EXPECT_EQ(kErrorTitle, shell()->web_contents()->GetTitle());

  ASSERT_EQ(2u, test_browser_client.access_allowed_args().size());
  EXPECT_EQ(lnk_path, test_browser_client.access_allowed_args()[0].path);
  EXPECT_EQ(AbsoluteFilePath(lnk_path),
            test_browser_client.access_allowed_args()[0].absolute_path);
  EXPECT_EQ(ProfilePath(),
            test_browser_client.access_allowed_args()[0].profile_path);

  EXPECT_EQ(TestFilePath(), test_browser_client.access_allowed_args()[1].path);
  EXPECT_EQ(AbsoluteFilePath(TestFilePath()),
            test_browser_client.access_allowed_args()[1].absolute_path);
  EXPECT_EQ(ProfilePath(),
            test_browser_client.access_allowed_args()[1].profile_path);
}

#endif  // BUILDFLAG(IS_WIN)

IN_PROC_BROWSER_TEST_F(FileURLLoaderFactoryBrowserTest,
                       RedirectToFileUrlMainFrame) {
  TestFileAccessContentBrowserClient test_browser_client;

  TestNavigationObserver navigation_observer(shell()->web_contents());
  EXPECT_FALSE(NavigateToURL(shell(), RedirectToFileURL()));
  EXPECT_FALSE(navigation_observer.last_navigation_succeeded());
  EXPECT_THAT(navigation_observer.last_net_error_code(),
              net::test::IsError(net::ERR_UNSAFE_REDIRECT));
  // The redirect should not have been followed. This is important so that a
  // reload will show the same error.
  EXPECT_EQ(RedirectToFileURL(), shell()->web_contents()->GetURL());
  EXPECT_EQ(kErrorTitle, shell()->web_contents()->GetTitle());
  // There should never have been a request for the file URL.
  EXPECT_TRUE(test_browser_client.access_allowed_args().empty());

  // Reloading returns the same error.
  TestNavigationObserver navigation_observer2(shell()->web_contents());
  shell()->Reload();
  navigation_observer2.Wait();
  EXPECT_FALSE(navigation_observer2.last_navigation_succeeded());
  EXPECT_THAT(navigation_observer2.last_net_error_code(),
              net::test::IsError(net::ERR_UNSAFE_REDIRECT));
  EXPECT_EQ(RedirectToFileURL(), shell()->web_contents()->GetURL());
  EXPECT_EQ(kErrorTitle, shell()->web_contents()->GetTitle());
  // There should never have been a request for the file URL.
  EXPECT_TRUE(test_browser_client.access_allowed_args().empty());
}

IN_PROC_BROWSER_TEST_F(FileURLLoaderFactoryBrowserTest,
                       RedirectToFileUrlFetch) {
  TestFileAccessContentBrowserClient test_browser_client;

  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/title2.html")));
  std::string fetch_redirect_to_file = base::StringPrintf(
      "(async () => {"
      "  try {"
      "    var resp = (await fetch('%s'));"
      "    return 'ok';"
      "  } catch (error) {"
      "    return 'error';"
      "  }"
      "})();",
      RedirectToFileURL().spec().c_str());
  // Unfortunately, fetch doesn't provide a way to unambiguously know if the
  // request failed due to the redirect being unsafe.
  EXPECT_EQ("error", EvalJs(shell()->web_contents()->GetPrimaryMainFrame(),
                            fetch_redirect_to_file));
  // There should never have been a request for the file URL.
  EXPECT_TRUE(test_browser_client.access_allowed_args().empty());
}

IN_PROC_BROWSER_TEST_F(FileURLLoaderFactoryBrowserTest,
                       RedirectToFileUrlSubFrame) {
  TestFileAccessContentBrowserClient test_browser_client;

  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("/page_with_iframe.html")));
  LOG(WARNING) << embedded_test_server()->GetURL("/page_with_iframe.html");

  TestNavigationObserver navigation_observer(shell()->web_contents());
  EXPECT_TRUE(NavigateIframeToURL(shell()->web_contents(), "test_iframe",
                                  RedirectToFileURL()));
  navigation_observer.Wait();
  EXPECT_FALSE(navigation_observer.last_navigation_succeeded());
  EXPECT_THAT(navigation_observer.last_net_error_code(),
              net::test::IsError(net::ERR_UNSAFE_REDIRECT));
  // There should never have been a request for the file URL.
  EXPECT_TRUE(test_browser_client.access_allowed_args().empty());
}

// The test below opens a |popup| in a way that doesn't trigger a
// RenderFrameImpl::CommitNavigation.  This means that the popup will depend on
// inheriting URLLoaderFactoryBundle from the opener.
//
// Before triggering any subresource fetches in the popup, the test closes the
// opener window.  If the URLLoaderFactoryBundle hasn't been inherited at this
// point yet, then we might run into trouble later.  Another way how we might
// get into trouble is if FileURLLoaderFactory's clone doesn't survive closing
// of the opener.
//
// Finally, the test triggers a subresource fetch in the popup.  We expect that
// this fetch should use the right URLLoaderFactoryBundle - one that contains
// a functional FileURLLoaderFactory.
//
// This is a regression test for https://crbug.com/1106995
IN_PROC_BROWSER_TEST_F(FileURLLoaderFactoryBrowserTest,
                       LifetimeOfClonedFactory) {
  GURL opener_url(GetTestUrl("", "title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), opener_url));

  // Open a |popup| in a way that doesn't trigger RFI::CommitNavigation.
  WebContentsAddedObserver popup_observer;
  ASSERT_TRUE(ExecJs(shell(), "window.open('', '_blank')"));
  WebContents* popup = popup_observer.GetWebContents();

  // Close the opener and make sure that the |popup| actually sees that the
  // |opener| has closed.
  //
  // This test step means that we cannot rely on the |opener| being alive when
  // the first subresource fetch happens in the |popup| (in the next test step
  // below).
  EXPECT_EQ(true, EvalJs(popup, "!!window.opener"));
  shell()->Close();
  const char kScriptToWaitForNoOpener[] = R"(
      new Promise(function (resolve, reject) {
          function CheckAndWaitMoreIfNeeded() {
            if (!window.opener)
              resolve('OPENER GONE');

            setTimeout(CheckAndWaitMoreIfNeeded, 50);
          }

          CheckAndWaitMoreIfNeeded();
      });
  )";
  EXPECT_EQ("OPENER GONE", EvalJs(popup, kScriptToWaitForNoOpener));

  // Trigger a subresource fetch in the popup.  This is the main test step - it
  // verifies that at this point the popup has access to a functional
  // FileURLLoaderFactory.
  const char kScriptTemplateToTriggerSubresourceFetch[] = R"(
      new Promise(function (resolve, reject) {
          var img = document.createElement('img');
          img.src = $1;
          img.onload = _ => resolve('OK');
          img.onerror = e => resolve('ERR: ' + e);
      });
  )";
  GURL img_url = GetTestUrl("site_isolation", "png-corp.png");
  EXPECT_EQ("OK",
            EvalJs(popup, JsReplace(kScriptTemplateToTriggerSubresourceFetch,
                                    img_url)));
}

class FileURLLoaderFactoryDisabledSecurityBrowserTest
    : public FileURLLoaderFactoryBrowserTest {
 public:
  FileURLLoaderFactoryDisabledSecurityBrowserTest() = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(switches::kDisableWebSecurity);
    FileURLLoaderFactoryBrowserTest::SetUpCommandLine(command_line);
  }
};

// Test whether sandboxed file: frames still can access file: subresources.
IN_PROC_BROWSER_TEST_F(FileURLLoaderFactoryDisabledSecurityBrowserTest,
                       SubresourcesInSandboxedFileFrame) {
  GURL main_url(GetTestUrl("", "title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  // Append a sandboxed file: subframe.
  {
    GURL sandboxed_url(GetTestUrl("", "title2.html"));
    TestNavigationObserver nav_observer(shell()->web_contents());
    const char kScriptTemplateToAddSubframe[] = R"(
        f = document.createElement('iframe');
        f.sandbox = 'allow-scripts';
        f.src = $1;
        document.body.appendChild(f);
    )";
    ASSERT_TRUE(ExecJs(shell(),
                       JsReplace(kScriptTemplateToAddSubframe, sandboxed_url)));
    nav_observer.Wait();
  }

  // Trigger a subresource fetch in the subframe.  This is the main test step -
  // it verifies that at this point the subframe has access to a functional
  // FileURLLoaderFactory.
  //
  // Access to a file: (local-scheme) subresource is normally forbidden by
  // blink::SecurityOrigin::CanDisplay if the requesting origin is opaque.
  // Example error message:
  //     Not allowed to load local resource:
  //     file:///.../src/content/test/data/site_isolation/png-corp.png,
  //     source: file:///...src/content/test/data/title2.html
  //
  // OTOH, this restriction can be relaxed by 1) the --disable-web-security
  // switch (this is what this test suite is doing) or 2) the following Android
  // WebView API: android.webkit.WebSettings.setAllowFileAccess.  This is why
  // the test asserts below that the access is successful.
  RenderFrameHost* main_frame = shell()->web_contents()->GetPrimaryMainFrame();
  RenderFrameHost* child_frame = ChildFrameAt(main_frame, 0);
  const char kScriptTemplateToTriggerSubresourceFetch[] = R"(
      new Promise(function (resolve, reject) {
          var img = document.createElement('img');
          img.src = $1;
          img.onload = _ => resolve('OK');
          img.onerror = e => resolve('ERR: ' + e);
      });
  )";
  GURL img_url = GetTestUrl("site_isolation", "png-corp.png");
  EXPECT_EQ("OK", EvalJs(child_frame,
                         JsReplace(kScriptTemplateToTriggerSubresourceFetch,
                                   img_url)));
}

IN_PROC_BROWSER_TEST_F(FileURLLoaderFactoryBrowserTest, LastModified) {
  // Create a temporary file with an arbitrary last-modified timestamp.
  const char kLastModified[] = "1994-11-15T12:45:26.000Z";
  base::FilePath path;
  {
    base::ScopedAllowBlockingForTesting allow_blocking;
    ASSERT_TRUE(base::CreateTemporaryFile(&path));
    base::Time last_modified_time;
    ASSERT_TRUE(base::Time::FromString(kLastModified, &last_modified_time));
    ASSERT_TRUE(base::TouchFile(path, /*last_accessed=*/base::Time::Now(),
                                last_modified_time));
  }
  EXPECT_TRUE(NavigateToURL(shell(), net::FilePathToFileURL(path)));

  // Verify the syntax
  EXPECT_THAT(content::EvalJs(shell()->web_contents(), "document.lastModified")
                  .ExtractString(),
              testing::MatchesRegex(R"(\d\d/\d\d/\d\d\d\d \d\d:\d\d:\d\d)"));
  // Verify the value (it's in local time, so we parse it and convert it in JS
  // to get a representation in UTC).
  EXPECT_EQ(kLastModified,
            content::EvalJs(shell()->web_contents(),
                            "new Date(document.lastModified).toISOString()"));
}

}  // namespace
}  // namespace content