Eén kleur erbij, alle Extra CSS weg

Ik wilde één kleur toevoegen aan het palet van deze site. Een klusje van een minuut, dat ik aan mijn AI-assistent overliet. Die weigerde: als hij dat opsloeg, zou alle Extra CSS van de site verdwijnen. Dat geloofde ik niet. Een beheerder mag toch gewoon de kleuren van zijn eigen site aanpassen?

Omslag: Eén kleur erbij, alle Extra CSS weg

Dat mag hij ook. En toch had de assistent gelijk. We hebben het uitgeprobeerd op een kopie van de site, zonder iets op te slaan. Van de 2123 tekens Extra CSS bleven er 0 over.

Wat er speelt. Deze site is onderdeel van een multisite: één WordPress met meerdere sites erin. Daar bestaan twee soorten beheerders. De netwerkbeheerder mag alles. Een gewone sitebeheerder mag zijn eigen site beheren, maar geen vrije code schrijven. En WordPress rekent CSS tot vrije code.

Het account van mijn assistent is bewust een gewone sitebeheerder. Ik ben zelf netwerkbeheerder, en daarom had ik het nooit gemerkt.

Waar het misgaat. In een blokthema staat alles wat je onder Stijlen instelt bij elkaar in één pakket: het kleurenpalet, de lettertypes en ook de Extra CSS. Slaat een gewone sitebeheerder dat pakket op, dan haalt WordPress er alles uit wat hij niet zelf had mogen schrijven. De Extra CSS dus, ook al heeft hij die niet aangeraakt en ook al stond die er al maanden.

Er komt geen melding. De kleur staat erbij, de pagina is opgeslagen, en de doorzichtige header, het menu-effect en de rest van het maatwerk zijn weg.

De proef. Op de kopie liet ik hetzelfde pakket twee keer door het opslagfilter van WordPress gaan, zonder het echt op te slaan:

  • als gewone sitebeheerder: Extra CSS van 2123 naar 0 tekens, palet ongewijzigd
  • als netwerkbeheerder: Extra CSS van 2123 naar 2123 tekens, palet ongewijzigd

Is dit een fout? Nee, het is zo bedoeld. WordPress kan niet zien wie de CSS ooit schreef, en vertrouwt op een multisite alleen de netwerkbeheerder. Ik ben niet de enige die ertegenaan liep, en WordPress heeft het verzoek om dit te veranderen afgewezen:

  • WordPress-ticket #58610: het verzoek om sitebeheerders op een multisite CSS toe te staan. Gesloten: wordt niet opgelost (wontfix)
  • WordPress-ticket #59123: de melding dat het paneel Extra CSS ontbreekt voor beheerders op een multisite. Gesloten: geen fout (invalid)
  • Gutenberg #47062 (2023): de wijziging die de CSS spaart voor wie het recht wél heeft, en dus niet voor de rest
  • Gutenberg #76650 (2026): hetzelfde gedrag voor CSS op losse blokken
  • Multisite Custom CSS: een plugin die sitebeheerders dit recht geeft

De oplossing is een paar regels in mijn eigen plugin: wie het thema van een site mag aanpassen, mag ook de CSS bewerken. Daarna dezelfde proef opnieuw, nu met een echte opslag: 2123 tekens voor, 2123 tekens na. Pas toen heeft de assistent de kleur toegevoegd, op alle drie de taalversies.

Dit is wel een bewuste keuze. Elke beheerder van een site in mijn netwerk mag nu vrije CSS schrijven. In mijn netwerk ken ik ze allemaal. In een netwerk met beheerders die je niet kent, zou ik het niet doen.

Bijvangst: een site die stuk ging

Om de oplossing overal te laten gelden, zette ik de plugin aan voor het hele netwerk. Eén van de andere sites gaf meteen een fout. Die site draait op een ouder thema, en dat thema bevat een functie met precies dezelfde naam als een functie in mijn plugin. Twee keer dezelfde naam mag niet.

Het gemene: de voorpagina van die site deed het gewoon. Die kwam uit de cache. Alleen wie wilde inloggen, kreeg de fout. Ik heb de functies in de plugin een eigen voorvoegsel gegeven, en daarna kon hij wel netwerkbreed aan.

Extraatje: de kleur die geen paletkleur was

