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 }