013 —Workflow
Legacy PHP-site snapshotten in één middag: een playbook
Je krijgt SFTP-credentials, een vage Slack-thread en een site waar niemand vier jaar aan heeft gezeten. Zo weet je voor vijf uur waar je naar kijkt.
Het is net één uur 's middags. Een bureau waar je twee keer eerder voor hebt gewerkt heeft een Bitwarden-share in je inbox gedropt: SFTP-credentials, een database-wachtwoord, de URL van een site waar niemand op het bureau in vier jaar op heeft ingelogd. Hun klant wil een offerte voor 'een beetje moderniseren' en het bureau wil voor vijven een schriftelijke beoordeling zodat ze vanavond een bedrag kunnen sturen. Je hebt de codebase nog nooit gezien. Je hebt vier uur.
Wat volgt is de playbook die we voor precies die situatie gebruiken. Hij gaat uit van een PHP legacy site op shared of VPS hosting met SFTP en directe MySQL-toegang. Om 17:00 zou je een lokale snapshot moeten hebben, een samenvatting van één pagina van wat er daadwerkelijk draait, en genoeg signaal om een bedrag te verdedigen.
De opdracht en de vier uur
Stel het doel expliciet voordat je iets opent. Je gaat niet refactoren. Je gaat niet patchen. Je produceert twee artefacten:
- Een bevroren kopie van de site (bestanden plus database) die je binnen vijftien minuten over de live site zou kunnen terugzetten.
- Een document van één pagina dat het bureau vertelt welke stack, welke versie, welke plugins, welke cron en welke bekende risico's er zijn.
Die framing doet ertoe, want de verleiding zodra je binnen bent is om de voor de hand liggende dingen te gaan fixen. Weersta die. Een snapshot is een meting. De fix is een apart gesprek, en dat gesprek gaat beter als je uit een document citeert.
Uur één: inventariseer de file tree
Open SFTP en weersta de neiging om eerst alles naar je laptop te downloaden. De meeste legacy WordPress-installaties zijn 2 tot 8 GB als je uploads meerekent, en je hebt geen uploads nodig om te weten wat de site is. Begin met vorm, niet met bytes.
Heb je shell-toegang, dan verdienen vier commando's zichzelf direct terug:
cd /var/www/site
find . -name "*.php" | wc -l
du -sh . */ 2>/dev/null | sort -h
ls -la wp-config.php sites/default/settings.php configuration.php app/etc/local.xml 2>/dev/null
php -v 2>/dev/null || php-cgi -v 2>/dev/null
Het eerste vertelt je grofweg hoeveel PHP je voor je hebt. Een kale WordPress is rond de 2.500 PHP-bestanden. Zie je er 20.000, dan kijk je naar een site volgestouwd met plugins, of een Drupal 7 met veel contrib-modules, of een Magento. Het tweede vertelt je welke directory de schijf heeft opgevreten (bijna altijd wp-content/uploads, soms een vergeten backup-db/-directory die iemand in 2019 heeft neergezet). Het derde vertelt je in één oogopslag met welk CMS je te maken hebt. Het vierde vertelt je welke PHP-versie er daadwerkelijk draait, wat ertoe doet, want shared hosts liegen in het controlepaneel tegen je.
Heb je geen shell, dan kan elke fatsoenlijke SFTP-client (Cyberduck, Transmit, FileZilla) bestandsaantallen per directory tonen. Zet een PHP-bestand van één regel in de webroot om de runtime-versie te krijgen:
<?php echo PHP_VERSION, ' ', PHP_SAPI, ' ', php_uname(); ?>
Bezoek het één keer. Verwijder het. Noteer de versie. De supported-versions pagina van PHP vertelt je of die versie nog security support krijgt, en op een legacy site is het antwoord meestal nee.
Het CMS herkennen zonder te gokken
Elk CMS laat een signatuur achter die je kunt vertrouwen:
- WordPress:
wp-config.phpin de root,wp-content/plugins/enwp-content/themes/. - Drupal:
sites/default/settings.php,core/CHANGELOG.txt(D8+), ofCHANGELOG.txtin de root (D7). - Joomla:
configuration.phpin de root met eenJConfig-klasse. - Magento 1:
app/etc/local.xml. Magento 2:app/etc/env.php. - Custom PHP: een
index.phpdie een router includeert, geen CMS-markers. Voorzichtig behandelen. Custom PHP uit 2014 is het genre dat het vaakst een vergeten admin-route bevat.
Uur twee: lees de database
Voordat je één rij leest, dump 'm. Een read-only audit is in iemands hoofd nog steeds een schrijfactie, en hosting kan op elk moment uitvallen. Een snapshot die je niet kunt herstellen is geen snapshot.
mysqldump --single-transaction --quick --routines --triggers \
-h DBHOST -u DBUSER -p DBNAME | gzip > site-$(date +%F).sql.gz
De --single-transaction-vlag is degene die je een consistente dump van een InnoDB-database laat maken zonder de site te locken. De mysqldump-referentie van MySQL legt de afwegingen tegenover MyISAM uit. Is de dump binnen twee minuten klaar, dan is de database klein genoeg om een kopie naar lokaal te trekken en de rest van de audit op je laptop te doen.
Kijk daarna naar vorm:
SELECT table_name,
engine,
table_rows,
ROUND(data_length/1024/1024, 1) AS data_mb,
ROUND(index_length/1024/1024, 1) AS index_mb
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY data_length DESC
LIMIT 20;
Deze ene query vertelt je waar het gewicht zit. Op WordPress staan bovenaan meestal wp_options (autoloaded option bloat), wp_postmeta (weesmeta van verwijderde plugins) en wp_posts (revisies). Op Magento 1 staan bovenaan bijna altijd core_url_rewrite en log_url_info, die beide niets betekenen voor gebruikers en alles voor back-ups.
Specifiek voor WordPress vertellen twee vervolgqueries je de rest van het verhaal:
SELECT option_name, LENGTH(option_value) AS bytes
FROM wp_options
WHERE autoload = 'yes'
ORDER BY bytes DESC
LIMIT 20;
SELECT post_status, COUNT(*) FROM wp_posts GROUP BY post_status;
De eerste haalt geautoloade opties boven die groter zijn dan een paar kilobyte (meestal een transient die vergeten is te vervallen, of een plugin die zijn volledige log in wp_options opslaat). De tweede vertelt je of de 80.000 rijen in wp_posts echte content zijn of 78.000 revisies.
Voor Drupal zijn de equivalenten de groottes van de cache_*-tabellen en de watchdog-tabel; voor Magento log_* en report_event. Niets hiervan hoeft vandaag gefikst te worden. Je doet een meting.
Uur drie: leg de runtime vast
De site doet dingen die je vanuit bestanden alleen niet kunt zien. Op twee plekken zit de rest van de waarheid verstopt.
De .htaccess-rondgang
Lees elke .htaccess vanaf de document root naar beneden. De interessante zitten zelden bovenaan. Een typische legacy WordPress heeft een standaardblok in de root en een handgemaakte redirect map twee directories diep die niemand heeft gedocumenteerd:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^old-product-([0-9]+)\.html$ /shop/?p=$1 [R=301,L]
RewriteCond %{HTTP_HOST} ^www\.
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]
</IfModule>
# php_value upload_max_filesize 64M
# php_value memory_limit 512M
SetEnvIf Request_URI "\.(jpg|png|css|js)$" no-log
Waar je naar zoekt: redirect-chains die dubbel hoppen, PHP-overrides die conflicteren met wat de PHP-FPM pool van de host doet, en IP-allowlists die het bureau buitensluiten op het moment dat ze van kantoor wisselen. Apache's mod_rewrite-documentatie is de referentie als een regel verdacht oogt.
Cron, queues en error logs
Voer crontab -l uit als je shell hebt. Zo niet, zoek naar de CMS-specifieke signaturen:
- WordPress:
DISABLE_WP_CRONinwp-config.phpbetekent dat ergens een echte system cronwp-cron.phpaanroept. Zoek 'm. - Drupal:
drush core:cronvanuit een system crontab, of een curl naar/cron/{key}. - Magento:
bin/magento cron:run, of het ouderecron.sh.
Is cron stuk (en op legacy sites is dat meestal het geval, omdat iemand van host is gewisseld en de entry nooit opnieuw heeft toegevoegd), dan zie je geplande posts die nooit gepubliceerd worden, transients die nooit vervallen en queue-tabellen die eindeloos groeien. Noteer het. Fix het niet.
Tail daarna de laatste 200 regels van wp-content/debug.log of welke error_log dan ook naast het script staat dat het meest crasht. Je leert in twee minuten meer over de werkelijke gezondheid van de site dan het bureau in twee jaar geleerd heeft. Herhaalde stack traces uit dezelfde plugin zijn meestal de kop van je overdrachtsdocument.
Uur vier: bevriezen, samenvatten, teruggeven
Je hebt nu alles wat je nodig hebt. Besteed het laatste uur aan het produceren van de twee artefacten.
De freeze:
tar --exclude='wp-content/cache' \
--exclude='wp-content/uploads' \
-czf files-$(date +%F).tar.gz .
Uploads uitsluiten is bewust. Je hebt de database al, je hebt de code al, en uploads zijn vanaf de live site herstelbaar als je ze ooit nodig hebt. Een code-archief van 200 MB plus een database-dump van 50 MB is een snapshot die je kunt mailen. Een tarball van 12 GB is een snapshot die op een schijf gaat staan rotten.
De samenvatting, op één pagina, met precies deze koppen:
- Stack: PHP-versie, MySQL-versie, webserver, CMS en versie.
- Surface: bestandsaantal, code-grootte, upload-grootte, top drie directories op gewicht.
- Database: totale grootte, top vijf tabellen op gewicht, charset en collation.
- Plugins of modules: aantal, namen van de vijf met de meeste database-rijen of bestanden.
- Cron: aanwezig of afwezig, mechanisme, laatste geslaagde run als je dat kunt zien.
- Risico's: PHP EOL-status, blootgestelde bestanden, redirect-chains, alles in de error log dat zich herhaalt.
- Restore: waar de snapshot staat, het ene commando om het terug te zetten.
Meer is het niet. Verstuur het. Het bureau heeft nu iets om een offerte tegenover te zetten, en jij hebt een bevroren moment waar je naar kunt terugkeren zonder alles vanaf nul opnieuw te ontdekken.
Waar de playbook je vaak bijt
Twee dingen gaan vaak genoeg mis om ze te benoemen.
De eerste: mysqldump tegen een database met stored procedures of triggers laat die stilzwijgend weg tenzij je --routines --triggers meegeeft. De site lijkt schoon terug te zetten en breekt drie dagen later bij de eerste checkout. Geef beide vlaggen altijd mee, ook als je zeker weet dat de database geen procedures heeft. Je weet het niet altijd zeker.
De tweede: bestandseigendom. Zet je via SFTP bestanden terug als jouw user, dan kan de webserver (draaiend als www-data of nginx) ze mogelijk niet lezen. Noteer tijdens je audit het eigendom van een paar representatieve bestanden (ls -la) en documenteer het. Herstellen zonder die notitie is een herstelbare fout. Herstellen zonder dat je het doorhebt is een vrijdagavondfout.
Bij het bouwen van Pier liepen we keer op keer tegen precies deze middag aan. Daarom opent de app een SFTP- en een MySQL-verbinding in hetzelfde venster en houdt hij een version history bij van elk bestand en elke rij vanaf het moment dat je verbinding maakt. De eerste audit die we ermee deden kostte twee uur in plaats van vier, vooral omdat we ophielden te tabben tussen de MySQL editor en de file tree.
Het kleinste wat je vandaag kunt doen
Pak een site waarvan je al credentials hebt en draai alleen uur één. De vier commando's bovenaan deze post, tegen een site waarvan je denkt dat je hem kent, vertellen je iets wat je nog niet wist. Dat is de hele belofte van een snapshot, en vier commando's is een klein genoeg begin om vandaag nog te doen voor de dag voorbij is.
— Vragen —
Moet ik echt de database dumpen voor ik 'm lees?
Ja. Een read-only audit kan nog steeds plugin-gedrag triggeren, en hosting kan op elk moment uitvallen. Een dump in je eerste tien minuten is de goedkoopste verzekering die je ooit afsluit.
Wat als ik geen shell-toegang heb, alleen SFTP?
Dezelfde playbook werkt over SFTP plus een MySQL-client. Je verliest de snelheid van find en du, maar de vier artefacten (bestandslijst, DB-dump, .htaccess, error log) zijn allemaal bereikbaar vanuit een desktop client.
Hoe lang moet het overdrachtsdocument zijn?
Eén pagina. Iets langers wordt diagonaal gelezen; iets korters laat een risico weg. Bulletvorm, benoemde koppen, geen verhaal. Het bureau plakt er direct uit in zijn offerte.