1 /// Main package containing all neccesarry functions
2 /// See_Also: <a href="aliases.html">`settings.aliases`</a> for shorter UDAs
3 module settings;
4 
5 import vibe.http.server;
6 import vibe.inet.webform;
7 import vibe.inet.url;
8 import vibe.web.web;
9 
10 import std.algorithm;
11 import std.ascii;
12 import std.conv;
13 import std.datetime;
14 import std.format;
15 import std.math;
16 import std.meta;
17 import std.regex;
18 import std.string;
19 import std.traits;
20 import std.typecons;
21 import std.xml : encode;
22 
23 ///
24 unittest
25 {
26 	enum FavoriteFood
27 	{
28 		// enum UDAs require at least dmd 2.082.0
29 		//dfmt off
30 		@settingTranslation(null, "Fish") @settingTranslation("de", "Fisch") @settingTranslation("ja", "魚")
31 		fish,
32 		@settingTranslation(null, "Meat") @settingTranslation("de", "Fleisch") @settingTranslation("ja", "肉")
33 		meat,
34 		@settingTranslation(null, "Vegetables") @settingTranslation("de", "Gemüse") @settingTranslation("ja", "野菜")
35 		vegetables,
36 		@settingTranslation(null, "Fruits") @settingTranslation("de", "Obst") @settingTranslation("ja", "フルーツ")
37 		fruit
38 		//dfmt on
39 	}
40 
41 	//dfmt off
42 	enum Country
43 	{
44 		none, AF, AX, AL, DZ, AS, AD, AO, AI, AQ, AG, AR, AM, AW, AC, AU, AT, AZ, BS, BH, BD, BB, BY, BE, BZ, BJ, BM,
45 		BT, BO, BA, BW, BR, IO, VG, BN, BG, BF, BI, KH, CM, CA, IC, CV, BQ, KY, CF, EA, TD, CL, CN, CX, CC, CO, KM,
46 		CG, CD, CK, CR, CI, HR, CU, CW, CY, CZ, DK, DG, DJ, DM, DO, EC, EG, SV, GQ, ER, EE, ET, FK, FO, FJ, FI, FR,
47 		GF, PF, TF, GA, GM, GE, DE, GH, GI, GR, GL, GD, GP, GU, GT, GG, GN, GW, GY, HT, HN, HK, HU, IS, IN, ID, IR,
48 		IQ, IE, IM, IL, IT, JM, JP, JE, JO, KZ, KE, KI, XK, KW, KG, LA, LV, LB, LS, LR, LY, LI, LT, LU, MO, MK, MG,
49 		MW, MY, MV, ML, MT, MH, MQ, MR, MU, YT, MX, FM, MD, MC, MN, ME, MS, MA, MZ, MM, NA, NR, NP, NL, NC, NZ, NI,
50 		NE, NG, NU, NF, KP, MP, NO, OM, PK, PW, PS, PA, PG, PY, PE, PH, PN, PL, PT, PR, QA, RE, RO, RU, RW, WS, SM,
51 		ST, SA, SN, RS, SC, SL, SG, SX, SK, SI, SB, SO, ZA, GS, KR, SS, ES, LK, BL, SH, KN, LC, MF, PM, VC, SD, SR,
52 		SJ, SZ, SE, CH, SY, TW, TJ, TZ, TH, TL, TG, TK, TO, TT, TA, TN, TR, TM, TC, TV, UM, VI, UG, UA, AE, GB, US,
53 		UY, UZ, VU, VA, VE, VN, WF, EH, YE, ZM, ZW
54 	}
55 	//dfmt on
56 
57 	enum SocialMedia
58 	{
59 		twitter = 1 << 0,
60 		facebook = 1 << 1,
61 		myspace = 1 << 2,
62 	}
63 
64 	struct Config
65 	{
66 		@requiredSetting // Must be filled out
67 		@nonAutomaticSetting // Don't auto sync when typing
68 		@emailSetting string userEmail;
69 		bool married;
70 		@urlSetting @settingLength(64) string resourceURI;
71 		// OR
72 		@settingLength(64) URL myWebsite;
73 		@multilineSetting @settingLength(1000) string aboutMe;
74 		@rangeSetting @settingRange(0, 10) int rating;
75 		@timeSetting string favoriteTimeOfDay;
76 		// OR
77 		TimeOfDay leastFavoriteTimeOfDay;
78 		@weekSetting string bestWeekYouHad;
79 		@monthSetting string firstMonthOfWork;
80 		// Timezone-less
81 		@datetimeLocalSetting string birthdayTimeAndDate;
82 		// OR
83 		DateTime myChildsBirthdayTimeAndDate;
84 		@dateSetting string myMothersBirthday;
85 		// OR
86 		Date myFathersBirthday;
87 		@colorSetting string favoriteColor;
88 		@disabledSetting string someInformation = "Just a hint, nothing changable";
89 		Country favoriteCountry;
90 		@settingTranslation("de", "Lieblingsessen")  // Translation of labels (only in translation contexts inside web interfaces)
91 		@settingTranslation("ja", "好きな食べ物")  // translations require at least vibe.d 0.8.1-alpha.3 to work
92 		@optionsSetting FavoriteFood favoriteFood;
93 		BitFlags!SocialMedia usedSocialMedia;
94 		@settingTitle("If you don't have any you can still say 1 because you have yourself.")  // Hover & validation text
95 		@settingMin(1) int numberOfFriends;
96 		@settingRange(0, 100) @settingStep(0.1) double englishSkillLevelPercentage;
97 		@settingMax(10) ubyte orderedProductCount;
98 		@settingLabel("Accept terms of service") @requiredSetting bool acceptTOS;
99 		@settingPattern(`(ISBN\s+)?\d{3}-\d-\d{5}-\d{3}-\d`) string favoriteBookISBN;
100 	}
101 
102 	import vibe.vibe;
103 
104 	auto router = new URLRouter;
105 	router.get("/style.css", serveStaticFile("styles/material.css"));
106 	router.get("/", staticRedirect("/settings"));
107 
108 	enum html = `<html>
109 		<head>
110 			<title>Settings</title>
111 			<meta charset="utf-8"/>
112 			<link rel="stylesheet" href="/style.css"/>
113 			<style>
114 				body,html{background:#efefef;color:rgba(0,0,0,0.87);font-family:Roboto,"Segoe UI",sans-serif;}
115 				.settings{background:white;border-radius:2px;padding:16px;margin:32px auto;box-shadow:0 2px 5px rgba(0,0,0,0.3);max-width:600px;}
116 			</style>
117 		</head>
118 		<body>
119 			<div class="settings">
120 				<h2>Settings</h2>
121 				%s
122 			</div>
123 		</body>
124 	</html>`;
125 
126 	struct TranslationContext
127 	{
128 		import std.meta;
129 
130 		alias languages = AliasSeq!("en", "de", "ja");
131 	}
132 
133 	Config settingsInstance; // You might fetch & save this per user, web-config only changes the struct
134 
135 	@translationContext!TranslationContext class SettingsInterface
136 	{
137 		@safe void getSettings(scope HTTPServerRequest req, scope HTTPServerResponse res)
138 		{
139 			string settings = renderSettings(settingsInstance);
140 			res.writeBody(html.format(settings), "text/html");
141 		}
142 
143 		@safe void postSettings(scope HTTPServerRequest req, scope HTTPServerResponse res)
144 		{
145 			// no-js & nonautomatic setting route
146 			auto ret = req.processSettings(settingsInstance);
147 			string settings = renderSettings(settingsInstance, ret);
148 			if (ret)
149 			{
150 				// Something changed, you can save here
151 			}
152 			res.writeBody(html.format(settings), "text/html");
153 		}
154 
155 		@path("/api/setting") @safe void postJsSettings(scope HTTPServerRequest req,
156 				scope HTTPServerResponse res)
157 		{
158 			// js route called for each individual setting
159 			if (req.processSettings(settingsInstance))
160 			{
161 				// Save settings
162 				res.writeBody("", 204); // Send 200 or 204
163 			}
164 			else
165 				res.writeBody("", HTTPStatus.badRequest);
166 		}
167 	}
168 
169 	router.registerWebInterface(new SettingsInterface);
170 	listenHTTP(new HTTPServerSettings, router);
171 	runApplication();
172 }
173 
174 /// Generates a HTML form for a configuration struct `T` with automatic instant updates using AJAX.
175 /// The fields can be annotated with the various UDAs found in this module. (setting enums + structs) $(BR)
176 /// Supported types: `enum` (drop down lists or radio box lists), `std.typecons.BitFlags` (checkbox lists),
177 /// `bool` (checkbox), string types (text, email, url, etc.), numeric types (number), `std.datetime.DateTime`
178 /// (datetime-local), `std.datetime.Date` (date), `std.datetime.TimeOfDay` (time), `vibe.inet.URL` (url)
179 /// Params:
180 ///   T = the config struct type.
181 ///   InputGenerator = the input generator to use.
182 ///   javascript = the javascript code to embed including script tag.
183 ///   value = an existing config value to prefill the inputs.
184 ///   set = a bitflag field which settings have been set properly. Any bit set to 0 will show an error string for the given field. Defaults to all success.
185 ///   formAttributes = extra HTML to put into the form.
186 ///   action = Path to the form submit HTTP endpoint.
187 ///   method = Method to use for the submit HTTP endpoint. Also replaces {method} inside the javascript template.
188 ///   jsAction = Path to the javascript form submit HTTP endpoint. Replaces {action} inside the javascript template. If empty then no js will be emitted.
189 string renderSettings(T, InputGenerator = DefaultInputGenerator,
190 		alias javascript = DefaultJavascriptCode)(T value, string formAttributes = "",
191 		string action = "/settings", string method = "POST", string jsAction = "/api/setting") @safe
192 {
193 	return renderSettings!(T, InputGenerator, javascript)(value, ulong.max,
194 			formAttributes, action, method, jsAction);
195 }
196 
197 /// ditto
198 string renderSettings(T, InputGenerator = DefaultInputGenerator,
199 		alias javascript = DefaultJavascriptCode)(T value, ulong set, string formAttributes = "",
200 		string action = "/settings", string method = "POST", string jsAction = "/api/setting") @safe
201 {
202 	method = method.toUpper;
203 	string[] settings;
204 	foreach (i, member; __traits(allMembers, T))
205 	{
206 		bool success = (set & (1 << cast(ulong) i)) != 0;
207 		settings ~= renderSetting!(InputGenerator, member)(value, success);
208 	}
209 	return `<form action="%s" method="%s"%s>%s<input type="submit" value="Save"/></form>`.format(
210 			action.encode, method.encode,
211 			formAttributes.length ? " " ~ formAttributes : "", settings.join()) ~ (jsAction.length
212 			? DefaultJavascriptCode.replace("{action}", jsAction).replace("{method}", method) : "");
213 }
214 
215 /// Generates a single input
216 string renderSetting(InputGenerator = DefaultInputGenerator, string name, Config)(
217 		ref Config config, bool success = true) @safe
218 {
219 	alias Member = AliasSeq!(__traits(getMember, config, name));
220 	auto value = __traits(getMember, config, name);
221 	alias T = Unqual!(typeof(value));
222 	enum isEmail = hasUDA!(Member[0], emailSetting);
223 	enum isUrl = hasUDA!(Member[0], urlSetting);
224 	enum isMultiline = hasUDA!(Member[0], multilineSetting);
225 	enum isRange = hasUDA!(Member[0], rangeSetting);
226 	enum isTime = hasUDA!(Member[0], timeSetting) || is(T == TimeOfDay);
227 	enum isWeek = hasUDA!(Member[0], weekSetting);
228 	enum isMonth = hasUDA!(Member[0], monthSetting);
229 	enum isDatetimeLocal = hasUDA!(Member[0], datetimeLocalSetting) || is(T == DateTime);
230 	enum isDate = hasUDA!(Member[0], dateSetting) || is(T == Date);
231 	enum isColor = hasUDA!(Member[0], colorSetting);
232 	enum isDisabled = hasUDA!(Member[0], disabledSetting);
233 	enum isRequired = hasUDA!(Member[0], requiredSetting);
234 	enum isNoJS = hasUDA!(Member[0], nonAutomaticSetting);
235 	enum isOptions = hasUDA!(Member[0], optionsSetting);
236 	enum mins = getUDAs!(Member[0], settingMin);
237 	enum maxs = getUDAs!(Member[0], settingMax);
238 	enum ranges = getUDAs!(Member[0], settingRange);
239 	enum lengths = getUDAs!(Member[0], settingLength);
240 	enum steps = getUDAs!(Member[0], settingStep);
241 	enum patterns = getUDAs!(Member[0], settingPattern);
242 	enum titles = getUDAs!(Member[0], settingTitle);
243 	enum labels = getUDAs!(Member[0], settingLabel);
244 	enum translations = getUDAs!(Member[0], settingTranslation);
245 	enum enumTranslations = getUDAs!(Member[0], enumTranslation);
246 	static if (labels.length)
247 		string uiName = labels[0].label;
248 	else
249 		string uiName = name.makeHumanName;
250 	static if (translations.length && is(typeof(language) == string))
251 	{
252 		auto lang = (() @trusted => language)();
253 		if (lang !is null)
254 			foreach (translation; translations)
255 				if (translation.language == lang)
256 					uiName = translation.label;
257 	}
258 	string raw = ` name="` ~ name ~ `"`;
259 	static if (isDisabled)
260 		raw ~= " disabled";
261 	else static if (!isNoJS)
262 		raw ~= ` onchange="updateSetting(this)"`;
263 	else
264 		raw ~= ` onchange="unlockForm(this)"`;
265 	static if (lengths.length)
266 	{
267 		auto minlength = lengths[0].min;
268 		auto maxlength = lengths[0].max;
269 		if (minlength > 0)
270 			raw ~= " minlength=\"" ~ minlength.to!string ~ "\"";
271 		if (maxlength > 0)
272 			raw ~= " maxlength=\"" ~ maxlength.to!string ~ "\"";
273 	}
274 	static if (patterns.length)
275 		raw ~= " pattern=\"" ~ patterns[0].regex.encode ~ "\"";
276 	else static if (isDatetimeLocal) // if browser doesn't support datetime-local
277 		raw ~= ` pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}"`;
278 	else static if (isTime) // if browser doesn't support time
279 		raw ~= ` pattern="[0-9]{2}:[0-9]{2}"`;
280 	else static if (isDate) // if browser doesn't support date
281 		raw ~= ` pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}"`;
282 	static if (titles.length)
283 		raw ~= " title=\"" ~ titles[0].title.encode ~ "\"";
284 	static if (isRequired)
285 		raw ~= " required";
286 	static if (is(T == enum))
287 	{
288 		static if (isOptions)
289 			return InputGenerator.optionList!(T, enumTranslations)(uiName, value, raw, success);
290 		else
291 			return InputGenerator.dropdownList!(T, enumTranslations)(uiName, value, raw, success);
292 	}
293 	else static if (is(T == BitFlags!Enum, Enum))
294 		return InputGenerator.checkboxList!(Enum, enumTranslations)(uiName, value, raw, success);
295 	else static if (is(T == bool))
296 		return InputGenerator.checkbox(uiName, value, raw, success);
297 	else static if (isSomeString!T)
298 	{
299 		static if (
300 			isEmail + isUrl + isMultiline + isTime + isWeek + isMonth
301 				+ isDatetimeLocal + isDate + isColor > 1)
302 			static assert(false, "string setting " ~ name ~ " has multiple type related attributes");
303 		static if (isMultiline)
304 			return InputGenerator.textarea(uiName, value.to!string, raw, success);
305 		else
306 			return InputGenerator.textfield(uiName, isEmail ? "email" : isUrl ? "url" : isTime ? "time" : isWeek
307 					? "week" : isMonth ? "month" : isDatetimeLocal ? "datetime-local" : isDate
308 					? "date" : isColor ? "color" : "text", value.to!string, raw, success);
309 	}
310 	else static if (is(T == DateTime))
311 		return InputGenerator.textfield(uiName, "datetime-local",
312 				value.toISOExtString[0 .. 16], raw, success);
313 	else static if (is(T == Date))
314 		return InputGenerator.textfield(uiName, "date", value.toISOExtString, raw, success);
315 	else static if (is(T == TimeOfDay))
316 		return InputGenerator.textfield(uiName, "time", value.toISOExtString[0 .. 5], raw, success);
317 	else static if (is(T == URL))
318 		return InputGenerator.textfield(uiName, "url", value.toString, raw, success);
319 	else static if (isNumeric!T)
320 	{
321 		double min, max;
322 		static if (mins.length)
323 			min = mins[0].min;
324 		static if (maxs.length)
325 			max = maxs[0].max;
326 		static if (ranges.length)
327 		{
328 			min = ranges[0].min;
329 			max = ranges[0].max;
330 		}
331 		if (min == min) // !isNaN
332 			raw ~= " min=\"" ~ min.to!string ~ "\"";
333 		if (max == max) // !isNaN
334 			raw ~= " max=\"" ~ max.to!string ~ "\"";
335 		static if (steps.length)
336 			raw ~= " step=\"" ~ steps[0].step.to!string ~ "\"";
337 		return InputGenerator.textfield(uiName, isRange ? "range" : "number",
338 				value.to!string, raw, success);
339 	}
340 	else
341 		static assert(false, "No setting generator for type " ~ T.stringof);
342 }
343 
344 /**
345 	Function processing user input and validating for correctness. $(BR)$(BR)
346 	The following validations are done: $(BR)
347 	If the setting is a  `disabledSetting`, it will always skip this field. $(BR)
348 	If the setting has a `settingPattern`, it will validate the raw value (no matter what type) against this regex. $(BR)
349 	If the setting is a number, std.conv.to will be used to try to convert it to a double and then it will be cast to the type after checking min/max/step. $(BR)
350 	If the setting is a `BitFlags!T` every passed argument will be checked if it is contained inside the enum `T` or when submitted via JS only the one specified argument will get validated and inverted if starting with `!` $(BR)
351 	If the setting is an enum the value will be checked if it is contained inside the enum. $(BR)
352 	Additionally if the setting is a floating point number and there hasn't been a min/max setup but it is a `rangeSetting`, the number will be finite. $(BR)
353 	Integral numbers will always be checked if finite & if no range is given they will be clamped. $(BR)$(BR)
354 	Attributes for strings: $(BR)
355 		`emailSetting` is validated using `std.net.isemail.isEmail(CheckDns.no, EmailStatusCode.any)` $(BR)
356 		`urlSetting` is validated using `vibe.inet.url.URL` $(BR)
357 		`timeSetting` is checked against pattern `00:00` + checking if 0 <= hour < 24 && 0 <= minute < 60 $(BR)
358 		`weekSetting` is checked against pattern `0{4,6}-W00` + checking if 1 <= year <= 200000 && 1 <= week <= 52 $(BR)
359 		`monthSetting` is checked against pattern `0{4,6}-00` + checking if 1 <= year <= 200000 && 1 <= month <= 12 $(BR)
360 		`datetimeLocalSetting` is checked against pattern `0000-00-00T00:00` + passing into `std.datetime.SysTime.fromISOExtString`` $(BR)
361 		`dateSetting` is checked against pattern `0000-00-00` + checking the date using `std.datetime.Date` $(BR)
362 		`colorSetting` is checked against pattern `#FFFFFF` $(BR)
363 	Values using these attributes can be used without the need to validate the input.
364 	Params:
365 		strict = if false, values will be fixed to conform to the input instead of discarding them.
366 		Currently only fixing numbers and string lengths and new lines in single line strings is implemented.
367 	Returns: a bit array where each bit represents an input and is set to 1 if valid
368 	*/
369 ulong processSettings(T)(scope HTTPServerRequest req, ref T config,
370 		bool strict = false, bool post = true) @safe
371 {
372 	ulong valid;
373 	auto field = (post ? req.form : req.query).get("_field", "");
374 	foreach (i, member; __traits(allMembers, T))
375 	{
376 		if (field.length && field != member)
377 			continue;
378 		valid |= req.processSetting!member(config, strict, post) << cast(ulong) i;
379 	}
380 	return valid;
381 }
382 
383 /// ditto
384 bool processSetting(string name, Config)(HTTPServerRequest req, ref Config config,
385 		bool strict = false, bool post = true) @safe
386 {
387 	alias Member = AliasSeq!(__traits(getMember, config, name));
388 	auto member = __traits(getMember, config, name);
389 	alias T = typeof(member);
390 	enum isEmail = hasUDA!(Member[0], emailSetting);
391 	enum isUrl = hasUDA!(Member[0], urlSetting);
392 	enum isMultiline = hasUDA!(Member[0], multilineSetting);
393 	enum isRange = hasUDA!(Member[0], rangeSetting);
394 	enum isTime = hasUDA!(Member[0], timeSetting);
395 	enum isWeek = hasUDA!(Member[0], weekSetting);
396 	enum isMonth = hasUDA!(Member[0], monthSetting);
397 	enum isDatetimeLocal = hasUDA!(Member[0], datetimeLocalSetting);
398 	enum isDate = hasUDA!(Member[0], dateSetting);
399 	enum isColor = hasUDA!(Member[0], colorSetting);
400 	enum isDisabled = hasUDA!(Member[0], disabledSetting);
401 	enum isRequired = hasUDA!(Member[0], requiredSetting);
402 	enum mins = getUDAs!(Member[0], settingMin);
403 	enum maxs = getUDAs!(Member[0], settingMax);
404 	enum ranges = getUDAs!(Member[0], settingRange);
405 	enum lengths = getUDAs!(Member[0], settingLength);
406 	enum steps = getUDAs!(Member[0], settingStep);
407 	enum patterns = getUDAs!(Member[0], settingPattern);
408 	static if (isDisabled)
409 		return true;
410 	else
411 	{
412 		int minlength = int.min, maxlength = int.max;
413 		static if (lengths.length)
414 		{
415 			minlength = lengths[0].min;
416 			maxlength = lengths[0].max;
417 		}
418 		T oldval = member;
419 		T newval = oldval;
420 		FormFields form = post ? req.form : req.query;
421 		auto allvals = form.getAll(name);
422 		bool isJS = form.get("_field", "").length != 0;
423 		string rawval = allvals.length ? allvals[0] : "";
424 		static if (patterns.length)
425 			if (!matchFirst(rawval, ctRegex!(patterns[0].regex)))
426 				return false;
427 		static if (isRequired)
428 			if (!allvals.length)
429 				return false;
430 		if (minlength != int.min && rawval.length < minlength)
431 			return false;
432 		if (maxlength != int.max && rawval.length > maxlength)
433 		{
434 			if (strict)
435 				return false;
436 			else
437 				rawval.length = maxlength;
438 		}
439 		static if (is(T == enum))
440 		{
441 			try
442 			{
443 				newval = cast(T) rawval.to!(OriginalType!T);
444 				bool exists = false;
445 				foreach (val; EnumMembers!T)
446 					if (val == newval)
447 					{
448 						exists = true;
449 						break;
450 					}
451 				if (!exists)
452 					return false;
453 			}
454 			catch (ConvException)
455 			{
456 				return false;
457 			}
458 		}
459 		else static if (is(T : BitFlags!Enum, Enum))
460 		{
461 			try
462 			{
463 				if (!rawval.length)
464 					return false;
465 				if (isJS)
466 				{
467 					bool negate = rawval[0] == '!';
468 					if (negate)
469 						rawval = rawval[1 .. $];
470 					auto enumType = cast(Enum) rawval.to!(OriginalType!Enum);
471 					bool exists = false;
472 					foreach (val; EnumMembers!Enum)
473 						if (val == enumType)
474 						{
475 							exists = true;
476 							break;
477 						}
478 					if (!exists)
479 						return false;
480 					if (negate)
481 						newval = oldval & ~T(enumType);
482 					else
483 						newval = oldval | enumType;
484 				}
485 				else
486 				{
487 					newval = T.init;
488 					foreach (rawval1; allvals)
489 					{
490 						auto enumType = cast(Enum) rawval1.to!(OriginalType!Enum);
491 						bool exists = false;
492 						foreach (val; EnumMembers!Enum)
493 							if (val == enumType)
494 							{
495 								exists = true;
496 								break;
497 							}
498 						if (!exists)
499 							return false;
500 						newval |= enumType;
501 					}
502 				}
503 			}
504 			catch (ConvException)
505 			{
506 				return false;
507 			}
508 		}
509 		else static if (is(T == bool))
510 			newval = allvals.length > 0;
511 		else static if (isSomeString!T)
512 		{
513 			static if (
514 				isEmail + isUrl + isMultiline + isTime + isWeek + isMonth
515 					+ isDatetimeLocal + isDate + isColor > 1)
516 				static assert(false, "string setting " ~ name ~ " has multiple type related attributes");
517 			static if (isMultiline)
518 				newval = rawval;
519 			else if (rawval.length)
520 			{
521 				if (strict && rawval.indexOfAny("\r\n") != -1)
522 					return false;
523 				else
524 					rawval = rawval.tr("\r\n", "  ");
525 				static if (isEmail)
526 				{
527 					rawval = rawval.strip;
528 					import std.net.isemail;
529 
530 					if ((()@trusted => !rawval.isEmail(CheckDns.no, EmailStatusCode.any))())
531 						return false;
532 					newval = rawval;
533 				}
534 				else static if (isUrl)
535 				{
536 					try
537 					{
538 						newval = URL(rawval.strip).toString;
539 					}
540 					catch (Exception)
541 					{
542 						return false;
543 					}
544 				}
545 				else static if (isTime)
546 				{
547 					rawval = rawval.strip;
548 					if (!validateTimeString(rawval))
549 						return false;
550 					newval = rawval;
551 				}
552 				else static if (isWeek)
553 				{
554 					rawval = rawval.strip;
555 					if (!validateWeekString(rawval))
556 						return false;
557 					newval = rawval;
558 				}
559 				else static if (isMonth)
560 				{
561 					rawval = rawval.strip;
562 					if (!validateMonthString(rawval))
563 						return false;
564 					newval = rawval;
565 				}
566 				else static if (isDatetimeLocal)
567 				{
568 					rawval = rawval.strip;
569 					if (!validateDatetimeLocalString(rawval))
570 						return false;
571 					newval = rawval;
572 				}
573 				else static if (isDate)
574 				{
575 					rawval = rawval.strip;
576 					if (!validateDateString(rawval))
577 						return false;
578 					newval = rawval;
579 				}
580 				else static if (isColor)
581 				{
582 					rawval = rawval.strip;
583 					if (!validateColorString(rawval))
584 						return false;
585 					newval = rawval;
586 				}
587 				else
588 					newval = rawval;
589 			}
590 			else
591 			{
592 				newval = "";
593 			}
594 		}
595 		else static if (is(T == DateTime))
596 		{
597 			rawval = rawval.strip;
598 			if (!validateDatetimeLocalString(rawval))
599 				return false;
600 			newval = DateTime.fromISOExtString(rawval ~ ":00");
601 		}
602 		else static if (is(T == Date))
603 		{
604 			rawval = rawval.strip;
605 			if (!validateDateString(rawval))
606 				return false;
607 			newval = Date.fromISOExtString(rawval);
608 		}
609 		else static if (is(T == TimeOfDay))
610 		{
611 			rawval = rawval.strip;
612 			if (!validateTimeString(rawval))
613 				return false;
614 			newval = TimeOfDay.fromISOExtString(rawval ~ ":00");
615 		}
616 		else static if (is(T == URL))
617 		{
618 			try
619 			{
620 				newval = URL(rawval.strip);
621 			}
622 			catch (Exception)
623 			{
624 				return false;
625 			}
626 		}
627 		else static if (isNumeric!T)
628 		{
629 			double min, max;
630 			static if (isIntegral!T)
631 			{
632 				min = T.min;
633 				max = T.max;
634 			}
635 			static if (mins.length)
636 				min = mins[0].min;
637 			static if (maxs.length)
638 				max = maxs[0].max;
639 			static if (ranges.length)
640 			{
641 				min = ranges[0].min;
642 				max = ranges[0].max;
643 			}
644 			double step = 1;
645 			static if (steps.length)
646 				step = steps[0].step;
647 			try
648 			{
649 				double val = rawval.to!double;
650 				if (min == min && val < min)
651 				{
652 					if (strict)
653 						return false;
654 					else
655 						val = min;
656 				}
657 				if (max == max && val > max)
658 				{
659 					if (strict)
660 						return false;
661 					else
662 						val = max;
663 				}
664 				val = floor(val / step) * step;
665 				bool isFinite = val == val && val != double.infinity && val != -double.infinity;
666 				static if (isRange && isFloatingPoint!T)
667 				{
668 					if (!isFinite)
669 						return false;
670 				}
671 				static if (!isFloatingPoint!T)
672 					if (!isFinite)
673 						return false;
674 				newval = cast(T) val;
675 			}
676 			catch (ConvException)
677 			{
678 				return false;
679 			}
680 		}
681 		else
682 			static assert(false, "No setting parser for type " ~ T.stringof);
683 		__traits(getMember, config, name) = newval;
684 		return true;
685 	}
686 }
687 
688 /// Validates s == pattern "00:00"
689 bool validateTimeString(string s) @safe
690 {
691 	if (s.length != 5)
692 		return false;
693 	if (!s[0].isDigit || !s[1].isDigit || s[2] != ':' || !s[3].isDigit || !s[4].isDigit)
694 		return false;
695 	ubyte h = s[0 .. 2].to!ubyte;
696 	ubyte m = s[3 .. 5].to!ubyte;
697 	if (h >= 24)
698 		return false;
699 	if (m >= 60)
700 		return false;
701 	return true;
702 }
703 
704 /// Validates s == pattern "0{4,6}-W00"
705 bool validateWeekString(string s) @safe
706 {
707 	if (s.length < 8 || s.length > 10)
708 		return false;
709 	auto dash = s.indexOf('-');
710 	if (dash == -1 || dash != s.length - 4)
711 		return false;
712 	if (s[dash + 1] != 'W' || !s[dash + 2].isDigit || !s[dash + 3].isDigit)
713 		return false;
714 	auto y = s[0 .. dash];
715 	auto w = s[dash + 2 .. $].to!ubyte;
716 	if (w < 1 || w > 52)
717 		return false;
718 	try
719 	{
720 		auto yi = y.to!uint;
721 		if (yi < 1 || yi > 200_000)
722 			return false;
723 		return true;
724 	}
725 	catch (ConvException)
726 	{
727 		return false;
728 	}
729 }
730 
731 /// Validates s == pattern "0{4,6}-00"
732 bool validateMonthString(string s) @safe
733 {
734 	if (s.length < 7 || s.length > 9)
735 		return false;
736 	auto dash = s.indexOf('-');
737 	if (dash == -1 || dash != s.length - 3)
738 		return false;
739 	if (!s[dash + 1].isDigit || !s[dash + 2].isDigit)
740 		return false;
741 	auto y = s[0 .. dash];
742 	auto m = s[dash + 1 .. $].to!ubyte;
743 	if (m < 1 || m > 12)
744 		return false;
745 	try
746 	{
747 		auto yi = y.to!uint;
748 		if (yi < 1 || yi > 200_000)
749 			return false;
750 		return true;
751 	}
752 	catch (ConvException)
753 	{
754 		return false;
755 	}
756 }
757 
758 /// Validates s == pattern "0000-00-00T00:00"
759 bool validateDatetimeLocalString(string s) @safe
760 {
761 	if (s.length != 16)
762 		return false;
763 	if (!s[0].isDigit || !s[1].isDigit || !s[2].isDigit || !s[3].isDigit
764 			|| s[4] != '-' || !s[5].isDigit || !s[6].isDigit || s[7] != '-'
765 			|| !s[8].isDigit || !s[9].isDigit || s[10] != 'T' || !s[11].isDigit
766 			|| !s[12].isDigit || s[13] != ':' || !s[14].isDigit || !s[15].isDigit)
767 		return false;
768 	try
769 	{
770 		return SysTime.fromISOExtString(s ~ ":00") != SysTime.init;
771 	}
772 	catch (DateTimeException)
773 	{
774 		return false;
775 	}
776 }
777 
778 /// Validates s == pattern "0000-00-00"
779 bool validateDateString(string s) @safe
780 {
781 	if (s.length != 10)
782 		return false;
783 	if (!s[0].isDigit || !s[1].isDigit || !s[2].isDigit || !s[3].isDigit
784 			|| s[4] != '-' || !s[5].isDigit || !s[6].isDigit || s[7] != '-'
785 			|| !s[8].isDigit || !s[9].isDigit)
786 		return false;
787 	try
788 	{
789 		return Date(s[0 .. 4].to!int, s[5 .. 7].to!int, s[8 .. 10].to!int) != Date.init;
790 	}
791 	catch (DateTimeException)
792 	{
793 		return false;
794 	}
795 }
796 
797 /// Validates s == pattern "#xxxxxx"
798 bool validateColorString(string s) @safe
799 {
800 	if (s.length != 7)
801 		return false;
802 	if (s[0] != '#' || !s[1].isHexDigit || !s[2].isHexDigit || !s[3].isHexDigit
803 			|| !s[4].isHexDigit || !s[5].isHexDigit || !s[6].isHexDigit)
804 		return false;
805 	return true;
806 }
807 
808 /// Converts correctBookISBN_number to "Correct Book ISBN Number"
809 string makeHumanName(string identifier) @safe
810 {
811 	string humanName;
812 	bool wasUpper = true;
813 	bool wasSpace = true;
814 	foreach (c; identifier)
815 	{
816 		if (c >= 'A' && c <= 'Z')
817 		{
818 			if (!wasUpper)
819 			{
820 				wasUpper = true;
821 				humanName ~= ' ';
822 			}
823 		}
824 		else
825 			wasUpper = false;
826 		if (c == '_')
827 		{
828 			wasSpace = true;
829 			humanName ~= ' ';
830 		}
831 		else if (wasSpace)
832 		{
833 			humanName ~= [c].toUpper;
834 			wasSpace = false;
835 		}
836 		else
837 			humanName ~= c;
838 	}
839 	return humanName.strip;
840 }
841 
842 /// Controls how the input HTML is generated
843 struct DefaultInputGenerator
844 {
845 @safe:
846 	private static string errorString(bool success)
847 	{
848 		if (success)
849 			return "";
850 		else
851 			return `<span class="error">Please fill out this field correctly.</span>`;
852 	}
853 
854 	/// Called for single line input types
855 	static string textfield(string name, string type, string value, string raw, bool success)
856 	{
857 		const className = success ? "" : ` class="error"`;
858 		return `<label` ~ className ~ `><span>%s</span><input type="%s" value="%s"%s/></label>`.format(name.encode,
859 				type.encode, value.encode, raw) ~ errorString(success);
860 	}
861 
862 	/// Called for textareas
863 	static string textarea(string name, string value, string raw, bool success)
864 	{
865 		const className = success ? "" : ` class="error"`;
866 		return `<label` ~ className ~ `><span>%s</span><textarea%s>%s</textarea></label>`.format(name.encode,
867 				raw, value.encode) ~ errorString(success);
868 	}
869 
870 	/// Called for boolean values
871 	static string checkbox(string name, bool checked, string raw, bool success)
872 	{
873 		const className = success ? "" : " error";
874 		return `<label class="checkbox` ~ className ~ `"><input type="checkbox" %s%s/><span>%s</span></label>`.format(
875 				checked ? "checked" : "", raw, name.encode) ~ errorString(success);
876 	}
877 
878 	/// Called for enums disabled as select (you need to iterate over the enum members)
879 	static string dropdownList(Enum, translations...)(string name, Enum value,
880 			string raw, bool success)
881 	{
882 		const className = success ? "" : " error";
883 		string ret = `<label class="select` ~ className ~ `"><span>` ~ name.encode
884 			~ `</span><select` ~ raw ~ `>`;
885 		foreach (member; __traits(allMembers, Enum))
886 			ret ~= `<option value="` ~ (cast(OriginalType!Enum) __traits(getMember,
887 					Enum, member)).to!string.encode ~ `"` ~ (value == __traits(getMember,
888 					Enum, member) ? " selected" : "") ~ `>` ~ __traits(getMember, Enum, member)
889 				.translateEnum!(Enum, translations)(member.makeHumanName) ~ `</option>`;
890 		return ret ~ "</select></label>" ~ errorString(success);
891 	}
892 
893 	/// Called for enums displayed as list of radio boxes (you need to iterate over the enum members)
894 	static string optionList(Enum, translations...)(string name, Enum value, string raw, bool success)
895 	{
896 		const className = success ? "" : " error";
897 		string ret = `<label class="checkbox options` ~ className ~ `"><span>` ~ name.encode ~ "</span>";
898 		foreach (member; __traits(allMembers, Enum))
899 			ret ~= checkbox(__traits(getMember, Enum, member).translateEnum!(Enum,
900 					translations)(member.makeHumanName), value == __traits(getMember, Enum, member),
901 					raw ~ ` value="` ~ (cast(OriginalType!Enum) __traits(getMember, Enum,
902 						member)).to!string.encode ~ `"`, true).replace(`type="checkbox"`, `type="radio"`);
903 		return ret ~ `</label>` ~ errorString(success);
904 	}
905 
906 	/// Called for BitFlags displayed as list of checkboxes.
907 	static string checkboxList(Enum, translations...)(string name,
908 			BitFlags!Enum value, string raw, bool success)
909 	{
910 		const className = success ? "" : " error";
911 		string ret = `<label class="checkbox flags` ~ className ~ `"><span>` ~ name.encode ~ "</span>";
912 		foreach (member; __traits(allMembers, Enum))
913 			ret ~= checkbox(__traits(getMember, Enum, member).translateEnum!(Enum,
914 					translations)(member.makeHumanName), !!(value & __traits(getMember, Enum,
915 					member)), raw ~ ` value="` ~ (cast(OriginalType!Enum) __traits(getMember,
916 					Enum, member)).to!string.encode ~ `"`, true);
917 		return ret ~ `</label>` ~ errorString(success);
918 	}
919 }
920 
921 /// Adds type="email" to string types
922 enum emailSetting;
923 /// Adds type="url" to string types
924 enum urlSetting;
925 /// Makes string types textareas
926 enum multilineSetting;
927 /// Adds type="range" to numeric types
928 enum rangeSetting;
929 /// Adds type="time" to string types
930 enum timeSetting;
931 /// Adds type="week" to string types
932 enum weekSetting;
933 /// Adds type="month" to string types
934 enum monthSetting;
935 /// Adds type="datetime-local" to string types
936 enum datetimeLocalSetting;
937 /// Adds type="date" to string types
938 enum dateSetting;
939 /// Adds type="color" to string types
940 enum colorSetting;
941 /// Adds disabled to any input
942 enum disabledSetting;
943 /// Adds required to any input
944 enum requiredSetting;
945 /// Disables automatic JS saving when changing the input
946 enum nonAutomaticSetting;
947 /// Changes a dropdown to a radio button list
948 enum optionsSetting;
949 
950 /// Changes the min="" attribute for numerical values
951 struct settingMin
952 {
953 	///
954 	double min;
955 }
956 
957 /// Changes the max="" attribute for numerical values
958 struct settingMax
959 {
960 	///
961 	double max;
962 }
963 
964 /// Changes the step="" attribute for numerical values
965 struct settingStep
966 {
967 	///
968 	double step;
969 }
970 
971 /// Changes the min="" and max="" attribute for numerical values
972 struct settingRange
973 {
974 	///
975 	double min, max;
976 }
977 
978 /// Changes the minlength="" and maxlength="" attribute for string values
979 struct settingLength
980 {
981 	///
982 	int max, min;
983 }
984 
985 /// Changes the pattern="regex" attribute
986 struct settingPattern
987 {
988 	///
989 	string regex;
990 }
991 
992 /// Changes the title="" attribute for custom error messages & tooltips
993 struct settingTitle
994 {
995 	///
996 	string title;
997 }
998 
999 /// Overrides the label of the input
1000 struct settingLabel
1001 {
1002 	///
1003 	string label;
1004 }
1005 
1006 /// Changes the label if the current language (using a WebInterface translation context) matches the given one.
1007 /// You need at least vibe-d v0.8.1-alpha.3 to use this UDA.
1008 struct settingTranslation
1009 {
1010 	///
1011 	string language;
1012 	///
1013 	string label;
1014 }
1015 
1016 /// Relables all enum member names for a language. Give `null` as first argument to change the default language
1017 struct enumTranslation
1018 {
1019 	///
1020 	string language;
1021 	///
1022 	string[] translations;
1023 }
1024 
1025 string translateEnum(T, translations...)(T value, string fallback) @safe
1026 		if (is(T == enum))
1027 {
1028 	static if (translations.length)
1029 	{
1030 		static if (is(typeof(language) == string))
1031 			auto lang = (() @trusted => language)();
1032 		enum NumEnumMembers = [EnumMembers!T].length;
1033 		foreach (i, other; EnumMembers!T)
1034 		{
1035 			if (other == value)
1036 			{
1037 				string ret = null;
1038 				foreach (translation; translations)
1039 				{
1040 					static assert(translation.translations.length == NumEnumMembers,
1041 							"Translation missing some values. Set them to null to skip");
1042 					if (translation.language is null && ret is null)
1043 						ret = translation.translations[i];
1044 					else static if (is(typeof(language) == string))
1045 					{
1046 						if (translation.language == lang)
1047 							ret = translation.translations[i];
1048 					}
1049 				}
1050 				return ret is null ? fallback : ret;
1051 			}
1052 		}
1053 	}
1054 	else static if (__traits(compiles, __traits(getAttributes,
1055 			__traits(getMember, T, __traits(allMembers, T)[0]))))
1056 	{
1057 		foreach (i, other; __traits(allMembers, T))
1058 		{
1059 			if (__traits(getMember, T, other) == value)
1060 			{
1061 				static if (is(typeof(language) == string))
1062 					auto lang = (() @trusted => language)();
1063 				string ret = null;
1064 				foreach (attr; __traits(getAttributes, __traits(getMember, T, other)))
1065 				{
1066 					static if (is(typeof(attr) == settingTranslation))
1067 					{
1068 						if (attr.language is null && ret is null)
1069 							ret = attr.label;
1070 						else static if (is(typeof(language) == string))
1071 						{
1072 							if (attr.language == lang)
1073 								ret = attr.label;
1074 						}
1075 					}
1076 				}
1077 				return ret is null ? fallback : ret;
1078 			}
1079 		}
1080 	}
1081 	return fallback;
1082 }
1083 
1084 /// Contains a updateSetting(input) function which automatically sends changes to the server.
1085 enum DefaultJavascriptCode = q{<script id="_setting_script_">
1086 	var timeouts = {};
1087 	function updateSetting(input) {
1088 		clearTimeout(timeouts[input]);
1089 		timeouts[input] = setTimeout(function() {
1090 			var form = input;
1091 			while (form && form.tagName != "FORM")
1092 				form = form.parentElement;
1093 			var submit = form.querySelector ? form.querySelector("input[type=submit]") : undefined;
1094 			if (submit)
1095 				submit.disabled = false;
1096 			name = input.name;
1097 			function attachError(elem, content) {
1098 				var label = elem;
1099 				while (label && label.tagName != "LABEL")
1100 					label = label.parentElement;
1101 				if (label)
1102 					label.classList.add("error");
1103 				var err = document.createElement("span");
1104 				err.className = "error";
1105 				err.textContent = content;
1106 				err.style.padding = "4px";
1107 				elem.parentElement.insertBefore(err, elem.nextSibling);
1108 				setTimeout(function() { err.parentElement.removeChild(err); }, 2500);
1109 			}
1110 			var label = input;
1111 			while (label && label.tagName != "LABEL")
1112 				label = label.parentElement;
1113 			if (label)
1114 				label.classList.remove("error");
1115 			var isFlags = false;
1116 			var flagLabel = label;
1117 			while (flagLabel) {
1118 				if (flagLabel.classList.contains("flags")) {
1119 					isFlags = true;
1120 					break;
1121 				}
1122 				flagLabel = flagLabel.parentElement;
1123 			}
1124 			var valid = input.checkValidity ? input.checkValidity() : true;
1125 			if (!valid) {
1126 				attachError(input, input.title || "Please fill out this input correctly.");
1127 				return;
1128 			}
1129 			var stillRequesting = true;
1130 			setTimeout(function () {
1131 				if (stillRequesting)
1132 					input.disabled = true;
1133 			}, 100);
1134 			var xhr = new XMLHttpRequest();
1135 			var method = "{method}";
1136 			var action = "{action}";
1137 			var query = "_field=" + encodeURIComponent(name);
1138 			if (input.type != "checkbox" || input.checked)
1139 				query += '&' + encodeURIComponent(name) + '=' + encodeURIComponent(input.value);
1140 			else if (isFlags)
1141 				query += '&' + encodeURIComponent(name) + "=!" + encodeURIComponent(input.value);
1142 			if (method != "POST")
1143 				action += query;
1144 			xhr.onload = function () {
1145 				if (xhr.status != 200 && xhr.status != 204)
1146 					attachError(input, input.title || "Please fill out this field correctly.");
1147 				else {
1148 					submit.value = "Saved!";
1149 					setTimeout(function() { submit.value = "Save"; }, 3000);
1150 				}
1151 				stillRequesting = false;
1152 				input.disabled = false;
1153 			};
1154 			xhr.onerror = function () {
1155 				stillRequesting = false;
1156 				input.disabled = false;
1157 				submit.disabled = false;
1158 			};
1159 			xhr.open(method, action);
1160 			xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
1161 			if (method == "POST")
1162 				xhr.send(query);
1163 			else
1164 				xhr.send();
1165 			submit.disabled = true;
1166 		}, 50);
1167 	}
1168 	function unlockForm(input) {
1169 		var form = input;
1170 		while (form && form.tagName != "FORM")
1171 			form = form.parentElement;
1172 		form.querySelector("input[type=submit]").disabled = false;
1173 	}
1174 	(document.currentScript || document.getElementById("_setting_script_")).previousSibling.querySelector("input[type=submit]").disabled = true;
1175 </script>};