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 @settingPlaceholder("you@example.com") 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 @settingClass("wide") 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 isPassword = hasUDA!(Member[0], passwordSetting);
246 	enum isMultiline = hasUDA!(Member[0], multilineSetting) || isStringArray;
247 	enum isRange = hasUDA!(Member[0], rangeSetting);
248 	enum isTime = hasUDA!(Member[0], timeSetting) || is(T == TimeOfDay);
249 	enum isWeek = hasUDA!(Member[0], weekSetting);
250 	enum isMonth = hasUDA!(Member[0], monthSetting);
251 	enum isDatetimeLocal = hasUDA!(Member[0], datetimeLocalSetting) || is(T == DateTime);
252 	enum isDate = hasUDA!(Member[0], dateSetting) || is(T == Date);
253 	enum isColor = hasUDA!(Member[0], colorSetting);
254 	enum isDisabled = hasUDA!(Member[0], disabledSetting);
255 	enum isRequired = hasUDA!(Member[0], requiredSetting);
256 	enum isNoJS = hasUDA!(Member[0], nonAutomaticSetting);
257 	enum isOptions = hasUDA!(Member[0], optionsSetting);
258 	enum mins = getUDAs!(Member[0], settingMin);
259 	enum maxs = getUDAs!(Member[0], settingMax);
260 	enum ranges = getUDAs!(Member[0], settingRange);
261 	enum lengths = getUDAs!(Member[0], settingLength);
262 	enum steps = getUDAs!(Member[0], settingStep);
263 	enum patterns = getUDAs!(Member[0], settingPattern);
264 	enum titles = getUDAs!(Member[0], settingTitle);
265 	enum labels = getUDAs!(Member[0], settingLabel);
266 	enum rows = getUDAs!(Member[0], settingRows);
267 	enum translations = getUDAs!(Member[0], settingTranslation);
268 	enum enumTranslations = getUDAs!(Member[0], enumTranslation);
269 	enum html = getUDAs!(Member[0], settingHTML);
270 	enum classNames = getUDAs!(Member[0], settingClass);
271 	enum placeholder = getUDAs!(Member[0], settingPlaceholder);
272 	static if (labels.length)
273 		string uiName = labels[0].label;
274 	else
275 		string uiName = name.makeHumanName;
276 	static if (translations.length && is(typeof(language) == string))
277 	{
278 		auto lang = (() @trusted => language)();
279 		if (lang !is null)
280 			foreach (translation; translations)
281 				if (translation.language == lang)
282 					uiName = translation.label;
283 	}
284 	string raw = ` name="` ~ name ~ `"`;
285 
286 	static if (classNames.length)
287 		enum string[] classes = [classNames].map!"a.className".array;
288 	else
289 		enum string[] classes = null;
290 
291 	static if (isDisabled)
292 		raw ~= " disabled";
293 	else static if (!isNoJS)
294 		raw ~= ` onchange="updateSetting(this)"`;
295 	else
296 		raw ~= ` onchange="unlockForm(this)"`;
297 	static if (lengths.length)
298 	{
299 		auto minlength = lengths[0].min;
300 		auto maxlength = lengths[0].max;
301 		if (minlength > 0)
302 			raw ~= " minlength=\"" ~ minlength.to!string ~ "\"";
303 		if (maxlength > 0)
304 			raw ~= " maxlength=\"" ~ maxlength.to!string ~ "\"";
305 	}
306 	static if (patterns.length)
307 		raw ~= " pattern=\"" ~ patterns[0].regex.encode ~ "\"";
308 	else static if (isDatetimeLocal) // if browser doesn't support datetime-local
309 		raw ~= ` pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}"`;
310 	else static if (isTime) // if browser doesn't support time
311 		raw ~= ` pattern="[0-9]{2}:[0-9]{2}"`;
312 	else static if (isDate) // if browser doesn't support date
313 		raw ~= ` pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}"`;
314 	static if (titles.length)
315 		raw ~= " title=\"" ~ titles[0].title.encode ~ "\"";
316 	static if (rows.length)
317 		raw ~= " rows=\"" ~ rows[0].count.to!string ~ "\"";
318 	static if (isRequired)
319 		raw ~= " required";
320 	static if (placeholder.length)
321 		raw ~= " placeholder=\"" ~ placeholder[0].placeholder.encode ~ "\"";
322 
323 	static if (html.length > 0)
324 		enum string pre = [html].map!"a.raw".join("\n");
325 	else
326 		enum string pre = null;
327 
328 	static if (is(T == enum))
329 	{
330 		static if (isOptions)
331 			return pre ~ InputGenerator.optionList!(T, enumTranslations)(uiName,
332 					value, raw, success, classes);
333 		else
334 			return pre ~ InputGenerator.dropdownList!(T, enumTranslations)(uiName,
335 					value, raw, success, classes);
336 	}
337 	else static if (is(T == BitFlags!Enum, Enum))
338 		return pre ~ InputGenerator.checkboxList!(Enum, enumTranslations)(uiName,
339 				value, raw, success, classes);
340 	else static if (is(T == bool))
341 		return pre ~ InputGenerator.checkbox(uiName, value, raw, success, classes);
342 	else static if (isSomeString!T || isStringArray)
343 	{
344 		static if (
345 			isEmail + isUrl + isMultiline + isTime + isWeek + isMonth + isDatetimeLocal
346 				+ isDate + isColor + isPassword > 1)
347 			static assert(false, "string setting " ~ name ~ " has multiple type related attributes");
348 		static if (isMultiline)
349 		{
350 			static if (isStringArray)
351 				return pre ~ InputGenerator.textarea(uiName, value.to!(string[])
352 						.join("\n"), raw, success, classes);
353 			else
354 				return pre ~ InputGenerator.textarea(uiName, value.to!string, raw, success, classes);
355 		}
356 		else
357 			return pre ~ InputGenerator.textfield(uiName, isEmail ? "email" : isUrl ? "url" : isTime ? "time"
358 					: isWeek ? "week" : isMonth ? "month" : isDatetimeLocal ? "datetime-local" : isDate ? "date" : isColor
359 					? "color" : isPassword ? "password" : "text", value.to!string, raw, success, classes);
360 	}
361 	else static if (is(T == DateTime))
362 		return pre ~ InputGenerator.textfield(uiName, "datetime-local",
363 				value.toISOExtString[0 .. 16], raw, success, classes);
364 	else static if (is(T == Date))
365 		return pre ~ InputGenerator.textfield(uiName, "date", value.toISOExtString,
366 				raw, success, classes);
367 	else static if (is(T == TimeOfDay))
368 		return pre ~ InputGenerator.textfield(uiName, "time",
369 				value.toISOExtString[0 .. 5], raw, success, classes);
370 	else static if (is(T == URL))
371 		return pre ~ InputGenerator.textfield(uiName, "url", value.toString, raw, success, classes);
372 	else static if (isNumeric!T)
373 	{
374 		double min, max;
375 		static if (mins.length)
376 			min = mins[0].min;
377 		static if (maxs.length)
378 			max = maxs[0].max;
379 		static if (ranges.length)
380 		{
381 			min = ranges[0].min;
382 			max = ranges[0].max;
383 		}
384 		if (min == min) // !isNaN
385 			raw ~= " min=\"" ~ min.to!string ~ "\"";
386 		if (max == max) // !isNaN
387 			raw ~= " max=\"" ~ max.to!string ~ "\"";
388 		static if (steps.length)
389 			raw ~= " step=\"" ~ steps[0].step.to!string ~ "\"";
390 		return pre ~ InputGenerator.textfield(uiName, isRange ? "range" : "number",
391 				value.to!string, raw, success, classes);
392 	}
393 	else
394 		static assert(false, "No setting generator for type " ~ T.stringof);
395 }
396 
397 /**
398 	Function processing user input and validating for correctness. $(BR)$(BR)
399 	The following validations are done: $(BR)
400 	If the setting is a  `disabledSetting`, it will always skip this field. $(BR)
401 	If the setting has a `settingPattern`, it will validate the raw value (no matter what type) against this regex. $(BR)
402 	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)
403 	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)
404 	If the setting is an enum the value will be checked if it is contained inside the enum. $(BR)
405 	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)
406 	Integral numbers will always be checked if finite & if no range is given they will be clamped. $(BR)$(BR)
407 	Attributes for strings: $(BR)
408 		`emailSetting` is validated using `std.net.isemail.isEmail(CheckDns.no, EmailStatusCode.any)` $(BR)
409 		`urlSetting` is validated using `vibe.inet.url.URL` $(BR)
410 		`timeSetting` is checked against pattern `00:00` + checking if 0 <= hour < 24 && 0 <= minute < 60 $(BR)
411 		`weekSetting` is checked against pattern `0{4,6}-W00` + checking if 1 <= year <= 200000 && 1 <= week <= 52 $(BR)
412 		`monthSetting` is checked against pattern `0{4,6}-00` + checking if 1 <= year <= 200000 && 1 <= month <= 12 $(BR)
413 		`datetimeLocalSetting` is checked against pattern `0000-00-00T00:00` + passing into `std.datetime.SysTime.fromISOExtString`` $(BR)
414 		`dateSetting` is checked against pattern `0000-00-00` + checking the date using `std.datetime.Date` $(BR)
415 		`colorSetting` is checked against pattern `#FFFFFF` $(BR)
416 	Values using these attributes can be used without the need to validate the input.
417 	Params:
418 		strict = if false, values will be fixed to conform to the input instead of discarding them.
419 		Currently only fixing numbers and string lengths and new lines in single line strings is implemented.
420 	Returns: a bit array where each bit represents an input and is set to 1 if valid
421 	*/
422 ulong processSettings(T)(scope HTTPServerRequest req, ref T config,
423 		bool strict = false, bool post = true) @safe
424 {
425 	ulong valid;
426 	auto field = (post ? req.form : req.query).get("_field", "");
427 	foreach (i, member; __traits(allMembers, T))
428 	{
429 		if (field.length && field != member)
430 			continue;
431 		valid |= req.processSetting!member(config, strict, post) << cast(ulong) i;
432 	}
433 	return valid;
434 }
435 
436 /// ditto
437 bool processSetting(string name, Config)(HTTPServerRequest req, ref Config config,
438 		bool strict = false, bool post = true) @safe
439 {
440 	alias Member = AliasSeq!(__traits(getMember, config, name));
441 	auto member = __traits(getMember, config, name);
442 	alias T = typeof(member);
443 	enum isStringArray = isDynamicArray!T && isSomeString!(ElementType!T);
444 	enum isEmail = hasUDA!(Member[0], emailSetting);
445 	enum isUrl = hasUDA!(Member[0], urlSetting);
446 	enum isPassword = hasUDA!(Member[0], passwordSetting);
447 	enum isMultiline = hasUDA!(Member[0], multilineSetting) || isStringArray;
448 	enum isRange = hasUDA!(Member[0], rangeSetting);
449 	enum isTime = hasUDA!(Member[0], timeSetting);
450 	enum isWeek = hasUDA!(Member[0], weekSetting);
451 	enum isMonth = hasUDA!(Member[0], monthSetting);
452 	enum isDatetimeLocal = hasUDA!(Member[0], datetimeLocalSetting);
453 	enum isDate = hasUDA!(Member[0], dateSetting);
454 	enum isColor = hasUDA!(Member[0], colorSetting);
455 	enum isDisabled = hasUDA!(Member[0], disabledSetting);
456 	enum isRequired = hasUDA!(Member[0], requiredSetting);
457 	enum mins = getUDAs!(Member[0], settingMin);
458 	enum maxs = getUDAs!(Member[0], settingMax);
459 	enum ranges = getUDAs!(Member[0], settingRange);
460 	enum lengths = getUDAs!(Member[0], settingLength);
461 	enum steps = getUDAs!(Member[0], settingStep);
462 	enum patterns = getUDAs!(Member[0], settingPattern);
463 	static if (isDisabled)
464 		return true;
465 	else
466 	{
467 		int minlength = int.min, maxlength = int.max;
468 		static if (lengths.length)
469 		{
470 			minlength = lengths[0].min;
471 			maxlength = lengths[0].max;
472 		}
473 		T oldval = member;
474 		T newval = oldval;
475 		FormFields form = post ? req.form : req.query;
476 		auto allvals = form.getAll(name);
477 		bool isJS = form.get("_field", "").length != 0;
478 		string rawval = allvals.length ? allvals[0] : "";
479 		static if (patterns.length)
480 			if (!matchFirst(rawval, ctRegex!(patterns[0].regex)))
481 				return false;
482 		static if (isRequired)
483 			if (!allvals.length)
484 				return false;
485 		if (minlength != int.min && rawval.length < minlength)
486 			return false;
487 		if (maxlength != int.max && rawval.length > maxlength)
488 		{
489 			if (strict)
490 				return false;
491 			else
492 				rawval.length = maxlength;
493 		}
494 		static if (is(T == enum))
495 		{
496 			try
497 			{
498 				newval = cast(T) rawval.to!(OriginalType!T);
499 				bool exists = false;
500 				foreach (val; EnumMembers!T)
501 					if (val == newval)
502 					{
503 						exists = true;
504 						break;
505 					}
506 				if (!exists)
507 					return false;
508 			}
509 			catch (ConvException)
510 			{
511 				return false;
512 			}
513 		}
514 		else static if (is(T : BitFlags!Enum, Enum))
515 		{
516 			try
517 			{
518 				if (!rawval.length)
519 					return false;
520 				if (isJS)
521 				{
522 					bool negate = rawval[0] == '!';
523 					if (negate)
524 						rawval = rawval[1 .. $];
525 					auto enumType = cast(Enum) rawval.to!(OriginalType!Enum);
526 					bool exists = false;
527 					foreach (val; EnumMembers!Enum)
528 						if (val == enumType)
529 						{
530 							exists = true;
531 							break;
532 						}
533 					if (!exists)
534 						return false;
535 					if (negate)
536 						newval = oldval & ~T(enumType);
537 					else
538 						newval = oldval | enumType;
539 				}
540 				else
541 				{
542 					newval = T.init;
543 					foreach (rawval1; allvals)
544 					{
545 						auto enumType = cast(Enum) rawval1.to!(OriginalType!Enum);
546 						bool exists = false;
547 						foreach (val; EnumMembers!Enum)
548 							if (val == enumType)
549 							{
550 								exists = true;
551 								break;
552 							}
553 						if (!exists)
554 							return false;
555 						newval |= enumType;
556 					}
557 				}
558 			}
559 			catch (ConvException)
560 			{
561 				return false;
562 			}
563 		}
564 		else static if (is(T == bool))
565 			newval = allvals.length > 0;
566 		else static if (isSomeString!T || isStringArray)
567 		{
568 			static if (
569 				isEmail + isUrl + isMultiline + isTime + isWeek + isMonth + isDatetimeLocal
570 					+ isDate + isColor + isPassword > 1)
571 				static assert(false, "string setting " ~ name ~ " has multiple type related attributes");
572 			static if (isMultiline)
573 			{
574 				static if (isStringArray)
575 				{
576 					static if (__traits(compiles, newval = rawval.lineSplitter.filter!"a.length".array))
577 						newval = rawval.lineSplitter.filter!"a.length".array;
578 					else
579 						newval = rawval.lineSplitter
580 							.filter!"a.length"
581 							.map!(to!(ElementType!T))
582 							.array;
583 				}
584 				else
585 					newval = rawval;
586 			}
587 			else
588 			{
589 				if (rawval.length)
590 				{
591 					if (strict && rawval.indexOfAny("\r\n") != -1)
592 						return false;
593 					else
594 						rawval = rawval.tr("\r\n", "  ");
595 					static if (isEmail)
596 					{
597 						rawval = rawval.strip;
598 						import std.net.isemail;
599 
600 						if ((()@trusted => !rawval.isEmail(CheckDns.no, EmailStatusCode.any))())
601 							return false;
602 						newval = rawval;
603 					}
604 					else static if (isUrl)
605 					{
606 						try
607 						{
608 							newval = URL(rawval.strip).toString;
609 						}
610 						catch (Exception)
611 						{
612 							return false;
613 						}
614 					}
615 					else static if (isTime)
616 					{
617 						rawval = rawval.strip;
618 						if (!validateTimeString(rawval))
619 							return false;
620 						newval = rawval;
621 					}
622 					else static if (isWeek)
623 					{
624 						rawval = rawval.strip;
625 						if (!validateWeekString(rawval))
626 							return false;
627 						newval = rawval;
628 					}
629 					else static if (isMonth)
630 					{
631 						rawval = rawval.strip;
632 						if (!validateMonthString(rawval))
633 							return false;
634 						newval = rawval;
635 					}
636 					else static if (isDatetimeLocal)
637 					{
638 						rawval = rawval.strip;
639 						if (!validateDatetimeLocalString(rawval))
640 							return false;
641 						newval = rawval;
642 					}
643 					else static if (isDate)
644 					{
645 						rawval = rawval.strip;
646 						if (!validateDateString(rawval))
647 							return false;
648 						newval = rawval;
649 					}
650 					else static if (isColor)
651 					{
652 						rawval = rawval.strip;
653 						if (!validateColorString(rawval))
654 							return false;
655 						newval = rawval;
656 					}
657 					else
658 						newval = rawval;
659 				}
660 				else
661 				{
662 					newval = "";
663 				}
664 			}
665 		}
666 		else static if (is(T == DateTime))
667 		{
668 			rawval = rawval.strip;
669 			if (!validateDatetimeLocalString(rawval))
670 				return false;
671 			newval = DateTime.fromISOExtString(rawval ~ ":00");
672 		}
673 		else static if (is(T == Date))
674 		{
675 			rawval = rawval.strip;
676 			if (!validateDateString(rawval))
677 				return false;
678 			newval = Date.fromISOExtString(rawval);
679 		}
680 		else static if (is(T == TimeOfDay))
681 		{
682 			rawval = rawval.strip;
683 			if (!validateTimeString(rawval))
684 				return false;
685 			newval = TimeOfDay.fromISOExtString(rawval ~ ":00");
686 		}
687 		else static if (is(T == URL))
688 		{
689 			try
690 			{
691 				newval = URL(rawval.strip);
692 			}
693 			catch (Exception)
694 			{
695 				return false;
696 			}
697 		}
698 		else static if (isNumeric!T)
699 		{
700 			double min, max;
701 			static if (isIntegral!T)
702 			{
703 				min = T.min;
704 				max = T.max;
705 			}
706 			static if (mins.length)
707 				min = mins[0].min;
708 			static if (maxs.length)
709 				max = maxs[0].max;
710 			static if (ranges.length)
711 			{
712 				min = ranges[0].min;
713 				max = ranges[0].max;
714 			}
715 			double step = 1;
716 			static if (steps.length)
717 				step = steps[0].step;
718 			try
719 			{
720 				double val = rawval.to!double;
721 				if (min == min && val < min)
722 				{
723 					if (strict)
724 						return false;
725 					else
726 						val = min;
727 				}
728 				if (max == max && val > max)
729 				{
730 					if (strict)
731 						return false;
732 					else
733 						val = max;
734 				}
735 				val = floor(val / step) * step;
736 				bool isFinite = val == val && val != double.infinity && val != -double.infinity;
737 				static if (isRange && isFloatingPoint!T)
738 				{
739 					if (!isFinite)
740 						return false;
741 				}
742 				static if (!isFloatingPoint!T)
743 					if (!isFinite)
744 						return false;
745 				newval = cast(T) val;
746 			}
747 			catch (ConvException)
748 			{
749 				return false;
750 			}
751 		}
752 		else
753 			static assert(false, "No setting parser for type " ~ T.stringof);
754 		__traits(getMember, config, name) = newval;
755 		return true;
756 	}
757 }
758 
759 /// Validates s == pattern "00:00"
760 bool validateTimeString(string s) @safe
761 {
762 	if (s.length != 5)
763 		return false;
764 	if (!s[0].isDigit || !s[1].isDigit || s[2] != ':' || !s[3].isDigit || !s[4].isDigit)
765 		return false;
766 	ubyte h = s[0 .. 2].to!ubyte;
767 	ubyte m = s[3 .. 5].to!ubyte;
768 	if (h >= 24)
769 		return false;
770 	if (m >= 60)
771 		return false;
772 	return true;
773 }
774 
775 /// Validates s == pattern "0{4,6}-W00"
776 bool validateWeekString(string s) @safe
777 {
778 	if (s.length < 8 || s.length > 10)
779 		return false;
780 	auto dash = s.indexOf('-');
781 	if (dash == -1 || dash != s.length - 4)
782 		return false;
783 	if (s[dash + 1] != 'W' || !s[dash + 2].isDigit || !s[dash + 3].isDigit)
784 		return false;
785 	auto y = s[0 .. dash];
786 	auto w = s[dash + 2 .. $].to!ubyte;
787 	if (w < 1 || w > 52)
788 		return false;
789 	try
790 	{
791 		auto yi = y.to!uint;
792 		if (yi < 1 || yi > 200_000)
793 			return false;
794 		return true;
795 	}
796 	catch (ConvException)
797 	{
798 		return false;
799 	}
800 }
801 
802 /// Validates s == pattern "0{4,6}-00"
803 bool validateMonthString(string s) @safe
804 {
805 	if (s.length < 7 || s.length > 9)
806 		return false;
807 	auto dash = s.indexOf('-');
808 	if (dash == -1 || dash != s.length - 3)
809 		return false;
810 	if (!s[dash + 1].isDigit || !s[dash + 2].isDigit)
811 		return false;
812 	auto y = s[0 .. dash];
813 	auto m = s[dash + 1 .. $].to!ubyte;
814 	if (m < 1 || m > 12)
815 		return false;
816 	try
817 	{
818 		auto yi = y.to!uint;
819 		if (yi < 1 || yi > 200_000)
820 			return false;
821 		return true;
822 	}
823 	catch (ConvException)
824 	{
825 		return false;
826 	}
827 }
828 
829 /// Validates s == pattern "0000-00-00T00:00"
830 bool validateDatetimeLocalString(string s) @safe
831 {
832 	if (s.length != 16)
833 		return false;
834 	if (!s[0].isDigit || !s[1].isDigit || !s[2].isDigit || !s[3].isDigit
835 			|| s[4] != '-' || !s[5].isDigit || !s[6].isDigit || s[7] != '-'
836 			|| !s[8].isDigit || !s[9].isDigit || s[10] != 'T' || !s[11].isDigit
837 			|| !s[12].isDigit || s[13] != ':' || !s[14].isDigit || !s[15].isDigit)
838 		return false;
839 	try
840 	{
841 		return SysTime.fromISOExtString(s ~ ":00") != SysTime.init;
842 	}
843 	catch (DateTimeException)
844 	{
845 		return false;
846 	}
847 }
848 
849 /// Validates s == pattern "0000-00-00"
850 bool validateDateString(string s) @safe
851 {
852 	if (s.length != 10)
853 		return false;
854 	if (!s[0].isDigit || !s[1].isDigit || !s[2].isDigit || !s[3].isDigit
855 			|| s[4] != '-' || !s[5].isDigit || !s[6].isDigit || s[7] != '-'
856 			|| !s[8].isDigit || !s[9].isDigit)
857 		return false;
858 	try
859 	{
860 		return Date(s[0 .. 4].to!int, s[5 .. 7].to!int, s[8 .. 10].to!int) != Date.init;
861 	}
862 	catch (DateTimeException)
863 	{
864 		return false;
865 	}
866 }
867 
868 /// Validates s == pattern "#xxxxxx"
869 bool validateColorString(string s) @safe
870 {
871 	if (s.length != 7)
872 		return false;
873 	if (s[0] != '#' || !s[1].isHexDigit || !s[2].isHexDigit || !s[3].isHexDigit
874 			|| !s[4].isHexDigit || !s[5].isHexDigit || !s[6].isHexDigit)
875 		return false;
876 	return true;
877 }
878 
879 /// Converts correctBookISBN_number to "Correct Book ISBN Number"
880 string makeHumanName(string identifier) @safe
881 {
882 	string humanName;
883 	bool wasUpper = true;
884 	bool wasSpace = true;
885 	foreach (c; identifier)
886 	{
887 		if (c >= 'A' && c <= 'Z')
888 		{
889 			if (!wasUpper)
890 			{
891 				wasUpper = true;
892 				humanName ~= ' ';
893 			}
894 		}
895 		else
896 			wasUpper = false;
897 		if (c == '_')
898 		{
899 			wasSpace = true;
900 			humanName ~= ' ';
901 		}
902 		else if (wasSpace)
903 		{
904 			humanName ~= [c].toUpper;
905 			wasSpace = false;
906 		}
907 		else
908 			humanName ~= c;
909 	}
910 	return humanName.strip;
911 }
912 
913 /// Controls how the input HTML is generated
914 struct DefaultInputGenerator
915 {
916 @safe:
917 	private static string errorString(bool success)
918 	{
919 		if (success)
920 			return "";
921 		else
922 			return `<span class="error">Please fill out this field correctly.</span>`;
923 	}
924 
925 	/// Called for single line input types
926 	static string textfield(string name, string type, string value, string raw,
927 			bool success, string[] classes)
928 	{
929 		if (!success)
930 			classes ~= "error";
931 		const className = classes.length ? ` class="` ~ classes.join(" ") ~ `"` : "";
932 		return `<label` ~ className ~ `><span>%s</span><input type="%s" value="%s"%s/></label>`.format(name.encode,
933 				type.encode, value.encode, raw) ~ errorString(success);
934 	}
935 
936 	/// Called for textareas
937 	static string textarea(string name, string value, string raw, bool success, string[] classes)
938 	{
939 		if (!success)
940 			classes ~= "error";
941 		const className = classes.length ? ` class="` ~ classes.join(" ") ~ `"` : "";
942 		return `<label` ~ className ~ `><span>%s</span><textarea%s>%s</textarea></label>`.format(name.encode,
943 				raw, value.encode) ~ errorString(success);
944 	}
945 
946 	/// Called for boolean values
947 	static string checkbox(string name, bool checked, string raw, bool success, string[] classes)
948 	{
949 		classes ~= "checkbox";
950 		if (!success)
951 			classes ~= "error";
952 		const className = ` class="` ~ classes.join(" ") ~ `"`;
953 		return `<label` ~ className ~ `><input type="checkbox" %s%s/><span>%s</span></label>`.format(checked
954 				? "checked" : "", raw, name.encode) ~ errorString(success);
955 	}
956 
957 	/// Called for enums disabled as select (you need to iterate over the enum members)
958 	static string dropdownList(Enum, translations...)(string name, Enum value,
959 			string raw, bool success, string[] classes)
960 	{
961 		classes ~= "select";
962 		if (!success)
963 			classes ~= "error";
964 		const className = ` class="` ~ classes.join(" ") ~ `"`;
965 		string ret = `<label` ~ className ~ `><span>` ~ name.encode ~ `</span><select` ~ raw ~ `>`;
966 		foreach (member; __traits(allMembers, Enum))
967 			ret ~= `<option value="` ~ (cast(OriginalType!Enum) __traits(getMember,
968 					Enum, member)).to!string.encode ~ `"` ~ (value == __traits(getMember,
969 					Enum, member) ? " selected" : "") ~ `>` ~ __traits(getMember, Enum, member)
970 				.translateEnum!(Enum, translations)(member.makeHumanName) ~ `</option>`;
971 		return ret ~ "</select></label>" ~ errorString(success);
972 	}
973 
974 	/// Called for enums displayed as list of radio boxes (you need to iterate over the enum members)
975 	static string optionList(Enum, translations...)(string name, Enum value,
976 			string raw, bool success, string[] classes)
977 	{
978 		classes ~= "checkbox options";
979 		if (!success)
980 			classes ~= "error";
981 		const className = ` class="` ~ classes.join(" ") ~ `"`;
982 		string ret = `<label` ~ className ~ `><span>` ~ name.encode ~ "</span>";
983 		foreach (member; __traits(allMembers, Enum))
984 			ret ~= checkbox(__traits(getMember, Enum, member).translateEnum!(Enum,
985 					translations)(member.makeHumanName), value == __traits(getMember, Enum, member),
986 					raw ~ ` value="` ~ (cast(OriginalType!Enum) __traits(getMember, Enum,
987 						member)).to!string.encode ~ `"`, true, classes).replace(`type="checkbox"`,
988 					`type="radio"`);
989 		return ret ~ `</label>` ~ errorString(success);
990 	}
991 
992 	/// Called for BitFlags displayed as list of checkboxes.
993 	static string checkboxList(Enum, translations...)(string name,
994 			BitFlags!Enum value, string raw, bool success, string[] classes)
995 	{
996 		classes ~= "checkbox flags";
997 		if (!success)
998 			classes ~= "error";
999 		const className = ` class="` ~ classes.join(" ") ~ `"`;
1000 		string ret = `<label` ~ className ~ `><span>` ~ name.encode ~ "</span>";
1001 		foreach (member; __traits(allMembers, Enum))
1002 			ret ~= checkbox(__traits(getMember, Enum, member).translateEnum!(Enum,
1003 					translations)(member.makeHumanName), !!(value & __traits(getMember, Enum,
1004 					member)), raw ~ ` value="` ~ (cast(OriginalType!Enum) __traits(getMember,
1005 					Enum, member)).to!string.encode ~ `"`, true, classes);
1006 		return ret ~ `</label>` ~ errorString(success);
1007 	}
1008 }
1009 
1010 /// Adds type="email" to string types
1011 enum emailSetting;
1012 /// Adds type="url" to string types
1013 enum urlSetting;
1014 /// Adds type="password" to string types
1015 enum passwordSetting;
1016 /// Makes string types textareas
1017 enum multilineSetting;
1018 /// Adds type="range" to numeric types
1019 enum rangeSetting;
1020 /// Adds type="time" to string types
1021 enum timeSetting;
1022 /// Adds type="week" to string types
1023 enum weekSetting;
1024 /// Adds type="month" to string types
1025 enum monthSetting;
1026 /// Adds type="datetime-local" to string types
1027 enum datetimeLocalSetting;
1028 /// Adds type="date" to string types
1029 enum dateSetting;
1030 /// Adds type="color" to string types
1031 enum colorSetting;
1032 /// Adds disabled to any input
1033 enum disabledSetting;
1034 /// Adds required to any input
1035 enum requiredSetting;
1036 /// Disables automatic JS saving when changing the input
1037 enum nonAutomaticSetting;
1038 /// Changes a dropdown to a radio button list
1039 enum optionsSetting;
1040 
1041 /// Changes the min="" attribute for numerical values
1042 struct settingMin
1043 {
1044 	///
1045 	double min;
1046 }
1047 
1048 /// Changes the max="" attribute for numerical values
1049 struct settingMax
1050 {
1051 	///
1052 	double max;
1053 }
1054 
1055 /// Changes the step="" attribute for numerical values
1056 struct settingStep
1057 {
1058 	///
1059 	double step;
1060 }
1061 
1062 /// Changes the min="" and max="" attribute for numerical values
1063 struct settingRange
1064 {
1065 	///
1066 	double min, max;
1067 }
1068 
1069 /// Changes the minlength="" and maxlength="" attribute for string values
1070 struct settingLength
1071 {
1072 	///
1073 	int max, min;
1074 }
1075 
1076 /// Changes the pattern="regex" attribute
1077 struct settingPattern
1078 {
1079 	///
1080 	string regex;
1081 }
1082 
1083 /// Changes the title="" attribute for custom error messages & tooltips
1084 struct settingTitle
1085 {
1086 	///
1087 	string title;
1088 }
1089 
1090 /// Overrides the label of the input
1091 struct settingLabel
1092 {
1093 	///
1094 	string label;
1095 }
1096 
1097 /// Sets the number of rows of a textarea
1098 struct settingRows
1099 {
1100 	///
1101 	int count;
1102 }
1103 
1104 /// Changes the label if the current language (using a WebInterface translation context) matches the given one.
1105 /// You need at least vibe-d v0.8.1-alpha.3 to use this UDA.
1106 struct settingTranslation
1107 {
1108 	///
1109 	string language;
1110 	///
1111 	string label;
1112 }
1113 
1114 /// Relables all enum member names for a language. Give `null` as first argument to change the default language
1115 struct enumTranslation
1116 {
1117 	///
1118 	string language;
1119 	///
1120 	string[] translations;
1121 }
1122 
1123 /// Inserts raw HTML code before an element.
1124 struct settingHTML
1125 {
1126 	///
1127 	string raw;
1128 }
1129 
1130 /// Inserts raw CSS class name for an element.
1131 struct settingClass
1132 {
1133 	///
1134 	string className;
1135 }
1136 
1137 /// Changes how the form HTML template looks
1138 struct formTemplate
1139 {
1140 	/// Contains the std.format formattable template code. $(BR)
1141 	/// Arguments in order are: string action, string method, string formArguments, string html $(BR)
1142 	/// html is last so you can embed it using %4$s without throwing an orphan arguments exception.
1143 	string code;
1144 }
1145 
1146 /// Sets the placeholder attribute for elements that support it
1147 struct settingPlaceholder
1148 {
1149 	///
1150 	string placeholder;
1151 }
1152 
1153 string translateEnum(T, translations...)(T value, string fallback) @safe
1154 		if (is(T == enum))
1155 {
1156 	static if (translations.length)
1157 	{
1158 		static if (is(typeof(language) == string))
1159 			auto lang = (() @trusted => language)();
1160 		enum NumEnumMembers = [EnumMembers!T].length;
1161 		foreach (i, other; EnumMembers!T)
1162 		{
1163 			if (other == value)
1164 			{
1165 				string ret = null;
1166 				foreach (translation; translations)
1167 				{
1168 					static assert(translation.translations.length == NumEnumMembers,
1169 							"Translation missing some values. Set them to null to skip");
1170 					if (translation.language is null && ret is null)
1171 						ret = translation.translations[i];
1172 					else static if (is(typeof(language) == string))
1173 					{
1174 						if (translation.language == lang)
1175 							ret = translation.translations[i];
1176 					}
1177 				}
1178 				return ret is null ? fallback : ret;
1179 			}
1180 		}
1181 	}
1182 	else static if (__traits(compiles, __traits(getAttributes,
1183 			__traits(getMember, T, __traits(allMembers, T)[0]))))
1184 	{
1185 		foreach (i, other; __traits(allMembers, T))
1186 		{
1187 			if (__traits(getMember, T, other) == value)
1188 			{
1189 				static if (is(typeof(language) == string))
1190 					auto lang = (() @trusted => language)();
1191 				string ret = null;
1192 				foreach (attr; __traits(getAttributes, __traits(getMember, T, other)))
1193 				{
1194 					static if (is(typeof(attr) == settingTranslation))
1195 					{
1196 						if (attr.language is null && ret is null)
1197 							ret = attr.label;
1198 						else static if (is(typeof(language) == string))
1199 						{
1200 							if (attr.language == lang)
1201 								ret = attr.label;
1202 						}
1203 					}
1204 				}
1205 				return ret is null ? fallback : ret;
1206 			}
1207 		}
1208 	}
1209 	return fallback;
1210 }
1211 
1212 /// Contains a updateSetting(input) function which automatically sends changes to the server.
1213 enum DefaultJavascriptCode = q{<script id="_setting_script_">
1214 	var timeouts = {};
1215 	function updateSetting(input) {
1216 		clearTimeout(timeouts[input]);
1217 		timeouts[input] = setTimeout(function() {
1218 			var form = input;
1219 			while (form && form.tagName != "FORM")
1220 				form = form.parentElement;
1221 			var submit = form.querySelector ? form.querySelector("input[type=submit]") : undefined;
1222 			if (submit)
1223 				submit.disabled = false;
1224 			name = input.name;
1225 			function attachError(elem, content) {
1226 				var label = elem;
1227 				while (label && label.tagName != "LABEL")
1228 					label = label.parentElement;
1229 				if (label)
1230 					label.classList.add("error");
1231 				var err = document.createElement("span");
1232 				err.className = "error";
1233 				err.textContent = content;
1234 				err.style.padding = "4px";
1235 				elem.parentElement.insertBefore(err, elem.nextSibling);
1236 				setTimeout(function() { err.parentElement.removeChild(err); }, 2500);
1237 			}
1238 			var label = input;
1239 			while (label && label.tagName != "LABEL")
1240 				label = label.parentElement;
1241 			if (label)
1242 				label.classList.remove("error");
1243 			var isFlags = false;
1244 			var flagLabel = label;
1245 			while (flagLabel) {
1246 				if (flagLabel.classList.contains("flags")) {
1247 					isFlags = true;
1248 					break;
1249 				}
1250 				flagLabel = flagLabel.parentElement;
1251 			}
1252 			var valid = input.checkValidity ? input.checkValidity() : true;
1253 			if (!valid) {
1254 				attachError(input, input.title || "Please fill out this input correctly.");
1255 				return;
1256 			}
1257 			var stillRequesting = true;
1258 			setTimeout(function () {
1259 				if (stillRequesting)
1260 					input.disabled = true;
1261 			}, 100);
1262 			var xhr = new XMLHttpRequest();
1263 			var method = "{method}";
1264 			var action = "{action}";
1265 			var query = "_field=" + encodeURIComponent(name);
1266 			if (input.type != "checkbox" || input.checked)
1267 				query += '&' + encodeURIComponent(name) + '=' + encodeURIComponent(input.value);
1268 			else if (isFlags)
1269 				query += '&' + encodeURIComponent(name) + "=!" + encodeURIComponent(input.value);
1270 			if (method != "POST")
1271 				action += query;
1272 			xhr.onload = function () {
1273 				if (xhr.status != 200 && xhr.status != 204)
1274 					attachError(input, input.title || "Please fill out this field correctly.");
1275 				else {
1276 					submit.value = "Saved!";
1277 					setTimeout(function() { submit.value = "Save"; }, 3000);
1278 				}
1279 				stillRequesting = false;
1280 				input.disabled = false;
1281 			};
1282 			xhr.onerror = function () {
1283 				stillRequesting = false;
1284 				input.disabled = false;
1285 				submit.disabled = false;
1286 			};
1287 			xhr.open(method, action);
1288 			xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
1289 			if (method == "POST")
1290 				xhr.send(query);
1291 			else
1292 				xhr.send();
1293 			submit.disabled = true;
1294 		}, 50);
1295 	}
1296 	function unlockForm(input) {
1297 		var form = input;
1298 		while (form && form.tagName != "FORM")
1299 			form = form.parentElement;
1300 		form.querySelector("input[type=submit]").disabled = false;
1301 	}
1302 	(document.currentScript || document.getElementById("_setting_script_")).previousSibling.querySelector("input[type=submit]").disabled = true;
1303 </script>};
1304 
1305 enum DefaultFormTemplate = `<form action="%s" method="%s"%s>%s<input type="submit" value="Save"/></form>`;