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 }