Zum Inhalt springen

2. CMS-Systeme erweitern

Zu Zen-Modus wechseln

Im Vorjahr haben Sie ein Content Management System aufgesetzt, ein Inhaltsmodell entworfen, Rollen vergeben und eine fertige Erweiterung eingebunden. Alles davon war Konfiguration: Sie haben dem System gesagt, was es tun soll, aus dem Vorrat dessen, was es ohnehin konnte.

Diese zwei Wochen behandeln den Fall, für den es keinen Vorrat gibt. Die Anforderung lautet, dass beim Speichern eines Projekts automatisch eine Vorschau erzeugt wird. Dass eine bestimmte Personengruppe genau ein Feld sehen darf und den Rest nicht. Dass Ihr Redaktionsbereich einen Endpunkt bekommt, den Ihre mobile Anwendung aufrufen kann. Kein Häkchen deckt das ab, also schreiben Sie Code.

Nur schreiben Sie ihn diesmal nicht in Ihr eigenes System, sondern in ein fremdes. Es ist groß, Sie kennen es nicht vollständig, es bekommt nächste Woche ein Update, und seine Entwickler wissen nichts von Ihnen. Das ist eine eigene Disziplin, und sie hat wenig mit dem Bauen einer eigenen Anwendung zu tun.

Vier Stufen, jede teurer als die vorige. Die Regel für die ganzen zwei Wochen lautet: Nehmen Sie die niedrigste Stufe, die die Anforderung erfüllt.

StufeWas Sie tunBeim nächsten Update
KonfigurierenEinstellungen, Felder, Rollen im System selbstpassiert nichts
Erweiterneigener Code an einem vorgesehenen Erweiterungspunktläuft weiter, solange der Punkt bleibt
Überschreibeneigene Vorlage oder Komponente gewinnt vor der mitgeliefertenläuft weiter, kann aber optisch veralten
PatchenÄnderung an fremdem Code, als Patch-Datei geführtPatch muss geprüft und oft angepasst werden
Forkeneigene Kopie des fremden CodesSie pflegen ab jetzt alles selbst

Die Kosten stehen nicht in der Woche, in der Sie es bauen, sondern in jeder Woche danach. Ein Fork ist am Anfang der bequemste Weg und nach einem Jahr der teuerste, weil jede Sicherheitskorrektur des Originals von Hand nachgezogen werden will.

Die fünfte Möglichkeit steht nicht in der Tabelle, weil sie keine ist: eine Datei des Systems aufmachen und die störende Zeile ändern. Das funktioniert sofort und genau bis zum nächsten Update, und danach haben Sie die Wahl zwischen zwei schlechten Ausgängen. Entweder das Update überschreibt Ihre Änderung, und die Funktion ist ohne Vorwarnung weg. Oder Sie spielen das Update nicht ein, und dann läuft Ihr System mit einer öffentlich dokumentierten Lücke weiter. Wie schnell danach jemand anklopft, haben Sie im Vorjahr im Zugriffsprotokoll gesehen.

Umgekehrt ist das prüfbar, und diese Prüfung gehört an den Anfang, wenn Sie ein System übernehmen, das jemand anderer aufgesetzt hat:

Terminal-Fenster
wp core verify-checksums # WordPress: compare the core against the original
composer diagnose && git status # TYPO3, Symfony: modified dependencies
docker diff <container> # what changed inside the running container

Findet man dabei etwas, hat man meistens auch die Erklärung dafür gefunden, warum das System seit zwei Jahren keine Aktualisierung mehr gesehen hat.

Jedes größere System bietet dieselben Arten von Einhängepunkten. Die Namen sind verschieden, die Idee ist es nicht.

PunktWozuWordPressTYPO3DirectusPayload
Inhaltsmodellneue Arten von Inhalten und FeldernCustom Post Type, TaxonomieTCA, Extbase-ModellCollection, FeldCollection, Field
Hookseigener Code an definierten Stellen im AblaufAction, FilterPSR-14 Eventfilter, actionbeforeChange, afterChange
DarstellungAusgabe überschreibenTheme und Child-ThemeFluid, TypoScripteigenes Frontendeigenes Frontend
EndpunkteDaten hinein und hinausregister_rest_routeMiddlewareEndpoint-ExtensionCustom Endpoint
Redaktionsoberflächeeigene Eingabehilfen und AnsichtenMeta-Box, BlockBackend-ModulInterface, PanelField Component
Automatisierungwiederkehrende ArbeitWP-CronSchedulerFlowsJobs

