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 
9 import std.algorithm;
10 import std.ascii;
11 import std.conv;
12 import std.datetime;
13 import std.format;
14 import std.math;
15 import std.meta;
16 import std.regex;
17 import std.string;
18 import std.traits;
19 import std.typecons;
20 import std.xml : encode;
21 
22 ///
23 unittest
24 {
25 	enum FavoriteFood
26 	{
27 		fish,
28 		meat,
29 		vegetables,
30 		fruit
31 	}
32 
33 	//dfmt off
34 	enum Country
35 	{
36 		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,
37 		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,
38 		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,
39 		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,
40 		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,
41 		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,
42 		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,
43 		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,
44 		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,
45 		UY, UZ, VU, VA, VE, VN, WF, EH, YE, ZM, ZW
46 	}
47 	//dfmt on
48 
49 	enum SocialMedia
50 	{
51 		twitter = 1 << 0,
52 		facebook = 1 << 1,
53 		myspace = 1 << 2,
54 	}
55 
56 	struct Config
57 	{
58 		@requiredSetting // Must be filled out
59 		@nonAutomaticSetting // Don't auto sync when typing
60 		@emailSetting string userEmail;
61 		bool married;
62 		@urlSetting @settingLength(64) string resourceURI;
63 		// OR
64 		@settingLength(64) URL myWebsite;
65 		@multilineSetting @settingLength(1000) string aboutMe;
66 		@rangeSetting @settingRange(0, 10) int rating;
67 		@timeSetting string favoriteTimeOfDay;
68 		// OR
69 		TimeOfDay leastFavoriteTimeOfDay;
70 		@weekSetting string bestWeekYouHad;
71 		@monthSetting string firstMonthOfWork;
72 		// Timezone-less
73 		@datetimeLocalSetting string birthdayTimeAndDate;
74 		// OR
75 		DateTime myChildsBirthdayTimeAndDate;
76 		@dateSetting string myMothersBirthday;
77 		// OR
78 		Date myFathersBirthday;
79 		@colorSetting string favoriteColor;
80 		@disabledSetting string someInformation = "Just a hint, nothing changable";
81 		Country favoriteCountry;
82 		@optionsSetting FavoriteFood favoriteFood;
83 		BitFlags!SocialMedia usedSocialMedia;
84 		@settingTitle("If you don't have any you can still say 1 because you have yourself.")  // Hover & validation text
85 		@settingMin(1) int numberOfFriends;
86 		@settingRange(0, 100) @settingStep(0.1) double englishSkillLevelPercentage;
87 		@settingMax(10) ubyte orderedProductCount;
88 		@settingLabel("Accept terms of service") @requiredSetting bool acceptTOS;
89 		@settingPattern(`(ISBN\s+)?\d{3}-\d-\d{5}-\d{3}-\d`) string favoriteBookISBN;
90 	}
91 
92 	import vibe.vibe;
93 
94 	auto router = new URLRouter;
95 	router.get("/style.css", serveStaticFile("styles/material.css"));
96 	router.get("/", staticRedirect("/settings"));
97 
98 	enum html = `<html>
99 		<head>
100 			<title>Settings</title>
101 			<link rel="stylesheet" href="/style.css"/>
102 			<style>
103 				body,html{background:#efefef;color:rgba(0,0,0,0.87);font-family:Roboto,"Segoe UI",sans-serif;}
104 				.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;}
105 			</style>
106 		</head>
107 		<body>
108 			<div class="settings">
109 				<h2>Settings</h2>
110 				%s
111 			</div>
112 		</body>
113 	</html>`;
114 
115 	Config settingsInstance; // You might fetch & save this per user, web-config only changes the struct
116 	router.get("/settings", delegate(scope req, scope res) @safe{
117 		string settings = renderSettings(settingsInstance);
118 		res.writeBody(html.format(settings), "text/html");
119 	});
120 	router.post("/settings", delegate(scope req, scope res) @safe{
121 		// no-js & nonautomatic setting route
122 		auto ret = req.processSettings(settingsInstance);
123 		string settings = renderSettings(settingsInstance, ret);
124 		if (ret)
125 		{
126 			// Something changed, you can save here
127 		}
128 		res.writeBody(html.format(settings), "text/html");
129 	});
130 	router.post("/api/setting", delegate(scope req, scope res) @safe{
131 		// js route called for each individual setting
132 		if (req.processSettings(settingsInstance))
133 		{
134 			// Save settings
135 			res.writeBody("", 204); // Send 200 or 204
136 		}
137 		else
138 			res.writeBody("", HTTPStatus.badRequest);
139 	});
140 	listenHTTP(new HTTPServerSettings, router);
141 	runApplication();
142 }
143 
144 /// Generates a HTML form for a configuration struct `T` with automatic instant updates using AJAX.
145 /// The fields can be annotated with the various UDAs found in this module. (setting enums + structs) $(BR)
146 /// Supported types: `enum` (drop down lists or radio box lists), `std.typecons.BitFlags` (checkbox lists),
147 /// `bool` (checkbox), string types (text, email, url, etc.), numeric types (number), `std.datetime.DateTime`
148 /// (datetime-local), `std.datetime.Date` (date), `std.datetime.TimeOfDay` (time), `vibe.inet.URL` (url)
149 string renderSettings(T, InputGenerator = DefaultInputGenerator,
150 		alias javascript = DefaultJavascriptCode)(T value, string formAttributes = "",
151 		string action = "/settings", string method = "POST", string jsAction = "/api/setting") @safe
152 {
153 	return renderSettings!(T, InputGenerator, javascript)(value, ulong.max,
154 			formAttributes, action, method, jsAction);
155 }
156 
157 /// ditto
158 string renderSettings(T, InputGenerator = DefaultInputGenerator,
159 		alias javascript = DefaultJavascriptCode)(T value, ulong set, string formAttributes = "",
160 		string action = "/settings", string method = "POST", string jsAction = "/api/setting") @safe
161 {
162 	method = method.toUpper;
163 	string[] settings;
164 	foreach (i, member; __traits(allMembers, T))
165 	{
166 		bool success = (set & (1 << cast(ulong) i)) != 0;
167 		settings ~= renderSetting!(InputGenerator, member)(value, success);
168 	}
169 	return `<form action="%s" method="%s"%s>%s<input type="submit" value="Save"/></form>`.format(
170 			action.encode, method.encode,
171 			formAttributes.length
172 			? " " ~ formAttributes : "", settings.join()) ~ DefaultJavascriptCode.replace(
173 			"{action}", jsAction).replace("{method}", method);
174 }
175 
176 /// Generates a single input
177 string renderSetting(InputGenerator = DefaultInputGenerator, string name, Config)(
178 		ref Config config, bool success = true) @safe
179 {
180 	alias Member = AliasSeq!(__traits(getMember, config, name));
181 	auto value = __traits(getMember, config, name);
182 	alias T = Unqual!(typeof(value));
183 	enum isEmail = hasUDA!(Member[0], emailSetting);
184 	enum isUrl = hasUDA!(Member[0], urlSetting);
185 	enum isMultiline = hasUDA!(Member[0], multilineSetting);
186 	enum isRange = hasUDA!(Member[0], rangeSetting);
187 	enum isTime = hasUDA!(Member[0], timeSetting) || is(T == TimeOfDay);
188 	enum isWeek = hasUDA!(Member[0], weekSetting);
189 	enum isMonth = hasUDA!(Member[0], monthSetting);
190 	enum isDatetimeLocal = hasUDA!(Member[0], datetimeLocalSetting) || is(T == DateTime);
191 	enum isDate = hasUDA!(Member[0], dateSetting) || is(T == Date);
192 	enum isColor = hasUDA!(Member[0], colorSetting);
193 	enum isDisabled = hasUDA!(Member[0], disabledSetting);
194 	enum isRequired = hasUDA!(Member[0], requiredSetting);
195 	enum isNoJS = hasUDA!(Member[0], nonAutomaticSetting);
196 	enum isOptions = hasUDA!(Member[0], optionsSetting);
197 	enum mins = getUDAs!(Member[0], settingMin);
198 	enum maxs = getUDAs!(Member[0], settingMax);
199 	enum ranges = getUDAs!(Member[0], settingRange);
200 	enum lengths = getUDAs!(Member[0], settingLength);
201 	enum steps = getUDAs!(Member[0], settingStep);
202 	enum patterns = getUDAs!(Member[0], settingPattern);
203 	enum titles = getUDAs!(Member[0], settingTitle);
204 	enum labels = getUDAs!(Member[0], settingLabel);
205 	static if (labels.length)
206 		string uiName = labels[0].label;
207 	else
208 		string uiName = name.makeHumanName;
209 	string raw = ` name="` ~ name ~ `"`;
210 	static if (isDisabled)
211 		raw ~= " disabled";
212 	else static if (!isNoJS)
213 		raw ~= ` onchange="updateSetting(this)"`;
214 	else
215 		raw ~= ` onchange="unlockForm(this)"`;
216 	static if (lengths.length)
217 	{
218 		auto minlength = lengths[0].min;
219 		auto maxlength = lengths[0].max;
220 		if (minlength > 0)
221 			raw ~= " minlength=\"" ~ minlength.to!string ~ "\"";
222 		if (maxlength > 0)
223 			raw ~= " maxlength=\"" ~ maxlength.to!string ~ "\"";
224 	}
225 	static if (patterns.length)
226 		raw ~= " pattern=\"" ~ patterns[0].regex.encode ~ "\"";
227 	else static if (isDatetimeLocal) // if browser doesn't support datetime-local
228 		raw ~= ` pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}"`;
229 	else static if (isTime) // if browser doesn't support time
230 		raw ~= ` pattern="[0-9]{2}:[0-9]{2}"`;
231 	else static if (isDate) // if browser doesn't support date
232 		raw ~= ` pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}"`;
233 	static if (titles.length)
234 		raw ~= " title=\"" ~ titles[0].title.encode ~ "\"";
235 	static if (isRequired)
236 		raw ~= " required";
237 	static if (is(T == enum))
238 	{
239 		static if (isOptions)
240 			return InputGenerator.optionList!T(uiName, value, raw, success);
241 		else
242 			return InputGenerator.dropdownList!T(uiName, value, raw, success);
243 	}
244 	else static if (is(T == BitFlags!Enum, Enum))
245 		return InputGenerator.checkboxList!Enum(uiName, value, raw, success);
246 	else static if (is(T == bool))
247 		return InputGenerator.checkbox(uiName, value, raw, success);
248 	else static if (isSomeString!T)
249 	{
250 		static if (
251 			isEmail + isUrl + isMultiline + isTime + isWeek + isMonth
252 				+ isDatetimeLocal + isDate + isColor > 1)
253 			static assert(false, "string setting " ~ name ~ " has multiple type related attributes");
254 		static if (isMultiline)
255 			return InputGenerator.textarea(uiName, value.to!string, raw, success);
256 		else
257 			return InputGenerator.textfield(uiName, isEmail ? "email" : isUrl ? "url" : isTime ? "time" : isWeek ? "week"
258 					: isMonth ? "month" : isDatetimeLocal ? "datetime-local" : isDate ? "date"
259 					: isColor ? "color" : "text", value.to!string, raw, success);
260 	}
261 	else static if (is(T == DateTime))
262 		return InputGenerator.textfield(uiName, "datetime-local",
263 				value.toISOExtString[0 .. 16], raw, success);
264 	else static if (is(T == Date))
265 		return InputGenerator.textfield(uiName, "date", value.toISOExtString, raw, success);
266 	else static if (is(T == TimeOfDay))
267 		return InputGenerator.textfield(uiName, "time", value.toISOExtString[0 .. 5], raw, success);
268 	else static if (is(T == URL))
269 		return InputGenerator.textfield(uiName, "url", value.toString, raw, success);
270 	else static if (isNumeric!T)
271 	{
272 		double min, max;
273 		static if (mins.length)
274 			min = mins[0].min;
275 		static if (maxs.length)
276 			max = maxs[0].max;
277 		static if (ranges.length)
278 		{
279 			min = ranges[0].min;
280 			max = ranges[0].max;
281 		}
282 		if (min == min) // !isNaN
283 			raw ~= " min=\"" ~ min.to!string ~ "\"";
284 		if (max == max) // !isNaN
285 			raw ~= " max=\"" ~ max.to!string ~ "\"";
286 		static if (steps.length)
287 			raw ~= " step=\"" ~ steps[0].step.to!string ~ "\"";
288 		return InputGenerator.textfield(uiName, isRange ? "range" : "number",
289 				value.to!string, raw, success);
290 	}
291 	else
292 		static assert(false, "No setting generator for type " ~ T.stringof);
293 }
294 
295 /**
296 	Function processing user input and validating for correctness. $(BR)$(BR)
297 	The following validations are done: $(BR)
298 	If the setting is a  `disabledSetting`, it will always skip this field. $(BR)
299 	If the setting has a `settingPattern`, it will validate the raw value (no matter what type) against this regex. $(BR)
300 	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)
301 	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)
302 	If the setting is an enum the value will be checked if it is contained inside the enum. $(BR)
303 	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)
304 	Integral numbers will always be checked if finite & if no range is given they will be clamped. $(BR)$(BR)
305 	Attributes for strings: $(BR)
306 		`emailSetting` is validated using `std.net.isemail.isEmail(CheckDns.no, EmailStatusCode.any)` $(BR)
307 		`urlSetting` is validated using `vibe.inet.url.URL` $(BR)
308 		`timeSetting` is checked against pattern `00:00` + checking if 0 <= hour < 24 && 0 <= minute < 60 $(BR)
309 		`weekSetting` is checked against pattern `0{4,6}-W00` + checking if 1 <= year <= 200000 && 1 <= week <= 52 $(BR)
310 		`monthSetting` is checked against pattern `0{4,6}-00` + checking if 1 <= year <= 200000 && 1 <= month <= 12 $(BR)
311 		`datetimeLocalSetting` is checked against pattern `0000-00-00T00:00` + passing into `std.datetime.SysTime.fromISOExtString`` $(BR)
312 		`dateSetting` is checked against pattern `0000-00-00` + checking the date using `std.datetime.Date` $(BR)
313 		`colorSetting` is checked against pattern `#FFFFFF` $(BR)
314 	Values using these attributes can be used without the need to validate the input.
315 	Params:
316 		strict = if false, values will be fixed to conform to the input instead of discarding them.
317 		Currently only fixing numbers and string lengths and new lines in single line strings is implemented.
318 	Returns: a bit array where each bit represents an input and is set to 1 if valid
319 	*/
320 ulong processSettings(T)(scope HTTPServerRequest req, ref T config,
321 		bool strict = false, bool post = true) @safe
322 {
323 	ulong valid;
324 	auto field = (post ? req.form : req.query).get("_field", "");
325 	foreach (i, member; __traits(allMembers, T))
326 	{
327 		if (field.length && field != member)
328 			continue;
329 		valid |= req.processSetting!member(config, strict, post) << cast(ulong) i;
330 	}
331 	return valid;
332 }
333 
334 /// ditto
335 bool processSetting(string name, Config)(HTTPServerRequest req, ref Config config,
336 		bool strict = false, bool post = true) @safe
337 {
338 	alias Member = AliasSeq!(__traits(getMember, config, name));
339 	auto member = __traits(getMember, config, name);
340 	alias T = typeof(member);
341 	enum isEmail = hasUDA!(Member[0], emailSetting);
342 	enum isUrl = hasUDA!(Member[0], urlSetting);
343 	enum isMultiline = hasUDA!(Member[0], multilineSetting);
344 	enum isRange = hasUDA!(Member[0], rangeSetting);
345 	enum isTime = hasUDA!(Member[0], timeSetting);
346 	enum isWeek = hasUDA!(Member[0], weekSetting);
347 	enum isMonth = hasUDA!(Member[0], monthSetting);
348 	enum isDatetimeLocal = hasUDA!(Member[0], datetimeLocalSetting);
349 	enum isDate = hasUDA!(Member[0], dateSetting);
350 	enum isColor = hasUDA!(Member[0], colorSetting);
351 	enum isDisabled = hasUDA!(Member[0], disabledSetting);
352 	enum isRequired = hasUDA!(Member[0], requiredSetting);
353 	enum mins = getUDAs!(Member[0], settingMin);
354 	enum maxs = getUDAs!(Member[0], settingMax);
355 	enum ranges = getUDAs!(Member[0], settingRange);
356 	enum lengths = getUDAs!(Member[0], settingLength);
357 	enum steps = getUDAs!(Member[0], settingStep);
358 	enum patterns = getUDAs!(Member[0], settingPattern);
359 	static if (isDisabled)
360 		return true;
361 	else
362 	{
363 		int minlength = int.min, maxlength = int.max;
364 		static if (lengths.length)
365 		{
366 			minlength = lengths[0].min;
367 			maxlength = lengths[0].max;
368 		}
369 		T oldval = member;
370 		T newval = oldval;
371 		FormFields form = post ? req.form : req.query;
372 		auto allvals = form.getAll(name);
373 		bool isJS = form.get("_field", "").length != 0;
374 		string rawval = allvals.length ? allvals[0] : "";
375 		static if (patterns.length)
376 			if (!matchFirst(rawval, ctRegex!(patterns[0].regex)))
377 				return false;
378 		static if (isRequired)
379 			if (!allvals.length)
380 				return false;
381 		if (minlength != int.min && rawval.length < minlength)
382 			return false;
383 		if (maxlength != int.max && rawval.length > maxlength)
384 		{
385 			if (strict)
386 				return false;
387 			else
388 				rawval.length = maxlength;
389 		}
390 		static if (is(T == enum))
391 		{
392 			try
393 			{
394 				newval = cast(T) rawval.to!(OriginalType!T);
395 				bool exists = false;
396 				foreach (val; EnumMembers!T)
397 					if (val == newval)
398 					{
399 						exists = true;
400 						break;
401 					}
402 				if (!exists)
403 					return false;
404 			}
405 			catch (ConvException)
406 			{
407 				return false;
408 			}
409 		}
410 		else static if (is(T : BitFlags!Enum, Enum))
411 		{
412 			try
413 			{
414 				if (!rawval.length)
415 					return false;
416 				if (isJS)
417 				{
418 					bool negate = rawval[0] == '!';
419 					if (negate)
420 						rawval = rawval[1 .. $];
421 					auto enumType = cast(Enum) rawval.to!(OriginalType!Enum);
422 					bool exists = false;
423 					foreach (val; EnumMembers!Enum)
424 						if (val == enumType)
425 						{
426 							exists = true;
427 							break;
428 						}
429 					if (!exists)
430 						return false;
431 					if (negate)
432 						newval = oldval & ~T(enumType);
433 					else
434 						newval = oldval | enumType;
435 				}
436 				else
437 				{
438 					newval = T.init;
439 					foreach (rawval1; allvals)
440 					{
441 						auto enumType = cast(Enum) rawval1.to!(OriginalType!Enum);
442 						bool exists = false;
443 						foreach (val; EnumMembers!Enum)
444 							if (val == enumType)
445 							{
446 								exists = true;
447 								break;
448 							}
449 						if (!exists)
450 							return false;
451 						newval |= enumType;
452 					}
453 				}
454 			}
455 			catch (ConvException)
456 			{
457 				return false;
458 			}
459 		}
460 		else static if (is(T == bool))
461 			newval = allvals.length > 0;
462 		else static if (isSomeString!T)
463 		{
464 			static if (
465 				isEmail + isUrl + isMultiline + isTime + isWeek + isMonth
466 					+ isDatetimeLocal + isDate + isColor > 1)
467 				static assert(false,
468 						"string setting " ~ name ~ " has multiple type related attributes");
469 			static if (isMultiline)
470 				newval = rawval;
471 			else if (rawval.length)
472 			{
473 				if (strict && rawval.indexOfAny("\r\n") != -1)
474 					return false;
475 				else
476 					rawval = rawval.tr("\r\n", "  ");
477 				static if (isEmail)
478 				{
479 					rawval = rawval.strip;
480 					import std.net.isemail;
481 
482 					if ((()@trusted => !rawval.isEmail(CheckDns.no, EmailStatusCode.any))())
483 						return false;
484 					newval = rawval;
485 				}
486 				else static if (isUrl)
487 				{
488 					try
489 					{
490 						newval = URL(rawval.strip).toString;
491 					}
492 					catch (Exception)
493 					{
494 						return false;
495 					}
496 				}
497 				else static if (isTime)
498 				{
499 					rawval = rawval.strip;
500 					if (!validateTimeString(rawval))
501 						return false;
502 					newval = rawval;
503 				}
504 				else static if (isWeek)
505 				{
506 					rawval = rawval.strip;
507 					if (!validateWeekString(rawval))
508 						return false;
509 					newval = rawval;
510 				}
511 				else static if (isMonth)
512 				{
513 					rawval = rawval.strip;
514 					if (!validateMonthString(rawval))
515 						return false;
516 					newval = rawval;
517 				}
518 				else static if (isDatetimeLocal)
519 				{
520 					rawval = rawval.strip;
521 					if (!validateDatetimeLocalString(rawval))
522 						return false;
523 					newval = rawval;
524 				}
525 				else static if (isDate)
526 				{
527 					rawval = rawval.strip;
528 					if (!validateDateString(rawval))
529 						return false;
530 					newval = rawval;
531 				}
532 				else static if (isColor)
533 				{
534 					rawval = rawval.strip;
535 					if (!validateColorString(rawval))
536 						return false;
537 					newval = rawval;
538 				}
539 				else
540 					newval = rawval;
541 			}
542 			else
543 			{
544 				newval = "";
545 			}
546 		}
547 		else static if (is(T == DateTime))
548 		{
549 			rawval = rawval.strip;
550 			if (!validateDatetimeLocalString(rawval))
551 				return false;
552 			newval = DateTime.fromISOExtString(rawval ~ ":00");
553 		}
554 		else static if (is(T == Date))
555 		{
556 			rawval = rawval.strip;
557 			if (!validateDateString(rawval))
558 				return false;
559 			newval = Date.fromISOExtString(rawval);
560 		}
561 		else static if (is(T == TimeOfDay))
562 		{
563 			rawval = rawval.strip;
564 			if (!validateTimeString(rawval))
565 				return false;
566 			newval = TimeOfDay.fromISOExtString(rawval ~ ":00");
567 		}
568 		else static if (is(T == URL))
569 		{
570 			try
571 			{
572 				newval = URL(rawval.strip);
573 			}
574 			catch (Exception)
575 			{
576 				return false;
577 			}
578 		}
579 		else static if (isNumeric!T)
580 		{
581 			double min, max;
582 			static if (isIntegral!T)
583 			{
584 				min = T.min;
585 				max = T.max;
586 			}
587 			static if (mins.length)
588 				min = mins[0].min;
589 			static if (maxs.length)
590 				max = maxs[0].max;
591 			static if (ranges.length)
592 			{
593 				min = ranges[0].min;
594 				max = ranges[0].max;
595 			}
596 			double step = 1;
597 			static if (steps.length)
598 				step = steps[0].step;
599 			try
600 			{
601 				double val = rawval.to!double;
602 				if (min == min && val < min)
603 				{
604 					if (strict)
605 						return false;
606 					else
607 						val = min;
608 				}
609 				if (max == max && val > max)
610 				{
611 					if (strict)
612 						return false;
613 					else
614 						val = max;
615 				}
616 				val = floor(val / step) * step;
617 				bool isFinite = val == val && val != double.infinity && val != -double.infinity;
618 				static if (isRange && isFloatingPoint!T)
619 				{
620 					if (!isFinite)
621 						return false;
622 				}
623 				static if (!isFloatingPoint!T)
624 					if (!isFinite)
625 						return false;
626 				newval = cast(T) val;
627 			}
628 			catch (ConvException)
629 			{
630 				return false;
631 			}
632 		}
633 		else
634 			static assert(false, "No setting parser for type " ~ T.stringof);
635 		__traits(getMember, config, name) = newval;
636 		return true;
637 	}
638 }
639 
640 /// Validates s == pattern "00:00"
641 bool validateTimeString(string s) @safe
642 {
643 	if (s.length != 5)
644 		return false;
645 	if (!s[0].isDigit || !s[1].isDigit || s[2] != ':' || !s[3].isDigit || !s[4].isDigit)
646 		return false;
647 	ubyte h = s[0 .. 2].to!ubyte;
648 	ubyte m = s[3 .. 5].to!ubyte;
649 	if (h >= 24)
650 		return false;
651 	if (m >= 60)
652 		return false;
653 	return true;
654 }
655 
656 /// Validates s == pattern "0{4,6}-W00"
657 bool validateWeekString(string s) @safe
658 {
659 	if (s.length < 8 || s.length > 10)
660 		return false;
661 	auto dash = s.indexOf('-');
662 	if (dash == -1 || dash != s.length - 4)
663 		return false;
664 	if (s[dash + 1] != 'W' || !s[dash + 2].isDigit || !s[dash + 3].isDigit)
665 		return false;
666 	auto y = s[0 .. dash];
667 	auto w = s[dash + 2 .. $].to!ubyte;
668 	if (w < 1 || w > 52)
669 		return false;
670 	try
671 	{
672 		auto yi = y.to!uint;
673 		if (yi < 1 || yi > 200_000)
674 			return false;
675 		return true;
676 	}
677 	catch (ConvException)
678 	{
679 		return false;
680 	}
681 }
682 
683 /// Validates s == pattern "0{4,6}-00"
684 bool validateMonthString(string s) @safe
685 {
686 	if (s.length < 7 || s.length > 9)
687 		return false;
688 	auto dash = s.indexOf('-');
689 	if (dash == -1 || dash != s.length - 3)
690 		return false;
691 	if (!s[dash + 1].isDigit || !s[dash + 2].isDigit)
692 		return false;
693 	auto y = s[0 .. dash];
694 	auto m = s[dash + 1 .. $].to!ubyte;
695 	if (m < 1 || m > 12)
696 		return false;
697 	try
698 	{
699 		auto yi = y.to!uint;
700 		if (yi < 1 || yi > 200_000)
701 			return false;
702 		return true;
703 	}
704 	catch (ConvException)
705 	{
706 		return false;
707 	}
708 }
709 
710 /// Validates s == pattern "0000-00-00T00:00"
711 bool validateDatetimeLocalString(string s) @safe
712 {
713 	if (s.length != 16)
714 		return false;
715 	if (!s[0].isDigit || !s[1].isDigit || !s[2].isDigit || !s[3].isDigit
716 			|| s[4] != '-' || !s[5].isDigit || !s[6].isDigit || s[7] != '-'
717 			|| !s[8].isDigit || !s[9].isDigit || s[10] != 'T' || !s[11].isDigit
718 			|| !s[12].isDigit || s[13] != ':' || !s[14].isDigit || !s[15].isDigit)
719 		return false;
720 	try
721 	{
722 		return SysTime.fromISOExtString(s ~ ":00") != SysTime.init;
723 	}
724 	catch (DateTimeException)
725 	{
726 		return false;
727 	}
728 }
729 
730 /// Validates s == pattern "0000-00-00"
731 bool validateDateString(string s) @safe
732 {
733 	if (s.length != 10)
734 		return false;
735 	if (!s[0].isDigit || !s[1].isDigit || !s[2].isDigit || !s[3].isDigit
736 			|| s[4] != '-' || !s[5].isDigit || !s[6].isDigit || s[7] != '-'
737 			|| !s[8].isDigit || !s[9].isDigit)
738 		return false;
739 	try
740 	{
741 		return Date(s[0 .. 4].to!int, s[5 .. 7].to!int, s[8 .. 10].to!int) != Date.init;
742 	}
743 	catch (DateTimeException)
744 	{
745 		return false;
746 	}
747 }
748 
749 /// Validates s == pattern "#xxxxxx"
750 bool validateColorString(string s) @safe
751 {
752 	if (s.length != 7)
753 		return false;
754 	if (s[0] != '#' || !s[1].isHexDigit || !s[2].isHexDigit
755 			|| !s[3].isHexDigit || !s[4].isHexDigit || !s[5].isHexDigit || !s[6].isHexDigit)
756 		return false;
757 	return true;
758 }
759 
760 /// Converts correctBookISBN_number to "Correct Book ISBN Number"
761 string makeHumanName(string identifier) @safe
762 {
763 	string humanName;
764 	bool wasUpper = true;
765 	bool wasSpace = true;
766 	foreach (c; identifier)
767 	{
768 		if (c >= 'A' && c <= 'Z')
769 		{
770 			if (!wasUpper)
771 			{
772 				wasUpper = true;
773 				humanName ~= ' ';
774 			}
775 		}
776 		else
777 			wasUpper = false;
778 		if (c == '_')
779 		{
780 			wasSpace = true;
781 			humanName ~= ' ';
782 		}
783 		else if (wasSpace)
784 		{
785 			humanName ~= [c].toUpper;
786 			wasSpace = false;
787 		}
788 		else
789 			humanName ~= c;
790 	}
791 	return humanName.strip;
792 }
793 
794 /// Controls how the input HTML is generated
795 struct DefaultInputGenerator
796 {
797 @safe:
798 	private static string errorString(bool success)
799 	{
800 		if (success)
801 			return "";
802 		else
803 			return `<span class="error">Please fill out this field correctly.</span>`;
804 	}
805 
806 	/// Called for single line input types
807 	static string textfield(string name, string type, string value, string raw, bool success)
808 	{
809 		const className = success ? "" : ` class="error"`;
810 		return `<label` ~ className ~ `><span>%s</span><input type="%s" value="%s"%s/></label>`.format(name.encode,
811 				type.encode, value.encode, raw) ~ errorString(success);
812 	}
813 
814 	/// Called for textareas
815 	static string textarea(string name, string value, string raw, bool success)
816 	{
817 		const className = success ? "" : ` class="error"`;
818 		return `<label` ~ className ~ `><span>%s</span><textarea%s>%s</textarea></label>`.format(name.encode,
819 				raw, value.encode) ~ errorString(success);
820 	}
821 
822 	/// Called for boolean values
823 	static string checkbox(string name, bool checked, string raw, bool success)
824 	{
825 		const className = success ? "" : " error";
826 		return `<label class="checkbox` ~ className ~ `"><input type="checkbox" %s%s/><span>%s</span></label>`.format(
827 				checked ? "checked" : "", raw, name.encode) ~ errorString(success);
828 	}
829 
830 	/// Called for enums disabled as select (you need to iterate over the enum members)
831 	static string dropdownList(Enum)(string name, Enum value, string raw, bool success)
832 	{
833 		const className = success ? "" : " error";
834 		string ret = `<label class="select` ~ className ~ `"><span>`
835 			~ name.encode ~ `</span><select` ~ raw ~ `>`;
836 		foreach (member; __traits(allMembers, Enum))
837 			ret ~= `<option value="` ~ (cast(OriginalType!Enum) __traits(getMember,
838 					Enum, member)).to!string.encode ~ `"` ~ (value == __traits(getMember,
839 					Enum, member) ? " selected" : "") ~ `>` ~ member.makeHumanName ~ `</option>`;
840 		return ret ~ "</select></label>" ~ errorString(success);
841 	}
842 
843 	/// Called for enums displayed as list of radio boxes (you need to iterate over the enum members)
844 	static string optionList(Enum)(string name, Enum value, string raw, bool success)
845 	{
846 		const className = success ? "" : " error";
847 		string ret = `<label class="checkbox options` ~ className ~ `"><span>`
848 			~ name.encode ~ "</span>";
849 		foreach (member; __traits(allMembers, Enum))
850 			ret ~= checkbox(member.makeHumanName, value == __traits(getMember, Enum, member),
851 					raw ~ ` value="` ~ (cast(OriginalType!Enum) __traits(getMember,
852 						Enum, member)).to!string.encode ~ `"`, true).replace(
853 					`type="checkbox"`, `type="radio"`);
854 		return ret ~ `</label>` ~ errorString(success);
855 	}
856 
857 	/// Called for BitFlags displayed as list of checkboxes.
858 	static string checkboxList(Enum)(string name, BitFlags!Enum value, string raw, bool success)
859 	{
860 		const className = success ? "" : " error";
861 		string ret = `<label class="checkbox flags` ~ className ~ `"><span>`
862 			~ name.encode ~ "</span>";
863 		foreach (member; __traits(allMembers, Enum))
864 			ret ~= checkbox(member.makeHumanName, !!(value & __traits(getMember,
865 					Enum, member)), raw ~ ` value="` ~ (cast(OriginalType!Enum) __traits(getMember,
866 					Enum, member)).to!string.encode ~ `"`, true);
867 		return ret ~ `</label>` ~ errorString(success);
868 	}
869 }
870 
871 /// Adds type="email" to string types
872 enum emailSetting;
873 /// Adds type="url" to string types
874 enum urlSetting;
875 /// Makes string types textareas
876 enum multilineSetting;
877 /// Adds type="range" to numeric types
878 enum rangeSetting;
879 /// Adds type="time" to string types
880 enum timeSetting;
881 /// Adds type="week" to string types
882 enum weekSetting;
883 /// Adds type="month" to string types
884 enum monthSetting;
885 /// Adds type="datetime-local" to string types
886 enum datetimeLocalSetting;
887 /// Adds type="date" to string types
888 enum dateSetting;
889 /// Adds type="color" to string types
890 enum colorSetting;
891 /// Adds disabled to any input
892 enum disabledSetting;
893 /// Adds required to any input
894 enum requiredSetting;
895 /// Disables automatic JS saving when changing the input
896 enum nonAutomaticSetting;
897 /// Changes a dropdown to a radio button list
898 enum optionsSetting;
899 
900 /// Changes the min="" attribute for numerical values
901 struct settingMin
902 {
903 	///
904 	double min;
905 }
906 
907 /// Changes the max="" attribute for numerical values
908 struct settingMax
909 {
910 	///
911 	double max;
912 }
913 
914 /// Changes the step="" attribute for numerical values
915 struct settingStep
916 {
917 	///
918 	double step;
919 }
920 
921 /// Changes the min="" and max="" attribute for numerical values
922 struct settingRange
923 {
924 	///
925 	double min, max;
926 }
927 
928 /// Changes the minlength="" and maxlength="" attribute for string values
929 struct settingLength
930 {
931 	///
932 	int max, min;
933 }
934 
935 /// Changes the pattern="regex" attribute
936 struct settingPattern
937 {
938 	///
939 	string regex;
940 }
941 
942 /// Changes the title="" attribute for custom error messages & tooltips
943 struct settingTitle
944 {
945 	///
946 	string title;
947 }
948 
949 /// Overrides the label of the input
950 struct settingLabel
951 {
952 	///
953 	string label;
954 }
955 
956 /// Contains a updateSetting(input) function which automatically sends changes to the server.
957 enum DefaultJavascriptCode = q{<script id="_setting_script_">
958 	var timeouts = {};
959 	function updateSetting(input) {
960 		clearTimeout(timeouts[input]);
961 		timeouts[input] = setTimeout(function() {
962 			var form = input;
963 			while (form && form.tagName != "FORM")
964 				form = form.parentElement;
965 			var submit = form.querySelector ? form.querySelector("input[type=submit]") : undefined;
966 			if (submit)
967 				submit.disabled = false;
968 			name = input.name;
969 			function attachError(elem, content) {
970 				var label = elem;
971 				while (label && label.tagName != "LABEL")
972 					label = label.parentElement;
973 				if (label)
974 					label.classList.add("error");
975 				var err = document.createElement("span");
976 				err.className = "error";
977 				err.textContent = content;
978 				err.style.padding = "4px";
979 				elem.parentElement.insertBefore(err, elem.nextSibling);
980 				setTimeout(function() { err.parentElement.removeChild(err); }, 2500);
981 			}
982 			var label = input;
983 			while (label && label.tagName != "LABEL")
984 				label = label.parentElement;
985 			if (label)
986 				label.classList.remove("error");
987 			var isFlags = false;
988 			var flagLabel = label;
989 			while (flagLabel) {
990 				if (flagLabel.classList.contains("flags")) {
991 					isFlags = true;
992 					break;
993 				}
994 				flagLabel = flagLabel.parentElement;
995 			}
996 			var valid = input.checkValidity ? input.checkValidity() : true;
997 			if (!valid) {
998 				attachError(input, input.title || "Please fill out this input correctly.");
999 				return;
1000 			}
1001 			var stillRequesting = true;
1002 			setTimeout(function () {
1003 				if (stillRequesting)
1004 					input.disabled = true;
1005 			}, 100);
1006 			var xhr = new XMLHttpRequest();
1007 			var method = "{method}";
1008 			var action = "{action}";
1009 			var query = "_field=" + encodeURIComponent(name);
1010 			if (input.type != "checkbox" || input.checked)
1011 				query += '&' + encodeURIComponent(name) + '=' + encodeURIComponent(input.value);
1012 			else if (isFlags)
1013 				query += '&' + encodeURIComponent(name) + "=!" + encodeURIComponent(input.value);
1014 			if (method != "POST")
1015 				action += query;
1016 			xhr.onload = function () {
1017 				if (xhr.status != 200 && xhr.status != 204)
1018 					attachError(input, input.title || "Please fill out this field correctly.");
1019 				else {
1020 					submit.value = "Saved!";
1021 					setTimeout(function() { submit.value = "Save"; }, 3000);
1022 				}
1023 				stillRequesting = false;
1024 				input.disabled = false;
1025 			};
1026 			xhr.onerror = function () {
1027 				stillRequesting = false;
1028 				input.disabled = false;
1029 				submit.disabled = false;
1030 			};
1031 			xhr.open(method, action);
1032 			xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
1033 			if (method == "POST")
1034 				xhr.send(query);
1035 			else
1036 				xhr.send();
1037 			submit.disabled = true;
1038 		}, 50);
1039 	}
1040 	function unlockForm(input) {
1041 		var form = input;
1042 		while (form && form.tagName != "FORM")
1043 			form = form.parentElement;
1044 		form.querySelector("input[type=submit]").disabled = false;
1045 	}
1046 	(document.currentScript || document.getElementById("_setting_script_")).previousSibling.querySelector("input[type=submit]").disabled = true;
1047 </script>};