// Copyright (c) 2012 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 <windows.h>
#include <atlsecurity.h>
#include <shellapi.h>
#include <string>
#include <vector>

#include "base/basictypes.h"
#include "base/file_util.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_handle.h"
#include "base/message_loop/message_loop.h"
#include "base/path_service.h"
#include "base/strings/utf_string_conversions.h"
#include "net/base/net_util.h"

#include "chrome/browser/automation/url_request_automation_job.h"
#include "chrome/common/chrome_version_info.h"
#include "chrome_frame/chrome_frame_automation.h"
#include "chrome_frame/chrome_frame_delegate.h"
#include "chrome_frame/html_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "webkit/common/user_agent/user_agent_util.h"

const char kChromeFrameUserAgent[] = "chromeframe";

class HtmlUtilUnittest : public testing::Test {
 protected:
  // Constructor
  HtmlUtilUnittest() {}

  // Returns the test path given a test case.
  virtual bool GetTestPath(const std::string& test_case, base::FilePath* path) {
    if (!path) {
      NOTREACHED();
      return false;
    }

    base::FilePath test_path;
    if (!PathService::Get(base::DIR_SOURCE_ROOT, &test_path)) {
      NOTREACHED();
      return false;
    }

    test_path = test_path.AppendASCII("chrome_frame");
    test_path = test_path.AppendASCII("test");
    test_path = test_path.AppendASCII("html_util_test_data");
    test_path = test_path.AppendASCII(test_case);

    *path = test_path;
    return true;
  }

  virtual bool GetTestData(const std::string& test_case, std::wstring* data) {
    if (!data) {
      NOTREACHED();
      return false;
    }

    base::FilePath path;
    if (!GetTestPath(test_case, &path)) {
      NOTREACHED();
      return false;
    }

    std::string raw_data;
    base::ReadFileToString(path, &raw_data);

    // Convert to wide using the "best effort" assurance described in
    // string_util.h
    data->assign(UTF8ToWide(raw_data));
    return true;
  }
};

TEST_F(HtmlUtilUnittest, BasicTest) {
  std::wstring test_data;
  GetTestData("basic_test.html", &test_data);

  HTMLScanner scanner(test_data.c_str());

  // Grab the meta tag from the document and ensure that we get exactly one.
  HTMLScanner::StringRangeList tag_list;
  scanner.GetTagsByName(L"meta", &tag_list, L"body");
  ASSERT_EQ(1, tag_list.size());

  // Pull out the http-equiv attribute and check its value:
  HTMLScanner::StringRange attribute_value;
  EXPECT_TRUE(tag_list[0].GetTagAttribute(L"http-equiv", &attribute_value));
  EXPECT_TRUE(attribute_value.Equals(L"X-UA-Compatible"));

  // Pull out the content attribute and check its value:
  EXPECT_TRUE(tag_list[0].GetTagAttribute(L"content", &attribute_value));
  EXPECT_TRUE(attribute_value.Equals(L"chrome=1"));
}

TEST_F(HtmlUtilUnittest, QuotesTest) {
  std::wstring test_data;
  GetTestData("quotes_test.html", &test_data);

  HTMLScanner scanner(test_data.c_str());

  // Grab the meta tag from the document and ensure that we get exactly one.
  HTMLScanner::StringRangeList tag_list;
  scanner.GetTagsByName(L"meta", &tag_list, L"body");
  ASSERT_EQ(1, tag_list.size());

  // Pull out the http-equiv attribute and check its value:
  HTMLScanner::StringRange attribute_value;
  EXPECT_TRUE(tag_list[0].GetTagAttribute(L"http-equiv", &attribute_value));
  EXPECT_TRUE(attribute_value.Equals(L"X-UA-Compatible"));

  // Pull out the content attribute and check its value:
  EXPECT_TRUE(tag_list[0].GetTagAttribute(L"content", &attribute_value));
  EXPECT_TRUE(attribute_value.Equals(L"chrome=1"));
}

TEST_F(HtmlUtilUnittest, DegenerateCasesTest) {
  std::wstring test_data;
  GetTestData("degenerate_cases_test.html", &test_data);

  HTMLScanner scanner(test_data.c_str());

  // Scan for meta tags in the document. We expect not to pick up the one
  // that appears to be there since it is technically inside a quote block.
  HTMLScanner::StringRangeList tag_list;
  scanner.GetTagsByName(L"meta", &tag_list, L"body");
  EXPECT_TRUE(tag_list.empty());
}

