1 module vibelog.db.mongo;
2 
3 import vibelog.db.dbcontroller;
4 
5 import vibe.core.log;
6 import vibe.core.stream;
7 import vibe.data.bson;
8 import vibe.db.mongo.mongo;
9 import vibe.mail.smtp;
10 import vibe.stream.memory;
11 
12 import std.exception;
13 import std.variant;
14 
15 
16 final class MongoDBController : DBController {
17 	private {
18 		MongoCollection m_configs;
19 		MongoCollection m_users;
20 		MongoCollection m_posts;
21 		MongoCollection m_postFiles;
22 		void delegate()[] m_onConfigChange;
23 	}
24 
25 	this(string db_url)
26 	{
27 		string database = "vibelog";
28 		MongoClientSettings dbsettings;
29 		if (parseMongoDBUrl(dbsettings, db_url))
30 			database = dbsettings.database;
31 
32 		auto db = connectMongoDB(db_url).getDatabase(database);
33 		m_configs = db["configs"];
34 		m_users = db["users"];
35 		m_posts = db["posts"];
36 		m_postFiles = db["postFiles"];
37 
38 		upgradeComments(db);
39 	}
40 
41 	Config getConfig(string name, bool createdefault = false)
42 	{
43 		auto configbson = m_configs.findOne(["name": Bson(name)]);
44 		if( !configbson.isNull() )
45 			return Config.fromBson(configbson);
46 		enforce(createdefault, "Configuration does not exist.");
47 		auto cfg = new Config;
48 		cfg.name = name;
49 		m_configs.insert(cfg.toBson());
50 		return cfg;
51 	}
52 
53 	void setConfig(Config cfg)
54 	{
55 		Bson update = cfg.toBson();
56 		m_configs.update(["name": Bson(cfg.name)], update);
57 		foreach (d; m_onConfigChange) d();
58 	}
59 
60 	void invokeOnConfigChange(void delegate() del)
61 	{
62 		m_onConfigChange ~= del;
63 	}
64 
65 	void deleteConfig(string name)
66 	{
67 		m_configs.remove(["name": Bson(name)]);
68 	}
69 
70 	Config[] getAllConfigs()
71 	{
72 		Bson[string] query;
73 		Config[] ret;
74 		foreach( config; m_configs.find(query) ){
75 			auto c = Config.fromBson(config);
76 			ret ~= c;
77 		}
78 		return ret;
79 	}
80 
81 	User[string] getAllUsers()
82 	{
83 		import vibelog.internal.passwordhash : generatePasswordHash;
84 
85 		Bson[string] query;
86 		User[string] ret;
87 		foreach( user; m_users.find(query) ){
88 			auto u = User.fromBson(user);
89 			ret[u.username] = u;
90 		}
91 		if( ret.length == 0 ){
92 			auto initial_admin = new User;
93 			initial_admin.username = "admin";
94 			initial_admin.password = generatePasswordHash("admin");
95 			initial_admin.name = "Default Administrator";
96 			initial_admin.groups ~= "admin";
97 			m_users.insert(initial_admin);
98 			ret["admin"] = initial_admin;
99 		}
100 		return ret;
101 	}
102 	
103 	User getUser(BsonObjectID userid)
104 	{
105 		auto userbson = m_users.findOne(["_id": Bson(userid)]);
106 		return User.fromBson(userbson);
107 	}
108 
109 	User getUserByName(string name)
110 	{
111 		auto userbson = m_users.findOne(["username": Bson(name)]);
112 		if (userbson.isNull()) {
113 			try {
114 				auto id = BsonObjectID.fromHexString(name);
115 				logDebug("%s <-> %s", name, id.toString());
116 				assert(id.toString() == name);
117 				userbson = m_users.findOne(["_id": Bson(id)]);
118 			} catch (Exception e) {
119 				return null;
120 			}
121 		}
122 		//auto userbson = m_users.findOne(Bson(["name" : Bson(name)]));
123 		return User.fromBson(userbson);
124 	}
125 
126 	User getUserByEmail(string email)
127 	{
128 		auto userbson = m_users.findOne(["email": Bson(email)]);
129 		return User.fromBson(userbson);
130 	}
131 
132 	BsonObjectID addUser(User user)
133 	{
134 		auto id = BsonObjectID.generate();
135 		Bson userbson = user.toBson();
136 		userbson["_id"] = Bson(id);
137 		m_users.insert(userbson);
138 		return id;
139 	}
140 
141 	void modifyUser(User user)
142 	{
143 		assert(user._id.valid);
144 		Bson update = user.toBson();
145 		m_users.update(["_id": Bson(user._id)], update);
146 	}
147 
148 	void deleteUser(BsonObjectID id)
149 	{
150 		assert(id.valid);
151 		m_users.remove(["_id": Bson(id)]);
152 	}
153 
154 	int countPostsForCategory(string[] categories)
155 	{
156 		int cnt;
157 		getPostsForCategory(categories, 0, (size_t, Post p){ if( p.isPublic ) cnt++; return true; });
158 		return cnt;
159 	}
160 
161 	void getPostsForCategory(string[] categories, int nskip, bool delegate(size_t idx, Post post) del)
162 	{
163 		auto cats = new Bson[categories.length];
164 		foreach( i; 0 .. categories.length ) cats[i] = Bson(categories[i]);
165 		Bson category = Bson(["$in" : Bson(cats)]);
166 		Bson[string] query = ["query" : Bson(["category" : category]), "orderby" : Bson(["date" : Bson(-1)])];
167 		foreach (idx, post; m_posts.find(query, null, QueryFlags.None, nskip).byPair) {
168 			if (!del(idx, Post.fromBson(post)))
169 				break;
170 		}
171 	}
172 
173 	void getPublicPostsForCategory(string[] categories, int nskip, bool delegate(size_t idx, Post post) del)
174 	{
175 		auto cats = new Bson[categories.length];
176 		foreach( i; 0 .. categories.length ) cats[i] = Bson(categories[i]);
177 		Bson category = Bson(["$in" : Bson(cats)]);
178 		Bson[string] query = ["query" : Bson(["category" : category, "isPublic": Bson(true)]), "orderby" : Bson(["date" : Bson(-1)])];
179 		foreach (idx, post; m_posts.find(query, null, QueryFlags.None, nskip).byPair) {
180 			if (!del(idx, Post.fromBson(post)))
181 				break;
182 		}
183 	}
184 
185 	void getAllPosts(int nskip, bool delegate(size_t idx, Post post) del)
186 	{
187 		Bson[string] query;
188 		Bson[string] extquery = ["query" : Bson(query), "orderby" : Bson(["date" : Bson(-1)])];
189 		foreach (idx, post; m_posts.find(extquery, null, QueryFlags.None, nskip).byPair) {
190 			if (!del(idx, Post.fromBson(post)))
191 				break;
192 		}
193 	}
194 
195 
196 	Post getPost(BsonObjectID postid)
197 	{
198 		auto postbson = m_posts.findOne(["_id": Bson(postid)]);
199 		return Post.fromBson(postbson);
200 	}
201 
202 	Post getPost(string name)
203 	{
204 		auto postbson = m_posts.findOne(["slug": Bson(name)]);
205 		if( postbson.isNull() )
206 			postbson = m_posts.findOne(["_id" : Bson(BsonObjectID.fromHexString(name))]);
207 		return Post.fromBson(postbson);
208 	}
209 
210 	bool hasPost(string name)
211 	{
212 		return !m_posts.findOne(["slug": Bson(name)]).isNull();
213 
214 	}
215 
216 	BsonObjectID addPost(Post post)
217 	{
218 		auto id = BsonObjectID.generate();
219 		Bson postbson = post.toBson();
220 		postbson["_id"] = Bson(id);
221 		m_posts.insert(postbson);
222 		return id;
223 	}
224 
225 	void modifyPost(Post post)
226 	{
227 		assert(post.id.valid);
228 		Bson update = post.toBson();
229 		m_posts.update(["_id": Bson(post.id)], update);
230 	}
231 
232 	void deletePost(BsonObjectID id)
233 	{
234 		assert(id.valid);
235 		m_posts.remove(["_id": Bson(id)]);
236 	}
237 
238 	void addFile(string post_name, string file_name, in ubyte[] contents)
239 	{
240 		import vibe.stream.operations : readAll;
241 		struct I {
242 			string postName;
243 			string fileName;
244 		}
245 		m_postFiles.insert(PostFile(post_name, file_name, cast(ubyte[])contents));
246 	}
247 
248 	string[] getFiles(string post_name)
249 	{
250 		import std.algorithm.iteration : map;
251 		import std.array : array;
252 		return m_postFiles.find(["postName": post_name], ["fileName": true]).map!(p => p["fileName"].get!string).array;
253 	}
254 
255 	InputStream getFile(string post_name, string file_name)
256 	{
257 		auto f = m_postFiles.findOne!PostFile(["postName": post_name, "fileName": file_name]);
258 		if (f.isNull) return null;
259 		return createMemoryStream(f.get.contents);
260 	}
261 
262 	void removeFile(string post_name, string file_name)
263 	{
264 		m_postFiles.remove(["postName": post_name, "fileName": file_name]);
265 	}
266 
267 	private void upgradeComments(MongoDatabase db)
268 	{
269 		import diskuto.commentstore : StoredComment, CommentStatus;
270 		import diskuto.commentstores.mongodb : MongoStruct;
271 
272 		auto comments = db["comments"];
273 
274 		// Upgrade post contained comments to their collection
275 		foreach( p; m_posts.find(["comments": ["$exists": true]], ["comments": 1]) ){
276 			foreach( c; p["comments"] ){
277 				c["_id"] = BsonObjectID.generate();
278 				c["postId"] = p["_id"];
279 				comments.insert(c);
280 			}
281 			m_posts.update(["_id": p["_id"]], ["$unset": ["comments": 1]]);
282 		}
283 
284 		// Upgrade old comments to Diskuto format
285 		foreach (c; comments.find(["postId": ["$exists": true]])) {
286 			auto oldc = OldComment.fromBson(c);
287 			StoredComment newc;
288 			newc.id = oldc.id.toString();
289 			newc.status = oldc.isPublic ? CommentStatus.active : CommentStatus.disabled;
290 			newc.topic = "vibelog-" ~ oldc.postId.toString();
291 			newc.author = "vibelog-...";
292 			newc.clientAddress = oldc.authorIP;
293 			newc.name = oldc.authorName;
294 			newc.email = oldc.authorMail;
295 			newc.website = oldc.authorHomepage;
296 			newc.text = oldc.content;
297 			newc.time = oldc.date;
298 			comments.update(["_id": c["_id"]], MongoStruct!StoredComment(newc));
299 		}
300 	}
301 }
302 
303 struct PostFile {
304 	string postName;
305 	string fileName;
306 	ubyte[] contents;
307 }
308 
309 
310 final class OldComment {
311 	BsonObjectID id;
312 	BsonObjectID postId;
313 	bool isPublic;
314 	SysTime date;
315 	int answerTo;
316 	string authorName;
317 	string authorMail;
318 	string authorHomepage;
319 	string authorIP;
320 	string header;
321 	string content;
322 
323 	static OldComment fromBson(Bson bson)
324 	{
325 		auto ret = new OldComment;
326 		ret.id = cast(BsonObjectID)bson["_id"];
327 		ret.postId = cast(BsonObjectID)bson["postId"];
328 		ret.isPublic = cast(bool)bson["isPublic"];
329 		ret.date = SysTime.fromISOExtString(cast(string)bson["date"]);
330 		ret.answerTo = cast(int)bson["answerTo"];
331 		ret.authorName = cast(string)bson["authorName"];
332 		ret.authorMail = cast(string)bson["authorMail"];
333 		ret.authorHomepage = cast(string)bson["authorHomepage"];
334 		ret.authorIP = bson["authorIP"].opt!string();
335 		ret.header = cast(string)bson["header"];
336 		ret.content = cast(string)bson["content"];
337 		return ret;
338 	}
339 }