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 |
|
26 | module.exports = (function () {
|
27 | ;
|
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);
|