mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-01-17 10:21:25 +00:00
Make data-hashtag-id authoritive for #links
This adds consistency: The mention links with `@` was implemented that way already. Instead of parsing the URL, we add some redundancy and add another attribute: data-hashtag-id So, what characters are valid for html attributes? Read: https://stackoverflow.com/questions/925994/what-characters-are-allowed-in-an-html-attribute-name Thanks to @Tirokk, who added some validations on the hahstag ids, I think we are all set. If you try to write a hashtag with a `"` double quotation mark for example, it gets automatically replaced with a valid hashtag. If someone wants to send us invalid hashtag ids to the backend directly, the regex there would filter it out.
This commit is contained in:
parent
15816dd97b
commit
431de3319f
@ -6,21 +6,21 @@ import { exec, build } from 'xregexp/xregexp-all.js'
|
||||
// 0. Search for whole string.
|
||||
// 1. Hashtag has only all unicode characters and '0-9'.
|
||||
// 2. If it starts with a digit '0-9' than a unicode character has to follow.
|
||||
const regX = build('^/search/hashtag/((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$')
|
||||
const regX = build('^((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$')
|
||||
|
||||
export default function(content) {
|
||||
if (!content) return []
|
||||
const $ = cheerio.load(content)
|
||||
// We can not search for class '.hashtag', because the classes are removed at the 'xss' middleware.
|
||||
// But we have to know, which Hashtags are removed from the content as well, so we search for the 'a' html-tag.
|
||||
const urls = $('a')
|
||||
const ids = $('a[data-hashtag-id]')
|
||||
.map((_, el) => {
|
||||
return $(el).attr('href')
|
||||
return $(el).attr('data-hashtag-id')
|
||||
})
|
||||
.get()
|
||||
const hashtags = []
|
||||
urls.forEach(url => {
|
||||
const match = exec(decodeURI(url), regX)
|
||||
ids.forEach(id => {
|
||||
const match = exec(id, regX)
|
||||
if (match != null) {
|
||||
hashtags.push(match[1])
|
||||
}
|
||||
|
||||
@ -8,9 +8,24 @@ describe('extractHashtags', () => {
|
||||
})
|
||||
|
||||
describe('searches through links', () => {
|
||||
it('finds links with and without ".hashtag" class and extracts Hashtag names', () => {
|
||||
const content =
|
||||
'<p><a class="hashtag" href="/search/hashtag/Elections">#Elections</a><a href="/search/hashtag/Democracy">#Democracy</a></p>'
|
||||
it('not `class="hashtag"` but `data-hashtag-id="something"` makes a link a hashtag link', () => {
|
||||
const content = `
|
||||
<p>
|
||||
<a
|
||||
class="hashtag"
|
||||
data-hashtag-id="Elections"
|
||||
href="/?hashtag=Elections"
|
||||
>
|
||||
#Elections
|
||||
</a>
|
||||
<a
|
||||
data-hashtag-id="Democracy"
|
||||
href="/?hashtag=Democracy"
|
||||
>
|
||||
#Democracy
|
||||
</a>
|
||||
</p>
|
||||
`
|
||||
expect(extractHashtags(content)).toEqual(['Elections', 'Democracy'])
|
||||
})
|
||||
|
||||
@ -20,23 +35,57 @@ describe('extractHashtags', () => {
|
||||
expect(extractHashtags(content)).toEqual([])
|
||||
})
|
||||
|
||||
describe('handles links', () => {
|
||||
it('ignores links with domains', () => {
|
||||
const content =
|
||||
'<p><a class="hashtag" href="http://localhost:3000/search/hashtag/Elections">#Elections</a><a href="/search/hashtag/Democracy">#Democracy</a></p>'
|
||||
expect(extractHashtags(content)).toEqual(['Democracy'])
|
||||
})
|
||||
|
||||
it('ignores Hashtag links with not allowed character combinations and handles `encodeURI` URLs', () => {
|
||||
// Allowed are all unicode letters '\pL' and all digits '0-9'. There haveto be at least one letter in it.
|
||||
const content =
|
||||
'<p>Something inspirational about <a href="/search/hashtag/AbcDefXyz0123456789!*(),2" class="hashtag" target="_blank">#AbcDefXyz0123456789!*(),2</a>, <a href="/search/hashtag/0123456789" class="hashtag" target="_blank">#0123456789</a>, <a href="/search/hashtag/0123456789a" class="hashtag" target="_blank">#0123456789a</a>, <a href="/search/hashtag/AbcDefXyz0123456789" target="_blank">#AbcDefXyz0123456789</a>, and <a href="/search/hashtag/%C4%A7%CF%80%CE%B1%CE%BB" target="_blank">#ħπαλ</a>.</p>'
|
||||
expect(extractHashtags(content).sort()).toEqual([
|
||||
'0123456789a',
|
||||
'AbcDefXyz0123456789',
|
||||
'ħπαλ',
|
||||
])
|
||||
})
|
||||
it('ignores hashtag links with not allowed character combinations', () => {
|
||||
// Allowed are all unicode letters '\pL' and all digits '0-9'. There haveto be at least one letter in it.
|
||||
const content = `
|
||||
<p>
|
||||
Something inspirational about
|
||||
<a
|
||||
href="/?hashtag=AbcDefXyz0123456789!*(),2"
|
||||
data-hashtag-id="AbcDefXyz0123456789!*(),2"
|
||||
class="hashtag"
|
||||
target="_blank"
|
||||
>
|
||||
#AbcDefXyz0123456789!*(),2
|
||||
</a>,
|
||||
<a
|
||||
href="/?hashtag=0123456789"
|
||||
data-hashtag-id="0123456789"
|
||||
class="hashtag"
|
||||
target="_blank"
|
||||
>
|
||||
#0123456789
|
||||
</a>,
|
||||
<a href="?hashtag=0123456789a"
|
||||
data-hashtag-id="0123456789a"
|
||||
class="hashtag"
|
||||
target="_blank"
|
||||
>
|
||||
#0123456789a
|
||||
</a>,
|
||||
<a
|
||||
href="/?hashtag=AbcDefXyz0123456789"
|
||||
data-hashtag-id="AbcDefXyz0123456789"
|
||||
class="hashtag"
|
||||
target="_blank"
|
||||
>
|
||||
#AbcDefXyz0123456789
|
||||
</a>, and
|
||||
<a
|
||||
href="/?hashtag=%C4%A7%CF%80%CE%B1%CE%BB"
|
||||
data-hashtag-id="ħπαλ"
|
||||
class="hashtag"
|
||||
target="_blank"
|
||||
>
|
||||
#ħπαλ
|
||||
</a>.
|
||||
</p>
|
||||
`
|
||||
expect(extractHashtags(content).sort()).toEqual([
|
||||
'0123456789a',
|
||||
'AbcDefXyz0123456789',
|
||||
'ħπαλ',
|
||||
])
|
||||
})
|
||||
|
||||
describe('does not crash if', () => {
|
||||
|
||||
@ -69,8 +69,27 @@ afterEach(async () => {
|
||||
describe('hashtags', () => {
|
||||
const id = 'p135'
|
||||
const title = 'Two Hashtags'
|
||||
const postContent =
|
||||
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Democracy">#Democracy</a> should work equal for everybody!? That seems to be the only way to have equal <a class="hashtag" href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||
const postContent = `
|
||||
<p>
|
||||
Hey Dude,
|
||||
<a
|
||||
class="hashtag"
|
||||
data-hashtag-id="Democracy"
|
||||
href="/?hashtag=Democracy">
|
||||
#Democracy
|
||||
</a>
|
||||
should work equal for everybody!? That seems to be the only way to have
|
||||
equal
|
||||
<a
|
||||
class="hashtag"
|
||||
data-hashtag-id="Liberty"
|
||||
href="/?hashtag=Liberty"
|
||||
>
|
||||
#Liberty
|
||||
</a>
|
||||
for everyone.
|
||||
</p>
|
||||
`
|
||||
const postWithHastagsQuery = gql`
|
||||
query($id: ID) {
|
||||
Post(id: $id) {
|
||||
@ -131,8 +150,27 @@ describe('hashtags', () => {
|
||||
|
||||
describe('updates the Post by removing, keeping and adding one hashtag respectively', () => {
|
||||
// The already existing hashtag has no class at this point.
|
||||
const postContent =
|
||||
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Elections">#Elections</a> should work equal for everybody!? That seems to be the only way to have equal <a href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||
const postContent = `
|
||||
<p>
|
||||
Hey Dude,
|
||||
<a
|
||||
class="hashtag"
|
||||
data-hashtag-id="Elections"
|
||||
href="?hashtag=Elections"
|
||||
>
|
||||
#Elections
|
||||
</a>
|
||||
should work equal for everybody!? That seems to be the only way to
|
||||
have equal
|
||||
<a
|
||||
data-hashtag-id="Liberty"
|
||||
href="?hashtag=Liberty"
|
||||
>
|
||||
#Liberty
|
||||
</a>
|
||||
for everyone.
|
||||
</p>
|
||||
`
|
||||
|
||||
it('only one previous Hashtag and the new Hashtag exists', async () => {
|
||||
await mutate({
|
||||
|
||||
@ -413,9 +413,9 @@ import { gql } from '../jest/helpers'
|
||||
const mention2 =
|
||||
'Hey <a class="mention" data-mention-id="u3" href="/profile/u3">@jenny-rostock</a>, here is another notification for you!'
|
||||
const hashtag1 =
|
||||
'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!'
|
||||
'See <a class="hashtag" data-hashtag-id="NaturphilosophieYoga" href="/?hashtag=NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!'
|
||||
const hashtagAndMention1 =
|
||||
'The new physics of <a class="hashtag" href="/search/hashtag/QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" href="/search/hashtag/QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
|
||||
'The new physics of <a class="hashtag" data-hashtag-id="QuantenFlussTheorie" href="/?hashtag=QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" data-hashtag-id="QuantumGravity" href="/?hashtag=QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
|
||||
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
|
||||
@ -21,11 +21,15 @@ export default class Hashtag extends TipTapMention {
|
||||
return {
|
||||
...super.schema,
|
||||
toDOM: node => {
|
||||
// use a dummy domain because URL cannot handle relative urls
|
||||
const url = new URL('/', 'http://example.org')
|
||||
url.searchParams.append('hashtag', node.attrs.id)
|
||||
|
||||
return [
|
||||
'a',
|
||||
{
|
||||
class: this.options.mentionClass,
|
||||
href: `/search/hashtag/${encodeURI(node.attrs.id)}`,
|
||||
href: `/${url.search}`,
|
||||
'data-hashtag-id': node.attrs.id,
|
||||
target: '_blank',
|
||||
},
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
import { exec, build } from 'xregexp/xregexp-all.js'
|
||||
|
||||
export default async ({ store, env, route, redirect }) => {
|
||||
let publicPages = env.publicPages
|
||||
// only affect non public pages
|
||||
if (publicPages.indexOf(route.name) >= 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const regX = build('^/search/hashtag/((\\pL+[\\pL0-9]*)|([0-9]+\\pL+[\\pL0-9]*))$')
|
||||
const matchHashtag = route.fullPath ? exec(decodeURI(route.fullPath), regX) : null
|
||||
|
||||
if (!matchHashtag) return true
|
||||
|
||||
return redirect(`/?hashtag=${encodeURI(matchHashtag[1])}`)
|
||||
}
|
||||
@ -123,7 +123,7 @@ export default {
|
||||
],
|
||||
|
||||
router: {
|
||||
middleware: ['authenticated', 'termsAndConditions', 'searchHashtag'],
|
||||
middleware: ['authenticated', 'termsAndConditions'],
|
||||
linkActiveClass: 'router-link-active',
|
||||
linkExactActiveClass: 'router-link-exact-active',
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
|
||||
@ -75,10 +75,7 @@ export default {
|
||||
MasonryGridItem,
|
||||
},
|
||||
data() {
|
||||
let { hashtag = null } = this.$route.query
|
||||
if (hashtag) {
|
||||
hashtag = decodeURI(hashtag)
|
||||
}
|
||||
const { hashtag = null } = this.$route.query
|
||||
return {
|
||||
posts: [],
|
||||
hasMore: true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user