test(share): extract + cover injectTrackerHead (#100, #98)

Extract the admin trackerHead <head> injection into a pure injectTrackerHead()
and test it: a snippet containing $&/$$/backtick-dollar survives BYTE-FOR-BYTE
(pins the function-replacer fix), empty/whitespace/undefined and a missing </head>
leave the html unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-21 05:46:35 +03:00
parent eb17109fe0
commit f90dc3a3ff
3 changed files with 104 additions and 15 deletions

View File

@@ -0,0 +1,60 @@
import { injectTrackerHead } from './inject-tracker-head.util';
// Pins the public-share trackerHead injection invariant (ShareSeoController).
// The admin snippet is trusted content and MUST land byte-for-byte before the
// first </head>. The critical regression these tests guard is the function vs
// string replacer: a string replacement interprets `$&`/`$$`/`` $` ``/`$'`
// inside the snippet as substitution patterns and mangles the tracker. The
// byte-for-byte test below FAILS on the old string-replacer implementation and
// passes only with the function replacer.
const HTML = '<html><head><title>t</title></head><body>b</body></html>';
describe('injectTrackerHead', () => {
it('inserts the snippet immediately before the first </head>', () => {
const out = injectTrackerHead(HTML, '<script>ga()</script>');
expect(out).toBe(
'<html><head><title>t</title><script>ga()</script>\n</head><body>b</body></html>',
);
});
it('inserts a snippet containing $& byte-for-byte (function replacer)', () => {
const snippet = '<script>var a="$&";</script>';
const out = injectTrackerHead(HTML, snippet);
expect(out).toContain(`${snippet}\n</head>`);
// The literal "$&" survives; a string replacer would have spliced in the
// matched "</head>" here.
expect(out).toContain('$&');
expect(out).not.toContain('</head>"');
});
it('inserts a snippet containing $$, $` and $\' byte-for-byte', () => {
// All four special replacement patterns in one snippet.
const snippet = "<!-- $$ $` $' $& -->";
const out = injectTrackerHead(HTML, snippet);
expect(out).toContain(`${snippet}\n</head>`);
});
it('returns html unchanged for an empty trackerHead', () => {
expect(injectTrackerHead(HTML, '')).toBe(HTML);
});
it('returns html unchanged for a whitespace-only trackerHead', () => {
expect(injectTrackerHead(HTML, ' \n\t ')).toBe(HTML);
});
it('returns html unchanged for an undefined trackerHead', () => {
expect(injectTrackerHead(HTML, undefined)).toBe(HTML);
});
it('returns html unchanged when there is no </head> marker', () => {
const noHead = '<html><body>no head here</body></html>';
expect(injectTrackerHead(noHead, '<script>ga()</script>')).toBe(noHead);
});
it('injects before only the FIRST </head> when several exist', () => {
const twoHeads = '<head></head><head></head>';
const out = injectTrackerHead(twoHeads, 'X');
expect(out).toBe('<head>X\n</head><head></head>');
});
});

View File

@@ -0,0 +1,30 @@
/**
* Injects an admin-authored analytics/tracker snippet verbatim into the
* <head> of a public-share page.
*
* `trackerHead` is admin-only trusted content (writable only via the
* admin-gated workspace settings) and must be inserted BYTE-FOR-BYTE before the
* first `</head>` marker. A plain string replacement would interpret `$&`,
* `$$`, `` $` `` and `$'` inside the snippet as substitution patterns and mangle
* the tracker, so a FUNCTION replacer is used: its return value is inserted
* literally with no special-pattern interpretation.
*
* The snippet is deliberately NOT escaped (it is trusted HTML/JS). Returns the
* html unchanged when:
* - trackerHead is undefined / empty / whitespace-only, or
* - there is no `</head>` marker to anchor the injection.
*/
export function injectTrackerHead(
html: string,
trackerHead: string | undefined,
): string {
if (typeof trackerHead !== 'string' || trackerHead.trim().length === 0) {
return html;
}
if (!html.includes('</head>')) {
return html;
}
// Function replacer: the return value is inserted literally, so `$&`/`$$`/
// `` $` ``/`$'` in the admin snippet are NOT treated as substitution patterns.
return html.replace('</head>', () => `${trackerHead}\n</head>`);
}

View File

@@ -8,6 +8,7 @@ import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
import { EnvironmentService } from '../../integrations/environment/environment.service';
import { Workspace } from '@docmost/db/types/entity.types';
import { htmlEscape } from '../../common/helpers/html-escaper';
import { injectTrackerHead } from './inject-tracker-head.util';
@Controller('share')
export class ShareSeoController {
@@ -97,21 +98,19 @@ export class ShareSeoController {
// pages only. It is trusted content, so it is NOT escaped. The htmlEmbed
// block itself is sandboxed and is the safe surface for everyone else.
const trackerHead = (workspace?.settings as any)?.trackerHead;
if (typeof trackerHead === 'string' && trackerHead.trim().length > 0) {
if (transformedHtml.includes('</head>')) {
// Function replacer: the snippet is admin-authored trusted content and
// must be injected verbatim. A string replacement would interpret `$&`,
// `$'`, `` $` `` and `$$` inside it as substitution patterns and mangle
// the tracker; a function return value is inserted literally.
transformedHtml = transformedHtml.replace(
'</head>',
() => `${trackerHead}\n</head>`,
);
} else {
this.logger.warn(
'trackerHead is configured but no </head> marker was found in the share index HTML; tracker snippet was not injected.',
);
}
const beforeInjection = transformedHtml;
transformedHtml = injectTrackerHead(transformedHtml, trackerHead);
if (
beforeInjection === transformedHtml &&
typeof trackerHead === 'string' &&
trackerHead.trim().length > 0
) {
// A non-empty snippet was configured but nothing was injected: the only
// reason injectTrackerHead leaves the html unchanged for a non-empty
// snippet is a missing </head> marker.
this.logger.warn(
'trackerHead is configured but no </head> marker was found in the share index HTML; tracker snippet was not injected.',
);
}
res.type('text/html').send(transformedHtml);