diff --git a/.scripts/start.js b/.scripts/start.js index 3f08b5608276..8c1854d91802 100644 --- a/.scripts/start.js +++ b/.scripts/start.js @@ -3,22 +3,45 @@ const path = require('path'); const fs = require('fs'); const extend = require('util')._extend; -const { exec } = require('child_process'); +const { spawn } = require('child_process'); +const net = require('net'); const processes = []; +let exitCode; const baseDir = path.resolve(__dirname, '..'); const srcDir = path.resolve(baseDir); +const isPortTaken = (port) => new Promise((resolve, reject) => { + const tester = net.createServer() + .once('error', (err) => (err.code === 'EADDRINUSE' ? resolve(true) : reject(err))) + .once('listening', () => tester.once('close', () => resolve(false)).close()) + .listen(port); +}); + +const waitPortRelease = (port) => new Promise((resolve, reject) => { + isPortTaken(port).then((taken) => { + if (!taken) { + return resolve(); + } + setTimeout(() => { + waitPortRelease(port).then(resolve).catch(reject); + }, 1000); + }); +}); + const appOptions = { env: { PORT: 3000, ROOT_URL: 'http://localhost:3000', + // MONGO_URL: 'mongodb://localhost:27017/test', + // MONGO_OPLOG_URL: 'mongodb://localhost:27017/local', }, }; function startProcess(opts, callback) { - const proc = exec( + const proc = spawn( opts.command, + opts.params, opts.options ); @@ -43,12 +66,28 @@ function startProcess(opts, callback) { proc.stderr.pipe(logStream); } - proc.on('close', function(code) { - console.log(opts.name, `exited with code ${ code }`); - for (let i = 0; i < processes.length; i += 1) { - processes[i].kill(); + proc.on('exit', function(code, signal) { + if (code != null) { + exitCode = code; + console.log(opts.name, `exited with code ${ code }`); + } else { + console.log(opts.name, `exited with signal ${ signal }`); + } + + processes.splice(processes.indexOf(proc), 1); + + processes.forEach((p) => p.kill()); + + if (processes.length === 0) { + waitPortRelease(appOptions.env.PORT).then(() => { + console.log(`Port ${ appOptions.env.PORT } was released, exiting with code ${ exitCode }`); + process.exit(exitCode); + }).catch((error) => { + console.error(`Error waiting port ${ appOptions.env.PORT } to be released, exiting with code ${ exitCode }`); + console.error(error); + process.exit(exitCode); + }); } - process.exit(code); }); processes.push(proc); } @@ -56,7 +95,10 @@ function startProcess(opts, callback) { function startApp(callback) { startProcess({ name: 'Meteor App', - command: 'node /tmp/build-test/bundle/main.js', + command: 'node', + params: ['/tmp/build-test/bundle/main.js'], + // command: 'node', + // params: ['.meteor/local/build/main.js'], waitForMessage: appOptions.waitForMessage, options: { cwd: srcDir, @@ -68,7 +110,10 @@ function startApp(callback) { function startChimp() { startProcess({ name: 'Chimp', - command: 'npm run chimp-test', + command: 'npm', + params: ['run', 'chimp-test'], + // command: 'exit', + // params: ['2'], options: { env: Object.assign({}, process.env, { NODE_PATH: `${ process.env.NODE_PATH + diff --git a/README.md b/README.md index 6e96e657e2f6..a48aaf79ad04 100644 --- a/README.md +++ b/README.md @@ -464,7 +464,7 @@ Testing with [BrowserStack](https://www.browserstack.com) Rocket.Chat will be free forever, but you can help us speed up the development! -[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ZL94ZE6LGVUSN) +[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=9MT88JJ9X4A6U&source=url) [BountySource](https://www.bountysource.com/teams/rocketchat) diff --git a/app/action-links/client/init.js b/app/action-links/client/init.js index 5d687707a082..4f85d99d9d53 100644 --- a/app/action-links/client/init.js +++ b/app/action-links/client/init.js @@ -2,25 +2,28 @@ import { Blaze } from 'meteor/blaze'; import { Template } from 'meteor/templating'; import { handleError } from '../../utils'; import { fireGlobalEvent, Layout } from '../../ui-utils'; +import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; import { actionLinks } from '../both/lib/actionLinks'; + + Template.room.events({ 'click .action-link'(event, instance) { event.preventDefault(); event.stopPropagation(); const data = Blaze.getData(event.currentTarget); - + const { msg } = messageArgs(data); if (Layout.isEmbedded()) { return fireGlobalEvent('click-action-link', { actionlink: $(event.currentTarget).data('actionlink'), - value: data._arguments[1]._id, - message: data._arguments[1], + value: msg._id, + message: msg, }); } - if (data && data._arguments && data._arguments[1] && data._arguments[1]._id) { - actionLinks.run($(event.currentTarget).data('actionlink'), data._arguments[1]._id, instance, (err) => { + if (msg._id) { + actionLinks.run($(event.currentTarget).data('actionlink'), msg._id, instance, (err) => { if (err) { handleError(err); } diff --git a/app/api/server/helpers/deprecationWarning.js b/app/api/server/helpers/deprecationWarning.js index 06dc47c3f096..52590c041f02 100644 --- a/app/api/server/helpers/deprecationWarning.js +++ b/app/api/server/helpers/deprecationWarning.js @@ -1,7 +1,7 @@ import { API } from '../api'; -API.helperMethods.set('deprecationWarning', function _deprecationWarning({ endpoint, versionWillBeRemove, response }) { - const warningMessage = `The endpoint "${ endpoint }" is deprecated and will be removed after version ${ versionWillBeRemove }`; +API.helperMethods.set('deprecationWarning', function _deprecationWarning({ endpoint, versionWillBeRemoved, response }) { + const warningMessage = `The endpoint "${ endpoint }" is deprecated and will be removed after version ${ versionWillBeRemoved }`; console.warn(warningMessage); if (process.env.NODE_ENV === 'development') { return { diff --git a/app/api/server/v1/emoji-custom.js b/app/api/server/v1/emoji-custom.js index f29c410a094a..79e54e62e479 100644 --- a/app/api/server/v1/emoji-custom.js +++ b/app/api/server/v1/emoji-custom.js @@ -3,12 +3,50 @@ import { EmojiCustom } from '../../../models'; import { API } from '../api'; import Busboy from 'busboy'; +// DEPRECATED +// Will be removed after v1.12.0 API.v1.addRoute('emoji-custom', { authRequired: true }, { get() { + const warningMessage = 'The endpoint "emoji-custom" is deprecated and will be removed after version v1.12.0'; + console.warn(warningMessage); const { query } = this.parseJsonQuery(); const emojis = Meteor.call('listEmojiCustom', query); - return API.v1.success({ emojis }); + return API.v1.success(this.deprecationWarning({ + endpoint: 'emoji-custom', + versionWillBeRemoved: '1.12.0', + response: { + emojis, + }, + })); + }, +}); + +API.v1.addRoute('emoji-custom.list', { authRequired: true }, { + get() { + const { query } = this.parseJsonQuery(); + const { updatedSince } = this.queryParams; + let updatedSinceDate; + if (updatedSince) { + if (isNaN(Date.parse(updatedSince))) { + throw new Meteor.Error('error-roomId-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); + } else { + updatedSinceDate = new Date(updatedSince); + } + return API.v1.success({ + emojis: { + update: EmojiCustom.find({ ...query, _updatedAt: { $gt: updatedSinceDate } }).fetch(), + remove: EmojiCustom.trashFindDeletedAfter(updatedSinceDate).fetch(), + }, + }); + } + + return API.v1.success({ + emojis: { + update: EmojiCustom.find(query).fetch(), + remove: [], + }, + }); }, }); diff --git a/app/api/server/v1/groups.js b/app/api/server/v1/groups.js index 5c52a8fdc8fa..41a93d3fd9a8 100644 --- a/app/api/server/v1/groups.js +++ b/app/api/server/v1/groups.js @@ -1,9 +1,12 @@ +import _ from 'underscore'; + import { Meteor } from 'meteor/meteor'; -import { Subscriptions, Rooms, Messages, Uploads, Integrations, Users } from '../../../models'; -import { hasPermission } from '../../../authorization'; -import { composeMessageObjectWithUser } from '../../../utils'; + +import { Subscriptions, Rooms, Messages, Uploads, Integrations, Users } from '../../../models/server'; +import { hasPermission, canAccessRoom } from '../../../authorization/server'; +import { composeMessageObjectWithUser } from '../../../utils/server'; + import { API } from '../api'; -import _ from 'underscore'; // Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property function findPrivateGroupByIdOrName({ params, userId, checkedArchived = true }) { @@ -11,22 +14,46 @@ function findPrivateGroupByIdOrName({ params, userId, checkedArchived = true }) throw new Meteor.Error('error-room-param-not-provided', 'The parameter "roomId" or "roomName" is required'); } - let roomSub; - if (params.roomId) { - roomSub = Subscriptions.findOneByRoomIdAndUserId(params.roomId, userId); - } else if (params.roomName) { - roomSub = Subscriptions.findOneByRoomNameAndUserId(params.roomName, userId); + const roomOptions = { + fields: { + t: 1, + ro: 1, + name: 1, + fname: 1, + prid: 1, + archived: 1, + }, + }; + const room = params.roomId ? + Rooms.findOneById(params.roomId, roomOptions) : + Rooms.findOneByName(params.roomName, roomOptions); + + if (!room || room.t !== 'p') { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); } - if (!roomSub || roomSub.t !== 'p') { + const user = Users.findOneById(userId, { fields: { username: 1 } }); + + if (!canAccessRoom(room, user)) { throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); } - if (checkedArchived && roomSub.archived) { - throw new Meteor.Error('error-room-archived', `The private group, ${ roomSub.name }, is archived`); + // discussions have their names saved on `fname` property + const roomName = room.prid ? room.fname : room.name; + + if (checkedArchived && room.archived) { + throw new Meteor.Error('error-room-archived', `The private group, ${ roomName }, is archived`); } - return roomSub; + const sub = Subscriptions.findOneByRoomIdAndUserId(room._id, userId, { fields: { open: 1 } }); + + return { + rid: room._id, + open: sub && sub.open, + ro: room.ro, + t: room.t, + name: roomName, + }; } API.v1.addRoute('groups.addAll', { authRequired: true }, { diff --git a/app/api/server/v1/misc.js b/app/api/server/v1/misc.js index 19d5c4aae23b..8f792eeb6c84 100644 --- a/app/api/server/v1/misc.js +++ b/app/api/server/v1/misc.js @@ -7,21 +7,35 @@ import { Users } from '../../../models'; import { settings } from '../../../settings'; import { API } from '../api'; +import s from 'underscore.string'; + +// DEPRECATED +// Will be removed after v1.12.0 API.v1.addRoute('info', { authRequired: false }, { get() { + const warningMessage = 'The endpoint "/v1/info" is deprecated and will be removed after version v1.12.0'; + console.warn(warningMessage); const user = this.getLoggedInUser(); if (user && hasRole(user._id, 'admin')) { - return API.v1.success({ - info: Info, - }); + return API.v1.success(this.deprecationWarning({ + endpoint: 'info', + versionWillBeRemoved: '1.12.0', + response: { + info: Info, + }, + })); } - return API.v1.success({ - info: { - version: Info.version, + return API.v1.success(this.deprecationWarning({ + endpoint: 'info', + versionWillBeRemoved: '1.12.0', + response: { + info: { + version: Info.version, + }, }, - }); + })); }, }); @@ -36,7 +50,8 @@ let onlineCacheDate = 0; const cacheInvalid = 60000; // 1 minute API.v1.addRoute('shield.svg', { authRequired: false }, { get() { - const { type, channel, name, icon } = this.queryParams; + const { type, icon } = this.queryParams; + let { channel, name } = this.queryParams; if (!settings.get('API_Enable_Shields')) { throw new Meteor.Error('error-endpoint-disabled', 'This endpoint is disabled', { route: '/api/v1/shield.svg' }); } @@ -102,29 +117,34 @@ API.v1.addRoute('shield.svg', { authRequired: false }, { const rightSize = text.length * 6 + 20; const width = leftSize + rightSize; const height = 20; + + channel = s.escapeHTML(channel); + text = s.escapeHTML(text); + name = s.escapeHTML(name); + return { headers: { 'Content-Type': 'image/svg+xml;charset=utf-8' }, body: ` - - - - - - - - - - - - - ${ hideIcon ? '' : '' } - + + + + + + + + + + + + + ${ hideIcon ? '' : '' } + ${ name ? `${ name } - ${ name }` : '' } - ${ text } - ${ text } - + ${ name }` : '' } + ${ text } + ${ text } + `.trim().replace(/\>[\s]+\<'), }; diff --git a/app/api/server/v1/permissions.js b/app/api/server/v1/permissions.js index e8925ea900a3..e0d0d55f3f58 100644 --- a/app/api/server/v1/permissions.js +++ b/app/api/server/v1/permissions.js @@ -30,7 +30,7 @@ API.v1.addRoute('permissions.list', { authRequired: true }, { return API.v1.success(this.deprecationWarning({ endpoint: 'permissions.list', - versionWillBeRemove: '0.85', + versionWillBeRemoved: '0.85', response: { permissions: result, }, diff --git a/app/api/server/v1/rooms.js b/app/api/server/v1/rooms.js index 0e5d98f9c6c8..0191f6bd9e19 100644 --- a/app/api/server/v1/rooms.js +++ b/app/api/server/v1/rooms.js @@ -218,3 +218,54 @@ API.v1.addRoute('rooms.leave', { authRequired: true }, { return API.v1.success(); }, }); + +API.v1.addRoute('rooms.createDiscussion', { authRequired: true }, { + post() { + const { prid, pmid, reply, t_name, users } = this.bodyParams; + if (!prid) { + return API.v1.failure('Body parameter "prid" is required.'); + } + if (!t_name) { + return API.v1.failure('Body parameter "t_name" is required.'); + } + if (users && !Array.isArray(users)) { + return API.v1.failure('Body parameter "users" must be an array.'); + } + + const discussion = Meteor.runAsUser(this.userId, () => Meteor.call('createDiscussion', { + prid, + pmid, + t_name, + reply, + users: users || [], + })); + + return API.v1.success({ discussion }); + }, +}); + +API.v1.addRoute('rooms.getDiscussions', { authRequired: true }, { + get() { + const room = findRoomByIdOrName({ params: this.requestParams() }); + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + if (!Meteor.call('canAccessRoom', room._id, this.userId, {})) { + return API.v1.failure('not-allowed', 'Not Allowed'); + } + const ourQuery = Object.assign(query, { prid: room._id }); + + const discussions = Rooms.find(ourQuery, { + sort: sort ? sort : { fname: 1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + discussions, + count: discussions.length, + offset, + total: Rooms.find(ourQuery).count(), + }); + }, +}); diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js index dd0d0f72a3df..df07b51930c0 100644 --- a/app/api/server/v1/users.js +++ b/app/api/server/v1/users.js @@ -265,7 +265,7 @@ API.v1.addRoute('users.setAvatar', { authRequired: true }, { let user; if (this.isUserFromParams()) { user = Meteor.users.findOne(this.userId); - } else if (hasPermission(this.userId, 'edit-other-user-info')) { + } else if (hasPermission(this.userId, 'edit-other-user-avatar')) { user = this.getUserFromParams(); } else { return API.v1.unauthorized(); @@ -403,7 +403,7 @@ API.v1.addRoute('users.getPreferences', { authRequired: true }, { get() { const user = Users.findOneById(this.userId); if (user.settings) { - const { preferences } = user.settings; + const { preferences = {} } = user.settings; preferences.language = user.language; return API.v1.success({ diff --git a/app/apps/client/admin/modalTemplates/iframeModal.html b/app/apps/client/admin/modalTemplates/iframeModal.html index aecd160b0099..6902d2cd0db6 100644 --- a/app/apps/client/admin/modalTemplates/iframeModal.html +++ b/app/apps/client/admin/modalTemplates/iframeModal.html @@ -1,5 +1,5 @@