fix(links): preserve percent-encoded URLs during normalization (#18792)
## Summary This preserves percent-encoded payloads when normalizing links fields. `lowercaseUrlOriginAndRemoveTrailingSlash` was decoding the path and query string while lowercasing the URL origin. That changes URLs where encoded payloads are semantically significant, such as Google Maps links containing `%2F` segments. Closes #18698. ## Changes - stop decoding the path/query payload in `lowercaseUrlOriginAndRemoveTrailingSlash` - preserve the raw path, query, and hash while still lowercasing the origin and trimming a trailing slash - update shared URL normalization tests to assert encoded payloads stay encoded - add a server-side regression test covering imported links field normalization ## Validation - `corepack yarn jest --config packages/twenty-shared/jest.config.mjs packages/twenty-shared/src/utils/url/__tests__/lowercaseUrlOriginAndRemoveTrailingSlash.test.ts --runInBand` - `corepack yarn jest --config packages/twenty-server/jest.config.mjs packages/twenty-server/src/engine/core-modules/record-transformer/utils/__tests__/transform-links-value.util.spec.ts --runInBand` - `corepack yarn nx test twenty-server --runInBand --testFile=src/engine/core-modules/record-transformer/utils/__tests__/transform-links-value.util.spec.ts` --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+28
@@ -75,5 +75,33 @@ describe('transformLinksValue', () => {
|
||||
|
||||
expect(transformLinksValue(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should preserve percent-encoded payloads when normalizing imported URLs', () => {
|
||||
const input = {
|
||||
primaryLinkUrl:
|
||||
'https://www.google.com/maps/place/Birdie+-+Eventlocation/data=!4m7!3m6!1s0x479e7674e1702985:0xe482992505cb1ba4!8m2!3d48.1584971!4d11.5538261!16s%2Fg%2F1ptwh8096!19sChIJhSlw4XR2nkcRpBvLBSWZguQ?authuser=0&hl=en&rclk=1',
|
||||
primaryLinkLabel: 'Birdie',
|
||||
secondaryLinks: JSON.stringify([
|
||||
{
|
||||
url: 'https://example.com/test%2520name',
|
||||
label: 'Encoded secondary link',
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const expected = {
|
||||
primaryLinkUrl:
|
||||
'https://www.google.com/maps/place/Birdie+-+Eventlocation/data=!4m7!3m6!1s0x479e7674e1702985:0xe482992505cb1ba4!8m2!3d48.1584971!4d11.5538261!16s%2Fg%2F1ptwh8096!19sChIJhSlw4XR2nkcRpBvLBSWZguQ?authuser=0&hl=en&rclk=1',
|
||||
primaryLinkLabel: 'Birdie',
|
||||
secondaryLinks: JSON.stringify([
|
||||
{
|
||||
url: 'https://example.com/test%2520name',
|
||||
label: 'Encoded secondary link',
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
expect(transformLinksValue(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+3
-9
@@ -1,11 +1,7 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { type LinkMetadataNullable } from 'twenty-shared/types';
|
||||
import {
|
||||
isDefined,
|
||||
lowercaseUrlOriginAndRemoveTrailingSlash,
|
||||
parseJson,
|
||||
} from 'twenty-shared/utils';
|
||||
import { isDefined, normalizeUrlOrigin, parseJson } from 'twenty-shared/utils';
|
||||
|
||||
import { removeEmptyLinks } from 'src/engine/core-modules/record-transformer/utils/remove-empty-links';
|
||||
|
||||
@@ -44,15 +40,13 @@ export const transformLinksValue = (
|
||||
|
||||
const processedSecondaryLinks = secondaryLinks?.map((link) => ({
|
||||
...link,
|
||||
url: isDefined(link.url)
|
||||
? lowercaseUrlOriginAndRemoveTrailingSlash(link.url)
|
||||
: link.url,
|
||||
url: isDefined(link.url) ? normalizeUrlOrigin(link.url) : link.url,
|
||||
}));
|
||||
|
||||
return {
|
||||
...value,
|
||||
primaryLinkUrl: isDefined(primaryLinkUrl)
|
||||
? lowercaseUrlOriginAndRemoveTrailingSlash(primaryLinkUrl)
|
||||
? normalizeUrlOrigin(primaryLinkUrl)
|
||||
: primaryLinkUrl,
|
||||
primaryLinkLabel,
|
||||
secondaryLinks: isEmpty(processedSecondaryLinks)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { getAbsoluteUrl } from 'twenty-shared/utils';
|
||||
import { ensureAbsoluteUrl } from 'twenty-shared/utils';
|
||||
|
||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||
import { WEBHOOK_RESPONSE_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/webhook/webhook-response';
|
||||
@@ -79,7 +79,7 @@ export class CallWebhookJob {
|
||||
);
|
||||
|
||||
const response = await axiosClient.post(
|
||||
getAbsoluteUrl(data.targetUrl),
|
||||
ensureAbsoluteUrl(data.targetUrl),
|
||||
payloadWithoutSecret,
|
||||
{
|
||||
headers,
|
||||
|
||||
Reference in New Issue
Block a user