HtmlUtils: strip every data-* attribute, not just the first one

BuildHtml() iterates $oElement->attributes -- a live DOMNamedNodeMap --
and calls removeAttribute() on the attribute the foreach is currently on.
That ends the iteration: the loop sees no further attribute, so exactly
one data-* is removed per element however many it carries.

Measured on PHP 8.5.10 / libxml 2.12.5, data-* left behind:

  <p data-a>                          -> none
  <p data-a data-b>                   -> data-b
  <p data-a data-b data-c>            -> data-b, data-c
  <p data-a data-b data-c data-d>     -> data-b, data-c, data-d
  <p DATA-A data-b>                   -> data-b

The single call site is buildMessage() in RainLoop/Actions/Messages.php,
reached from both DoSaveMessage() and DoSendMessage(), as the docblock on
BuildHtml() says. So the attributes that survive go out in the sent
message and into the draft that is APPENDed and read back later. It is
not the display path: an incoming body is only decoded and charset
converted in MailSo\Mail\Message::fromFetchResponse(), and the browser
filters it with the allowlists in dev/Common/Html.js.

The fix collects the names first, then removes them.
This commit is contained in:
Fathi Ben Nasr 2026-09-05 21:01:25 +00:00
parent c154d23cfe
commit 08ddc323f9

View file

@ -160,13 +160,20 @@ abstract class HtmlUtils
// Remove all remaining data-* attributes
foreach ($xpath->query('//*[@*[starts-with(name(), "data-")]]') as $oElement) {
$sTagNameLower = \strtolower($oElement->nodeName);
if ($oElement->hasAttributes()) {
// DOMNamedNodeMap is live: removing the attribute the foreach is
// currently on ends the iteration, so exactly one data-* was
// removed per element however many it carried. Collect the
// names first, then remove.
$aNames = [];
foreach ($oElement->attributes as $oAttr) {
if ('data-' === \substr(\strtolower($oAttr->nodeName), 0, 5)) {
$oElement->removeAttribute($oAttr->nodeName);
$aNames[] = $oAttr->nodeName;
}
}
foreach ($aNames as $sName) {
$oElement->removeAttribute($sName);
}
}
}