De nieuwe kleur was bedoeld voor een knop. Die knop had ik eerder al blauw gemaakt, met een losse kleurcode. Toen de kleur in het palet stond, toonde de editor hem bij de knop netjes als gekozen. Klaar, dacht ik.

Niet dus. De editor vergelijkt alleen de kleurcode. Is die toevallig gelijk aan een kleur uit het palet, dan krijgt die paletkleur het vinkje. Opgeslagen stond nog steeds de losse code. Het verschil merk je pas als je de paletkleur later aanpast: de knop kleurt dan niet mee.

Je ziet het alleen in de code van het blok. Daar heeft de assistent het ook omgezet.

Wat ik ervan leer

“Dat kan een beheerder toch gewoon” was waar, en toch geen reden om door te gaan. De handeling mocht. Het bijeffect was het probleem.

Laat je iemand met minder rechten in de Site-editor werken, een collega, een klant of een assistent, probeer dan op een kopie uit wat er bij opslaan gebeurt. Niet of het lukt, maar wat er daarna nog staat.

En of een site het doet, meet je niet aan de voorpagina.

Voor de techneuten: wat er precies gebeurt

Omgeving: WordPress 7.1.2 multisite (subdomeinen), thema Ollie, een gebruiker met de rol Administrator op de site die geen superadmin is.

1. Het recht. edit_css is in WordPress gekoppeld aan unfiltered_html. In map_meta_cap() (wp-includes/capabilities.php):

case 'edit_css':
case 'unfiltered_html':
    if ( defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML ) {
        $caps[] = 'do_not_allow';
    } elseif ( is_multisite() && ! is_super_admin( $user_id ) ) {
        $caps[] = 'do_not_allow';
    } else {
        $caps[] = 'unfiltered_html';
    }

2. Het filter. Voor gebruikers zonder unfiltered_html hangt WordPress bij het opslaan wp_filter_global_styles_post() aan content_save_pre. Dat roept WP_Theme_JSON::remove_insecure_properties() aan, en daar staat:

// The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
    $output = $input;
} else {
    $output = static::remove_insecure_styles( $input );
}

Zonder het recht blijft styles.css dus achter. Het maakt niet uit of het verzoek de CSS zelf bevatte: het filter loopt over de hele inhoud van het bericht van het type wp_global_styles.

3. Meten zonder schrijven. Vraag de global styles op via de REST API met context=edit en kijk in _links. Staat wp:action-edit-css er niet bij, dan heeft deze gebruiker het recht niet en moet hij niet opslaan.

4. De proef. Met WP-CLI, per gebruiker wp_set_current_user() en dan:

$post = get_post( $id_van_de_global_styles );
$na   = json_decode( wp_unslash( wp_filter_global_styles_post( wp_slash( $post->post_content ) ) ), true );
echo strlen( $na['styles']['css'] ?? '' );

5. De oplossing (in een eigen plugin):

function webtaurus_sitebeheerder_mag_css( $caps, $cap ) {
    if ( 'edit_css' !== $cap || ! is_multisite() ) {
        return $caps;
    }
    if ( defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML ) {
        return $caps;
    }
    return array( 'edit_theme_options' );
}
add_filter( 'map_meta_cap', 'webtaurus_sitebeheerder_mag_css', 10, 2 );

Dit raakt alleen edit_css. Ongefilterde HTML blijft voorbehouden aan de superadmin; dat heb ik na de wijziging gecontroleerd.

De waarschuwing die er wél is. Voor CSS op losse blokken toont de blokeditor wel een waarschuwing: “If you save this post, the custom CSS will be removed.” Of de Site-editor bij Stijlen ook waarschuwt, heb ik niet gezien: de assistent werkt via de REST API en ziet geen scherm.

De botsing. Cannot redeclare unregister_default_wp_widgets(): dezelfde functienaam in functions.php van het thema en in de plugin. Een plugin die per site actief is op sites met een ander thema, merkt daar niets van. Netwerkbreed wel. Meet zoiets op /wp-json/, niet op de voorpagina, als er een paginacache draait.

De kleur. Opgeslagen stond "style":{"color":{"background":"#084b77"}}. Een paletkleur ziet er zo uit: "backgroundColor":"bg-blauw", met de klasse has-bg-blauw-background-color op de knop in plaats van een background-color in het style-attribuut.