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