Skip to content

Issue 255 Article Previews Showed HTML Codes

Ed Mozley edited this page Nov 8, 2026 · 1 revision

Article previews showed HTML codes instead of letters

Reported and fixed by: PR #255 from Dr4inder, November 2026 Β· Module: Knowledge Β· Fixed in: the release after 4.11.0 (changelog #2753)


What you saw

In Knowledge, the short preview under each article's title showed codes where letters should be:

Änderung für Öl & Gas

instead of

Γ„nderung fΓΌr Γ–l & Gas

It hit German, French, Spanish and any other language with accented letters, and in English it showed anywhere an article used &, <, >, € or a non-breaking space. The article itself was always fine. Only the preview was wrong.


What was actually wrong

The editor saves some characters as HTML entities: Γ€ can be stored as &auml;, and & is always stored as &amp;. To build a preview, the server took the start of the body and ran PHP's strip_tags() over it:

// api/knowledge/knowledge_articles.php - before
$article['preview'] = strip_tags($article['preview']);

strip_tags() removes tags, but it leaves entities exactly as they are. The page then escaped the preview before drawing it, which is right, because a preview must never be able to inject HTML. But escaping &auml; turns its & into &amp;, so the browser showed the six characters &auml; instead of the letter.

It was four places, not one

Four places show an article preview, and each made its own:

Where Before
The analyst article list strip_tags() - entities shown as codes
The record preview panel (hovering a linked article) strip_tags() - entities shown as codes
The REST API's preview field strip_tags() - entities sent as codes
The self-service portal (Help Centre, home page, New ticket suggestions) portalArticlePreview() - already right

The portal had fixed this long before, in a function that also handled two other problems:

  • Words glued together across tags. strip_tags() leaves nothing where a tag was, so <h3>Password Reset</h3><p>You can became "Password ResetYou can".
  • A cut in the middle of an entity or word. The list takes the first 300 characters of the HTML, so it can stop at &au or halfway through a word.

The fix

The PR decoded the entities in the analyst list, which was the right fix for that one line. The review went one step further: it moved the portal's function into its own file and used it in all four places, so every preview behaves the same way.

// includes/knowledge/article_preview.php
function knowledgeArticlePreview(?string $body, int $length = 400, bool $wasCut = false): string {
    $raw = preg_replace('#<(script|style)\b[^>]*>.*?</\1>#is', ' ', (string) $body);  // drop script/style whole
    $raw = str_replace('<', ' <', (string) $raw);                                      // keep words apart
    if ($wasCut) $raw = preg_replace('/&#?\w*$/', '', (string) $raw);                  // drop a half entity
    $text = html_entity_decode(strip_tags($raw), ENT_QUOTES | ENT_HTML5, 'UTF-8');      // &auml; -> Γ€
    $text = trim(preg_replace('/\s+/u', ' ', $text));
    // ...then cut to $length on a whole word, with an ellipsis
}

The analyst list now also tells it when the body was longer than the 300 characters it fetched, as the portal already did:

// api/knowledge/knowledge_articles.php
LEFT(a.body, 300) as preview, CHAR_LENGTH(a.body) > 300 AS preview_cut,
...
$article['preview'] = knowledgeArticlePreview($article['preview'], 300, (bool) $article['preview_cut']);

portalArticlePreview() is still there, as a one-line wrapper, so the portal's code reads as before.

TRAP: the preview is plain text with entities decoded, so it must be escaped where it is drawn. &lt;b&gt; becomes the text <b>. All four callers escape it: escapeHtml() in knowledge.js, htmlspecialchars() on the portal pages, esc() in record-preview.js, and the REST API returns JSON. A new caller that puts it into innerHTML unescaped would let an article's text become markup.

No database change, nothing to run after upgrading.


Files changed

File Change
🟣 includes/knowledge/article_preview.php new: knowledgeArticlePreview(), moved from the portal
🟣 includes/knowledge/portal_reader.php portalArticlePreview() now calls it
πŸ”΅ api/knowledge/knowledge_articles.php the analyst list uses it, with preview_cut
πŸ”΅ includes/record_preview.php the record preview panel uses it
πŸ”Œ api/v1/resources/knowledge.php the REST API preview uses it
πŸ§ͺ tests/knowledge-article-preview.php new: 13 checks, no database

Review of PR #255

Verdict: merge, with fixes. The PR was correct and small, and it said exactly where the escaping happens, which is the question that matters for a change like this. Merged as it was, so the commit stays Dr4inder's. The one-liner was then replaced by the shared function, so the same fix reached the record preview panel and the REST API, which had the same bug.

How it was proved

  • tests/knowledge-article-preview.php, 13 checks: named, numeric and hex entities; escaped markup becoming text; words kept apart; script and style dropped; a half entity dropped; whole-word cuts; and that all four callers use the shared function. The control: replacing the function with the old strip_tags() alone fails 7 of them.
  • Live, on a throwaway install with an article containing &Auml;nderung, &uuml;, &amp; and &lt;b&gt;: the analyst list and the record preview panel both returned Γ„nderung FΓΌr Γ–l & Gas <b>fett</b>, and both draw it escaped.

Not proven here: the REST API was checked by the test (it uses the shared function), not called live.

What changes for an API user

The REST API's preview now has entities decoded, words kept apart, and an ellipsis when it was cut. Anyone who was decoding entities themselves will see no difference. Anyone who was showing preview as HTML should escape it, as it is plain text.


What this means for you

  • Article previews show accented letters and symbols correctly, in every language.
  • A heading no longer runs into the sentence after it in a preview.
  • Previews end on a whole word with "…" when there is more.

Related

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Responses to feedback

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally