1 module vibelog.vibelog;
2 
3 import vibelog.dbcontroller;
4 import vibelog.rss;
5 
6 import vibe.db.mongo.db;
7 import vibe.http.auth.basic_auth;
8 import vibe.http.router;
9 import vibe.templ.diet;
10 import vibe.core.log;
11 
12 import std.conv;
13 import std.datetime;
14 import std.exception;
15 import std.string;
16 
17 class VibeLogSettings {
18 	string databaseHost = "localhost";
19 	ushort databasePort = MongoConnection.defaultPort;
20 	string databaseName = "vibelog";
21 	string configName = "default";
22 	int postsPerPage = 4;
23 	string basePath = "/";
24 	string function(string)[] textFilters;
25 }
26 
27 void registerVibeLog(VibeLogSettings settings, UrlRouter router)
28 {
29 	new VibeLog(settings, router);
30 }
31 
32 class VibeLog {
33 	private {
34 		DBController m_db;
35 		string m_subPath;
36 		VibeLogSettings m_settings;
37 		Config m_config;
38 	}
39 
40 	this(VibeLogSettings settings, UrlRouter router)
41 	{
42 		m_settings = settings;
43 		m_db = new DBController(settings.databaseHost, settings.databasePort, settings.databaseName);
44 		try m_config = m_db.getConfig(settings.configName, true);
45 		catch( Exception e ){
46 			logError("ERR: %s", e);
47 			throw e;
48 		}
49 		
50 		enforce(settings.basePath.startsWith("/"), "All local URLs must start with '/'.");
51 		if( !settings.basePath.endsWith("/") ) settings.basePath ~= "/";
52 
53 		m_subPath = settings.basePath;
54 
55 		//
56 		// public pages
57 		//
58 		if( m_subPath.length > 1 ) router.get(m_subPath[0 .. $-1], staticRedirect(m_subPath));
59 		router.get(m_subPath, &showPostList);
60 		router.get(m_subPath ~ "posts/:postname", &showPost);
61 		router.post(m_subPath ~ "posts/:postname/post_comment", &postComment);
62 		router.get(m_subPath ~ "feed/rss", &rssFeed);
63 
64 		//
65 		// restricted pages
66 		//
67 		router.get(m_subPath ~ "manage",                      auth(&showAdminPanel));
68 
69 		router.get(m_subPath ~ "configs/",                    auth(&showConfigList));
70 		router.get(m_subPath ~ "configs/:configname/edit",    auth(&showConfigEdit));
71 		router.post(m_subPath ~ "configs/:configname/put",    auth(&putConfig));
72 		router.post(m_subPath ~ "configs/:configname/delete", auth(&deleteConfig));
73 
74 		router.get(m_subPath ~ "users/",                      auth(&showUserList));
75 		router.get(m_subPath ~ "users/:username/edit",        auth(&showUserEdit));
76 		router.post(m_subPath ~ "users/:username/put",        auth(&putUser));
77 		router.post(m_subPath ~ "users/:username/delete",     auth(&deleteUser));
78 		router.post(m_subPath ~ "add_user",                   auth(&addUser));
79 
80 		router.get(m_subPath ~ "posts/",                      auth(&showEditPosts));
81 		router.get(m_subPath ~ "posts/:postname/edit",        auth(&showEditPost));
82 		router.post(m_subPath ~ "posts/:postname/put",        auth(&putPost));
83 		router.post(m_subPath ~ "posts/:postname/delete",     auth(&deletePost));
84 		router.post(m_subPath ~ "posts/:postname/set_comment_public", auth(&setCommentPublic));
85 		router.get(m_subPath ~ "make_post",                   auth(&showMakePost));
86 		router.post(m_subPath ~ "make_post",                  auth(&putPost));
87 	}
88 
89 	int getPageCount()
90 	{
91 		int cnt = m_db.countPostsForCategory(m_config.categories);
92 		return (cnt + m_settings.postsPerPage - 1) / m_settings.postsPerPage;
93 	}
94 
95 	Post[] getPostsForPage(int n)
96 	{
97 		Post[] ret;
98 		try {
99 			size_t cnt = 0;
100 			m_db.getPublicPostsForCategory(m_config.categories, n*m_settings.postsPerPage, (size_t i, Post p){
101 				ret ~= p;
102 				if( ++cnt >= m_settings.postsPerPage )
103 					return false;
104 				return true;
105 			});
106 		} catch( Exception e ){
107 			auto p = new Post;
108 			p.header = "ERROR";
109 			p.subHeader = e.msg;
110 			ret ~= p;
111 		}
112 		return ret;
113 	}
114 
115 	string getShowPagePath(int page)
116 	{
117 		return m_subPath ~ "?page=" ~ to!string(page+1);
118 	}
119 
120 	//
121 	// public pages
122 	//
123 
124 	protected void showPostList(HttpServerRequest req, HttpServerResponse res)
125 	{
126 		User[string] users = m_db.getAllUsers();
127 		int pageNumber = 0;
128 		auto pageCount = getPageCount();
129 		if( auto pp = "page" in req.query ) pageNumber = to!int(*pp)-1;
130 		else pageNumber = 0;
131 		auto posts = getPostsForPage(pageNumber);
132 		long[] commentCount;
133 		foreach( p; posts ) commentCount ~= m_db.getCommentCount(p.id);
134 		//res.render!("vibelog.postlist.dt", req, posts, pageNumber, pageCount)(res.bodyWriter);
135 		res.renderCompat!("vibelog.postlist.dt",
136 			HttpServerRequest, "req",
137 			User[string], "users",
138 			Post[], "posts",
139 			long[], "commentCount",
140 			string function(string)[], "textFilters",
141 			int, "pageNumber",
142 			int, "pageCount")
143 			(Variant(req), Variant(users), Variant(posts), Variant(commentCount), Variant(m_settings.textFilters), Variant(pageNumber), Variant(pageCount));
144 	}
145 
146 	protected void showPost(HttpServerRequest req, HttpServerResponse res)
147 	{
148 		User[string] users = m_db.getAllUsers();
149 		Post post;
150 		try post = m_db.getPost(req.params["postname"]);
151 		catch(Exception e){ return; } // -> gives 404 error
152 		Comment[] comments = m_db.getComments(post.id);
153 		//res.render!("vibelog.post.dt", req, users, post, textFilters);
154 		res.renderCompat!("vibelog.post.dt",
155 			HttpServerRequest, "req",
156 			User[string], "users",
157 			Post, "post",
158 			Comment[], "comments",
159 			string function(string)[], "textFilters")
160 			(Variant(req), Variant(users), Variant(post), Variant(comments), Variant(m_settings.textFilters));
161 	}
162 
163 	protected void postComment(HttpServerRequest req, HttpServerResponse res)
164 	{
165 		auto post = m_db.getPost(req.params["postname"]);
166 		enforce(post.commentsAllowed, "Posting comments is not allowed for this article.");
167 
168 		auto c = new Comment;
169 		c.isPublic = true;
170 		c.date = Clock.currTime().toUTC();
171 		c.authorName = req.form["name"];
172 		c.authorMail = req.form["email"];
173 		c.authorHomepage = req.form["homepage"];
174 		c.authorIP = req.peer;
175 		if( auto fip = "X-Forwarded-For" in req.headers ) c.authorIP = *fip;
176 		if( c.authorHomepage == "http://" ) c.authorHomepage = "";
177 		c.content = req.form["message"];
178 		m_db.addComment(post.id, c);
179 
180 		res.redirect(m_subPath ~ "posts/"~post.name);
181 	}
182 
183 	protected void rssFeed(HttpServerRequest req, HttpServerResponse res)
184 	{
185 		auto ch = new RssChannel;
186 		ch.title = m_config.feedTitle;
187 		ch.link = m_config.feedLink;
188 		ch.description = m_config.feedDescription;
189 		ch.copyright = m_config.copyrightString;
190 		ch.pubDate = Clock.currTime(UTC());
191 		ch.imageTitle = m_config.feedImageTitle;
192 		ch.imageUrl = m_config.feedImageUrl;
193 		ch.imageLink = m_config.feedLink;
194 
195 		m_db.getPostsForCategory(m_config.categories, 0, (size_t i, Post p){
196 				if( !p.isPublic ) return true;
197 				auto itm = new RssEntry;
198 				itm.title = p.header;
199 				itm.description = p.subHeader;
200 				itm.link = "http://vibed.org/blog/posts/"~p.name;
201 				itm.author = p.author;
202 				itm.guid = "xxyyzz";
203 				itm.pubDate = p.date;
204 				ch.entries ~= itm;
205 				return i < 10;
206 			});
207 
208 		auto feed = new RssFeed;
209 		feed.channels ~= ch;
210 
211 		res.headers["Content-Type"] = "application/rss+xml";
212 		feed.render(res.bodyWriter);
213 	}
214 
215 	protected HttpServerRequestDelegate auth(void delegate(HttpServerRequest, HttpServerResponse, User[string], User) del)
216 	{
217 		return (HttpServerRequest req, HttpServerResponse res)
218 		{
219 			User[string] users = m_db.getAllUsers();
220 			bool testauth(string user, string password)
221 			{
222 				auto pu = user in users;
223 				if( pu is null ) return false;
224 				return testPassword(password, pu.password);
225 			}
226 			string username = performBasicAuth(req, res, "VibeLog admin area", &testauth);
227 			auto pusr = username in users;
228 			assert(pusr, "Authorized with unknown username !?");
229 			del(req, res, users, *pusr);
230 		};
231 	}
232 
233 	protected void showAdminPanel(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
234 	{
235 		res.renderCompat!("vibelog.admin.dt",
236 			HttpServerRequest, "req",
237 			User[string], "users",
238 			User, "loginUser")
239 			(Variant(req), Variant(users), Variant(loginUser));
240 	}
241 
242 	//
243 	// Configs
244 	//
245 
246 	protected void showConfigList(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
247 	{
248 		enforce(loginUser.isConfigAdmin());
249 		Config[] configs = m_db.getAllConfigs();
250 		res.renderCompat!("vibelog.editconfiglist.dt",
251 			HttpServerRequest, "req",
252 			User, "loginUser",
253 			Config[], "configs")
254 			(Variant(req), Variant(loginUser), Variant(configs));
255 	}
256 
257 	protected void showConfigEdit(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
258 	{
259 		auto globalConfig = m_db.getConfig("global", true);
260 		enforce(loginUser.isConfigAdmin());
261 		Config config = m_db.getConfig(req.params["configname"]);
262 		res.renderCompat!("vibelog.editconfig.dt",
263 			HttpServerRequest, "req",
264 			User, "loginUser",
265 			Config, "globalConfig",
266 			Config, "config")
267 			(Variant(req), Variant(loginUser), Variant(globalConfig), Variant(config));
268 	}
269 
270 	protected void putConfig(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
271 	{
272 		enforce(loginUser.isConfigAdmin());
273 		Config cfg = m_db.getConfig(req.params["configname"]);
274 		if( cfg.name == "global" )
275 			cfg.categories = req.form["categories"].splitLines();
276 		else {
277 			cfg.categories = null;
278 			foreach( k, v; req.form ){
279 				if( k.startsWith("category_") )
280 					cfg.categories ~= k[9 .. $];
281 			}
282 		}
283 		cfg.language = req.form["language"];
284 		cfg.copyrightString = req.form["copyrightString"];
285 		cfg.feedTitle = req.form["feedTitle"];
286 		cfg.feedLink = req.form["feedLink"];
287 		cfg.feedDescription = req.form["feedDescription"];
288 		cfg.feedImageTitle = req.form["feedImageTitle"];
289 		cfg.feedImageUrl = req.form["feedImageUrl"];
290 	
291 		m_db.setConfig(cfg);
292 
293 		m_config = cfg;
294 		res.redirect(m_subPath ~ "configs/");
295 	}
296 
297 	protected void deleteConfig(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
298 	{
299 		enforce(loginUser.isConfigAdmin());
300 		m_db.deleteConfig(req.params["configname"]);
301 		res.redirect(m_subPath ~ "configs/");
302 	}
303 
304 
305 	//
306 	// Users
307 	//
308 
309 	protected void showUserList(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
310 	{
311 		res.renderCompat!("vibelog.edituserlist.dt",
312 			HttpServerRequest, "req",
313 			User, "loginUser",
314 			User[string], "users")
315 			(Variant(req), Variant(loginUser), Variant(users));
316 	}
317 
318 	protected void showUserEdit(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
319 	{
320 		auto globalConfig = m_db.getConfig("global", true);
321 		User user = m_db.getUser(req.params["username"]);
322 		res.renderCompat!("vibelog.edituser.dt",
323 			HttpServerRequest, "req",
324 			User, "loginUser",
325 			Config, "globalConfig",
326 			User, "user")
327 			(Variant(req), Variant(loginUser), Variant(globalConfig), Variant(user));
328 	}
329 
330 	protected void putUser(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
331 	{
332 		auto id = req.form["id"];
333 		User usr;
334 		if( id.length > 0 ){
335 			enforce(loginUser.isUserAdmin() || req.form["username"] == loginUser.username,
336 				"You can only change your own account.");
337 			usr = m_db.getUser(BsonObjectID.fromHexString(id));
338 			enforce(usr.username == req.form["username"], "Cannot change the user name!");
339 		} else {
340 			enforce(loginUser.isUserAdmin(), "You are not allowed to add users.");
341 			usr = new User;
342 			usr.username = req.form["username"];
343 			foreach( u; users )
344 				enforce(u.username != usr.username, "A user with the specified user name already exists!");
345 		}
346 		enforce(req.form["password"] == req.form["passwordConfirmation"], "Passwords do not match!");
347 
348 		usr.name = req.form["name"];
349 		usr.email = req.form["email"];
350 
351 		if( req.form["password"].length || req.form["passwordConfirmation"].length ){
352 			enforce(loginUser.isUserAdmin() || testPassword(req.form["oldPassword"], usr.password), "Old password does not match.");
353 			usr.password = generatePasswordHash(req.form["password"]);
354 		}
355 
356 		if( loginUser.isUserAdmin() ){
357 			usr.groups = null;
358 			foreach( k, v; req.form ){
359 				if( k.startsWith("group_") )
360 					usr.groups ~= k[6 .. $];
361 			}
362 
363 			usr.allowedCategories = null;
364 			foreach( k, v; req.form ){
365 				if( k.startsWith("category_") )
366 					usr.allowedCategories ~= k[9 .. $];
367 			}
368 		}
369 
370 		if( id.length > 0 ){
371 			m_db.modifyUser(usr);
372 		} else {
373 			usr._id = m_db.addUser(usr);
374 		}
375 
376 		if( loginUser.isUserAdmin() ) res.redirect(m_subPath~"users/");
377 		else res.redirect(m_subPath~"manage");
378 	}
379 
380 	protected void deleteUser(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
381 	{
382 		enforce(loginUser.isUserAdmin(), "You are not authorized to delete users!");
383 		enforce(loginUser.username != req.form["username"], "Cannot delete the own user account!");
384 		foreach( usr; users )
385 			if( usr.username == req.form["username"] ){
386 				m_db.deleteUser(usr._id);
387 				res.redirect(m_subPath ~ "edit_posts");
388 				return;
389 			}
390 		enforce(false, "Unknown user name.");
391 	}
392 
393 	protected void addUser(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
394 	{
395 		enforce(loginUser.isUserAdmin(), "You are not authorized to add users!");
396 		string uname = req.form["username"];
397 		if( uname !in users ){
398 			auto u = new User;
399 			u.username = uname;
400 			m_db.addUser(u);
401 		}
402 		res.redirect(m_subPath ~ "users/" ~ uname ~ "/edit");
403 	}
404 
405 	//
406 	// Posts
407 	//
408 
409 	protected void showEditPosts(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
410 	{
411 		Post[] posts;
412 		m_db.getAllPosts(0, (size_t idx, Post post){
413 			if( loginUser.isPostAdmin() || post.author == loginUser.username
414 				|| loginUser.mayPostInCategory(post.category) )
415 			{
416 				posts ~= post;
417 			}
418 			return true;
419 		});
420 		logInfo("Showing %d posts.", posts.length);
421 		//parseJadeFile!("vibelog.postlist.dt", req, posts, pageNumber, pageCount)(res.bodyWriter);
422 		res.renderCompat!("vibelog.editpostslist.dt",
423 			HttpServerRequest, "req",
424 			User[string], "users",
425 			User, "loginUser",
426 			Post[], "posts")
427 			(Variant(req), Variant(users), Variant(loginUser), Variant(posts));
428 	}
429 
430 	protected void showMakePost(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
431 	{
432 		auto globalConfig = m_db.getConfig("global", true);
433 		Post post;
434 		Comment[] comments;
435 		res.renderCompat!("vibelog.editpost.dt",
436 			HttpServerRequest, "req",
437 			User[string], "users",
438 			User, "loginUser",
439 			Config, "globalConfig",
440 			Post, "post",
441 			Comment[], "comments")
442 			(Variant(req), Variant(users), Variant(loginUser), Variant(globalConfig), Variant(post), Variant(comments));
443 	}
444 
445 	protected void showEditPost(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
446 	{
447 		auto globalConfig = m_db.getConfig("global", true);
448 		auto post = m_db.getPost(req.params["postname"]);
449 		auto comments = m_db.getComments(post.id, true);
450 		res.renderCompat!("vibelog.editpost.dt",
451 			HttpServerRequest, "req",
452 			User[string], "users",
453 			User, "loginUser",
454 			Config, "globalConfig",
455 			Post, "post",
456 			Comment[], "comments")
457 			(Variant(req), Variant(users), Variant(loginUser), Variant(globalConfig), Variant(post), Variant(comments));
458 	}
459 
460 	protected void deletePost(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
461 	{
462 		auto id = BsonObjectID.fromHexString(req.form["id"]);
463 		m_db.deletePost(id);
464 		res.redirect(m_subPath ~ "posts/");
465 	}
466 
467 	protected void setCommentPublic(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
468 	{
469 		auto id = BsonObjectID.fromHexString(req.form["id"]);
470 		m_db.setCommentPublic(id, to!int(req.form["public"]) != 0);
471 		res.redirect(m_subPath ~ "posts/"~req.params["postname"]~"/edit");
472 	}
473 
474 	protected void putPost(HttpServerRequest req, HttpServerResponse res, User[string] users, User loginUser)
475 	{
476 		auto id = req.form["id"];
477 		Post p;
478 		if( id.length > 0 ){
479 			p = m_db.getPost(BsonObjectID.fromHexString(id));
480 			enforce(req.params["postname"] == p.name, "URL does not match the edited post!");
481 		} else {
482 			p = new Post;
483 			p.category = "default";
484 			p.date = Clock.currTime().toUTC();
485 		}
486 		enforce(loginUser.mayPostInCategory(req.form["category"]), "You are now allowed to post in the '"~req.form["category"]~"' category.");
487 
488 		p.isPublic = ("isPublic" in req.form) !is null;
489 		p.commentsAllowed = ("commentsAllowed" in req.form) !is null;
490 		p.author = req.form["author"];
491 		p.category = req.form["category"];
492 		p.slug = req.form["slug"].length ? req.form["slug"] : makeSlugFromHeader(req.form["header"]);
493 		p.headerImage = req.form["headerImage"];
494 		p.header = req.form["header"];
495 		p.subHeader = req.form["subHeader"];
496 		p.content = req.form["content"];
497 
498 		enforce(!m_db.hasPost(p.slug) || m_db.getPost(p.slug).id == p.id);
499 
500 		if( id.length > 0 ){
501 			m_db.modifyPost(p);
502 			req.params["postname"] = p.name;
503 		} else {
504 			p.id = m_db.addPost(p);
505 		}
506 		res.redirect(m_subPath~"posts/");
507 	}
508 }