WordPress search and replace in de database zonder geserialiseerde data te slopen
Wijzig de site-URL van WordPress in de database zonder widgets, menu's of thema-instellingen leeg te maken. Met het WP-CLI commando dat geserialiseerde data heel laat.
Gepubliceerd
Draai geen kale UPDATE ... SET column = REPLACE(column, 'oldurl', 'newurl') over een WordPress-database. De tekst wordt keurig aangepast en je instellingen gaan er tegelijk aan, want WordPress bewaart arrays en objecten als PHP-geserialiseerde strings, en dat formaat legt van elke string de lengte in bytes vast. Een kale SQL-replace herschrijft de tekst maar raakt dat vastgelegde getal nooit aan, en de waarde is daarna onleesbaar.
Gebruik in plaats daarvan gereedschap dat serialisatie begrijpt. Op de commandoregel is dat wp search-replace. Maak eerst een back-up van de database, draai het als dry run, en pas daarna echt.
Wat er precies stukgaat
Een PHP-geserialiseerde string ziet er zo uit:
s:18:"http://example.com"
Die 18 is het aantal bytes van de tekst erachter. Tel maar na — http://example.com is precies 18 tekens.
Stel je nu een opgeslagen thema-instelling voor:
a:1:{s:4:"logo";s:18:"http://example.com";}
Vervang met kale SQL http:// door https:// en je krijgt:
a:1:{s:4:"logo";s:19:"https://example.com";}
Alleen krijg je dat niet. Je krijgt dit:
a:1:{s:4:"logo";s:18:"https://example.com";}
De string is nu 19 bytes maar beweert nog steeds 18 te zijn. PHP leest de opgegeven lengte, loopt 18 bytes vooruit, vindt daar niet het afsluitende aanhalingsteken met puntkomma en geeft het op. Het hele array laat zich niet meer unserialiseren.
Waarom het geruisloos misgaat
Dit is het deel dat de bug zo duur maakt. WordPress gooit geen fout en stopt niet. Bij het uitlezen van een optie gaat de waarde door de unserialize-helper, PHP geeft false terug voor de kapotte data, en WordPress levert je code een waarde die zich gedraagt alsof de optie nooit is ingesteld. De plugin of het thema valt vervolgens terug op zijn standaard.
Het symptoom is dus geen foutpagina. Het symptoom is:
- Widgets verdwijnen uit sidebars
- Customizer-instellingen springen terug naar de standaard van het thema
- De lay-outdata van een paginabouwer komt leeg terug en de pagina rendert leeg
- Instellingenpagina’s van plugins zien eruit alsof ze net zijn geïnstalleerd
- Menu’s raken hun toegewezen locatie kwijt
Nergens in het beheer staat dat er iets mis is. De rij staat nog in de database, nog steeds vol tekst die er correct uitziet. Alleen het lengteprefix zit er een byte of twee naast. PHP geeft wel degelijk een melding wanneer unserialiseren mislukt, maar op een productiesite met foutweergave uit ziet niemand die. Zet WP_DEBUG aan met WP_DEBUG_LOG ingeschakeld en WP_DEBUG_DISPLAY op false, en die meldingen belanden in een logbestand waar je ze wél kunt lezen.
De schade is bovendien ongelijk verdeeld. Simpele losse waarden — siteurl, home, gewone berichtinhoud, een URL in een eigen kolom — overleven een naïeve replace prima. Alleen geserialiseerde waarden gaan stuk. Daarom lijkt een verkeerde replace op het eerste gezicht vaak gelukt.
Kijk in wp-config.php voordat je iets aanraakt
Definieert wp-config.php de constanten WP_HOME of WP_SITEURL, dan winnen die het van alles wat in de database staat. wp_options aanpassen lijkt dan helemaal niets te doen. Zoek naar regels als deze voordat je concludeert dat de database het probleem is:
define( 'WP_HOME', 'http://example.com' );
define( 'WP_SITEURL', 'http://example.com' );
Werk ze bij naar de nieuwe URL of haal ze weg en laat de database het stuur overnemen.
Maak eerst een back-up, echt waar
Een gebroken serialisatie is niet altijd makkelijk terug te draaien. Zodra het lengteprefix en de inhoud niet meer overeenkomen, valt de oorspronkelijke waarde niet uit de kapotte waarde te herleiden — je kunt niet weten wat de juiste lengte had moeten zijn zonder de originele string te kennen. Een back-up is de enige echte ongedaan-maken-knop.
wp db export backup-before-replace.sql
Of met mysqldump als WP-CLI niet beschikbaar is:
mysqldump -u USER -p DBNAME > backup-before-replace.sql
Bewaar hem zo mogelijk buiten de server. Een back-up in de webroot is zowel een herstelpunt als een beveiligingsprobleem.
Het veilige commando
De search-replace van WP-CLI unserialiseert elke waarde, loopt door de gedecodeerde structuur, vervangt daarbinnen en serialiseert opnieuw met kloppende lengtes. Eerst een dry run:
wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid
Lees het rapport. Het vertelt hoeveel vervangingen er per tabel en kolom komen. Ziet de tabellijst er vreemd uit, of heeft een tabel die je niet herkent duizenden treffers, stop dan en kijk eerst voordat je het echt draait.
En dan het echte werk:
wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only
Eerlijk over de opties:
--all-tablesneemt ook tabellen mee die WordPress zelf niet registreert, en daar zit plugin-data vaak. Zonder deze optie worden sommige plugintabellen volledig overgeslagen.--skip-columns=guiddoet ertoe. Deguidis een permanente identificatie voor feedlezers, geen levende URL. Hem herschrijven kan abonnees je hele archief als nieuwe berichten voorschotelen.- Zoek zonder afsluitende schuine streep en zonder de protocolvarianten die je niet wilt raken. Zoek je op kaal
example.com, dan raak je ook e-mailadressen en elke vermelding in de berichtteksten.
Kun je de commandoregel niet gebruiken, dan doen migratieplugins die met geserialiseerde data overweg kunnen hetzelfde decodeerwerk via het beheerscherm. Het gedrag waar je naar zoekt wordt met zoveel woorden omschreven als het correct verwerken van geserialiseerde data; zegt een tool dat niet, ga er dan van uit dat het niet zo is.
Welke tabellen en kolommen erbij betrokken zijn
| Tabel | Kolom | Risico op serialisatie |
|---|---|---|
| wp_options | option_value | Hoog — widgets, theme mods, transients, de meeste plugin-instellingen |
| wp_postmeta | meta_value | Hoog — lay-outs van paginabouwers, arrays met aangepaste velden |
| wp_usermeta | meta_value | Hoog — rechten, scherm- en dashboardvoorkeuren |
| wp_termmeta | meta_value | Gemiddeld — hangt van de plugins af |
| wp_posts | post_content, guid | Laag voor inhoud, maar shortcode-attributen en blokmarkup bevatten URL’s |
| wp_comments | comment_content, comment_author_url | Laag |
| wp_links | link_url, link_image | Laag, en vaak ongebruikt |
Bij multisite komen daar wp_blogs, wp_site, wp_sitemeta bij, plus de tabellen per site zoals wp_2_options, wp_3_options enzovoort. Een URL-wijziging bij multisite kent extra regels bovenop een search and replace — behandel dat als een aparte klus.
De gevallen die search-replace niet volledig oplost
Even eerlijk over de grenzen:
JSON die in de database staat. Sommige paginabouwers slaan lay-outdata op als JSON met ge-escapete schuine strepen, waardoor je URL er als https:\/\/example.com uitziet in plaats van https://example.com. Zoeken op de normale vorm mist die dan. Mogelijk heb je een tweede ronde nodig op de ge-escapete variant. Test op een kopie.
Base64-gecodeerde waarden. Codeert een plugin zijn instellingen voordat hij ze opslaat, dan kan geen enkele tekstgebaseerde replace de URL zien. Zulke instellingen moeten meestal opnieuw worden ingevoerd via het eigen scherm van de plugin.
URL’s die hard in bestanden staan. Alles wat in functions.php, een template van een child theme of een vast asset-pad is geschreven, staat niet in de database en wordt dus niet aangeraakt. Grep de themamap apart.
Gecachte en gegenereerde uitvoer. Objectcache, paginacache en CDN-kopieën blijven de oude URL’s serveren nadat de replace correct is verlopen. Leeg ze en controleer opnieuw voordat je concludeert dat de replace mislukt is.
Als je de foute query al hebt gedraaid
Zet de back-up terug. Dat is het antwoord, en het is beter dat gewoon te zeggen dan een herstelprocedure aan te bieden die meestal niet werkt.
Is er geen back-up, dan is het pad smal: waarden die nooit geserialiseerd waren zijn in orde, en de kapotte geserialiseerde waarden moeten met de hand worden gereconstrueerd of gereset door het betreffende instellingenscherm opnieuw op te slaan. Een widgetgebied opnieuw opslaan, een menulocatie opnieuw kiezen of de instellingen van een plugin opnieuw invoeren schrijft een verse, correct geserialiseerde waarde over de kapotte heen. Bewerkelijk, maar het werkt, en het is beter dan corrupte rijen laten staan waar ze de volgende persoon die naar de site kijkt in verwarring brengen.
Wil je de juiste statements opstellen voordat je iets uitvoert, bouw ze dan met de WordPress search and replace SQL-tool — die laat zien welke kolommen je veilig met kale SQL kunt aanpakken en welke serialisatiebewuste afhandeling nodig hebben, zodat je de scheidslijn ziet voordat je je aan een commando vastlegt.
FAQ
Vragen
Waarom sloopt een gewone SQL REPLACE een WordPress-site?
Omdat WordPress arrays en objecten opslaat als PHP-geserialiseerde tekst, en dat formaat legt van elke string de lengte in bytes vast. Een kale REPLACE past de tekst aan maar laat het oude lengtegetal staan. PHP weigert de waarde dan te unserialiseren, WordPress valt terug op een standaardwaarde, en instellingen lijken leeg in plaats van fout.
Wat is de veiligste manier om de site-URL van WordPress in de database te wijzigen?
Maak eerst een volledige databaseback-up en draai daarna het WP-CLI commando wp search-replace, met een dry run vooraf. WP-CLI unserialiseert elke waarde, vervangt binnen de gedecodeerde structuur en serialiseert opnieuw met kloppende lengtes. Migratieplugins die met geserialiseerde data overweg kunnen doen hetzelfde werk via het beheerscherm, als je geen toegang tot de commandoregel hebt.
In welke tabellen staat de oude URL?
Vooral in wp_options, wp_posts, wp_postmeta, wp_usermeta, wp_termmeta en wp_comments. Het gevaar van serialisatie zit in wp_options en in elke meta_value-kolom, omdat widgets, theme mods, transients en data van paginabouwers als arrays worden opgeslagen. Bij multisite dragen wp_blogs, wp_site en wp_sitemeta het domein ook mee.
Helpt het aanpassen van de database als wp-config.php de URL vastlegt?
Nee. Definieert wp-config.php de constanten WP_HOME of WP_SITEURL, dan winnen die het van wat siteurl en home in wp_options bevatten. Je kunt de database aanpassen zoveel je wilt, er verandert niets zichtbaars tot die constanten zijn bijgewerkt of verwijderd. Kijk dus eerst in wp-config.php; die ene regel verklaart heel wat mislukte migraties.
Moet ik de kolom guid ook vervangen?
Nee. De guid is een permanente identificatie voor feedlezers, geen werkende URL, en hem herschrijven kan ervoor zorgen dat abonnees al je oude berichten opnieuw als nieuw binnenkrijgen. Geef skip-columns met guid mee wanneer je WP-CLI draait. De enige gangbare uitzondering is een site die nooit publieke feeds of abonnees heeft gehad.
Hoe weet ik of een waarde stuk is na een verkeerde replace?
Kapotte geserialiseerde waarden zien er in de database normaal uit maar komen in WordPress leeg terug. Widgets verdwijnen, instellingen in de customizer springen terug, en plugin-opties vallen zonder enige foutmelding terug op hun standaard. Zet WP_DEBUG en WP_DEBUG_LOG aan en de unserialize-melding komt bovendrijven. De opgegeven stringlengte vergelijken met het werkelijke aantal bytes bevestigt het direct.