Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/node/utils/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,13 @@ export type SettingsType = {
favicon: string | null,
publicURL: string | null,
socialMeta: {
description: string | null,
// Runtime type is wider than what an operator writes by hand: when
// `socialMeta.description` is sourced from an env var (e.g.
// `"${SOCIAL_META_DESCRIPTION:null}"` in settings.json.docker), the
// settings loader's `coerceValue()` turns numeric-looking strings into
// numbers and "true"/"false" into booleans. Downstream code stringifies
// before use; the wider type stops callers (and tests) needing casts.
description: string | number | boolean | null,
},
ttl: {
AccessToken: number,
Expand Down
23 changes: 16 additions & 7 deletions src/node/utils/socialMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ type SocialMetaSettings = {
favicon?: string | null,
publicURL?: string | null,
socialMeta?: {
description?: string | null,
// Wider than the operator-facing type: env-var-driven settings get
// pre-coerced by Settings.coerceValue(), so we may receive number/boolean
// even though "the value an operator types" is a string. Stringified at
// resolve time.
description?: string | number | boolean | null,
},
};

Expand Down Expand Up @@ -161,16 +165,21 @@ export type RenderOpts = {
padName?: string,
};

// Operator override wins, but only when it's a non-empty string. An empty
// string from settings would silently blank out og:description / twitter:
// description and break previews, so we treat empty/whitespace-only as unset
// and fall back to the i18n catalog.
// Operator override wins when set. Settings.ts coerces env-var strings to
// their typed form (e.g. SOCIAL_META_DESCRIPTION="123" arrives as the number
// 123 and ="true" as the boolean true), so we accept string | number | boolean
// and stringify before comparing. Empty / whitespace-only values are treated
// as unset — an accidental "" in settings would otherwise silently blank out
// og:description and break previews entirely.
const resolveDescriptionWithOverride = (
override: string | null | undefined,
override: string | number | boolean | null | undefined,
locales: {[lang: string]: {[key: string]: string}} | undefined,
renderLang: string,
): string => {
if (typeof override === 'string' && override.trim() !== '') return override;
if (override !== null && override !== undefined) {
const s = String(override);
if (s.trim() !== '') return s;
}
return resolveDescription(locales, renderLang);
};

Expand Down
32 changes: 32 additions & 0 deletions src/tests/backend/specs/socialMeta-unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,38 @@ describe(__filename, function () {
});
assert.equal(ogTag(html, 'og:description'), 'Catalog');
});

it('numeric override is stringified (env-var coercion safety)', function () {
// Settings.ts coerceValue() turns numeric-looking env vars into numbers,
// so SOCIAL_META_DESCRIPTION="2026" arrives here as the number 2026.
// Without stringification the resolver would silently fall back to i18n.
const html = renderSocialMeta({
req: fakeReq(),
settings: {
title: 'Etherpad', favicon: null,
socialMeta: {description: 2026},
},
availableLangs: {en: {}}, locales: enLocales,
kind: 'pad', padName: 'P',
});
assert.equal(ogTag(html, 'og:description'), '2026');
});

it('boolean override is stringified (covers "true"/"false" env-var coercion)', function () {
// Less likely than the numeric case but possible: setting
// SOCIAL_META_DESCRIPTION="true" yields a boolean. Treat it like the
// operator wrote that literal string rather than silently dropping it.
const html = renderSocialMeta({
req: fakeReq(),
settings: {
title: 'Etherpad', favicon: null,
socialMeta: {description: true},
},
availableLangs: {en: {}}, locales: enLocales,
kind: 'pad', padName: 'P',
});
assert.equal(ogTag(html, 'og:description'), 'true');
});
});

describe('renderSocialMeta — image URL', function () {
Expand Down
Loading