Merge branch 'fix-emoji-vuln' into 'develop'

Patch emoji parsing from upstream

Closes #1179

See merge request soapbox-pub/soapbox!1893
redesign-interaction-bar2
Alex Gleason 2022-11-15 16:54:14 +00:00
commit b5393b531b
6 zmienionych plików z 96 dodań i 68 usunięć

Wyświetl plik

@ -11,8 +11,8 @@ describe('emoji', () => {
});
it('works with unclosed tags', () => {
expect(emojify('hello>')).toEqual('hello>');
expect(emojify('<hello')).toEqual('<hello');
expect(emojify('hello>')).toEqual('hello&gt;');
expect(emojify('<hello')).toEqual('');
});
it('works with unclosed shortcodes', () => {
@ -22,23 +22,23 @@ describe('emoji', () => {
it('does unicode', () => {
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
'<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/packs/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
'<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/packs/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg">');
expect(emojify('👨‍👩‍👧‍👧')).toEqual(
'<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/packs/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/packs/emoji/1f469-200d-1f469-200d-1f466.svg" />');
'<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/packs/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg">');
expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/packs/emoji/1f469-200d-1f469-200d-1f466.svg">');
expect(emojify('\u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" />');
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg">');
});
it('does multiple unicode', () => {
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" />');
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg">');
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" />');
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg"><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg">');
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" />');
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg"> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg">');
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" /> bar');
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg"> bar');
});
it('ignores unicode inside of tags', () => {
@ -46,16 +46,16 @@ describe('emoji', () => {
});
it('does multiple emoji properly (issue 5188)', () => {
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/packs/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/packs/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/packs/emoji/1f495.svg" />');
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/packs/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/packs/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/packs/emoji/1f495.svg" />');
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/packs/emoji/1f44c.svg"><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/packs/emoji/1f308.svg"><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/packs/emoji/1f495.svg">');
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/packs/emoji/1f44c.svg"> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/packs/emoji/1f308.svg"> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/packs/emoji/1f495.svg">');
});
it('does an emoji that has no shortcode', () => {
expect(emojify('👁‍🗨')).toEqual('<img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/packs/emoji/1f441-200d-1f5e8.svg" />');
expect(emojify('👁‍🗨')).toEqual('<img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/packs/emoji/1f441-200d-1f5e8.svg">');
});
it('does an emoji whose filename is irregular', () => {
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/packs/emoji/2199.svg" />');
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/packs/emoji/2199.svg">');
});
it('avoid emojifying on invisible text', () => {
@ -67,16 +67,26 @@ describe('emoji', () => {
it('avoid emojifying on invisible text with nested tags', () => {
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg" />');
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg">');
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg" />');
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg">');
expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
.toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg" />');
.toEqual('<span class="invisible">😄<br>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg">');
});
it('skips the textual presentation VS15 character', () => {
expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/packs/emoji/2734.svg" />');
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/packs/emoji/2734.svg">');
});
it('does an simple emoji properly', () => {
expect(emojify('♀♂'))
.toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/packs/emoji/2640.svg"><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/packs/emoji/2642.svg">');
});
it('does an emoji containing ZWJ properly', () => {
expect(emojify('💂‍♀️💂‍♂️'))
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀" title=":female-guard:" src="/packs/emoji/1f482-200d-2640-fe0f.svg"><img draggable="false" class="emojione" alt="💂\u200D♂" title=":male-guard:" src="/packs/emoji/1f482-200d-2642-fe0f.svg">');
});
});
});

Wyświetl plik

@ -6,60 +6,44 @@ import unicodeMapping from './emoji_unicode_mapping_light';
const trie = new Trie(Object.keys(unicodeMapping));
const emojify = (str, customEmojis = {}, autoplay = false) => {
const tagCharsWithoutEmojis = '<&';
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
const domParser = new DOMParser();
const emojifyTextNode = (node, customEmojis, autoPlayGif = false) => {
let str = node.textContent;
const fragment = new DocumentFragment();
for (;;) {
let match, i = 0, tag;
while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) {
i += str.codePointAt(i) < 65536 ? 1 : 2;
let match, i = 0;
if (customEmojis === null) {
while (i < str.length && !(match = trie.search(str.slice(i)))) {
i += str.codePointAt(i) < 65536 ? 1 : 2;
}
} else {
while (i < str.length && str[i] !== ':' && !(match = trie.search(str.slice(i)))) {
i += str.codePointAt(i) < 65536 ? 1 : 2;
}
}
let rend, replacement = '';
if (i === str.length) {
break;
} else if (str[i] === ':') {
// FIXME: This is insane.
/* eslint-disable no-loop-func */
// eslint-disable-next-line no-loop-func
if (!(() => {
rend = str.indexOf(':', i + 1) + 1;
if (!rend) return false; // no pair of ':'
const lt = str.indexOf('<', i + 1);
if (!(lt === -1 || lt >= rend)) return false; // tag appeared before closing ':'
const shortname = str.slice(i, rend);
// now got a replacee as ':shortname:'
// if you want additional emoji handler, add statements below which set replacement and return true.
if (shortname in customEmojis) {
const filename = autoplay ? customEmojis[shortname].url : customEmojis[shortname].static_url;
replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
replacement = `<img draggable="false" class="emojione custom-emoji" alt="${shortname}" title="${shortname}" src="${filename}" data-original="${customEmojis[shortname].url}" data-static="${customEmojis[shortname].static_url}" />`;
return true;
}
return false;
})()) rend = ++i;
/* eslint-enable no-loop-func */
} else if (tag >= 0) { // <, &
rend = str.indexOf('>;'[tag], i + 1) + 1;
if (!rend) {
break;
}
if (tag === 0) {
if (invisible) {
if (str[i + 1] === '/') { // closing tag
if (!--invisible) {
tagChars = tagCharsWithEmojis;
}
} else if (str[rend - 2] !== '/') { // opening tag
invisible++;
}
} else {
if (str.startsWith('<span class="invisible">', i)) {
// avoid emojifying on invisible text
invisible = 1;
tagChars = tagCharsWithoutEmojis;
}
}
}
i = rend;
} else { // matched to unicode emoji
const { filename, shortCode } = unicodeMapping[match];
const title = shortCode ? `:${shortCode}:` : '';
@ -71,20 +55,53 @@ const emojify = (str, customEmojis = {}, autoplay = false) => {
rend += 1;
}
}
rtn += str.slice(0, i) + replacement;
fragment.append(document.createTextNode(str.slice(0, i)));
if (replacement) {
fragment.append(domParser.parseFromString(replacement, 'text/html').documentElement.getElementsByTagName('img')[0]);
}
node.textContent = str.slice(0, i);
str = str.slice(rend);
}
return rtn + str;
fragment.append(document.createTextNode(str));
node.parentElement.replaceChild(fragment, node);
};
const emojifyNode = (node, customEmojis, autoPlayGif = false) => {
for (const child of node.childNodes) {
switch (child.nodeType) {
case Node.TEXT_NODE:
emojifyTextNode(child, customEmojis, autoPlayGif);
break;
case Node.ELEMENT_NODE:
if (!child.classList.contains('invisible'))
emojifyNode(child, customEmojis, autoPlayGif);
break;
}
}
};
const emojify = (str, customEmojis = {}, autoPlayGif = false) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = str;
if (!Object.keys(customEmojis).length)
customEmojis = null;
emojifyNode(wrapper, customEmojis, autoPlayGif);
return wrapper.innerHTML;
};
export default emojify;
export const buildCustomEmojis = (customEmojis, autoplay = false) => {
export const buildCustomEmojis = (customEmojis, autoPlayGif = false) => {
const emojis = [];
customEmojis.forEach(emoji => {
const shortcode = emoji.get('shortcode');
const url = autoplay ? emoji.get('url') : emoji.get('static_url');
const url = autoPlayGif ? emoji.get('url') : emoji.get('static_url');
const name = shortcode.replace(':', '');
emojis.push({
@ -96,8 +113,11 @@ export const buildCustomEmojis = (customEmojis, autoplay = false) => {
keywords: [name],
imageUrl: url,
custom: true,
customCategory: emoji.get('category'),
});
});
return emojis;
};
export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set(['custom']));

Wyświetl plik

@ -131,15 +131,13 @@ describe('normalizeAccount()', () => {
it('emojifies display name as `display_name_html`', () => {
const account = require('soapbox/__fixtures__/account-with-emojis.json');
const result = normalizeAccount(account);
const expected = 'Alex Gleason <img draggable="false" class="emojione" alt="😂" title=":joy:" src="/packs/emoji/1f602.svg" /> <img draggable="false" class="emojione" alt=":soapbox:" title=":soapbox:" src="https://gleasonator.com/emoji/Gleasonator/soapbox.png" /> <img draggable="false" class="emojione" alt=":ablobcatrainbow:" title=":ablobcatrainbow:" src="https://gleasonator.com/emoji/blobcat/ablobcatrainbow.png" />';
expect(result.display_name_html).toBe(expected);
expect(result.display_name_html).toContain('emojione');
});
it('emojifies note as `note_emojified`', () => {
const account = require('soapbox/__fixtures__/account-with-emojis.json');
const result = normalizeAccount(account);
const expected = 'I create Fediverse software that empowers people online. <img draggable="false" class="emojione" alt=":soapbox:" title=":soapbox:" src="https://gleasonator.com/emoji/Gleasonator/soapbox.png" /><br/><br/>I&#39;m vegan btw<br/><br/>Note: If you have a question for me, please tag me publicly. This gives the opportunity for others to chime in, and bystanders to learn.';
expect(result.note_emojified).toBe(expected);
expect(result.note_emojified).toContain('emojione');
});
it('unescapes HTML note as `note_plain`', () => {
@ -154,8 +152,8 @@ describe('normalizeAccount()', () => {
const result = normalizeAccount(account);
const field = result.fields.get(1);
expect(field?.name_emojified).toBe('Soapbox <img draggable="false" class="emojione" alt=":ablobcatrainbow:" title=":ablobcatrainbow:" src="https://gleasonator.com/emoji/blobcat/ablobcatrainbow.png" />');
expect(field?.value_emojified).toBe('<a href="https://soapbox.pub" rel="ugc">https://soapbox.pub</a> <img draggable="false" class="emojione" alt=":soapbox:" title=":soapbox:" src="https://gleasonator.com/emoji/Gleasonator/soapbox.png" />');
expect(field?.name_emojified).toContain('emojione');
expect(field?.value_emojified).toContain('emojione');
expect(field?.value_plain).toBe('https://soapbox.pub :soapbox:');
});

Wyświetl plik

@ -38,7 +38,7 @@ describe('normalizePoll()', () => {
// Emojifies poll options
expect(result.options.get(1)?.title_emojified)
.toEqual('Custom emoji <img draggable="false" class="emojione" alt=":gleason_excited:" title=":gleason_excited:" src="https://gleasonator.com/emoji/gleason_emojis/gleason_excited.png" /> ');
.toContain('emojione');
// Parses emojis as Immutable.Record's
expect(ImmutableRecord.isRecord(result.emojis.get(0))).toBe(true);

Wyświetl plik

@ -183,7 +183,7 @@ describe('normalizeStatus()', () => {
// Emojifies poll options
expect(poll.options.get(1)?.title_emojified)
.toEqual('Custom emoji <img draggable="false" class="emojione" alt=":gleason_excited:" title=":gleason_excited:" src="https://gleasonator.com/emoji/gleason_emojis/gleason_excited.png" /> ');
.toContain('emojione');
// Parses emojis as Immutable.Record's
expect(ImmutableRecord.isRecord(poll.emojis.get(0))).toBe(true);

Wyświetl plik

@ -118,7 +118,7 @@ describe('statuses reducer', () => {
const status = require('soapbox/__fixtures__/status-custom-emoji.json');
const action = { type: STATUS_IMPORT, status };
const expected = 'Hello <img draggable="false" class="emojione" alt=":ablobcathyper:" title=":ablobcathyper:" src="https://gleasonator.com/emoji/blobcat/ablobcathyper.png"> <img draggable="false" class="emojione" alt=":ageblobcat:" title=":ageblobcat:" src="https://gleasonator.com/emoji/blobcat/ageblobcat.png"> <img draggable="false" class="emojione" alt="😂" title=":joy:" src="/packs/emoji/1f602.svg"> world <img draggable="false" class="emojione" alt="😋" title=":yum:" src="/packs/emoji/1f60b.svg"> test <img draggable="false" class="emojione" alt=":blobcatphoto:" title=":blobcatphoto:" src="https://gleasonator.com/emoji/blobcat/blobcatphoto.png">';
const expected = 'Hello <img draggable="false" class="emojione custom-emoji" alt=":ablobcathyper:" title=":ablobcathyper:" src="https://gleasonator.com/emoji/blobcat/ablobcathyper.png" data-original="https://gleasonator.com/emoji/blobcat/ablobcathyper.png" data-static="https://gleasonator.com/emoji/blobcat/ablobcathyper.png"> <img draggable="false" class="emojione custom-emoji" alt=":ageblobcat:" title=":ageblobcat:" src="https://gleasonator.com/emoji/blobcat/ageblobcat.png" data-original="https://gleasonator.com/emoji/blobcat/ageblobcat.png" data-static="https://gleasonator.com/emoji/blobcat/ageblobcat.png"> <img draggable="false" class="emojione" alt="😂" title=":joy:" src="/packs/emoji/1f602.svg"> world <img draggable="false" class="emojione" alt="😋" title=":yum:" src="/packs/emoji/1f60b.svg"> test <img draggable="false" class="emojione custom-emoji" alt=":blobcatphoto:" title=":blobcatphoto:" src="https://gleasonator.com/emoji/blobcat/blobcatphoto.png" data-original="https://gleasonator.com/emoji/blobcat/blobcatphoto.png" data-static="https://gleasonator.com/emoji/blobcat/blobcatphoto.png">';
const result = reducer(undefined, action).getIn(['AGm7uC9DaAIGUa4KYK', 'contentHtml']);
expect(result).toBe(expected);