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 diskuto.web;
12 import vibe.core.log;
13 import vibe.db.mongo.connection;
14 import vibe.http.fileserver;
15 import vibe.http.router;
16 import vibe.inet.url;
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 	import vibelog.internal.diskuto;
29 
30 	string sub_path = controller.settings.rootDir;
31 	assert(sub_path.endsWith("/"), "Blog site URL must end with '/'.");
32 
33 	if (sub_path.length > 1) router.get(sub_path[0 .. $-1], staticRedirect(sub_path));
34 
35 	auto diskuto = router.registerDiskuto(controller);
36 
37 	auto web = new VibeLogWeb(controller, diskuto);
38 
39 	auto websettings = new WebInterfaceSettings;
40 	websettings.urlPrefix = sub_path;
41 	router.registerWebInterface(web, websettings);
42 
43 	auto fsettings = new HTTPFileServerSettings;
44 	fsettings.serverPathPrefix = sub_path;
45 	router.get(sub_path ~ "*", serveStaticFiles("public", fsettings));
46 }
47 
48 
49 /// private
50 /*private*/ final class VibeLogWeb {
51 	private {
52 		VibeLogController m_ctrl;
53 		VibeLogSettings m_settings;
54 		DiskutoWeb m_diskuto;
55 		SessionVar!(string, "vibelog.loggedInUser") m_loggedInUser;
56 	}
57 
58 	this(VibeLogController controller, DiskutoWeb diskuto)
59 	{
60 		m_settings = controller.settings;
61 		m_ctrl = controller;
62 		m_diskuto = diskuto;
63 
64 		enforce(m_settings.rootDir.startsWith("/") && m_settings.rootDir.endsWith("/"), "All local URLs must start with and end with '/'.");
65 	}
66 
67 	//
68 	// public pages
69 	//
70 
71 	void get(int page = 1, string _error = null)
72 	{
73 		auto info = PageInfo(m_settings, m_ctrl.getPostListInfo(page - 1));
74 		info.refPath = m_settings.rootDir;
75 		info.loginError = _error;
76 		render!("vibelog.postlist.dt", info);
77 	}
78 
79 	@errorDisplay!getPost
80 	@path("posts/:postname")
81 	void getPost(string _postname, string _error)
82 	{
83 		m_diskuto.setupRequest();
84 
85 		auto info = PostInfo(m_settings);
86 		info.users = m_ctrl.db.getAllUsers();
87 		try info.post = m_ctrl.db.getPost(_postname);
88 		catch(Exception e){ return; } // -> gives 404 error
89 		if (m_settings.enableBackButton)
90 			info.postPage = m_ctrl.getPostPage(info.post.id);
91 		info.recentPosts = m_ctrl.getRecentPosts();
92 		info.refPath = m_settings.rootDir~"posts/"~_postname;
93 		info.error = _error;
94 		info.diskuto = m_diskuto;
95 
96 		render!("vibelog.post.dt", info);
97 	}
98 
99 	@path("posts/:postname/:filename")
100 	void getPostFile(string _postname, string _filename, HTTPServerResponse res)
101 	{
102 		import vibe.core.stream : pipe;
103 		import vibe.inet.mimetypes : getMimeTypeForFile;
104 
105 		auto f = m_ctrl.db.getFile(_postname, _filename);
106 		if (f) {
107 			res.contentType = getMimeTypeForFile(_filename);
108 			f.pipe(res.bodyWriter);
109 		}
110 	}
111 
112 	@path("feed/rss")
113 	void getRSSFeed(HTTPServerResponse res)
114 	{
115 		auto cfg = m_ctrl.config;
116 
117 		auto ch = new RssChannel;
118 		ch.title = cfg.feedTitle;
119 		ch.link = cfg.feedLink;
120 		ch.description = cfg.feedDescription;
121 		ch.copyright = cfg.copyrightString;
122 		ch.pubDate = Clock.currTime(UTC());
123 		ch.imageTitle = cfg.feedImageTitle;
124 		ch.imageUrl = cfg.feedImageUrl;
125 		ch.imageLink = cfg.feedLink;
126 
127 		m_ctrl.db.getPostsForCategory(cfg.categories, 0, (size_t i, Post p){
128 				if( !p.isPublic ) return true;
129 
130 				auto usr = m_ctrl.db.getUserByName(p.author);
131 
132 				auto itm = new RssEntry;
133 				itm.title = p.header;
134 				itm.description = p.subHeader;
135 				itm.link = m_settings.siteURL.toString() ~ "posts/" ~ p.name;
136 				itm.author = usr
137 					? usr.email ~ " (" ~ usr.name ~ ")"
138 					: "unknown@unknown.unknown (Unknown)";
139 				itm.guid = p.id.toString;
140 				itm.pubDate = p.date;
141 				ch.entries ~= itm;
142 				return i < 10;
143 			});
144 
145 		auto feed = new RssFeed;
146 		feed.channels ~= ch;
147 
148 		res.headers["Content-Type"] = "application/rss+xml";
149 		feed.render(res.bodyWriter);
150 	}
151 
152 	@path("/filter")
153 	void getFilter(string message, string filters, HTTPServerResponse res)
154 	{
155 		auto p = new Post;
156 		p.content = message;
157 		import std.array : split;
158 		p.filters = filters.split();
159 		res.writeBody(p.renderContentAsHtml(m_settings));
160 	}
161 
162 	@path("/sitemap.xml")
163 	void getSitemap(HTTPServerResponse res)
164 	{
165 		res.contentType = "application/xml";
166 		res.bodyWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
167 		res.bodyWriter.write("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
168 		void writeEntry(string[] parts...){
169 			res.bodyWriter.write("<url><loc>");
170 			res.bodyWriter.write(m_settings.siteURL.toString());
171 			foreach( p; parts )
172 				res.bodyWriter.write(p);
173 			res.bodyWriter.write("</loc></url>\n");
174 		}
175 
176 		// home page
177 		writeEntry();
178 
179 		m_ctrl.db.getPostsForCategory(m_ctrl.config.categories, 0, (size_t i, Post p){
180 				if( p.isPublic ) writeEntry("posts/", p.name);
181 				return true;
182 			});
183 		
184 		res.bodyWriter.write("</urlset>\n");
185 		res.bodyWriter.flush();
186 	}
187 
188 	@errorDisplay!get
189 	void postLogin(string username, string password, string redirect = null)
190 	{
191 		import vibelog.internal.passwordhash : validatePasswordHash;
192 
193 		auto usr = m_ctrl.db.getUserByName(username);
194 		enforce(usr && validatePasswordHash(usr.password, password),
195 			"Invalid user name or password.");
196 		m_loggedInUser = username;
197 		.redirect(redirect.length ? redirect : m_ctrl.settings.rootDir);
198 	}
199 
200 	void getLogout()
201 	{
202 		m_loggedInUser = null;
203 		redirect(m_ctrl.settings.rootDir);
204 	}
205 }
206 
207 import vibelog.info : VibeLogInfo;
208 struct PageInfo
209 {
210 	import vibelog.controller : PostListInfo;
211 	PostListInfo pli;
212 	alias pli this;
213 	string rootPath;
214 	string refPath;
215 	string loginError;
216 
217 	import vibelog.settings : VibeLogSettings;
218 	this(VibeLogSettings settings, PostListInfo pli)
219 	{
220 		this.pli = pli;
221 		this.rootPath = settings.siteURL.path.toString();
222 	}
223 }
224 
225 struct PostInfo
226 {
227 	string loginError;
228 
229 	import vibelog.info : VibeLogInfo;
230 	VibeLogInfo vli;
231 	alias vli this;
232 
233 	import vibelog.user : User;
234 	User[string] users;
235 
236 	import vibelog.settings : VibeLogSettings;
237 	VibeLogSettings settings;
238 
239 	import vibelog.post : Post;
240 	Post post;
241 
242 	int postPage;
243 
244 	DiskutoWeb diskuto;
245 
246 	Post[] recentPosts;
247 	string rootPath;
248 	string refPath;
249 	string error;
250 
251 	this(VibeLogSettings settings)
252 	{
253 		vli = VibeLogInfo(settings);
254 		this.settings = settings;
255 		this.rootPath = settings.siteURL.path.toString;
256 	}
257 }