1 /**
2 	(module summary)
3 
4 	Copyright: © 2012-2016 RejectedSoftware e.K.
5 	License: Subject to the terms of the General Public License version 3, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module vibenews.admin;
9 
10 import vibenews.controller;
11 import vibenews.vibenews;
12 
13 import userman.db.controller : User;
14 static import userman.db.controller;
15 
16 import vibe.core.log;
17 import vibe.data.bson;
18 import vibe.http.router;
19 import vibe.http.server;
20 import vibe.http.fileserver;
21 import vibe.textfilter.urlencode;
22 import vibe.utils.validation;
23 
24 import std.algorithm : map;
25 import std.array;
26 import std.conv;
27 import std.exception;
28 import std.string;
29 import std.variant;
30 
31 class AdminInterface {
32 	private {
33 		Controller m_ctrl;
34 	}
35 
36 	this(Controller ctrl)
37 	{
38 		m_ctrl = ctrl;
39 	}
40 
41 	void listen()
42 	{
43 		auto vnsettings = m_ctrl.settings;
44 
45 		auto settings = new HTTPServerSettings;
46 		settings.port = vnsettings.adminPort;
47 		settings.bindAddresses = vnsettings.adminBindAddresses;
48 
49 		auto router = new URLRouter;
50 		register(router);
51 
52 		listenHTTP(settings, router);
53 	}
54 
55 	void register(URLRouter router)
56 	{
57 		router.get("/", &showAdminPanel);
58 		router.post("/categories/create", &createGroupCategory);
59 		router.get("/categories/:category/show", &showGroupCategory);
60 		router.post("/categories/:category/update", &updateGroupCategory);
61 		router.post("/categories/:category/delete", &deleteGroupCategory);
62 		router.post("/reclassify_spam", &reclassifySpam);
63 		router.post("/groups/create", &createGroup);
64 		router.post("/groups/repair-numbers", &repairGroupNumbers);
65 		router.post("/groups/repair-threads", &repairGroupThreads);
66 		router.get("/groups/:groupname/show", &showGroup);
67 		router.post("/groups/:groupname/update", &updateGroup);
68 		router.post("/groups/:groupname/purge", &purgeGroup);
69 		router.get("/groups/:groupname/articles", &showArticles);
70 		router.post("/articles/:articleid/activate", &activateArticle);
71 		router.post("/articles/:articleid/deactivate", &deactivateArticle);
72 		router.post("/articles/:articleid/mark_ham", &markAsHam);
73 		router.post("/articles/:articleid/mark_spam", &markAsSpam);
74 		router.get("/users/", &showListUsers);
75 		router.get("/users/:user/", &showUser);
76 		router.post("/users/:user/update", &updateUser);
77 		router.post("/users/:user/delete", &deleteUser);
78 		router.post("/users/deleteOrphaned", &postDeleteOrphanedUsers);
79 		router.get("*", serveStaticFiles("public"));
80 	}
81 
82 	void showAdminPanel(HTTPServerRequest req, HTTPServerResponse res)
83 	{
84 		struct Info {
85 			VibeNewsSettings settings;
86 		}
87 		auto info = Info(m_ctrl.settings);
88 
89 		Group[] groups;
90 		GroupCategory[] categories;
91 		m_ctrl.enumerateGroups((idx, group){ groups ~= group; }, true);
92 		m_ctrl.enumerateGroupCategories((idx, cat){ categories ~= cat; });
93 		res.render!("vibenews.admin.index.dt", req, info, groups, categories);
94 	}
95 
96 	void showGroupCategory(HTTPServerRequest req, HTTPServerResponse res)
97 	{
98 		struct Info {
99 			VibeNewsSettings settings;
100 		}
101 		auto info = Info(m_ctrl.settings);
102 
103 		auto category = m_ctrl.getGroupCategory(BsonObjectID.fromString(req.params["category"]));
104 		Group[] groups;
105 		m_ctrl.enumerateGroups((idx, grp){ groups ~= grp; });
106 		res.render!("vibenews.admin.editcategory.dt", req, info, category, groups);
107 	}
108 
109 	void updateGroupCategory(HTTPServerRequest req, HTTPServerResponse res)
110 	{
111 		auto id = BsonObjectID.fromString(req.params["category"]);
112 		auto caption = req.form["caption"];
113 		auto index = req.form["index"].to!int();
114 		BsonObjectID[] groups;
115 		m_ctrl.enumerateGroups((idx, grp){ if( grp._id.toString() in req.form ) groups ~= grp._id; });
116 		m_ctrl.updateGroupCategory(id, caption, index, groups);
117 		res.redirect("/categories/"~id.toString()~"/show");
118 	}
119 
120 	void deleteGroupCategory(HTTPServerRequest req, HTTPServerResponse res)
121 	{
122 		auto id = BsonObjectID.fromString(req.params["category"]);
123 		m_ctrl.deleteGroupCategory(id);
124 		res.redirect("/");
125 	}
126 
127 	void showGroup(HTTPServerRequest req, HTTPServerResponse res)
128 	{
129 		struct Info {
130 			VibeNewsSettings settings;
131 		}
132 		auto info = Info(m_ctrl.settings);
133 
134 		auto group = m_ctrl.getGroupByName(req.params["groupname"], true);
135 		res.render!("vibenews.admin.editgroup.dt", req, info, group);
136 	}
137 
138 	void createGroupCategory(HTTPServerRequest req, HTTPServerResponse res)
139 	{
140 		auto id = m_ctrl.createGroupCategory(req.form["caption"], req.form["index"].to!int());
141 		res.redirect("/categories/"~id.toString()~"/show");
142 	}
143 
144 	void reclassifySpam(HTTPServerRequest req, HTTPServerResponse res)
145 	{
146 		m_ctrl.reclassifySpam();
147 		res.redirect("/");
148 	}
149 
150 	void createGroup(HTTPServerRequest req, HTTPServerResponse res)
151 	{
152 		enforce(!m_ctrl.groupExists(req.form["name"], true), "A group with the specified name already exists");
153 
154 		Group group;
155 		group._id = BsonObjectID.generate();
156 		group.active = false;
157 		group.name = req.form["name"];
158 		group.caption = req.form["caption"];
159 		m_ctrl.addGroup(group);
160 
161 		res.redirect("/groups/"~urlEncode(group.name)~"/show");
162 	}
163 
164 	void updateGroup(HTTPServerRequest req, HTTPServerResponse res)
165 	{
166 		auto group = m_ctrl.getGroupByName(req.params["groupname"], true);
167 		group.caption = req.form["caption"];
168 		group.description = req.form["description"];
169 		group.active = ("active" in req.form) !is null;
170 		group.readOnlyAuthTags = req.form["roauthtags"].split(",").map!(s => authGroupPrefix ~ strip(s))().array();
171 		group.readWriteAuthTags = req.form["rwauthtags"].split(",").map!(s => authGroupPrefix ~ strip(s))().array();
172 		m_ctrl.updateGroup(group);
173 		res.redirect("/groups/"~urlEncode(group.name)~"/show");
174 	}
175 
176 	void purgeGroup(HTTPServerRequest req, HTTPServerResponse res)
177 	{
178 		m_ctrl.purgeGroup(req.params["groupname"]);
179 		res.redirect("/groups/"~req.params["groupname"]~"/show");
180 	}
181 
182 	void repairGroupNumbers(HTTPServerRequest req, HTTPServerResponse res)
183 	{
184 		m_ctrl.repairGroupNumbers();
185 		res.redirect("/");
186 	}
187 
188 	void repairGroupThreads(HTTPServerRequest req, HTTPServerResponse res)
189 	{
190 		m_ctrl.repairThreads();
191 		res.redirect("/");
192 	}
193 
194 	void showArticles(HTTPServerRequest req, HTTPServerResponse res)
195 	{
196 		struct Info {
197 			VibeNewsSettings settings;
198 			enum articlesPerPage = 20;
199 			string groupname;
200 			int page;
201 			Article[] articles;
202 			int articleCount;
203 			int pageCount;
204 			bool onlyActive;
205 		}
206 
207 		Info info;
208 		info.settings = m_ctrl.settings;
209 		info.groupname = req.params["groupname"];
210 		info.page = ("page" in req.query) ? to!int(req.query["page"])-1 : 0;
211 		info.onlyActive = req.query.get("only_active", "") == "1";
212 		if (info.onlyActive) {
213 			m_ctrl.enumerateActiveArticlesBackwards(info.groupname, info.page*info.articlesPerPage, info.articlesPerPage, (ref art){ info.articles ~= art; });
214 			info.articleCount = cast(int)m_ctrl.getActiveArticlesCount(info.groupname);
215 		} else {
216 			m_ctrl.enumerateAllArticlesBackwards(info.groupname, info.page*info.articlesPerPage, info.articlesPerPage, (ref art){ info.articles ~= art; });
217 			info.articleCount = cast(int)m_ctrl.getAllArticlesCount(info.groupname);
218 		}
219 		info.pageCount = (info.articleCount-1)/info.articlesPerPage + 1;
220 
221 		res.render!("vibenews.admin.listarticles.dt", req, info);
222 	}
223 
224 	void activateArticle(HTTPServerRequest req, HTTPServerResponse res)
225 	{
226 		auto artid = BsonObjectID.fromString(req.params["articleid"]);
227 		m_ctrl.activateArticle(artid);
228 		redirectBackToArticles(req, res);
229 	}
230 
231 	void deactivateArticle(HTTPServerRequest req, HTTPServerResponse res)
232 	{
233 		auto artid = BsonObjectID.fromString(req.params["articleid"]);
234 		m_ctrl.deactivateArticle(artid);
235 		redirectBackToArticles(req, res);
236 	}
237 
238 	void markAsSpam(HTTPServerRequest req, HTTPServerResponse res)
239 	{
240 		auto artid = BsonObjectID.fromString(req.params["articleid"]);
241 		m_ctrl.markAsSpam(artid, true);
242 		redirectBackToArticles(req, res);
243 	}
244 
245 	void markAsHam(HTTPServerRequest req, HTTPServerResponse res)
246 	{
247 		auto artid = BsonObjectID.fromString(req.params["articleid"]);
248 		m_ctrl.markAsSpam(artid, false);
249 		redirectBackToArticles(req, res);
250 	}
251 
252 	private void redirectBackToArticles(HTTPServerRequest req, HTTPServerResponse res)
253 	{
254 		string suff = req.form.get("only_active", "") == "1" ? "&only_active=1" : null;
255 		res.redirect("/groups/"~req.form["groupname"]~"/articles?page="~req.form["page"]~suff);
256 	}
257 
258 	void showListUsers(HTTPServerRequest req, HTTPServerResponse res)
259 	{
260 		struct Info {
261 			VibeNewsSettings settings;
262 			enum itemsPerPage = 20;
263 			UserInfo[] users;
264 			int page;
265 			int itemCount;
266 			int pageCount;
267 		}
268 
269 		Info info;
270 		info.settings = m_ctrl.settings;
271 		info.page = ("page" in req.query) ? to!int(req.query["page"])-1 : 0;
272 		m_ctrl.enumerateUsers(info.page*info.itemsPerPage, info.itemsPerPage, (ref user){
273 			info.users ~= getUserInfo(m_ctrl, user);
274 		});
275 		info.itemCount = cast(int)m_ctrl.getUserCount();
276 		info.pageCount = (info.itemCount-1)/info.itemsPerPage + 1;
277 
278 		res.render!("vibenews.admin.listusers.dt", req, info);
279 	}
280 
281 	void showUser(HTTPServerRequest req, HTTPServerResponse res)
282 	{
283 		struct Info {
284 			VibeNewsSettings settings;
285 			UserInfo user;
286 		}
287 		auto uid = User.ID.fromString(req.params["user"]);
288 		User usr = m_ctrl.getUser(uid);
289 		Info info;
290 		info.settings = m_ctrl.settings;
291 		info.user = getUserInfo(m_ctrl, usr);
292 		res.render!("vibenews.admin.edituser.dt", req, info);
293 	}
294 
295 	void updateUser(HTTPServerRequest req, HTTPServerResponse res)
296 	{
297 		import std.algorithm.iteration : splitter;
298 
299 		auto uid = User.ID.fromString(req.params["user"]);
300 		auto user = m_ctrl.getUser(uid);
301 		if (auto pv = "email" in req.form) {
302 			validateEmail(*pv);
303 			user.email = user.name = *pv;
304 		}
305 		if (auto pv = "fullName" in req.form) user.fullName = *pv;
306 		if (auto pv = "groups" in req.form) {
307 			user.groups.length = 0;
308 			foreach (grp; (*pv).splitter(",").map!(g => authGroupPrefix ~ g.strip())) {
309 				try user.groups ~= m_ctrl.getAuthGroupByName(grp).id;
310 				catch (Exception) {
311 					m_ctrl.userManController.addGroup(grp, "VibeNews authentication group");
312 					user.groups ~= m_ctrl.getAuthGroupByName(grp).id;
313 				}
314 			}
315 		}
316 		user.active = ("active" in req.form) !is null;
317 		user.banned = ("banned" in req.form) !is null;
318 		m_ctrl.updateUser(user);
319 
320 		res.redirect("/users/");
321 	}
322 
323 	void deleteUser(HTTPServerRequest req, HTTPServerResponse res)
324 	{
325 		m_ctrl.deleteUser(User.ID.fromString(req.params["user"]));
326 		res.redirect("/users/");
327 	}
328 
329 	void postDeleteOrphanedUsers(HTTPServerRequest req, HTTPServerResponse res)
330 	{
331 		m_ctrl.deleteOrphanedUsers();
332 		res.redirect("/users/");
333 	}
334 
335 }
336 
337 struct UserInfo {
338 	User user;
339 	alias user this;
340 	ulong messageCount;
341 	ulong deletedMessageCount;
342 	string[] groupStrings;
343 }
344 
345 private UserInfo getUserInfo(Controller ctrl, User user)
346 {
347 	UserInfo nfo;
348 	nfo.user = user;
349 	ctrl.getUserMessageCount(user.email, nfo.messageCount, nfo.deletedMessageCount);
350 	foreach (grpname; user.groups) {
351 		if (!grpname.startsWith(authGroupPrefix)) continue;
352 		grpname = grpname[authGroupPrefix.length .. $];
353 		nfo.groupStrings ~= grpname;
354 	}
355 	return nfo;
356 }