diff --git a/server/src/modules/opds/__tests__/opds.service.test.ts b/server/src/modules/opds/__tests__/opds.service.test.ts index 0af201077..b6b0585d7 100644 --- a/server/src/modules/opds/__tests__/opds.service.test.ts +++ b/server/src/modules/opds/__tests__/opds.service.test.ts @@ -522,4 +522,40 @@ describe('OpdsService', () => { expect(xml).toContain('''); }); }); + + describe('book descriptions', () => { + function feedFor(description: string | null): string { + return makeService().generateAcquisitionFeed( + 'Catalog', + 'urn:bookorbit:catalog', + [sampleBook({ description })], + 1, + 1, + 50, + `${BASE}/catalog?page=1&size=50`, + 'test-token', + ); + } + + it('sends an HTML description as plain text with its paragraph and line breaks', () => { + const xml = feedFor('

The first book & its sequel.

Line one
line two

'); + + expect(xml).toContain('The first book & its sequel.\nLine one\nline two'); + expect(xml).not.toContain('<p>'); + expect(xml).not.toContain('<strong>'); + }); + + it('keeps a plain-text description and its line breaks unchanged', () => { + const xml = feedFor('Part one.\n\nPart two.'); + + expect(xml).toContain('Part one.\n\nPart two.'); + }); + + it.each([ + ['no description', null], + ['a description holding only markup', '


'], + ])('sends empty content for %s', (_label, description) => { + expect(feedFor(description)).toContain(''); + }); + }); }); diff --git a/server/src/modules/opds/opds.service.ts b/server/src/modules/opds/opds.service.ts index f40aaad75..8c2c694b9 100644 --- a/server/src/modules/opds/opds.service.ts +++ b/server/src/modules/opds/opds.service.ts @@ -1,6 +1,7 @@ import { Injectable, Optional } from '@nestjs/common'; import { esc, fileMimeType, OPDS_MIME_ACQ, OPDS_MIME_ATOM, OPDS_MIME_NAV, OPDS_MIME_SEARCH, xmlEl, xmlLink } from './opds-xml.helpers'; +import { htmlToPlainText } from '../../common/utils/html-to-text.utils'; import { DeliverySelectionService } from '../book/delivery-selection.service'; import type { OpdsRequestUser } from './opds-auth.guard'; import type { OpdsBookEntry } from './opds-book.service'; @@ -172,8 +173,12 @@ export class OpdsService { lines.push(` ${xmlEl('name', author)}`); } - if (book.description) { - lines.push(` ${esc(book.description)}`); + // Descriptions are stored as HTML. Clients that honour type="text" show markup literally, + // and type="html" would hand unsanitized markup from book files and providers to clients + // that render it, so the feed carries the text alone. + const description = book.description ? htmlToPlainText(book.description, { preserveLineBreaks: true }) : ''; + if (description) { + lines.push(` ${esc(description)}`); } else { lines.push(' '); }