UNPKG

54 kBJavaScriptView Raw
1/**
2* vim:set sw=2 ts=2 sts=2 ft=javascript expandtab:
3*
4* # API Module
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* ## Description
26*
27* This module holds all public functions, used for the API of mypads.
28* Please refer to binded function when no details are given.
29*/
30
31var rFS = require('fs').readFileSync;
32// External dependencies
33var ld = require('lodash');
34var passport = require('passport');
35var jwt = require('jsonwebtoken');
36var testMode = false;
37var express;
38try {
39 // Normal case : when installed as a plugin
40 express = require('ep_etherpad-lite/node_modules/express');
41}
42catch (e) {
43 // Testing case : we need to mock the express dependency
44 testMode = true;
45 express = require('express');
46}
47var settings;
48try {
49 // Normal case : when installed as a plugin
50 settings = require('ep_etherpad-lite/node/utils/Settings');
51}
52catch (e) {
53 // Testing case : we need to mock the express dependency
54 if (process.env.TEST_LDAP) {
55 settings = {
56 'ep_mypads': {
57 'ldap': {
58 'url': 'ldap://rroemhild-test-openldap',
59 'bindDN': 'cn=admin,dc=planetexpress,dc=com',
60 'bindCredentials': 'GoodNewsEveryone',
61 'searchBase': 'ou=people,dc=planetexpress,dc=com',
62 'searchFilter': '(uid={{username}})',
63 'properties': {
64 'login': 'uid',
65 'email': 'mail',
66 'firstname': 'givenName',
67 'lastname': 'sn'
68 },
69 'defaultLang': 'fr'
70 }
71 }
72 };
73 } else {
74 settings = {};
75 }
76}
77var bodyParser = require('body-parser');
78var cookieParser = require('cookie-parser');
79var decode = require('js-base64').Base64.decode;
80// Local dependencies
81var conf = require('./configuration.js');
82var mail = require('./mail.js');
83var user = require('./model/user.js');
84var userCache = require('./model/user-cache.js');
85var group = require('./model/group.js');
86var pad = require('./model/pad.js');
87var auth = require('./auth.js');
88var perm = require('./perm.js');
89var common = require('./model/common.js');
90
91module.exports = (function () {
92 'use strict';
93
94 var api = {};
95 api.initialRoute = '/mypads/api/';
96 var authAPI;
97 var configurationAPI;
98 var userAPI;
99 var groupAPI;
100 var padAPI;
101 var cacheAPI;
102 var statsAPI;
103
104 /**
105 * `init` is the first function that takes an Express app as argument.
106 * It initializes authentication, permissions and also all API requirements,
107 * particularly mypads routes.
108 *
109 * It needs to be fast to finish *before* YAJSML plugin (which takes over all
110 * requests otherwise and refuse anything apart GET and HEAD HTTP methods.
111 * That's why api.init is used without any callback and is called before
112 * storage initialization.
113 */
114
115 api.init = function (app, callback) {
116 // Use this for .JSON storage
117 app.use(bodyParser.json());
118 app.use(cookieParser());
119 app.use('/mypads', express.static(__dirname + '/static'));
120 if (testMode) {
121 // Only allow functional testing in testing mode
122 app.use('/mypads/functest', express.static(__dirname + '/spec/frontend'));
123 }
124 var rFSOpts = { encoding: 'utf8' };
125 api.l10n = {
126 mail: {
127 de: JSON.parse(rFS(__dirname + '/templates/mail_de.json', rFSOpts)),
128 en: JSON.parse(rFS(__dirname + '/templates/mail_en.json', rFSOpts)),
129 es: JSON.parse(rFS(__dirname + '/templates/mail_es.json', rFSOpts)),
130 fr: JSON.parse(rFS(__dirname + '/templates/mail_fr.json', rFSOpts)),
131 it: JSON.parse(rFS(__dirname + '/templates/mail_it.json', rFSOpts)),
132 }
133 };
134 auth.init(app);
135 authAPI(app);
136 configurationAPI(app);
137 userAPI(app);
138 groupAPI(app);
139 padAPI(app);
140 cacheAPI(app);
141 statsAPI(app);
142 perm.init(app);
143
144 /**
145 * `index.html` is a simple lodash template
146 */
147 var idxTpl = ld.template(rFS(__dirname + '/templates/index.html', rFSOpts));
148 var idxHandle = function (req, res) {
149 var mypadsURL = req.protocol + '://' + req.header('host') + req.path;
150 return res.send(idxTpl({
151 HTMLExtraHead: conf.get('HTMLExtraHead'),
152 hideHelpBlocks: conf.get('hideHelpBlocks'),
153 mypadsURL: mypadsURL
154 }));
155 };
156 app.get('/mypads/index.html', idxHandle);
157 app.get('/mypads/', idxHandle);
158
159 callback(null);
160 };
161
162 /**
163 * ## Internal functions helpers
164 *
165 * These functions are not private like with closures, for testing purposes,
166 * but they are expected be used only internally by other MyPads functions.
167 */
168
169 var fn = {};
170
171 /**
172 * `get` internal takes a mandatory `module` argument to call its `get` method.
173 * It will use `req.params.key` to get the database record and returns an
174 * according response. By the way, an optional `cond`ition can be passed. It
175 * is a function that takes the result of the module.get and *true* or
176 * *false*. If *false*, an error will be returned.
177 */
178
179 fn.get = function (module, req, res, next, cond) {
180 try {
181 module.get(req.params.key, function (err, val) {
182 if (err) {
183 return res.status(404).send({
184 error: err.message,
185 key: req.params.key
186 });
187 }
188 if (cond && !cond(val)) {
189 if (conf.get('allPadsPublicsAuthentifiedOnly')) {
190 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.MUST_BE');
191 } else {
192 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.DENIED_RECORD');
193 }
194 } else {
195 return res.send({ key: req.params.key, value: val });
196 }
197 });
198 }
199 catch (e) {
200 res.status(400).send({ error: e.message });
201 }
202 };
203
204 /**
205 * `set` internal takes :
206 *
207 * - a `setFn` bounded function targetted the original `set` from the module
208 * used in the case of this public API
209 * - `key` and `value` that has been given to the `setFn` function
210 * - `req` and `res` express request and response
211 */
212
213 fn.set = function (setFn, key, value, req, res) {
214 try {
215 setFn(function (err, data) {
216 if (err) { return res.status(400).send({ error: err.message }); }
217 res.send({ success: true, key: key || data._id, value: data || value });
218 });
219 }
220 catch (e) {
221 res.status(400).send({ error: e.message });
222 }
223 };
224
225 /**
226 * `del` internal takes four arguments :
227 *
228 * - `delFn` bounded function targetted the original `del` method from the
229 * module used
230 * - classical `req` and `res` express parameters, with mandatory
231 * *req.params.key*.
232 */
233
234 fn.del = function (delFn, req, res) {
235 var key = req.params.key;
236 delFn(key, function (err) {
237 if (err) { return res.status(404).send({ error: err.message }); }
238 res.send({ success: true, key: key });
239 });
240 };
241
242 /**
243 * `denied` is an internal helper that just takes `res` express response and
244 * an `errorCode` string. It returns an unothorized status.
245 */
246
247 fn.denied = function (res, errCode) {
248 return res
249 .status(401)
250 .send({ error: errCode });
251 };
252
253 /**
254 * `mailMessage` is an internal helper that computed email templates. It takes
255 * the `tpl` key of the template and the `data` needed. Optionally, a
256 * `lang`uage can be defined.
257 * It returns the computed template.
258 */
259
260 fn.mailMessage = function (tpl, data, lang) {
261 lang = lang || conf.get('defaultLanguage');
262 tpl = ld.template(api.l10n.mail[lang][tpl]);
263 return tpl(data);
264 };
265
266
267 /**
268 * `ensureAuthenticated` internal is an Express middleware takes `req`,
269 * `res` and `next`. It returns error or lets the next middleware go.
270 * It relies on `passport.authenticate` with default strategy : *jwt*.
271 */
272
273 fn.ensureAuthenticated = passport.authenticate('jwt', { session: false });
274
275 /**
276 * `isAdmin` internal is a function that checks if current connnected user is
277 * an Etherpad instance admin or not. It returns a boolean.
278 */
279
280 fn.isAdmin = function (req) {
281 var token = req.query.auth_token || req.body.auth_token;
282 if (!token) { return false; }
283 try {
284 var jwt_payload = jwt.verify(token, auth.secret);
285 var admin = auth.adminTokens[jwt_payload.login];
286 return (admin && (admin.key === jwt_payload.key));
287 }
288 catch (e) { return false; }
289 };
290
291 /**
292 * `ensureAdmin` internal is an Express middleware that takes classic `req`,
293 * `res` and `next`. It returns an error if the connected user is not
294 * an Etherpad instance admin.
295 */
296
297 fn.ensureAdmin = function (req, res, next) {
298 if (!fn.isAdmin(req)) {
299 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.ADMIN');
300 } else {
301 return next();
302 }
303 };
304
305 /**
306 * `ensureAdminOrSelf` internal Express middleware takes the classic `req`,
307 * `res` and `next`. It returns an error if the connected user tries to manage
308 * users other than himself and he is not an Etherpad logged admin.
309 */
310
311 fn.ensureAdminOrSelf = function (req, res, next) {
312 var isAdmin = fn.isAdmin(req);
313
314 var token = (req.body.auth_token || req.query.auth_token);
315 var login = req.params.key;
316 var u = auth.fn.getUser(token);
317 var isSelf = (u && login === u.login);
318 if (isSelf) { req.mypadsLogin = u.login; }
319
320 if (!isAdmin && !isSelf) {
321 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.DENIED');
322 } else {
323 return next();
324 }
325 };
326
327 /**
328 * ## Authentication API
329 */
330
331 authAPI = function (app) {
332 var authRoute = api.initialRoute + 'auth';
333
334 /**
335 * POST method : check, method returning success or error if given *login*
336 * and *password* do not match to what is stored into database
337 *
338 * Sample URL:
339 * http://etherpad.ndd/mypads/api/auth/check
340 */
341
342 app.post(authRoute + '/check', fn.ensureAuthenticated,
343 function (req, res) {
344 auth.fn.localFn(req.body.login, req.body.password, function (err, u) {
345 if (err) { return res.status(400).send({ error: err.message }); }
346 if (!u) {
347 var emsg = 'BACKEND.ERROR.AUTHENTICATION.PASSWORD_INCORRECT';
348 return res.status(400).send({ error: emsg });
349 }
350 res.status(200).send({ success: true });
351 });
352 }
353 );
354
355 /**
356 * GET method : login/cas, method redirecting to home if auth
357 * is a success, plus fixes a `login` session.
358 * Only used with CAS authentication
359 *
360 * Sample URL:
361 * http://etherpad.ndd/mypads/api/auth/login/cas
362 */
363
364 app.post(authRoute + '/login/cas', function (req, res) {
365 auth.fn.casAuth(req, res, function(err, infos) {
366 if (err) { return res.status(400).send({ error: err.message }); }
367 // JWTFn false arg = non-admin authentication
368 auth.fn.JWTFn(req, infos, false, function (err, u, info) {
369 if (err) { return res.status(400).send({ error: err.message }); }
370 if (!u) { return res.status(400).send({ error: info.message }); }
371 if (u.active) {
372 var token = {
373 login: u.login,
374 key: auth.tokens[u.login].key
375 };
376 return res.status(200).send({
377 success: true,
378 user: ld.omit(u, 'password'),
379 token: jwt.sign(token, auth.secret)
380 });
381 } else {
382 var msg = 'BACKEND.ERROR.AUTHENTICATION.ACTIVATION_NEEDED';
383 return fn.denied(res, msg);
384 }
385 });
386 });
387 });
388
389 /**
390 * POST method : login, method returning user object minus password if auth
391 * is a success, plus fixes a `login` session.
392 *
393 * Sample URL:
394 * http://etherpad.ndd/mypads/api/auth/login
395 */
396
397 app.post(authRoute + '/login', function (req, res) {
398 var eplAuthorToken;
399 if (typeof(req.body.login) !== 'undefined' && typeof(req.cookies['token-'+req.body.login]) !== 'undefined') {
400 eplAuthorToken = req.cookies['token-'+req.body.login];
401 } else if (typeof(req.cookies.token) !== 'undefined') {
402 eplAuthorToken = req.cookies.token;
403 }
404 auth.fn.JWTFn(req, req.body, false, function (err, u, info) {
405 if (err) { return res.status(400).send({ error: err.message }); }
406 if (!u) { return res.status(400).send({ error: info.message }); }
407 if (u.active) {
408 var token = {
409 login: u.login,
410 key: auth.tokens[u.login].key
411 };
412 if (!u.eplAuthorToken && typeof(eplAuthorToken) !== 'undefined') {
413 u.eplAuthorToken = eplAuthorToken;
414 var uToUpdate = ld.cloneDeep(u);
415 uToUpdate.password = req.body.password;
416 user.set(uToUpdate, function (err) {
417 if (err) { return res.status(400).send({ error: err.message }); }
418 return res.status(200).send({
419 success: true,
420 user: ld.omit(u, 'password'),
421 token: jwt.sign(token, auth.secret)
422 });
423 });
424 } else {
425 return res.status(200).send({
426 success: true,
427 user: ld.omit(u, 'password'),
428 token: jwt.sign(token, auth.secret)
429 });
430 }
431 } else {
432 var msg = 'BACKEND.ERROR.AUTHENTICATION.ACTIVATION_NEEDED';
433 return fn.denied(res, msg);
434 }
435 });
436 });
437
438 /**
439 * GET method : logout, method that destroy current cached token
440 *
441 * Sample URL:
442 * http://etherpad.ndd/mypads/api/auth/logout
443 */
444
445 app.get(authRoute + '/logout', fn.ensureAuthenticated, function (req, res) {
446 delete auth.tokens[req.mypadsLogin];
447 res.status(200).send({ success: true });
448 });
449
450 /**
451 * POST method : admin login, method that checks credentials and create
452 * admin token in case of success
453 *
454 * Sample URL:
455 * http://etherpad.ndd/mypads/api/auth/admin/login
456 */
457
458 app.post(authRoute + '/admin/login', function (req, res) {
459 auth.fn.JWTFn(req, req.body, true, function (err, u, info) {
460 if (err) { return res.status(400).send({ error: err.message }); }
461 if (!u) { return res.status(400).send({ error: info.message }); }
462 var token = {
463 login: u.login,
464 key: auth.adminTokens[u.login].key
465 };
466 return res.status(200).send({
467 success: true,
468 token: jwt.sign(token, auth.secret)
469 });
470 });
471 });
472
473 /**
474 * GET method : admin logout, method that destroy current admin token
475 *
476 * Sample URL:
477 * http://etherpad.ndd/mypads/api/auth/admin/logout
478 */
479
480 app.get(authRoute + '/admin/logout', fn.ensureAdmin, function (req, res) {
481 delete auth.adminTokens[req.mypadsLogin];
482 return res.status(200).send({ success: true });
483 });
484
485 };
486
487 /**
488 * ## Configuration API
489 */
490
491 configurationAPI = function (app) {
492 var confRoute = api.initialRoute + 'configuration';
493
494 /**
495 * GET method : get all configuration plus user info if logged, else config
496 * public fields.
497 *
498 * Sample URL:
499 * http://etherpad.ndd/mypads/api/configuration
500 */
501
502 app.get(confRoute, function (req, res) {
503 var u = auth.fn.getUser(req.query.auth_token);
504 var isAdmin = fn.isAdmin(req);
505 var action = (isAdmin === true) ? 'all' : 'public';
506 var value = conf[action]();
507 var resp = { value: value };
508 resp.auth = isAdmin || !!u;
509 if (u) { resp.user = u; }
510 /* Fix IE11 stupid habit of caching AJAX calls
511 * See http://www.dashbay.com/2011/05/internet-explorer-caches-ajax/
512 * and https://framagit.org/framasoft/Etherpad/ep_mypads/issues/220
513 */
514 res.set('Expires', '-1');
515 res.send(resp);
516 });
517
518 /**
519 * GET method : `configuration.get` key
520 * Reserved to Etherpad administrators
521 *
522 * Sample URL:
523 * http://etherpad.ndd/mypads/api/configuration/something
524 */
525
526 app.get(confRoute + '/:key', fn.ensureAdmin, function (req, res) {
527 var value = conf.get(req.params.key);
528 if (ld.isUndefined(value)) {
529 return res.status(404).send({
530 error: 'BACKEND.ERROR.CONFIGURATION.KEY_NOT_FOUND',
531 key: req.params.key
532 });
533 }
534 /* Fix IE11 stupid habit of caching AJAX calls
535 * See http://www.dashbay.com/2011/05/internet-explorer-caches-ajax/
536 * and https://framagit.org/framasoft/Etherpad/ep_mypads/issues/220
537 */
538 res.set('Expires', '-1');
539 res.send({ key: req.params.key, value: value });
540 });
541
542 /**
543 * POST/PUT methods : `configuration.set` key and value on initial
544 * Reserved to Etherpad administrators
545 *
546 * Sample URL for POST:
547 * http://etherpad.ndd/mypads/api/configuration
548 * for PUT
549 * http://etherpad.ndd/mypads/api/configuration/something
550 */
551
552 var _set = function (req, res) {
553 var key = (req.method === 'POST') ? req.body.key : req.params.key;
554 var value = req.body.value;
555 var setFn = ld.partial(conf.set, key, value);
556 fn.set(setFn, key, value, req, res);
557 };
558
559 app.post(confRoute, fn.ensureAdmin, _set);
560 app.put(confRoute + '/:key', fn.ensureAdmin, _set);
561
562 /**
563 * DELETE method : `configuration.del` key
564 * Reserved to Etherpad administrators
565 *
566 * Sample URL:
567 * http://etherpad.ndd/mypads/api/configuration/something
568 */
569
570 app.delete(confRoute + '/:key', fn.ensureAdmin,
571 ld.partial(fn.del, conf.del));
572
573 /**
574 * GET method
575 *
576 * Return the value of useFirstLastNameInPads configuration setting
577 *
578 * Sample URL:
579 * http://etherpad.ndd/mypads/api/configuration/public/usefirstlastname
580 */
581
582 app.get(confRoute + '/public/usefirstlastname', function (req, res) {
583 return res.send({ success: true, usefirstlastname: conf.get('useFirstLastNameInPads') });
584 });
585
586 /**
587 * GET method
588 *
589 * Return the value of allPadsPublicsAuthentifiedOnly configuration setting
590 * + given pad's group if allPadsPublicsAuthentifiedOnly is true
591 *
592 * Sample URL:
593 * http://etherpad.ndd/mypads/api/configuration/public/allpadspublicsauthentifiedonly
594 */
595
596 app.get(confRoute + '/public/allpadspublicsauthentifiedonly', function (req, res) {
597 var confValue = conf.get('allPadsPublicsAuthentifiedOnly');
598 var data = {
599 success: true,
600 allpadspublicsauthentifiedonly: confValue
601 };
602 /* Fix IE11 stupid habit of caching AJAX calls
603 * See http://www.dashbay.com/2011/05/internet-explorer-caches-ajax/
604 * and https://framagit.org/framasoft/Etherpad/ep_mypads/issues/220
605 */
606 res.set('Expires', '-1');
607 if (confValue) {
608 pad.get(req.query.pid, function (err, p) {
609 if (err) {
610 return res.send({ success: false, error: err });
611 }
612 data.group = p.group;
613 return res.send(data);
614 });
615 } else {
616 return res.send(data);
617 }
618 });
619
620 };
621
622 /**
623 * ## User API
624 *
625 * Most methods need `fn.ensureAdminOrSelf`
626 */
627
628 userAPI = function (app) {
629 var userRoute = api.initialRoute + 'user';
630 var allUsersRoute = api.initialRoute + 'all-users';
631 var searchUsersRoute = api.initialRoute + 'search-users';
632 var userlistRoute = api.initialRoute + 'userlist';
633
634 /**
635 * GET method : `user.userlist` with crud fixed to *get* and current login.
636 * Returns user userlists
637 * ensureAuthenticated needed
638 *
639 * Sample URL:
640 * http://etherpad.ndd/mypads/api/userlist
641 */
642
643 app.get(userlistRoute, fn.ensureAuthenticated,
644 function (req, res) {
645 var opts = { crud: 'get', login: req.mypadsLogin };
646 user.userlist(opts, function (err, u) {
647 if (err) { return res.status(400).send({ error: err.message }); }
648 /* Fix IE11 stupid habit of caching AJAX calls
649 * See http://www.dashbay.com/2011/05/internet-explorer-caches-ajax/
650 * and https://framagit.org/framasoft/Etherpad/ep_mypads/issues/220
651 */
652 res.set('Expires', '-1');
653 res.send({ value: u.userlists });
654 });
655 }
656 );
657
658 /**
659 * POST method : `user.userlist` creation with crud fixed to *add*, current
660 * login and userlist parameters, name and optionnally user logins
661 * ensureAuthenticated needed
662 *
663 * Sample URL :
664 * http://etherpad.ndd/mypads/api/userlist
665 */
666
667 app.post(userlistRoute, fn.ensureAuthenticated,
668 function (req, res) {
669 try {
670 var users = { absent: [], present: [] };
671 var lm = req.body.loginsOrEmails;
672 if (lm) { users = userCache.fn.getIdsFromLoginsOrEmails(lm); }
673 var opts = {
674 crud: 'add',
675 login: req.mypadsLogin,
676 name: req.body.name
677 };
678 if (users.uids) { opts.uids = users.uids; }
679 user.userlist(opts, function (err, u) {
680 if (err) { return res.status(400).send({ error: err.message }); }
681 return res.send({
682 success: true,
683 value: u.userlists,
684 present: users.present,
685 absent: users.absent
686 });
687 });
688 }
689 catch (e) { return res.status(400).send({ error: e.message }); }
690 }
691 );
692
693 /**
694 * PUT method : `user.userlist` update with crud fixed to *set*, current
695 * login, mandatory `ulistid` via request parameter and userlist arguments,
696 * `name` or user `logins`
697 * ensureAuthenticated needed
698 *
699 * Sample URL :
700 * http://etherpad.ndd/mypads/api/userlist/xxx
701 */
702
703 app.put(userlistRoute + '/:key', fn.ensureAuthenticated,
704 function (req, res) {
705 try {
706 var users = { absent: [], present: [] };
707 var lm = req.body.loginsOrEmails;
708 if (lm) { users = userCache.fn.getIdsFromLoginsOrEmails(lm); }
709 var opts = {
710 crud: 'set',
711 login: req.mypadsLogin,
712 ulistid: req.params.key,
713 name: req.body.name
714 };
715 if (users.uids) { opts.uids = users.uids; }
716 user.userlist(opts, function (err, u) {
717 if (err) { return res.status(400).send({ error: err.message }); }
718 return res.send({
719 success: true,
720 value: u.userlists,
721 present: users.present,
722 absent: users.absent
723 });
724 });
725 }
726 catch (e) { return res.status(400).send({ error: e.message }); }
727 }
728 );
729
730 /**
731 * DELETE method : `user.userlist` removal with crud fixed to *del*, current
732 * login, mandatory `ulistid` via request parameter
733 * ensureAuthenticated needed
734 *
735 * Sample URL :
736 * http://etherpad.ndd/mypads/api/userlist/xxx
737 */
738
739 app.delete(userlistRoute + '/:key', fn.ensureAuthenticated,
740 function (req, res) {
741 try {
742 var opts = {
743 crud: 'del',
744 login: req.mypadsLogin,
745 ulistid: req.params.key
746 };
747 user.userlist(opts, function (err, u) {
748 if (err) { return res.status(400).send({ error: err.message }); }
749 res.send({ success: true, value: u.userlists });
750 });
751 }
752 catch (e) { return res.status(400).send({ error: e.message }); }
753 }
754 );
755
756
757 /**
758 * GET method : `user.get` login (key)
759 *
760 * Sample URL:
761 * http://etherpad.ndd/mypads/api/user/someone
762 */
763
764 app.get(userRoute + '/:key', fn.ensureAdminOrSelf,
765 ld.partial(fn.get, user));
766
767 /**
768 * GET method : get all users from cache
769 *
770 * exemple: {
771 * usersCount: 1,
772 * users: {
773 * foo: {
774 * email: foo@bar.org,
775 * firstname: Foo,
776 * lastname: Bar
777 * }
778 * }
779 * }
780 *
781 * Sample URL:
782 * http://etherpad.ndd/mypads/api/all-users
783 */
784
785 app.get(allUsersRoute, fn.ensureAdmin,
786 function (req, res) {
787 var emails = ld.reduce(userCache.emails, function (result, n, key) {
788 result[n] = key;
789 return result;
790 }, {});
791 var users = ld.reduce(userCache.logins, function (result, n, key) {
792 result[key] = {
793 email: emails[n],
794 firstname: userCache.firstname[n],
795 lastname: userCache.lastname[n]
796 };
797 return result;
798 }, {});
799 res.send({ users: users, usersCount: ld.size(users) });
800 }
801 );
802
803 /**
804 * GET method : search users from their firstname, lastname, login or email
805 *
806 * exemple: {
807 * usersCount: 1,
808 * users: {
809 * foo: {
810 * email: foo@bar.org,
811 * firstname: Foo,
812 * lastname: Bar
813 * }
814 * }
815 * }
816 *
817 * Sample URL:
818 * http://etherpad.ndd/mypads/api/search-users/parker
819 */
820
821 app.get(searchUsersRoute + '/:key', fn.ensureAdmin,
822 function (req, res) {
823 var users = userCache.fn.searchUserInfos(req.params.key);
824 res.send({ users: users, usersCount: ld.size(users) });
825 }
826 );
827
828 // `set` for POST and PUT, see below
829 var _set = function (req, res) {
830 var key;
831 var value = req.body;
832 var stop;
833 if (req.method === 'POST' && !fn.isAdmin(req)) {
834 if (conf.isNotInternalAuth() || !conf.get('openRegistration')) {
835 stop = true;
836 res.status(400).send({ error: 'BACKEND.ERROR.AUTHENTICATION.NO_REGISTRATION' });
837 } else {
838 key = req.body.login;
839 if (conf.get('checkMails')) {
840 var token = mail.genToken({ login: key, action: 'accountconfirm' });
841 var url = conf.get('rootUrl') +
842 '/mypads/index.html?/accountconfirm/' + token;
843 console.log(url);
844 var lang = (function () {
845 if (ld.includes(ld.keys(conf.cache.languages), req.body.lang)) {
846 return req.body.lang;
847 } else {
848 return conf.get('defaultLanguage');
849 }
850 })();
851 var subject = fn.mailMessage('ACCOUNT_CONFIRMATION_SUBJECT', {
852 title: conf.get('title') });
853 var message = fn.mailMessage('ACCOUNT_CONFIRMATION', {
854 login: key,
855 title: conf.get('title'),
856 url: url,
857 duration: conf.get('tokenDuration')
858 }, lang);
859 mail.send(req.body.email, subject, message, function (err) {
860 if (err) {
861 stop = true;
862 return res.status(501).send({ error: err });
863 }
864 }, lang);
865 }
866 }
867 } else {
868 key = req.params.key;
869 value.login = req.body.login || key;
870 value._id = userCache.logins[key];
871 }
872 // Update needed session values
873 if (!stop) {
874 var u = auth.fn.getUser(req.body.auth_token);
875 if (u && !fn.isAdmin(req)) {
876 auth.tokens[u.login].color = req.body.color || u.color;
877 if (!ld.isUndefined(req.body.useLoginAndColorInPads)) {
878 auth.tokens[u.login].useLoginAndColorInPads = req.body.useLoginAndColorInPads;
879 }
880 }
881 if (fn.isAdmin(req) && req.method !== 'POST') {
882 delete value.auth_token;
883 delete value.passwordConfirm;
884 user.get(value.login, function (err, u) {
885 if (err) { return res.status(400).send({ error: err.message }); }
886 if (value.password) {
887 common.hashPassword(null, value.password, function (err, pass) {
888 if (err) { return res.status(400).send({ error: err }); }
889
890 value.password = pass;
891 ld.assign(u, value);
892 var setFn = ld.partial(user.fn.set, u);
893 fn.set(setFn, key, u, req, res);
894 });
895 } else {
896 ld.assign(u, value);
897 var setFn = ld.partial(user.fn.set, u);
898 fn.set(setFn, key, u, req, res);
899 }
900 });
901 } else {
902 var setFn = ld.partial(user.set, value);
903 fn.set(setFn, key, value, req, res);
904 }
905 }
906 };
907
908 /**
909 * POST method : `user.set` with user value for user creation
910 * Only method without permission verification
911 *
912 * Sample URL:
913 * http://etherpad.ndd/mypads/api/user
914 */
915
916 app.post(userRoute, _set);
917
918 /**
919 * PUT method : `user.set` with user key/login plus value for existing user
920 *
921 * Sample URL:
922 * http://etherpad.ndd/mypads/api/user/someone
923 */
924
925 app.put(userRoute + '/:key', fn.ensureAdminOrSelf, _set);
926
927 /**
928 * DELETE method : `user.del` with user key/login
929 * Destroy session if self account removal
930 *
931 * Sample URL:
932 * http://etherpad.ndd/mypads/api/user/someone
933 */
934
935 app.delete(userRoute + '/:key', fn.ensureAdminOrSelf, function (req, res) {
936 var isSelf = (req.params.key === req.mypadsLogin);
937 if (isSelf) { delete auth.tokens[req.mypadsLogin]; }
938 fn.del(user.del, req, res);
939 });
940
941 /**
942 * POST method : `user.mark` with user session login, bookmark type and key
943 * Only need ensureAuthenticated because use own login for marking
944 *
945 * Sample URL:
946 * http://etherpad.ndd/mypads/api/usermark
947 */
948
949 app.post(userRoute + 'mark', fn.ensureAuthenticated,
950 function (req, res) {
951 try {
952 user.mark(req.mypadsLogin, req.body.type, req.body.key,
953 function (err) {
954 if (err) { return res.status(404).send({ error: err.message }); }
955 res.send({ success: true });
956 }
957 );
958 }
959 catch (e) { res.status(400).send({ error: e.message }); }
960 }
961 );
962
963 /**
964 * POST method : special password recovery with mail sending.
965 * Need to have the email address into the body
966 *
967 * Sample URL:
968 * http://etherpad.ndd/mypads/api/passrecover
969 */
970
971 app.post(api.initialRoute + 'passrecover', function (req, res) {
972 var email = req.body.email;
973 var err;
974 if (conf.isNotInternalAuth()) {
975 err = 'BACKEND.ERROR.AUTHENTICATION.NO_RECOVER';
976 return res.status(400).send({ error: err });
977 }
978 if (!ld.isEmail(email)) {
979 err = 'BACKEND.ERROR.TYPE.MAIL';
980 return res.status(400).send({ error: err });
981 }
982 if (!userCache.emails[email]) {
983 err = 'BACKEND.ERROR.USER.NOT_FOUND';
984 return res.status(404).send({ error: err });
985 }
986 if (conf.get('rootUrl').length === 0) {
987 err = 'BACKEND.ERROR.CONFIGURATION.ROOTURL_NOT_CONFIGURED';
988 return res.status(501).send({ error: err });
989 }
990 user.get(email, function (err, u) {
991 if (err) { return res.status(400).send({ error: err }); }
992 var token = mail.genToken({ login: u.login, action: 'passrecover' });
993 console.log(conf.get('rootUrl') + '/mypads/index.html?/passrecover/' +
994 token);
995 var subject = fn.mailMessage('PASSRECOVER_SUBJECT', {
996 title: conf.get('title') }, u.lang);
997 var message = fn.mailMessage('PASSRECOVER', {
998 login: u.login,
999 title: conf.get('title'),
1000 url: conf.get('rootUrl') + '/mypads/index.html?/passrecover/' + token,
1001 duration: conf.get('tokenDuration')
1002 }, u.lang);
1003 mail.send(u.email, subject, message, function (err) {
1004 if (err) { return res.status(501).send({ error: err }); }
1005 return res.send({ success: true });
1006 });
1007 });
1008 });
1009
1010 /**
1011 * PUT method : password recovery with token and new password
1012 * Need to have the login into the body
1013 *
1014 * Sample URL:
1015 * http://etherpad.ndd/mypads/api/passrecover
1016 */
1017
1018 app.put(api.initialRoute + 'passrecover/:token', function (req, res) {
1019 var err;
1020 var val = mail.tokens[req.params.token];
1021 var badLogin = (!val || !val.login || !userCache.logins[val.login]);
1022 var badAction = (!val || !val.action || (val.action !== 'passrecover'));
1023 if (conf.isNotInternalAuth()) {
1024 err = 'BACKEND.ERROR.AUTHENTICATION.NO_RECOVER';
1025 return res.status(400).send({ error: err });
1026 }
1027 if (badLogin || badAction) {
1028 err = 'BACKEND.ERROR.TOKEN.INCORRECT';
1029 return res.status(400).send({ error: err });
1030 }
1031 if (!mail.isValidToken(req.params.token)) {
1032 err = 'BACKEND.ERROR.TOKEN.EXPIRED';
1033 return res.status(400).send({ error: err });
1034 }
1035 var pass = req.body.password;
1036 var passC = req.body.passwordConfirm;
1037 if (!pass || (pass !== passC)) {
1038 err = 'USER.ERR.PASSWORD_MISMATCH';
1039 return res.status(400).send({ error: err });
1040 }
1041 user.get(val.login, function (err, u) {
1042 if (err) { return res.status(400).send({ error: err.message }); }
1043 u.password = pass;
1044 if (!u.active) { u.active = true; }
1045 user.set(u, function (err) {
1046 if (err) { return res.status(400).send({ error: err.message }); }
1047 res.send({ success: true, login: val.login });
1048 });
1049 });
1050 });
1051
1052 /**
1053 * POST method : account confirmation with token on body
1054 *
1055 * Sample URL:
1056 * http://etherpad.ndd/mypads/api/accountconfirm
1057 */
1058
1059 app.post(api.initialRoute + 'accountconfirm', function (req, res) {
1060 var val = mail.tokens[req.body.token];
1061 var err;
1062 if (conf.isNotInternalAuth()) {
1063 err = 'BACKEND.ERROR.AUTHENTICATION.NO_RECOVER';
1064 return res.status(400).send({ error: err });
1065 }
1066 if (!val || !val.action || (val.action !== 'accountconfirm')) {
1067 err = 'BACKEND.ERROR.TOKEN.INCORRECT';
1068 return res.status(400).send({ error: err });
1069 }
1070 if (!mail.isValidToken(req.body.token)) {
1071 err = 'BACKEND.ERROR.TOKEN.EXPIRED';
1072 return res.status(400).send({ error: err });
1073 }
1074 user.get(val.login, function (err, u) {
1075 if (err) { return res.status(400).send({ error: err.message }); }
1076 u.active = true;
1077 user.fn.set(u, function (err) {
1078 if (err) { return res.status(400).send({ error: err.message }); }
1079 res.send({ success: true, login: val.login });
1080 });
1081 });
1082 });
1083
1084 };
1085
1086 /**
1087 * ## Group API
1088 *
1089 * All methods except creation need special permissions.
1090 */
1091
1092 groupAPI = function (app) {
1093 var groupRoute = api.initialRoute + 'group';
1094
1095 /**
1096 * GET method : `group.getByUser` via user login. passwords are omitted
1097 * Returns all groups and pads, filtered from sensitive data.
1098 *
1099 * Only for authenticated users.
1100 *
1101 * Sample URL:
1102 * http://etherpad.ndd/mypads/api/group
1103 */
1104
1105 app.get(groupRoute, fn.ensureAuthenticated,
1106 function (req, res) {
1107 user.get(req.mypadsLogin, function (err, u) {
1108 if (err) { return res.status(400).send({ error: err }); }
1109 try {
1110 group.getByUser(u, true, function (err, data) {
1111 if (err) {
1112 return res.status(404).send({
1113 error: err.message
1114 });
1115 }
1116 data.groups = ld.transform(data.groups,
1117 function (memo, val, key) {
1118 memo[key] = ld.omit(val, 'password');
1119 }
1120 );
1121 data.pads = ld.transform(data.pads,
1122 function (memo, val, key) {
1123 memo[key] = ld.omit(val, 'password');
1124 }
1125 );
1126 data.users = ld.transform(data.users, function (memo, val, key) {
1127 memo[key] = ld.pick(val, '_id', 'login', 'firstname',
1128 'lastname', 'email');
1129 });
1130 data.admins = ld.transform(data.admins,
1131 function (memo, val, key) {
1132 memo[key] = ld.pick(val, '_id', 'login', 'firstname',
1133 'lastname', 'email');
1134 }
1135 );
1136 data.bookmarks = { groups: {}, pads: {} };
1137 group.getBookmarkedGroupsByUser(u, function (err, bookmarks) {
1138 if (err) {
1139 return res.status(404).send({
1140 error: err.message
1141 });
1142 }
1143 data.bookmarks.groups = ld.transform(bookmarks,
1144 function (memo, val, key) {
1145 memo[key] = ld.omit(val, 'password');
1146 }
1147 );
1148 pad.getBookmarkedPadsByUser(u, function (err, bookmarks) {
1149 if (err) {
1150 return res.status(404).send({
1151 error: err.message
1152 });
1153 }
1154 data.bookmarks.pads = ld.transform(bookmarks,
1155 function (memo, val, key) {
1156 memo[key] = ld.omit(val, 'password');
1157 }
1158 );
1159 /* Fix IE11 stupid habit of caching AJAX calls
1160 * See http://www.dashbay.com/2011/05/internet-explorer-caches-ajax/
1161 * and https://framagit.org/framasoft/Etherpad/ep_mypads/issues/220
1162 */
1163 res.set('Expires', '-1');
1164 res.send({ value: data });
1165 });
1166 });
1167 });
1168 }
1169 catch (e) {
1170 res.status(400).send({ error: e.message });
1171 }
1172 });
1173 }
1174 );
1175
1176 /**
1177 * GET method : `group.getWithPads` unique id
1178 * Returns pads too because useful for public groups and unauth users.
1179 *
1180 * Sample URL:
1181 * http://etherpad.ndd/mypads/api/group/xxxx
1182 */
1183
1184 app.get(groupRoute + '/:key', function (req, res) {
1185 try {
1186 var key = req.params.key;
1187 group.getWithPads(key, function (err, g, pads) {
1188 if (err) {
1189 return res.status(404).send({ key: key, error: err.message });
1190 }
1191 var u = auth.fn.getUser(req.query.auth_token);
1192 var isAdmin = fn.isAdmin(req);
1193 var isUser = (u && ld.includes(ld.union(g.admins, g.users), u._id));
1194 var isAllowedForPublic = (g.visibility === 'public');
1195
1196 // allPadsPublicsAuthentifiedOnly feature
1197 if (conf.get('allPadsPublicsAuthentifiedOnly')) {
1198 isAllowedForPublic = false;
1199 if (u && !isAdmin) {
1200 isUser = true;
1201 }
1202 }
1203
1204 if (isAllowedForPublic && !isAdmin && !isUser) {
1205 pads = ld.transform(pads, function (memo, p, key) {
1206 if (!p.visibility || p.visibility === g.visibility) {
1207 memo[key] = p;
1208 }
1209 });
1210 }
1211 if (isAdmin || isUser || isAllowedForPublic) {
1212 return res.send({ key: key, value: g, pads: pads });
1213 }
1214 var isPrivate = (g.visibility === 'private' && !conf.get('allPadsPublicsAuthentifiedOnly'));
1215 if (isPrivate) {
1216 if (req.query.password) {
1217 var pwd = (ld.isUndefined(req.query.password)) ? undefined : decode(req.query.password);
1218 auth.fn.isPasswordValid(g, pwd,
1219 function (err, valid) {
1220 if (!err && !valid) {
1221 err = { message: 'BACKEND.ERROR.PERMISSION.UNAUTHORIZED' };
1222 }
1223 if (err) {
1224 return res.status(401)
1225 .send({ key: key, error: err.message });
1226 }
1227 if (isPrivate && !isAdmin && !isUser) {
1228 pads = ld.transform(pads, function (memo, p, key) {
1229 if (!p.visibility || p.visibility === g.visibility) {
1230 memo[key] = p;
1231 }
1232 });
1233 }
1234 return res.send({ key: key, value: g, pads: pads });
1235 }
1236 );
1237 } else {
1238 var value = ld.pick(g, 'name', 'visibility');
1239 return res.send({ key: req.params.key, value: value });
1240 }
1241 } else {
1242 if (conf.get('allPadsPublicsAuthentifiedOnly')) {
1243 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.MUST_BE');
1244 } else {
1245 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.DENIED_RECORD');
1246 }
1247 }
1248 });
1249 }
1250 catch (e) { res.status(400).send({ error: e.message }); }
1251 });
1252
1253 /**
1254 * `canEdit` is an asynchronous internal group helper to check common
1255 * permissions for edit methods used here (set && delete ones). It ensures
1256 * that the current user is either an Etherpad admin or a group admin.
1257 *
1258 * It takes the `req` and `res` request and response Express objects, and a
1259 * `successFn` function, called if the user is allowed. Otherwise, it uses
1260 * response to return statusCode and Error messages.
1261 */
1262
1263 var canEdit = function (req, res, successFn) {
1264 var isAdmin = fn.isAdmin(req);
1265 if (isAdmin) { return successFn(); }
1266 var u = auth.fn.getUser(req.body.auth_token);
1267 if (!u) {
1268 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.NOT_AUTH');
1269 }
1270 group.get(req.params.key, function (err, g) {
1271 if (err) { return res.status(400).send({ error: err.message }); }
1272 var isAllowed = ld.includes(g.admins, auth.tokens[u.login]._id);
1273 if (isAllowed) {
1274 return successFn();
1275 } else {
1276 return fn.denied(res,
1277 'BACKEND.ERROR.AUTHENTICATION.DENIED_RECORD_EDIT');
1278 }
1279 });
1280 };
1281
1282 // `set` for POST and PUT, see below
1283 var _set = function (req, res) {
1284 var setFn = ld.partial(group.set, req.body);
1285 fn.set(setFn, req.body._id, req.body, req, res);
1286 };
1287
1288 /**
1289 * POST method : `group.set` with user value for group creation
1290 *
1291 * Sample URL:
1292 * http://etherpad.ndd/mypads/api/group
1293 */
1294
1295 app.post(groupRoute, function (req, res) {
1296 var isAdmin = fn.isAdmin(req);
1297 var u = auth.fn.getUser(req.body.auth_token);
1298 if (!u && !isAdmin) {
1299 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.NOT_AUTH');
1300 }
1301 if (!isAdmin) { req.body.admin = u._id; }
1302 _set(req, res);
1303 });
1304
1305 /**
1306 * PUT method : `group.set` with group id plus value for existing group
1307 *
1308 * Sample URL:
1309 * http://etherpad.ndd/mypads/api/group/xxx
1310 */
1311
1312 app.put(groupRoute + '/:key', function (req, res) {
1313 canEdit(req, res, ld.partial(_set, req, res));
1314 });
1315
1316 /**
1317 * DELETE method : `group.del` with group id
1318 *
1319 * Sample URL:
1320 * http://etherpad.ndd/mypads/api/group/xxxx
1321 */
1322
1323 app.delete(groupRoute + '/:key', function (req, res) {
1324 canEdit(req, res, ld.partial(fn.del, group.del, req, res));
1325 }
1326 );
1327
1328 /**
1329 * POST method : `group.inviteOrShare` with gid group id, array of all
1330 * concerned loginsOrEmails and invite boolean
1331 * This method is open to all authenticated users.
1332 *
1333 * Sample URL:
1334 * http://etherpad.ndd/mypads/api/group/invite
1335 */
1336
1337 app.post(groupRoute + '/invite', function (req, res) {
1338 if (!req.body.gid) {
1339 return res.status(400)
1340 .send({ error: 'BACKEND.ERROR.TYPE.PARAMS_REQUIRED' });
1341 }
1342 req.params.key = req.body.gid;
1343 var successFn = ld.partial(function (req, res) {
1344 try {
1345 group.inviteOrShare(req.body.invite, req.body.gid,
1346 req.body.loginsOrEmails, function (err, g, uids) {
1347 if (err) {
1348 return res.status(401).send({ error: err.message });
1349 }
1350 return res.send(ld.assign({ success: true, value: g }, uids));
1351 });
1352 }
1353 catch (e) { res.status(400).send({ error: e.message }); }
1354 }, req, res);
1355 canEdit(req, res, successFn);
1356 });
1357
1358 /**
1359 * POST method : `group.resign` with gid group id and current session uid.
1360 *
1361 * Sample URL:
1362 * http://etherpad.ndd/mypads/api/group/resign
1363 */
1364
1365 app.post(groupRoute + '/resign', fn.ensureAuthenticated,
1366 function (req, res) {
1367 try {
1368 group.resign(req.body.gid, auth.tokens[req.mypadsLogin]._id,
1369 function (err, g) {
1370 if (err) { return res.status(400).send({ error: err.message }); }
1371 return res.send({ success: true, value: g });
1372 }
1373 );
1374 }
1375 catch (e) { return res.status(400).send({ error: e.message }); }
1376 }
1377 );
1378
1379 };
1380
1381 /**
1382 * ## Pad API
1383 *
1384 * All methods needs special permissions
1385 */
1386
1387 padAPI = function (app) {
1388 var padRoute = api.initialRoute + 'pad';
1389
1390 /**
1391 * `canAct` is a pad internal function that checks permissions to allow or
1392 * not interaction with the pad object.
1393 *
1394 * It takes :
1395 *
1396 * - `edit` boolean, if the pad should be only read or updated
1397 * - `successFn` functional, to be called if allowed, that takes req, res
1398 * and value
1399 * - classic `req`uest and `res`ponse Express objects
1400 */
1401
1402 var canAct = function (edit, successFn, req, res) {
1403 pad.get(req.params.key, function (err, p) {
1404 var key = req.params.key;
1405 if (err) { return res.status(404).send({ error: err.message }); }
1406 var isAdmin = fn.isAdmin(req);
1407 if (isAdmin) { return successFn(req, res, p); }
1408
1409 // allPadsPublicsAuthentifiedOnly feature
1410 if (conf.get('allPadsPublicsAuthentifiedOnly') && req.route.method === 'get') {
1411 var token = req.body.auth_token || req.query.auth_token;
1412 var u = auth.fn.getUser(token);
1413 return (!u) ? fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.MUST_BE') : successFn(req, res, p);
1414 }
1415
1416 if (!edit && (p.visibility === 'public')) {
1417 return successFn(req, res, p);
1418 }
1419 if (!edit && (p.visibility === 'private')) {
1420 var pwd = (ld.isUndefined(req.query.password)) ? undefined : decode(req.query.password);
1421 auth.fn.isPasswordValid(p, pwd, function (err, valid) {
1422 if (!err && !valid) {
1423 err = { message: 'BACKEND.ERROR.PERMISSION.UNAUTHORIZED' };
1424 }
1425 if (err) {
1426 return res.status(401).send({ key: key, error: err.message });
1427 }
1428 return successFn(req, res, p);
1429 });
1430 } else {
1431 group.get(p.group, function (err, g) {
1432 if (err) { return res.status(400).send({ error: err.message }); }
1433 if (!edit && (g.visibility === 'public')) {
1434 return successFn(req, res, p);
1435 } else if (!edit && (g.visibility === 'private')) {
1436 var pwd = (ld.isUndefined(req.query.password)) ? undefined : decode(req.query.password);
1437 auth.fn.isPasswordValid(g, pwd,
1438 function (err, valid) {
1439 if (!err && !valid) {
1440 err = { message: 'BACKEND.ERROR.PERMISSION.UNAUTHORIZED' };
1441 }
1442 if (err) {
1443 return res.status(401)
1444 .send({ key: key, error: err.message });
1445 }
1446 return successFn(req, res, p);
1447 }
1448 );
1449 } else {
1450 var token = req.body.auth_token || req.query.auth_token;
1451 var u = auth.fn.getUser(token);
1452 if (!u) {
1453 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.MUST_BE');
1454 }
1455 var users = edit ? g.admins : ld.union(g.admins, g.users);
1456 var uid = auth.tokens[u.login]._id;
1457 var isAllowed = ld.includes(users, uid);
1458 if (isAllowed) {
1459 return successFn(req, res, p);
1460 } else {
1461 var msg = edit ? 'DENIED_RECORD_EDIT' : 'DENIED_RECORD';
1462 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.' + msg);
1463 }
1464 }
1465 });
1466 }
1467 });
1468 };
1469
1470 /**
1471 * GET method : `pad.get` unique id
1472 * Only for group admin or users, and global admin
1473 *
1474 * Sample URL:
1475 * http://etherpad.ndd/mypads/api/pad/xxxx
1476 */
1477
1478 // TODO: + admin, no pass needed...
1479 app.get(padRoute + '/:key',
1480 ld.partial(canAct, false, function (req, res, val) {
1481 return res.send({ key: req.params.key, value: val });
1482 }));
1483
1484 // `set` for POST and PUT, see below
1485 var _set = function (req, res) {
1486 var setFn = ld.partial(pad.set, req.body);
1487 fn.set(setFn, req.body._id, req.body, req, res);
1488 };
1489
1490 /**
1491 * POST method : `pad.set` with user value for pad creation
1492 *
1493 * Sample URL:
1494 * http://etherpad.ndd/mypads/api/pad
1495 */
1496
1497 app.post(padRoute, function (req, res) {
1498 var isAdmin = fn.isAdmin(req);
1499 var u;
1500 if (!isAdmin) { u = auth.fn.getUser(req.body.auth_token); }
1501 if (!isAdmin && !u) {
1502 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.NOT_AUTH');
1503 }
1504 if (isAdmin) {
1505 _set(req, res);
1506 } else {
1507 if (req.body.group) {
1508 group.get(req.body.group, function (err, g) {
1509 if (err) {
1510 if (ld.isEqual(err, new Error('BACKEND.ERROR.CONFIGURATION.KEY_NOT_FOUND'))) {
1511 err = 'BACKEND.ERROR.PAD.ITEMS_NOT_FOUND';
1512 }
1513 return res.status(400).send({ success: false, error: err });
1514 }
1515 if (ld.isUndefined(g)) {
1516 return res.status(400).send({ success: false, error: 'BACKEND.ERROR.PAD.ITEMS_NOT_FOUND'});
1517 } else {
1518 if (ld.indexOf(g.admins, u._id) !== -1 ||
1519 (g.allowUsersToCreatePads && ld.indexOf(g.users, u._id) !== -1)) {
1520 _set(req, res);
1521 } else {
1522 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.DENIED');
1523 }
1524 }
1525 });
1526 } else {
1527 return res.status(400).send({ success: false, error: 'BACKEND.ERROR.TYPE.PARAM_STR'});
1528 }
1529 }
1530 });
1531
1532 /**
1533 * PUT method : `pad.set` with group id plus value for existing pad
1534 *
1535 * Sample URL:
1536 * http://etherpad.ndd/mypads/api/pad/xxx
1537 */
1538
1539 app.put(padRoute + '/:key', ld.partial(canAct, true, _set));
1540
1541 /**
1542 * DELETE method : `pad.del` with pad id
1543 *
1544 * Sample URL:
1545 * http://etherpad.ndd/mypads/api/pad/xxxx
1546 */
1547
1548 app.delete(padRoute + '/:key', function (req, res) {
1549 var isAdmin = fn.isAdmin(req);
1550 var u;
1551 if (!isAdmin) { u = auth.fn.getUser(req.body.auth_token); }
1552 if (!isAdmin && !u) {
1553 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.NOT_AUTH');
1554 }
1555 canAct(true, ld.partial(fn.del, pad.del), req, res);
1556 });
1557
1558 /**
1559 * DELETE method : `pad.delChatHistory` with pad id
1560 *
1561 * Sample URL:
1562 * http://etherpad.ndd/mypads/api/pad/chathistory/xxxx
1563 */
1564
1565 app.delete(padRoute + '/chathistory/:key', function (req, res) {
1566 var isAdmin = fn.isAdmin(req);
1567 var u;
1568 if (!isAdmin) { u = auth.fn.getUser(req.body.auth_token); }
1569 if (!isAdmin && !u) {
1570 return fn.denied(res, 'BACKEND.ERROR.AUTHENTICATION.NOT_AUTH');
1571 }
1572 canAct(true, ld.partial(fn.del, pad.delChatHistory), req, res);
1573 });
1574
1575 /**
1576 * GET method : with pad id
1577 *
1578 * Return true if the pad is public
1579 *
1580 * Sample URL:
1581 * http://etherpad.ndd/mypads/api/pad/ispublic/xxxx
1582 */
1583
1584 app.get(padRoute + '/ispublic/:key', function (req, res) {
1585 pad.get(req.params.key, function(err, p) {
1586 if (err) {
1587 return res.send({ success: false, error: err });
1588 } else if (p.visibility !== null) {
1589 return res.send({ success: true, key: req.params.key, ispublic: (p.visibility === 'public') });
1590 } else {
1591 group.get(p.group, function(err, g) {
1592 if (err) { return res.send({ success: false, error: err }); }
1593 return res.send({ success: true, key: req.params.key, ispublic: (g.visibility === 'public') });
1594 });
1595 }
1596 });
1597 });
1598 };
1599
1600 cacheAPI = function (app) {
1601 var cacheRoute = api.initialRoute + 'cache';
1602
1603 /**
1604 * GET method : check, method returning information about the end of users
1605 * cache loading
1606 *
1607 * exemple: { "userCacheReady": true }
1608 *
1609 * Sample URL:
1610 * http://etherpad.ndd/mypads/api/cache/check
1611 */
1612
1613 app.get(cacheRoute + '/check', function (req, res) {
1614 return res.send({ userCacheReady: userCache.userCacheReady });
1615 });
1616
1617 };
1618
1619 statsAPI = function (app) {
1620 var statsRoute = api.initialRoute + 'stats';
1621
1622 /**
1623 * GET method : stats.json, method returning some stats about MyPads
1624 * instance usage
1625 *
1626 * exemple: { "timestamp":1524035674, "users":3, "pad":3, "groups":3 }
1627 *
1628 * Sample URL:
1629 * http://etherpad.ndd/mypads/api/stats/stats.json
1630 */
1631
1632 app.get(statsRoute + '/stats.json', function (req, res) {
1633 var time = Math.floor(Date.now() / 1000);
1634
1635 pad.count(function(err, pcount) {
1636 if (err) { return res.send({ timestamp: time, err: err }); }
1637 group.count(function(err, gcount) {
1638 if (err) { return res.send({ timestamp: time, err: err }); }
1639 return res.send({
1640 timestamp: time,
1641 users: ld.size(userCache.logins),
1642 pad: pcount,
1643 groups: gcount
1644 });
1645 });
1646 });
1647 });
1648 };
1649
1650 return api;
1651
1652}).call(this);