Skip to content
Open
679 changes: 679 additions & 0 deletions ACKNOWLEDGMENTS.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -6980,6 +6980,14 @@
"messageformat": "For example, :-) will be converted to <emojify>🙂</emojify>",
"description": "Description for the auto convert emoji setting"
},
"icu:Preferences__auto-remove-url-tracking--title": {
"messageformat": "Remove URL tracking parameters on paste",
"description": "Title for the auto remove URL tracking setting"
},
"icu:Preferences__auto-remove-url-tracking--description": {
"messageformat": "Automatically remove tracking parameters from a URL when pasting into the composition box",
"description": "Description for the auto remove URL tracking setting"
},
"icu:Preferences--advanced": {
"messageformat": "Advanced",
"description": "Title for advanced settings"
Expand Down
2 changes: 2 additions & 0 deletions ts/components/Preferences.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export default {
phoneNumber: '+1 555 123-4567',
hasAudioNotifications: true,
hasAutoConvertEmoji: true,
hasAutoRemoveUrlTracking: true,
hasAutoDownloadUpdate: true,
hasAutoLaunch: true,
hasCallNotifications: true,
Expand Down Expand Up @@ -387,6 +388,7 @@ export default {
makeSyncRequest: action('makeSyncRequest'),
onAudioNotificationsChange: action('onAudioNotificationsChange'),
onAutoConvertEmojiChange: action('onAutoConvertEmojiChange'),
onAutoRemoveUrlTrackingChange: action('onAutoRemoveUrlTrackingChange'),
onAutoDownloadAttachmentChange: action('onAutoDownloadAttachmentChange'),
onAutoDownloadUpdateChange: action('onAutoDownloadUpdateChange'),
onAutoLaunchChange: action('onAutoLaunchChange'),
Expand Down
17 changes: 17 additions & 0 deletions ts/components/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export type PropsDataType = {
emojiSkinToneDefault: EmojiSkinTone;
hasAudioNotifications?: boolean;
hasAutoConvertEmoji: boolean;
hasAutoRemoveUrlTracking: boolean;
hasAutoDownloadUpdate: boolean;
hasAutoLaunch: boolean | undefined;
hasCallNotifications: boolean;
Expand Down Expand Up @@ -273,6 +274,7 @@ type PropsFunctionType = {
// Change handlers
onAudioNotificationsChange: CheckboxChangeHandlerType;
onAutoConvertEmojiChange: CheckboxChangeHandlerType;
onAutoRemoveUrlTrackingChange: CheckboxChangeHandlerType;
onAutoDownloadAttachmentChange: (
setting: AutoDownloadAttachmentType
) => unknown;
Expand Down Expand Up @@ -390,6 +392,7 @@ export function Preferences({
getPreferredBadge,
hasAudioNotifications,
hasAutoConvertEmoji,
hasAutoRemoveUrlTracking,
hasAutoDownloadUpdate,
hasAutoLaunch,
hasCallNotifications,
Expand Down Expand Up @@ -433,6 +436,7 @@ export function Preferences({
notificationContent,
onAudioNotificationsChange,
onAutoConvertEmojiChange,
onAutoRemoveUrlTrackingChange,
onAutoDownloadAttachmentChange,
onAutoDownloadUpdateChange,
onAutoLaunchChange,
Expand Down Expand Up @@ -1167,6 +1171,19 @@ export function Preferences({
name="autoConvertEmoji"
onChange={onAutoConvertEmojiChange}
/>
<Checkbox
checked={hasAutoRemoveUrlTracking}
description={
<I18n
i18n={i18n}
id="icu:Preferences__auto-remove-url-tracking--description"
/>
}
label={i18n('icu:Preferences__auto-remove-url-tracking--title')}
moduleClassName="Preferences__checkbox"
name="autoRemoveUrlTracking"
onChange={onAutoRemoveUrlTrackingChange}
/>
<SettingsRow>
<Control
left={i18n('icu:Preferences__EmojiSkinToneDefaultSetting__Label')}
Expand Down
13 changes: 12 additions & 1 deletion ts/quill/signal-clipboard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { deleteRange } from '@signalapp/quill-cjs/modules/keyboard';
import { FormattingMenu, QuillFormattingStyle } from '../formatting/menu';
import { insertEmojiOps } from '../util';
import { createEventHandler } from './util';
import { applyAllRules } from '../../util/stripUrlTracking';
import { maybeParseUrl } from '../../util/url';

type ClipboardOptions = Readonly<{
isDisabled: boolean;
Expand Down Expand Up @@ -56,7 +58,7 @@ export class SignalClipboard {

const { clipboard } = this.quill;
const selection = this.quill.getSelection();
const text = event.clipboardData.getData('text/plain');
let text = event.clipboardData.getData('text/plain');
const signal = event.clipboardData.getData('text/signal');

const clipboardContainsFiles = event.clipboardData.files?.length > 0;
Expand All @@ -76,6 +78,15 @@ export class SignalClipboard {
return;
}

// If URL tracking parameter stripping is enabled, see if the pasted text is an HTTPS
// or HTTP URL. If so, strip the tracking parameters
if (text && window.storage.get('autoRemoveUrlTracking', true)) {
const url = maybeParseUrl(text);
if (url && (url.protocol === 'https:' || url.protocol === 'http:')) {
text = applyAllRules(url).toString();
}
}

const { ops } = this.quill.getContents(selection.index, selection.length);
// Only enable formatting on the pasted text if the entire selection has it enabled!
const formats =
Expand Down
4 changes: 4 additions & 0 deletions ts/state/smart/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,8 @@ export function SmartPreferences(): JSX.Element | null {
'autoConvertEmoji',
true
);
const [hasAutoRemoveUrlTracking, onAutoRemoveUrlTrackingChange] =
createItemsAccess('autoRemoveUrlTracking', true);
const [hasAutoDownloadUpdate, onAutoDownloadUpdateChange] = createItemsAccess(
'auto-download-update',
true
Expand Down Expand Up @@ -753,6 +755,7 @@ export function SmartPreferences(): JSX.Element | null {
getPreferredBadge={getPreferredBadge}
hasAudioNotifications={hasAudioNotifications}
hasAutoConvertEmoji={hasAutoConvertEmoji}
hasAutoRemoveUrlTracking={hasAutoRemoveUrlTracking}
hasAutoDownloadUpdate={hasAutoDownloadUpdate}
hasAutoLaunch={hasAutoLaunch}
hasCallNotifications={hasCallNotifications}
Expand Down Expand Up @@ -799,6 +802,7 @@ export function SmartPreferences(): JSX.Element | null {
notificationContent={notificationContent}
onAudioNotificationsChange={onAudioNotificationsChange}
onAutoConvertEmojiChange={onAutoConvertEmojiChange}
onAutoRemoveUrlTrackingChange={onAutoRemoveUrlTrackingChange}
onAutoDownloadAttachmentChange={onAutoDownloadAttachmentChange}
onAutoDownloadUpdateChange={onAutoDownloadUpdateChange}
onAutoLaunchChange={onAutoLaunchChange}
Expand Down
103 changes: 103 additions & 0 deletions ts/test-node/util/stripUrlTracking_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import { assert } from 'chai';
import { applyAllRules } from '../../util/stripUrlTracking';

const TEST_VECTORS: Array<Array<string>> = [
// All domains should have igshid stripped
[
'https://instagram.com/blasdflkjasdf?igshid=192u34oijserr',
'https://instagram.com/blasdflkjasdf',
],
[
'https://fake.invalid/blasdflkjasdf?igshid=192u34oijserr',
'https://fake.invalid/blasdflkjasdf',
],
// A `*` in the middle of a path rule should match almost anything, including a `/`
[
'https://luisaviaroma.com/_5@%foo/_@()!bar/p?x=y&lvrid=test',
'https://luisaviaroma.com/_5@%foo/_@()!bar/p?x=y',
],
// A `*` in the middle of a path rule should NOT match a `#`, since that indicates the
// start of the hash property
[
'https://luisaviaroma.com/_%@#()!bar/p?x=y&lvrid=test',
'https://luisaviaroma.com/_%@#()!bar/p?x=y&lvrid=test',
],
// Address rules that are just `*` should match anything at all
[
'https://fake.invalid/blasdflkjasdf?service_id=192u34oijserr',
'https://fake.invalid/blasdflkjasdf',
],
// It shouldn't matter where the param occurs
[
'https://fake.invalid/blasdflkjasdf?foo=bar&service_id=192u34oijserr&baz=biff',
'https://fake.invalid/blasdflkjasdf?foo=bar&baz=biff',
],
// Spotify domains should have `si` stripped
[
'https://x.y.z.open.spotify.com/playlist/1QF7oA9PlHfSGuPmhSGjku?si=Yo-ZJ1yWRIiNnlyutkR7VA&pi=HKd_5k2iSMq0v',
'https://x.y.z.open.spotify.com/playlist/1QF7oA9PlHfSGuPmhSGjku?pi=HKd_5k2iSMq0v',
],
// Tiktok domains should have the `q` and `is_from_webapp` stripped
[
'https://www.tiktok.com/@username/video/6862253018223177445?is_from_webapp=v1&q=some%20query%20here&t=1632318677116',
'https://www.tiktok.com/@username/video/6862253018223177445?t=1632318677116',
],
// Tiktok domains should have referrer_video_id stripped
[
'https://tiktok.com/blasdflkjasdf?referer_video_id=092u34oijserr',
'https://tiktok.com/blasdflkjasdf',
],
// Domains outside Tiktok should NOT have igshid stripped
[
'https://test.invalid/blasdflkjasdf?referer_video_id=092u34oijserr',
'https://test.invalid/blasdflkjasdf?referer_video_id=092u34oijserr',
],
// Missing leading `/` should not matter
['https://tiktok.com?referer_video_id=092u34oijserr', 'https://tiktok.com/'],
// If there is a query param and no subpath, it should be normalized to `/?`
[
'https://test.invalid?referer_video_id=092u34oijserr',
'https://test.invalid/?referer_video_id=092u34oijserr',
],
// All domains should have `fbclid` stripped
[
'https://fake.invalid/today/foo/baz?fbclid=PAA0xDSwLCWK1leORuA2FlbQIxMQABpwS8_WWHe9A5RfMfEJfDsP7dJUnorFNdMrotQhkqfsT_oxs38CBvPA6RuCc4_aem_KZEOIfyvmi8iyBTnvr5sFg',
'https://fake.invalid/today/foo/baz',
],
// All domains should have `cuid` stripped
['https://fake.invalid/foo/bar?cuid=baz', 'https://fake.invalid/foo/bar'],
// Negative rule for `em.dynamicyield.com` means that `cuid` should not be stripped
[
'https://em.dynamicyield.com/foo/bar?cuid=baz',
'https://em.dynamicyield.com/foo/bar?cuid=baz',
],
// Negative rule for `empflix.com` is a naked removeparam. That means absolutely
// everything must stay
[
'https://empflix.com/?fbclid=hello&cuid=world',
'https://empflix.com/?fbclid=hello&cuid=world',
],
// The stripping shouldn't be too eager. If some undesirable params appear url-encoded
// in another param, leave them alone Some other impls don't handle this well, eg
// https://github.com/AdguardTeam/AdguardBrowserExtension/issues/3076
[
'https://adguardteam.github.io/AnonymousRedirect/redirect.html?url=https%3A%2F%2Fregister.hollywoodbets.net%2Fsouth-africa%2F1%3Futm_source%3DAdCash%26utm_medium%3DDirect%2B%26utm_campaign%3DGenericPopunder',
'https://adguardteam.github.io/AnonymousRedirect/redirect.html?url=https%3A%2F%2Fregister.hollywoodbets.net%2Fsouth-africa%2F1%3Futm_source%3DAdCash%26utm_medium%3DDirect%2B%26utm_campaign%3DGenericPopunder',
],
];

describe('URL tracking parameter stripping', () => {
it('passes known-answer tests', () => {
// We check that the sanitized URL equals the expected URL
for (const [urlStr, expected] of TEST_VECTORS) {
const url = new URL(urlStr);
const out = applyAllRules(url);
// Check that the output matches the expected value. Panic on false. This is
// browser code so `assert` is not defined
assert.strictEqual(out.toString(), expected);
}
});
});
1 change: 1 addition & 0 deletions ts/types/Storage.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type StorageAccessType = {
'auto-download-update': boolean;
'auto-download-attachment': AutoDownloadAttachmentType;
autoConvertEmoji: boolean;
autoRemoveUrlTracking: boolean;
'badge-count-muted-conversations': boolean;
'blocked-groups': ReadonlyArray<string>;
'blocked-uuids': ReadonlyArray<ServiceIdString>;
Expand Down
1 change: 1 addition & 0 deletions ts/types/StorageUIKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const STORAGE_UI_KEYS: ReadonlyArray<keyof StorageAccessType> = [
'audioMessage',
'auto-download-update',
'autoConvertEmoji',
'autoRemoveUrlTracking',
'badge-count-muted-conversations',
'call-ringtone-notification',
'call-system-notification',
Expand Down
Loading