Skip to content

feat(module): Tiger_Module_Longform — one renderer for a listing's long-form copy - #195

Merged
WebTigers merged 1 commit into
mainfrom
feat/module-longform
Aug 29, 2026
Merged

feat(module): Tiger_Module_Longform — one renderer for a listing's long-form copy#195
WebTigers merged 1 commit into
mainfrom
feat/module-longform

Conversation

@WebTigers

Copy link
Copy Markdown
Owner

Why

Two surfaces show the same "plugin page" body — the Module Manager's View more and a marketplace's own listing page — and they rendered it two different ways with two different safety policies. A listing could look safe in one surface and not the other.

Tiger_Module_Longform (@api) is now the single component behind both.

The safety change (the actual point)

Both call sites previously did:

$this->_scrub((new Tiger_Cms_Renderer())->renderBody($md, 'markdown'));

renderBody('markdown') goes through the shared Parsedown::instance() with markup allowed, then runs the [shortcode] pass. That is right for a trusted CMS author and wrong for a file written by whoever published the module: it emits their markup and lets them invoke this install's shortcodes. A regex scrubber over the rendered HTML was the only thing in between.

Longform escapes inline HTML at parse time (safe-mode Parsedown, on its own instance so the CMS singleton is untouched) and filters dangerous URL schemes. Escaping at parse time is strictly stronger than stripping tags afterwards. _scrub() is removed along with its last caller.

A contract that had never fired

_inspectMarketplace() reads $listing['readme'] — written that way because a PASS module's repo is private, so its review copy comes from the marketplace index rather than a GitHub TIGER.md fetch. No producer ever emitted that field, so a paid listing's View more only showed its one-line description. TigerMarketplace's module adapter now declares readme, so the feed carries it and this renders the seller's real copy.

Verified on the live chain before opening this: dev-com's feed carries readme for pay, tiger-dev resolves it via Tiger_Module_Registry::listing('pay','webtigers'), and it renders to HTML headed "TigerStripe — payments for your Tiger site" with no active content.

Resolution order

Source When
readme inline a paid/private module — its marketplace serves the copy
body inline alias, for listings authored before the name settled
tiger_md URL a public repo's TIGER.md — https-only, 256 KB cap, 1 h disk cache

Tests

15 new unit tests. The load-bearing ones are safety: raw HTML escaped rather than emitted, an event-handler attribute cannot survive as an attribute, javascript: links filtered, and an http:// body refused without being fetched.

Full suite green — 2111 tests, 21490 assertions, no failures.

Note

The 15 deprecations in the run are pre-existing (vendored Parsedown 1.7.4 implicit-nullable params on PHP 8.4+); failOnDeprecation is not set and they occur on main too. Worth a separate one-line-each fix.

…ng-form copy

Two surfaces show the same "plugin page" body — the Module Manager's "View more" and a
marketplace's own listing page — and they rendered it two different ways, with two different
safety policies. A listing could look safe in one and not the other.

Tiger_Module_Longform is now the single component behind both. It resolves the copy from where
it actually lives — inline `readme` on the listing (a paid/PASS module's repo is PRIVATE, so its
marketplace serves the review copy in the feed; `body` kept as an alias) or a public repo's
`tiger_md` URL, https-only, size-capped and disk-cached — and renders it once, safely.

The safety change is the point. Both call sites previously ran
`Tiger_Cms_Renderer::renderBody($md, 'markdown')` and then regex-stripped active content from the
RESULT. That path renders through the shared Parsedown::instance() with markup ALLOWED and then
runs the [shortcode] pass — correct for a trusted CMS author, wrong for a file written by whoever
published the module, since it emits their markup and lets them invoke this install's shortcodes.
The scrub-afterwards regex was the only thing standing between the two. Longform instead escapes
inline HTML at PARSE time (safe-mode Parsedown, its own instance so the CMS singleton is
untouched) and filters dangerous URL schemes. Escaping at parse time is strictly stronger than
stripping tags afterwards, which is the classic thing to get subtly wrong. `_scrub()` is removed
with its last caller.

Also fixes a contract that had never fired: _inspectMarketplace() reads `$listing['readme']`, but
no producer emitted that field, so a paid listing's "View more" only ever showed its one-line
description. TigerMarketplace's module adapter now declares it, so the feed carries it and the
Module Manager renders the seller's real copy.

15 unit tests, the load-bearing ones being safety: raw HTML is escaped rather than emitted, an
event-handler attribute cannot survive as an attribute, a javascript: link is filtered, and an
http:// body is refused WITHOUT being fetched. Full suite green (2111 tests).
@WebTigers
WebTigers merged commit caf5ef4 into main Aug 29, 2026
13 checks passed
@WebTigers
WebTigers deleted the feat/module-longform branch August 29, 2026 06:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant