UNPKG

12.7 kBJavaScriptView Raw
1/**
2* vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
3*
4* # Pad Model
5*
6* ## License
7*
8* Licensed to the Apache Software Foundation (ASF) under one
9* or more contributor license agreements. See the NOTICE file
10* distributed with this work for additional information
11* regarding copyright ownership. The ASF licenses this file
12* to you under the Apache License, Version 2.0 (the
13* "License"); you may not use this file except in compliance
14* with the License. You may obtain a copy of the License at
15*
16* http://www.apache.org/licenses/LICENSE-2.0
17*
18* Unless required by applicable law or agreed to in writing,
19* software distributed under the License is distributed on an
20* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21* KIND, either express or implied. See the License for the
22* specific language governing permissions and limitations
23* under the License.
24*/
25
26module.exports = (function () {
27 'use strict';
28
29 // Dependencies
30 var removePad;
31 var unloadPad;
32 var getChatHead;
33 try {
34 // Normal case : when installed as a plugin
35 removePad = require('ep_etherpad-lite/node/db/API').deletePad;
36 unloadPad = require('ep_etherpad-lite/node/db/PadManager').removePad;
37 getChatHead = require('ep_etherpad-lite/node/db/API').getChatHead;
38 }
39 catch (e) {
40 // Testing case : noop function
41 removePad = function (pad, callback) {
42 return callback(null, {});
43 };
44 unloadPad = function () {};
45 getChatHead = function () {};
46 }
47 var ld = require('lodash');
48 var cuid = require('cuid');
49 var slugg = require('slugg');
50 // Local dependencies
51 var common = require('./common.js');
52 var conf = require('../configuration.js');
53 var storage = require('../storage.js');
54 var commonGroupPad = require ('./common-group-pad.js');
55 var PPREFIX = storage.DBPREFIX.PAD;
56 var UPREFIX = storage.DBPREFIX.USER;
57 var GPREFIX = storage.DBPREFIX.GROUP;
58 var JPREFIX = storage.DBPREFIX.JOBQ+'deletePad:';
59
60 /**
61 * ## Description
62 *
63 * The pad module contains business logic for private pads. These belong to
64 * groups and can have their own visibility settings.
65 *
66 * A pad can be viewed as an object like :
67 *
68 * var pad = {
69 * _id: 'autoGeneratedUniqueString',
70 * ctime: 123456789,
71 * name: 'title',
72 * group: 'idOfTheLinkedGroup',
73 * visibility: 'restricted',
74 * users: ['u1', 'u2'],
75 * password: null,
76 * readonly: true
77 * };
78 */
79
80 var pad = {};
81
82 /**
83 * ## Internal functions
84 *
85 * These functions are not private like with closures, for testing purposes,
86 * but they are expected be used only internally by other MyPads functions.
87 * They are tested through public functions and API.
88 */
89
90 pad.fn = {};
91
92 /**
93 * ### assignProps
94 *
95 * `assignProps` takes params object and assign defaults if needed.
96 * It creates :
97 *
98 * - a `users` array, empty if `visibility` is not 'restricted', with given
99 * keys otherwise
100 * - a `visibility` string, *null* or with *restricted*, *private* or *public*
101 * - a `password` string, *null* by default
102 * - a `readonly` boolean, *null* by default
103 *
104 * *null* fields are intented to tell MyPads that group properties should be
105 * applied here. `assignProps` returns the pad object.
106 */
107
108 pad.fn.assignProps = function (params) {
109 var p = params;
110 var u = { name: p.name, group: p.group };
111 if (p.visibility === 'restricted' && ld.isArray(p.users)) {
112 u.users = ld.filter(p.users, ld.isString);
113 } else {
114 u.users = [];
115 }
116 var vVal = ['restricted', 'private', 'public'];
117 var v = p.visibility;
118
119 u.visibility = (ld.isString(v) && ld.includes(vVal, v)) ? v : null;
120 u.password = ld.isString(p.password) ? p.password : null;
121 u.readonly = ld.isBoolean(p.readonly) ? p.readonly : null;
122 return u;
123 };
124
125 /**
126 * ### checkSet
127 *
128 * `checkSet` is an async function that ensures that all given users exist.
129 * If true, it calls `fn.set`, else it will return an *Error*. It takes :
130 *
131 * - a `p` pad object
132 * - a `callback` function returning an *Error* or *null* and the `p` object.
133 */
134
135 pad.fn.checkSet = function (p, callback) {
136 var keys = ld.map(p.users, function (v) { return UPREFIX + v; });
137 keys.push(GPREFIX + p.group);
138 common.checkMultiExist(keys, function (err, res) {
139 if (err) { return callback(err); }
140 var e = new Error('BACKEND.ERROR.PAD.ITEMS_NOT_FOUND');
141 if (!res) { return callback(e); }
142 pad.fn.set(p, callback);
143 });
144 };
145
146 /**
147 * ### indexGroups
148 *
149 * `indexGroups` is an asynchronous function which handles secondary indexes
150 * for *group.pads* and *user.bookmarks.pads* after pad creation, update,
151 * removal. It takes :
152 *
153 * - a `del` boolean to know if we have to delete key from index or add it
154 * - the `pad` object
155 * - a `callback` function, returning *Error* or *null* if succeeded
156 */
157
158 pad.fn.indexGroups = function (del, pad, callback) {
159 var _set = function (g) {
160 storage.db.set(GPREFIX + g._id, g, function (err) {
161 if (err) { return callback(err); }
162 callback(null);
163 });
164 };
165 var removeFromBookmarks = function (g) {
166 var uids = ld.union(g.admins, g.users);
167 var ukeys = ld.map(uids, function (u) { return UPREFIX + u; });
168 storage.fn.getKeys(ukeys, function (err, users) {
169 if (err) { return callback(err); }
170 users = ld.reduce(users, function (memo, u, k) {
171 if (ld.includes(u.bookmarks.pads, pad._id)) {
172 ld.pull(u.bookmarks.pads, pad._id);
173 memo[k] = u;
174 }
175 return memo;
176 }, {});
177 storage.fn.setKeys(users, function (err) {
178 if (err) { return callback(err); }
179 _set(g);
180 });
181 });
182 };
183 storage.db.get(GPREFIX + pad.group, function (err, g) {
184 if (err) { return callback(err); }
185 if (del) {
186 ld.pull(g.pads, pad._id);
187 removeFromBookmarks(g);
188 } else {
189 if (!ld.includes(g.pads, pad._id)) {
190 g.pads.push(pad._id);
191 _set(g);
192 } else {
193 callback(null);
194 }
195 }
196 });
197 };
198
199 /**
200 * ### set
201 *
202 * `set` is internal function that sets the pad into the database.
203 *
204 * It takes, as arguments :
205 *
206 * - the `p` pad object
207 * - the `callback` function returning an *Error* or *null* and the `p`
208 * object.
209 */
210
211 pad.fn.set = function (p, callback) {
212 storage.db.set(PPREFIX + p._id, p, function (err) {
213 if (err) { return callback(err); }
214 var indexFn = function (err) {
215 if (err) { return callback(err); }
216 pad.fn.indexGroups(false, p, function (err) {
217 if (err) { return callback(err); }
218 callback(null, p);
219 });
220 };
221 if (p.moveGroup) {
222 pad.fn.indexGroups(true, { _id: p._id, group: p.moveGroup }, indexFn);
223 } else {
224 indexFn(null);
225 }
226 });
227 };
228
229 /**
230 * ## Public functions
231 *
232 * ### get
233 *
234 * This function uses `common.getDel` with `del` to *false* and *PPREFIX*
235 * fixed. It will takes mandatory key string and callback function. See
236 * `common.getDel` for documentation.
237 */
238
239 pad.get = ld.partial(common.getDel, false, PPREFIX);
240
241 /**
242 * ### set
243 *
244 * This function adds a new pad or updates properties of an existing one.
245 * It fixes a flag if the group has changed, to ensure correct local index
246 * updates. It checks the fields, throws error if needed, sets defaults
247 * options. As arguments, it takes mandatory :
248 *
249 * - `params` object, with
250 *
251 * - a `name` string that can't be empty
252 * - an `group` string, the unique key identifying the linked required group
253 * - `visibility`, `password`, `readonly` the same strings as for
254 * `model.group`, but optional : it will takes the group value if not
255 * defined
256 * - `users` array, with ids of users invited to read and/or edit the pad, for
257 * restricted visibility only
258 * - `callback` function returning *Error* if error, *null* otherwise and the
259 * pad object;
260 * - a special `edit` boolean, defaults to *false* for reusing the function for
261 * set (edit) an existing pad.
262 *
263 * Finally, in case of new pad, it sets the unique identifier to the name
264 * slugged and suffixes by random id generator and uses a special ctime
265 * field for epoch time.
266 */
267
268 pad.set = function (params, callback) {
269 common.addSetInit(params, callback, ['name', 'group']);
270 var p = pad.fn.assignProps(params);
271 var check = function () {
272 commonGroupPad.handlePassword(p, function (err, password) {
273 if (err) { return callback(err); }
274 if (password) { p.password = password; }
275 pad.fn.checkSet(p, callback);
276 });
277 };
278 if (params._id) {
279 p._id = params._id;
280 storage.db.get(PPREFIX + p._id, function(err, res) {
281 if (err) { return callback(err); }
282 if (!res) {
283 return callback(new Error('BACKEND.ERROR.PAD.INEXISTENT'));
284 }
285 if (res.group !== p.group) { p.moveGroup = res.group; }
286 if ((res.visibility === 'private') && !p.password) {
287 p.password = res.password;
288 }
289 p.ctime = res.ctime;
290 check();
291 });
292 } else {
293 p._id = (slugg(p.name) + '-' + cuid.slug());
294 p.ctime = Date.now();
295 check();
296 }
297 };
298
299 /**
300 * ### del
301 *
302 * This function uses `common.getDel` with `del` to *false* and *PPREFIX*
303 * fixed. It will take mandatory key string and callback function. See
304 * `common.getDel` for documentation.
305 *
306 * It also removes the pad from Etherpad instance, using the internal API.
307 * It uses the `callback` function to handle secondary indexes for groups.
308 */
309
310 pad.del = function (key, callback) {
311 if (!ld.isFunction(callback)) {
312 throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
313 }
314 common.getDel(true, PPREFIX, key, function (err, p) {
315 if (err) { return callback(err); }
316 storage.db.get('pad:'+p._id, function(err, value) {
317 if (err) { return callback(err); }
318
319 if (typeof(value) !== 'undefined' && value !== null && value.atext) {
320 if (conf.get('deleteJobQueue')) {
321 storage.db.set(JPREFIX+p._id, p._id, function(err) {
322 if (err) { return callback(err); }
323 unloadPad(p._id);
324 pad.fn.indexGroups(true, p, callback);
325 });
326 } else {
327 removePad(p._id, function(err) {
328 if (err) { return callback(err); }
329 pad.fn.indexGroups(true, p, callback);
330 });
331 }
332 } else {
333 pad.fn.indexGroups(true, p, callback);
334 }
335 });
336 });
337 };
338
339 /**
340 * ### getBookmarkedPadsByUser
341 *
342 * `getBookmarkedPadsByUser` is an asynchronous function that returns all
343 * bookmarked pads for a defined user, using `storage.fn.getKeys`. It takes :
344 *
345 * - a `user` object
346 * - a `callback` function, called with *error* if needed, *null* and the
347 * results, an object with keys and groups values, otherwise.
348 *
349 */
350
351 pad.getBookmarkedPadsByUser = function(user, callback) {
352 if (!ld.isObject(user) || !ld.isArray(user.bookmarks.pads)) {
353 throw new TypeError('BACKEND.ERROR.TYPE.USER_INVALID');
354 }
355 if (!ld.isFunction(callback)) {
356 throw new TypeError('BACKEND.ERROR.TYPE.CALLBACK_FN');
357 }
358 storage.fn.getKeys(
359 ld.map(user.bookmarks.pads, function (p) { return PPREFIX + p; }),
360 function (err, pads) {
361 if (err) { return callback(err); }
362 pads = ld.reduce(pads, function (memo, val, key) {
363 key = key.substr(PPREFIX.length);
364 memo[key] = val;
365 return memo;
366 }, {});
367 callback(null, pads);
368 }
369 );
370 };
371
372 /**
373 * ### delChatHistory
374 *
375 * Removes all chat messages for a pad
376 * As arguments, it takes mandatory :
377 * - `padId`, the id of the pad
378 * - a `callback` function
379 *
380 */
381 pad.delChatHistory = function(padID, callback) {
382 getChatHead(padID, function(err, res) {
383 if (err) { return callback(err); }
384 for (var i = 0; i <= res.chatHead; i++) {
385 storage.db.remove('pad:' + padID + ':chat:'+i);
386 }
387 callback();
388 });
389 };
390
391 /**
392 * ### count
393 *
394 * Returns the number of pads of the MyPads instance (anonymous pads are
395 * not counted)
396 * As arguments, it takes mandatory :
397 * - a `callback` function
398 */
399
400 pad.count = function(callback) {
401 storage.db.findKeys(PPREFIX + '*', null, function (err, res) {
402 if (err) { return callback(err); }
403 return callback(null, ld.size(res));
404 });
405 };
406
407 return pad;
408
409}).call(this);