Commit 90512ec7 authored by kristipark's avatar kristipark Committed by Commit Bot

[NTP] Add better URL validation and adjust input field requirements for custom link dialog

Use URL validation provided by GURL, and added URL formatting using
FixupURL. This tries to make "smart" adjustments to obviously invalid
input (see url_fixer.h). The fixed URL will default to "https://" if a
scheme was not specified.

Also disabled spellcheck in the URL field and removed "required" from
the Name field. The URL will be set as the title if Name is left empty.

Bug: 874194
Change-Id: I0d40f551f3e79d77aa93ce3dd0f36f736bf987ee
Reviewed-on: https://chromium-review.googlesource.com/1182418
Commit-Queue: Kristi Park <kristipark@chromium.org>
Reviewed-by: default avatarMarc Treib <treib@chromium.org>
Cr-Commit-Position: refs/heads/master@{#586024}
parent d24b31e2
...@@ -72,7 +72,7 @@ input { ...@@ -72,7 +72,7 @@ input {
width: calc(100% - 16px); width: calc(100% - 16px);
} }
#url-field:not(.text-modified) { input::placeholder {
color: rgba(32, 33, 36, 0.38); color: rgba(32, 33, 36, 0.38);
} }
......
...@@ -17,15 +17,15 @@ ...@@ -17,15 +17,15 @@
<label id="title-field-name" class="field-title"></label> <label id="title-field-name" class="field-title"></label>
<div class="input-container"> <div class="input-container">
<input id="title-field" class="field-input" type="text" <input id="title-field" class="field-input" type="text"
autocomplete="off" tabindex="0" required></input> autocomplete="off" tabindex="0"></input>
<div class="underline"></div> <div class="underline"></div>
</div> </div>
</div> </div>
<div id="url" class="field-container"> <div id="url" class="field-container">
<label id="url-field-name" class="field-title"></label> <label id="url-field-name" class="field-title"></label>
<div class="input-container"> <div class="input-container">
<input id="url-field" class="field-input" type="text" value="https://" <input id="url-field" class="field-input" type="text" placeholder="https://"
autocomplete="off" tabindex="0" required></input> autocomplete="off" spellcheck="false" tabindex="0" required></input>
<div class="underline"></div> <div class="underline"></div>
</div> </div>
<div id="invalid-url" class="error-msg"></div> <div id="invalid-url" class="error-msg"></div>
......
...@@ -35,17 +35,6 @@ const IDS = { ...@@ -35,17 +35,6 @@ const IDS = {
}; };
/**
* Enum for classes.
* @enum {string}
* @const
*/
const CLASSES = {
// Applied if the input field has been modified.
TEXT_MODIFIED: 'text-modified',
};
/** /**
* Enum for key codes. * Enum for key codes.
* @enum {int} * @enum {int}
...@@ -105,19 +94,6 @@ let editLinkTitle = ''; ...@@ -105,19 +94,6 @@ let editLinkTitle = '';
let deleteLinkTitle = ''; let deleteLinkTitle = '';
/**
* True if the provided url is valid.
* @type {string}
*/
function isValidURL(url) {
let a = document.createElement('a');
a.href = url;
// Invalid URLs will not match the current host.
let isValid = a.host && a.host != window.location.host;
return isValid;
}
/** /**
* Handler for the 'linkData' message from the host page. Pre-populates the url * Handler for the 'linkData' message from the host page. Pre-populates the url
* and title fields with link's data obtained using the rid. Called if we are * and title fields with link's data obtained using the rid. Called if we are
...@@ -135,7 +111,6 @@ function prepopulateFields(rid) { ...@@ -135,7 +111,6 @@ function prepopulateFields(rid) {
prepopulatedLink.rid = rid; prepopulatedLink.rid = rid;
$(IDS.TITLE_FIELD).value = prepopulatedLink.title = data.title; $(IDS.TITLE_FIELD).value = prepopulatedLink.title = data.title;
$(IDS.URL_FIELD).value = prepopulatedLink.url = data.url; $(IDS.URL_FIELD).value = prepopulatedLink.url = data.url;
$(IDS.URL_FIELD).classList.add(CLASSES.TEXT_MODIFIED);
// Set accessibility names. // Set accessibility names.
$(IDS.DELETE).setAttribute('aria-label', deleteLinkTitle + ' ' + data.title); $(IDS.DELETE).setAttribute('aria-label', deleteLinkTitle + ' ' + data.title);
...@@ -175,20 +150,25 @@ function showInvalidUrlUntilTextInput() { ...@@ -175,20 +150,25 @@ function showInvalidUrlUntilTextInput() {
* completed. If the fields were unchanged, does not update the link data. * completed. If the fields were unchanged, does not update the link data.
*/ */
function finishEditLink() { function finishEditLink() {
// Show error message for invalid urls.
if (!isValidURL($(IDS.URL_FIELD).value)) {
showInvalidUrlUntilTextInput();
disableSubmitUntilTextInput();
return;
}
let newUrl = ''; let newUrl = '';
let newTitle = ''; let newTitle = '';
if ($(IDS.URL_FIELD).value != prepopulatedLink.url &&
$(IDS.URL_FIELD).value != 'https://') const urlValue = $(IDS.URL_FIELD).value;
newUrl = $(IDS.URL_FIELD).value; if (urlValue != prepopulatedLink.url) {
if ($(IDS.TITLE_FIELD).value != prepopulatedLink.title) newUrl = chrome.embeddedSearch.newTabPage.fixupAndValidateUrl(urlValue);
newTitle = $(IDS.TITLE_FIELD).value; // Show error message for invalid urls.
if (!newUrl) {
showInvalidUrlUntilTextInput();
disableSubmitUntilTextInput();
return;
}
}
const titleValue = $(IDS.TITLE_FIELD).value;
if (!titleValue) // Set the URL input as the title if no title is provided.
newTitle = urlValue;
else if (titleValue != prepopulatedLink.title)
newTitle = titleValue;
// Update the link only if a field was changed. // Update the link only if a field was changed.
if (!!newUrl || !!newTitle) { if (!!newUrl || !!newTitle) {
...@@ -219,7 +199,6 @@ function closeDialog() { ...@@ -219,7 +199,6 @@ function closeDialog() {
window.setTimeout(() => { window.setTimeout(() => {
$(IDS.FORM).reset(); $(IDS.FORM).reset();
$(IDS.URL_FIELD_CONTAINER).classList.remove('invalid'); $(IDS.URL_FIELD_CONTAINER).classList.remove('invalid');
$(IDS.URL_FIELD).classList.remove(CLASSES.TEXT_MODIFIED);
$(IDS.DELETE).disabled = false; $(IDS.DELETE).disabled = false;
$(IDS.DONE).disabled = false; $(IDS.DONE).disabled = false;
prepopulatedLink.rid = -1; prepopulatedLink.rid = -1;
...@@ -321,10 +300,6 @@ function init() { ...@@ -321,10 +300,6 @@ function init() {
closeDialog(); closeDialog();
} }
}; };
$(IDS.URL_FIELD).addEventListener('input', (event) => {
if (!$(IDS.URL_FIELD).classList.contains(CLASSES.TEXT_MODIFIED))
$(IDS.URL_FIELD).classList.add(CLASSES.TEXT_MODIFIED);
});
$(IDS.DELETE).addEventListener('click', deleteLink); $(IDS.DELETE).addEventListener('click', deleteLink);
$(IDS.CANCEL).addEventListener('click', closeDialog); $(IDS.CANCEL).addEventListener('click', closeDialog);
$(IDS.FORM).addEventListener('submit', (event) => { $(IDS.FORM).addEventListener('submit', (event) => {
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
#include "chrome/renderer/searchbox/searchbox_extension.h" #include "chrome/renderer/searchbox/searchbox_extension.h"
#include "components/favicon_base/favicon_types.h" #include "components/favicon_base/favicon_types.h"
#include "components/favicon_base/favicon_url_parser.h" #include "components/favicon_base/favicon_url_parser.h"
#include "components/url_formatter/url_fixer.h"
#include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_view.h" #include "content/public/renderer/render_view.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
...@@ -203,6 +204,23 @@ bool TranslateIconRestrictedUrl(const GURL& transient_url, ...@@ -203,6 +204,23 @@ bool TranslateIconRestrictedUrl(const GURL& transient_url,
return true; return true;
} }
std::string FixupAndValidateUrl(const std::string& url) {
GURL gurl = url_formatter::FixupURL(url, /*desired_tld=*/std::string());
if (!gurl.is_valid())
return std::string();
// Unless "http" was specified, replaces FixupURL's default "http" with
// "https".
if (url.find(std::string("http://")) == std::string::npos &&
gurl.SchemeIs(url::kHttpScheme)) {
GURL::Replacements replacements;
replacements.SetSchemeStr(url::kHttpsScheme);
gurl = gurl.ReplaceComponents(replacements);
}
return gurl.spec();
}
} // namespace internal } // namespace internal
SearchBox::IconURLHelper::IconURLHelper() = default; SearchBox::IconURLHelper::IconURLHelper() = default;
...@@ -367,6 +385,10 @@ void SearchBox::ResetCustomLinks() { ...@@ -367,6 +385,10 @@ void SearchBox::ResetCustomLinks() {
embedded_search_service_->ResetCustomLinks(page_seq_no_); embedded_search_service_->ResetCustomLinks(page_seq_no_);
} }
std::string SearchBox::FixupAndValidateUrl(const std::string& url) const {
return internal::FixupAndValidateUrl(url);
}
void SearchBox::SetCustomBackgroundURL(const GURL& background_url) { void SearchBox::SetCustomBackgroundURL(const GURL& background_url) {
embedded_search_service_->SetCustomBackgroundURL(background_url); embedded_search_service_->SetCustomBackgroundURL(background_url);
} }
......
...@@ -136,6 +136,11 @@ class SearchBox : public content::RenderFrameObserver, ...@@ -136,6 +136,11 @@ class SearchBox : public content::RenderFrameObserver,
// Sends ResetCustomLinks to the browser. // Sends ResetCustomLinks to the browser.
void ResetCustomLinks(); void ResetCustomLinks();
// Attempts to fix obviously invalid URLs. Uses the "https" scheme unless
// otherwise specified. Returns the fixed URL if valid, otherwise returns an
// empty string.
std::string FixupAndValidateUrl(const std::string& url) const;
// Updates the NTP custom background preferences, sometimes this includes // Updates the NTP custom background preferences, sometimes this includes
// image attributions. // image attributions.
void SetCustomBackgroundURL(const GURL& background_url); void SetCustomBackgroundURL(const GURL& background_url);
......
...@@ -624,6 +624,7 @@ class NewTabPageBindings : public gin::Wrappable<NewTabPageBindings> { ...@@ -624,6 +624,7 @@ class NewTabPageBindings : public gin::Wrappable<NewTabPageBindings> {
const std::string& title); const std::string& title);
static void UndoCustomLinkAction(); static void UndoCustomLinkAction();
static void ResetCustomLinks(); static void ResetCustomLinks();
static std::string FixupAndValidateUrl(const std::string& url);
static void LogEvent(int event); static void LogEvent(int event);
static void LogMostVisitedImpression( static void LogMostVisitedImpression(
int position, int position,
...@@ -679,6 +680,8 @@ gin::ObjectTemplateBuilder NewTabPageBindings::GetObjectTemplateBuilder( ...@@ -679,6 +680,8 @@ gin::ObjectTemplateBuilder NewTabPageBindings::GetObjectTemplateBuilder(
.SetMethod("undoCustomLinkAction", .SetMethod("undoCustomLinkAction",
&NewTabPageBindings::UndoCustomLinkAction) &NewTabPageBindings::UndoCustomLinkAction)
.SetMethod("resetCustomLinks", &NewTabPageBindings::ResetCustomLinks) .SetMethod("resetCustomLinks", &NewTabPageBindings::ResetCustomLinks)
.SetMethod("fixupAndValidateUrl",
&NewTabPageBindings::FixupAndValidateUrl)
.SetMethod("logEvent", &NewTabPageBindings::LogEvent) .SetMethod("logEvent", &NewTabPageBindings::LogEvent)
.SetMethod("logMostVisitedImpression", .SetMethod("logMostVisitedImpression",
&NewTabPageBindings::LogMostVisitedImpression) &NewTabPageBindings::LogMostVisitedImpression)
...@@ -886,6 +889,14 @@ void NewTabPageBindings::ResetCustomLinks() { ...@@ -886,6 +889,14 @@ void NewTabPageBindings::ResetCustomLinks() {
search_box->LogEvent(NTPLoggingEventType::NTP_CUSTOMIZE_SHORTCUT_RESTORE_ALL); search_box->LogEvent(NTPLoggingEventType::NTP_CUSTOMIZE_SHORTCUT_RESTORE_ALL);
} }
// static
std::string NewTabPageBindings::FixupAndValidateUrl(const std::string& url) {
SearchBox* search_box = GetSearchBoxForCurrentContext();
if (!search_box || !HasOrigin(GURL(chrome::kChromeSearchMostVisitedUrl)))
return std::string();
return search_box->FixupAndValidateUrl(url);
}
// static // static
void NewTabPageBindings::LogEvent(int event) { void NewTabPageBindings::LogEvent(int event) {
SearchBox* search_box = GetSearchBoxForCurrentContext(); SearchBox* search_box = GetSearchBoxForCurrentContext();
......
...@@ -77,6 +77,9 @@ bool TranslateIconRestrictedUrl(const GURL& transient_url, ...@@ -77,6 +77,9 @@ bool TranslateIconRestrictedUrl(const GURL& transient_url,
const SearchBox::IconURLHelper& helper, const SearchBox::IconURLHelper& helper,
GURL* url); GURL* url);
// Defined in searchbox.cc
std::string FixupAndValidateUrl(const std::string& url);
TEST(SearchBoxUtilTest, ParseViewIdAndRestrictedIdSuccess) { TEST(SearchBoxUtilTest, ParseViewIdAndRestrictedIdSuccess) {
int view_id = -1; int view_id = -1;
InstantRestrictedID rid = -1; InstantRestrictedID rid = -1;
...@@ -264,4 +267,55 @@ TEST(SearchBoxUtilTest, TranslateIconRestrictedUrlFailure) { ...@@ -264,4 +267,55 @@ TEST(SearchBoxUtilTest, TranslateIconRestrictedUrlFailure) {
} }
} }
TEST(SearchBoxUtilTest, FixupAndValidateUrlReturnsEmptyIfInvalid) {
struct TestCase {
const char* url;
bool is_valid;
} test_cases[] = {
{" ", false},
{"^&*@)^)", false},
{"foo", true},
{"http://foo", true},
{"\thttp://foo", true},
{" http://foo", true},
{"https://foo", true},
{"foo.com", true},
{"http://foo.com", true},
{"https://foo.com", true},
{"blob://foo", true},
};
for (const TestCase& test_case : test_cases) {
const std::string& url = FixupAndValidateUrl(test_case.url);
EXPECT_EQ(!url.empty(), test_case.is_valid)
<< " for test_case '" << test_case.url << "'";
}
}
TEST(SearchBoxUtilTest, FixupAndValidateUrlDefaultsToHttps) {
struct TestCase {
const char* url;
const char* expected_scheme;
} test_cases[] = {
// No scheme.
{"foo.com", url::kHttpsScheme},
// With "http".
{"http://foo.com", url::kHttpScheme},
// With "http" and whitespace.
{"\thttp://foo", url::kHttpScheme},
{" http://foo", url::kHttpScheme},
// With "https".
{"https://foo.com", url::kHttpsScheme},
// Non "http"/"https".
{"blob://foo", url::kBlobScheme},
};
for (const TestCase& test_case : test_cases) {
const GURL url(FixupAndValidateUrl(test_case.url));
EXPECT_TRUE(url.SchemeIs(test_case.expected_scheme))
<< " for test case '" << test_case.url << "'";
}
}
} // namespace internal } // namespace internal
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment