diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index 8bd71f70..f5c16bf8 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -46,6 +46,7 @@ import CommandsPreview from './CommandsPreview'; import { getUserSelector } from '../../selectors/login'; import Navigation from '../../lib/Navigation'; import { withActionSheet } from '../ActionSheet'; +import { sanitizeLikeString } from '../../lib/database/utils'; const imagePickerConfig = { cropping: true, @@ -491,8 +492,9 @@ class MessageBox extends Component { const db = database.active; if (keyword) { const customEmojisCollection = db.collections.get('custom_emojis'); + const likeString = sanitizeLikeString(keyword); let customEmojis = await customEmojisCollection.query( - Q.where('name', Q.like(`${ Q.sanitizeLikeString(keyword) }%`)) + Q.where('name', Q.like(`${ likeString }%`)) ).fetch(); customEmojis = customEmojis.slice(0, MENTIONS_COUNT_TO_DISPLAY); const filteredEmojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, MENTIONS_COUNT_TO_DISPLAY); @@ -504,8 +506,9 @@ class MessageBox extends Component { getSlashCommands = debounce(async(keyword) => { const db = database.active; const commandsCollection = db.collections.get('slash_commands'); + const likeString = sanitizeLikeString(keyword); const commands = await commandsCollection.query( - Q.where('id', Q.like(`${ Q.sanitizeLikeString(keyword) }%`)) + Q.where('id', Q.like(`${ likeString }%`)) ).fetch(); this.setState({ mentions: commands || [] }); }, 300) @@ -734,8 +737,9 @@ class MessageBox extends Component { const db = database.active; const commandsCollection = db.collections.get('slash_commands'); const command = message.replace(/ .*/, '').slice(1); + const likeString = sanitizeLikeString(command); const slashCommand = await commandsCollection.query( - Q.where('id', Q.like(`${ Q.sanitizeLikeString(command) }%`)) + Q.where('id', Q.like(`${ likeString }%`)) ).fetch(); if (slashCommand.length > 0) { logEvent(events.COMMAND_RUN); diff --git a/app/lib/database/utils.js b/app/lib/database/utils.js index 8e580030..6c5d86b5 100644 --- a/app/lib/database/utils.js +++ b/app/lib/database/utils.js @@ -1 +1,7 @@ +import XRegExp from 'xregexp'; + +// Matches letters from any alphabet and numbers +const likeStringRegex = new XRegExp('[^\\p{L}\\p{Nd}]', 'g'); +export const sanitizeLikeString = str => str.replace(likeStringRegex, '_'); + export const sanitizer = r => r; diff --git a/app/lib/database/utils.test.js b/app/lib/database/utils.test.js new file mode 100644 index 00000000..dd1e3996 --- /dev/null +++ b/app/lib/database/utils.test.js @@ -0,0 +1,37 @@ +/* eslint-disable no-undef */ +import * as utils from './utils'; + +describe('sanitizeLikeStringTester', () => { + // example chars that shouldn't return + const disallowedChars = ',./;[]!@#$%^&*()_-=+~'; + + const sanitizeLikeStringTester = str => expect(utils.sanitizeLikeString(`${ str }${ disallowedChars }`)).toBe(`${ str }${ '_'.repeat(disallowedChars.length) }`); + + // Testing a couple of different alphabets + test('render test (latin)', () => { + sanitizeLikeStringTester('test123'); + }); + + test('render test (arabic)', () => { + sanitizeLikeStringTester('اختبار123'); + }); + + test('render test (russian)', () => { + sanitizeLikeStringTester('тест123'); + }); + + test('render test (chinese trad)', () => { + sanitizeLikeStringTester('測試123'); + }); + + test('render test (japanese)', () => { + sanitizeLikeStringTester('テスト123'); + }); +}); + +describe('sanitizer', () => { + test('render the same result', () => { + const content = { a: true }; + expect(utils.sanitizer(content)).toBe(content); + }); +}); diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 483f183b..5e60b09f 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -56,6 +56,7 @@ import { useSsl } from '../utils/url'; import UserPreferences from './userPreferences'; import { Encryption } from './encryption'; import EventEmitter from '../utils/events'; +import { sanitizeLikeString } from './database/utils'; const TOKEN_KEY = 'reactnativemeteor_usertoken'; const CURRENT_SERVER = 'currentServer'; @@ -517,8 +518,12 @@ const RocketChat = { } const db = database.active; + const likeString = sanitizeLikeString(searchText); let data = await db.collections.get('subscriptions').query( - Q.where('name', Q.like(`%${ Q.sanitizeLikeString(searchText) }%`)) + Q.or( + Q.where('name', Q.like(`%${ likeString }%`)), + Q.where('fname', Q.like(`%${ likeString }%`)) + ) ).fetch(); if (filterUsers && !filterRooms) { diff --git a/app/views/NewServerView/index.js b/app/views/NewServerView/index.js index 8b2047ac..5d5e4704 100644 --- a/app/views/NewServerView/index.js +++ b/app/views/NewServerView/index.js @@ -30,6 +30,7 @@ import { CloseModalButton } from '../../containers/HeaderButton'; import { showConfirmationAlert } from '../../utils/info'; import database from '../../lib/database'; import ServerInput from './ServerInput'; +import { sanitizeLikeString } from '../../lib/database/utils'; const styles = StyleSheet.create({ title: { @@ -138,10 +139,11 @@ class NewServerView extends React.Component { Q.experimentalSortBy('updated_at', Q.desc), Q.experimentalTake(3) ]; + const likeString = sanitizeLikeString(text); if (text) { whereClause = [ ...whereClause, - Q.where('url', Q.like(`%${ Q.sanitizeLikeString(text) }%`)) + Q.where('url', Q.like(`%${ likeString }%`)) ]; } const serversHistory = await serversHistoryCollection.query(...whereClause).fetch(); diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js index db7d7013..607711eb 100644 --- a/app/views/SearchMessagesView/index.js +++ b/app/views/SearchMessagesView/index.js @@ -22,6 +22,7 @@ import { getUserSelector } from '../../selectors/login'; import SafeAreaView from '../../containers/SafeAreaView'; import { CloseModalButton } from '../../containers/HeaderButton'; import database from '../../lib/database'; +import { sanitizeLikeString } from '../../lib/database/utils'; class SearchMessagesView extends React.Component { static navigationOptions = ({ navigation, route }) => { @@ -83,12 +84,13 @@ class SearchMessagesView extends React.Component { if (this.encrypted) { const db = database.active; const messagesCollection = db.collections.get('messages'); + const likeString = sanitizeLikeString(searchText); return messagesCollection .query( // Messages of this room Q.where('rid', this.rid), // Message content is like the search text - Q.where('msg', Q.like(`%${ Q.sanitizeLikeString(searchText) }%`)) + Q.where('msg', Q.like(`%${ likeString }%`)) ) .fetch(); } diff --git a/app/views/ShareListView/index.js b/app/views/ShareListView/index.js index 250498e4..1f94ee74 100644 --- a/app/views/ShareListView/index.js +++ b/app/views/ShareListView/index.js @@ -26,6 +26,7 @@ import { animateNextTransition } from '../../utils/layoutAnimation'; import { withTheme } from '../../theme'; import SafeAreaView from '../../containers/SafeAreaView'; import RocketChat from '../../lib/rocketchat'; +import { sanitizeLikeString } from '../../lib/database/utils'; const permission = { title: I18n.t('Read_External_Permission'), @@ -195,13 +196,14 @@ class ShareListView extends React.Component { Q.experimentalSortBy('room_updated_at', Q.desc) ]; if (text) { + const likeString = sanitizeLikeString(text); return db.collections .get('subscriptions') .query( ...defaultWhereClause, Q.or( - Q.where('name', Q.like(`%${ Q.sanitizeLikeString(text) }%`)), - Q.where('fname', Q.like(`%${ Q.sanitizeLikeString(text) }%`)) + Q.where('name', Q.like(`%${ likeString }%`)), + Q.where('fname', Q.like(`%${ likeString }%`)) ) ).fetch(); } diff --git a/package.json b/package.json index 82aadc5e..b47d79da 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,8 @@ "semver": "7.3.2", "ua-parser-js": "^0.7.21", "url-parse": "^1.4.7", - "use-deep-compare-effect": "^1.3.1" + "use-deep-compare-effect": "^1.3.1", + "xregexp": "^4.3.0" }, "devDependencies": { "@babel/core": "^7.8.4", diff --git a/yarn.lock b/yarn.lock index ee38acfd..bc63f35e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16109,7 +16109,7 @@ xregexp@4.1.1: resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.1.1.tgz#eb8a032aa028d403f7b1b22c47a5f16c24b21d8d" integrity sha512-QJ1gfSUV7kEOLfpKFCjBJRnfPErUzkNKFMso4kDSmGpp3x6ZgkyKf74inxI7PnnQCFYq5TqYJCd7DrgDN8Q05A== -xregexp@^4.2.4: +xregexp@^4.2.4, xregexp@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50" integrity sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==