1 /**
2 	String input validation routines
3 
4 	Copyright: © 2012-2014 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module vibe.utils.validation;
9 
10 import vibe.utils.string;
11 
12 import std.algorithm : canFind;
13 import std.array : appender;
14 import std.compiler;
15 import std.conv;
16 import std.exception;
17 import std.format;
18 import std.net.isemail;
19 import std.range : isOutputRange;
20 import std.string;
21 import std.utf;
22 
23 @safe:
24 
25 // TODO: add nothrow to the exception-less versions (but formattedWrite isn't nothrow)
26 
27 
28 /** Provides a simple email address validation.
29 
30 	Note that the validation could be stricter in some cases than required. The user name
31 	is forced to be ASCII, which is not strictly required as of RFC 6531. It also does not
32 	allow quotiations for the user name part (RFC 5321).
33 
34 	Invalid email adresses will cause an exception with the error description to be thrown.
35 */
36 string validateEmail()(string str, size_t max_length = 64)
37 {
38 	auto err = appender!string();
39 	enforce(validateEmail(err, str, max_length), err.data);
40 	return str;
41 }
42 /// ditto
43 bool validateEmail(R)(ref R error_sink, string str, size_t max_length = 64)
44 	if (isOutputRange!(R, char))
45 {
46 	if (str.length > max_length) {
47 		error_sink.formattedWrite("The email address may not be longer than %s characters.", max_length);
48 		return false;
49 	}
50 	auto at_idx = str.indexOf('@');
51 	if (at_idx < 0) {
52 		error_sink.put("Email is missing the '@'.");
53 		return false;
54 	}
55 
56 	if (!validateIdent(error_sink, str[0 .. at_idx], "!#$%&'*+-/=?^_`{|}~.(),:;<>@[\\]", "An email user name", false))
57 		return false;
58 
59 	auto domain = str[at_idx+1 .. $];
60 	auto dot_idx = domain.indexOf('.');
61 	if (dot_idx <= 0 || dot_idx >= str.length-2) {
62 		error_sink.put("The email domain is not valid.");
63 		return false;
64 	}
65 	if (domain.anyOf(" @,[](){}<>!\"'%&/\\?*#;:|")) {
66 		error_sink.put("The email domain contains invalid characters.");
67 		return false;
68 	}
69 
70 	if (() @trusted { return !isEmail(str); }()) {
71 		error_sink.put("The email address is invalid.");
72 		return false;
73 	}
74 
75 	return true;
76 }
77 
78 unittest {
79 	assertNotThrown(validateEmail("0a0@b.com"));
80 	assertNotThrown(validateEmail("123@123.com"));
81 	assertThrown(validateEmail("§@b.com"));
82 }
83 
84 /** Validates a user name string.
85 
86 	User names may only contain ASCII letters and digits or any of the specified additional
87 	letters.
88 
89 	Invalid user names will cause an exception with the error description to be thrown.
90 */
91 string validateUserName()(string str, int min_length = 3, int max_length = 32, string additional_chars = "-_", bool no_number_start = true)
92 {
93 	auto err = appender!string();
94 	enforce(validateUserName(err, str, min_length, max_length, additional_chars, no_number_start), err.data);
95 	return str;
96 }
97 /// ditto
98 bool validateUserName(R)(ref R error_sink, string str, int min_length = 3, int max_length = 32, string additional_chars = "-_", bool no_number_start = true)
99 	if (isOutputRange!(R, char))
100 {
101 	// FIXME: count graphemes instead of code units!
102 	if (str.length < min_length) {
103 		error_sink.formattedWrite("The user name must be at least %s characters long.", min_length);
104 		return false;
105 	}
106 
107 	if (str.length > max_length) {
108 		error_sink.formattedWrite("The user name must not be longer than %s characters.", max_length);
109 		return false;
110 	}
111 
112 	if (!validateIdent(error_sink, str, additional_chars, "A user name", no_number_start))
113 		return false;
114 
115 	return true;
116 }
117 
118 /** Validates an identifier string as used in most programming languages.
119 
120 	The identifier must begin with a letter or with any of the additional_chars and may
121 	contain only ASCII letters and digits and any of the additional_chars.
122 
123 	Invalid identifiers will cause an exception with the error description to be thrown.
124 */
125 string validateIdent()(string str, string additional_chars = "_", string entity_name = "An identifier", bool no_number_start = true)
126 {
127 	auto err = appender!string();
128 	enforce(validateIdent(err, str, additional_chars, entity_name, no_number_start), err.data);
129 	return str;
130 }
131 /// ditto
132 bool validateIdent(R)(ref R error_sink, string str, string additional_chars = "_", string entity_name = "An identifier", bool no_number_start = true)
133 	if (isOutputRange!(R, char))
134 {
135 	// NOTE: this is meant for ASCII identifiers only!
136 	foreach (i, char ch; str) {
137 		if (ch >= 'a' && ch <= 'z') continue;
138 		if (ch >= 'A' && ch <= 'Z') continue;
139 		if (ch >= '0' && ch <= '9') {
140 			if (!no_number_start || i > 0) continue;
141 			else {
142 				error_sink.formattedWrite("%s must not begin with a number.", entity_name);
143 				return false;
144 			}
145 		}
146 		if (additional_chars.canFind(ch)) continue;
147 		error_sink.formattedWrite("%s may only contain numbers, letters and one of (%s)", entity_name, additional_chars);
148 		return false;
149 	}
150 
151 	return true;
152 }
153 
154 /** Checks a password for minimum complexity requirements
155 */
156 string validatePassword()(string str, string str_confirm, size_t min_length = 8, size_t max_length = 64)
157 {
158 	auto err = appender!string();
159 	enforce(validatePassword(err, str, str_confirm, min_length, max_length), err.data);
160 	return str;
161 }
162 /// ditto
163 bool validatePassword(R)(ref R error_sink, string str, string str_confirm, size_t min_length = 8, size_t max_length = 64)
164 	if (isOutputRange!(R, char))
165 {
166 	// FIXME: count graphemes instead of code units!
167 	if (str.length < min_length) {
168 		error_sink.formattedWrite("The password must be at least %s characters long.", min_length);
169 		return false;
170 	}
171 
172 	if (str.length > max_length) {
173 		error_sink.formattedWrite("The password must not be longer than %s characters.", max_length);
174 		return false;
175 	}
176 
177 	if (str != str_confirm) {
178 		error_sink.put("The password and the confirmation differ.");
179 		return false;
180 	}
181 
182 	return true;
183 }
184 
185 /** Checks if a string falls within the specified length range.
186 */
187 string validateString(string str, size_t min_length = 0, size_t max_length = 0, string entity_name = "String")
188 {
189 	auto err = appender!string();
190 	enforce(validateString(err, str, min_length, max_length, entity_name), err.data);
191 	return str;
192 }
193 /// ditto
194 bool validateString(R)(ref R error_sink, string str, size_t min_length = 0, size_t max_length = 0, string entity_name = "String")
195 	if (isOutputRange!(R, char))
196 {
197 	try std.utf.validate(str);
198 	catch (Exception e) {
199 		error_sink.put(e.msg);
200 		return false;
201 	}
202 
203 	// FIXME: count graphemes instead of code units!
204 	if (str.length < min_length) {
205 		error_sink.formattedWrite("%s must be at least %s characters long.", entity_name, min_length);
206 		return false;
207 	}
208 
209 	if (max_length > 0 && str.length > max_length) {
210 		error_sink.formattedWrite("%s must not be longer than %s characters.", entity_name, max_length);
211 		return false;
212 	}
213 
214 	return true;
215 }