diff --git a/backend/package.json b/backend/package.json index 8555e4196..27a9df058 100644 --- a/backend/package.json +++ b/backend/package.json @@ -100,6 +100,7 @@ "slug": "~1.1.0", "trunc-html": "~1.1.2", "uuid": "~3.3.3", + "xregexp": "^4.2.4", "wait-on": "~3.3.0" }, "devDependencies": { @@ -116,7 +117,7 @@ "chai": "~4.2.0", "cucumber": "~5.1.0", "eslint": "~6.3.0", - "eslint-config-prettier": "~6.1.0", + "eslint-config-prettier": "~6.2.0", "eslint-config-standard": "~14.1.0", "eslint-plugin-import": "~2.18.2", "eslint-plugin-jest": "~22.16.0", @@ -126,8 +127,8 @@ "eslint-plugin-standard": "~4.0.1", "graphql-request": "~1.8.2", "jest": "~24.9.0", - "nodemon": "~1.19.1", + "nodemon": "~1.19.2", "prettier": "~1.18.2", "supertest": "~4.0.2" } -} +} \ No newline at end of file diff --git a/backend/src/middleware/hashtags/extractHashtags.js b/backend/src/middleware/hashtags/extractHashtags.js index fd6613065..c7782e59d 100644 --- a/backend/src/middleware/hashtags/extractHashtags.js +++ b/backend/src/middleware/hashtags/extractHashtags.js @@ -1,17 +1,18 @@ import cheerio from 'cheerio' +import { exec, build } from 'xregexp/xregexp-all.js' // formats of a Hashtag: // https://en.wikipedia.org/w/index.php?title=Hashtag&oldid=905141980#Style // here: // 0. Search for whole string. -// 1. Hashtag has only 'a-z', 'A-Z', and '0-9'. -// 2. If it starts with a digit '0-9' than 'a-z', or 'A-Z' has to follow. -const ID_REGEX = /^\/search\/hashtag\/(([a-zA-Z]+[a-zA-Z0-9]*)|([0-9]+[a-zA-Z]+[a-zA-Z0-9]*))$/g +// 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]*))$') 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 es well, so we search for the 'a' html-tag. + // 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') .map((_, el) => { return $(el).attr('href') @@ -19,8 +20,8 @@ export default function(content) { .get() const hashtags = [] urls.forEach(url => { - let match - while ((match = ID_REGEX.exec(url)) != null) { + const match = exec(url, regX) + if (match != null) { hashtags.push(match[1]) } }) diff --git a/backend/src/middleware/hashtags/extractHashtags.spec.js b/backend/src/middleware/hashtags/extractHashtags.spec.js index eb581d8f5..2e1761718 100644 --- a/backend/src/middleware/hashtags/extractHashtags.spec.js +++ b/backend/src/middleware/hashtags/extractHashtags.spec.js @@ -28,9 +28,14 @@ describe('extractHashtags', () => { }) 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 = - '

Something inspirational about #AbcDefXyz0123456789!*(),2, #0123456789, #0123456789a and #AbcDefXyz0123456789.

' - expect(extractHashtags(content)).toEqual(['0123456789a', 'AbcDefXyz0123456789']) + '

Something inspirational about #AbcDefXyz0123456789!*(),2, #0123456789, #0123456789a, #AbcDefXyz0123456789, and #λαπ.