TEST_F(HtmlUtilUnittest, MultipleTagsTest) {
  std::wstring test_data;
  GetTestData("multiple_tags.html", &test_data);

  HTMLScanner scanner(test_data.c_str());

  // Grab the meta tag from the document and ensure that we get exactly three.
  HTMLScanner::StringRangeList tag_list;
  scanner.GetTagsByName(L"meta", &tag_list, L"body");
  EXPECT_EQ(7, tag_list.size());

  // Pull out the content attribute for each tag and check its value:
  HTMLScanner::StringRange attribute_value;
  HTMLScanner::StringRangeList::const_iterator tag_list_iter(
      tag_list.begin());
  int valid_tag_count = 0;
  for (; tag_list_iter != tag_list.end(); tag_list_iter++) {
    HTMLScanner::StringRange attribute_value;
    if (tag_list_iter->GetTagAttribute(L"http-equiv", &attribute_value) &&
        attribute_value.Equals(L"X-UA-Compatible")) {
      EXPECT_TRUE(tag_list_iter->GetTagAttribute(L"content", &attribute_value));
      EXPECT_TRUE(attribute_value.Equals(L"chrome=1"));
      valid_tag_count++;
    }
  }
  EXPECT_EQ(3, valid_tag_count);
}

TEST_F(HtmlUtilUnittest, ShortDegenerateTest1) {
  std::wstring test_data(
      L"<foo><META http-equiv=X-UA-Compatible content='chrome=1'");

  HTMLScanner scanner(test_data.c_str());

  // Scan for meta tags in the document. We expect not to pick up the one
  // that is there since it is not properly closed.
  HTMLScanner::StringRangeList tag_list;
  scanner.GetTagsByName(L"meta", &tag_list, L"body");
  EXPECT_TRUE(tag_list.empty());
}

TEST_F(HtmlUtilUnittest, ShortDegenerateTest2) {
  std::wstring test_data(
    L"<foo <META http-equiv=X-UA-Compatible content='chrome=1'/>");

  HTMLScanner scanner(test_data.c_str());

  // Scan for meta tags in the document. We expect not to pick up the one
  // that appears to be there since it is inside a non-closed tag.
  HTMLScanner::StringRangeList tag_list;
  scanner.GetTagsByName(L"meta", &tag_list, L"body");
  EXPECT_TRUE(tag_list.empty());
}

TEST_F(HtmlUtilUnittest, QuoteInsideHTMLCommentTest) {
  std::wstring test_data(
    L"<!-- comment' --><META http-equiv=X-UA-Compatible content='chrome=1'/>");

  HTMLScanner scanner(test_data.c_str());

  // Grab the meta tag from the document and ensure that we get exactly one.
  HTMLScanner::StringRangeList tag_list;
  scanner.GetTagsByName(L"meta", &tag_list, L"body");
  ASSERT_EQ(1, tag_list.size());

  // Pull out the http-equiv attribute and check its value:
  HTMLScanner::StringRange attribute_value;
  EXPECT_TRUE(tag_list[0].GetTagAttribute(L"http-equiv", &attribute_value));
  EXPECT_TRUE(attribute_value.Equals(L"X-UA-Compatible"));

  // Pull out the content attribute and check its value:
  EXPECT_TRUE(tag_list[0].GetTagAttribute(L"content", &attribute_value));
  EXPECT_TRUE(attribute_value.Equals(L"chrome=1"));
}

TEST_F(HtmlUtilUnittest, CloseTagInsideHTMLCommentTest) {
  std::wstring test_data(
    L"<!-- comment> <META http-equiv=X-UA-Compatible content='chrome=1'/>-->");

  HTMLScanner scanner(test_data.c_str());

  // Ensure that the the meta tag is NOT detected.
  HTMLScanner::StringRangeList tag_list;
  scanner.GetTagsByName(L"meta", &tag_list, L"body");
  ASSERT_TRUE(tag_list.empty());
}

TEST_F(HtmlUtilUnittest, IEConditionalCommentTest) {
  std::wstring test_data(
      L"<!--[if lte IE 8]><META http-equiv=X-UA-Compatible content='chrome=1'/>"
      L"<![endif]-->");

  HTMLScanner scanner(test_data.c_str());

  // Ensure that the the meta tag IS detected.
  HTMLScanner::StringRangeList tag_list;
  scanner.GetTagsByName(L"meta", &tag_list, L"body");
  ASSERT_EQ(1, tag_list.size());
}

TEST_F(HtmlUtilUnittest, IEConditionalCommentWithNestedCommentTest) {
  std::wstring test_data(
      L"<!--[if IE]><!--<META http-equiv=X-UA-Compatible content='chrome=1'/>"
      L"--><![endif]-->");

  HTMLScanner scanner(test_data.c_str());

  // Ensure that the the meta tag IS NOT detected.
  HTMLScanner::StringRangeList tag_list;
  scanner.GetTagsByName(L"meta", &tag_list, L"body");
  ASSERT_TRUE(tag_list.empty());
}

TEST_F(HtmlUtilUnittest, IEConditionalCommentWithMultipleNestedTagsTest) {
  std::wstring test_data(
      L"<!--[if lte IE 8]>        <META http-equiv=X-UA-Compatible "
      L"content='chrome=1'/><foo bar></foo><foo baz/><![endif]-->"
      L"<boo hoo><boo hah>");

  HTMLScanner scanner(test_data.c_str());

  // Ensure that the the meta tag IS detected.
  HTMLScanner::StringRangeList meta_tag_list;
  scanner.GetTagsByName(L"meta", &meta_tag_list, L"body");
  ASSERT_EQ(1, meta_tag_list.size());

  // Ensure that the foo tags are also detected.
  HTMLScanner::StringRangeList foo_tag_list;
  scanner.GetTagsByName(L"foo", &foo_tag_list, L"body");
  ASSERT_EQ(2, foo_tag_list.size());

  // Ensure that the boo tags are also detected.
  HTMLScanner::StringRangeList boo_tag_list;
  scanner.GetTagsByName(L"boo", &boo_tag_list, L"body");
  ASSERT_EQ(2, boo_tag_list.size());
}

TEST_F(HtmlUtilUnittest, IEConditionalCommentWithAlternateEndingTest) {
  std::wstring test_data(
      L"<!--[if lte IE 8]>        <META http-equiv=X-UA-Compatible "
      L"content='chrome=1'/><foo bar></foo><foo baz/><![endif]>"
      L"<boo hoo><!--><boo hah>");

  HTMLScanner scanner(test_data.c_str());

  // Ensure that the the meta tag IS detected.
  HTMLScanner::StringRangeList meta_tag_list;
  scanner.GetTagsByName(L"meta", &meta_tag_list, L"body");
  ASSERT_EQ(1, meta_tag_list.size());

  // Ensure that the foo tags are also detected.
  HTMLScanner::StringRangeList foo_tag_list;
  scanner.GetTagsByName(L"foo", &foo_tag_list, L"body");
  ASSERT_EQ(2, foo_tag_list.size());

  // Ensure that the boo tags are also detected.
  HTMLScanner::StringRangeList boo_tag_list;
  scanner.GetTagsByName(L"boo", &boo_tag_list, L"body");
  ASSERT_EQ(2, boo_tag_list.size());
}

TEST_F(HtmlUtilUnittest, IEConditionalCommentNonTerminatedTest) {
  // This test shouldn't detect any tags up until the end of the conditional
  // comment tag.
  std::wstring test_data(
      L"<!--[if lte IE 8>        <META http-equiv=X-UA-Compatible "
      L"content='chrome=1'/><foo bar></foo><foo baz/><![endif]>"
      L"<boo hoo><!--><boo hah>");

  HTMLScanner scanner(test_data.c_str());

  // Ensure that the the meta tag IS NOT detected.
  HTMLScanner::StringRangeList meta_tag_list;
  scanner.GetTagsByName(L"meta", &meta_tag_list, L"body");
  ASSERT_TRUE(meta_tag_list.empty());

  // Ensure that the foo tags are NOT detected.
  HTMLScanner::StringRangeList foo_tag_list;
  scanner.GetTagsByName(L"foo", &foo_tag_list, L"body");
  ASSERT_TRUE(foo_tag_list.empty());

  // Ensure that the boo tags are detected.
  HTMLScanner::StringRangeList boo_tag_list;
  scanner.GetTagsByName(L"boo", &boo_tag_list, L"body");
  ASSERT_EQ(2, boo_tag_list.size());
}