Für Ihr Projekt brauchen Sie mindestens die ersten vier. Der fünfte Punkt ist der, den die meisten übersehen und der die Redaktion am meisten freut: ein eigenes Eingabefeld, das genau zu Ihren Daten passt, statt eines Textfeldes mit einer Erklärung daneben.

Ein Hook ist eine Stelle, an der das System kurz stehen bleibt und fragt, ob jemand mitreden will. Zwei Sorten, und die Unterscheidung entscheidet darüber, ob Ihr Code funktioniert:

  • Ein Filter läuft vor dem Vorgang, bekommt einen Wert und gibt einen Wert zurück. Er darf verändern und darf abbrechen.
  • Eine Aktion läuft danach und wird nur informiert. Sie darf nichts mehr aufhalten.
sequenceDiagram
  participant R as Redaktion
  participant K as System
  participant F as Ihr Filter
  participant A as Ihre Aktion
  participant W as Warteschlange
  R->>K: Beitrag speichern
  K->>F: Wert vor dem Schreiben
  alt Wert nicht in Ordnung
    F-->>K: Fehler
    K-->>R: Abbruch mit Meldung
  else Wert in Ordnung
    F-->>K: bereinigter Wert
    K->>K: schreiben
    K->>A: Ereignis nach dem Schreiben
    A->>W: Auftrag einreihen
    K-->>R: Bestätigung
  end
extensions/hooks/project-validation/src/index.ts
import { defineHook } from "@directus/extensions-sdk";
export default defineHook(({ filter, action }, { logger }) => {
filter("projects.items.create", async (input: any) => {
input.title = input.title?.trim();
if (!input.title) {
throw new Error("The title must not be empty.");
}
return input; // without a return the value is lost
});
action("projects.items.create", async ({ key }) => {
await queue.enqueue("thumbnail", { id: key });
logger.info({ id: key }, "Project created");
});
});

Vier Punkte, an denen Erweiterungen regelmäßig scheitern:

Im Filter nichts zurückgeben. Der Wert verschwindet stillschweigend, es gibt keine Fehlermeldung, und der Beitrag ist beim nächsten Aufruf leer. Das ist der teuerste Tippfehler dieses Kapitels.

Prüfen in der Aktion statt im Filter. Nach dem Schreiben ist es zu spät; der ungültige Datensatz steht schon in der Datenbank.

Rekursion. Wenn Ihre Aktion nach dem Speichern denselben Datensatz aktualisiert, feuert die Aktion erneut. Das Ergebnis ist eine Schleife, die je nach System die Datenbank oder den Arbeitsspeicher füllt.

const inProgress = new Set<string>();
action("projects.items.update", async ({ keys }) => {
for (const key of keys) {
if (inProgress.has(key)) continue; // our own write
inProgress.add(key);
try {
await updatePreview(key);
} finally {
inProgress.delete(key);
}
}
});

Langsames im Hook. Eine Bildkonvertierung oder der Aufruf eines fremden Dienstes verlängert das Speichern für die Redaktion um Sekunden, und wenn der fremde Dienst hängt, hängt das Speichern mit. Solche Arbeit wird eingereiht und später erledigt. Die Reihenfolge mehrerer Erweiterungen am selben Punkt steuert übrigens eine Priorität; verlassen Sie sich nicht auf die zufällige Ladereihenfolge.

Der Lehrplan nennt sie ausdrücklich, und sie sind mehr als eine Marotte einzelner Hersteller. Eine Vorlagensprache wie Twig, Fluid, Blade oder Liquid ist eine absichtlich beschränkte Sprache für die Darstellungsschicht. Sie kann ausgeben, wiederholen, verzweigen, Werte durch Filter schicken und Bausteine erben, und sie kann mit Absicht keine Datenbankabfragen und keine Dateizugriffe.

Diese Beschränkung ist der Zweck. Erstens dürfen damit Personen arbeiten, die keinen Zugriff auf die Anwendungslogik bekommen sollen. Zweitens bleibt die Logik dort, wo sie hingehört, statt sich über die Vorlagen zu verteilen. Drittens maskieren diese Sprachen ihre Ausgabe standardmäßig, und das ist der wirksamste Schutz gegen Cross-Site-Scripting, den ein CMS anbieten kann.

{% for project in projects %}
<article>
<h2>{{ project.title }}</h2>
<p>{{ project.text|striptags|slice(0, 200) }}</p>
<a href="{{ path('project_detail', { id: project.id }) }}">Details</a>
</article>
{% else %}
<p>No projects are published at the moment.</p>
{% endfor %}
<f:for each="{projects}" as="project">
<h2>{project.title}</h2>
<f:link.action action="show" arguments="{project: project}">Details</f:link.action>
</f:for>

