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