diff --git a/packages/pl-fe/LICENSE-GPL-3.0 b/packages/pl-fe/LICENSE-GPL-3.0 new file mode 100644 index 000000000..e69de29bb diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 8e90799c3..eabfe8f0a 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -55,6 +55,7 @@ "@lexical/utils": "^0.23.1", "@mkljczk/lexical-remark": "^0.4.0", "@mkljczk/react-hotkeys": "^1.2.2", + "@mkljczk/url-purify": "^0.0.1", "@reach/combobox": "^0.18.0", "@reach/rect": "^0.18.0", "@reach/tabs": "^0.18.0", diff --git a/packages/pl-fe/src/components/parsed-content.tsx b/packages/pl-fe/src/components/parsed-content.tsx index 7d308ad01..f2bcf9207 100644 --- a/packages/pl-fe/src/components/parsed-content.tsx +++ b/packages/pl-fe/src/components/parsed-content.tsx @@ -8,6 +8,7 @@ import { Link } from 'react-router-dom'; import Emojify from 'pl-fe/features/emoji/emojify'; import { makeEmojiMap } from 'pl-fe/utils/normalizers'; +import Purify from 'pl-fe/utils/url-purify'; import HashtagLink from './hashtag-link'; import HoverAccountWrapper from './hover-account-wrapper'; @@ -62,12 +63,12 @@ const uniqueHashtagsWithCaseHandling = (hashtags: string[]) => { }; function parseContent(props: IParsedContent): ReturnType; -function parseContent(props: IParsedContent, extractHashtags: true, greentext: boolean): { +function parseContent(props: IParsedContent, extractHashtags: true, cleanUrls: boolean, greentext: boolean): { hashtags: Array; content: ReturnType; }; -function parseContent({ html, mentions, hasQuote, emojis }: IParsedContent, extractHashtags = false, greentext = false) { +function parseContent({ html, mentions, hasQuote, emojis }: IParsedContent, extractHashtags = false, cleanUrls = false, greentext = false) { if (html.length === 0) { return extractHashtags ? { content: null, hashtags: [] } : null; } @@ -108,8 +109,9 @@ function parseContent({ html, mentions, hasQuote, emojis }: IParsedContent, extr // eslint-disable-next-line jsx-a11y/no-static-element-interactions e.stopPropagation()} - rel='nofollow noopener' + rel='nofollow noopener noreferrer' target='_blank' title={domNode.attribs.href} > diff --git a/packages/pl-fe/src/components/status-content.tsx b/packages/pl-fe/src/components/status-content.tsx index bf6b4db11..b1f3999ce 100644 --- a/packages/pl-fe/src/components/status-content.tsx +++ b/packages/pl-fe/src/components/status-content.tsx @@ -82,7 +82,7 @@ const StatusContent: React.FC = React.memo(({ preview, withMedia, }) => { - const { displaySpoilers } = useSettings(); + const { cleanUrls, displaySpoilers } = useSettings(); const { greentext } = usePlFeConfig(); const [collapsed, setCollapsed] = useState(null); @@ -146,7 +146,7 @@ const StatusContent: React.FC = React.memo(({ mentions: status.mentions, hasQuote: !!status.quote_id, emojis: status.emojis, - }, true, greentext), [content]); + }, true, cleanUrls, greentext), [content]); useEffect(() => { setLineClamp(!spoilerNode.current || spoilerNode.current.clientHeight >= 96); diff --git a/packages/pl-fe/src/schemas/pl-fe/settings.ts b/packages/pl-fe/src/schemas/pl-fe/settings.ts index 446731377..289603159 100644 --- a/packages/pl-fe/src/schemas/pl-fe/settings.ts +++ b/packages/pl-fe/src/schemas/pl-fe/settings.ts @@ -37,6 +37,7 @@ const settingsSchema = v.object({ autoTranslate: v.fallback(v.boolean(), false), knownLanguages: v.fallback(v.array(v.string()), []), showWrenchButton: v.fallback(v.boolean(), false), + cleanUrls: v.fallback(v.boolean(), false), theme: v.fallback(v.optional(v.object({ brandColor: v.fallback(v.string(), ''), diff --git a/packages/pl-fe/src/utils/url-purify.ts b/packages/pl-fe/src/utils/url-purify.ts new file mode 100644 index 000000000..a76cdac13 --- /dev/null +++ b/packages/pl-fe/src/utils/url-purify.ts @@ -0,0 +1,93 @@ +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// I hope I got this relicensing stuff right xD +import { URLPurify, type SerializedRules } from '@mkljczk/url-purify'; + +// Adapted from ClearURLs Rules +// https://github.com/ClearURLs/Rules/blob/master/data.min.json +// Licensed under the LGPL-3.0 license. +const DEFAULT_RULESET: SerializedRules = { + providers: { + globalRules: { + urlPattern: '.*', + rules: [ + '(?:%3F)?utm(?:_[a-z_]*)?', + '(?:%3F)?ga_[a-z_]+', + '(?:%3F)?yclid', + '(?:%3F)?_openstat', + '(?:%3F)?fb_action_(?:types|ids)', + '(?:%3F)?fb_(?:source|ref)', + '(?:%3F)?fbclid', + '(?:%3F)?action_(?:object|type|ref)_map', + '(?:%3F)?gs_l', + '(?:%3F)?mkt_tok', + '(?:%3F)?hmb_(?:campaign|medium|source)', + '(?:%3F)?gclid', + '(?:%3F)?srsltid', + '(?:%3F)?otm_[a-z_]*', + '(?:%3F)?cmpid', + '(?:%3F)?os_ehash', + '(?:%3F)?_ga', + '(?:%3F)?_gl', + '(?:%3F)?__twitter_impression', + '(?:%3F)?wt_?z?mc', + '(?:%3F)?wtrid', + '(?:%3F)?[a-z]?mc', + '(?:%3F)?dclid', + 'Echobox', + '(?:%3F)?spm', + '(?:%3F)?vn(?:_[a-z]*)+', + '(?:%3F)?tracking_source', + '(?:%3F)?ceneo_spo', + '(?:%3F)?itm_(?:campaign|medium|source)', + '(?:%3F)?__hsfp', + '(?:%3F)?__hssc', + '(?:%3F)?__hstc', + '(?:%3F)?_hsenc', + '(?:%3F)?__s', + '(?:%3F)?hsCtaTracking', + '(?:%3F)?mc_(?:eid|cid|tc)', + '(?:%3F)?ml_subscriber', + '(?:%3F)?ml_subscriber_hash', + '(?:%3F)?msclkid', + '(?:%3F)?oly_anon_id', + '(?:%3F)?oly_enc_id', + '(?:%3F)?rb_clickid', + '(?:%3F)?s_cid', + '(?:%3F)?vero_conv', + '(?:%3F)?vero_id', + '(?:%3F)?wickedid', + '(?:%3F)?twclid', + '(?:%3F)?ref_?', + '(?:%3F)?referrer', + ], + }, + youtube: { + urlPattern: '^https?:\\/\\/(?:[a-z0-9-]+\\.)*?(youtube\\.com|youtu\\.be)', + rules: ['feature', 'gclid', 'kw', 'si', 'pp'], + exceptions: [ + '^https?:\\/\\/(?:[a-z0-9-]+\\.)*?youtube\\.com\\/signin\\?.*?', + ], + redirections: [ + '^https?:\\/\\/(?:[a-z0-9-]+\\.)*?youtube\\.com\\/redirect?.*?q=([^&]*)', + ], + }, + }, +}; + +const Purify = new URLPurify({ + rulesFromMemory: DEFAULT_RULESET, +}); + +export default Purify; diff --git a/packages/pl-fe/yarn.lock b/packages/pl-fe/yarn.lock index 92c9a93a5..003183439 100644 --- a/packages/pl-fe/yarn.lock +++ b/packages/pl-fe/yarn.lock @@ -1727,6 +1727,11 @@ lodash "^4.17.21" mousetrap "^1.6.5" +"@mkljczk/url-purify@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@mkljczk/url-purify/-/url-purify-0.0.1.tgz#4406b43e9cc054c67ca48fd6d684c700e119c85c" + integrity sha512-7QmSb6+d5aGVBZqBP8XkQQ3Z97YQXo8tHW9hq99aZWW7M7ugIxvb6arNjRqA/CKgiTc3tQGfvAS0SGfu5zedOw== + "@nexusmods/eslint-plugin-nexusmods@^0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@nexusmods/eslint-plugin-nexusmods/-/eslint-plugin-nexusmods-0.0.7.tgz#2db1e8e50096480fd725d9978560c4c6d26c782d"