1 /**
2 	String input validation routines
3 
4 	Copyright: © 2012-2014 RejectedSoftware e.K.
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 	static if (__VERSION__ >= 2072) {
71 		if (() @trusted { return !isEmail(str); }()) {
72 			error_sink.put("The email address is invalid.");
73 			return false;
74 		}
75 	}
76 
77 	return true;
78 }
79 
80 unittest {
81 	assertNotThrown(validateEmail("0a0@b.com"));
82 	assertNotThrown(validateEmail("123@123.com"));
83 	assertThrown(validateEmail("§@b.com"));
84 }
85 
86 /** Validates a user name string.
87 
88 	User names may only contain ASCII letters and digits or any of the specified additional
89 	letters.
90 
91 	Invalid user names will cause an exception with the error description to be thrown.
92 */
93 string validateUserName()(string str, int min_length = 3, int max_length = 32, string additional_chars = "-_", bool no_number_start = true)
94 {
95 	auto err = appender!string();
96 	enforce(validateUserName(err, str, min_length, max_length, additional_chars, no_number_start), err.data);
97 	return str;
98 }
99 /// ditto
100 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)
101 	if (isOutputRange!(R, char))
102 {
103 	// FIXME: count graphemes instead of code units!
104 	if (str.length < min_length) {
105 		error_sink.formattedWrite("The user name must be at least %s characters long.", min_length);
106 		return false;
107 	}
108 
109 	if (str.length > max_length) {
110 		error_sink.formattedWrite("The user name must not be longer than %s characters.", max_length);
111 		return false;
112 	}
113 
114 	if (!validateIdent(error_sink, str, additional_chars, "A user name", no_number_start))
115 		return false;
116 
117 	return true;
118 }
119 
120 /** Validates an identifier string as used in most programming languages.
121 
122 	The identifier must begin with a letter or with any of the additional_chars and may
123 	contain only ASCII letters and digits and any of the additional_chars.
124 
125 	Invalid identifiers will cause an exception with the error description to be thrown.
126 */
127 string validateIdent()(string str, string additional_chars = "_", string entity_name = "An identifier", bool no_number_start = true)
128 {
129 	auto err = appender!string();
130 	enforce(validateIdent(err, str, additional_chars, entity_name, no_number_start), err.data);
131 	return str;
132 }
133 /// ditto
134 bool validateIdent(R)(ref R error_sink, string str, string additional_chars = "_", string entity_name = "An identifier", bool no_number_start = true)
135 	if (isOutputRange!(R, char))
136 {
137 	// NOTE: this is meant for ASCII identifiers only!
138 	foreach (i, char ch; str) {
139 		if (ch >= 'a' && ch <= 'z') continue;
140 		if (ch >= 'A' && ch <= 'Z') continue;
141 		if (ch >= '0' && ch <= '9') {
142 			if (!no_number_start || i > 0) continue;
143 			else {
144 				error_sink.formattedWrite("%s must not begin with a number.", entity_name);
145 				return false;
146 			}
147 		}
148 		if (additional_chars.canFind(ch)) continue;
149 		error_sink.formattedWrite("%s may only contain numbers, letters and one of (%s)", entity_name, additional_chars);
150 		return false;
151 	}
152 
153 	return true;
154 }
155 
156 /** Checks a password for minimum complexity requirements
157 */
158 string validatePassword()(string str, string str_confirm, size_t min_length = 8, size_t max_length = 64)
159 {
160 	auto err = appender!string();
161 	enforce(validatePassword(err, str, str_confirm, min_length, max_length), err.data);
162 	return str;
163 }
164 /// ditto
165 bool validatePassword(R)(ref R error_sink, string str, string str_confirm, size_t min_length = 8, size_t max_length = 64)
166 	if (isOutputRange!(R, char))
167 {
168 	// FIXME: count graphemes instead of code units!
169 	if (str.length < min_length) {
170 		error_sink.formattedWrite("The password must be at least %s characters long.", min_length);
171 		return false;
172 	}
173 
174 	if (str.length > max_length) {
175 		error_sink.formattedWrite("The password must not be longer than %s characters.", max_length);
176 		return false;
177 	}
178 
179 	if (str != str_confirm) {
180 		error_sink.put("The password and the confirmation differ.");
181 		return false;
182 	}
183 
184 	return true;
185 }
186 
187 /** Checks if a string falls within the specified length range.
188 */
189 string validateString(string str, size_t min_length = 0, size_t max_length = 0, string entity_name = "String")
190 {
191 	auto err = appender!string();
192 	enforce(validateString(err, str, min_length, max_length, entity_name), err.data);
193 	return str;
194 }
195 /// ditto
196 bool validateString(R)(ref R error_sink, string str, size_t min_length = 0, size_t max_length = 0, string entity_name = "String")
197 	if (isOutputRange!(R, char))
198 {
199 	try std.utf.validate(str);
200 	catch (Exception e) {
201 		error_sink.put(e.msg);
202 		return false;
203 	}
204 
205 	// FIXME: count graphemes instead of code units!
206 	if (str.length < min_length) {
207 		error_sink.formattedWrite("%s must be at least %s characters long.", entity_name, min_length);
208 		return false;
209 	}
210 
211 	if (max_length > 0 && str.length > max_length) {
212 		error_sink.formattedWrite("%s must not be longer than %s characters.", entity_name, max_length);
213 		return false;
214 	}
215 
216 	return true;
217 }