1 module vibelog.web; 2 3 public import vibelog.controller; 4 5 import vibelog.config; 6 import vibelog.post; 7 import vibelog.rss; 8 import vibelog.settings; 9 import vibelog.user; 10 11 import vibe.core.log; 12 import vibe.db.mongo.connection; 13 import vibe.http.fileserver; 14 import vibe.http.router; 15 import vibe.inet.url; 16 import vibe.templ.diet; 17 import vibe.textfilter.markdown; 18 import vibe.web.web; 19 20 import std.conv; 21 import std.datetime; 22 import std.exception; 23 import std.string; 24 25 26 void registerVibeLogWeb(URLRouter router, VibeLogController controller) 27 { 28 auto sub_path = controller.settings.siteURL.path.toString(); 29 assert(sub_path.endsWith("/"), "Blog site URL must end with '/'."); 30 31 if (sub_path.length > 1) router.get(sub_path[0 .. $-1], staticRedirect(sub_path)); 32 33 auto web = new VibeLogWeb(controller); 34 35 auto websettings = new WebInterfaceSettings; 36 websettings.urlPrefix = sub_path; 37 router.registerWebInterface(web, websettings); 38 39 auto fsettings = new HTTPFileServerSettings; 40 fsettings.serverPathPrefix = sub_path; 41 router.get(sub_path ~ "*", serveStaticFiles("public", fsettings)); 42 } 43 44 45 private final class VibeLogWeb { 46 private { 47 VibeLogController m_ctrl; 48 string m_subPath; 49 VibeLogSettings m_settings; 50 } 51 52 this(VibeLogController controller) 53 { 54 m_settings = controller.settings; 55 m_ctrl = controller; 56 m_subPath = controller.settings.siteURL.path.toString(); 57 58 enforce(m_subPath.startsWith("/") && m_subPath.endsWith("/"), "All local URLs must start with and end with '/'."); 59 } 60 61 // 62 // public pages 63 // 64 65 void get(int page = 1) 66 { 67 auto info = m_ctrl.getPostListInfo(page - 1); 68 render!("vibelog.postlist.dt", info); 69 } 70 71 @path("posts/:postname") 72 void getPost(string _postname) 73 { 74 struct ShowPostInfo { 75 string rootDir; 76 User[string] users; 77 VibeLogSettings settings; 78 Post post; 79 Comment[] comments; 80 Post[] recentPosts; 81 } 82 83 ShowPostInfo info; 84 info.rootDir = m_subPath; // TODO: use relative path 85 info.users = m_ctrl.db.getAllUsers(); 86 info.settings = m_settings; 87 try info.post = m_ctrl.db.getPost(_postname); 88 catch(Exception e){ return; } // -> gives 404 error 89 info.comments = m_ctrl.db.getComments(info.post.id); 90 info.recentPosts = m_ctrl.getRecentPosts(); 91 92 render!("vibelog.post.dt", info); 93 } 94 95 @path("posts/:postname/:filename") 96 void getPostFile(string _postname, string _filename, HTTPServerResponse res) 97 { 98 import vibe.inet.mimetypes; 99 auto f = m_ctrl.db.getFile(_postname, _filename); 100 if (f) { 101 res.contentType = getMimeTypeForFile(_filename); 102 res.bodyWriter.write(f); 103 } 104 } 105 106 @path("/posts/:postname/post_comment") 107 void postComment(string name, string email, string homepage, string message, string _postname, HTTPServerRequest req) 108 { 109 auto post = m_ctrl.db.getPost(_postname); 110 enforce(post.commentsAllowed, "Posting comments is not allowed for this article."); 111 112 auto c = new Comment; 113 c.isPublic = true; 114 c.date = Clock.currTime().toUTC(); 115 c.authorName = name; 116 c.authorMail = email; 117 c.authorHomepage = homepage; 118 c.authorIP = req.peer; 119 if (auto fip = "X-Forwarded-For" in req.headers) c.authorIP = *fip; 120 if (c.authorHomepage == "http://") c.authorHomepage = ""; 121 c.content = message; 122 m_ctrl.db.addComment(post.id, c); 123 124 redirect(m_subPath ~ "posts/" ~ post.name); 125 } 126 127 @path("feed/rss") 128 void getRSSFeed(HTTPServerResponse res) 129 { 130 auto cfg = m_ctrl.config; 131 132 auto ch = new RssChannel; 133 ch.title = cfg.feedTitle; 134 ch.link = cfg.feedLink; 135 ch.description = cfg.feedDescription; 136 ch.copyright = cfg.copyrightString; 137 ch.pubDate = Clock.currTime(UTC()); 138 ch.imageTitle = cfg.feedImageTitle; 139 ch.imageUrl = cfg.feedImageUrl; 140 ch.imageLink = cfg.feedLink; 141 142 m_ctrl.db.getPostsForCategory(cfg.categories, 0, (size_t i, Post p){ 143 if( !p.isPublic ) return true; 144 auto itm = new RssEntry; 145 itm.title = p.header; 146 itm.description = p.subHeader; 147 itm.link = m_settings.siteURL.toString() ~ "posts/" ~ p.name; 148 itm.author = p.author; 149 itm.guid = "xxyyzz"; 150 itm.pubDate = p.date; 151 ch.entries ~= itm; 152 return i < 10; 153 }); 154 155 auto feed = new RssFeed; 156 feed.channels ~= ch; 157 158 res.headers["Content-Type"] = "application/rss+xml"; 159 feed.render(res.bodyWriter); 160 } 161 162 void postMarkup(string message, HTTPServerRequest req, HTTPServerResponse res) 163 { 164 auto post = new Post; 165 post.content = message; 166 res.writeBody(post.renderContentAsHtml(m_settings, req.path), "text/html"); 167 } 168 169 @path("/sitemap.xml") 170 void getSitemap(HTTPServerResponse res) 171 { 172 res.contentType = "application/xml"; 173 res.bodyWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); 174 res.bodyWriter.write("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"); 175 void writeEntry(string[] parts...){ 176 res.bodyWriter.write("<url><loc>"); 177 res.bodyWriter.write(m_settings.siteURL.toString()); 178 foreach( p; parts ) 179 res.bodyWriter.write(p); 180 res.bodyWriter.write("</loc></url>\n"); 181 } 182 183 // home page 184 writeEntry(); 185 186 m_ctrl.db.getPostsForCategory(m_ctrl.config.categories, 0, (size_t i, Post p){ 187 if( p.isPublic ) writeEntry("posts/", p.name); 188 return true; 189 }); 190 191 res.bodyWriter.write("</urlset>\n"); 192 res.bodyWriter.flush(); 193 } 194 }