1 /// Written in the D programming language. 2 3 module semitwistWeb.doc; 4 5 import std.array; 6 import std.conv; 7 import std.file; 8 import std.range; 9 import std.stdio; 10 import std.traits; 11 import std.typecons; 12 import std.typetuple; 13 14 import vibe.vibe; 15 import semitwist.util.all; 16 17 import semitwistWeb.conf; 18 import semitwistWeb.db; 19 import semitwistWeb.session; 20 import semitwistWeb.util; 21 import semitwistWeb.handler; //TODO: Only needed for BaseHandler.noCache, eliminate this import. 22 23 void clearDocHelperCache() 24 { 25 mustache.clearCache(); 26 } 27 28 void addCommonContext(Mustache.Context c, SessionData sess) 29 { 30 // Basic information 31 string mainFrame(string content) 32 { 33 c["pageBody"] = content; 34 return mustache.render("frame-main", c); 35 } 36 37 c["mainFrame"] = &mainFrame; 38 c["urlBase"] = conf.urlBase; 39 c["staticsUrl"] = conf.staticsUrl; 40 c["stylesheetFilename"] = conf.staticsUrl ~ "style.css"; 41 42 // Session information 43 c.useSection(sess.isLoggedIn? "loggedIn" : "loggedOut"); 44 if(sess.oneShotMessage != "") 45 { 46 c.useSection("hasPageMessage"); 47 c["pageMessage"] = sess.oneShotMessage; 48 } 49 50 // Pages 51 foreach(page; PageBase.registeredPages) 52 { 53 if(page.numParams == 0) 54 c[page.viewName] = page.buildUrl(); 55 else if(page.numParams == 1) 56 c[page.viewName] = (string content) => page.buildUrl( content ); 57 else 58 c[page.viewName] = (string content) => page.buildUrl( content.split("|") ); 59 } 60 } 61 62 struct HtmlTemplateAccess 63 { 64 /// lookup[templateName] == html source after the form system's adjustments 65 private static string[string] lookup; 66 67 /+ 68 Optional callback to be run immediately after each template is loaded. 69 The name of template loaded, and it's filePath, are passed in as params. 70 The delegate may freely read 'HtmlTemplateAccess[templateName]', 71 and must return a new value for 'HtmlTemplateAccess[templateName]'. 72 73 Example: 74 ------------- 75 HtmlTemplateAccess.onLoad = delegate string(string templateName, string filePath) { 76 // Do nothing: 77 return HtmlTemplateAccess[templateName]; 78 }; 79 ------------- 80 81 Example: 82 ------------- 83 HtmlTemplateAccess.onLoad = delegate string(string templateName, string filePath) { 84 // Replace the entire template: 85 return "Hello World, your HTML template has been replaced with this."; 86 }; 87 ------------- 88 +/ 89 static string delegate(string templateName, string filePath) onLoad; 90 91 /// Retreive an html template. Automatically loads, caches, and calls 92 /// HtmlTemplateAccess.onLoad (if exists) if the template hasn't already been loaded. 93 static string opIndex(string templateName) 94 { 95 static bool isInCallback = false; // Avoid infinite recursion 96 97 if(!isInCallback) 98 if(templateName !in lookup || BaseHandler.noCache) 99 { 100 import std.file : read; 101 import std.path : buildPath; 102 103 auto filePath = buildPath(mustache.path, templateName ~ "." ~ mustache.ext); 104 lookup[templateName] = cast(string)read(filePath); 105 106 if(onLoad) 107 { 108 isInCallback = true; 109 lookup[templateName] = onLoad(templateName, filePath); 110 isInCallback = false; 111 } 112 } 113 114 return lookup[templateName]; 115 } 116 } 117 118 /+ 119 private ref string pageSelect(alias page)(LoginState state) 120 { 121 if(state == LoginState.Out) 122 return page!(LoginState.Out); 123 else 124 return page!(LoginState.In); 125 } 126 +/ 127 128 struct DefinePage 129 { 130 string method; 131 string dispatcher; 132 string name; 133 string urlRoute; 134 string targs; 135 136 this(string method, string dispatcher, string name, string urlRoute, string targs="") 137 { 138 this.method = method; 139 this.dispatcher = dispatcher; 140 this.name = name; 141 this.urlRoute = urlRoute; 142 this.targs = targs; 143 } 144 145 string _makePageStr; /// Treat as private 146 } 147 148 string definePages(DefinePage[] pages) 149 { 150 string str; 151 152 foreach(ref page; pages) 153 { 154 auto method = page.method=="ANY"? "Nullable!HTTPMethod()" : "Nullable!HTTPMethod(HTTPMethod."~page.method~")"; 155 page._makePageStr = "makePage!("~method~", "~page.dispatcher~", `"~page.name~"`, `"~page.urlRoute~"`, "~page.targs~")()"; 156 } 157 158 str ~= "import std.typecons : Nullable;\n"; 159 160 foreach(page; pages) 161 str ~= "Page!("~page.targs~") page_"~page.name~";\n"; 162 163 foreach(page; pages) 164 str ~= 165 "template page(string name) if(name == `"~page.name~"`)\n"~ 166 " { alias page_"~page.name~" page; }\n"; 167 168 str ~= "void initPages() {\n"; 169 170 foreach(page; pages) 171 str ~= " page!`"~page.name~"` = "~page._makePageStr~";\n"; 172 173 foreach(page; pages) 174 str ~= " page!`"~page.name~"`.register();\n"; 175 176 str ~= "}\n"; 177 178 return str.replace("\n", ""); 179 } 180 181 auto makePage(alias method, alias dispatcher, string name, string urlRoute, TArgs...)() 182 { 183 enum sections = getUrlRouteSections(urlRoute); 184 static assert( 185 TArgs.length == sections.length-1, 186 "Wrong number of argument types for makePage:\n"~ 187 " URL route: "~urlRoute~"\n"~ 188 " Received "~to!string(TArgs.length)~" args but expected "~to!string(sections.length-1)~"." 189 ); 190 191 return new Page!(TArgs)(name, sections, urlRoute, method, (a,b) => dispatcher!name(a,b)); 192 } 193 194 private string[] getUrlRouteSections(string urlRoute) 195 { 196 enum State { normal, tag, wildcard } 197 198 string[] sections; 199 size_t sectionStart = 0; 200 State state; 201 202 urlRoute = urlRoute ~ '\0'; 203 foreach(i; 0..urlRoute.length) 204 { 205 final switch(state) 206 { 207 case State.normal: 208 if(urlRoute[i] == ':') 209 { 210 sections ~= urlRoute[sectionStart..i]; 211 state = State.tag; 212 } 213 else if(urlRoute[i] == '*') 214 { 215 sections ~= urlRoute[sectionStart..i]; 216 state = State.wildcard; 217 } 218 break; 219 220 case State.tag: 221 case State.wildcard: 222 if(urlRoute[i] == '/' || urlRoute[i] == '\0') 223 { 224 state = State.normal; 225 sectionStart = i; 226 } 227 else 228 { 229 if(state == State.wildcard) 230 throw new Exception("Unexpected character in urlRoute after *: Expected / or end-of-string"); 231 } 232 break; 233 } 234 } 235 236 if(sectionStart == urlRoute.length) 237 sections ~= ""; 238 else 239 sections ~= urlRoute[sectionStart..$-1]; // Exclude the added \0 240 241 return sections; 242 } 243 244 alias void delegate(HTTPServerRequest, HTTPServerResponse) PageHandler; 245 246 abstract class PageBase 247 { 248 static string loginPageName; 249 250 protected string _name; 251 final @property string name() 252 { 253 return _name; 254 } 255 256 protected string _viewName; 257 final @property string viewName() 258 { 259 return _viewName; 260 } 261 262 /// HTTP Handler 263 PageHandler handler; 264 265 /// Null implies "any method" 266 Nullable!HTTPMethod method; 267 268 protected int _numParams; 269 final @property int numParams() 270 { 271 return _numParams; 272 } 273 274 protected string[] urlSections; 275 276 protected string _urlRouteRelativeToBase; 277 final @property string urlRouteRelativeToBase() 278 { 279 return _urlRouteRelativeToBase; 280 } 281 final @property string urlRouteAbsolute() 282 { 283 return conf.urlBase ~ _urlRouteRelativeToBase; 284 } 285 286 private static PageBase[string] registeredPages; 287 static PageBase get(string name) 288 { 289 return registeredPages[name]; 290 } 291 292 private static string[] registeredPageNames; 293 private static bool registeredPageNamesInited = false; 294 static string[] getNames() 295 { 296 if(!registeredPageNamesInited) 297 { 298 registeredPageNames = registeredPages.keys; 299 registeredPageNamesInited = true; 300 } 301 302 return registeredPageNames; 303 } 304 305 final void register() 306 { 307 if(_name in registeredPages) 308 throw new Exception(text("A page named '", _name, "' is already registered.")); 309 310 registeredPages[_name] = this; 311 registeredPageNames = null; 312 registeredPageNamesInited = false; 313 } 314 315 static bool isRegistered(string pageName) 316 { 317 return !!(pageName in registeredPages); 318 } 319 320 final bool isRegistered() 321 { 322 return 323 _name in registeredPages && this == registeredPages[_name]; 324 } 325 326 final void unregister() 327 { 328 if(!isRegistered()) 329 { 330 if(_name in registeredPages) 331 throw new Exception(text("Cannot unregister page '", _name, "' because it's unequal to the registered page of the same name.")); 332 else 333 throw new Exception(text("Cannot unregister page '", _name, "' because it's not registered.")); 334 } 335 336 registeredPages.remove(_name); 337 registeredPageNames = null; 338 registeredPageNamesInited = false; 339 } 340 341 static void validateLoginPageName() 342 { 343 if(loginPageName == "") 344 throw new Exception("Required value was not set: PageBase.loginPageName"); 345 346 if(!isRegistered(loginPageName)) 347 throw new Exception("PageBase.loginPageName is not a registered page: '"~PageBase.loginPageName~"'"); 348 } 349 350 /// This is a low-level tool provided for the sake of generic code. 351 /// In general, you should use 'Page.url' or 'Page.urlSink' instead of this 352 /// because those verify both the types and number of args at compile-time. 353 /// This, however, only verifies number of args, and only at runtime. 354 final string buildUrl()(string[] args...) 355 { 356 if(args.length != _numParams) 357 throw new Exception(text("Expected ", _numParams, " args, not ", args.length)); 358 359 if(_numParams == 0) 360 return urlSections[0]; 361 362 Appender!string sink; 363 buildUrl(sink, args); 364 return sink.data; 365 } 366 367 ///ditto 368 final void buildUrl(Sink)(ref Sink sink, string[] args...) if(isOutputRange!(Sink, string)) 369 { 370 if(args.length != _numParams) 371 throw new Exception(text("Expected ", _numParams, " args, not ", args.length)); 372 373 size_t index=0; 374 foreach(arg; args) 375 { 376 sink.put(urlSections[index]); 377 sink.put(arg); 378 index++; 379 } 380 381 sink.put(urlSections[$-1]); 382 } 383 } 384 385 final class Page(TArgs...) : PageBase 386 { 387 enum numSections = TArgs.length+1; 388 389 private this( 390 string name, 391 string[numSections] urlSections, string urlRouteRelativeToBase, 392 Nullable!HTTPMethod method, PageHandler handler 393 ) 394 { 395 this._name = name; 396 this._viewName = "page-" ~ name; 397 this.urlSections = urlSections[].dup; 398 this._urlRouteRelativeToBase = urlRouteRelativeToBase; 399 this.handler = handler; 400 this._numParams = TArgs.length; 401 402 if(method.isNull) 403 this.method.nullify(); 404 else 405 this.method = method; 406 407 this.urlSections[0] = conf.urlBase ~ this.urlSections[0]; 408 } 409 410 string url(TArgs args) 411 { 412 if(_numParams == 0) 413 return urlSections[0]; 414 415 Appender!string sink; 416 417 // Workaround for DMD Issue #9894 418 //urlSink(sink, args); 419 this.callUrlSink(sink, args); 420 421 return sink.data; 422 } 423 424 void urlSink(Sink)(ref Sink sink, TArgs args) if(isOutputRange!(Sink, string)) 425 { 426 size_t index=0; 427 foreach(arg; args) 428 { 429 sink.put(urlSections[index]); 430 sink.put(to!string(arg)); 431 index++; 432 } 433 434 sink.put(urlSections[$-1]); 435 } 436 } 437 438 // Workaround for DMD Issue #9894 439 //TODO: Remove this workaround once DMD 2.063 is required 440 private void callUrlSink(TPage, Sink, TArgs...)(TPage p, ref Sink sink, TArgs args) 441 { 442 p.urlSink!(Sink)(sink, args); 443 } 444 445 void addPage(URLRouter router, PageBase page) 446 { 447 void addPageImpl(string urlRoute) 448 { 449 if(page.method.isNull) 450 router.any(urlRoute, page.handler); 451 else 452 router.match(page.method.get(), urlRoute, page.handler); 453 } 454 455 addPageImpl(page.urlRouteAbsolute); 456 457 if(page.urlRouteAbsolute == conf.urlBase && conf.urlBase != "") 458 addPageImpl(conf.urlBase[0..$-1]); // Sans trailing slash 459 } 460 461 // For the unittests below 462 string testDispatcherResult; 463 private void testDispatcher(string pageName)(HTTPServerRequest req, HTTPServerResponse res) 464 { 465 testDispatcherResult = "Did "~pageName; 466 } 467 468 //TODO: This can probably be changed to a normal unittest once using Vibe.d v0.7.14 469 void runDocHelperUnittest() 470 { 471 import std.stdio; 472 writeln("Unittest: docHelper.makePage"); stdout.flush(); 473 474 enum m = Nullable!HTTPMethod(); 475 476 auto p1 = makePage!(m, testDispatcher, "name", "client/*/observer/:oid/survey", int, int)(); 477 assert(p1.url(10, 20) == conf.urlBase~"client/10/observer/20/survey"); 478 assert(p1.url(111, 222) == conf.urlBase~"client/111/observer/222/survey"); 479 480 static assert( __traits( compiles, p1.url(111, 222) )); 481 static assert(!__traits( compiles, p1.url(111, `222`) )); 482 static assert(!__traits( compiles, p1.url(111) )); 483 static assert(!__traits( compiles, p1.url(111, 222, 333) )); 484 485 static assert( __traits( compiles, makePage!(m, testDispatcher, "name", "client/*/observer/:oid/survey", int, int )() )); 486 static assert( __traits( compiles, makePage!(m, testDispatcher, "name", "client/*/observer/:oid/survey", int, string )() )); 487 static assert(!__traits( compiles, makePage!(m, testDispatcher, "name", "client/*/observer/:oid/survey", int )() )); 488 static assert(!__traits( compiles, makePage!(m, testDispatcher, "name", "client/*/observer/:oid/survey", int, int, int )() )); 489 490 assert(makePage!(m, testDispatcher, "name", "client") ().url() == conf.urlBase~"client"); 491 assert(makePage!(m, testDispatcher, "name", "") ().url() == conf.urlBase~""); 492 assert(makePage!(m, testDispatcher, "name", "client/*", string) ().url("hello") == conf.urlBase~"client/hello"); 493 assert(makePage!(m, testDispatcher, "name", "client/:foo", int) ().url(5) == conf.urlBase~"client/5"); 494 assert(makePage!(m, testDispatcher, "name", "*", int) ().url(5) == conf.urlBase~"5"); 495 }