— Artikel — № 085

085 —Joomla

Joomla 3 500 bij opslaan: ontbrekende #__assets-rij in 40 min

Een Joomla 3-site gaf een 500 bij elke save. De oorzaak: één ontbrekende rij in #__assets. Hier is de trace, met de SQL erbij.

Bovenaanzicht van handgeschreven Joomla-incidentvel, assets-tabelschema, manilla tab, messing sleutel, envelop met lakzegel.
Hero · gestileerd stilleven№ 085

De Loom kwam binnen om 07:14 op een dinsdag. Een Nederlands bureau waar we mee samenwerken had een klant op Joomla 3.10, een regionale evenementenuitgever, en elke save op elk artikel gaf een 500 terug. Geen rode flits in de admin, geen validatiemelding: een keiharde Apache-foutpagina met de verkeerde charset, het soort pagina waardoor redacteuren rechtstreeks de bureau-eigenaar bellen. De redacteur was al een uur bezig om een stuk over een dorpsfeest te publiceren. De bureau-lead was twintig minuten wakker en had de twee dingen geprobeerd die je als eerste probeert: de Joomla-cache leeggegooid en PHP-FPM herstart. Geen van beide hielp.

Wat hieronder volgt is de daadwerkelijke trace, opgeschreven omdat Joomla 3 dit mensen blijven aandoen. Extended security support stopte in augustus 2023, en de PHP-versies waar deze sites meestal op draaien zijn grotendeels ook end-of-life. Die combinatie levert een heel specifiek type Joomla 3-incident op, waarbij de foutmelding niets zegt en de oorzaak structureel is.

De eerste tien minuten: lezen wat de server écht zegt

De Joomla-admin liet een generieke 0 - Save failed with the following error: -1 banner zien, het soort foutcode dat lijkt te bestaan om middagen te verzieken. Dat getal is geen HTTP-code en geen MySQL-code. Het komt uit de nested-set-logica binnen JTableNested, en op zichzelf vertelt het je vrijwel niets.

De eerste nuttige stap was wegkijken van de browser en gaan kijken naar de machine zelf. Het bureau had SSH op deze host, dus we tailden twee dingen tegelijk:

tail -f /var/log/apache2/error.log
tail -f /var/www/example/administrator/logs/error.php

Joomla schrijft zijn eigen log onder administrator/logs/ als log_path is ingesteld in configuration.php, en die is voor applicatiefouten bijna altijd nuttiger dan de Apache-log. Binnen één save-poging produceerde de Joomla-log de regel die de zaak openbrak:

PHP Fatal error: Uncaught RuntimeException: Left-Right data inconsistency.
Node has no valid parent. in /var/www/example/libraries/joomla/table/nested.php:760

Dat is de echte signatuur. JTableNested is de klasse achter alles in Joomla dat nested sets gebruikt: categorieën, menu's, en de #__assets-tabel die het permissiesysteem ondersteunt. Als hij tijdens een rebuild de parent van een node niet kan vinden, throwt hij en faalt de save. De 500 was alleen maar de catch-all-wrapper eromheen.

Nested sets en de #__assets-tabel

Als je alleen met WordPress hebt gewerkt, is het de moeite waard om de #__assets-tabel te begrijpen, want Joomla legt daar een groot deel van zijn ACL op neer. Elke component, categorie en elk item heeft een bijbehorende rij in #__assets die zijn positie in een permissieboom opslaat via lft- en rgt-kolommen. Wanneer je een artikel in Joomla 3 opslaat, gaat het save-pad ongeveer zo:

  1. Zoek de parent asset voor dit artikel op (meestal de asset van de categorie, of de root van de component).
  2. Voeg de rij van het artikel toe of update hem in #__assets, en bereken nieuwe lft- en rgt-waarden ten opzichte van de parent.
  3. Voeg het artikel toe of update het in #__content en koppel asset_id terug.

Stap één was waar deze site stierf. De categorie die de redacteur had gekozen, "Evenementen", was twee jaar eerder aangemaakt door een admin die inmiddels vertrokken was. Ergens onderweg, vrijwel zeker tijdens een mislukte extension-uninstall, was de rij van die categorie in #__assets verwijderd, terwijl de rij in #__categories nog steeds verwees naar het inmiddels ontbrekende asset_id. Joomla zocht de parent asset op, kreeg niets terug, en de nested-set-rebuild throwde.

De trace van 20 minuten: de ontbrekende rij bevestigen

Uit de fout hadden we al twee sterke hypothesen: een kapotte nested-set-boom, of een dangling asset_id. Beide zijn te controleren met drie korte queries. Hier is de volgorde die we daadwerkelijk hebben gedraaid, op de live database met eerst een verse back-up.

-- 1. Find any category whose asset_id does not exist in #__assets
SELECT c.id, c.title, c.asset_id
FROM   jos_categories c
LEFT   JOIN jos_assets a ON a.id = c.asset_id
WHERE  c.asset_id > 0
AND    a.id IS NULL;

