content warnings can have emoji (#122)

fixes #105
This commit is contained in:
Nolan Lawson 2018-04-14 15:50:06 -07:00 committed by GitHub
parent a6e737bdbb
commit 6230c2703e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 61 additions and 16 deletions

View file

@ -39,6 +39,7 @@
"compression": "^1.7.1", "compression": "^1.7.1",
"cross-env": "^5.1.3", "cross-env": "^5.1.3",
"css-loader": "^0.28.7", "css-loader": "^0.28.7",
"escape-html": "^1.0.3",
"esm": "^3.0.12", "esm": "^3.0.12",
"events": "^2.0.0", "events": "^2.0.0",
"express": "^4.16.2", "express": "^4.16.2",

View file

@ -58,10 +58,10 @@
</style> </style>
<script> <script>
import { replaceAll } from '../../_utils/strings'
import { mark, stop } from '../../_utils/marks' import { mark, stop } from '../../_utils/marks'
import { store } from '../../_store/store' import { store } from '../../_store/store'
import { classname } from '../../_utils/classname' import { classname } from '../../_utils/classname'
import { emojifyText } from '../../_utils/emojifyText'
export default { export default {
oncreate() { oncreate() {
@ -79,20 +79,9 @@
}, },
massagedContent: (originalStatus, $autoplayGifs) => { massagedContent: (originalStatus, $autoplayGifs) => {
let content = originalStatus.content let content = originalStatus.content
// emojify
if (originalStatus.emojis && originalStatus.emojis.length) { content = emojifyText(content, originalStatus.emojis, $autoplayGifs)
for (let emoji of originalStatus.emojis) {
let { shortcode, url, static_url } = emoji
let urlToUse = $autoplayGifs ? url : static_url
let shortcodeWithColons = `:${shortcode}:`
content = replaceAll(
content,
shortcodeWithColons,
`<img class="status-emoji" draggable="false" src="${urlToUse}"
alt="${shortcodeWithColons}" title="${shortcodeWithColons}" />`
)
}
}
// GNU Social and Pleroma don't add <p> tags // GNU Social and Pleroma don't add <p> tags
if (!content.startsWith('<p>')) { if (!content.startsWith('<p>')) {
content = `<p>${content}</p>` content = `<p>${content}</p>`

View file

@ -1,5 +1,5 @@
<div class="status-spoiler {{isStatusInNotification ? 'status-in-notification' : ''}} {{isStatusInOwnThread ? 'status-in-own-thread' : ''}}"> <div class="status-spoiler {{isStatusInNotification ? 'status-in-notification' : ''}} {{isStatusInOwnThread ? 'status-in-own-thread' : ''}}">
<p>{{originalStatus.spoiler_text}}</p> <p>{{{massagedSpoilerText}}}</p>
</div> </div>
<div class="status-spoiler-button {{isStatusInOwnThread ? 'status-in-own-thread' : ''}}"> <div class="status-spoiler-button {{isStatusInOwnThread ? 'status-in-own-thread' : ''}}">
<button type="button" delegate-key="{{delegateKey}}"> <button type="button" delegate-key="{{delegateKey}}">
@ -16,6 +16,12 @@
margin: 10px 5px; margin: 10px 5px;
} }
:global(.status-spoiler .status-emoji) {
width: 20px;
height: 20px;
margin: -3px 0;
}
.status-spoiler.status-in-own-thread { .status-spoiler.status-in-own-thread {
font-size: 1.3em; font-size: 1.3em;
margin: 20px 5px 10px; margin: 20px 5px 10px;
@ -42,6 +48,8 @@
import { store } from '../../_store/store' import { store } from '../../_store/store'
import { registerClickDelegate, unregisterClickDelegate } from '../../_utils/delegate' import { registerClickDelegate, unregisterClickDelegate } from '../../_utils/delegate'
import { mark, stop } from '../../_utils/marks' import { mark, stop } from '../../_utils/marks'
import { emojifyText } from '../../_utils/emojifyText'
import escapeHtml from 'escape-html'
export default { export default {
oncreate() { oncreate() {
@ -52,6 +60,11 @@
}, },
store: () => store, store: () => store,
computed: { computed: {
spoilerText: (originalStatus) => originalStatus.spoiler_text,
massagedSpoilerText: (spoilerText, originalStatus, $autoplayGifs) => {
spoilerText = escapeHtml(spoilerText)
return emojifyText(spoilerText, originalStatus.emojis, $autoplayGifs)
},
delegateKey: (uuid) => `spoiler-${uuid}` delegateKey: (uuid) => `spoiler-${uuid}`
}, },
methods: { methods: {

View file

@ -0,0 +1,17 @@
import { replaceAll } from './strings'
export function emojifyText (text, emojis, autoplayGifs) {
if (emojis && emojis.length) {
for (let emoji of emojis) {
let urlToUse = autoplayGifs ? emoji.url : emoji.static_url
let shortcodeWithColons = `:${emoji.shortcode}:`
text = replaceAll(
text,
shortcodeWithColons,
`<img class="status-emoji" draggable="false" src="${urlToUse}"
alt="${shortcodeWithColons}" title="${shortcodeWithColons}" />`
)
}
}
return text
}

View file

@ -32,3 +32,28 @@ test('content warnings are not posted if removed', async t => {
.expect(getNthStatus(0).innerText).notContains('content warning!') .expect(getNthStatus(0).innerText).notContains('content warning!')
.expect(getNthStatus(0).find('.status-content').innerText).contains('hi this is another toot') .expect(getNthStatus(0).find('.status-content').innerText).contains('hi this is another toot')
}) })
test('content warnings can have emoji', async t => {
await t.useRole(foobarRole)
.typeText(composeInput, 'I can: :blobnom:')
.click(contentWarningButton)
.typeText(composeContentWarning, 'can you feel the :blobpats: tonight')
.click(composeButton)
.expect(getNthStatus(0).innerText).contains('can you feel the', {timeout: 30000})
.expect(getNthStatus(0).find('.status-spoiler img.status-emoji').getAttribute('alt')).eql(':blobpats:')
.click(getNthShowOrHideButton(0))
.expect(getNthStatus(0).find('.status-content img.status-emoji').getAttribute('alt')).eql(':blobnom:')
})
test('no XSS in content warnings or text', async t => {
let pwned1 = `<script>alert("pwned!")</script>`
let pwned2 = `<script>alert("pwned from CW!")</script>`
await t.useRole(foobarRole)
.typeText(composeInput, pwned1)
.click(contentWarningButton)
.typeText(composeContentWarning, pwned2)
.click(composeButton)
.expect(getNthStatus(0).find('.status-spoiler').innerText).contains(pwned2)
.click(getNthShowOrHideButton(0))
.expect(getNthStatus(0).find('.status-content').innerText).contains(pwned1)
})