Updates für deaktivierte Plugins: Wie sich meine WordPress-Plugins gegenseitig aktualisieren
WordPress führt deaktivierte Plugins nicht aus, also auch nicht ihren Updater. Wie Updater 1.5.0 das für meine Plugin-Sammlung löst: Registry, Leader-Wahl, Prüfsummen-Sperre — am Beispiel von JPKCom Harden & Trim.
von Jean Pierre Kolb ·
Im Juli habe ich in einem Making-of beschrieben, wie meine WordPress-Plugins sich außerhalb von wordpress.org selbst aktualisieren: GitHub Actions baut ZIP, Prüfsumme und Manifest, eine kleine Updater-Klasse im Plugin hängt sich in das Update-System von WordPress. Das funktioniert seitdem zuverlässig — mit einer Lücke, die in der Bauform selbst steckt. Ein deaktiviertes Plugin bekommt nie ein Update.
Der Grund ist schlicht: WordPress lädt den Code deaktivierter Plugins nicht. Die Updater-Klasse liegt aber im Plugin. Ist das Plugin aus, läuft sein Updater nicht, und WordPress erfährt nie, dass es eine neue Version gibt. Das alte Plugin bleibt liegen, und wer es in einem Jahr wieder einschaltet, schaltet die Version von vor einem Jahr ein.
Mit den Releases vom 21. Oktober 2026 ist das für alle meine Plugins behoben. Jedes bringt jetzt Updater 1.5.0 mit, und eine einzige geladene Kopie aktualisiert die ganze Sammlung — aktive und deaktivierte Plugins. In diesem Artikel zeige ich, wie das funktioniert, wo die Sicherheitsfallen liegen und was du als Betreiber dafür tun musst. Als durchgehendes Beispiel dient JPKCom Harden & Trim, mein jüngstes Plugin; der Updater ist in allen Plugins bis auf Namespace und Slug identisch.
Wenn du nur wissen willst, was sich für deine Seite ändert: Spring zur Kurzfassung oder zur Checkliste.
Die Kurzfassung für Seitenbetreiber
| Was | Bis Updater 1.4 | Ab Updater 1.5.0 |
|---|---|---|
| Deaktiviertes JPKCom-Plugin | bekommt nie ein Update | bekommt Updates, sobald irgendein JPKCom-Plugin mit Updater 1.5.0 oder neuer aktiv ist (Standard-Ordner, kein Git-Checkout) |
| „Details anzeigen" | nur für aktive Plugins | für alle installierten JPKCom-Plugins |
| Prüfsumme (SHA-256) | für aktive Plugins | für jedes Plugin, auch für ein Paket, das ein anderes Plugin liefert |
| Download-Adressen in der Datenbank | gespeichert | nicht mehr gespeichert, nur während ein Updater läuft |
| Git-Checkout, umbenannter Ordner | wurde mit dem ZIP überschrieben | Hinweis auf neue Version, aber kein automatisches Update |
| Wer den Update-Abruf auslöst | jeder Aufruf, der die Update-Daten liest | nur Konten mit Update-Recht, WP-Cron und WP-CLI |
Damit das greift, muss mindestens ein aktives JPKCom-Plugin auf seiner Version mit Updater 1.5.0 stehen — bei Harden & Trim ist das 1.1.3, bei Post Filter 1.4.7, bei den anderen Plugins steht die Version im jeweiligen Changelog. Die neuen Schutzmechanismen gelten für ein aktives Plugin aber erst, wenn es selbst auf diesem Stand ist; bring also jedes aktive Plugin einmal dorthin.
Das Problem genau: Warum wordpress.org-Plugins das können
Bei Plugins aus dem offiziellen Verzeichnis kennst du das Problem nicht. Auch ein deaktiviertes Plugin zeigt dort „Eine neue Version von … ist verfügbar." Das liegt nicht am Plugin, sondern an WordPress selbst: Die Funktion wp_update_plugins() liest per get_plugins() alle installierten Plugins aus — aktive und deaktivierte — und schickt die Liste an api.wordpress.org. Die Antwort landet im Site-Transient update_plugins, aus dem der Update-Bildschirm seine Zeilen baut.
Ein selbst gehostetes Plugin hat diesen Weg nicht. Es muss sich selbst in den Transient eintragen, und dafür braucht es laufenden Code: einen Filter auf site_transient_update_plugins, der beim Lesen die eigene Zeile ergänzt. Diesen Filter registriert der Updater im Plugin. Ein deaktiviertes Plugin registriert ihn nie.
Die naheliegenden Auswege haben alle einen Haken:
- Ein eigenes Update-Plugin, das alle anderen bedient. Dann muss das jeder Betreiber zusätzlich installieren und aktiv halten, und es wird zum Single Point of Failure.
- Ein Must-use-Plugin, das immer läuft. Das lässt sich nicht über den normalen Plugin-Weg installieren und aktualisieren.
- Den Code des deaktivierten Plugins trotzdem laden. Das widerspricht dem, was „deaktiviert" für den Betreiber bedeutet.
Meine Lösung nutzt, was ohnehin da ist: Jedes meiner Plugins trägt eine Kopie des Updaters. Solange auch nur eines davon aktiv ist, läuft mindestens eine Kopie. Die muss nur wissen, dass es die anderen gibt.
Die Idee: eine Familie statt Einzelkämpfern
Updater 1.5.0 macht aus den einzelnen Kopien eine Familie. Jede Kopie trägt sich beim Start in eine gemeinsame Liste ein, eine davon wird gewählt und erledigt die Arbeit für alle installierten JPKCom-Plugins, die anderen bleiben passiv.
init, Priorität 5
┌─────────────────────────┐ ┌─────────────────────────┐
│ JPKCom ACF Jobs (aktiv) │ │ Allow Block Types │
│ Updater 1.5.0 │ │ (aktiv), Updater 1.5.0 │
└────────────┬────────────┘ └────────────┬────────────┘
└──────────┬──────────────────┘
▼
$GLOBALS['jpkcom_git_updater_registry']
│ höchste UPDATER_VERSION gewinnt,
│ bei Gleichstand die erste Kopie
▼
Leader: z. B. die Kopie von ACF Jobs
│
┌───────────────────┼───────────────────────┐
▼ ▼ ▼
ACF Jobs Allow Block Types Harden & Trim (deaktiviert)
Update-Zeile, Update-Zeile, gefunden per Ordner-Scan:
Icon, Details Icon, Details Update-Zeile, Icon, DetailsDrei Bausteine tragen das: die Registry, die Leader-Wahl und die Suche nach deaktivierten Plugins.
Die Registry: eine Liste in $GLOBALS
Jede Kopie des Updaters lebt in ihrem eigenen Namespace — bei Harden & Trim JPKComHardenTrimGitUpdate, bei ACF Jobs ein anderer. Das ist Absicht: So kollidieren die Klassen nicht, auch wenn zwei Plugins verschiedene Updater-Versionen mitbringen. Die Kehrseite ist, dass sich die Kopien nichts teilen, nicht einmal statische Eigenschaften. Der einzige gemeinsame Ort in PHP ist globaler Zustand, also trägt sich jede Kopie im Konstruktor dort ein:
private const REGISTRY = 'jpkcom_git_updater_registry';
private function register(): void {
if ( ! isset( $GLOBALS[ self::REGISTRY ] ) || ! is_array( $GLOBALS[ self::REGISTRY ] ) ) {
$GLOBALS[ self::REGISTRY ] = [];
}
$GLOBALS[ self::REGISTRY ][] = [
'v' => self::UPDATER_VERSION, // '1.5.0' — decides the election
'id' => spl_object_id( $this ),
'file' => plugin_basename( $this->plugin_file ),
'slug' => $this->plugin_slug,
'manifest' => $this->manifest_url,
'version' => $this->current_version,
];
}Die Einträge sind bewusst einfache Arrays mit skalaren Werten. Keine Kopie ruft je eine Methode auf einem fremden Eintrag auf, ein kaputter Eintrag kann eine andere Kopie also nicht mitreißen. Künftige Versionen dürfen Schlüssel ergänzen, aber laut Code-Kommentar keine umbenennen oder umtypisieren.
Die Leader-Wahl: höchste Version gewinnt
Wer die Arbeit macht, entscheidet ein einfacher Vergleich: Die Kopie mit der höchsten UPDATER_VERSION gewinnt, bei Gleichstand die zuerst eingetragene. Das hat einen schönen Nebeneffekt: Bringe ich später Updater 1.6.0 heraus, übernimmt automatisch die neueste Kopie, sobald das erste aktive Plugin damit aktualisiert ist.
private function elect( array $entries ): ?int {
$best = null;
foreach ( $entries as $entry ) {
// Looser than the registry check on purpose: a future copy that changes
// its entry shape must still win, instead of leaving two leaders.
if ( ! $this->looks_like_a_copy( $entry ) ) { // abridged
continue;
}
if ( null === $best || version_compare( $entry['v'], $best['v'], '>' ) ) {
$best = $entry; // strictly greater: ties stay with the first copy
}
}
return null !== $best ? $best['id'] : null;
}Eine Feinheit steckt im Zeitpunkt. Die Plugins starten ihren Updater nacheinander auf init mit Priorität 5. Liest WordPress den Update-Transient, bevor alle Kopien eingetragen sind, wäre ein einmal gespeichertes Wahlergebnis falsch. Deshalb wählt jede Kopie bei jedem Hook-Aufruf neu — aber nur, wenn die Registry seit der letzten Wahl gewachsen ist. Alle Kopien registrieren alle Hooks; jeder Callback, der für die Familie arbeitet, fragt is_leader() ab und kehrt bei jeder Kopie außer dem Leader sofort zurück.
Deaktivierte Plugins finden
Der Leader kennt aus der Registry nur die aktiven Plugins. Die deaktivierten muss er selbst suchen. Dafür liest er das Plugin-Verzeichnis und prüft jeden Ordner, der wie ein JPKCom-Plugin aussieht:
private const PLUGIN_URI_PATTERN = '~^https://github\.com/JPKCom/(jpkcom-[a-z0-9-]+)/?\z~';
foreach ( scandir( WP_PLUGIN_DIR ) as $slug ) {
$path = WP_PLUGIN_DIR . '/' . $slug . '/' . $slug . '.php'; // canonical layout only
if ( 1 !== preg_match( self::SLUG_PATTERN, $slug ) || ! is_file( $path ) ) {
continue;
}
$data = get_plugin_data( $path, false, false );
// Plugin URI must point to the JPKCom repository of exactly this slug.
if ( 1 !== preg_match( self::PLUGIN_URI_PATTERN, $data['PluginURI'] ?? '', $match )
|| $match[1] !== $slug ) {
continue;
}
$found[ "$slug/$slug.php" ] = [ /* slug, version, manifest URL, frozen … */ ];
}Drei Entscheidungen stecken darin:
- Erkennungsmerkmal ist die
Plugin URI, nicht der neuere HeaderUpdate URI. DiePlugin URIträgt jede je veröffentlichte Version jedes meiner Plugins, denUpdate URI-Header erst die Releases vom 19. Oktober 2026. So findet der Leader auch ein deaktiviertes Plugin, das seit Monaten auf einer alten Version steht. - Nur das Standard-Layout
<slug>/<slug>.php, und nur Ordner, die mitjpkcom-beginnen. Der Scan läuft in jedem Request, der den Update-Transient liest, also soll er keine fremden Plugin-Header parsen. scandir()stattglob(): Enthält der Pfad zum Plugin-Verzeichnis eckige Klammern, würde ein Glob-Muster daraus eine Zeichenklasse machen und stillschweigend nichts finden.
Zuletzt prüft der Leader, ob der Code eines gefundenen Plugins in diesem Request tatsächlich geladen wurde — über get_included_files(), nicht über die Option active_plugins. Der Unterschied zählt bei wp --skip-plugins=…: Dann steht ein Plugin als aktiv in der Datenbank, sein Code läuft aber nicht. Der Leader behandelt es wie ein deaktiviertes.
Was der Leader dann tut
Für jedes Mitglied der Familie holt der Leader das Manifest von GitHub Pages und schreibt beim Lesen des Transients eine Update-Zeile mit Version, Icon und Kompatibilitätsangaben. Den Filter plugins_api beantwortet er ebenfalls für alle Mitglieder, sodass „Details anzeigen" auch bei einem deaktivierten Plugin funktioniert. Das ist nicht nur Komfort: Ohne Antwort fiele WordPress für das Detailfenster auf wordpress.org zurück — und dort könnte unter demselben Slug ein fremdes Plugin liegen.
Mit älteren Kopien kommt der Leader ebenfalls klar. Läuft in einem aktiven Plugin noch ein Updater vor 1.5, kennt der die Registry nicht und bedient sein Plugin weiter selbst. Der Leader erkennt das (geladen, aber nicht eingetragen) und schreibt für dieses Plugin weder Update-Zeile noch Details, damit sich nicht zwei Updater über Manifest und Paket uneinig werden. Nur wenn die alte Kopie beim Download kein Paket liefert, prüft seine Sperre die Prüfsumme an ihrer Stelle.
Die Sicherheitsseite: Wer installiert eigentlich was?
Ein Updater, der für fremde Plugins Pakete anbietet, ist ein heikler Baustein. Die Prüfsumme aus dem ersten Artikel bleibt das Fundament, aber mit der Familie kommen neue Wege hinzu, auf denen ein ungeprüftes Paket durchrutschen könnte. Drei davon schließt Updater 1.5.0 ausdrücklich.
Keine Download-Adressen in der Datenbank
Der Update-Transient wird in der Datenbank gespeichert. Steht darin die Download-Adresse eines Pakets, kann WordPress es später installieren — auch in einem Request, in dem gar kein JPKCom-Updater geladen ist, etwa wenn alle meine Plugins deaktiviert sind oder WP-CLI mit --skip-plugins läuft. Dann prüft niemand die Prüfsumme.
Deshalb entfernt der Leader die Paket-Adressen seiner Familie, bevor der Transient gespeichert wird, und setzt sie bei jedem Lesen wieder ein:
// Hooked on pre_set_site_transient_update_plugins at PHP_INT_MAX.
public function strip_packages( mixed $value ): mixed {
if ( ! $this->is_leader() || ! is_object( $value ) || empty( $value->response ) ) {
return $value;
}
$members = $this->members();
$value = clone $value;
foreach ( $value->response as $file => $entry ) {
$package = is_string( $entry->package ?? null ) ? $entry->package : '';
if ( isset( $members[ $file ] ) || 1 === preg_match( self::PACKAGE_PATTERN, $package ) ) {
$entry = clone $entry;
$entry->package = ''; // re-injected on every read by check_update()
$value->response[ $file ] = $entry;
}
}
return $value;
}Findet WordPress beim Update ein leeres Paket, verweigert es die Aktualisierung mit seiner eigenen Meldung, statt ein ungeprüftes ZIP zu installieren. Der Code-Kommentar nennt die Grenze selbst: Ein drittes Plugin, das die Option direkt in die Datenbank schreibt, umgeht diesen Filter. Es ist eine Absicherung nach bestem Vermögen, kein Beweis.
Die Prüfsummen-Sperre prüft auch fremde Pakete
Der wichtigste Hook ist weiter upgrader_pre_download. Er feuert, bevor WordPress ein Paket herunterlädt, und darf den Vorgang abbrechen oder selbst eine Datei liefern. Mit mehreren Updatern und womöglich weiteren Plugins, die sich dort einklinken, stellt sich eine neue Frage: Was, wenn ein früherer Filter schon eine Datei zurückgegeben hat?
Bis 1.4 hing die Sperre auf der Standard-Priorität 10 und prüfte nur das Paket ihres eigenen Plugins; ein später eingehängter Filter konnte ihre geprüfte Datei noch ersetzen. Seit 1.5.0 hängt sie mit Priorität PHP_INT_MAX - 100 sehr spät und behandelt jedes Paket, das zur Familie gehört, gleich: Sie rechnet den SHA-256-Wert dessen aus, was tatsächlich installiert würde, egal wer die Datei geliefert hat.
// Fail closed: no manifest or no checksum is an error, not a legacy case.
$remote = $this->get_remote_manifest( $slug, $manifest );
if ( ! $remote ) {
return new \WP_Error( 'manifest_unavailable', /* … */ );
}
if ( empty( $remote->checksum_sha256 ) ) {
return new \WP_Error( 'checksum_missing', /* … */ );
}
$expected = strtolower( trim( $remote->checksum_sha256 ) );
if ( false !== $reply ) {
// Someone already supplied the package: hash what will be installed.
$hash = hash_file( 'sha256', $reply );
return hash_equals( $expected, $hash ) ? $reply : $this->checksum_error( $hash, $expected );
}
$temp_file = download_url( $url );
$hash = hash_file( 'sha256', $temp_file );
if ( ! hash_equals( $expected, $hash ) ) {
wp_delete_file( $temp_file );
return $this->checksum_error( $hash, $expected );
}
// Hand over the verified file, so WordPress installs exactly these bytes.
return $temp_file;Zwei Details aus der Zeit seit dem ersten Artikel stecken hier mit drin. Seit Updater 1.3.0 ist die Prüfung zwingend: Fehlt im Manifest die Prüfsumme, bricht das Update ab. Im Making-of stand noch die nachsichtige Variante; wer das Manifest manipulieren könnte, müsste so nur ein Feld löschen, um die Prüfung abzuschalten. Und die Sperre gibt die geprüfte Datei direkt an WordPress zurück. Würde sie nur „passt" melden, lüde WordPress das Paket ein zweites Mal herunter und installierte Bytes, die niemand geprüft hat.
Bei einer Installation per URL, etwa wp plugin install <url>, steht kein Plugin-Name im Aufruf. Dann ordnet die Sperre das Paket über den Dateinamen <slug>.zip zu, nicht über eine bestimmte Schreibweise der Adresse. GitHub liefert dieselbe Release-Datei unter releases/latest/download/, in beliebiger Groß- und Kleinschreibung, mit www., über http und mit Query-String aus. Jede dieser Varianten muss die Prüfsumme bestehen. Im Code-Kommentar steht der Grundsatz dahinter: Zu viel zu beanspruchen ist sicher, denn ein ZIP, das nicht zum Manifest passt, wird abgelehnt.
Wer den Abruf auslöst
Ein Leader, der für bis zu 19 Plugins Manifeste holt, darf keine Seite ausbremsen. Der Update-Transient wird nämlich auch im Frontend gelesen, etwa für die Admin-Leiste. Seit 1.5.0 holt der Updater ein Manifest, das nicht im Cache liegt, beim bloßen Anzeigen nur noch unter drei Bedingungen:
- für angemeldete Konten mit dem Recht
update_plugins, - in einem WP-Cron-Lauf, etwa dem Update-Cron von WordPress,
- in WP-CLI.
Der Seitenaufruf eines anonymen Besuchers holt nie selbst ein Manifest; das übernimmt höchstens ein Cron-Lauf im Hintergrund. Dazu kommen ein Budget von höchstens fünf ungecachten Abrufen pro interaktivem Request, ein Schutzschalter, der bei einem gestörten oder langsamen GitHub Pages eine Viertelstunde Ruhe gibt, und gestaffelte Cache-Laufzeiten: Jedes Manifest lebt einen Tag plus einen vom Slug abhängigen Zuschlag von bis zu sechs Stunden. So laufen nicht alle Caches im selben Request ab.
Wo der Updater bewusst nicht aktualisiert
Zwei Fälle markiert der Updater als eingefroren: Das Plugin erfährt von einer neuen Version, bekommt aber kein Paket.
Git- und andere Versionskontroll-Checkouts. Liegt im Plugin-Ordner ein .git, .svn, .hg oder .bzr, würde das Update den Ordner durch den Inhalt des ZIPs ersetzen und den Arbeitsstand löschen. Die Ordnerliste und den Filter automatic_updates_is_vcs_checkout übernimmt der Updater von den automatischen Updates des Core. Anders als der Core prüft er aber den einzelnen Plugin-Ordner: Der Core schaut nur auf das Plugin-Verzeichnis und alles darüber und bemerkt einen Checkout in einem einzelnen Plugin gar nicht. Einen Checkout aktualisierst du per git pull.
Umbenannte Ordner. Lädst du bei GitHub den Quelltext eines Repositorys als ZIP herunter, heißt der Ordner darin jpkcom-harden-trim-main. Das Release-ZIP entpackt aber nach jpkcom-harden-trim/. Ein Update würde den umbenannten Ordner löschen, einen neuen danebenlegen — und das aktive Plugin wäre weg. Deshalb gibt es auch hier nur den Hinweis.
In der Plugin-Liste zeigt WordPress in beiden Fällen seine eigene Meldung („Eine automatische Aktualisierung für dieses Plugin ist nicht verfügbar."). Versucht jemand trotzdem ein Update, etwa über Dashboard → Aktualisierungen oder über eine veraltete Zeile, die ein anderer geschrieben hat, lehnt die Prüfsummen-Sperre es mit dem Fehler no_package ab („Das Aktualisierungs-Paket ist nicht verfügbar."). Eine Einschränkung ist wichtig: Ein deaktiviertes Plugin in einem umbenannten Ordner findet der Updater gar nicht, weil der Scan nur das Standard-Layout kennt. Es bekommt dann auch keinen Hinweis.
Am Beispiel: Harden & Trim deaktiviert
So sieht das konkret aus. Auf einer Seite sind JPKCom ACF Jobs und JPKCom Allow Block Types aktiv, beide mit Updater 1.5.0. Harden & Trim ist installiert, aber deaktiviert, und steht auf 1.1.3 — der ersten Version mit Updater 1.5.0. Am 21. Oktober erscheint 1.2.0 mit dem neuen WP-CLI-Befehl wp harden-trim.
- Der Update-Cron von WordPress liest den Transient
update_plugins. Beide aktiven Plugins starten aufinitihren Updater und tragen sich in die Registry ein. Bei Gleichstand gewinnt die zuerst eingetragene Kopie. - Der Leader findet beim Ordner-Scan
jpkcom-harden-trim/jpkcom-harden-trim.php, prüft diePlugin URIund stellt fest, dass der Code nicht geladen ist. Harden & Trim wird Mitglied der Familie. - Er holt
https://jpkcom.github.io/jpkcom-harden-trim/plugin_jpkcom-harden-trim.json, vergleicht 1.1.3 mit 1.2.0 und schreibt eine Update-Zeile mit dem JPKCom-Logo als Icon. Bevor WordPress den Transient speichert, entfernt er die Paket-Adresse daraus wieder. - Unter Dashboard → Aktualisierungen erscheint Harden & Trim mit Version 1.2.0. „Details der Version 1.2.0 anzeigen" öffnet die Änderungen aus dem Manifest, „Details anzeigen" in der Plugin-Liste die Beschreibung.
- Startest du das Update, setzt der Leader die Adresse beim Lesen wieder ein. Die Prüfsummen-Sperre lädt das ZIP, vergleicht den SHA-256-Wert mit dem Manifest und gibt die geprüfte Datei an WordPress. Harden & Trim steht auf 1.2.0 — und ist weiter deaktiviert.
Dasselbe gilt auf der Kommandozeile. Mit wp plugin update jpkcom-harden-trim aktualisiert WP-CLI das deaktivierte Plugin, solange ein anderes JPKCom-Plugin mit Updater 1.5.0 aktiv ist und geladen wird. Mit --skip-plugins dagegen läuft kein Updater, die Update-Zeile hat kein Paket, und WordPress verweigert das Update — genau so soll es sein.
Wie ich das teste
Der Updater ist Sicherheitscode, der in 19 Plugins gleich arbeiten muss. Zwei Dinge sichern das ab.
Eine eigene Testsuite für die Familie. tests/test-updater-family.php läuft ohne WordPress: Die WordPress-Funktionen, die der Updater braucht, sind nachgebaut — Site-Transients im Speicher, ein kleines Hook-System mit Prioritäten, Header-Parsing über echte Dateien, HTTP und download_url() über Testdateien. In einem temporären Plugin-Verzeichnis liegen erfundene JPKCom-Plugins: im Standard-Layout, in umbenannten Ordnern, mit fremder Plugin URI, als Git-Checkout und eines, in dem noch ein alter Updater läuft. Mehrere Kopien des Updaters lädt die Suite gleichzeitig, indem sie den Namespace umschreibt, für die Wahl-Tests auch die UPDATER_VERSION.
Ich habe die Suite für diesen Artikel aus dem Tag v1.2.0 von Harden & Trim noch einmal laufen lassen:
php tests/test-updater-family.php
# 170 passed, 0 failed
UPDATER_FILE=/pfad/zum/updater-1.4.0.php php tests/test-updater-family.php
# 59 passed, 96 failedDer zweite Lauf ist der wichtigere. Gegen den Updater 1.4.0 aus Harden & Trim 1.1.1 muss die Suite scheitern — und zwar mit benannten Prüfungen, nicht mit einem Absturz. Laut Kopfkommentar der Suite fängt sie dafür jeden Aufruf ab, den 1.4.0 nicht beantworten kann. Eine Testsuite, die gegen den alten Code nicht rot wird, testet das neue Verhalten nicht.
Ein Fingerabdruck gegen Abweichungen. In Harden & Trim vergleicht tests/test-debug-gate.php einen normalisierten SHA-256-Wert der eigenen Updater-Datei — Namespace und Slug herausgerechnet — mit dem der Referenzkopie aus JPKCom Allow Block Types. Weicht die Kopie ab, schlägt der Test fehl. Die Familien-Suite selbst liegt in allen 19 Plugins byte-gleich.
Was du als Betreiber tun musst
Die gute Nachricht: fast nichts, und das Meiste nur einmal.
# JPKCom-Plugins: Umstieg auf Updater 1.5.0
- [ ] Jedes aktive JPKCom-Plugin einmal auf seine Version mit Updater 1.5.0 aktualisiert
- [ ] Mindestens eines davon bleibt aktiv — dann bekommen auch alle deaktivierten ihre Updates
- [ ] Multisite: dieses Plugin ist netzwerkweit oder auf der Hauptseite aktiv
- [ ] Plugins in umbenannten Ordnern (z. B. `jpkcom-…-main`) durch das Release-ZIP ersetzt
- [ ] Git-Checkouts: bewusst so gewollt? Dann weiter per `git pull` aktualisierenDeaktivierte Plugins musst du dafür nicht anfassen. Auch eines, das noch auf einem alten Updater steht, findet der Leader über seine Plugin URI und bietet ihm das Update an — der alte Code darin spielt keine Rolle, weil er ohnehin nicht läuft. Anders bei aktiven Plugins mit altem Updater: Die bedienen sich weiter selbst, ohne die neuen Schutzmechanismen. Erst wenn alle aktiven auf 1.5.0 stehen, gilt das Verhalten aus diesem Artikel überall.
Auf Multisite reicht ein Plugin, das nur auf einer Unterseite aktiv ist, nicht: Updates verwaltet das Netzwerk, und dort muss ein Updater laufen. Fehlt er, wird nichts angeboten — aber auch nichts Ungeprüftes installiert.
Grenzen und offene Punkte
- Ohne aktives JPKCom-Plugin keine Updates. Sind alle meine Plugins deaktiviert, läuft kein Updater. Dann bleibt alles auf dem alten Stand, bis eines wieder aktiv ist. Das ist die bewusste Kehrseite davon, keinen Code deaktivierter Plugins zu laden.
- Best Effort beim Entfernen der Adressen. Wer den Transient an den Filtern vorbei direkt in die Datenbank schreibt, umgeht
strip_packages(). Die Prüfsummen-Sperre greift trotzdem, sobald ein Updater ab 1.5.0 geladen ist. - Prüfsumme, keine Signatur. Daran hat sich seit dem Making-of nichts geändert: SHA-256 belegt, dass das ZIP dem Manifest entspricht, nicht, wer es gebaut hat. Wie Pipeline, Prüfsumme und Manifest zusammenspielen, beschreibt der erste Artikel.
- Fremde Plugins bleiben außen vor. Die Familie erkennt nur Plugins mit meiner
Plugin URIund einem Slug, der mitjpkcom-beginnt. Das Muster lässt sich auf eigene Plugin-Sammlungen übertragen, die Klasse selbst ist auf meine Sammlung zugeschnitten.
FAQ
Muss ich dafür ein zusätzliches Plugin installieren?
Nein. Jedes JPKCom-Plugin bringt den Updater mit. Es reicht, dass eines davon aktiv ist und auf seiner Version mit Updater 1.5.0 steht.
Lädt WordPress jetzt den Code meiner deaktivierten Plugins?
Nein. Der Leader liest nur den Plugin-Header der deaktivierten Plugins — dieselbe Funktion get_plugin_data(), mit der WordPress die Plugin-Liste anzeigt. Ausgeführt wird von ihnen nichts.
Was passiert, wenn zwei Plugins verschiedene Updater-Versionen haben?
Die Kopie mit der höchsten Version wird Leader. Kopien ab 1.5.0 ordnen sich unter. Eine aktive Kopie vor 1.5 bedient ihr eigenes Plugin weiter selbst; der Leader schreibt dafür keine Update-Zeile und springt nur bei der Prüfsumme ein, wenn die alte Kopie kein Paket liefert.
Warum wird mein Plugin aus dem GitHub-Quelltext-ZIP nicht aktualisiert?
Weil der Ordner dann …-main heißt und ein Update ihn löschen würde. Installiere stattdessen das ZIP aus dem jeweiligen Release. Dessen Ordner trägt den richtigen Namen, und ab dann laufen die Updates normal.
Funktioniert das auch mit WP-CLI?
Ja. In WP-CLI holt der Updater Manifeste ohne Rechteprüfung, und wp plugin update aktualisiert auch deaktivierte JPKCom-Plugins, solange ein anderes JPKCom-Plugin mit Updater 1.5.0 geladen ist. Die Prüfsumme wird dabei genauso geprüft.
Fazit
Der Fehler im alten Aufbau war kein Programmierfehler, sondern eine Folge davon, wo der Updater wohnt: im Plugin, das er aktualisieren soll. Updater 1.5.0 verlegt die Verantwortung von der einzelnen Kopie auf die Familie. Irgendein aktives Plugin reicht, und es kümmert sich um alle anderen, auch um die, die niemand mehr eingeschaltet hat.
Dass ein Updater nun Pakete für Plugins anbietet, deren Code gar nicht läuft, hat die Sicherheitsfragen schärfer gemacht. Deshalb steht hinter der Familie eine strengere Sperre als vorher: keine Download-Adressen in der Datenbank, eine Prüfsumme, die zwingend ist und auch fremd gelieferte Pakete prüft, und Abrufe nur für Konten, die Updates auch einspielen dürfen, sowie in Cron und WP-CLI. Für dich als Betreiber bleibt eine einzige Aufgabe: jedes aktive Plugin einmal auf seine Version mit Updater 1.5.0 bringen.
Weiterlesen
Wie die Pipeline aus GitHub Actions, Prüfsumme und Manifest auf GitHub Pages aufgebaut ist, steht im Making-of zu den Auto-Updates. Was Harden & Trim selbst kann, inklusive des neuen WP-CLI-Befehls, beschreibt der Guide zu JPKCom Harden & Trim. Die Versionsgeschichte steht im Changelog von Harden & Trim, die Updater-Klasse selbst liegt im Repository (öffnet in neuem Tab).