TYPO3 bringt mit TypoScript noch eine zweite Sprache mit, die keine Vorlagensprache ist, sondern eine Konfigurationssprache: Sie beschreibt, wie Seiten aufgebaut, welche Vorlagen verwendet und welche Daten geladen werden. Wer TYPO3 einsetzt, kommt daran nicht vorbei und sollte die zwei Wochen dafür einplanen.

Zwei Regeln gelten für jede dieser Sprachen:

Keine Logik in der Vorlage. Sobald in einem Template gerechnet, sortiert oder gefiltert wird, ist die Zuständigkeit verrutscht. Bereiten Sie die Daten in Ihrem Code auf und geben Sie der Vorlage fertige Werte.

Die automatische Maskierung ist kein Freibrief. Jede dieser Sprachen hat einen Ausweg, der die Maskierung abschaltet, meist |raw oder ein gleichwertiger Aufruf. Jede Verwendung davon braucht eine Begründung im Code daneben. Und in Systemen, deren Vorlagen einfach PHP sind, etwa WordPress, gibt es gar keine automatische Maskierung: Dort maskieren Sie jede einzelne Ausgabe von Hand.

Ziel der AusgabeBeispielWas nötig ist
HTML-Text<p>HIER</p>Standardmaskierung
Attributtitle="HIER"Attributmaskierung
URLhref="HIER"URL-Maskierung und Prüfung des Schemas
JavaScriptconst x = "HIER"JSON-Kodierung, besser über ein data-Attribut
CSSstyle="HIER"vermeiden

Dieselbe Zeichenkette ist im Fließtext harmlos und in einem href ein Problem, weil javascript: dort ein gültiges Schema ist. Einmal am Eingang zu säubern genügt deshalb nicht; maskiert wird am Ausgang, passend zum Ziel. Vertieft wird das in Kapitel 7.

extensions/directus-extension-htl-projekte/
├── package.json Name, Version, Kompatibilität, Directus-Abschnitt
├── src/
│ ├── index.ts Einstiegspunkt
│ ├── hooks.ts Filter und Aktionen
│ └── endpunkte.ts eigene Routen
├── CHANGELOG.md
└── README.md Zweck, Konfiguration, Deinstallation
{
"name": "directus-extension-htl-projects",
"version": "1.0.0",
"type": "module",
"directus:extension": {
"type": "bundle",
"path": { "app": "dist/app.js", "api": "dist/api.js" },
"source": { "app": "src/app.ts", "api": "src/api.ts" },
"host": "^11.0.0"
}
}

Der Eintrag host ist die Kompatibilitätsangabe: Sie sagt, mit welchen Versionen des Systems die Erweiterung laufen soll. Ohne sie merken Sie erst im Betrieb, dass ein Hauptversionssprung Ihre Schnittstelle geändert hat.

Unabhängig vom System gelten fünf Punkte:

  1. Eigenes Verzeichnis, eigenes Repository. Ihre Erweiterung ist ein eigenständiges Stück Software mit eigener Versionsnummer nach Semantic Versioning. Sie liegt niemals zwischen den Dateien des Systems.

  2. Namensraum oder Präfix für alles. Funktionen, Optionen, Tabellen, Inhaltstypen, Ereignisnamen. Ohne Präfix kollidieren Sie irgendwann mit einer anderen Erweiterung, und der Fehler ist von außen nicht erklärbar, weil er nur in dieser Kombination auftritt.

  3. Konfiguration statt fester Werte. Mailadressen, Grenzwerte, Kennungen fremder Dienste gehören in die Konfigurationsoberfläche oder in Umgebungsvariablen. Ein fest eingetragener Wert wird spätestens dann zum Problem, wenn dieselbe Erweiterung in Test- und Produktionsumgebung läuft.

  4. Eigenes Schema mit Migrationen. Wenn Ihre Erweiterung Tabellen oder Felder anlegt, tut sie das über die Migrationsmechanik des Systems und nicht mit einem CREATE TABLE beim ersten Aufruf. Und sie kennt ihre eigene Schemaversion, damit ein Update weiß, was zu tun ist.

  5. Deinstallation aufräumen. Was die Erweiterung angelegt hat, muss sie beim Entfernen wieder loswerden können. Eine Erweiterung, die Spuren hinterlässt, ist nicht fertig; ein System, in dem drei Jahre lang Reste entfernter Erweiterungen liegen, ist nicht mehr durchschaubar.