struct UserAgentTestCase {
  std::string input_;
  std::string expected_;
} user_agent_test_cases[] = {
  {
    "", ""
  }, {
    "Mozilla/4.7 [en] (WinNT; U)",
    "Mozilla/4.7 [en] (WinNT; U; chromeframe/0.0.0.0)"
  }, {
    "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT)",
    "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; chromeframe/0.0.0.0)"
  }, {
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; T312461; "
        ".NET CLR 1.1.4322)",
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; T312461; "
        ".NET CLR 1.1.4322; chromeframe/0.0.0.0)"
  }, {
    "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 4.0) Opera 5.11 [en]",
    "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 4.0; chromeframe/0.0.0.0) "
        "Opera 5.11 [en]"
  }, {
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; "
        "chromeframe/0.0.0.0)"
  }, {
    "Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.0.2) "
        "Gecko/20030208 Netscape/7.02",
    "Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.0.2; "
        "chromeframe/0.0.0.0) Gecko/20030208 Netscape/7.02"
  }, {
    "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.6) Gecko/20040612 "
        "Firefox/0.8",
    "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.6; chromeframe/0.0.0.0) "
        "Gecko/20040612 Firefox/0.8"
  }, {
    "Mozilla/5.0 (compatible; Konqueror/3.2; Linux) (KHTML, like Gecko)",
    "Mozilla/5.0 (compatible; Konqueror/3.2; Linux; chromeframe/0.0.0.0) "
        "(KHTML, like Gecko)"
  }, {
    "Lynx/2.8.4rel.1 libwww-FM/2.14 SSL-MM/1.4.1 OpenSSL/0.9.6h",
    "Lynx/2.8.4rel.1 libwww-FM/2.14 SSL-MM/1.4.1 "
        "OpenSSL/0.9.6h chromeframe/0.0.0.0",
  }, {
    "Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.7.10) "
        "Gecko/20050716 Firefox/1.0.6",
    "Mozilla/5.0 (X11; U; Linux i686 (x86_64; chromeframe/0.0.0.0); en-US; "
        "rv:1.7.10) Gecko/20050716 Firefox/1.0.6"
  }, {
    "Invalid/1.1 ((((((",
    "Invalid/1.1 (((((( chromeframe/0.0.0.0",
  }, {
    "Invalid/1.1 ()))))",
    "Invalid/1.1 ( chromeframe/0.0.0.0)))))",
  }, {
    "Strange/1.1 ()",
    "Strange/1.1 ( chromeframe/0.0.0.0)",
  }
};

TEST_F(HtmlUtilUnittest, AddChromeFrameToUserAgentValue) {
  for (int i = 0; i < arraysize(user_agent_test_cases); ++i) {
    std::string new_ua(
        http_utils::AddChromeFrameToUserAgentValue(
            user_agent_test_cases[i].input_));
    EXPECT_EQ(user_agent_test_cases[i].expected_, new_ua);
  }

  // Now do the same test again, but test that we don't add the chromeframe
  // tag if we've already added it.
  for (int i = 0; i < arraysize(user_agent_test_cases); ++i) {
    std::string ua(user_agent_test_cases[i].expected_);
    std::string new_ua(http_utils::AddChromeFrameToUserAgentValue(ua));
    EXPECT_EQ(user_agent_test_cases[i].expected_, new_ua);
  }
}

TEST_F(HtmlUtilUnittest, RemoveChromeFrameFromUserAgentValue) {
  for (int i = 0; i < arraysize(user_agent_test_cases); ++i) {
    std::string new_ua(
        http_utils::RemoveChromeFrameFromUserAgentValue(
            user_agent_test_cases[i].expected_));
    EXPECT_EQ(user_agent_test_cases[i].input_, new_ua);
  }

  // Also test that we don't modify the UA if chromeframe is not present.
  for (int i = 0; i < arraysize(user_agent_test_cases); ++i) {
    std::string ua(user_agent_test_cases[i].input_);
    std::string new_ua(http_utils::RemoveChromeFrameFromUserAgentValue(ua));
    EXPECT_EQ(user_agent_test_cases[i].input_, new_ua);
  }
}

TEST_F(HtmlUtilUnittest, GetDefaultUserAgentHeaderWithCFTag) {
  std::string ua(http_utils::GetDefaultUserAgentHeaderWithCFTag());
  EXPECT_NE(0u, ua.length());
  EXPECT_NE(std::string::npos, ua.find("Mozilla"));
  EXPECT_NE(std::string::npos, ua.find(kChromeFrameUserAgent));
}

