EsHighlighter
in package
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
POST_TAG
public
mixed
POST_TAG
= ""
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
highlightjednoho 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