Systeme mit eigener Ausgabe suchen ihre Vorlage in einer festen Reihenfolge und nehmen die erste, die sie finden. Genau daraus entsteht der Erweiterungspunkt: Ihre Vorlage liegt weiter vorne und gewinnt, ohne dass Sie das Original anfassen.

1. htl-child/single-htl_projekt.php <- Ihre Fassung gewinnt
2. htl-basis/single-htl_projekt.php
3. htl-basis/single.php
4. Standard des Systems

Daraus folgt die Regel für jede Gestaltungsarbeit an einem fremden Theme: ein eigenes abgeleitetes Theme anlegen und dort überschreiben. Das gekaufte oder mitgelieferte Theme bekommt Updates, Ihr abgeleitetes gehört Ihnen.

Bei einem Headless-System entfällt die Vorlagenschicht, und an ihre Stelle tritt Ihre eigene Komponente im Frontend. Der Gedanke bleibt derselbe: Sie ersetzen die Darstellung, nicht die Logik dahinter.

Im Vorjahr haben Sie Rollen vergeben, die das System mitbrachte. Jetzt kommt der Fall dazu, dass die mitgelieferten Rollen nicht passen.

Berechtigungen greifen auf drei Ebenen, und Ihr Rechtekonzept braucht alle drei:

EbeneFrageBeispiel
SammlungDarf diese Rolle Projekte überhaupt bearbeiten?Autor ja, Gast nein
DatensatzDarf sie diesen Datensatz bearbeiten?nur eigene Projekte
FeldDarf sie dieses Feld sehen oder ändern?Freigabevermerk nur für die Leitung

Die zweite Ebene ist dieselbe Objektberechtigung, die im Vorjahr in Kapitel 8 zum Thema wurde, nur eine Etage höher. Die dritte wird fast immer vergessen und ist der Grund, warum interne Vermerke in Exporten auftauchen.

Wenn die mitgelieferten Rollen nicht reichen, definieren Sie eine eigene Berechtigung, etwa htl_project_approve, und vergeben sie gezielt. Der unsaubere Weg ist, jemandem „vorübergehend” Administratorrechte zu geben; vorübergehend dauert dann bis zur nächsten Neuinstallation.

Geprüft wird serverseitig, in jedem eigenen Endpunkt und in jeder ändernden Aktion:

register_rest_route('htl/v1', '/projects/(?P<id>\d+)/approve', [
'methods' => 'POST',
'callback' => 'htl_project_approve',
'permission_callback' => function (WP_REST_Request $request) {
return current_user_can('htl_project_approve', (int) $request['id']);
},
]);

Ein ausgeblendeter Menüpunkt ist keine Berechtigung. Die Prüfung dafür dauert zwei Minuten: Melden Sie sich als Autor an und rufen Sie die Adresse der Verwaltungsfunktion direkt auf.

