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 		info.recentPosts = m_ctrl.getRecentPosts();
90 		info.refPath = m_settings.rootDir~"posts/"~_postname;
91 		info.error = _error;
92 		info.diskuto = m_diskuto;
93 
94 		render!("vibelog.post.dt", info);
95 	}
96 
97 	@path("posts/:postname/:filename")
98 	void getPostFile(string _postname, string _filename, HTTPServerResponse res)
99 	{
100 		import vibe.core.stream : pipe;
101 		import vibe.inet.mimetypes : getMimeTypeForFile;
102 
103 		auto f = m_ctrl.db.getFile(_postname, _filename);
104 		if (f) {
105 			res.contentType = getMimeTypeForFile(_filename);
106 			f.pipe(res.bodyWriter);
107 		}
108 	}
109 
110 	@path("feed/rss")
111 	void getRSSFeed(HTTPServerResponse res)
112 	{
113 		auto cfg = m_ctrl.config;
114 
115 		auto ch = new RssChannel;
116 		ch.title = cfg.feedTitle;
117 		ch.link = cfg.feedLink;
118 		ch.description = cfg.feedDescription;
119 		ch.copyright = cfg.copyrightString;
120 		ch.pubDate = Clock.currTime(UTC());
121 		ch.imageTitle = cfg.feedImageTitle;
122 		ch.imageUrl = cfg.feedImageUrl;
123 		ch.imageLink = cfg.feedLink;
124 
125 		m_ctrl.db.getPostsForCategory(cfg.categories, 0, (size_t i, Post p){
126 				if( !p.isPublic ) return true;
127 				auto itm = new RssEntry;
128 				itm.title = p.header;
129 				itm.description = p.subHeader;
130 				itm.link = m_settings.siteURL.toString() ~ "posts/" ~ p.name;
131 				itm.author = p.author;
132 				itm.guid = "xxyyzz";
133 				itm.pubDate = p.date;
134 				ch.entries ~= itm;
135 				return i < 10;
136 			});
137 
138 		auto feed = new RssFeed;
139 		feed.channels ~= ch;
140 
141 		res.headers["Content-Type"] = "application/rss+xml";
142 		feed.render(res.bodyWriter);
143 	}
144 
145 	@path("/filter")
146 	void getFilter(string message, string filters, HTTPServerResponse res)
147 	{
148 		auto p = new Post;
149 		p.content = message;
150 		import std.array : split;
151 		p.filters = filters.split();
152 		res.writeBody(p.renderContentAsHtml(m_settings));
153 	}
154 
155 	@path("/sitemap.xml")
156 	void getSitemap(HTTPServerResponse res)
157 	{
158 		res.contentType = "application/xml";
159 		res.bodyWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
160 		res.bodyWriter.write("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
161 		void writeEntry(string[] parts...){
162 			res.bodyWriter.write("<url><loc>");
163 			res.bodyWriter.write(m_settings.siteURL.toString());
164 			foreach( p; parts )
165 				res.bodyWriter.write(p);
166 			res.bodyWriter.write("</loc></url>\n");
167 		}
168 
169 		// home page
170 		writeEntry();
171 
172 		m_ctrl.db.getPostsForCategory(m_ctrl.config.categories, 0, (size_t i, Post p){
173 				if( p.isPublic ) writeEntry("posts/", p.name);
174 				return true;
175 			});
176 		
177 		res.bodyWriter.write("</urlset>\n");
178 		res.bodyWriter.flush();
179 	}
180 
181 	@errorDisplay!get
182 	void postLogin(string username, string password, string redirect = null)
183 	{
184 		import vibelog.internal.passwordhash : validatePasswordHash;
185 
186 		auto usr = m_ctrl.db.getUserByName(username);
187 		enforce(usr && validatePasswordHash(usr.password, password),
188 			"Invalid user name or password.");
189 		m_loggedInUser = username;
190 		.redirect(redirect.length ? redirect : m_ctrl.settings.rootDir);
191 	}
192 
193 	void getLogout()
194 	{
195 		m_loggedInUser = null;
196 		redirect(m_ctrl.settings.rootDir);
197 	}
198 }
199 
200 import vibelog.info : VibeLogInfo;
201 struct PageInfo
202 {
203 	import vibelog.controller : PostListInfo;
204 	PostListInfo pli;
205 	alias pli this;
206 	string rootPath;
207 	string refPath;
208 	string loginError;
209 
210 	import vibelog.settings : VibeLogSettings;
211 	this(VibeLogSettings settings, PostListInfo pli)
212 	{
213 		this.pli = pli;
214 		this.rootPath = settings.siteURL.path.toString();
215 	}
216 }
217 
218 struct PostInfo
219 {
220 	string loginError;
221 
222 	import vibelog.info : VibeLogInfo;
223 	VibeLogInfo vli;
224 	alias vli this;
225 
226 	import vibelog.user : User;
227 	User[string] users;
228 
229 	import vibelog.settings : VibeLogSettings;
230 	VibeLogSettings settings;
231 
232 	import vibelog.post : Post;
233 	Post post;
234 
235 	DiskutoWeb diskuto;
236 
237 	Post[] recentPosts;
238 	string rootPath;
239 	string refPath;
240 	string error;
241 
242 	this(VibeLogSettings settings)
243 	{
244 		vli = VibeLogInfo(settings);
245 		this.settings = settings;
246 		this.rootPath = settings.siteURL.path.toString;
247 	}
248 }