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