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 }