Skip to content

Commit

Permalink
Prevent >64k text in composition box; truncate too-large drafts
Browse files Browse the repository at this point in the history
  • Loading branch information
scottnonnenberg-signal committed Sep 16, 2019
1 parent 87ae65c commit 095cd88
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 7 deletions.
21 changes: 18 additions & 3 deletions js/conversation_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

window.Whisper = window.Whisper || {};

const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;

const conversations = new Whisper.ConversationCollection();
const inboxCollection = new (Backbone.Collection.extend({
initialize() {
Expand Down Expand Up @@ -183,12 +185,25 @@

this._initialFetchComplete = true;
await Promise.all(
conversations.map(conversation => {
conversations.map(async conversation => {
if (!conversation.get('lastMessage')) {
return conversation.updateLastMessage();
await conversation.updateLastMessage();
}

return null;
// In case a too-large draft was saved to the database
const draft = conversation.get('draft');
if (draft && draft.length > MAX_MESSAGE_BODY_LENGTH) {
this.model.set({
draft: draft.slice(0, MAX_MESSAGE_BODY_LENGTH),
});
await window.Signal.Data.updateConversation(
conversation.id,
conversation.attributes,
{
Conversation: Whisper.Conversation,
}
);
}
})
);
window.log.info('ConversationController: done with initial fetch');
Expand Down
1 change: 1 addition & 0 deletions js/views/conversation_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@
onSubmit: message => this.sendMessage(message),
onEditorStateChange: (msg, caretLocation) =>
this.onEditorStateChange(msg, caretLocation),
onTextTooLong: () => this.showToast(Whisper.MessageBodyTooLongToast),
onChooseAttachment: this.onChooseAttachment.bind(this),
micCellEl,
attachmentListEl,
Expand Down
8 changes: 7 additions & 1 deletion ts/components/CompositionArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ export type OwnProps = {

export type Props = Pick<
CompositionInputProps,
'onSubmit' | 'onEditorSizeChange' | 'onEditorStateChange' | 'startingText'
| 'onSubmit'
| 'onEditorSizeChange'
| 'onEditorStateChange'
| 'onTextTooLong'
| 'startingText'
> &
Pick<
EmojiButtonProps,
Expand Down Expand Up @@ -76,6 +80,7 @@ export const CompositionArea = ({
compositionApi,
onEditorSizeChange,
onEditorStateChange,
onTextTooLong,
startingText,
// EmojiButton
onPickEmoji,
Expand Down Expand Up @@ -336,6 +341,7 @@ export const CompositionArea = ({
onSubmit={handleSubmit}
onEditorSizeChange={onEditorSizeChange}
onEditorStateChange={onEditorStateChange}
onTextTooLong={onTextTooLong}
onDirtyChange={setDirty}
skinTone={skinTone}
startingText={startingText}
Expand Down
87 changes: 87 additions & 0 deletions ts/components/CompositionInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from './emoji/lib';
import { LocalizerType } from '../types/Util';

const MAX_LENGTH = 64 * 1024;
const colonsRegex = /(?:^|\s):[a-z0-9-_+]+:?/gi;
const triggerEmojiRegex = /^(?:[-+]\d|[a-z]{2})/i;

Expand All @@ -43,6 +44,7 @@ export type Props = {
onDirtyChange?(dirty: boolean): unknown;
onEditorStateChange?(messageText: string, caretLocation: number): unknown;
onEditorSizeChange?(rect: ContentRect): unknown;
onTextTooLong(): unknown;
onPickEmoji(o: EmojiPickDataType): unknown;
onSubmit(message: string): unknown;
};
Expand Down Expand Up @@ -78,6 +80,43 @@ function getTrimmedMatchAtIndex(str: string, index: number, pattern: RegExp) {
return null;
}

function getLengthOfSelectedText(state: EditorState): number {
const currentSelection = state.getSelection();
let length = 0;

const currentContent = state.getCurrentContent();
const startKey = currentSelection.getStartKey();
const endKey = currentSelection.getEndKey();
const startBlock = currentContent.getBlockForKey(startKey);
const isStartAndEndBlockAreTheSame = startKey === endKey;
const startBlockTextLength = startBlock.getLength();
const startSelectedTextLength =
startBlockTextLength - currentSelection.getStartOffset();
const endSelectedTextLength = currentSelection.getEndOffset();
const keyAfterEnd = currentContent.getKeyAfter(endKey);

if (isStartAndEndBlockAreTheSame) {
length +=
currentSelection.getEndOffset() - currentSelection.getStartOffset();
} else {
let currentKey = startKey;

while (currentKey && currentKey !== keyAfterEnd) {
if (currentKey === startKey) {
length += startSelectedTextLength + 1;
} else if (currentKey === endKey) {
length += endSelectedTextLength;
} else {
length += currentContent.getBlockForKey(currentKey).getLength() + 1;
}

currentKey = currentContent.getKeyAfter(currentKey);
}
}

return length;
}

function getWordAtIndex(str: string, index: number) {
const start = str
.slice(0, index + 1)
Expand Down Expand Up @@ -172,6 +211,7 @@ export const CompositionInput = ({
onDirtyChange,
onEditorStateChange,
onEditorSizeChange,
onTextTooLong,
onPickEmoji,
onSubmit,
skinTone,
Expand Down Expand Up @@ -298,6 +338,51 @@ export const CompositionInput = ({
]
);

const handleBeforeInput = React.useCallback(
(): DraftHandleValue => {
if (!editorStateRef.current) {
return 'not-handled';
}

const editorState = editorStateRef.current;
const plainText = editorState.getCurrentContent().getPlainText();
const selectedTextLength = getLengthOfSelectedText(editorState);

if (plainText.length - selectedTextLength > MAX_LENGTH - 1) {
onTextTooLong();

return 'handled';
}

return 'not-handled';
},
[onTextTooLong, editorStateRef]
);

const handlePastedText = React.useCallback(
(pastedText: string): DraftHandleValue => {
if (!editorStateRef.current) {
return 'not-handled';
}

const editorState = editorStateRef.current;
const plainText = editorState.getCurrentContent().getPlainText();
const selectedTextLength = getLengthOfSelectedText(editorState);

if (
plainText.length + pastedText.length - selectedTextLength >
MAX_LENGTH
) {
onTextTooLong();

return 'handled';
}

return 'not-handled';
},
[onTextTooLong, editorStateRef]
);

const resetEditorState = React.useCallback(
() => {
const newEmptyState = EditorState.createEmpty(compositeDecorator);
Expand Down Expand Up @@ -694,6 +779,8 @@ export const CompositionInput = ({
onEscape={handleEscapeKey}
onTab={onTab}
handleKeyCommand={handleEditorCommand}
handleBeforeInput={handleBeforeInput}
handlePastedText={handlePastedText}
keyBindingFn={editorKeybindingFn}
spellCheck={true}
stripPastedStyles={true}
Expand Down
6 changes: 3 additions & 3 deletions ts/util/lint/exceptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,15 @@
"rule": "jQuery-load(",
"path": "js/conversation_controller.js",
"line": " async load() {",
"lineNumber": 169,
"lineNumber": 171,
"reasonCategory": "falseMatch",
"updated": "2019-07-31T00:19:18.696Z"
},
{
"rule": "jQuery-load(",
"path": "js/conversation_controller.js",
"line": " this._initialPromise = load();",
"lineNumber": 204,
"lineNumber": 219,
"reasonCategory": "falseMatch",
"updated": "2019-07-31T00:19:18.696Z"
},
Expand Down Expand Up @@ -7475,7 +7475,7 @@
"rule": "DOM-innerHTML",
"path": "ts/components/CompositionArea.tsx",
"line": " el.innerHTML = '';",
"lineNumber": 65,
"lineNumber": 69,
"reasonCategory": "usageTrusted",
"updated": "2019-08-01T14:10:37.481Z",
"reasonDetail": "Our code, no user input, only clearing out the dom"
Expand Down

0 comments on commit 095cd88

Please sign in to comment.