Workbench Plugin Contract v1
Der verbindliche, versionierte Vertrag, über den ein Plugin zur Werkbank
beiträgt — Aktivitätsleiste, Seitenspalte, Zusatzspalte, unteres Panel,
Statusleiste, Befehlsleiste/-palette und das Modulverzeichnis. Diese Seite
ist die deutsche Kurzfassung für Betreiber und Autoren; die verbindliche,
vollständige Spezifikation (API, Lebenszyklus, Fehlerverhalten, Beispiel)
steht im Repository: lib/plugins/wkfluentui/docs/contract-v1.md.
Verwandt: wkfluentui-Übersicht · Styles-Contract
Warum es diesen Vertrag gibt
Bis Contract v1 lief ein Beitrag zur Werkbank über fünf getrennte, nur über
method_exists() erkannte Mechanismen (getModuleInfo(),
getShellRegions(), getShellBadge(), getShellViews(), das Ereignis
WKFLUENTUI_ADMIN_COMMANDS). Mindestens einer davon transportierte gar
keine echte Information mehr: der Rückgabewert von getShellRegions() wird
nirgends gelesen — allein die Existenz der Methode war das Signal. Es gab
keine Möglichkeit zu fragen „unterstützt die Werkbank ein unteres Panel
überhaupt„, bevor man eines benutzt, und keine Stelle, an der ein Plugin eine
Berechtigung enger als „darf dieser Benutzer den Bildschirm überhaupt öffnen“
erklären konnte.
Nichts davon wird abgeschaltet. Ein Plugin, das eine der fünf alten
Methoden benutzt, funktioniert unverändert weiter — ein Adapter
(helper/legacyadapter.php) übersetzt sie automatisch in dieselbe
Beschreibungsform, die Contract v1 selbst benutzt.
Die drei Bauflächen
| Fläche | Bedeutung |
|---|---|
classic | reines do=admin (ohne mode=fluentui), oder eine gewöhnliche Wiki-Seite ohne <module>-Tag — keine Werkbank-Beteiligung |
contributed | Regelfall: wkfluentui baut die komplette Schale um den vom Bildschirm gelieferten Inhalt, allein aus dessen erklärten Beiträgen |
selfHosted | das Plugin ruft widgets→workbench() selbst auf, aus seinem eigenen html() heraus — erklärt durch eine bloße Ankündigung ohne kind |
Die sieben Bereiche
| Bereich | Erscheint in | Anmerkung |
|---|---|---|
activityBar | Modul-Wechselleiste | nur ein Abzeichen ist hier beitragbar — das Symbol selbst kommt automatisch aus der Aufzählung installierter Verwaltungs-Plugins |
sidebar | linke Ansichts-Spalte | wie im bisherigen Ansichts-Register benannt |
auxbar | rechte Ansichts-Spalte (oder zweites Dokument im geteilten Ansicht) | dito |
bottomPanel | unteres Panel | im bisherigen Register panel — hier umbenannt, weil das bloße Wort außerhalb seiner Definitionsdatei als „irgendein Panel„ gelesen wird |
statusBar | Statuszeile | ein kind: view-Beitrag nennt eine Tatsache darüber, wo der Leser ist — siehe unten |
commandPalette | Befehlsleiste (sichtbar) und Befehlspalette (durchsuchbar, Strg+Umschalt+P) | siehe unten |
adminModule | Modulverzeichnis für <module name=„…“> | v1-Entsprechung von getModuleInfo() |
Die Statuszeile nimmt Tatsachen, keine Auszeichnung
Jeder andere Bereich trägt vertrauenswürdiges HTML des Aufrufers; dieser trägt reinen Text und maskiert ihn selbst. Die Statuszeile hat eine Zeile, und ein Beitrag, der ein Element öffnen könnte, nähme den Rest davon.
| Feld | Bedeutung |
|---|---|
label | der Text |
href | wohin er führt, wenn er irgendwohin führt |
disabledReason | wird als Titel (Kurzhinweis beim Zeigen) gerendert |
badge.tone | warning oder error für die Einfärbung |
priority | wie wichtig die Angabe ist — siehe unten |
Gültigkeitsbereich wie bei einem Befehl: ein Beitrag, dessen Besitzer der gerade gezeigte Bildschirm ist, erscheint; einer, dessen Besitzer überhaupt keinen Verwaltungsbildschirm hat, gilt als allgemeingültig; einer, der zu einem anderen Bildschirm gehört, erscheint nicht. Es ist buchstäblich dieselbe Prüfung, nicht eine zweite mit gleichem Wortlaut.
Zwei Stufen des Überlebens, aus einer Zahl. Unter 1024 px fällt weg, was
keine Priorität trägt; unter 768 px zusätzlich alles unterhalb von
helper_plugin_wkfluentui_widgets::STATUS_PHONE_PRIORITY (80). Ein Beitrag
darf priority selbst nennen; der Wert wird auf 10–95 geklemmt, damit nichts
Beigetragenes den Rückweg aus der Werkbank überholt, und ein Beitrag ohne
Angabe bekommt 85.
priority ist nicht order. order sortiert innerhalb eines
Bereichs, priority entscheidet über das Überleben bei schmaler Breite. Wären
sie ein Feld, würde ein Umsortieren der Seitenleiste ändern, was ein Telefon in
der Statuszeile zeigt.
Dieser Bereich war zweimal eine Zusage ohne Einlösung, in zwei verschiedenen Formen — und beide standen auf dieser Seite.
Bis zum 24.08.2026 stand hier, ein Beitrag werde „derzeit noch von keinem
Verbraucher gerendert“, während helper/capabilities.php den Namen
workbench.contribution.statusBar seit dem ersten Tag als wahr führte.
Eine Fähigkeit, deren Antwort „ja„ lautet, während es das Merkmal nicht gibt,
ist schlechter als eine Versionsnummer — sie ist der Mechanismus, der eine
ersetzen sollte.
Am 25.08.2026 gab es den Verbraucher, und priority stand hier beschrieben —
aber normalise() baut einen festen Schlüsselsatz, und das Feld war nicht
darin. Ein Beitragender konnte es setzen; es verschwand in der Registratur,
bevor irgendein Verbraucher es sah. Die Folge war die umgekehrte der zuerst
vermuteten: weil der Compositor ein fehlendes priority mit 85 vorbelegte,
überlebte jeder beigetragene Eintrag bis in die Telefonstufe, ob sein Besitzer
das wollte oder nicht — kaputt war die Steuerbarkeit, nicht die Sichtbarkeit.
Erst fehlte der Verbraucher, dann die Leitung zwischen zwei arbeitenden Enden.
Eine Ansicht darf ihr ''html'' als Callable liefern
Die Deskriptor-Tabelle nennt html seit jeher als string | callable. Bis
zum 25.08.2026 schrieb der einzige Renderweg is_string(…) ? … : '' — ein
Callable wurde also zur leeren Zeichenkette, und viewStack() lässt eine
Ansicht mit leerem Körper weg. Kein Fehler, kein Protokolleintrag, kein
Bereich.
Der Unterschied ist nicht kosmetisch. Das Registrierungs-Ereignis feuert
einmal, und bei einem Verwaltungsbildschirm geschieht das, bevor der
Bildschirm gerendert hat. Eine Zeichenkette muss also zur Registrierungszeit
berechenbar sein; ein Callable wird zur Kompositionszeit aufgelöst, also
nachdem das eigene html() gelaufen ist. Genau dafür ist es da.
Ein werfendes Callable kostet diese eine Ansicht und wird protokolliert.
Fähigkeit: workbench.view.html.callable.
Ein Bildschirm darf seine Arbeitsobjekte als Reiter beitragen
Bis zum 25.08.2026 löste adminshell::tabStripHtml() jede gespeicherte Marke
mit plugin_load('admin', …) auf — die Leiste konnte also ausschließlich
Module benennen. Die Registratur darunter war nie die Grenze: sie führt eine
geordnete Liste undurchsichtiger Zeichenketten je Arbeitsbereich und sagt
das auch.
Zwei Bildschirme brauchten dasselbe gleichzeitig — ein Einstellungs-Editor, dessen Arbeitsobjekt ein Bereich ist, und eine Abfrage-Werkbank, deren Arbeitsobjekt eine Abfrage ist. Einmal im Wirt gebaut, weil die Alternative in beiden Fällen eine zweite Reiterleiste im Inhalt gewesen wäre.
| Regel | Warum |
|---|---|
Die Marke wird von der Schale zu <modul>:<objekt> erweitert | eine gespeicherte Marke sagt damit immer, wer sie zurückübersetzen kann |
| Die Reiter eines Bildschirms ersetzen dessen Modulreiter | ein Reiter für „der Konfigurationsmanager“ neben Reitern für die Bereiche, die er verwaltet, wäre eine zweite Reiterebene in den Kleidern der ersten |
| Nur Modulreiter bekommen Verschiebe-Bedienelemente | das Formular sendet die sichtbare Reihenfolge in die Modulliste; eine Objektmarke dort wäre beim nächsten Aufruf nicht auflösbar und nähme die umsortierten Modulreiter mit |
| Der Bildschirm schreibt seine eigene Anheftung | die Schale weiß absichtlich nicht, was die Marke benennt |
| Ein Reiter ohne Titel oder Adresse wird verworfen | er wäre nicht erreichbar, nicht als Lesezeichen zu setzen und von seinem Nachbarn nicht zu unterscheiden |
Fähigkeit: workbench.screen.tabs.v1.
Vier Haken für einen mitgelieferten Bildschirm
Ein Ersatzbildschirm wird von wkfluentui für ein mitgeliefertes
Plugin gezeichnet, und der Vertrag v1 erreicht ihn deshalb nicht:
nativeViewsFor($owner) vergleicht den Besitzer eines Beitrags mit dem
gerade gezeigten Bildschirm — der Besitzer wäre wkfluentui, der Bildschirm
config, und die beiden treffen sich nie. Unter dem Namen des
mitgelieferten Plugins zu registrieren hiesse, dieses Plugin gäbe vor, ein
anderes zu sein.
Ein solcher Bildschirm beantwortet deshalb vier optionale Methoden unmittelbar:
commands(), views(), statusItems(), tabs(). Alle vier laufen
durch eine abgesicherte Naht, ein Fehlschlag kostet also genau diese eine
Antwort und erreicht das Protokoll.
Die eigene Ansicht eines Bildschirms verdrängt die generische der Schale für
denselben Bereich. Ein Bildschirm, der seine eigene Navigation zeichnet, darf
nicht zusätzlich das generische Inhaltsverzeichnis danebengestellt bekommen; und
einer mit eigener Kontextspalte nicht zusätzlich plugin.info.txt. Dieselbe
Regel, die für ein selbst-hostendes Plugin schon gilt — und derselbe
Fehlermodus: nichts schlägt fehl, es erscheint einfach eine zweite Seitenleiste.
Berechtigungen
permission ist ein Callable (keine Argumente, liefert bool) oder
null. Zusätzlich zur ACL, nie an ihrer Stelle — der zugrundeliegende
Verwaltungsbildschirm bleibt unverändert durch DokuWikis eigene
isAccessibleByCurrentUser()-Prüfung geschützt. Ein permission-Wert,
der weder null noch aufrufbar ist, gilt als fehlerhafte Erklärung und
schließt aus — nicht als fehlende und lässt damit durch. Eine werfende Prüfung
gilt als Verweigerung, nicht als Absturz, und wird nicht als Fehler
protokolliert: „dieser Benutzer darf es nicht sehen„ ist der Mechanismus, der
funktioniert, kein Defekt.
Fähigkeits-Erkennung
helper_plugin_wkfluentui_capabilities::has($name) — ein Name, den ein
Plugin vor der Benutzung abfragt, statt eine Versionsnummer oder eine
Klasse zu prüfen. Der Grund: eine Fähigkeit kann später ausgeliefert werden
als die Version, die sie ankündigte — workbench.command.palette wurde
erst wahr, als der Compositor den Palettendialog tatsächlich in die Seite
einfügte, lange nachdem der Dialog selbst schon existierte und ungenutzt blieb.
Seit dem 25.08.2026 zwei weitere Namen:
| Fähigkeit | Bedeutung |
|---|---|
workbench.view.html.callable | das html einer Ansicht darf ein Callable sein und wird zur Kompositionszeit aufgelöst |
workbench.screen.tabs.v1 | ein Bildschirm darf seine eigenen Arbeitsobjekte in der einen Reiterleiste benennen |
Aus dem Registrierungs-Ereignis heraus antwortet has() nur mit dem
Kernwortschatz — und das ist die vollständige Wahrheit, kein Kompromiss. Wenn
der eigene Handler läuft, ist noch kein provides irgendeines anderen
Beitragenden eingesammelt. Eine Frage nach der Fähigkeit eines fremden
Pakets hat zu diesem Zeitpunkt keine Antwort und bekommt false.
Das war bis zum 25.08.2026 schlimmer als eine Einschränkung: list() liest
provides aus der Registratur, und die wird erst am Ende von all()
gemerkt — die hier empfohlene Prüfung rief also all() erneut auf, feuerte das
Ereignis erneut, rief jeden Handler erneut, und tat das, bis der Speicher
ausging. In wksqliteds gemessen als Allowed memory size of 134217728 bytes
exhausted, mit einem Stack, der irgendwo endete, nur nicht bei der Ursache.
all() markiert sich jetzt für die Dauer des Ereignisses, und list()
antwortet solange mit dem Kernwortschatz, ohne zu merken. Die Empfehlung oben
ist seither gefahrlos zu befolgen; vorher war sie es nicht.
Bekannter, in diesem Durchgang behobener Fehlstand
Zwei Fähigkeiten waren bereits vollständig gebaut, aber nirgends erreichbar:
- Das untere Panel war bis in
widgets→workbench()verdrahtet, wurde aber vom Compositor beim Kopieren der Regionen über eine fest verdrahtete Namensliste stillschweigend ausgelassen — eine dorthin verschobene Ansicht verschwand beim Rendern. - Die Befehlspalette (Dialog plus JavaScript-Indexierung) war fertig gebaut, aber nirgends in die Seite eingefügt.
Beides ist mit der Vereinheitlichung auf Contract v1 behoben — siehe
docs/contract-v1.md-Abschnitt „Versioning“ für die Begründung, warum
gerade das der Beleg dafür ist, dass eine Fähigkeits-Prüfung einer
Versionsprüfung vorzuziehen ist.
Nachweis
Geprüft in Isolation gegen Attrappen (26 Zusicherungen) und live gegen die echte Installation in drei getrennten Prozessläufen – angemeldete Administrator-Identität, anonym und mit vorübergehend umbenanntem Testplugin-Verzeichnis zur Simulation einer Deinstallation.