diff --git a/.meteor/packages b/.meteor/packages index 5e9e6cc59c89..9fcf0f66925f 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -14,6 +14,7 @@ accounts-twitter@1.4.0 blaze-html-templates check@1.2.5 ddp-rate-limiter@1.0.7 +ddp-common dynamic-import@0.2.0 ecmascript@0.9.0 ejson@1.1.0 diff --git a/packages/rocketchat-api/package.js b/packages/rocketchat-api/package.js index 422067dd31df..a59b59125b45 100644 --- a/packages/rocketchat-api/package.js +++ b/packages/rocketchat-api/package.js @@ -32,12 +32,15 @@ Package.onUse(function(api) { //Add v1 routes api.addFiles('server/v1/channels.js', 'server'); + api.addFiles('server/v1/rooms.js', 'server'); + api.addFiles('server/v1/subscriptions.js', 'server'); api.addFiles('server/v1/chat.js', 'server'); api.addFiles('server/v1/commands.js', 'server'); api.addFiles('server/v1/groups.js', 'server'); api.addFiles('server/v1/im.js', 'server'); api.addFiles('server/v1/integrations.js', 'server'); api.addFiles('server/v1/misc.js', 'server'); + api.addFiles('server/v1/push.js', 'server'); api.addFiles('server/v1/settings.js', 'server'); api.addFiles('server/v1/stats.js', 'server'); api.addFiles('server/v1/users.js', 'server'); diff --git a/packages/rocketchat-api/server/api.js b/packages/rocketchat-api/server/api.js index 0f8f04c4a851..86955deda3b1 100644 --- a/packages/rocketchat-api/server/api.js +++ b/packages/rocketchat-api/server/api.js @@ -1,4 +1,4 @@ -/* global Restivus */ +/* global Restivus, DDP, DDPCommon */ import _ from 'underscore'; class API extends Restivus { @@ -13,6 +13,7 @@ class API extends Restivus { $loki: 0, meta: 0, members: 0, + usernames: 0, // Please use the `channel/dm/group.members` endpoint. This is disabled for performance reasons importIds: 0 }; this.limitedUserFieldsToExclude = { @@ -31,7 +32,7 @@ class API extends Restivus { customFields: 0 }; - this._config.defaultOptionsEndpoint = function() { + this._config.defaultOptionsEndpoint = function _defaultOptionsEndpoint() { if (this.request.method === 'OPTIONS' && this.request.headers['access-control-request-method']) { if (RocketChat.settings.get('API_Enable_CORS') === true) { this.response.writeHead(200, { @@ -57,6 +58,8 @@ class API extends Restivus { success(result={}) { if (_.isObject(result)) { result.success = true; + // TODO: Remove this after three versions have been released. That means at 0.64 this should be gone. ;) + result.developerWarning = '[WARNING]: The "usernames" field has been removed for performance reasons. Please use the "*.members" endpoint to get a list of members/users in a room.'; } return { @@ -96,6 +99,16 @@ class API extends Restivus { }; } + notFound(msg) { + return { + statusCode: 404, + body: { + success: false, + error: msg ? msg : 'Nothing was found' + } + }; + } + addRoute(routes, options, endpoints) { //Note: required if the developer didn't provide options if (typeof endpoints === 'undefined') { @@ -143,8 +156,167 @@ class API extends Restivus { super.addRoute(route, options, endpoints); }); } + + _initAuth() { + const loginCompatibility = (bodyParams) => { + // Grab the username or email that the user is logging in with + const {user, username, email, password, code} = bodyParams; + const auth = { + password + }; + + if (typeof user === 'string') { + auth.user = user.includes('@') ? {email: user} : {username: user}; + } else if (username) { + auth.user = {username}; + } else if (email) { + auth.user = {email}; + } + + if (auth.user == null) { + return bodyParams; + } + + if (auth.password && auth.password.hashed) { + auth.password = { + digest: auth.password, + algorithm: 'sha-256' + }; + } + + if (code) { + return { + totp: { + code, + login: auth + } + }; + } + + return auth; + }; + + const self = this; + + this.addRoute('login', {authRequired: false}, { + post() { + const args = loginCompatibility(this.bodyParams); + + const invocation = new DDPCommon.MethodInvocation({ + connection: {} + }); + + let auth; + try { + auth = DDP._CurrentInvocation.withValue(invocation, () => Meteor.call('login', args)); + } catch (error) { + let e = error; + if (error.reason === 'User not found') { + e = { + error: 'Unauthorized', + reason: 'Unauthorized' + }; + } + + return { + statusCode: 401, + body: { + status: 'error', + error: e.error, + message: e.reason || e.message + } + }; + } + + this.user = Meteor.users.findOne({ + _id: auth.id + }); + + this.userId = this.user._id; + + // Remove tokenExpires to keep the old behavior + Meteor.users.update({ + _id: this.user._id, + 'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(auth.token) + }, { + $unset: { + 'services.resume.loginTokens.$.when': 1 + } + }); + + const response = { + status: 'success', + data: { + userId: this.userId, + authToken: auth.token + } + }; + + const extraData = self._config.onLoggedIn && self._config.onLoggedIn.call(this); + + if (extraData != null) { + _.extend(response.data, { + extra: extraData + }); + } + + return response; + } + }); + + const logout = function() { + // Remove the given auth token from the user's account + const authToken = this.request.headers['x-auth-token']; + const hashedToken = Accounts._hashLoginToken(authToken); + const tokenLocation = self._config.auth.token; + const index = tokenLocation.lastIndexOf('.'); + const tokenPath = tokenLocation.substring(0, index); + const tokenFieldName = tokenLocation.substring(index + 1); + const tokenToRemove = {}; + tokenToRemove[tokenFieldName] = hashedToken; + const tokenRemovalQuery = {}; + tokenRemovalQuery[tokenPath] = tokenToRemove; + + Meteor.users.update(this.user._id, { + $pull: tokenRemovalQuery + }); + + const response = { + status: 'success', + data: { + message: 'You\'ve been logged out!' + } + }; + + // Call the logout hook with the authenticated user attached + const extraData = self._config.onLoggedOut && self._config.onLoggedOut.call(this); + if (extraData != null) { + _.extend(response.data, { + extra: extraData + }); + } + return response; + }; + + /* + Add a logout endpoint to the API + After the user is logged out, the onLoggedOut hook is called (see Restfully.configure() for + adding hook). + */ + return this.addRoute('logout', { + authRequired: true + }, { + get() { + console.warn('Warning: Default logout via GET will be removed in Restivus v1.0. Use POST instead.'); + console.warn(' See https://github.com/kahmali/meteor-restivus/issues/100'); + return logout.call(this); + }, + post: logout + }); + } } + RocketChat.API = {}; const getUserAuth = function _getUserAuth() { diff --git a/packages/rocketchat-api/server/v1/chat.js b/packages/rocketchat-api/server/v1/chat.js index 5bf1bf084533..15f1500d0b2a 100644 --- a/packages/rocketchat-api/server/v1/chat.js +++ b/packages/rocketchat-api/server/v1/chat.js @@ -17,39 +17,48 @@ RocketChat.API.v1.addRoute('chat.delete', { authRequired: true }, { return RocketChat.API.v1.failure('The room id provided does not match where the message is from.'); } + if (this.bodyParams.asUser && msg.u._id !== this.userId && !RocketChat.authz.hasPermission(Meteor.userId(), 'force-delete-message', msg.rid)) { + return RocketChat.API.v1.failure('Unauthorized. You must have the permission "force-delete-message" to delete other\'s message as them.'); + } + Meteor.runAsUser(this.bodyParams.asUser ? msg.u._id : this.userId, () => { Meteor.call('deleteMessage', { _id: msg._id }); }); return RocketChat.API.v1.success({ _id: msg._id, - ts: Date.now() + ts: Date.now(), + message: msg }); } }); RocketChat.API.v1.addRoute('chat.syncMessages', { authRequired: true }, { get() { - const { rid } = this.queryParams; - let lastUpdate = this.queryParams; - lastUpdate = lastUpdate ? new Date(lastUpdate) : lastUpdate; - if (!rid) { - return RocketChat.API.v1.failure('The "rid" query parameter must be provided.'); + const { roomId, lastUpdate } = this.queryParams; + + if (!roomId) { + throw new Meteor.Error('error-roomId-param-not-provided', 'The required "roomId" query param is missing.'); } + if (!lastUpdate) { - return RocketChat.API.v1.failure('The "lastUpdate" query parameter must be provided.'); + throw new Meteor.Error('error-lastUpdate-param-not-provided', 'The required "lastUpdate" query param is missing.'); + } else if (isNaN(Date.parse(lastUpdate))) { + throw new Meteor.Error('error-roomId-param-invalid', 'The "lastUpdate" query parameter must be a valid date.'); } let result; Meteor.runAsUser(this.userId, () => { - result = Meteor.call('messages/get', rid, { lastUpdate }); + result = Meteor.call('messages/get', roomId, { lastUpdate: new Date(lastUpdate) }); }); if (!result) { return RocketChat.API.v1.failure(); } - return RocketChat.API.v1.success({result}); + return RocketChat.API.v1.success({ + result + }); } }); @@ -59,7 +68,6 @@ RocketChat.API.v1.addRoute('chat.getMessage', { authRequired: true }, { return RocketChat.API.v1.failure('The "msgId" query parameter must be provided.'); } - let msg; Meteor.runAsUser(this.userId, () => { msg = Meteor.call('getSingleMessage', this.queryParams.msgId); @@ -78,7 +86,7 @@ RocketChat.API.v1.addRoute('chat.getMessage', { authRequired: true }, { RocketChat.API.v1.addRoute('chat.pinMessage', { authRequired: true }, { post() { if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { - throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.'); + throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is missing.'); } const msg = RocketChat.models.Messages.findOneById(this.bodyParams.messageId); @@ -112,6 +120,49 @@ RocketChat.API.v1.addRoute('chat.postMessage', { authRequired: true }, { } }); +RocketChat.API.v1.addRoute('chat.search', { authRequired: true }, { + get() { + const { roomId, searchText, limit } = this.queryParams; + + if (!roomId) { + throw new Meteor.Error('error-roomId-param-not-provided', 'The required "roomId" query param is missing.'); + } + + if (!searchText) { + throw new Meteor.Error('error-searchText-param-not-provided', 'The required "searchText" query param is missing.'); + } + + if (limit && (typeof limit !== 'number' || isNaN(limit) || limit <= 0)) { + throw new Meteor.Error('error-limit-param-invalid', 'The "limit" query parameter must be a valid number and be greater than 0.'); + } + + let result; + Meteor.runAsUser(this.userId, () => result = Meteor.call('messageSearch', searchText, roomId, limit)); + + return RocketChat.API.v1.success({ + messages: result.messages + }); + } +}); + +// The difference between `chat.postMessage` and `chat.sendMessage` is that `chat.sendMessage` allows +// for passing a value for `_id` and the other one doesn't. Also, `chat.sendMessage` only sends it to +// one channel whereas the other one allows for sending to more than one channel at a time. +RocketChat.API.v1.addRoute('chat.sendMessage', { authRequired: true }, { + post() { + if (!this.bodyParams.message) { + throw new Meteor.Error('error-invalid-params', 'The "message" parameter must be provided.'); + } + + let message; + Meteor.runAsUser(this.userId, () => message = Meteor.call('sendMessage', this.bodyParams.message)); + + return RocketChat.API.v1.success({ + message + }); + } +}); + RocketChat.API.v1.addRoute('chat.starMessage', { authRequired: true }, { post() { if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { diff --git a/packages/rocketchat-api/server/v1/push.js b/packages/rocketchat-api/server/v1/push.js new file mode 100644 index 000000000000..4194a01ac633 --- /dev/null +++ b/packages/rocketchat-api/server/v1/push.js @@ -0,0 +1,59 @@ +/* globals Push */ + +RocketChat.API.v1.addRoute('push.token', { authRequired: true }, { + post() { + const { type, value, appName } = this.bodyParams; + let { id } = this.bodyParams; + + if (id && typeof id !== 'string') { + throw new Meteor.Error('error-id-param-not-valid', 'The required "id" body param is invalid.'); + } else { + id = Random.id(); + } + + if (!type || (type !== 'apn' && type !== 'gcm')) { + throw new Meteor.Error('error-type-param-not-valid', 'The required "type" body param is missing or invalid.'); + } + + if (!value || typeof value !== 'string') { + throw new Meteor.Error('error-token-param-not-valid', 'The required "token" body param is missing or invalid.'); + } + + if (!appName || typeof appName !== 'string') { + throw new Meteor.Error('error-appName-param-not-valid', 'The required "appName" body param is missing or invalid.'); + } + + + let result; + Meteor.runAsUser(this.userId, () => result = Meteor.call('raix:push-update', { + id, + token: { [type]: value }, + appName, + userId: this.userId + })); + + return RocketChat.API.v1.success({ result }); + }, + delete() { + const { token } = this.bodyParams; + + if (!token || typeof token !== 'string') { + throw new Meteor.Error('error-token-param-not-valid', 'The required "token" body param is missing or invalid.'); + } + + const affectedRecords = Push.appCollection.remove({ + $or: [{ + 'token.apn': token + }, { + 'token.gcm': token + }], + userId: this.userId + }); + + if (affectedRecords === 0) { + return RocketChat.API.v1.notFound(); + } + + return RocketChat.API.v1.success(); + } +}); diff --git a/packages/rocketchat-api/server/v1/rooms.js b/packages/rocketchat-api/server/v1/rooms.js new file mode 100644 index 000000000000..9b2da80c664a --- /dev/null +++ b/packages/rocketchat-api/server/v1/rooms.js @@ -0,0 +1,93 @@ +RocketChat.API.v1.addRoute('rooms.get', { authRequired: true }, { + get() { + const { updatedSince } = this.queryParams; + + let updatedSinceDate; + if (updatedSince) { + if (isNaN(Date.parse(updatedSince))) { + throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); + } else { + updatedSinceDate = new Date(updatedSince); + } + } + + let result; + Meteor.runAsUser(this.userId, () => result = Meteor.call('rooms/get', updatedSinceDate)); + + if (Array.isArray(result)) { + result = { + update: result, + remove: [] + }; + } + + return RocketChat.API.v1.success(result); + } +}); + +RocketChat.API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, { + post() { + const room = Meteor.call('canAccessRoom', this.urlParams.rid, this.userId); + + if (!room) { + return RocketChat.API.v1.unauthorized(); + } + + const Busboy = Npm.require('busboy'); + const busboy = new Busboy({ headers: this.request.headers }); + const files = []; + const fields = {}; + + Meteor.wrapAsync((callback) => { + busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { + if (fieldname !== 'file') { + return files.push(new Meteor.Error('invalid-field')); + } + + const fileDate = []; + file.on('data', data => fileDate.push(data)); + + file.on('end', () => { + files.push({ fieldname, file, filename, encoding, mimetype, fileBuffer: Buffer.concat(fileDate) }); + }); + }); + + busboy.on('field', (fieldname, value) => fields[fieldname] = value); + + busboy.on('finish', Meteor.bindEnvironment(() => callback())); + + this.request.pipe(busboy); + })(); + + if (files.length === 0) { + return RocketChat.API.v1.failure('File required'); + } + + if (files.length > 1) { + return RocketChat.API.v1.failure('Just 1 file is allowed'); + } + + const file = files[0]; + + const fileStore = FileUpload.getStore('Uploads'); + + const details = { + name: file.filename, + size: file.fileBuffer.length, + type: file.mimetype, + rid: this.urlParams.rid + }; + + Meteor.runAsUser(this.userId, () => { + const uploadedFile = Meteor.wrapAsync(fileStore.insert.bind(fileStore))(details, file.fileBuffer); + + uploadedFile.description = fields.description; + + delete fields.description; + + RocketChat.API.v1.success(Meteor.call('sendFileMessage', this.urlParams.rid, null, uploadedFile, fields)); + }); + + return RocketChat.API.v1.success(); + } +}); diff --git a/packages/rocketchat-api/server/v1/settings.js b/packages/rocketchat-api/server/v1/settings.js index b954ec1b9be4..3662ba148530 100644 --- a/packages/rocketchat-api/server/v1/settings.js +++ b/packages/rocketchat-api/server/v1/settings.js @@ -1,6 +1,34 @@ import _ from 'underscore'; // settings endpoints +RocketChat.API.v1.addRoute('settings.public', { authRequired: false }, { + get() { + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + let ourQuery = { + hidden: { $ne: true }, + 'public': true + }; + + ourQuery = Object.assign({}, query, ourQuery); + + const settings = RocketChat.models.Settings.find(ourQuery, { + sort: sort ? sort : { _id: 1 }, + skip: offset, + limit: count, + fields: Object.assign({ _id: 1, value: 1 }, fields) + }).fetch(); + + return RocketChat.API.v1.success({ + settings, + count: settings.length, + offset, + total: RocketChat.models.Settings.find(ourQuery).count() + }); + } +}); + RocketChat.API.v1.addRoute('settings', { authRequired: true }, { get() { const { offset, count } = this.getPaginationItems(); @@ -56,3 +84,13 @@ RocketChat.API.v1.addRoute('settings/:_id', { authRequired: true }, { return RocketChat.API.v1.failure(); } }); + +RocketChat.API.v1.addRoute('service.configurations', { authRequired: false }, { + get() { + const ServiceConfiguration = Package['service-configuration'].ServiceConfiguration; + + return RocketChat.API.v1.success({ + configurations: ServiceConfiguration.configurations.find({}, {fields: {secret: 0}}).fetch() + }); + } +}); diff --git a/packages/rocketchat-api/server/v1/subscriptions.js b/packages/rocketchat-api/server/v1/subscriptions.js new file mode 100644 index 000000000000..9b82c0de0007 --- /dev/null +++ b/packages/rocketchat-api/server/v1/subscriptions.js @@ -0,0 +1,26 @@ +RocketChat.API.v1.addRoute('subscriptions.get', { authRequired: true }, { + get() { + const { updatedSince } = this.queryParams; + + let updatedSinceDate; + if (updatedSince) { + if (isNaN(Date.parse(updatedSince))) { + throw new Meteor.Error('error-roomId-param-invalid', 'The "lastUpdate" query parameter must be a valid date.'); + } else { + updatedSinceDate = new Date(updatedSince); + } + } + + let result; + Meteor.runAsUser(this.userId, () => result = Meteor.call('subscriptions/get', updatedSinceDate)); + + if (Array.isArray(result)) { + result = { + update: result, + remove: [] + }; + } + + return RocketChat.API.v1.success(result); + } +}); diff --git a/packages/rocketchat-lib/server/methods/sendMessage.js b/packages/rocketchat-lib/server/methods/sendMessage.js index 698136ba0614..03e4272efaf9 100644 --- a/packages/rocketchat-lib/server/methods/sendMessage.js +++ b/packages/rocketchat-lib/server/methods/sendMessage.js @@ -3,11 +3,13 @@ import moment from 'moment'; Meteor.methods({ sendMessage(message) { check(message, Object); + if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendMessage' }); } + if (message.ts) { const tsDiff = Math.abs(moment(message.ts).diff()); if (tsDiff > 60000) { @@ -22,21 +24,25 @@ Meteor.methods({ } else { message.ts = new Date(); } + if (message.msg && message.msg.length > RocketChat.settings.get('Message_MaxAllowedSize')) { throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', { method: 'sendMessage' }); } + const user = RocketChat.models.Users.findOneById(Meteor.userId(), { fields: { username: 1, name: 1 } }); + const room = Meteor.call('canAccessRoom', message.rid, user._id); if (!room) { return false; } + const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId()); if (subscription && subscription.blocked || subscription.blocker) { RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { @@ -57,12 +63,15 @@ Meteor.methods({ }); return false; } + if (message.alias == null && RocketChat.settings.get('Message_SetNameToAliasEnabled')) { message.alias = user.name; } + if (Meteor.settings['public'].sandstorm) { message.sandstormSessionId = this.connection.sandstormSessionId(); } + RocketChat.metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 return RocketChat.sendMessage(user, message, room); } diff --git a/packages/rocketchat-livechat/app/package-lock.json b/packages/rocketchat-livechat/app/package-lock.json index 271d169de205..ba47b08e4890 100644 --- a/packages/rocketchat-livechat/app/package-lock.json +++ b/packages/rocketchat-livechat/app/package-lock.json @@ -86,7 +86,7 @@ "bcrypt": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-1.0.3.tgz", - "integrity": "sha1-sC3cbAtS6ha40883XVoy54DatUg=", + "integrity": "sha512-pRyDdo73C8Nim3jwFJ7DWe3TZCgwDfWZ6nHS5LSdU77kWbj1frruvdndP02AOavtD4y8v6Fp2dolbHgp4SDrfg==", "requires": { "nan": "2.6.2", "node-pre-gyp": "0.6.36" @@ -314,7 +314,7 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "requires": { "fs.realpath": "1.0.0", "inflight": "1.0.6", @@ -469,7 +469,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "1.1.8" } @@ -530,7 +530,7 @@ "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { "are-we-there-yet": "1.1.4", "console-control-strings": "1.1.0", @@ -626,7 +626,7 @@ "readable-stream": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", @@ -640,7 +640,7 @@ "regenerator-runtime": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", - "integrity": "sha1-flT+W1zNXWYk6mJVw0c74JC4AuE=" + "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==" }, "request": { "version": "2.83.0", @@ -682,12 +682,12 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=" + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" }, "set-blocking": { "version": "2.0.0", @@ -841,7 +841,7 @@ "uuid": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=" + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" }, "verror": { "version": "1.10.0", @@ -856,7 +856,7 @@ "wide-align": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "requires": { "string-width": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" } diff --git a/server/methods/messageSearch.js b/server/methods/messageSearch.js index b6c6b0804f37..fc241f7025cc 100644 --- a/server/methods/messageSearch.js +++ b/server/methods/messageSearch.js @@ -2,22 +2,16 @@ import s from 'underscore.string'; Meteor.methods({ messageSearch(text, rid, limit) { + check(text, String); + check(rid, String); + check(limit, Match.Optional(Number)); + + // TODO: Evaluate why we are returning `users` and `channels`, as the only thing that gets set is the `messages`. const result = { messages: [], users: [], channels: [] }; - const query = {}; - const options = { - sort: { - ts: -1 - }, - limit: limit || 20 - }; - - check(text, String); - check(rid, String); - check(limit, Match.Optional(Number)); const currentUserId = Meteor.userId(); if (!currentUserId) { @@ -25,9 +19,23 @@ Meteor.methods({ method: 'messageSearch' }); } + + // Don't process anything else if the user can't access the room + if (!Meteor.call('canAccessRoom', rid, currentUserId)) { + return result; + } + const currentUserName = Meteor.user().username; const currentUserTimezoneOffset = Meteor.user().utcOffset; + const query = {}; + const options = { + sort: { + ts: -1 + }, + limit: limit || 20 + }; + // I would place these methods at the bottom of the file for clarity but travis doesn't appreciate that. // (no-use-before-define) @@ -121,24 +129,28 @@ Meteor.methods({ from.push(username); return ''; }); + if (from.length > 0) { query['u.username'] = { $regex: from.join('|'), $options: 'i' }; } + // Query for senders const mention = []; text = text.replace(/mention:([a-z0-9.-_]+)/ig, function(match, username) { mention.push(username); return ''; }); + if (mention.length > 0) { query['mentions.username'] = { $regex: mention.join('|'), $options: 'i' }; } + // Filter on messages that are starred by the current user. text = text.replace(/has:star/g, filterStarred); // Filter on messages that have an url. @@ -184,6 +196,7 @@ Meteor.methods({ }; } } + if (Object.keys(query).length > 0) { query.t = { $ne: 'rm' //hide removed messages (useful when searching for user messages) @@ -191,17 +204,14 @@ Meteor.methods({ query._hidden = { $ne: true // don't return _hidden messages }; - if (rid != null) { - query.rid = rid; - if (Meteor.call('canAccessRoom', rid, currentUserId) !== false) { - if (!RocketChat.settings.get('Message_ShowEditedStatus')) { - options.fields = { - 'editedAt': 0 - }; - } - result.messages = RocketChat.models.Messages.find(query, options).fetch(); - } + query.rid = rid; + + if (!RocketChat.settings.get('Message_ShowEditedStatus')) { + options.fields = { + 'editedAt': 0 + }; } + result.messages = RocketChat.models.Messages.find(query, options).fetch(); } return result; diff --git a/server/publications/subscription.js b/server/publications/subscription.js index 2685aca38e02..2b00302f00fa 100644 --- a/server/publications/subscription.js +++ b/server/publications/subscription.js @@ -39,9 +39,7 @@ Meteor.methods({ this.unblock(); - const options = { - fields - }; + const options = { fields }; const records = RocketChat.models.Subscriptions.findByUserId(Meteor.userId(), options).fetch(); @@ -60,6 +58,7 @@ Meteor.methods({ }).fetch() }; } + return records; } }); diff --git a/tests/end-to-end/api/00-miscellaneous.js b/tests/end-to-end/api/00-miscellaneous.js index 6e457a376845..a871aa758559 100644 --- a/tests/end-to-end/api/00-miscellaneous.js +++ b/tests/end-to-end/api/00-miscellaneous.js @@ -3,7 +3,7 @@ /* eslint no-unused-vars: 0 */ import {getCredentials, api, login, request, credentials} from '../../data/api-data.js'; -import {adminEmail} from '../../data/user.js'; +import {adminEmail, adminUsername, adminPassword} from '../../data/user.js'; import supertest from 'supertest'; describe('miscellaneous', function() { @@ -29,6 +29,32 @@ describe('miscellaneous', function() { expect(credentials).to.have.property('X-User-Id').with.length.at.least(1); }); + it('/login (wrapper username)', (done) => { + request.post(api('login')) + .send({ + user: { + username: adminUsername + }, + password: adminPassword + }) + .expect('Content-Type', 'application/json') + .expect(200) + .end(done); + }); + + it('/login (wrapper email)', (done) => { + request.post(api('login')) + .send({ + user: { + email: adminEmail + }, + password: adminPassword + }) + .expect('Content-Type', 'application/json') + .expect(200) + .end(done); + }); + it('/me', (done) => { request.get(api('me')) .set(credentials) diff --git a/tests/end-to-end/api/05-chat.js b/tests/end-to-end/api/05-chat.js index f10ff93932e9..f62fda5d56d0 100644 --- a/tests/end-to-end/api/05-chat.js +++ b/tests/end-to-end/api/05-chat.js @@ -72,6 +72,70 @@ describe('[Chat]', function() { .end(done); }); + it('/chat.sendMessage', (done) => { + message._id = `id-${ Date.now() }`; + request.post(api('chat.sendMessage')) + .set(credentials) + .send({ + message: { + _id: message._id, + rid: 'GENERAL', + msg: 'Sample message', + alias: 'Gruggy', + emoji: ':smirk:', + avatar: 'http://res.guggy.com/logo_128.png', + attachments: [{ + color: '#ff0000', + text: 'Yay for gruggy!', + ts: '2016-12-09T16:53:06.761Z', + thumb_url: 'http://res.guggy.com/logo_128.png', + message_link: 'https://google.com', + collapsed: false, + author_name: 'Bradley Hilton', + author_link: 'https://rocket.chat/', + author_icon: 'https://avatars.githubusercontent.com/u/850391?v=3', + title: 'Attachment Example', + title_link: 'https://youtube.com', + title_link_download: 'https://rocket.chat/download', + image_url: 'http://res.guggy.com/logo_128.png', + audio_url: 'http://www.w3schools.com/tags/horse.mp3', + video_url: 'http://www.w3schools.com/tags/movie.mp4', + fields: [{ + short: true, + title: 'Test', + value: 'Testing out something or other' + }, { + short: true, + title: 'Another Test', + value: '[Link](https://google.com/) something and this and that.' + }] + }] + } + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.deep.property('message.msg', 'Sample message'); + }) + .end(done); + }); + + it('/chat.getMessage', (done) => { + request.get(api('chat.getMessage')) + .set(credentials) + .query({ + msgId: message._id + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.deep.property('message._id', message._id); + }) + .end(done); + }); + it('/chat.update', (done) => { request.post(api('chat.update')) .set(credentials) @@ -88,4 +152,20 @@ describe('[Chat]', function() { }) .end(done); }); + + it('/chat.search', (done) => { + request.get(api('chat.search')) + .set(credentials) + .query({ + roomId: 'GENERAL', + searchText: 'This message was edited via API' + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages'); + }) + .end(done); + }); }); diff --git a/tests/end-to-end/api/08-settings.js b/tests/end-to-end/api/08-settings.js new file mode 100644 index 000000000000..fa6cd5b0a65b --- /dev/null +++ b/tests/end-to-end/api/08-settings.js @@ -0,0 +1,68 @@ +/* eslint-env mocha */ +/* globals expect */ + +import {getCredentials, api, request, credentials} from '../../data/api-data.js'; + +describe('[Settings]', function() { + this.retries(0); + + before(done => getCredentials(done)); + + describe('[/settings.public]', () => { + it('should return public settings', (done) => { + request.get(api('settings.public')) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('settings'); + expect(res.body).to.have.property('count'); + }) + .end(done); + }); + }); + + describe('[/settings]', () => { + it('should return private settings', (done) => { + request.get(api('settings')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('settings'); + expect(res.body).to.have.property('count'); + }) + .end(done); + }); + }); + + describe('[/settings/:_id]', () => { + it('should return one setting', (done) => { + request.get(api('settings/Site_Url')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('_id', 'Site_Url'); + expect(res.body).to.have.property('value'); + }) + .end(done); + }); + }); + + describe('[/service.configurations]', () => { + it('should return service configurations', (done) => { + request.get(api('service.configurations')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('configurations'); + }) + .end(done); + }); + }); +}); diff --git a/tests/end-to-end/api/09-rooms.js b/tests/end-to-end/api/09-rooms.js new file mode 100644 index 000000000000..82410c239935 --- /dev/null +++ b/tests/end-to-end/api/09-rooms.js @@ -0,0 +1,37 @@ +/* eslint-env mocha */ +/* globals expect */ + +import {getCredentials, api, request, credentials } from '../../data/api-data.js'; + +describe('[Rooms]', function() { + this.retries(0); + + before(done => getCredentials(done)); + + it('/rooms.get', (done) => { + request.get(api('rooms.get')) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('update'); + expect(res.body).to.have.property('remove'); + }) + .end(done); + }); + + it('/rooms.get?updatedSince', (done) => { + request.get(api('rooms.get')) + .set(credentials) + .query({ + updatedSince: new Date + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('update').that.have.lengthOf(0); + expect(res.body).to.have.property('remove').that.have.lengthOf(0); + }) + .end(done); + }); +}); diff --git a/tests/end-to-end/api/10-subscriptions.js b/tests/end-to-end/api/10-subscriptions.js new file mode 100644 index 000000000000..74c7e9b97f5b --- /dev/null +++ b/tests/end-to-end/api/10-subscriptions.js @@ -0,0 +1,38 @@ +/* eslint-env mocha */ +/* globals expect */ + +import {getCredentials, api, request, credentials } from '../../data/api-data.js'; + +describe('[Subscriptions]', function() { + this.retries(0); + + before(done => getCredentials(done)); + + it('/subscriptions.get', (done) => { + request.get(api('subscriptions.get')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('update'); + expect(res.body).to.have.property('remove'); + }) + .end(done); + }); + + it('/subscriptions.get?updatedSince', (done) => { + request.get(api('subscriptions.get')) + .set(credentials) + .query({ + updatedSince: new Date + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('update').that.have.lengthOf(0); + expect(res.body).to.have.property('remove').that.have.lengthOf(0); + }) + .end(done); + }); +});