diff --git a/framework/src/play/data/validation/URL.java b/framework/src/play/data/validation/URL.java index fae27f8e20..1167b08f75 100644 --- a/framework/src/play/data/validation/URL.java +++ b/framework/src/play/data/validation/URL.java @@ -17,5 +17,17 @@ public @interface URL { String message() default URLCheck.mes; + + /** + * TLDs have been made mandatory so single names like "localhost" fails' + * The default regex was built to match URLs having a real domain name (at least 2 labels separated by a dot). + * see: https://gist.github.com/dperini/729294 + */ + boolean tldMandatory() default true; + + /** + * exclude loopback address space + */ + boolean excludeLoopback() default true; } diff --git a/framework/src/play/data/validation/URLCheck.java b/framework/src/play/data/validation/URLCheck.java index f288c1e801..4747057b9b 100644 --- a/framework/src/play/data/validation/URLCheck.java +++ b/framework/src/play/data/validation/URLCheck.java @@ -1,20 +1,66 @@ package play.data.validation; -import java.util.regex.Pattern; import net.sf.oval.Validator; import net.sf.oval.configuration.annotation.AbstractAnnotationCheck; import net.sf.oval.context.OValContext; +import java.util.regex.Pattern; + @SuppressWarnings("serial") public class URLCheck extends AbstractAnnotationCheck { static final String mes = "validation.url"; - static Pattern urlPattern = Pattern.compile("^(http|https|ftp)\\://[a-zA-Z0-9\\-\\.]+\\.[a-z" + - "A-Z]{2,3}(:[a-zA-Z0-9]*)?/?([a-zA-Z0-9\\-\\._\\?\\,\\'/\\\\\\+&%\\$#\\=~\\!])*$"); + + boolean tldMandatory; + boolean excludeLoopback; + + /** + * big thank you to https://gist.github.com/dperini/729294 + * well suited url pattern for most of internet routeable ips and real domains + */ + static String[] regexFragments = { + /* 00 */ "^", + // protocol identifier + /* 01 */ "(?:(?:https?|s?ftp|rtsp|mms)://)", + // user:pass authentication + /* 02 */ "(?:\\S+(?::\\S*)?@)?", + /* 03 */ "(?:", + // IP address exclusion + // private & local networks + /* 04 */ "(?!(?:10|127)(?:\\.\\d{1,3}){3})", + /* 05 */ "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})", + /* 06 */ "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})", + // IP address dotted notation octets + // excludes loopback network 0.0.0.0 + // excludes reserved space >= 224.0.0.0 + // excludes network & broacast addresses + // (first & last IP address of each class) + /* 07 */ "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])", + /* 08 */ "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}", + /* 09 */ "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))", + /* 10 */ "|", + // host name + /* 11 */ "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)", + // domain name + /* 12 */ "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*", + // TLD identifier + /* 13 */ "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))", + // TLD may end with dot + /* 14 */ "\\.?", + /* 15 */ ")", + // port number + /* 16 */ "(?::\\d{2,5})?", + // resource path + /* 17 */ "(?:[/?#]\\S*)?", + /* 18 */ "$" + }; + static Pattern urlPattern = Pattern.compile(String.join("", regexFragments), Pattern.CASE_INSENSITIVE); @Override public void configure(URL url) { setMessage(url.message()); + this.tldMandatory = url.tldMandatory(); + this.excludeLoopback = url.excludeLoopback(); } @Override @@ -22,7 +68,17 @@ public boolean isSatisfied(Object validatedObject, Object value, OValContext con if (value == null || value.toString().length() == 0) { return true; } - return urlPattern.matcher(value.toString()).matches(); + if (!tldMandatory || !excludeLoopback) { + //slow for special cases + String[] localRegexFragments = new String[regexFragments.length]; + System.arraycopy(regexFragments, 0, localRegexFragments, 0, regexFragments.length); + if (!excludeLoopback) localRegexFragments[4] = "(?!(?:10)(?:\\.\\d{1,3}){3})"; + if (!tldMandatory) localRegexFragments[13] = ""; + Pattern localUrlPattern = Pattern.compile(String.join("", localRegexFragments), Pattern.CASE_INSENSITIVE); + return localUrlPattern.matcher(value.toString()).matches(); + } else { + return urlPattern.matcher(value.toString()).matches(); + } } } diff --git a/framework/src/play/data/validation/Validation.java b/framework/src/play/data/validation/Validation.java index 7042acf39c..dd616fc64e 100644 --- a/framework/src/play/data/validation/Validation.java +++ b/framework/src/play/data/validation/Validation.java @@ -400,14 +400,25 @@ public ValidationResult email(Object o) { return Validation.email(key, o); } - public static ValidationResult url(String key, Object o) { + public static ValidationResult url(String key, Object o, boolean tldMandatory, boolean excludeLoopback) { URLCheck check = new URLCheck(); + check.tldMandatory = tldMandatory; + check.excludeLoopback = excludeLoopback; return applyCheck(check, key, o); } + public static ValidationResult url(String key, Object o) { + return url(key, o, true, true); + } + + public ValidationResult url(Object o, boolean tldMandatory, boolean excludeLoopback ) { + String key = getLocalName(o); + return Validation.url(key, o, tldMandatory, excludeLoopback); + } + public ValidationResult url(Object o) { String key = getLocalName(o); - return Validation.url(key, o); + return Validation.url(key, o, true, true); } public static ValidationResult phone(String key, Object o) { diff --git a/framework/test-src/play/data/validation/URLTest.java b/framework/test-src/play/data/validation/URLTest.java new file mode 100644 index 0000000000..6ef3fb052a --- /dev/null +++ b/framework/test-src/play/data/validation/URLTest.java @@ -0,0 +1,89 @@ +package play.data.validation; + +import org.junit.Test; +import play.i18n.MessagesBuilder; + +import static org.junit.Assert.assertEquals; + +public class URLTest { + @Test + public void validURLValidationTest() { + new MessagesBuilder().build(); + Validation.current.set(new Validation()); + + assertValidURL(true, "http://foo.com/blah_blah"); + assertValidURL(true, "http://foo.com/blah_blah/"); + assertValidURL(true, "http://foo.com/blah_blah_(wikipedia)"); + assertValidURL(true, "http://foo.com/blah_blah_(wikipedia)_(again)"); + assertValidURL(true, "http://www.example.com/wpstyle/?p=364"); + assertValidURL(true, "https://www.example.com/foo/?bar=baz&inga=42&quux"); + assertValidURL(true, "http://✪df.ws/123"); + assertValidURL(true, "http://userid:password@example.com:8080"); + assertValidURL(true, "http://userid:password@example.com:8080/"); + assertValidURL(true, "http://userid@example.com"); + assertValidURL(true, "http://userid@example.com/"); + assertValidURL(true, "http://userid@example.com:8080"); + assertValidURL(true, "http://userid@example.com:8080/"); + assertValidURL(true, "http://userid:password@example.com"); + assertValidURL(true, "http://userid:password@example.com/"); + assertValidURL(true, "http://142.42.1.1/"); + assertValidURL(true, "http://142.42.1.1:8080/"); + assertValidURL(true, "http://➡.ws/䨹"); + assertValidURL(true, "http://⌘.ws"); + assertValidURL(true, "http://⌘.ws/"); + assertValidURL(true, "http://foo.com/blah_(wikipedia)#cite-1"); + assertValidURL(true, "http://foo.com/blah_(wikipedia)_blah#cite-1"); + assertValidURL(true, "http://foo.com/unicode_(✪)_in_parens"); + assertValidURL(true, "http://foo.com/(something)?after=parens"); + assertValidURL(true, "http://☺.damowmow.com/"); + assertValidURL(true, "http://code.google.com/events/#&product=browser"); + assertValidURL(true, "http://j.mp"); + assertValidURL(true, "ftp://foo.bar/baz"); + assertValidURL(true, "http://foo.bar/?q=Test%20URL-encoded%20stuff"); + assertValidURL(true, "http://مثال.إختبار"); + assertValidURL(true, "http://例子.测试"); + assertValidURL(true, "http://उदाहरण.परीक्षा"); + assertValidURL(true, "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com"); + assertValidURL(true, "http://1337.net"); + assertValidURL(true, "http://a.b-c.de"); + assertValidURL(true, "http://223.255.255.254"); + } + + /** + * see ticket https://play.lighthouseapp.com/projects/57987-play-framework/tickets/1764-url-regex-is-not-correct + */ + @Test + public void validURL_1764_Test() { + new MessagesBuilder().build(); + Validation.current.set(new Validation()); + + assertValidURL(false, "http://localhost/"); + assertValidURL(false, "http://LOCALHOST/"); + assertValidURL(false, "http://localhost:80/"); + + assertValidURL(true, "http://localhost/", false, true); + assertValidURL(true, "http://LOCALHOST/", false, true); + assertValidURL(true, "https://LOCALHOST/", false, true); + assertValidURL(true, "ftp://LOCALHOST/", false, true); + assertValidURL(true, "sftp://LOCALHOST/", false, true); + assertValidURL(true, "rtsp://LOCALHOST/", false, true); + assertValidURL(true, "mms://LOCALHOST/", false, true); + assertValidURL(true, "http://localhost:80/", false, true); + + assertValidURL(false, "http://127.0.0.1"); + assertValidURL(false, "http://127.0.0.1:80"); + + assertValidURL(true, "http://127.0.0.1", true, false); + assertValidURL(true, "http://127.0.0.1:80", true, false); + } + + private void assertValidURL(boolean valid, String url, boolean tldMandatory, boolean excludeLoopback) { + Validation.clear(); + Validation.ValidationResult result = Validation.url("url", url, tldMandatory, excludeLoopback); + assertEquals("Validation url [" + url + "] should be " + valid, valid, result.ok); + } + + private void assertValidURL(Boolean valid, String url) { + assertValidURL(valid, url, true, true); + } +}