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