TEST_F(HtmlUtilUnittest, GetChromeUserAgent) {
  // This code is duplicated from chrome_content_client.cc to avoid
  // introducing a link-time dependency on chrome_common.
  chrome::VersionInfo version_info;
  std::string product("Chrome/");
  product += version_info.is_valid() ? version_info.Version() : "0.0.0.0";
  std::string chrome_ua(webkit_glue::BuildUserAgentFromProduct(product));

  const char* ua = http_utils::GetChromeUserAgent();
  EXPECT_EQ(ua, chrome_ua);
}

TEST_F(HtmlUtilUnittest, GetDefaultUserAgent) {
  std::string ua(http_utils::GetDefaultUserAgent());
  EXPECT_NE(0u, ua.length());
  EXPECT_NE(std::string::npos, ua.find("Mozilla"));
}

TEST_F(HtmlUtilUnittest, GetChromeFrameUserAgent) {
  const char* call1 = http_utils::GetChromeFrameUserAgent();
  const char* call2 = http_utils::GetChromeFrameUserAgent();
  // Expect static buffer since caller does no cleanup.
  EXPECT_EQ(call1, call2);
  std::string ua(call1);
  EXPECT_EQ("chromeframe/0.0.0.0", ua);
}

TEST(HttpUtils, HasFrameBustingHeader) {
  // Simple negative cases.
  EXPECT_FALSE(http_utils::HasFrameBustingHeader(""));
  EXPECT_FALSE(http_utils::HasFrameBustingHeader("Content-Type: text/plain"));
  EXPECT_FALSE(http_utils::HasFrameBustingHeader("X-Frame-Optionss: ALLOWALL"));
  // Explicit negative cases, test that we ignore case.
  EXPECT_FALSE(http_utils::HasFrameBustingHeader("X-Frame-Options: ALLOWALL"));
  EXPECT_FALSE(http_utils::HasFrameBustingHeader("X-Frame-Options: allowall"));
  EXPECT_FALSE(http_utils::HasFrameBustingHeader("X-Frame-Options: ALLowalL"));
  // Added space, ensure stripped out
  EXPECT_FALSE(http_utils::HasFrameBustingHeader(
    "X-Frame-Options: ALLOWALL "));
  // Added space with linefeed, ensure still stripped out
  EXPECT_FALSE(http_utils::HasFrameBustingHeader(
    "X-Frame-Options: ALLOWALL \r\n"));
  // Multiple identical headers, all of them allowing framing.
  EXPECT_FALSE(http_utils::HasFrameBustingHeader(
    "X-Frame-Options: ALLOWALL\r\n"
    "X-Frame-Options: ALLOWALL\r\n"
    "X-Frame-Options: ALLOWALL"));
  // Interleave with other headers.
  EXPECT_FALSE(http_utils::HasFrameBustingHeader(
    "Content-Type: text/plain\r\n"
    "X-Frame-Options: ALLOWALL\r\n"
    "Content-Length: 42"));

  // Simple positive cases.
  EXPECT_TRUE(http_utils::HasFrameBustingHeader("X-Frame-Options: deny"));
  EXPECT_TRUE(http_utils::HasFrameBustingHeader(
    "X-Frame-Options: SAMEorigin"));

  // Verify that we pick up case changes in the header name too:
  EXPECT_TRUE(http_utils::HasFrameBustingHeader("X-FRAME-OPTIONS: deny"));
  EXPECT_TRUE(http_utils::HasFrameBustingHeader("x-frame-options: deny"));
  EXPECT_TRUE(http_utils::HasFrameBustingHeader("X-frame-optionS: deny"));
  EXPECT_TRUE(http_utils::HasFrameBustingHeader("X-Frame-optionS: deny"));

  // Allowall entries do not override the denying entries, are
  // order-independent, and the deny entries can interleave with
  // other headers.
  EXPECT_TRUE(http_utils::HasFrameBustingHeader(
    "Content-Length: 42\r\n"
    "X-Frame-Options: ALLOWall\r\n"
    "X-Frame-Options: deny\r\n"));
  EXPECT_TRUE(http_utils::HasFrameBustingHeader(
    "X-Frame-Options: ALLOWall\r\n"
    "Content-Length: 42\r\n"
    "X-Frame-Options: SAMEORIGIN\r\n"));
  EXPECT_TRUE(http_utils::HasFrameBustingHeader(
    "X-Frame-Options: deny\r\n"
    "X-Frame-Options: ALLOWall\r\n"
    "Content-Length: 42\r\n"));
  EXPECT_TRUE(http_utils::HasFrameBustingHeader(
    "X-Frame-Options: SAMEORIGIN\r\n"
    "X-Frame-Options: ALLOWall\r\n"));
}