' + expect(extractHashtags(content).sort()).toEqual([ + '0123456789a', + 'AbcDefXyz0123456789', + 'λαπ', + ]) }) }) diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js index ad088d4aa..4fe21f92a 100644 --- a/backend/src/schema/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -4,6 +4,7 @@ import Factory from '../../seed/factories' import { gql } from '../../jest/helpers' import { createTestClient } from 'apollo-server-testing' import createServer, { context } from '../../server' +import encode from '../../jwt/encode' const factory = Factory() let query @@ -12,16 +13,9 @@ let variables let req let user -// This is a bearer token of a user with id `u3`: -const userBearerToken = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsIm5hbWUiOiJKZW5ueSBSb3N0b2NrIiwiZGlzYWJsZWQiOmZhbHNlLCJhdmF0YXIiOiJodHRwczovL3MzLmFtYXpvbmF3cy5jb20vdWlmYWNlcy9mYWNlcy90d2l0dGVyL2tleXVyaTg1LzEyOC5qcGciLCJpZCI6InUzIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUub3JnIiwic2x1ZyI6Implbm55LXJvc3RvY2siLCJpYXQiOjE1Njc0NjgyMDIsImV4cCI6MTU2NzU1NDYwMiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0MDAwIiwic3ViIjoidTMifQ.RkmrdJDL1kIqGnMWUBl_sJJ4grzfpTEGdT6doMsbLW8' - -// This is a bearer token of a user with id `u2`: -const moderatorBearerToken = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoibW9kZXJhdG9yIiwibmFtZSI6IkJvYiBkZXIgQmF1bWVpc3RlciIsImRpc2FibGVkIjpmYWxzZSwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9hbmRyZXdvZmZpY2VyLzEyOC5qcGciLCJpZCI6InUyIiwiZW1haWwiOiJtb2RlcmF0b3JAZXhhbXBsZS5vcmciLCJzbHVnIjoiYm9iLWRlci1iYXVtZWlzdGVyIiwiaWF0IjoxNTY3NDY4MDUwLCJleHAiOjE1Njc1NTQ0NTAsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMCIsInN1YiI6InUyIn0.LdVFPKqIcoY0a7_kFZSTgnc8NzmZD7CrR3vkWLSqedM' - const disable = async id => { await factory.create('User', { id: 'u2', role: 'moderator' }) + const moderatorBearerToken = encode({ id: 'u2' }) req = { headers: { authorization: `Bearer ${moderatorBearerToken}` } } await mutate({ mutation: gql` @@ -74,6 +68,7 @@ describe('isLoggedIn', () => { describe('authenticated', () => { beforeEach(async () => { user = await factory.create('User', { id: 'u3' }) + const userBearerToken = encode({ id: 'u3' }) req = { headers: { authorization: `Bearer ${userBearerToken}` } } }) @@ -139,6 +134,7 @@ describe('currentUser', () => { slug: 'matilde-hermiston', role: 'user', }) + const userBearerToken = encode({ id: 'u3' }) req = { headers: { authorization: `Bearer ${userBearerToken}` } } }) @@ -276,6 +272,7 @@ describe('change password', () => { describe('authenticated', () => { beforeEach(async () => { await factory.create('User', { id: 'u3' }) + const userBearerToken = encode({ id: 'u3' }) req = { headers: { authorization: `Bearer ${userBearerToken}` } } }) describe('old password === new password', () => { diff --git a/backend/yarn.lock b/backend/yarn.lock index 526d40eb1..aed8c1f55 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -681,7 +681,7 @@ pirates "^4.0.0" source-map-support "^0.5.9" -"@babel/runtime-corejs2@^7.5.5": +"@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.5.5.tgz#c3214c08ef20341af4187f1c9fbdc357fbec96b2" integrity sha512-FYATQVR00NSNi7mUfpPDp7E8RYMXDuO8gaix7u/w3GekfUinKgX1AcTxs7SoiEmoEW9mbpjrwqWSW6zCmw5h8A== @@ -3283,10 +3283,10 @@ escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@~6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.1.0.tgz#e6f678ba367fbd1273998d5510f76f004e9dce7b" - integrity sha512-k9fny9sPjIBQ2ftFTesJV21Rg4R/7a7t7LCtZVrYQiHEp8Nnuk3EGaDmsKSAnsPj0BYcgB2zxzHa2NTkIxcOLg== +eslint-config-prettier@~6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.2.0.tgz#80e0b8714e3f6868c4ac2a25fbf39c02e73527a7" + integrity sha512-VLsgK/D+S/FEsda7Um1+N8FThec6LqE3vhcMyp8mlmto97y3fGf3DX7byJexGuOb1QY0Z/zz222U5t+xSfcZDQ== dependencies: get-stdin "^6.0.0" @@ -6300,10 +6300,10 @@ nodemailer@^6.3.0: resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.0.tgz#a89b0c62d3937bdcdeecbf55687bd7911b627e12" integrity sha512-TEHBNBPHv7Ie/0o3HXnb7xrPSSQmH1dXwQKRaMKDBGt/ZN54lvDVujP6hKkO/vjkIYL9rK8kHSG11+G42Nhxuw== -nodemon@~1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.1.tgz#576f0aad0f863aabf8c48517f6192ff987cd5071" - integrity sha512-/DXLzd/GhiaDXXbGId5BzxP1GlsqtMGM9zTmkWrgXtSqjKmGSbLicM/oAy4FR0YWm14jCHRwnR31AHS2dYFHrg== +nodemon@~1.19.2: + version "1.19.2" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.2.tgz#b0975147dc99b3761ceb595b3f9277084931dcc0" + integrity sha512-hRLYaw5Ihyw9zK7NF+9EUzVyS6Cvgc14yh8CAYr38tPxJa6UrOxwAQ351GwrgoanHCF0FalQFn6w5eoX/LGdJw== dependencies: chokidar "^2.1.5" debug "^3.1.0" @@ -8862,6 +8862,13 @@ xmldom@0.1.19: resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc" integrity sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw= +xregexp@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.2.4.tgz#02a4aea056d65a42632c02f0233eab8e4d7e57ed" + integrity sha512-sO0bYdYeJAJBcJA8g7MJJX7UrOZIfJPd8U2SC7B2Dd/J24U0aQNoGp33shCaBSWeb0rD5rh6VBUIXOkGal1TZA== + dependencies: + "@babel/runtime-corejs2" "^7.2.0" + xtend@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql index 6fad4218d..f09b5ad71 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions/contributions.cql @@ -125,7 +125,6 @@ [ ] wasSeeded: { type: Boolean } } */ - CALL apoc.load.json("file:${IMPORT_CHUNK_PATH_CQL_FILE}") YIELD value as post MERGE (p:Post {id: post._id["$oid"]}) ON CREATE SET @@ -148,6 +147,10 @@ MATCH (c:Category {id: categoryId}) MERGE (p)-[:CATEGORIZED]->(c) WITH p, post.tags AS tags UNWIND tags AS tag -MERGE (t:Tag {id: apoc.text.clean(tag), disabled: false, deleted: false}) +WITH apoc.text.replace(tag, '[^\\p{L}0-9]', '') as tagNoSpacesAllowed +CALL apoc.when(tagNoSpacesAllowed =~ '^((\\p{L}+[\\p{L}0-9]*)|([0-9]+\\p{L}+[\\p{L}0-9]*))$', 'RETURN tagNoSpacesAllowed', '', {tagNoSpacesAllowed: tagNoSpacesAllowed}) +YIELD value as validated +WHERE validated.tagNoSpacesAllowed IS NOT NULL +MERGE (t:Tag { id: validated.tagNoSpacesAllowed, disabled: false, deleted: false }) MERGE (p)-[:TAGGED]->(t) ; diff --git a/webapp/components/Editor/Editor.vue b/webapp/components/Editor/Editor.vue index 27a94e6b6..75f550c2a 100644 --- a/webapp/components/Editor/Editor.vue +++ b/webapp/components/Editor/Editor.vue @@ -26,6 +26,7 @@ import { Editor, EditorContent } from 'tiptap' import { History } from 'tiptap-extensions' import linkify from 'linkify-it' import stringHash from 'string-hash' +import { replace, build } from 'xregexp/xregexp-all.js' import * as key from '../../constants/keycodes' import { HASHTAG, MENTION } from '../../constants/editor' @@ -214,8 +215,9 @@ export default { }, sanitizeQuery(query) { if (this.suggestionType === HASHTAG) { - // remove all not allowed chars - query = query.replace(/[^a-zA-Z0-9]/gm, '') + // remove all non unicode letters and non digits + const regexMatchAllNonUnicodeLettersOrDigits = build('[^\\pL0-9]') + query = replace(query, regexMatchAllNonUnicodeLettersOrDigits, '', 'all') // if the query is only made of digits, make it empty return query.replace(/[0-9]/gm, '') === '' ? '' : query } diff --git a/webapp/components/Editor/SuggestionList.vue b/webapp/components/Editor/SuggestionList.vue index b351e6b74..3d480d187 100644 --- a/webapp/components/Editor/SuggestionList.vue +++ b/webapp/components/Editor/SuggestionList.vue @@ -7,15 +7,15 @@ :class="{ 'is-selected': navigatedItemIndex === index }" @click="selectItem(item)" > - {{ createItemLabel(item) }} + {{ createItemLabel(item) | truncate(50) }}