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 }