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