blob: 0f2ed7160725172d5e7775b7ed16cbd47b47c376 [file] [log] [blame]
// Copyright 2017 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.
#import "ios/web/navigation/wk_navigation_util.h"
#include <memory>
#include <vector>
#include "base/json/json_reader.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "ios/web/common/features.h"
#import "ios/web/navigation/navigation_item_impl.h"
#include "ios/web/test/test_url_constants.h"
#include "net/base/escape.h"
#import "net/base/mac/url_conversions.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/platform_test.h"
#include "url/scheme_host_port.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace web {
namespace wk_navigation_util {
namespace {
// Creates a vector with given number of navigation items. All items will
// have distinct titles and URLs.
void CreateTestNavigationItems(
size_t count,
std::vector<std::unique_ptr<NavigationItem>>& items) {
for (size_t i = 0; i < count; i++) {
auto item = std::make_unique<NavigationItemImpl>();
item->SetURL(GURL(base::StringPrintf("http://www.%zu.com", i)));
item->SetTitle(base::ASCIIToUTF16(base::StringPrintf("Test%zu", i)));
items.push_back(std::move(item));
}
}
// Extracts session dictionary from |restore_session_url|.
base::JSONReader::ValueWithError ExtractSessionDict(GURL restore_session_url) {
NSString* fragment = net::NSURLWithGURL(restore_session_url).fragment;
NSString* encoded_session =
[fragment substringFromIndex:strlen(kRestoreSessionSessionHashPrefix)];
std::string session_json =
net::UnescapeBinaryURLComponent(base::SysNSStringToUTF8(encoded_session));
return base::JSONReader::ReadAndReturnValueWithError(session_json,
base::JSON_PARSE_RFC);
}
} // namespace
typedef PlatformTest WKNavigationUtilTest;
// Tests various inputs for GetSafeItemRange.
TEST_F(WKNavigationUtilTest, GetSafeItemRange) {
// Session size does not exceed kMaxSessionSize and last_committed_item_index
// is in range.
for (int item_count = 0; item_count <= kMaxSessionSize; item_count++) {
for (int item_index = 0; item_index < item_count; item_index++) {
int offset = 0;
int size = 0;
EXPECT_EQ(item_index,
GetSafeItemRange(item_index, item_count, &offset, &size))
<< "item_count: " << item_count << " item_index: " << item_index;
EXPECT_EQ(0, offset) << "item_count: " << item_count
<< " item_index: " << item_index;
EXPECT_EQ(item_count, size)
<< "item_count: " << item_count << " item_index: " << item_index;
}
}
// Session size is 1 item longer than kMaxSessionSize.
int offset = 0;
int size = 0;
EXPECT_EQ(0, GetSafeItemRange(0, kMaxSessionSize + 1, &offset, &size));
EXPECT_EQ(0, offset);
EXPECT_EQ(kMaxSessionSize, size);
offset = 0;
size = 0;
EXPECT_EQ(
kMaxSessionSize - 1,
GetSafeItemRange(kMaxSessionSize, kMaxSessionSize + 1, &offset, &size));
EXPECT_EQ(1, offset);
EXPECT_EQ(kMaxSessionSize, size);
offset = 0;
size = 0;
EXPECT_EQ(kMaxSessionSize / 2,
GetSafeItemRange(kMaxSessionSize / 2, kMaxSessionSize + 1, &offset,
&size));
EXPECT_EQ(0, offset);
EXPECT_EQ(kMaxSessionSize, size);
offset = 0;
size = 0;
EXPECT_EQ(kMaxSessionSize / 2,
GetSafeItemRange(kMaxSessionSize / 2 + 1, kMaxSessionSize + 1,
&offset, &size));
EXPECT_EQ(1, offset);
EXPECT_EQ(kMaxSessionSize, size);
offset = 0;
size = 0;
EXPECT_EQ(kMaxSessionSize / 2 - 1,
GetSafeItemRange(kMaxSessionSize / 2 - 1, kMaxSessionSize + 1,
&offset, &size));
EXPECT_EQ(0, offset);
EXPECT_EQ(kMaxSessionSize, size);
}
TEST_F(WKNavigationUtilTest, CreateRestoreSessionUrl) {
auto item0 = std::make_unique<NavigationItemImpl>();
item0->SetURL(GURL("http://www.0.com"));
item0->SetTitle(u"Test Website 0");
auto item1 = std::make_unique<NavigationItemImpl>();
item1->SetURL(GURL("http://www.1.com"));
// Create an App-specific URL
auto item2 = std::make_unique<NavigationItemImpl>();
GURL url2("http://webui");
GURL::Replacements scheme_replacements;
scheme_replacements.SetSchemeStr(kTestWebUIScheme);
item2->SetURL(url2.ReplaceComponents(scheme_replacements));
std::vector<std::unique_ptr<NavigationItem>> items;
items.push_back(std::move(item0));
items.push_back(std::move(item1));
items.push_back(std::move(item2));
int first_index = 0;
GURL restore_session_url;
CreateRestoreSessionUrl(0 /* last_committed_item_index */, items,
&restore_session_url, &first_index);
ASSERT_EQ(0, first_index);
ASSERT_TRUE(IsRestoreSessionUrl(restore_session_url));
ASSERT_TRUE(IsRestoreSessionUrl(net::NSURLWithGURL(restore_session_url)));
std::string session_json =
net::UnescapeBinaryURLComponent(restore_session_url.ref());
EXPECT_EQ("session={\"offset\":-2,\"titles\":[\"Test Website 0\",\"\",\"\"],"
"\"urls\":[\"http://www.0.com/\",\"http://www.1.com/\","
"\"testwebui://webui/\"]}",
session_json);
}
// In the past the math within CreateRestoreSessionUrl has had some edge case
// crashes. Ensure that nothing crashes.
TEST_F(WKNavigationUtilTest, CreateRestoreSessionBruteForce) {
std::vector<std::unique_ptr<NavigationItem>> items;
int first_index = 0;
GURL restore_session_url;
for (int num_items = 70; num_items < 80; num_items++) {
std::vector<std::unique_ptr<NavigationItem>> items;
CreateTestNavigationItems(num_items, items);
for (int last_committed_index = 0; last_committed_index < num_items;
last_committed_index++) {
CreateRestoreSessionUrl(last_committed_index, items, &restore_session_url,
&first_index);
// Extract session JSON from restoration URL.
base::JSONReader::ValueWithError value_with_error =
ExtractSessionDict(restore_session_url);
base::Value* urls_value = value_with_error.value->FindKey("urls");
if (num_items > kMaxSessionSize) {
ASSERT_EQ(kMaxSessionSize,
static_cast<int>(urls_value->GetList().size()));
} else {
ASSERT_EQ(num_items, static_cast<int>(urls_value->GetList().size()));
}
}
}
}
// Verifies that large session can be stored in NSURL. GURL is converted to
// NSURL, because NSURL is passed to WKWebView during the session restoration.
TEST_F(WKNavigationUtilTest, CreateRestoreSessionUrlForLargeSession) {
// Create restore session URL with large number of items.
const size_t kItemCount = kMaxSessionSize;
std::vector<std::unique_ptr<NavigationItem>> items;
CreateTestNavigationItems(kItemCount, items);
int first_index = 0;
GURL restore_session_url;
CreateRestoreSessionUrl(
/*last_committed_item_index=*/0, items, &restore_session_url,
&first_index);
ASSERT_TRUE(IsRestoreSessionUrl(restore_session_url));
ASSERT_TRUE(IsRestoreSessionUrl(net::NSURLWithGURL(restore_session_url)));
// Extract session JSON from restoration URL.
base::JSONReader::ValueWithError value_with_error =
ExtractSessionDict(restore_session_url);
ASSERT_TRUE(value_with_error.value.has_value());
// Verify that all titles and URLs are present.
base::Value* titles_value = value_with_error.value->FindKey("titles");
ASSERT_TRUE(titles_value);
ASSERT_TRUE(titles_value->is_list());
ASSERT_EQ(kItemCount, titles_value->GetList().size());
base::Value* urls_value = value_with_error.value->FindKey("urls");
ASSERT_TRUE(urls_value);
ASSERT_TRUE(urls_value->is_list());
ASSERT_EQ(kItemCount, urls_value->GetList().size());
}
// Verifies that large session can be stored in NSURL and that extra items
// are trimmed from the right side of |last_committed_item_index|.
TEST_F(WKNavigationUtilTest, CreateRestoreSessionUrlForExtraLargeForwardList) {
// Create restore session URL with large number of items that exceeds
// kMaxSessionSize.
const size_t kItemCount = kMaxSessionSize * 3;
std::vector<std::unique_ptr<NavigationItem>> items;
CreateTestNavigationItems(kItemCount, items);
int first_index = 0;
GURL restore_session_url;
CreateRestoreSessionUrl(
/*last_committed_item_index=*/0, items, &restore_session_url,
&first_index);
ASSERT_EQ(0, first_index);
ASSERT_TRUE(IsRestoreSessionUrl(restore_session_url));
ASSERT_TRUE(IsRestoreSessionUrl(net::NSURLWithGURL(restore_session_url)));
// Extract session JSON from restoration URL.
base::JSONReader::ValueWithError value_with_error =
ExtractSessionDict(restore_session_url);
ASSERT_TRUE(value_with_error.value.has_value());
// Verify that first kMaxSessionSize titles and URLs are present.
base::Value* titles_value = value_with_error.value->FindKey("titles");
ASSERT_TRUE(titles_value);
ASSERT_TRUE(titles_value->is_list());
ASSERT_EQ(static_cast<size_t>(kMaxSessionSize),
titles_value->GetList().size());
ASSERT_EQ("Test0", titles_value->GetList()[0].GetString());
ASSERT_EQ("Test74", titles_value->GetList()[kMaxSessionSize - 1].GetString());
base::Value* urls_value = value_with_error.value->FindKey("urls");
ASSERT_TRUE(urls_value);
ASSERT_TRUE(urls_value->is_list());
ASSERT_EQ(static_cast<size_t>(kMaxSessionSize), urls_value->GetList().size());
ASSERT_EQ("http://www.0.com/", urls_value->GetList()[0].GetString());
ASSERT_EQ("http://www.74.com/",
urls_value->GetList()[kMaxSessionSize - 1].GetString());
// Verify the offset is correct.
ASSERT_EQ(1 - kMaxSessionSize,
value_with_error.value->FindKey("offset")->GetInt());
}
// Verifies that large session can be stored in NSURL and that extra items
// are trimmed from the left side of |last_committed_item_index|.
TEST_F(WKNavigationUtilTest, CreateRestoreSessionUrlForExtraLargeBackList) {
// Create restore session URL with large number of items that exceeds
// kMaxSessionSize.
const size_t kItemCount = kMaxSessionSize * 3;
std::vector<std::unique_ptr<NavigationItem>> items;
CreateTestNavigationItems(kItemCount, items);
int first_index = 0;
GURL restore_session_url;
CreateRestoreSessionUrl(
/*last_committed_item_index=*/kItemCount - 1, items, &restore_session_url,
&first_index);
ASSERT_EQ(150, first_index);
ASSERT_TRUE(IsRestoreSessionUrl(restore_session_url));
ASSERT_TRUE(IsRestoreSessionUrl(net::NSURLWithGURL(restore_session_url)));
// Extract session JSON from restoration URL.
base::JSONReader::ValueWithError value_with_error =
ExtractSessionDict(restore_session_url);
ASSERT_TRUE(value_with_error.value.has_value());
// Verify that last kMaxSessionSize titles and URLs are present.
base::Value* titles_value = value_with_error.value->FindKey("titles");
ASSERT_TRUE(titles_value);
ASSERT_TRUE(titles_value->is_list());
ASSERT_EQ(static_cast<size_t>(kMaxSessionSize),
titles_value->GetList().size());
ASSERT_EQ("Test150", titles_value->GetList()[0].GetString());
ASSERT_EQ("Test224",
titles_value->GetList()[kMaxSessionSize - 1].GetString());
base::Value* urls_value = value_with_error.value->FindKey("urls");
ASSERT_TRUE(urls_value);
ASSERT_TRUE(urls_value->is_list());
ASSERT_EQ(static_cast<size_t>(kMaxSessionSize), urls_value->GetList().size());
ASSERT_EQ("http://www.150.com/", urls_value->GetList()[0].GetString());
ASSERT_EQ("http://www.224.com/",
urls_value->GetList()[kMaxSessionSize - 1].GetString());
// Verify the offset is correct.
ASSERT_EQ(0, value_with_error.value->FindKey("offset")->GetInt());
}
// Verifies that large session can be stored in NSURL and that extra items
// are trimmed from the left and right sides of |last_committed_item_index|.
TEST_F(WKNavigationUtilTest,
CreateRestoreSessionUrlForExtraLargeBackAndForwardList) {
// Create restore session URL with large number of items that exceeds
// kMaxSessionSize.
const size_t kItemCount = kMaxSessionSize * 2;
std::vector<std::unique_ptr<NavigationItem>> items;
CreateTestNavigationItems(kItemCount, items);
int first_index = 0;
GURL restore_session_url;
CreateRestoreSessionUrl(
/*last_committed_item_index=*/kMaxSessionSize, items,
&restore_session_url, &first_index);
ASSERT_EQ(38, first_index);
ASSERT_TRUE(IsRestoreSessionUrl(restore_session_url));
ASSERT_TRUE(IsRestoreSessionUrl(net::NSURLWithGURL(restore_session_url)));
// Extract session JSON from restoration URL.
base::JSONReader::ValueWithError value_with_error =
ExtractSessionDict(restore_session_url);
ASSERT_TRUE(value_with_error.value.has_value());
// Verify that last kMaxSessionSize titles and URLs are present.
base::Value* titles_value = value_with_error.value->FindKey("titles");
ASSERT_TRUE(titles_value);
ASSERT_TRUE(titles_value->is_list());
ASSERT_EQ(static_cast<size_t>(kMaxSessionSize),
titles_value->GetList().size());
ASSERT_EQ("Test38", titles_value->GetList()[0].GetString());
ASSERT_EQ("Test112",
titles_value->GetList()[kMaxSessionSize - 1].GetString());
base::Value* urls_value = value_with_error.value->FindKey("urls");
ASSERT_TRUE(urls_value);
ASSERT_TRUE(urls_value->is_list());
ASSERT_EQ(static_cast<size_t>(kMaxSessionSize), urls_value->GetList().size());
ASSERT_EQ("http://www.38.com/", urls_value->GetList()[0].GetString());
ASSERT_EQ("http://www.112.com/",
urls_value->GetList()[kMaxSessionSize - 1].GetString());
// Verify the offset is correct.
ASSERT_EQ((1 - kMaxSessionSize) / 2,
value_with_error.value->FindKey("offset")->GetInt());
}
TEST_F(WKNavigationUtilTest, IsNotRestoreSessionUrl) {
EXPECT_FALSE(IsRestoreSessionUrl(GURL()));
EXPECT_FALSE(IsRestoreSessionUrl([NSURL URLWithString:@""]));
EXPECT_FALSE(IsRestoreSessionUrl(GURL("file://somefile")));
EXPECT_FALSE(IsRestoreSessionUrl([NSURL URLWithString:@"file://somefile"]));
EXPECT_FALSE(IsRestoreSessionUrl(GURL("http://www.1.com")));
EXPECT_FALSE(IsRestoreSessionUrl([NSURL URLWithString:@"http://www.1.com"]));
}
// Tests that CreateRedirectUrl and ExtractTargetURL used back-to-back is an
// identity transformation.
TEST_F(WKNavigationUtilTest, CreateAndExtractTargetURL) {
GURL target_url = GURL("http://www.1.com?query=special%26chars");
GURL url = CreateRedirectUrl(target_url);
ASSERT_TRUE(url.SchemeIsFile());
GURL extracted_url;
ASSERT_TRUE(ExtractTargetURL(url, &extracted_url));
EXPECT_EQ(target_url, extracted_url);
}
TEST_F(WKNavigationUtilTest, IsPlaceholderUrl) {
if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage))
return;
// Valid placeholder URLs.
EXPECT_TRUE(IsPlaceholderUrl(GURL("about:blank?for=")));
EXPECT_TRUE(IsPlaceholderUrl(GURL("about:blank?for=chrome%3A%2F%2Fnewtab")));
EXPECT_TRUE(IsPlaceholderUrl([NSURL URLWithString:@"about:blank?for="]));
EXPECT_TRUE(IsPlaceholderUrl(GURL("about:blank?for=chrome%3A%2F%2Fnewtab")));
EXPECT_TRUE(IsPlaceholderUrl(
[NSURL URLWithString:@"about:blank?for=chrome%3A%2F%2Fnewtab"]));
// Not an about:blank URL.
EXPECT_FALSE(IsPlaceholderUrl(GURL::EmptyGURL()));
EXPECT_FALSE(IsPlaceholderUrl([NSURL URLWithString:@""]));
// Missing ?for= query parameter.
EXPECT_FALSE(IsPlaceholderUrl(GURL("about:blank")));
EXPECT_FALSE(IsPlaceholderUrl([NSURL URLWithString:@"about:blank"]));
EXPECT_FALSE(IsPlaceholderUrl(GURL("about:blank?chrome:%3A%2F%2Fnewtab")));
EXPECT_FALSE(IsPlaceholderUrl(
[NSURL URLWithString:@"about:blank?chrome:%3A%2F%2Fnewtab"]));
}
TEST_F(WKNavigationUtilTest, EncodReturnsEmptyOnInvalidUrls) {
if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage))
return;
EXPECT_EQ(GURL::EmptyGURL(), CreatePlaceholderUrlForUrl(GURL::EmptyGURL()));
EXPECT_EQ(GURL::EmptyGURL(), CreatePlaceholderUrlForUrl(GURL("notaurl")));
}
TEST_F(WKNavigationUtilTest, EncodeDecodeValidUrls) {
if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage))
return;
{
GURL original("chrome://chrome-urls");
GURL encoded("about:blank?for=chrome%3A%2F%2Fchrome-urls");
EXPECT_EQ(encoded, CreatePlaceholderUrlForUrl(original));
EXPECT_EQ(original, ExtractUrlFromPlaceholderUrl(encoded));
}
{
GURL original("about:blank");
GURL encoded("about:blank?for=about%3Ablank");
EXPECT_EQ(encoded, CreatePlaceholderUrlForUrl(original));
EXPECT_EQ(original, ExtractUrlFromPlaceholderUrl(encoded));
}
}
// Tests that invalid URLs will be rejected in decoding.
TEST_F(WKNavigationUtilTest, DecodeRejectInvalidUrls) {
if (base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage))
return;
GURL encoded("about:blank?for=thisisnotanurl");
EXPECT_EQ(GURL::EmptyGURL(), ExtractUrlFromPlaceholderUrl(encoded));
}
// Tests that app specific urls and non-placeholder about: urls do not need a
// user agent type, but normal urls and placeholders do.
TEST_F(WKNavigationUtilTest, URLNeedsUserAgentType) {
// Not app specific or non-placeholder about urls.
GURL non_user_agent_urls("http://newtab");
GURL::Replacements scheme_replacements;
scheme_replacements.SetSchemeStr(kTestAppSpecificScheme);
EXPECT_FALSE(URLNeedsUserAgentType(
non_user_agent_urls.ReplaceComponents(scheme_replacements)));
scheme_replacements.SetSchemeStr(url::kAboutScheme);
EXPECT_FALSE(URLNeedsUserAgentType(
non_user_agent_urls.ReplaceComponents(scheme_replacements)));
if (base::FeatureList::IsEnabled(features::kUseJSForErrorPage)) {
// about:blank pages.
EXPECT_FALSE(URLNeedsUserAgentType(GURL("about:blank")));
} else {
// Not a placeholder.
EXPECT_TRUE(URLNeedsUserAgentType(GURL("about:blank?for=")));
}
// Normal URL.
EXPECT_TRUE(URLNeedsUserAgentType(GURL("http://www.0.com")));
// file:// URL.
EXPECT_FALSE(URLNeedsUserAgentType(GURL("file://foo.pdf")));
// App specific URL or a placeholder for an app specific URL.
GURL app_specific(
url::SchemeHostPort(kTestAppSpecificScheme, "foo", 0).Serialize());
EXPECT_FALSE(URLNeedsUserAgentType(app_specific));
if (!base::FeatureList::IsEnabled(web::features::kUseJSForErrorPage))
EXPECT_FALSE(
URLNeedsUserAgentType(CreatePlaceholderUrlForUrl(app_specific)));
}
} // namespace wk_navigation_util
} // namespace web