Inhaltsverzeichnis
Zurück: edwiki
DokuWiki-Plugin-Leitfaden (edwiki 2.0)
Arbeitsgrundlage für Claude bei jeder DokuWiki-Plugin-Arbeit in diesem Projekt (structauto u. a.). Angepasst aus tracker-user/dokuwiki-claude (GPL-2.0, https://github.com/tracker-user/dokuwiki-claude) – Umgebung, Browser-Vorgaben und Übersetzungen auf Olivers Setup umgestellt, Windows/Docker-Teile entfernt.
1. Umgebung (Oliver)
- Entwicklung/Test: lima-city-Webspace (Freehosting) – kein Shell-Zugang, keine Server-Logs; Dateien per Upload/FTP. Einige
Options-Zeilen in.htaccessmüssen dort auskommentiert werden. - Produktiv: edwiki.de auf UD MEDIA (Klassik-Hosting) – PHP-Version noch per
phpinfo()verifizieren. - DokuWiki-Version: auf dem Testwiki prüfen (
?do=check) und hier eintragen; bis dahin auf die aktuelle Version „Mort“ (2026-07-14) hin entwickeln, aber alte API-Pfade nicht blind entfernen. - PHP: Ziel 8.x – exakte Untergrenze (
phpmin) erst nach Prüfung beider Hoster festlegen. - Lokale Dev-Maschine: CachyOS (Linux). Optional lokales Test-DokuWiki per Docker, ist aber kein Muss.
- Browser-Untergrenze: moderne Browser (aktuelles Firefox/Chrome) – keine Altbrowser-Einschränkungen nötig.
- Sprachen: jedes Plugin bekommt
en+de. - Debugging ohne Logs:
?do=checkfür Umgebung/Rechte,allowdebug+Logger::debug()(Ausgabe im LogViewer unter Admin), notfalls temporäremsg()-Ausgaben – vor dem Commit wieder entfernen.
2. Arbeitsweise
- Erst planen, dann coden: Idee/Plan besprechen, Code erst nach ausdrücklicher Freigabe („leg los“).
- Nicht raten, nachschlagen: DokuWiki-APIs, Hooks und Klassen gegen den echten Quellcode prüfen (github.com/dokuwiki/dokuwiki bzw. struct-Plugin-Quellcode), nicht aus dem Gedächtnis.
- Nach Änderungen an JS/CSS/lang Cache brechen (siehe Abschnitt 9).
- Zeilenenden: LF in allen Dateien.
3. Plugin-Aufbau
<name>/ ├── plugin.info.txt # Manifest (Pflicht) ├── syntax.php # Wiki-Syntax ├── action.php # Event-Hooks, AJAX, UI-Einbindung ├── admin.php # Admin-Seite ├── helper.php # reine Logik/API, von anderen Plugins nutzbar ├── renderer.php / remote.php / auth.php / cli.php ├── <typ>/<komp>.php # mehrere Komponenten eines Typs ├── script.js / style.css # werden GLOBAL auf jeder Seite gebündelt (sparsam!) ├── conf/default.php # Standardwerte ($conf[...]) ├── conf/metadata.php # ACP-Metadaten ($meta[...]; kein require_once!) ├── lang/<iso>/lang.php # UI-Texte ($lang[...]; keine Wiki-Syntax) ├── lang/<iso>/settings.php # Labels für den Konfigurationsmanager └── deleted.files # optional: beim Update zu löschende Dateien
Benennung: <typ>.php → Klasse <typ>_plugin_<name>. Mehrere Komponenten: <typ>/<komp>.php → <typ>_plugin_<name>_<komp> (Syntax-Modus dann plugin_<name>_<komp>). Plugin-Name nur a-z0-9, kein Unterstrich.
Basisklassen (\dokuwiki\Extension\): SyntaxPlugin, ActionPlugin, AdminPlugin, Plugin (für Helper), RemotePlugin, AuthPlugin, CLIPlugin, EventHandler. Renderer erben von Doku_Renderer.
4. PHP-Konventionen
Pflicht
if (!defined('DOKU_INC')) die();auf allen Logik-Dateien (action/syntax/helper/admin …), nicht aufconf/undlang/.- Sichtbarkeit immer explizit;
protectedstattprivatebevorzugen. - Kein schließendes
?>. Code-Stil PSR-12. - Docblocks (
@param,@return, Zweck) auf public/protected-Methoden. - Eingaben nur über
$INPUT->post->str()/int()/arr(),$INPUT->get->str()– nie$_REQUEST/$_POST/$_GET. - Seiten-IDs mit
cleanID()säubern. - Dateien:
io_saveFile()ist bereits atomar – nicht zusätzlich locken (Deadlock). Für Lesen-Ändern-Schreiben eigenesio_lock()/io_unlock()um die ganze Sequenz. - JSON speichern mit
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE.
Stil: [] statt array(), ?? für Fallbacks, kein @, in_array(..., true), str_starts_with(), keine var_dump/print_r/error_log-Reste.
Rechte-Muster: Helper sind reine Logik und bekommen Fakten als Parameter; die Action-Schicht sammelt sie ($INPUT->server->str('REMOTE_USER'), auth_isadmin()).
5. JS & CSS
- Vanilla JS, kein Framework. AJAX per
fetch()mitJSINFO.sectok. jQuery nur alsjQuery(), nie$(). - Alle Bundles laden
defer. Inline-JS nur übertpl_inlineScript()(setzt CSP-Nonce), nie handgeschriebenes<script>. - JSINFO-Werte im Action-Plugin bei
DOKUWIKI_STARTEDunter einemplugin_<name>-Unterschlüssel setzen. - CSS: DokuWiki-Platzhalter (
__background__,__text__,__border__,__link__bzw.@ini_…), Klassen/IDs mit Plugin-Namen präfixen, IDs als<name>__id(doppelter Unterstrich). - Plugin-CSS/JS lädt auf jeder Seite. Feature-spezifisches per
TPL_METAHEADER_OUTPUTbedingt einbinden. - Stolperfalle LESS: Alles CSS läuft durch lesserphp.
rgba(var(--x), 0.3)wird zu Schwarz kompiliert → so schreiben:background: ~"rgba(var(--x), 0.3)";(relevant fürs Schwarz/Orange-Theme mit CSS-Variablen).
6. DokuWiki-API kompakt
Syntax-Plugins
getType:container,baseonly,formatting,substition(Tippfehler ist korrekt!),protected,disabled,paragraphs.getPType:normal/block/stack.getSort: niedrigste gewinnt.- Patterns:
addSpecialPattern(einmalig),addEntryPattern+addExitPattern(Paar). Keine Delimiter,(?:…)für Alternativen, keine Rückverweise, nur(?i). handle()macht die schwere Arbeit – Ergebnis wird gecacht; Daten nur über das Rückgabe-Array anrender().render(): auf$format == 'xhtml'prüfen, an$renderer->docanhängen, mithsc()escapen.$format == 'metadata'behandeln und Links via$renderer->internallink()registrieren (Backlinks).- Seiten-Flags (z. B. „diese Seite hat eine Infobox“) im aktuellen Metadaten-Zweig setzen:
$renderer->meta['plugin']['<name>'] = true;inrender()beimetadata. Dann verschwindet das Flag von selbst, wenn die Syntax entfernt wird. Persistent gesetzte Flags bleiben für immer kleben. p_render()/render()(Wikitext parsen) ist teuer – sparsam einsetzen und Ergebnis cachen.
Events
$controller->register_hook($name, 'BEFORE'|'AFTER', $this, 'methode', $param, $seq); Handler müssen public sein.$seq: −3999…−1 früher, 0 Standard, 1…3999 später.$e->preventDefault()(nur BEFORE) überspringt die Standardaktion;$e->stopPropagation()überspringt spätere Handler.- Häufige Events:
DOKUWIKI_STARTED,TPL_METAHEADER_OUTPUT,TPL_ACT_RENDER,RENDERER_CONTENT_POSTPROCESS,PARSER_CACHE_USE,ACTION_ACT_PREPROCESS,AJAX_CALL_UNKNOWN,MENU_ITEMS_ASSEMBLY. - Externe Bibliotheken im Konstruktor oder bei Bedarf laden, nie am Dateianfang. Konstruktoren von Syntax-Plugins laufen bei jedem Parse – keine Seiteneffekte dort.
Konfiguration (conf/metadata.php) $meta['k'] = ['<klasse>', '_param' => …]; – Klassen: string, numeric/numericopt, onoff (Default als int 0/1, nicht bool), multichoice (_choices), multicheckbox, regex, fieldset … Labels in lang/<iso>/settings.php.
Lokalisierung: getLang('k'), locale_xhtml('datei'); JS-Texte über $lang['js']['k'] → LANG.plugins.<name>.k. Fehlende Schlüssel fallen auf Englisch zurück.
Caching & Metadaten
- Cache nicht pauschal abschalten – Abhängigkeiten per
PARSER_CACHE_USEergänzen.~~NOCACHE~~erzwingt Neu-Rendern. - Wichtig für structauto: Wenn eine Seite Daten anderer Seiten/Struct-Tabellen anzeigt, muss der Cache wissen, wann er ungültig wird (sonst veraltete Infoboxen).
p_get_metadata($id, $key, $render)/p_set_metadata(...); Plugin-Daten unterplugin.<name>.
Globale & Konstanten: $ID, $INFO, $ACT (kann ein Array sein → !is_array() prüfen), $conf, $INPUT, $USERINFO, $JSINFO. DOKU_INC, DOKU_PLUGIN, DOKU_BASE, DOKU_URL.
Formulare: dokuwiki\Form\Form benutzen – Sicherheitstoken wird automatisch eingefügt.
7. Sicherheit (die teuersten Lektionen)
- Escapen:
hsc()für HTML,rawurlencode()für URL-Werte; nutzergelieferte URLs nur mit^https?://erlauben (javascript:blocken). - Attribut-Injection – häufigste XSS-Ursache in Syntax-Plugins: Ein Wert aus dem Wiki-Markup landet ungeprüft in
class=,style=,id=,title=usw. Ein"bricht aus dem Attribut aus. Regel: (a) Wert per Regex-Capture-Group mit strengem Zeichensatz holen, beide Enden verankern (^…$), keine Quantoren, die leer matchen können (*,?,\d*); nie nurtrim(); (b) bei der Ausgabe zusätzlichhsc(). Redakteure gelten als nicht vertrauenswürdig. - Struct-Werte sind Nutzereingaben: Alles, was aus Struct-Feldern in HTML (Infobox) geschrieben wird, escapen – oder, wie bei structauto, als Wikitext über
p_render()laufen lassen. - Regex-Nachbearbeitung von HTML (z. B.
RENDERER_CONTENT_POSTPROCESS): nur Strukturen entfernen, nie Nutzerdaten unescaped wieder einsetzen. - Daten ins Inline-
JSINFO-Script:json_encode(..., JSON_HEX_TAG)verwenden.JSON_UNESCAPED_SLASHESallein macht</script>-Ausbruch möglich → gespeichertes XSS. - JS: Fremddaten (AJAX-Antworten) mit
textContent/.text()einsetzen, nieinnerHTML/.html(). - HTML-Kommentare: Nutzerdaten nie in
<!-- … -->ausgeben – Browser beenden Kommentare auch bei--!>. - CSRF ≠ Berechtigung: Zustandsändernde Aktionen brauchen beides:
checkSecurityToken()undauth_quickaclcheck($id)gegen die nötige Stufe (AUTH_EDIT,AUTH_DELETE,AUTH_ADMIN). Benutzer-ID über$INPUT->server->str('REMOTE_USER'). - Keine eigenständigen PHP-Skripte, die selbst
inc/init.phpladen und Anfrage-Daten ausgeben – die umgehen ACL und CSRF. Immer überaction.php/AJAX_CALL_UNKNOWN. - Keine Stacktraces/Serverpfade an normale Nutzer ausgeben.
- Ausnahme Editor-Round-Trip: In Daten, die zurück ins Markup geschrieben werden (WYSIWYG), nicht escapen – nur an der HTML-Grenze, sonst wird
&bei jedem Speichern zu&.
8. Review-Checkliste
- Fatale Fehler auf aktueller DokuWiki-Version (Namespace-Änderungen)
- DOKU_INC-Guard auf Logik-Dateien (nicht conf/lang)
- Sichtbarkeit auf allen Methoden,
protectedbevorzugt, kein?> - Docblocks auf public/protected
- Keine
$_REQUEST/$_POST/$_GET, kein@ - Globales JS/CSS nur wenn nötig, sonst bedingt laden
- Datei-Schreibzugriffe korrekt gelockt (nie
io_saveFile()extra locken) - Alle Ausgaben escaped (
hsc()), Attribut-Werte per Allowlist-Regex - Null-Sicherheit bei Array-Zugriffen (
?? []) conf/metadata.phpohnerequire_once;default.php↔metadata.phpSchlüssel synchron- Keine Debug-Reste, TODO/
markieren
- CSRF-Token und ACL-Prüfung bei zustandsändernden Aktionen
- URLs
rawurlencode(), nurhttp(s) plugin.info.txtDatum/Version aktuell- Übersetzungen
en+devollständig; hartkodierte Texte nachlang/en/lang.phpauslagern - README stimmt mit Code überein (Code hat Vorrang)
9. Häufige Stolperfallen
- Cache-Buster: Nach Änderungen an
script.js/style.css/langliefern Browser weiter das alte Bündel, weil Plugin-Dateien nicht in die Bündel-URL einfließen. Lösung: im Konfigurationsmanager irgendeine Einstellung speichern (oderconf/local.phpneu hochladen/anfassen). Das bricht auch den Metadaten-Render-Cache. - Metadaten-Cache: Code-Änderungen am Plugin invalidieren gerenderte Metadaten nicht – gleicher Trick wie oben.
~~MARKER~~für den Rest der Seite:addSpecialPattern('~~MARKER~~[\s\S]*', …)fängt alles bis Seitenende als ein ungeparstes Token (Lexer läuft mit DOTALL).class_exists()löst Autoload aus;class_exists($c, false)nur, wenn man das explizit nicht will.metaFN($id, '.myplugin')liefert einen Pfad indata/meta/passend zur Seite.- Admin-Tabellen mit Zeilen-Buttons: keine verschachtelten
<form>(HTML5form=-Attribut nutzen), kein Feld namenspage.
10. plugin.info.txt & Veröffentlichung
- Eigene Plugins:
date= heutiges Datum (YYYY-MM-DD),phpmin X.Yfalls nötig, keine Kommentare in der Datei. - Repo-Schema:
github.com/<user>/dokuwiki-<name>, Branchmain. - Plugin-Seite auf dokuwiki.org: Seiten-ID exakt
plugin:<name>;downloadurl→…/zipball/main; Typ-Bitmaske: 1 Syntax, 2 Admin, 4 Action, 8 Render, 16 Helper, 64 Remote, 128 Auth, 256 CLI (structauto = Syntax + Action =Syntax, Action). - Vorlagen dafür:
PUBLISHING.md,PLUGIN_REPO_REFERENCE.md,PROMPT_NEW_PLUGIN_PAGE.mdim Quell-Repo. - WYSIWYG (ProseMirror-Editor)-Unterstützung bei Bedarf:
PROSEMIRROR_PLUGIN_SUPPORT.mdim Quell-Repo. Eigenständige Knoten gehen über die öffentliche API; Inline-Formatierungen mit Attributen erfordern Änderungen am ProseMirror-Plugin selbst.
11. Tests (optional)
Nur wenn beim Review ohnehin nötig: <plugin>/_test/, Namespace \dokuwiki\plugin\<name>\test\, erbt DokuWikiTest, @group plugin_<name>, protected $pluginsEnabled = ['<name>'];. Ein GeneralTest.php prüft plugin.info.txt und conf-Schlüssel-Sync. Läuft nur in einer lokalen DokuWiki-Installation, nicht auf lima-city.
