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 }