Dit gaf precies één rij terug: categorie 47, "Evenementen", met asset_id = 312, en geen bijbehorende rij in jos_assets. Dat bevestigde de dangling referentie.

-- 2. Verify the nested-set tree is still consistent for the rest of #__assets
SELECT id, name, lft, rgt
FROM   jos_assets
WHERE  rgt < lft
OR     lft < 0
OR     rgt < 0;

Lege resultaatset. De boom zelf was prima. De corruptie was beperkt tot één ontbrekend blad, wat de goedkope variant van dit probleem is.

-- 3. Find the parent we want to graft under (the com_content root asset)
SELECT id, name, lft, rgt
FROM   jos_assets
WHERE  name = 'com_content';

Dat leverde id = 8 op, met bruikbare lft- en rgt-grenzen. Op dat moment was de diagnose compleet en de fix structureel: de asset-rij van categorie 47 herbouwen onder com_content en asset_id opnieuw aan koppelen.

De fix: bouw de asset opnieuw op, laat Joomla de rest doen

Er is een verleiding om met de hand een INSERT in #__assets in elkaar te zetten met zelf uitgerekende lft- en rgt-waarden. Doe dat niet. De veiligere route is een placeholder-rij invoegen, de categorie daaraan koppelen, en daarna Joomla vanuit de admin de boom laten herbouwen, wat een gedocumenteerde operatie is in de Joomla-docs.

-- Insert a minimal asset row owned by com_content (parent id 8)
INSERT INTO jos_assets
  (parent_id, lft, rgt, level, name, title, rules)
VALUES
  (8, 0, 0, 1, 'com_content.category.47', 'Evenementen', '{}');

-- Re-point the category at the new asset row
UPDATE jos_categories
SET    asset_id = LAST_INSERT_ID()
WHERE  id = 47;

Daarna, vanuit de Joomla-admin, System → Global Configuration → Permissions, één keer op Save drukken. Joomla detecteert de dirty lft/rgt-waarden en herbouwt de nested set voor de hele #__assets-tabel. Na die ene save ging de redacteur er weer in, probeerde het eerdere artikel, en dat sloeg in één keer schoon op. Van het binnenkomen van de Loom tot de groene save-banner was veertig minuten, waarvan het grootste deel ging zitten in logs lezen en de dump draaien.

Onderhoudsgewoontes voor Joomla 3-sites

Drie dingen om mee te nemen, want dit is geen eenmalig patroon.

Ten eerste: draai op elke legacy site die nog op Joomla 3 staat de dangling-asset_id-query van hierboven als routinematige health check. Het kost milliseconden en het vangt problemen voordat een redacteur het doet. Een wekelijkse cron die de bureau-eigenaar mailt als de resultaatset niet leeg is, is een kwartier werk en bespaart je meerdere dinsdagochtenden.

Ten tweede: JTableNested-fouten zijn vrijwel altijd terug te leiden naar #__assets, #__categories of #__menu. Als je "Left-Right data inconsistency" in een log ziet, kijk je ergens naar een nested-set-tabel, en het fix-patroon is steeds hetzelfde: vind de kapotte rij, beslis of je hem verwijdert of opnieuw inhangt, en laat de admin daarna herbouwen.

Ten derde: elke wijziging die je in deze tabellen maakt zou omkeerbaar moeten zijn. Wij konden snel handelen omdat de pre-fix mysqldump op de machine stond en we hem binnen een minuut terug hadden kunnen rollen als de rebuild zich misdragen had.

Toen we Pier bouwden, was dit type incident een van de patronen waar we steeds tegenaan liepen bij klanten. Onze oplossing was om elke database-write via de MySQL editor eerst een snapshot van de geraakte rijen te laten maken en die naar version history te schrijven, op dezelfde manier waarop bestandswijzigingen versioned zijn. De fix hierboven blijft de fix; het verschil is dat de rollback één klik is in plaats van een mysql < dump.sql.

Beheer je vandaag een Joomla 3-site, open dan vóór de lunch een database-client en draai de dangling-asset_id-query erop. Komt er een rij terug, dan heb je zojuist een toekomstige Loom van 07:14 gevonden.

— Vragen —

Wat betekent de Joomla-fout 'Left-Right data inconsistency' eigenlijk?

Het betekent dat een nested-set-tabel (meestal #__assets, #__categories of #__menu) een rij heeft waarvan de parent niet meer te vinden is, waardoor JTableNested de lft- en rgt-waarden tijdens het opslaan niet kan herberekenen.

Is het veilig om #__assets direct in MySQL aan te passen?

Alleen met een volledige database-dump vooraf, en alleen door placeholder-rijen toe te voegen en Joomla daarna de boom vanuit de admin te laten herbouwen. lft en rgt met de hand uitrekenen is de manier waarop mensen de hele tabel om zeep helpen.

Gebeurt dit ook op Joomla 4 en Joomla 5?

Het onderliggende nested-set-model is vergelijkbaar, maar de asset-rebuild is defensiever in nieuwere versies. Het dangling-asset_id-patroon is grotendeels een Joomla 3-probleem dat ontstaat door oude extension-uninstalls.