浏览代码

- Fix critical and moderate security vulnerabilities reported at 2020-02-26 with
responsible disclosure by [Dejan Zelic](https://twitter.com/dejandayoff),
Justin Benjamin and others at [Offensive Security](https://twitter.com/offsectraining),
that follow standard 90 days before public disclosure.
Thanks to xet7.
- Fix webhook error that prevented some card etc deleting from web UI of board.
Thanks to xet7.
- Add some more Font Awesome icons.
Thanks to xet7.
- Remove autofocus from many form input boxes so that they would not cause warnings.
Thanks to xet7.

Lauri Ojansivu 5 年之前
父节点
当前提交
aac7c380c8

+ 2 - 2
client/components/settings/peopleBody.jade

@@ -110,7 +110,7 @@ template(name="editUserPopup")
     label.hide.userId(type="text" value=user._id)
     label.hide.userId(type="text" value=user._id)
     label
     label
       | {{_ 'fullname'}}
       | {{_ 'fullname'}}
-      input.js-profile-fullname(type="text" value=user.profile.fullname autofocus)
+      input.js-profile-fullname(type="text" value=user.profile.fullname)
     label
     label
       | {{_ 'username'}}
       | {{_ 'username'}}
       span.error.hide.username-taken
       span.error.hide.username-taken
@@ -159,7 +159,7 @@ template(name="newUserPopup")
     //label.hide.userId(type="text" value=user._id)
     //label.hide.userId(type="text" value=user._id)
     label
     label
       | {{_ 'fullname'}}
       | {{_ 'fullname'}}
-      input.js-profile-fullname(type="text" value="" autofocus)
+      input.js-profile-fullname(type="text" value="")
     label
     label
       | {{_ 'username'}}
       | {{_ 'username'}}
       span.error.hide.username-taken
       span.error.hide.username-taken

+ 6 - 3
client/components/sidebar/sidebar.jade

@@ -245,7 +245,7 @@ template(name="outgoingWebhooksPopup")
         b  
         b  
         .materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}")
         .materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}")
       input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
       input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
-      input.js-outgoing-webhooks-url(type="text" name="url" value=url autofocus)
+      input.js-outgoing-webhooks-url(type="text" name="url" value=url)
       input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token")
       input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token")
       select.js-outgoing-webhooks-type(name="type")
       select.js-outgoing-webhooks-type(name="type")
           each _type in types
           each _type in types
@@ -257,7 +257,7 @@ template(name="outgoingWebhooksPopup")
       input(type="hidden" value=_id name="id")
       input(type="hidden" value=_id name="id")
       input.primary.wide(type="submit" value="{{_ 'save'}}")
       input.primary.wide(type="submit" value="{{_ 'save'}}")
   form.integration-form
   form.integration-form
-    input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" autofocus)
+    input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title")
     input.js-outgoing-webhooks-url(placeholder="{{_ 'URL' }}" type="text" name="url")
     input.js-outgoing-webhooks-url(placeholder="{{_ 'URL' }}" type="text" name="url")
     input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" name="token")
     input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" name="token")
     select.js-outgoing-webhooks-type(name="type")
     select.js-outgoing-webhooks-type(name="type")
@@ -267,7 +267,10 @@ template(name="outgoingWebhooksPopup")
 
 
 template(name="boardMenuPopup")
 template(name="boardMenuPopup")
   ul.pop-over-list
   ul.pop-over-list
-    li: a.js-custom-fields {{_ 'custom-fields'}}
+    li
+      a.js-custom-fields
+        i.fa.fa-list-alt
+        | {{_ 'custom-fields'}}
     li
     li
       a.js-open-archives
       a.js-open-archives
         i.fa.fa-archive
         i.fa.fa-archive

+ 6 - 3
models/activities.js

