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