Der Lehrplan verlangt beides, eigene Erweiterungen und die Weiterentwicklung bestehender. Das zweite ist im Berufsleben der häufigere Fall und der unbeliebtere, weil man sich in fremden Code einlesen muss, bevor man eine Zeile schreibt.

  1. Lizenz zuerst. Darf der Code geändert und weitergegeben werden? Bei GPL ja, mit der Auflage, dass die Weitergabe wieder unter GPL erfolgt. Bei MIT und BSD praktisch uneingeschränkt. Bei kommerziellen Erweiterungen oft nicht, und dann endet die Aufgabe hier. Das ist die erste Frage, nicht die letzte.

  2. Ausgangszustand festhalten. Den unveränderten Stand mit Versionsnummer in ein eigenes Repository legen und als eigenen Commit sichern, das Original als zweite Gegenstelle eintragen. Ohne diesen Schritt kann später niemand mehr trennen, was von Ihnen stammt und was nicht.

  3. Verstehen, bevor Sie ändern. Einstiegspunkt suchen, alle registrierten Hooks auflisten, einen vollständigen Ablauf einmal durchgehen. Erst danach entscheiden, wo die Änderung hingehört.

  4. Von außen erweitern, wenn es geht. Viele Erweiterungen bieten selbst Hooks an. Was Sie darüber lösen, bleibt aktualisierbar. Was Sie im fremden Code ändern, ist ab sofort Ihre Baustelle.

  5. Patch statt Fork, wenn es sein muss. Eine geführte Patch-Datei, die beim Installieren angewendet wird, ist der Mittelweg: Die Änderung ist sichtbar, klein und beim nächsten Update sofort als Konflikt erkennbar. Ein Fork ist die Notlösung.

  6. Zurückgeben. Bei quelloffenen Erweiterungen ist der Beitrag an das Original der beste Weg. Wird er angenommen, pflegt Ihre Änderung ab dann jemand anderer.

  7. Dokumentieren. Was geändert wurde und warum, kommt in CHANGELOG.md. Ein Jahr später erinnert sich niemand an die Begründung, auch Sie nicht.

  1. Kern-Dateien ändern. Danach bleibt die Wahl zwischen ewiger Nacharbeit und einer offenen Lücke.
  2. Das fremde Theme statt eines abgeleiteten anpassen. Derselbe Fehler eine Ebene höher.
  3. Sofort forken, weil das schneller aussieht als das Einlesen in die Hooks.
  4. Namen ohne Präfix. Die Kollision tritt erst bei jemand anderem auf, und niemand versteht sie.
  5. Im Filter nichts zurückgeben. Der Wert ist stillschweigend weg.
  6. Im Hook prüfen, nachdem schon geschrieben wurde.
  7. Rekursion beim Speichern und eine Schleife, die erst auffällt, wenn der Server steht.
  8. Langsame Arbeit im Hook. Die Redaktion wartet und weiß nicht, warum.
  9. Ausgabe nicht maskiert oder die automatische Maskierung mit raw abgeschaltet, weil „das HTML sonst nicht funktioniert”.
  10. Rechte nur in der Oberfläche versteckt.
  11. Feldebene vergessen. Interne Vermerke stehen im Export.
  12. Ohne Testumgebung updaten und deshalb gar nicht updaten.
  • Erklären: begründen, warum Änderungen am Kern eines fremden Systems ein Wartungs- und Sicherheitsproblem sind.
  • Beurteilen: zwischen Konfigurieren, Erweitern, Überschreiben, Patchen und Forken die passende Stufe wählen und die Wahl mit den Folgekosten begründen.
  • Nennen: die Erweiterungspunkte eines Systems benennen und im eigenen System auffinden.
  • Erklären: Filter und Aktionen unterscheiden und angeben, wann welcher Typ zu verwenden ist.
  • Anwenden: eine eigene Erweiterung mit Manifest, Kompatibilitätsangabe, Präfix, Konfiguration und Deinstallation anlegen.
  • Anwenden: Hooks so schreiben, dass sie weder rekursiv laufen noch den Ablauf verlangsamen.
  • Erklären: Zweck und Grenzen einer CMS-Skriptsprache darstellen, einschließlich der automatischen Maskierung.
  • Anwenden: eine Vorlage über den Überschreibmechanismus des Systems ersetzen und jede Ausgabe kontextgerecht maskieren.
  • Anwenden: ein Rechtekonzept auf den Ebenen Sammlung, Datensatz und Feld umsetzen und serverseitig prüfen.
  • Anwenden: eine bestehende Erweiterung einlesen, lizenzkonform anpassen und die Änderung nachvollziehbar führen.
  • Beurteilen: die Aktualisierbarkeit einer Erweiterung in der Testumgebung prüfen und bewerten.
  • Eine eigene Erweiterung für das CMS oder Backend-System Ihres Projekts, im eigenen Repository, mit Manifest, Version, Präfix, Konfiguration und Deinstallation.
  • Darin mindestens: ein eigener Inhaltstyp oder eine eigene Sammlung, ein Filter, eine Aktion und ein eigener Endpunkt.
  • Mindestens eine überschriebene Vorlage oder eigene Darstellungskomponente, mit kontextgerechter Maskierung.
  • Ein Rechtekonzept mit mindestens drei Rollen und einer eigenen Berechtigung, nachgewiesen durch den Aufruf einer Verwaltungsadresse als Autor.
  • Eine bestehende fremde Erweiterung eingelesen und gezielt weiterentwickelt, mit Lizenzvermerk, CHANGELOG.md und der Angabe, ob die Änderung eingereicht wurde.
  • Ein Update-Test in der Testumgebung samt Prüfliste und Ergebnis in docs/betrieb.md.
  • Aufgabe 01 - Erweiterungspunkte finden
  • Aufgabe 02 - Eigener Inhaltstyp und eigene Erweiterung
  • Aufgabe 03 - Filter und Aktion mit Rekursionsschutz
  • Aufgabe 04 - Rechtekonzept auf drei Ebenen
  • Aufgabe 05 - Fremde Erweiterung weiterentwickeln