CoolCMS API

EsHighlighter
in package

FinalYes

Highlighted snippets from ES, and the only place in the system where an ES answer becomes HTML.

Whoever prints a highlight gets a finished safe string (EsSearchResult::$highlights); the raw ES fragment never leaves.

ES wraps matches in two Private Use Area characters (self::PRE_TAG/self::POST_TAG) instead of <em>: parsing <em> out of a fragment and escaping the rest would mean telling our tags from user markup inside the text, which is exactly where XSS happens. toSafeHtml() therefore (1) escapes the whole fragment, so no executable markup remains, and only then (2) swaps the sentinels for <em>/</em>. The allowlist is structural: the only tags that can appear are <em> and </em> without any attribute, and no ES or DB input can bypass it.

NOTE: if a user gets the characters U+E000/U+E001 into a text, the worst case is a highlight in the wrong place; safety is not affected, because the output is always paired and contains nothing but <em>.

Table of Contents

Constants

POST_TAG  : mixed = ""
PRE_TAG  : mixed = ""
Sentinels ES wraps matches in (U+E000/U+E001). They do not occur in real texts and `htmlspecialchars` leaves them unchanged, so they stay recognizable after escaping.

Methods

fromHit()  : array<string, string>
Maps the fragments of one hit to `logical key => safe HTML`.
requestBody()  : array<string, mixed>
Builds the `highlight` block of the ES query.
toSafeHtml()  : string
Turns an ES fragment into HTML that may go to a template through `|noescape`.

Constants

PRE_TAG

Sentinels ES wraps matches in (U+E000/U+E001). They do not occur in real texts and `htmlspecialchars` leaves them unchanged, so they stay recognizable after escaping.

public mixed PRE_TAG = ""

Methods

fromHit()

Maps the fragments of one hit to `logical key => safe HTML`.

public static fromHit(array<string|int, mixed> $hitHighlight, array<string, array<int, string>> $fields) : array<string, string>

Fields are tried in the order the source listed them: first the base one (stemming analyzer, highlights inflected forms too), then .folded (query without diacritics). ES omits a field without a match from highlight (require_field_match defaults to true), so the key is simply missing and the template prints the original text.

Parameters
$hitHighlight : array<string|int, mixed>

blok highlight jednoho hitu

$fields : array<string, array<int, string>>
Return values
array<string, string>

requestBody()

Builds the `highlight` block of the ES query.

public static requestBody(array<string, array<int, string>> $fields) : array<string, mixed>

number_of_fragments: 0 returns the whole field with highlighted matches instead of cut snippets, since the Blog listing prints the whole title and perex. encoder: default makes ES encode nothing in the fragment; escaping is done by PHP (toSafeHtml()) and doing it twice would break entities.

Parameters
$fields : array<string, array<int, string>>

logical key mapped to ES fields (base + .folded)

Return values
array<string, mixed>

toSafeHtml()

Turns an ES fragment into HTML that may go to a template through `|noescape`.

public static toSafeHtml(string $fragment) : string

IMPORTANT: everything is escaped first and only then the sentinels become <em>. The reverse order would be a stored XSS, because article content and perex come from an editor and may contain <script> or "><img onerror=….

The output is also always paired: nested or unpaired sentinels (characters planted by a user) are dropped and a broken structure never appears.

Parameters
$fragment : string
Return values
string
On this page

Search results