@@ -108,7 +108,7 @@ if (Meteor.isServer) {
     let participants = [];
     let participants = [];
     let watchers = [];
     let watchers = [];
     let title = 'act-activity-notify';
     let title = 'act-activity-notify';
-    let board = null;
+    const board = Boards.findOne(activity.boardId);
     const description = `act-${activity.activityType}`;
     const description = `act-${activity.activityType}`;
     const params = {
     const params = {
       activityId: activity._id,
       activityId: activity._id,
@@ -122,8 +122,11 @@ if (Meteor.isServer) {
       params.userId = activity.userId;
       params.userId = activity.userId;
     }
     }
     if (activity.boardId) {
     if (activity.boardId) {
-      board = activity.board();
-      params.board = board.title;
+      if (board.title.length > 0) {
+        params.board = board.title;
+      } else {
+        params.board = '';
+      }
       title = 'act-withBoardTitle';
       title = 'act-withBoardTitle';
       params.url = board.absoluteUrl();
       params.url = board.absoluteUrl();
       params.boardId = activity.boardId;
       params.boardId = activity.boardId;

+ 89 - 80
models/users.js

@@ -620,44 +620,6 @@ Users.mutations({
 });
 });
 
 
 Meteor.methods({
 Meteor.methods({
-  setCreateUser(fullname, username, password, isAdmin, isActive, email) {
-    if (Meteor.user().isAdmin) {
-      check(fullname, String);
-      check(username, String);
-      check(password, String);
-      check(isAdmin, String);
-      check(isActive, String);
-      check(email, String);
-
-      const nUsersWithUsername = Users.find({ username }).count();
-      const nUsersWithEmail = Users.find({ email }).count();
-      if (nUsersWithUsername > 0) {
-        throw new Meteor.Error('username-already-taken');
-      } else if (nUsersWithEmail > 0) {
-        throw new Meteor.Error('email-already-taken');
-      } else {
-        Accounts.createUser({
-          fullname,
-          username,
-          password,
-          isAdmin,
-          isActive,
-          email: email.toLowerCase(),
-          from: 'admin',
-        });
-      }
-    }
-  },
-  setUsername(username, userId) {
-    check(username, String);
-    check(userId, String);
-    const nUsersWithUsername = Users.find({ username }).count();
-    if (nUsersWithUsername > 0) {
-      throw new Meteor.Error('username-already-taken');
-    } else {
-      Users.update(userId, { $set: { username } });
-    }
-  },
   setListSortBy(value) {
   setListSortBy(value) {
     check(value, String);
     check(value, String);
     Meteor.user().setListSortBy(value);
     Meteor.user().setListSortBy(value);
@@ -678,51 +640,97 @@ Meteor.methods({
     check(limit, Number);
     check(limit, Number);
     Meteor.user().setShowCardsCountAt(limit);
     Meteor.user().setShowCardsCountAt(limit);
   },
   },
-  setEmail(email, userId) {
-    if (Array.isArray(email)) {
-      email = email.shift();
-    }
-    check(email, String);
-    const existingUser = Users.findOne(
-      { 'emails.address': email },
-      { fields: { _id: 1 } },
-    );
-    if (existingUser) {
-      throw new Meteor.Error('email-already-taken');
-    } else {
-      Users.update(userId, {
-        $set: {
-          emails: [
-            {
-              address: email,
-              verified: false,
-            },
-          ],
-        },
-      });
-    }
-  },
-  setUsernameAndEmail(username, email, userId) {
-    check(username, String);
-    if (Array.isArray(email)) {
-      email = email.shift();
-    }
-    check(email, String);
-    check(userId, String);
-    Meteor.call('setUsername', username, userId);
-    Meteor.call('setEmail', email, userId);
-  },
-  setPassword(newPassword, userId) {
-    check(userId, String);
-    check(newPassword, String);
-    if (Meteor.user().isAdmin) {
-      Accounts.setPassword(userId, newPassword);
-    }
-  },
 });
 });
 
 
 if (Meteor.isServer) {
 if (Meteor.isServer) {
   Meteor.methods({
   Meteor.methods({
+    setCreateUser(fullname, username, password, isAdmin, isActive, email) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(fullname, String);
+        check(username, String);
+        check(password, String);
+        check(isAdmin, String);
+        check(isActive, String);
+        check(email, String);
+
+        const nUsersWithUsername = Users.find({ username }).count();
+        const nUsersWithEmail = Users.find({ email }).count();
+        if (nUsersWithUsername > 0) {
+          throw new Meteor.Error('username-already-taken');
+        } else if (nUsersWithEmail > 0) {
+          throw new Meteor.Error('email-already-taken');
+        } else {
+          Accounts.createUser({
+            fullname,
+            username,
+            password,
+            isAdmin,
+            isActive,
+            email: email.toLowerCase(),
+            from: 'admin',
+          });
+        }
+      }
+    },
+    setUsername(username, userId) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(username, String);
+        check(userId, String);
+        const nUsersWithUsername = Users.find({ username }).count();
+        if (nUsersWithUsername > 0) {
+          throw new Meteor.Error('username-already-taken');
+        } else {
+          Users.update(userId, { $set: { username } });
+        }
+      }
+    },
+    setEmail(email, userId) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        if (Array.isArray(email)) {
+          email = email.shift();
+        }
+        check(email, String);
+        const existingUser = Users.findOne(
+          { 'emails.address': email },
+          { fields: { _id: 1 } },
+        );
+        if (existingUser) {
+          throw new Meteor.Error('email-already-taken');
+        } else {
+          Users.update(userId, {
+            $set: {
+              emails: [
+                {
+                  address: email,
+                  verified: false,
+                },
+              ],
+            },
+          });
+        }
+      }
+    },
+    setUsernameAndEmail(username, email, userId) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(username, String);
+        if (Array.isArray(email)) {
+          email = email.shift();
+        }
+        check(email, String);
+        check(userId, String);
+        Meteor.call('setUsername', username, userId);
+        Meteor.call('setEmail', email, userId);
+      }
+    },
+    setPassword(newPassword, userId) {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        check(userId, String);
+        check(newPassword, String);
+        if (Meteor.user().isAdmin) {
+          Accounts.setPassword(userId, newPassword);
+        }
+      }
+    },
     // we accept userId, username, email
     // we accept userId, username, email
     inviteUserToBoard(username, boardId) {
     inviteUserToBoard(username, boardId) {
       check(username, String);
       check(username, String);
@@ -754,8 +762,9 @@ if (Meteor.isServer) {
           throw new Meteor.Error('error-user-notAllowSelf');
           throw new Meteor.Error('error-user-notAllowSelf');
       } else {
       } else {
         if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
         if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
-        if (Settings.findOne().disableRegistration)
+        if (Settings.findOne({ disableRegistration: true })) {
           throw new Meteor.Error('error-user-notCreated');
           throw new Meteor.Error('error-user-notCreated');
+        }
         // Set in lowercase email before creating account
         // Set in lowercase email before creating account
         const email = username.toLowerCase();
         const email = username.toLowerCase();
         username = email.substring(0, posAt);
         username = email.substring(0, posAt);

+ 184 - 177
server/notifications/outgoing.js

@@ -1,192 +1,199 @@
-const postCatchError = Meteor.wrapAsync((url, options, resolve) => {
-  HTTP.post(url, options, (err, res) => {
-    if (err) {
-      resolve(null, err.response);
-    } else {
-      resolve(null, res);
-    }
+if (Meteor.isServer) {
+  const postCatchError = Meteor.wrapAsync((url, options, resolve) => {
+    HTTP.post(url, options, (err, res) => {
+      if (err) {
+        resolve(null, err.response);
+      } else {
+        resolve(null, res);
+      }
+    });
   });
   });
-});
 
 
-const Lock = {
-  _lock: {},
-  _timer: {},
-  echoDelay: 500, // echo should be happening much faster
-  normalDelay: 1e3, // normally user typed comment will be much slower
-  ECHO: 2,
-  NORMAL: 1,
-  NULL: 0,
-  has(id, value) {
-    const existing = this._lock[id];
-    let ret = this.NULL;
-    if (existing) {
-      ret = existing === value ? this.ECHO : this.NORMAL;
-    }
-    return ret;
-  },
-  clear(id, delay) {
-    const previous = this._timer[id];
-    if (previous) {
-      Meteor.clearTimeout(previous);
-    }
-    this._timer[id] = Meteor.setTimeout(() => this.unset(id), delay);
-  },
-  set(id, value) {
-    const state = this.has(id, value);
-    let delay = this.normalDelay;
-    if (state === this.ECHO) {
-      delay = this.echoDelay;
-    }
-    if (!value) {
-      // user commented, we set a lock
-      value = 1;
-    }
-    this._lock[id] = value;
-    this.clear(id, delay); // always auto reset the locker after delay
-  },
-  unset(id) {
-    delete this._lock[id];
-  },
-};
+  const Lock = {
+    _lock: {},
+    _timer: {},
+    echoDelay: 500, // echo should be happening much faster
+    normalDelay: 1e3, // normally user typed comment will be much slower
+    ECHO: 2,
+    NORMAL: 1,
+    NULL: 0,
+    has(id, value) {
+      const existing = this._lock[id];
+      let ret = this.NULL;
+      if (existing) {
+        ret = existing === value ? this.ECHO : this.NORMAL;
+      }
+      return ret;
+    },
+    clear(id, delay) {
+      const previous = this._timer[id];
+      if (previous) {
+        Meteor.clearTimeout(previous);
+      }
+      this._timer[id] = Meteor.setTimeout(() => this.unset(id), delay);
+    },
+    set(id, value) {
+      const state = this.has(id, value);
+      let delay = this.normalDelay;
+      if (state === this.ECHO) {
+        delay = this.echoDelay;
+      }
+      if (!value) {
+        // user commented, we set a lock
+        value = 1;
+      }
+      this._lock[id] = value;
+      this.clear(id, delay); // always auto reset the locker after delay
+    },
+    unset(id) {
+      delete this._lock[id];
+    },
+  };
 
 
-const webhooksAtbts = (process.env.WEBHOOKS_ATTRIBUTES &&
-  process.env.WEBHOOKS_ATTRIBUTES.split(',')) || [
-  'cardId',
-  'listId',
-  'oldListId',
-  'boardId',
-  'comment',
-  'user',
-  'card',
-  'commentId',
-  'swimlaneId',
-];
-const responseFunc = data => {
-  const paramCommentId = data.commentId;
-  const paramCardId = data.cardId;
-  const paramBoardId = data.boardId;
-  const newComment = data.comment;
-  if (paramCardId && paramBoardId && newComment) {
-    // only process data with the cardid, boardid and comment text, TODO can expand other functions here to react on returned data
-    const comment = CardComments.findOne({
-      _id: paramCommentId,
-      cardId: paramCardId,
-      boardId: paramBoardId,
-    });
-    const board = Boards.findOne(paramBoardId);
-    const card = Cards.findOne(paramCardId);
-    if (board && card) {
-      if (comment) {
-        Lock.set(comment._id, newComment);
-        CardComments.direct.update(comment._id, {
-          $set: {
+  const webhooksAtbts = (process.env.WEBHOOKS_ATTRIBUTES &&
+    process.env.WEBHOOKS_ATTRIBUTES.split(',')) || [
+    'cardId',
+    'listId',
+    'oldListId',
+    'boardId',
+    'comment',
+    'user',
+    'card',
+    'commentId',
+    'swimlaneId',
+  ];
+  const responseFunc = data => {
+    const paramCommentId = data.commentId;
+    const paramCardId = data.cardId;
+    const paramBoardId = data.boardId;
+    const newComment = data.comment;
+    if (paramCardId && paramBoardId && newComment) {
+      // only process data with the cardid, boardid and comment text, TODO can expand other functions here to react on returned data
+      const comment = CardComments.findOne({
+        _id: paramCommentId,
+        cardId: paramCardId,
+        boardId: paramBoardId,
+      });
+      const board = Boards.findOne(paramBoardId);
+      const card = Cards.findOne(paramCardId);
+      if (board && card) {
+        if (comment) {
+          Lock.set(comment._id, newComment);
+          CardComments.direct.update(comment._id, {
+            $set: {
+              text: newComment,
+            },
+          });
+        }
+      } else {
+        const userId = data.userId;
+        if (userId) {
+          const inserted = CardComments.direct.insert({
             text: newComment,
             text: newComment,
-          },
-        });
-      }
-    } else {
-      const userId = data.userId;
-      if (userId) {
-        const inserted = CardComments.direct.insert({
-          text: newComment,
-          userId,
-          cardId,
-          boardId,
-        });
-        Lock.set(inserted._id, newComment);
+            userId,
+            cardId,
+            boardId,
+          });
+          Lock.set(inserted._id, newComment);
+        }
       }
       }
     }
     }
-  }
-};
-Meteor.methods({
-  outgoingWebhooks(integration, description, params) {
-    check(integration, Object);
-    check(description, String);
-    check(params, Object);
-    this.unblock();
+  };
+  Meteor.methods({
+    outgoingWebhooks(integration, description, params) {
+      if (Meteor.user()) {
+        check(integration, Object);
+        check(description, String);
+        check(params, Object);
+        this.unblock();
 
 
-    // label activity did not work yet, see wekan/models/activities.js
-    const quoteParams = _.clone(params);
-    const clonedParams = _.clone(params);
-    [
-      'card',
-      'list',
-      'oldList',
-      'board',
-      'oldBoard',
-      'comment',
-      'checklist',
-      'swimlane',
-      'oldSwimlane',
-      'label',
-      'attachment',
-    ].forEach(key => {
-      if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`;
-    });
+        // label activity did not work yet, see wekan/models/activities.js
+        const quoteParams = _.clone(params);
+        const clonedParams = _.clone(params);
+        [
+          'card',
+          'list',
+          'oldList',
+          'board',
+          'oldBoard',
+          'comment',
+          'checklist',
+          'swimlane',
+          'oldSwimlane',
+          'label',
+          'attachment',
+        ].forEach(key => {
+          if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`;
+        });
 
 
-    const userId = params.userId ? params.userId : integrations[0].userId;
-    const user = Users.findOne(userId);
-    const text = `${params.user} ${TAPi18n.__(
-      description,
-      quoteParams,
-      user.getLanguage(),
-    )}\n${params.url}`;
+        const userId = params.userId ? params.userId : integrations[0].userId;
+        const user = Users.findOne(userId);
+        const text = `${params.user} ${TAPi18n.__(
+          description,
+          quoteParams,
+          user.getLanguage(),
+        )}\n${params.url}`;
 
 
-    if (text.length === 0) return;
+        if (text.length === 0) return;
 
 
-    const value = {
-      text: `${text}`,
-    };
+        const value = {
+          text: `${text}`,
+        };
 
 
-    webhooksAtbts.forEach(key => {
-      if (params[key]) value[key] = params[key];
-    });
-    value.description = description;
-    //integrations.forEach(integration => {
-    const is2way = integration.type === Integrations.Const.TWOWAY;
-    const token = integration.token || '';
-    const headers = {
-      'Content-Type': 'application/json',
-    };
-    if (token) headers['X-Wekan-Token'] = token;
-    const options = {
-      headers,
-      data: is2way ? { description, ...clonedParams } : value,
-    };
-    const url = integration.url;
-    if (is2way) {
-      const cid = params.commentId;
-      const comment = params.comment;
-      const lockState = cid && Lock.has(cid, comment);
-      if (cid && lockState !== Lock.NULL) {
-        // it's a comment  and there is a previous lock
-        return;
-      } else if (cid) {
-        Lock.set(cid, comment); // set a lock here
-      }
-    }
-    const response = postCatchError(url, options);
+        webhooksAtbts.forEach(key => {
+          if (params[key]) value[key] = params[key];
+        });
+        value.description = description;
+        //integrations.forEach(integration => {
+        const is2way = integration.type === Integrations.Const.TWOWAY;
+        const token = integration.token || '';
+        const headers = {
+          'Content-Type': 'application/json',
+        };
+        if (token) headers['X-Wekan-Token'] = token;
+        const options = {
+          headers,
+          data: is2way ? { description, ...clonedParams } : value,
+        };
 
 
-    if (
-      response &&
-      response.statusCode &&
-      response.statusCode >= 200 &&
-      response.statusCode < 300
-    ) {
-      if (is2way) {
-        const data = response.data; // only an JSON encoded response will be actioned
-        if (data) {
-          try {
-            responseFunc(data);
-          } catch (e) {
-            throw new Meteor.Error('error-process-data');
+        if (!Integrations.findOne({ url: integration.url })) return;
+
+        const url = integration.url;
+
+        if (is2way) {
+          const cid = params.commentId;
+          const comment = params.comment;
+          const lockState = cid && Lock.has(cid, comment);
+          if (cid && lockState !== Lock.NULL) {
+            // it's a comment  and there is a previous lock
+            return;
+          } else if (cid) {
+            Lock.set(cid, comment); // set a lock here
           }
           }
         }
         }
+        const response = postCatchError(url, options);
+
+        if (
+          response &&
+          response.statusCode &&
+          response.statusCode >= 200 &&
+          response.statusCode < 300
+        ) {
+          if (is2way) {
+            const data = response.data; // only an JSON encoded response will be actioned
+            if (data) {
+              try {
+                responseFunc(data);
+              } catch (e) {
+                throw new Meteor.Error('error-process-data');
+              }
+            }
+          }
+          return response; // eslint-disable-line consistent-return
+        } else {
+          throw new Meteor.Error('error-invalid-webhook-response');
+        }
       }
       }
-      return response; // eslint-disable-line consistent-return
-    } else {
-      throw new Meteor.Error('error-invalid-webhook-response');
-    }
-    //});
-  },
-});
+    },
+  });
+}

+ 73 - 65
server/statistics.js

@@ -1,68 +1,76 @@
 import { MongoInternals } from 'meteor/mongo';
 import { MongoInternals } from 'meteor/mongo';
 
 
-Meteor.methods({
-  getStatistics() {
-    const os = require('os');
-    const pjson = require('/package.json');
-    const statistics = {};
-    let wekanVersion = pjson.version;
-    wekanVersion = wekanVersion.replace('v', '');
-    statistics.version = wekanVersion;
-    statistics.os = {
-      type: os.type(),
-      platform: os.platform(),
-      arch: os.arch(),
-      release: os.release(),
-      uptime: os.uptime(),
-      loadavg: os.loadavg(),
-      totalmem: os.totalmem(),
-      freemem: os.freemem(),
-      cpus: os.cpus(),
-    };
-    let nodeVersion = process.version;
-    nodeVersion = nodeVersion.replace('v', '');
-    statistics.process = {
-      nodeVersion,
-      pid: process.pid,
-      uptime: process.uptime(),
-    };
-    // Remove beginning of Meteor release text METEOR@
-    let meteorVersion = Meteor.release;
-    meteorVersion = meteorVersion.replace('METEOR@', '');
-    statistics.meteor = {
-      meteorVersion,
-    };
-    // Thanks to RocketChat for MongoDB version detection !
-    // https://github.com/RocketChat/Rocket.Chat/blob/develop/app/utils/server/functions/getMongoInfo.js
-    let mongoVersion;
-    let mongoStorageEngine;
-    let mongoOplogEnabled;
-    try {
-      const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
-      oplogEnabled = Boolean(
-        mongo._oplogHandle && mongo._oplogHandle.onOplogEntry,
-      );
-      const { version, storageEngine } = Promise.await(
-        mongo.db.command({ serverStatus: 1 }),
-      );
-      mongoVersion = version;
-      mongoStorageEngine = storageEngine.name;
-      mongoOplogEnabled = oplogEnabled;
-    } catch (e) {
-      try {
-        const { version } = Promise.await(mongo.db.command({ buildinfo: 1 }));
-        mongoVersion = version;
-        mongoStorageEngine = 'unknown';
-      } catch (e) {
-        mongoVersion = 'unknown';
-        mongoStorageEngine = 'unknown';
+if (Meteor.isServer) {
+  Meteor.methods({
+    getStatistics() {
+      if (Meteor.user() && Meteor.user().isAdmin) {
+        const os = require('os');
+        const pjson = require('/package.json');
+        const statistics = {};
+        let wekanVersion = pjson.version;
+        wekanVersion = wekanVersion.replace('v', '');
+        statistics.version = wekanVersion;
+        statistics.os = {
+          type: os.type(),
+          platform: os.platform(),
+          arch: os.arch(),
+          release: os.release(),
+          uptime: os.uptime(),
+          loadavg: os.loadavg(),
+          totalmem: os.totalmem(),
+          freemem: os.freemem(),
+          cpus: os.cpus(),
+        };
+        let nodeVersion = process.version;
+        nodeVersion = nodeVersion.replace('v', '');
+        statistics.process = {
+          nodeVersion,
+          pid: process.pid,
+          uptime: process.uptime(),
+        };
+        // Remove beginning of Meteor release text METEOR@
+        let meteorVersion = Meteor.release;
+        meteorVersion = meteorVersion.replace('METEOR@', '');
+        statistics.meteor = {
+          meteorVersion,
+        };
+        // Thanks to RocketChat for MongoDB version detection !
+        // https://github.com/RocketChat/Rocket.Chat/blob/develop/app/utils/server/functions/getMongoInfo.js
+        let mongoVersion;
+        let mongoStorageEngine;
+        let mongoOplogEnabled;
+        try {
+          const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
+          oplogEnabled = Boolean(
+            mongo._oplogHandle && mongo._oplogHandle.onOplogEntry,
+          );
+          const { version, storageEngine } = Promise.await(
+            mongo.db.command({ serverStatus: 1 }),
+          );
+          mongoVersion = version;
+          mongoStorageEngine = storageEngine.name;
+          mongoOplogEnabled = oplogEnabled;
+        } catch (e) {
+          try {
+            const { version } = Promise.await(
+              mongo.db.command({ buildinfo: 1 }),
+            );
+            mongoVersion = version;
+            mongoStorageEngine = 'unknown';
+          } catch (e) {
+            mongoVersion = 'unknown';
+            mongoStorageEngine = 'unknown';
+          }
+        }
+        statistics.mongo = {
+          mongoVersion,
+          mongoStorageEngine,
+          mongoOplogEnabled,
+        };
+        return statistics;
+      } else {
+        return false;
       }
       }
-    }
-    statistics.mongo = {
-      mongoVersion,
-      mongoStorageEngine,
-      mongoOplogEnabled,
-    };
-    return statistics;
-  },
-});
+    },
+  });
+}