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>`;