zeroBSCRM_do_meta_box_html() builds a string of data-* attributes and then runs esc_attr() over the whole string rather than over each value, at includes/ZeroBSCRM.MetaBox.php:839:
echo '<div class="' . esc_attr( $classes ) . '" id="' . esc_attr( $box['id'] ) . '" ' . esc_attr( $extraAttrs . $dataAttrStr ) . '>';
$dataAttrStr is already markup by then, built a few lines up at :826:
$dataAttrStr .= 'data-' . str_replace( '_', '-', $capKey ) . '="' . $capVStr . '"';
So every quote in it becomes ". What the renderer builds:
data-tab="zbs-contact-files-metabox"data-can-hide="1" data-areas="normal,side" data-can-accept-tabs="" ...
and what it echoes:
data-tab="zbs-contact-files-metabox"data-can-hide="1" data-areas="normal,side" ...
The browser reads those as unquoted attribute values, decodes the character references, and hands back values with literal quote characters in them: data-areas is "normal,side" rather than normal,side.
There is a second thing wrong on the same line. $extraAttrs . $dataAttrStr has no separator between the two, so on a tab pane data-tab and data-can-hide run together. An unquoted attribute value only ends at whitespace, so the parser folds the second attribute into the first one's value.
What actually breaks
Less than you would think, and I want to be honest about which part is real.
The data-can-* and data-areas attributes are written and never read. I grepped js/, includes/, admin/ and the root, and nothing anywhere consumes them. That half of this is inert, and only matters if someone later writes JS against them and can't work out why the values are wrong.
The one that looks live is data-tab on a tab pane, set at :850. Semantic UI's tab module is initialised on metabox tab groups at js/ZeroBSCRM.admin.metabox.manager.js:101, and it matches panes to menu items by data-tab. The menu item's own data-tab is escaped correctly, in zeroBSCRM_do_meta_box_htmlTabHead() at :699, so the two values don't agree: the menu item says zbs-contact-files-metabox and the pane says "zbs-contact-files-metabox"data-can-hide="1". Tab groups are reachable, several metaboxes set can_become_tab => true (Contacts :848, :1155, :2275, Quotes :976), and they're formed by dragging one metabox onto another in the edit screen.
I have confirmed the PHP output exactly, by running the concatenation and esc_attr() through WP. I have not confirmed the broken-tab symptom in a browser, so treat that last paragraph as a strong reading of the code rather than a reproduction. Worth doing before anyone spends time on it.
Fix
Escape values, not attribute strings. Something like: give $dataAttrStr and $extraAttrs an esc_attr() on each value where they are built, at :826 and :850, and echo them unescaped with a phpcs:ignore saying so. Put the missing space back between the two while you're in there.
Origin
Long-standing. The Feb 2026 formatting sweep (1285bad, #46809) reindented the line but the esc_attr($extraAttrs.$dataAttrStr) shape predates it.
Found while working on #38.
zeroBSCRM_do_meta_box_html()builds a string ofdata-*attributes and then runsesc_attr()over the whole string rather than over each value, atincludes/ZeroBSCRM.MetaBox.php:839:$dataAttrStris already markup by then, built a few lines up at:826:So every quote in it becomes
". What the renderer builds:and what it echoes:
The browser reads those as unquoted attribute values, decodes the character references, and hands back values with literal quote characters in them:
data-areasis"normal,side"rather thannormal,side.There is a second thing wrong on the same line.
$extraAttrs . $dataAttrStrhas no separator between the two, so on a tab panedata-tabanddata-can-hiderun together. An unquoted attribute value only ends at whitespace, so the parser folds the second attribute into the first one's value.What actually breaks
Less than you would think, and I want to be honest about which part is real.
The
data-can-*anddata-areasattributes are written and never read. I greppedjs/,includes/,admin/and the root, and nothing anywhere consumes them. That half of this is inert, and only matters if someone later writes JS against them and can't work out why the values are wrong.The one that looks live is
data-tabon a tab pane, set at:850. Semantic UI's tab module is initialised on metabox tab groups atjs/ZeroBSCRM.admin.metabox.manager.js:101, and it matches panes to menu items bydata-tab. The menu item's owndata-tabis escaped correctly, inzeroBSCRM_do_meta_box_htmlTabHead()at:699, so the two values don't agree: the menu item sayszbs-contact-files-metaboxand the pane says"zbs-contact-files-metabox"data-can-hide="1". Tab groups are reachable, several metaboxes setcan_become_tab => true(Contacts:848,:1155,:2275, Quotes:976), and they're formed by dragging one metabox onto another in the edit screen.I have confirmed the PHP output exactly, by running the concatenation and
esc_attr()through WP. I have not confirmed the broken-tab symptom in a browser, so treat that last paragraph as a strong reading of the code rather than a reproduction. Worth doing before anyone spends time on it.Fix
Escape values, not attribute strings. Something like: give
$dataAttrStrand$extraAttrsanesc_attr()on each value where they are built, at:826and:850, and echo them unescaped with aphpcs:ignoresaying so. Put the missing space back between the two while you're in there.Origin
Long-standing. The Feb 2026 formatting sweep (1285bad, #46809) reindented the line but the
esc_attr($extraAttrs.$dataAttrStr)shape predates it.Found while working on #38.