Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 42 additions & 42 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,42 @@ packages:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.19.1"
cupertino_icons:
dependency: "direct main"
description:
Expand All @@ -53,10 +53,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.3"
flutter:
dependency: "direct main"
description: flutter
Expand All @@ -78,34 +78,34 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
Expand All @@ -118,87 +118,87 @@ packages:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.16.0"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.9.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
version: "0.7.6"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "14.2.5"
version: "15.0.2"
sdks:
dart: ">=3.3.0 <4.0.0"
dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
1 change: 1 addition & 0 deletions lib/form_validator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ library form_validator;
export 'src/locale.dart';
export 'src/validator_builder.dart'
show ValidationBuilder, StringValidationCallback;
export 'src/validator_options.dart' show ValidatorOptions;
110 changes: 104 additions & 6 deletions lib/src/validator_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,96 @@ class ValidationBuilder {
add((v) => regExp.hasMatch(v!) ? null : message);

/// Value must be a well formatted email
ValidationBuilder email([String? message]) => add((v) =>
_options.emailRegExp.hasMatch(v!) ? null : message ?? _locale.email(v));
ValidationBuilder email([String? message]) =>
add((v) => (_options.emailRegExp != null
? _options.emailRegExp!.hasMatch(v!)
: _isValidEmail(v!))
? null
: message ?? _locale.email(v));

static final RegExp _emailLocalSpecialChars = RegExp(r'["(),:;<>@\[\\\]]');

bool _isValidEmail(String s) {
// The goal is to allow as much values as possible while eliminating obvious
// invalid values. False negatives are way more harmful than false positives
// for client side email validation.
//
// A proper server-side SMTP based validation should be used on top whenever
// the validity of the email address is a concern.

/*
Ref 1: https://stackoverflow.com/a/48170419
Ref 2: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html#syntactic-validation

1. The email address contains two parts, separated with an @ symbol.
2. The email address does not contain dangerous characters (such as backticks, single or double quotes, or null bytes).
Exactly which characters are dangerous will depend on how the address is going to be used (echoed in page, inserted into database, etc).
3. The domain part contains only letters, numbers, hyphens (-) and periods (.).
4. The email address is a reasonable length:
4.1. The local part (before the @) should be no more than 63 characters.
4.2. The total length should be no more than 254 characters.
*/

// 2. "dangerous characters" is backend dependent, thus we can't implement one fits all solution

// 4.2.
if (s.length > 254 || s.length < 3) return false;

// 1.
final atIndex = s.lastIndexOf('@');
if (atIndex < 0) return false;

// 4.
final local = s.substring(0, atIndex);
if (local.length > 63 || local.length == 0) return false;

final localIsPotentiallyQuoted = local.contains('"') && local.length > 2;
if (!localIsPotentiallyQuoted) {
/*
Ref 3: https://en.wikipedia.org/wiki/Email_address#:~:text=Space%20and%20special%20characters

Space and special characters "(),:;<>@[\] are allowed with
restrictions (they are only allowed inside a quoted string,
as described in the paragraph below, and in that quoted string,
any backslash or double-quote must be preceded once by a backslash);
*/
if (_emailLocalSpecialChars.hasMatch(local)) return false;
}

// 3.
final domain = s.substring(atIndex + 1);
// Not practical, but syntactically correct
if (domain.length < 3) return false;
Comment thread
themisir marked this conversation as resolved.

if (domain.startsWith('[')) {
if (domain.endsWith(']') && domain.length > 3) {
// IPv4 or IPv6
final ip = domain.substring(1, domain.length - 1);
if (!_isValidIpv4(ip) && !_isValidIpv6(ip)) {
return false;
}
} else {
// unterminated
return false;
}
} else {
// 3.
if (!domain.contains('.')) return false;

/*
Ref 4: https://webmasters.stackexchange.com/a/119105

> Each node has a label, which is zero to 63 octets in length. [...]
> One label is reserved, and that is the null (i.e., zero length) label used for the root.
>
> RFC 1034
*/
if (domain.startsWith('.')) return false;
if (domain.contains('..')) return false;
}

return true;
}

// needed for short circuiting the full validation
static final RegExp _anyLetter = RegExp(r'[A-Za-z]');
Expand All @@ -148,12 +236,22 @@ class ValidationBuilder {
: message ?? _locale.phoneNumber(v));

/// Value must be a well formatted IPv4 address
ValidationBuilder ip([String? message]) => add((v) =>
_options.ipv4RegExp.hasMatch(v!) ? null : message ?? _locale.ip(v));
ValidationBuilder ip([String? message]) =>
add((v) => _isValidIpv4(v!) ? null : message ?? _locale.ip(v));

bool _isValidIpv4(String v) {
// todo: change to something that doesn't rely on RegExp
return _options.ipv4RegExp.hasMatch(v);
}

/// Value must be a well formatted IPv6 address
ValidationBuilder ipv6([String? message]) => add((v) =>
_options.ipv6RegExp.hasMatch(v!) ? null : message ?? _locale.ipv6(v));
ValidationBuilder ipv6([String? message]) =>
add((v) => _isValidIpv6(v!) ? null : message ?? _locale.ipv6(v));

bool _isValidIpv6(String v) {
// todo: change to something that doesn't rely on RegExp
return _options.ipv6RegExp.hasMatch(v);
}

/// Value must be a well formatted URL address
ValidationBuilder url([String? message]) => add((v) =>
Expand Down
8 changes: 3 additions & 5 deletions lib/src/validator_options.dart
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
typedef ValidatorPredicate = bool Function(String value);

class ValidatorOptions {
ValidatorOptions({
RegExp? emailRegExp,
RegExp? phoneRegExp,
RegExp? ipv4RegExp,
RegExp? ipv6RegExp,
RegExp? urlRegExp,
}) : this.emailRegExp = emailRegExp ?? _defaultEmailRegExp,
}) : this.emailRegExp = emailRegExp,
this.phoneRegExp = phoneRegExp ?? _defaultPhoneRegExp,
this.ipv4RegExp = ipv4RegExp ?? _defaultIpv4RegExp,
this.ipv6RegExp = ipv6RegExp ?? _defaultIpv6RegExp,
this.urlRegExp = urlRegExp ?? _defaultUrlRegExp;

RegExp emailRegExp;
RegExp? emailRegExp;
RegExp phoneRegExp;
RegExp ipv4RegExp;
RegExp ipv6RegExp;
RegExp urlRegExp;

static final RegExp _defaultEmailRegExp = RegExp(
static final RegExp LegacyEmailRegExpThatHadFalseNegatives = RegExp(
r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9\-\_]+(\.[a-zA-Z]+)*$");

static final RegExp _defaultPhoneRegExp = RegExp(r'^\d{7,15}$');
Expand Down
Loading
Loading