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:
60
apps/server/src/core/share/inject-tracker-head.util.spec.ts
Normal file
60
apps/server/src/core/share/inject-tracker-head.util.spec.ts
Normal 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>');
|
||||
});
|
||||
});
|
||||
30
apps/server/src/core/share/inject-tracker-head.util.ts
Normal file
30
apps/server/src/core/share/inject-tracker-head.util.ts
Normal 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>`);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user