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 }