Handbuch für Template-Programmierer
camindo CMS liefert Inhalte mittels Templates aus, beispielsweise HTML auf einer Website. CMS-Befehle bereiten Inhalte auf und fügen diese dynamisch im Template ein. Beispiel für ein stark vereinfachtes Seitentemplate:
<html>
<body>
<h1>(CMS:TEXT title)</h1>
<div>(CMS:TEXT text)</div>
<div>(CMS:MEDIA field=bild_klein attr[id]=bild1)</div>
<div>erstellt: (CMS:DATE created_at format="dd.MM.y")</div>
</body>
</html>
CMS-Befehle dienen dabei nicht nur als Platzhalter. Sie stellen komplexe gekapselte Funktionalitäten dar, die mittels Parametern angepasst und miteinander kombiniert werden können. Das spart viel Entwicklungszeit. Zudem sind sie erprobt und gewährleisten Stabilität.
Beispiele dafür, was CMS-Befehle leisten:
- Inhalte bestimmter Kriterien aus der Datenbank ermitteln
- Inhalte aufbereiten
- Benutzereingaben prüfen, speichern oder versenden
- Registrierung von Websitebesuchern, An- und Abmeldung
- weitere Templates einbinden
Ausgabeformate: Neben Websites mit HTML-Seiten sind weitere Ausgabemedien und Publikationswege möglich. Inhalte können etwa als XML-Dateien, PDF-Dateien, E-Mails… formatiert werden und über eine URL zum Abruf bereitgestellt, als Dateien ins Dateisystem geschrieben oder über ein Netzwerk geschoben werden.
Template-Satz: Nicht jeder muss das Rad neu erfinden. Inhaltsbereiche (ihre Konfiguration inkl. Eingabefeldern, auf Wunsch mit Inhalten) und die zugehörigen Templates können im XML-Format exportiert werden. Beim Import in eine andere camindo-Installation bleiben die Beziehungen zwischen Bereichen und Templates bestehen. Der komplette Anwendungsteil ist sofort einsatzbereit. Das ermöglicht die einfache Integration fertiger Bausätze (Forum, Newsfeed, ...) oder auch den Export selbst entwickelter Anwendungen für die Wiederverwendung in späteren Projekten.
Inhalt
- Grundlagen – wie eine Seite entsteht, Adressen, Syntax, Listen, Verknüpfungen, das Konzept „jeder Datensatz hat eine Seite“, Filter, Mehrsprachigkeit, Sicherheits-Header (CSS/JS/JSON ausliefern, Einbetten in Partnerwebsites), zeitgesteuerte Templates, Templates im Dateisystem
- Template-Sätze – Funktionen kapseln, portabel halten, weitergeben und anpassen
- Trigger-Templates – PHP-Methoden für die Ereignisse der Datensätze eines Bereichs (Speichern, Löschen …) und für die Aktionen der Lösch-Policies
- Befehlsreferenz – eine Seite je Befehl (unten)
Wie Templates im Backend organisiert und den Inhaltsbereichen zugewiesen werden, beschreibt das Handbuch für Administratoren.
Befehlsreferenz
(CMS:BEFEHL wert parameter=wert parameter[schlüssel]="wert" …)
Befehlsname und Parameternamen sind unabhängig von Groß- und Kleinschreibung. Der
erste Wert ohne = gilt als Hauptparameter des Befehls ((CMS:TEXT titel) =
(CMS:TEXT field=titel)). Werte in Anführungszeichen dürfen PHP-Ausdrücke enthalten,
so wie sie in einem PHP-String üblich sind ("{$set['x']}").
Ausgabeparameter sind bei den meisten Befehlen dieselben:
ifempty (Ersatz bei leerem Wert),
instead (Ersatz bei nicht leerem Wert),
prefix / suffix (nur bei nicht leerem Wert),
set (Ergebnis wird in die Variable $set[…] geschrieben, statt auf die Seite),
id (anderer Datensatz als Quelle),
replace (Teiltexte ersetzen) bei TEXT, DATE, DATETIME, TIME und NUMBER.
Ausgabe
| Befehl | |
|---|---|
| CMS:DATE | Datum ausgeben |
| CMS:DATETIME | Datum und Uhrzeit ausgeben |
| CMS:EMAIL | E-Mail versenden, Formular-Pflichtfelder prüfen |
| CMS:MEDIA | Bild oder Mediendatei ausgeben |
| CMS:MESSAGE | Fehlermeldungen ausgeben |
| CMS:NUMBER | Numerischen Inhalt ausgeben |
| CMS:RAW | Inhalt unformatiert ausgeben |
| CMS:SNIPPET | Snippet (Textbaustein) in der Sprache des Aufrufs ausgeben |
| CMS:TEXT | Textinhalt ausgeben |
| CMS:TIME | Uhrzeit ausgeben |
| CMS:URL | URL eines Datensatzes erzeugen |
| CMS:XML | XML-Deklaration ausgeben |
| CMS:ZIP | Dateien mehrerer Datensätze als ZIP-Archiv herunterladen |
Verknüpfungen
| Befehl | |
|---|---|
| CMS:RELATION | Hauptfeld verknüpfter Datensätze ausgeben (nur für interne Vorschau!) |
Abfragen
| Befehl | |
|---|---|
| CMS:SELECT | Datensätze abfragen |
| CMS:SELECT_LANGVERSION | Sprachen der Instanz als Ergebnismenge |
| CMS:RESULT_START | Beginn der Ergebnisschleife |
| CMS:RESULT_END | Ende der Ergebnisschleife |
Navigation
| Befehl | |
|---|---|
| CMS:JUMP_LIST | Blätter-Navigation erzeugen |
| CMS:JUMP_NEXT | Weiterblättern-Verweis erzeugen |
| CMS:JUMP_PREV | Zurückblättern-Verweis erzeugen |
Steuerung
| Befehl | |
|---|---|
| CMS:CONTINUE | Nach einem Success-Template weitermachen |
| CMS:CSP | Script-Policy (CSP) der Seite anpassen |
| CMS:HEADER | HTTP-Header senden |
| CMS:INCLUDE | Template einbinden, wahlweise mit Ausgabe-Cache |
| CMS:NONCE | Nonce für per PHP erzeugte Scripts |
| CMS:SAVEDATA | Datensatz in einer PHP-Variable ablegen |
Frontend-Editing
| Befehl | |
|---|---|
| CMS:FED | Bearbeiten-Verweis bei Frontend-Editing |
| CMS:FED_CODE | HTML nur bei Frontend-Editing ausgeben |
| CMS:FED_SCRIPT | Script für die Bearbeiten-Verweise (einmal je Seite) |
Community
| Befehl | |
|---|---|
| CMS:USER_2FA | Zweiten Faktor verwalten |
| CMS:USER_AUTH | Session des Frontend-Users prüfen |
| CMS:USER_BASKET | Warenkorb bearbeiten |
| CMS:USER_DELETE | Datensatz des Users löschen |
| CMS:USER_INPUT | Datensatz aus Usereingaben speichern |
| CMS:USER_LOGIN | Frontend-User anmelden |
| CMS:USER_LOGIN_CODE | Anmeldung mit zweitem Faktor abschließen |
| CMS:USER_LOGOUT | Frontend-User abmelden |
| CMS:USER_PASSWORD_RULES | Passwort-Regeln für die Anzeige im Formular |
| CMS:USER_REGISTER | Frontend-User registrieren (Profil-Datensatz anlegen) |
| CMS:USER_REGISTER_CONFIRM | Registrierung freischalten |
| CMS:USER_PROTECT | Template für nicht angemeldete Besucher sperren |
| CMS:USER_PW | Passwort-Reset vorbereiten |
| CMS:USER_TOKEN | Sicherheits-Token für Formulare |
Die Community-Funktionen richtet der Administrator ein (siehe Handbuch für Administratoren).
Zusatzmodul PDF
Das Zusatzmodul PDF (camindo/module-pdf) ergänzt die Befehle (CMS:PDF_…), mit
denen Templates PDF-Dokumente erzeugen – Seite für Seite, auf Wunsch auf einer
PDF-Vorlage wie einem Briefbogen, mit Texten, HTML, Bildern, Linien und Tabellen:
(CMS:PDF_INIT margins=25,20,30,20)
(CMS:PDF_PAGE briefbogen.pdf)
(CMS:PDF_FONT helvetica style=B size=16)
(CMS:PDF_TEXT "Rechnung {$cms['nummer']}" y=50)
(CMS:PDF_PRINT rechnung.pdf)
Die vollständige Befehlsliste enthält die Dokumentation des Moduls.
1 Grundlagen der Template-Programmierung
Was ein Template ist
Ein Template ist ein Textdokument – meist HTML –, in dem CMS-Befehle stehen. Die Befehle fragen Datensätze ab, setzen Inhalte ein, binden andere Templates ein, werten Formulare aus und steuern den Ablauf. Dazwischen darf PHP-Code stehen.
(CMS:INCLUDE header)
<h1>(CMS:TEXT title)</h1>
<div>(CMS:TEXT text)</div>
<div>(CMS:MEDIA field=bild_klein attr[id]=bild1)</div>
<div>erstellt: (CMS:DATE created_at format="dd.MM.y")</div>
(CMS:INCLUDE footer)
Beim Speichern übersetzt camindo CMS das Template in PHP. Zur Laufzeit wird nur noch dieses PHP ausgeführt – es braucht keine Template-Engine, die den Text bei jedem Aufruf neu auswertet.
Wie eine Seite entsteht
Ruft ein Besucher eine Adresse auf, ermittelt camindo CMS:
- die Instanz – aus der Domain,
- die Sprache – aus dem Sprachkürzel der Adresse, sonst aus der Spracheinstellung des Browsers, sonst die Hauptsprache,
- den Datensatz – aus dem Slug oder der Nummer (
record_id) in der Adresse; ohne Angabe den Start-Datensatz der Instanz, - das Template – aus der Adresse, aus dem Datensatz oder das Standard-Template seines Inhaltsbereichs.
Die Inhalte eines aufgerufenen Datensatzes stehen im Template zur Verfügung. Standardmäßig
beziehen sich alle Ausgabe-Befehle auf diesen Datensatz – vgl. oben (CMS:TEXT title). Führt
das Template eine Abfrage mittels CMS:SELECT durch, stehen die Inhalte der ermittelten
Datensätze in gleicher Weise im Ergebnis-Template zur Verfügung; siehe unten Listen, Select, Ergebnisse.
Adressen
[/{sprache}]/{slug oder nummer}[,{template}]
| Adresse | Bedeutung |
|---|---|
/ |
Startseite der Instanz |
/betriebsfeier-2026 |
Datensatz mit diesem Slug, Standard-Template |
/termine/betriebsfeier-2026 |
Datensatz mit dem Slug betriebsfeier-2026 im Bereich mit dem Slug-Prefix termine |
/termine |
Übersicht des Bereichs mit dem Slug-Prefix termine – sein Übersichts-Template, ohne Datensatz |
/1758 |
Datensatz Nummer 1758 |
/en/summer-party-2026 |
englische Sprachversion |
/betriebsfeier-2026,druck |
Datensatz mit dem Template druck – nur wenn es für den Bereich als URL-Override freigegeben ist |
/0,suche |
Template suche ohne Datensatz – nur wenn es als „Standalone“ markiert ist |
Reine Ziffernfolgen sind immer Datensatznummern, alles andere ist ein Slug. Ist der erste Teil der Adresse der Slug-Prefix eines Inhaltsbereichs, wird der Rest nur in diesem Bereich gesucht; sonst der ganze Pfad in den Bereichen ohne Präfix. Das Sprachkürzel steht in der Adresse, sobald die Instanz mehr als eine Sprache führt. Adressen erzeugen Sie mit CMS:URL – nie von Hand, dann stimmen Sprache, Slug und Domain immer.
Besteht die Adresse nur aus dem Sprachkürzel (/en, ohne Schrägstrich am Ende),
leitet camindo CMS auf die Startseite der Sprache um (/en/ oder /en_US/ – je
nachdem, wie diese eingerichtet ist). Führt der Auftritt nur eine Sprache und hat ein
Datensatz genau diesen Slug, wird stattdessen der Datensatz gezeigt. Adressen, unter
denen es nichts gibt – unbekannter Datensatz, unbekannte Sprache, nicht freigegebenes
Template –, antworten mit dem Status 404.
Syntax der CMS-Befehle
(CMS:BEFEHL wert parameter=wert parameter[schlüssel]="wert" …)
- Befehlsnamen und Parameternamen sind unabhängig von Groß- und Kleinschreibung.
- Parameter sind durch Leerzeichen getrennt; ein Befehl darf über mehrere Zeilen gehen.
- Die Reihenfolge, in der Parameter verarbeitet werden, bestimmt jeder Befehl selbst, nicht die Reihenfolge, in der die Parameter geschrieben werden.
- Der erste Wert ohne
=gilt als Hauptparameter des Befehls:(CMS:TEXT titel)ist dasselbe wie(CMS:TEXT field=titel). - Werte, die Leerzeichen enthalten, stehen in Anführungszeichen, einfache oder doppelte:
(CMS:TEXT field="title")entspricht(CMS:TEXT field='title'). - Unterschlüssel in eckigen Klammern:
filter[area]=artikel. Leere Klammern hängen an eine Liste an:sort[]=datum sort[]=titel. Wird derselbe Unterschlüssel mehrfach angegeben, entsteht ebenfalls eine Liste ([]ist im Gegensatz zur PHP-Syntax für Arrays optional). - Statt
=darf ein Vergleichsoperator zwischen Parameter und Wert stehen –==,!=,!,!!,<,>,<=,>=,<<,>>:filter[preis] >= 100ist dasselbe wiefilter[preis]=">=100". Der Wert gilt dann unverändert, ein Operator an seinem Anfang wird nicht mehr ausgewertet:filter[code] < ">2"vergleicht mit dem Text „>2“. Bei=bleibt es wie gehabt –filter[code] = ">2"bedeutet „größer als 2“ (siehe Filter-Syntax). - Der Zeilenumbruch hinter einem CMS-Befehl wird geschluckt, analog zu einem
<?php … ?>-Block. - Befehlsparameter in doppelten Anführungszeichen dürfen PHP-Variablen enthalten:
attr[alt]="{$myvar}"(Parameter ohne Anführungszeichen werden intern wie Parameter mit doppelten Anführungszeichen behandelt).
Fehler – ein unbekannter Befehl, ein unbekannter Parameter, eine fehlende Klammer – verhindern das Speichern nicht. Die Detailansicht des Templates zeigt sie mit Zeilennummer an. Sie erscheinen auch auf der Startseite im Widget „Template-Meldungen“ – für den Fall, dass Templates über das Dateisystem bearbeitet werden.
CMS-Befehl und PHP-Code kombinieren
$set <?php … ?>-Blöcke dürfen frei im Template stehen, CMS-Befehle innerhalb von PHP-Blöcken
führen allerdings zu Fehlern! Um Inhalte aus CMS-Befehlen in PHP-Blöcken zu verwenden,
nutzen Sie den Parameter set (etwa set = myvar) und greifen den Inhalt im PHP-Block aus $set
($set['myvar']) auf.
$cms Inhalte aus dem aufgerufenen Datensatz stehen in $cms; ebenso Inhalte aus Datensätzen, die mittels
CMS:SELECT ermittelt werden.
Für die Ausgabe sollten immer die CMS-Ausgabebefehle (etwa
CMS:TEXT) verwendet werden. Diese Befehle kümmern sich um korrekte Formatierung, ein<?php echo $cms[…] ?>um nichts davon. Verwenden Sie$cmsnur dort, wo ein Befehl einen Wert als Parameter braucht, etwafilter[kategorie]="{$cms['record_id']}".
Gemeinsame Parameter der Ausgabebefehle
| Parameter | Wirkung |
|---|---|
ifempty |
Ersatztext, wenn der Wert leer ist |
instead |
Ersatztext, wenn der Wert nicht leer ist |
prefix, suffix |
Text davor bzw. dahinter – nur wenn der Wert nicht leer ist |
set |
den Wert nicht ausgeben, sondern in $set[…] ablegen |
id |
aus einem anderen Datensatz lesen (Nummer oder Slug), z. B. (CMS:TEXT titel id=impressum) – nur veröffentlichte Datensätze, wie in der URL |
replace |
Teiltexte ersetzen (replace="alt?neu") – bei TEXT, NUMBER, DATE, DATETIME, TIME |
(CMS:TEXT untertitel prefix="<h2>" suffix="</h2>")
(CMS:NUMBER preis format="%.2f" decimal="," suffix=" €" ifempty="auf Anfrage")
Werte in Attributen und Javascript
camindo CMS entfernt aus allen Datensatz-Inhalten Script, Event-Handler (onclick …),
iframe, object, embed und javascript:-Links – beim Speichern und noch einmal bei
der Ausgabe. HTML-Formatierung bleibt, deshalb gibt CMS:TEXT Inhalte unmaskiert aus.
Setzen Sie einen Wert in ein HTML-Attribut ein, maskieren Sie ihn mit specialchars=true
– ein Anführungszeichen im Inhalt beendete sonst das Attribut:
<img src="…" alt="(CMS:TEXT title specialchars=true)">
<a href="(CMS:URL)" title="(CMS:TEXT teaser specialchars=true striptags=true lbrconvert=false)">…</a>
Dasselbe gilt für Formulareingaben, die Sie nach dem Absenden wieder ins Feld setzen:
<input name="data[nickname]" value="(CMS:TEXT useval="{$_POST['data']['nickname']}" specialchars=true)">
Befehle, die selbst maskieren: Einige Befehle maskieren ihre Ausgabe bereits mit
htmlspecialchars. Dort entfällt specialchars – ein bereits maskierter Wert würde
doppelt maskiert, aus & würde sichtbar &:
| Befehl | maskiert |
|---|---|
| CMS:MEDIA | alle Attribute des <img>-Tags: src, alt, width, height und attr[…] – nicht die Einzelwerte über subfield |
| CMS:USER_TOKEN | den Wert des versteckten Feldes |
| CMS:USER_PASSWORD_RULES | die gesamte Ausgabe (JSON für ein Attribut) |
| CMS:XML | version und encoding |
| CMS:JUMP_LIST, CMS:JUMP_PREV, CMS:JUMP_NEXT | die URL in {href} |
Was Sie einem dieser Befehle übergeben, muss deshalb unmaskierter Text sein – ohne HTML-Tags und Entities:
(CMS:TEXT title striptags=true ampconvert=false lbrconvert=false set=title)
(CMS:MEDIA bild attr[alt]="{$set['title']}")
Nie maskiert werden prefix, suffix, ifempty und instead – sie stammen aus
dem Template und dürfen HTML enthalten. specialchars kennen nur
CMS:TEXT und CMS:SNIPPET. Werte anderer Befehle, etwa
CMS:MEDIA … subfield=filename, CMS:RELATION oder CMS:RAW,
legen Sie für ein Attribut mit set ab und geben sie über CMS:TEXT maskiert aus:
(CMS:MEDIA dokument subfield=filename set=dateiname)
<a href="(CMS:MEDIA dokument subfield=path)" title="(CMS:TEXT useval="{$set['dateiname']}" specialchars=true lbrconvert=false)">…</a>
CMS:URL und CMS:MEDIA … subfield=path erzeugen nur URL-taugliche Zeichen, ebenso
CMS:TEXT … urlencode=true – sie können ohne Maskierung in href und src stehen.
Braucht Javascript einen Wert, geben Sie ihn als data--Attribut aus und lesen ihn
dort – so steht nie Inhalt im Script-Code selbst:
<div id="produkt" data-title="(CMS:TEXT title specialchars=true)"></div>
<script>var title = document.getElementById('produkt').dataset.title;</script>
Listen: SELECT und die Ergebnisschleife
Listen entstehen immer aus zwei Templates: Das aufrufende Template fragt mit CMS:SELECT ab und nennt ein Listentemplate. Das Listentemplate durchläuft die Treffer zwischen CMS:RESULT_START und CMS:RESULT_END.
Aufrufendes Template:
<h2>Aktuelle Meldungen</h2>
(CMS:SELECT meldungen_liste
filter[area]=meldungen
sort=datum
order=desc
limit=10
noresult="<p>Keine Meldungen.</p>"
)
Listentemplate meldungen_liste:
<ul>
(CMS:RESULT_START)
<li><a href="(CMS:URL)">(CMS:TEXT titel)</a> – (CMS:DATE datum)</li>
(CMS:RESULT_END)
</ul>
Innerhalb der Schleife ist die jeweilige Zeile der laufende Datensatz. Ein
Listentemplate darf selbst wieder CMS:SELECT enthalten – etwa die Schlagwörter
jedes Artikels.
Blätterfunktion: count=true zählt alle Treffer; CMS:JUMP_LIST,
CMS:JUMP_PREV und CMS:JUMP_NEXT erzeugen daraus die
Navigation.
Veröffentlichung: Templates finden nur veröffentlichte Datensätze –
freigeschaltet, Veröffentlichungsdatum erreicht, Ablaufdatum nicht erreicht. Die
Parameter future, archive lockern das je Abfrage, etwa für ein Archiv
oder eine Terminvorschau.
Der Parameter
stateist ausschließlich für Backend-interne Ansichten vorgesehen.
Bereiche ansprechen
Inhaltsbereiche werden über ihren Namen oder über ihre Nummer angesprochen (area_id):
filter[area]=meldungen, filter[area]=12 oder filter[area_id]=12. Mehrere Bereiche
trennen Sie durch Komma. Weil Feldnamen systemweit einheitlich sind, lassen sich mehrere
Bereiche gemeinsam filtern und sortieren:
(CMS:SELECT zeitleiste
filter[area]=meldungen,termine,blog
filter[datum] >= 2026-01-01
sort=datum
order=desc
)
sort=area sortiert nach Bereich; _default steht in Filtern und Sortierung für das
Hauptfeld jedes Bereichs, egal wie es dort heißt.
Übersichtsseite eines Bereichs
Die Adresse aus dem Slug-Prefix allein (/termine) ruft das Übersichts-Template
des Bereichs auf – ohne Datensatz. An dessen Stelle stehen die Angaben des Bereichs:
| Wert | Inhalt |
|---|---|
area_id |
Nummer des Bereichs |
area |
Name des Bereichs |
area_label |
Beschriftung des Bereichs (Snippets in der Sprache des Aufrufs) |
area_type, area_icon |
Typ und Icon des Bereichs |
instance_id, language_id |
Instanz des Bereichs und Sprache des Aufrufs |
slug_prefix |
Slug-Prefix in der Sprache des Aufrufs |
record_id |
immer 0 – es gibt keinen laufenden Datensatz |
overview |
true |
So listet ein Übersichts-Template die Datensätze seines Bereichs, ohne dessen Namen fest einzutragen – und lässt sich für mehrere Bereiche verwenden:
<h1>(CMS:TEXT area_label specialchars=true)</h1>
(CMS:SELECT termin_liste
filter[area]="{$cms['area_id']}"
sort=datum order=asc
)
Verknüpfte Datensätze
Mit relation fragt CMS:SELECT die Datensätze ab, die mit dem laufenden (oder mit
src_id genannten) Datensatz über ein Verknüpfungsfeld verbunden sind.
(CMS:SELECT galerie_liste
relation=see_also
sort="created_at desc"
)
Mit sort="_custom asc" erscheinen verknüpfte Datensätze in der Reihenfolge, die die Redaktion
im Lookup-Feld (mittels der Pfeil-Schaltflächen) festgelegt hat:
(CMS:SELECT galerie_liste
relation=bilder
sort="_custom asc"
)
Nach Feldern verknüpfter Datensätze sortieren und filtern Sie mit der
Punkt-Schreibweise: sort=autor.nachname.
Steht kein laufender Datensatz zur Verfügung und ist src_id nicht angegeben oder ungültig,
findet die Suche mit dem Parameter relation nichts.
Das Konzept: Jeder Datensatz hat eine Seite
In camindo CMS hat jeder Datensatz eine eigene Seite im Frontend – nicht nur
Artikel und Produkte, sondern auch jede Kategorie, jedes Schlagwort, jedes Material.
Dafür braucht eine Website keine Parameter wie ?cat=12 oder ?tag=eiche:
- Verlinken: Listen verlinken jeden Datensatz mit CMS:URL – die Kategorie ebenso wie das Produkt.
- Standard-Template je Bereich: Jeder Inhaltsbereich bekommt ein
Standard-Template – eine Kategorie-Seite, eine Schlagwort-Seite, eine
Produkt-Seite. Mit einem Slug-Prefix erhält jede Art von Seite ihren eigenen
Adressraum (
/de/kategorie/wohnen,/de/schlagwort/eiche). - Verknüpfte Inhalte listen: Auf der Seite steht der Datensatz in
$cms; seine verknüpften Datensätze fragt das Template direkt über ihn ab.
Kategorie-Seite (Standard-Template des Bereichs kategorien):
<h1>(CMS:TEXT titel)</h1>
(CMS:TEXT text prefix="<p>" suffix="</p>")
(CMS:SELECT produkt_liste
relation=rel_produkte
sort=position
order=asc
)
Listentemplate produkt_liste – die Produkte verlinken wieder auf ihre eigene Seite:
<ul>
(CMS:RESULT_START)
<li><a href="(CMS:URL)">(CMS:TEXT titel)</a></li>
(CMS:RESULT_END)
</ul>
Die Navigation ist ebenso einfach – eine Liste aller Kategorien mit Links:
(CMS:RESULT_START)
<a href="(CMS:URL)">(CMS:TEXT titel)</a>
(CMS:RESULT_END)
Welche Abfrage die verknüpften Datensätze liefert, hängt davon ab, von welcher Seite die Verknüpfung ausgeht:
| Verknüpfung | Abfrage auf der Seite des Datensatzes |
|---|---|
| geht vom Datensatz aus (sein eigenes Verknüpfungsfeld) | (CMS:SELECT liste relation=feldname) |
Beziehungsfeld (relation) eines anderen Bereichs zeigt auf ihn |
(CMS:SELECT liste relation=rel_bereichsname) – über das Gegenfeld |
Verweisfeld (pointer) eines anderen Bereichs zeigt auf ihn |
(CMS:SELECT liste filter[area]=bereichsname filter[feldname]="{$cms['record_id']}") |
Beziehungsfelder (relation, relation1) sind in beide Richtungen verknüpft:
Beim Anlegen erzeugt camindo CMS im Zielbereich ein Gegenfeld namens
rel_{bereichsname} (etwa rel_produkte), das die Verknüpfung von der anderen Seite
führt. Die Kategorie-Seite fragt ihre Produkte deshalb mit
relation=rel_produkte ab, die Produktseite ihre Kategorien mit
relation=kategorien.
Verweisfelder (pointer, pointer1) sind gerichtet – sie können dafür auf
mehrere Bereiche zeigen. Der Zieldatensatz kennt seine Verweise nicht; das Gegenstück
ist ein Filter auf das Verweisfeld mit der Nummer des laufenden Datensatzes:
(CMS:SELECT produkt_liste
filter[area]=produkte
filter[kategorie]="{$cms['record_id']}"
)
Achtung: Die Nummer des laufenden Datensatzes steht in
$cms['record_id']– einen Schlüsselidgibt es nicht. Ein leerer Filterwert hebt den Filter auf:filter[kategorie]="{$cms['id']}"findet deshalb nicht keine, sondern alle Produkte.
Suchen und freie Filterkombinationen bleiben Parameter (?q=…), denn sie sind keine
Datensätze. Alles, was ein Datensatz ist, hat dagegen seine eigene Adresse – und damit
auch Seitentitel, Beschreibung und Frontend-Editing wie jede andere Seite.
Filter-Syntax
Die Werte in filter[…] folgen derselben Syntax wie die Suchmaske im Backend:
| Wert | Bedeutung |
|---|---|
Berlin |
gleich |
Berlin,Hamburg |
eines von beiden (mit filter_mode[feld]=AND: beide) |
Hen*, *mann, *berg* |
Platzhalter |
!Berlin, !*mann |
ungleich |
==Stern* |
exakt gleich, * ohne Platzhalterwirkung |
>100, >=100, <100, <=100 |
Vergleiche – auch für Datumsangaben |
>2 AND <4, Hen* OR Her* |
Bedingungen im selben Feld verknüpfen |
2026, 2026-06 |
Datum: das ganze Jahr bzw. der ganze Monat |
now, today, now-2h, today+7d |
Datum relativ zum Aufruf, in der Zeitzone des Benutzers bzw. der Sprache wie ein getipptes Datum; today ohne Stunden-Versatz ist der ganze Tag. Versatz in s, m, h, d |
utc_now, utc_today, utc_now-15m |
dasselbe in UTC – für Vergleiche mit Zeitpunkten, die unabhängig vom Besucher gelten (Sperrfristen, Zeitfenster eines Scheduler-Laufs) |
(empty), !(empty) |
ohne eigenen Wert (leer oder nicht übersetzt) bzw. mit |
Der Operator darf auch zwischen Schlüssel und Wert stehen – praktisch, wenn der Wert aus einer Variablen kommt:
(CMS:SELECT sperrfrist_mail
filter[area] = pressemeldungen
filter[online_at] > {$set['last_run']}
filter[online_at] <= {$set['this_run']}
filter_mode[online_at] = AND
future = true
)
entspricht filter[online_at]=">{$set['last_run']}" und
filter[online_at]="<={$set['this_run']}". Mit einem solchen Operator ist der Wert
selbst frei von Operatoren: Beginnt der Inhalt der Variablen mit <, > oder !,
wird das als Text verglichen.
Ein Datumswert, der sich nicht auswerten lässt (etwa ein Unix-Zeitstempel wie
$_SERVER['REQUEST_TIME']), wird ohne Meldung übergangen – die Liste zeigt dann
alle Datensätze, als stünde der Filter nicht da. Für Analysen in Ihrer Entwicklungsumgebung
hilft debug=true: Die erzeugte SQL-Abfrage wird angezeigt.
Textvergleiche nach Sprache: Text- und Zeichenfelder (auch _default) werden
nach den Regeln der Sprachversion verglichen und sortiert, in der gesucht wird –
abgeleitet aus dem Sprachkürzel, einzustellen ist nichts. Immer gilt: keine
Unterscheidung von Groß-/Kleinschreibung und Akzenten (cafe findet „Café“), ß
gleich ss. Dazu kommen die Regeln einzelner Sprachen:
| Sprache | Regel | Beispiel |
|---|---|---|
| Deutsch | ä = ae, ö = oe, ü = ue (Telefonbuch-Regel) |
Mue* findet „Müller“, Mul* nicht |
| Schwedisch | å, ä, ö sind eigene Buchstaben |
o findet kein ö; ö sortiert hinter z |
| Dänisch, Norwegisch | æ, ø, å sind eigene Buchstaben, aa = å |
Aa* findet „Ålesund“ |
| Türkisch | i/İ und ı/I sind verschieden |
Eigene Regeln haben außerdem Bulgarisch, Esperanto, Estnisch, Galicisch, Isländisch, Kroatisch und Bosnisch, Lettisch, Litauisch, Polnisch, Rumänisch, Russisch, Serbisch (lateinisch), Slowakisch, Slowenisch, Spanisch, Tschechisch, Ungarisch und Vietnamesisch. Alle übrigen Sprachen – auch Englisch – folgen den allgemeinen.
Das gilt für =, Platzhalter und Vergleiche wie > gleichermaßen, ebenso für
Volltextfelder (Mueller findet „Müller“, Muller nicht). Im Backend ist es die
Sprache, die für die Instanz des durchsuchten Bereichs eingestellt ist; führen die
Instanzen einer Suche auf verschiedene Regeln, gelten nur die allgemeinen.
Verschiedene Filter werden immer mit UND verknüpft. Volltextfelder verstehen
+muss -darfnicht; verschlüsselte Felder nur den exakten Wert.
In Bereichen mit Baumstruktur liefert filter[parent_id]=42 alle Nachkommen des
Datensatzes 42, zusammen mit filter[depth]=1 nur die direkten Kinder – die
Grundlage für Navigationen.
Templates einbinden und organisieren
CMS:INCLUDE bindet ein anderes Template ein – etwa Kopf und Fuß der Seite:
(CMS:INCLUDE layout.kopf)
<main>…</main>
(CMS:INCLUDE layout.fuss)
Template-Namen müssen nur innerhalb ihres Template-Bereichs eindeutig sein. Gibt es
einen Namen in mehreren Bereichen, sprechen Sie ihn als bereichsname.templatename
an. Einen Namen, der nicht eindeutig ist, meldet die Seite als Fehler.
Übliche Aufteilung:
| Art | Aufgabe |
|---|---|
| Seiten-Templates | Standard-Templates der Inhaltsbereiche: geben einen Datensatz als ganze Seite aus |
| Listentemplates | durchlaufen die Treffer eines CMS:SELECT |
| Bausteine | Kopf, Fuß, Navigation, Teaser – per CMS:INCLUDE eingebunden |
| Standalone-Templates | Seiten ohne Datensatz: Suche, Sitemap, Formulare |
| Zeitgesteuerte Templates | laufen als Aufgabe des Zeitplaners, ohne Besucher (siehe unten) |
| Listen-Templates (Backend) | gestalten die Datensatzliste der Redaktion (siehe unten) |
Caching
Aufwendige Bausteine – eine Navigation aus vielen Abfragen, eine Produktliste – kann
CMS:INCLUDE mit caching zwischenspeichern: (CMS:INCLUDE layout.navigation caching=1h) erzeugt die Navigation höchstens einmal in der Stunde je Website und
Sprache. Wovon die Ausgabe sonst noch abhängt, gehört in cacheparams – siehe
CMS:INCLUDE.
Mehrsprachigkeit
Ein Template gibt automatisch die Sprachversion des Aufrufs aus – bei fehlender
Übersetzung die Hauptsprache. Links mit CMS:URL behalten die Sprache bei;
mittels Parameter lang wird explizit auf eine andere Sprachversion verlinkt. Einen
Sprachumschalter bauen Sie mit CMS:SELECT_LANGVERSION.
Datums- und Zahlenformate richten sich nach der Sprachversion; die Zeitzone ebenso.
Feste Beschriftungen („Weiterlesen“, „Suchen“) pflegt die Redaktion als eigenständige Datensätze (etwa
in einem Bereich namens „Sonstige Texte“) in allen Frontend-Sprachen. In diesem Bereich sollte
es ein einsprachiges slug-Feld und ein mehrsprachiges text-Feld geben. Dann arbeitet CMS:TEXT
als Snippet-Lieferant, vgl. CMS:TEXT mit id-Parameter.
<a href="(CMS:URL)">(CMS:TEXT id=mysnippets.readmore)</a>
Frontend-Editing
CMS:FED setzt an einen Inhalt eine Bearbeiten-Markierung, die nur Benutzer mit eingeschaltetem Frontend-Editing sehen. Ein Klick öffnet die Eingabemaske des Datensatzes im Backend. CMS:FED_CODE gibt HTML nur in diesem Fall aus – etwa ein Stylesheet für die Markierungen.
Sicherheits-Header: CSS, Javascript, JSON und Einbetten
camindo CMS schickt vor jedem Template einige HTTP-Header mit, die der Browser als Schutz umsetzt:
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'
Referrer-Policy: strict-origin-when-cross-origin
Für gewöhnliche HTML-Seiten müssen Sie nichts tun. Zwei Fälle wirken sich aber aus – und die Ursache ist nicht auf den ersten Blick zu sehen:
Templates, die CSS, Javascript oder JSON ausliefern
Ein Template wird als text/html ausgeliefert, wenn es nichts anderes sagt. Wegen
nosniff nimmt der Browser dann den Inhalt nicht als Stylesheet oder Script an, auch
wenn er richtig ist. Die Seite sieht ungestaltet aus, das Script läuft nicht – und die
Konsole des Browsers meldet etwas wie „refused to execute script … MIME type
('text/html') is not executable“ bzw. „… not a supported stylesheet MIME type“.
Setzen Sie deshalb am Anfang jedes solchen Templates den passenden Typ – vor jeder Ausgabe, ohne Leerzeile davor:
(CMS:HEADER "Content-Type: text/css; charset=utf-8")
(CMS:HEADER "Content-Type: text/javascript; charset=utf-8")
(CMS:HEADER "Content-Type: application/json; charset=utf-8")
Ausgaben in anderen Websites einbetten
Eine Seite der Website lässt sich nur innerhalb derselben Website in einem
<iframe> zeigen. Das schützt Anmeldung, Profil und Warenkorb davor, dass eine fremde
Seite sie unsichtbar überlagert und Besuchern Klicks unterschiebt (Clickjacking). Bindet
eine Partnerwebsite eine Seite per <iframe> ein, bleibt der Rahmen deshalb leer oder
zeigt eine Fehlerseite des Browsers; die Konsole meldet „Refused to frame … because an
ancestor violates … frame-ancestors“ bzw. „… X-Frame-Options to 'sameorigin'“.
Soll ein Template eingebettet werden dürfen – ein Widget, ein Terminkalender für
Partner –, geben Sie das nur in diesem Template frei: X-Frame-Options ohne Wert
entfernen und die erlaubten Websites in frame-ancestors nennen:
(CMS:HEADER "X-Frame-Options:")
(CMS:HEADER "Content-Security-Policy: frame-ancestors 'self' https://www.partner.example https://*.partner.example")
Beide Zeilen gehören zusammen: Ein Browser, der X-Frame-Options noch sieht, verweigert
den Rahmen trotz frame-ancestors. Nennen Sie die Partner einzeln – frame-ancestors *
erlaubt jeder Website das Einbetten – auch das der Seiten mit Formularen.
Andere Einbindungen sind nicht betroffen: Bilder, Downloads, per Javascript geladene
Daten (JSON) und Links auf Ihre Website funktionieren ohne Freigabe.
Mehr zu den Header-Zeilen: CMS:HEADER.
Templates im Dateisystem
Jedes Template liegt als Datei unter
var/templates/{instance_id}/{bereichsname}/{templatename}.cms.php. Sie können es
dort mit einem Code-Editor bearbeiten und mit Git verwalten. camindo CMS übernimmt
Änderungen minütlich (oder sofort über „Aktualisieren“ im Modul Templates) und
übersetzt das Template neu. Umbenennen sollten Sie Templates im Backend – im
Dateisystem gilt Umbenennen als Löschen und Neuanlegen, und die Zuweisungen gehen
verloren. Fehlermeldungen erscheinen im Widget „Template-Meldungen“.
Editor-Unterstützung
Für Visual Studio Code gibt es die Erweiterung „camindo CMS Templates“
(github.com/dirkintelligence/camindo-vscode,
auch im Visual Studio Marketplace und bei Open VSX). Sie färbt in *.cms.php-Dateien die
CMS-Befehle lila und die PHP-Ausdrücke darin orange – wie die Ansicht im Backend –, ergänzt
nach (CMS: die Befehlsnamen und danach die Parameter des Befehls und zeigt beim Überfahren
deren Beschreibung, in derselben Sprache wie das Backend.
Die Erweiterung kennt die Befehle des Kerns und der Standardmodule. Bringen Ihre Module eigene Befehle mit, schreiben Sie die Befehlsliste Ihres Projekts mit
php Core/cron/camindo.php template commands --all --out=var/vscode
und tragen das Verzeichnis in der VS-Code-Einstellung camindo.commandsFile ein.
Zeitgesteuerte Templates
Ein Template mit der Verwendung „Zeitgesteuert“ (Reiter „Zugriff“ des Templates) läuft nicht als Seite, sondern als Aufgabe des Zeitplaners (Aufgabentyp „Template“, siehe Handbuch für Administratoren). Über eine Adresse ist es nie erreichbar.
Es läuft wie ein Seitenaufruf ohne Datensatz: in der Website seines Template-Bereichs, in der Sprache, die die Aufgabe nennt – mit deren Texten, Snippets, Datumsformaten und Zeitzone. Es gibt keinen Besucher, also keine Anmeldung und keinen Warenkorb. Die Aufgabe übergibt das Zeitfenster seit dem letzten erfolgreichen Lauf:
| Variable | Inhalt |
|---|---|
$set['last_run'] |
Beginn des Zeitfensters (ausschließlich), Zeitzone der Sprache |
$set['this_run'] |
Ende des Zeitfensters (einschließlich), Zeitzone der Sprache |
$set['last_run_utc'], $set['this_run_utc'] |
dasselbe in UTC |
Beim ersten Lauf ist das Zeitfenster leer. Schlägt ein Lauf fehl, holt der nächste den
ganzen Zeitraum nach – nichts geht verloren und nichts wird doppelt verarbeitet. Das
Zeitfenster passt direkt in einen Filter; zwei Bedingungen für dasselbe Feld verknüpft
filter_mode mit AND:
(CMS:SELECT sperrfrist_mail
filter[area]=pressemeldungen
filter[online_at] > {$set['last_run']}
filter[online_at] <= {$set['this_run']}
filter_mode[online_at]=AND
future=true
)
Das Ergebnis-Template verschickt dann etwa mit CMS:EMAIL (submit=1) je
Treffer eine Nachricht; CMS:URL mit absolute=true liefert die vollständige
Adresse für Links. Meldet ein Befehl einen Fehler, gilt der Lauf als fehlgeschlagen.
Was das Template ausgibt, speichert die Aufgabe auf Wunsch unter var/custom/ – so
entsteht etwa eine Sitemap oder ein Feed als Datei.
Vorschau
Die Vorschau aus dem Backend zeigt auch unveröffentlichte Datensätze mit ihrem
Template. Listen darin zeigen weiterhin nur veröffentlichte Datensätze, ebenso
id=… und CMS:RELATION. In der
Vorschau liefert $this->isPreview() true.
Listen-Templates für das Backend
Ein Inhaltsbereich kann ein Listen-Template haben, das nur im Backend verwendet
wird: für eine zusätzliche Spalte der Datensatzliste und die ausführliche Vorschau in
Lookup-Feldern. Im Template steht der Datensatz der Zeile als $cms bereit;
die Ausgabebefehle CMS:TEXT, CMS:MEDIA… funktionieren wie im Frontend:
(CMS:MEDIA bild_klein width=60)
<small>(CMS:NUMBER preis format="%.2f" decimal="," suffix=" €")</small>
CMS:RELATION ist hier hilfreich, um das Hauptfeld verknüpfter Datensätze zu listen.
2 Template-Sätze
Gekapselte Funktionalität
Ein Template-Satz bündelt eine Funktion der Website: Templates, Inhaltsbereiche, ggf. Datensätze und Snippets. Beispiele:
| Template-Satz | Inhaltsbereiche | Templates |
|---|---|---|
| News | Meldungen, Rubriken | Übersicht mit Blätterfunktion, Detailseite, Rubrikenseite, Archiv, Teaser für die Startseite, RSS-Feed |
| Veranstaltungen | Termine, Orte | Kalender, Detailseite mit Anfahrt, Terminvorschau |
| Mitgliederbereich | Mitglieder (Profil), Beiträge (UGC) | Registrierung, Bestätigung, Anmeldung, Passwort vergessen, Profil bearbeiten, geschützte Seiten |
| Seitenbaukasten | Seiten, Textblock, Bildblock, Galerie (abhängige Bereiche) | Seite, je Blocktyp ein Baustein |
Ein guter Template-Satz ist direkt betriebsbereit: Importieren, Inhalte eingeben, fertig. Und er ist anpassbar an den individuellen Aufbau einer Website, ohne dass die Funktion neu entwickelt werden muss. Weitergegeben wird er per XML-Export mit der Option „Templates für Import in Fremddatenbank optimieren“ (siehe Handbuch für Administratoren).
Regeln für portable Template-Sätze
Ein Ordner, ein Template-Bereich
Legen Sie die Inhaltsbereiche und den Template-Bereich eines Satzes gemeinsam in einen Ordner. So exportiert ein Klick den ganzen Satz, und beim Import ist klar, was zusammengehört.
Namen und Ids direkt im Befehlsparameter
Templates, Inhaltsbereiche, Datensätze werden in den Templates mittels ihrer ID oder ihres Namens (bzw. Slugs) angesprochen. In einer fremden Datenbank sind diese unter Umständen schon belegt; dann vergibt der Importer neue und passt die Templates daran an. Die Anpassung gelingt aber nur, wenn IDs, Namen, Slugs direkt in den Befehlsparametern stehen. Steht im Parameter eine PHP-Variable, die erst zur Laufzeit ihren Wert enthält, schlägt die Anpassung fehl.
Einheitliche Feldnamen
Für Ihre eigenen Template-Sätze: Verwenden Sie dieselben Feldnamen wie im Rest der Website
(titel, datum, text, bild, slug). Dann funktionieren bereichsübergreifende Listen – etwa eine
Zeitleiste aus News und Terminen – ohne Anpassung, und ein Template-Satz lässt sich
leicht mit bestehenden Bereichen kombinieren.
Für Template-Sätze in anderen Datenbanken: Nutzen Sie die Namens-Präfixe, damit es beim Import nicht zu Typkonflikten kommt (siehe Reservierte Namens-Präfixe).
Logik und Gestaltung trennen
Halten Sie die Logik (Abfragen, Filter, Blätterfunktion, Formularauswertung) in wenigen Templates zusammen und die Gestaltung in eigenen, kleinen Bausteinen:
news_uebersicht (CMS:SELECT news_liste filter[area]=meldungen … count=true)
news_liste (CMS:RESULT_START) (CMS:INCLUDE news_teaser) (CMS:RESULT_END)
(CMS:JUMP_LIST …)
news_teaser <article class="teaser"> … (CMS:TEXT titel) … </article>
Wer den Satz an ein anderes Design anpasst, ändert nur news_teaser und die übrigen
Gestaltungsbausteine; die Funktion bleibt unberührt.
Das Layout der Website einbinden
Verwenden Sie feste Kopf- und Fuß-Templates; diese können einfach durch vorhandene ersetzt werden.
Texte in einem eigenen Inhaltsbereich
Feste Beschriftungen („Weiterlesen“, „Zurück zur Übersicht“, Fehlermeldungen von
Formularen) gehören in einen eigenen Inhaltsbereich (etwa „Beschriftungen“).
Eingebunden werden sie mittels CMS:TEXT, z. B. (CMS:TEXT id=beschriftungen.weiterlesen).
So können sie einfach für die spätere Website angepasst / übersetzt werden.
Label als Custom-Snippets
Um auch Bereichsnamen und Eingabefelder mehrsprachig zu halten, können Custom-Snippets angelegt werden (siehe Modul Snippets). Die im Template-Satz verwendeten Snippets werden automatisch in die Export-Datei integriert.
Einstellungen als Datensatz
Soll die Redaktion das Verhalten eines Satzes steuern – Anzahl der Einträge je Seite, Titel der Übersichtsseite, ein Einleitungstext –, legen Sie dafür einen kleinen Inhaltsbereich „Einstellungen“ mit einem einzigen Datensatz an und lesen ihn über seinen Slug:
(CMS:TEXT einleitung id=news-einstellungen)
(CMS:NUMBER eintraege_je_seite id=news-einstellungen set=limit)
(CMS:SELECT news_liste filter[area]=meldungen limit="{$set['limit']}" count=true)
Beispielinhalte mitliefern
Ein paar Beispiel-Datensätze zeigen nach dem Import sofort, wie der Satz aussieht und funktioniert. Exportieren Sie sie mit, samt Bildern.
Dokumentieren
Legen Sie dem Satz eine kurze Beschreibung bei – als Hinweis des Ordners oder als Datensatz: welche Templates als Standard-Template zuzuweisen sind, welche Layout-Templates erwartet werden, welche Snippets und Konverter der Satz nutzt.
CSS als Template
Binden Sie das benötigte CSS als Template ein – so erscheint der Template-Satz sofort im gewollten Design. Der Nutzer kann das CSS jederzeit in eine Datei auslagern. Beachten Sie jedoch, dass dieses Template gesonderte Header-Anweisungen benötigt, siehe Templates, die CSS, Javascript oder JSON ausliefern.
Einen Template-Satz einsetzen
- Importieren (Modul Inhalte → Startseite → Menü → Import), vorzugsweise zuerst in einer Testumgebung.
- Mit der Vorschau testen – insbesondere Verweise, die das Importprotokoll als „nicht zugeordnet“ meldet.
- Header und Footer anpassen oder durch eigene ersetzen.
- Falls es sich beim Template-Satz um eine ganze Website handelt: Startseiten-Id oder Startseiten-Template in der Instanz eintragen.
- Rechtliche Aspekte klären, ggf. Texte und Lösch-Policies anpassen.
3 Trigger-Templates
Ein Trigger-Template enthält PHP-Methoden, die camindo CMS bei Ereignissen der Datensätze eines Inhaltsbereichs aufruft – beim Anlegen, Speichern, Löschen, Wiederherstellen und bei den Aktionen einer Lösch-Policy. So lassen sich Inhalte beim Speichern aufbereiten (mehrere Felder zu einem zusammenfassen, Werte prüfen), verbundene Daten nachziehen oder personenbezogene Daten nach eigenen Regeln anonymisieren.
Die Methoden laufen, gleich woher die Änderung kommt: Eingabemaske, Frontend
(CMS:SAVEDATA, CMS:USER_…), API, Import, Stapelverarbeitung, Papierkorb-Cronjob
und Lösch-Policies.
Zuweisen
Ein Trigger-Template ist ein gewöhnliches Template in einem Template-Bereich. Sie weisen es in den Einstellungen des Inhaltsbereichs unter „Trigger-Templates“ zu (siehe Handbuch für Administratoren). Ein Bereich kann mehrere Trigger-Templates haben, und dasselbe Template darf mehreren Bereichen zugewiesen sein. So lassen sich wiederverwendbare Bausteine bilden – etwa ein Template, das in allen Bereichen Slugs pflegt, und daneben eines mit den Regeln eines einzelnen Bereichs.
Reihenfolge
Die Templates eines Bereichs laufen bei jedem Ereignis nacheinander in der angezeigten Reihenfolge; Sie ändern sie mit den Pfeil-Schaltflächen neben jedem Eintrag.
- Ein Template ohne die Methode des Ereignisses wird übergangen.
- Alle bekommen dieselben Variablen: Was ein Template an
$dataoder$affectedFieldsändert, sieht das nächste schon. - Wirft ein Template eine Ausnahme, laufen die folgenden nicht mehr, und der Vorgang wird abgebrochen (siehe Abbrechen).
- Jedes Template ist ein eigenes Objekt: Eigenschaften und Hilfsmethoden gleichen
Namens in verschiedenen Templates stören sich nicht. Daten zwischen Templates
tauschen Sie über
$dataaus.
Aufbau
Das Template enthält nur Methoden-Definitionen – keine Ausgabe, keine Anweisungen außerhalb der Methoden. camindo CMS setzt den übersetzten Code in den Rumpf einer Klasse; jede Methode wird so zur Methode eines Objekts:
<?php
// Trigger-Template "mitglieder_trigger" für den Bereich Mitglieder
function onSave(array &$affectedFields, array &$data, array &$previousData): void
{
// Anzeigename aus Vor- und Nachname
$data['name'] = trim(($data['vorname'] ?? '') . ' ' . ($data['nachname'] ?? ''));
if (!in_array('name', $affectedFields, true)) $affectedFields[] = 'name';
}
private function normalize(string $text): string
{
return trim(preg_replace('/\s+/', ' ', $text));
}
- Alle Ereignis-Methoden sind optional. Fehlt eine, passiert bei dem Ereignis nichts.
- Eigene Hilfsmethoden (
private function …), Eigenschaften und Konstanten sind erlaubt. - CMS:SELECT ist erlaubt, um Inhalte verknüpfter Datensätze zu ermitteln (Anmerkungen unten beachten!)
- Klassen sprechen Sie mit vollem Namen an (
\Camindo\Core\Cms,\DateTimeImmutable) –usegeht nicht, weil der Code mitten in einer Klasse steht. - Ein Template, das sich nicht laden lässt (Syntaxfehler, Ausgabe außerhalb der Methoden), bricht den Vorgang mit einer Fehlermeldung ab. Datensätze ohne die vorgesehene Aufbereitung zu speichern wäre schlimmer.
Was im Template bereitsteht
$this->cms |
Cms – snippet(), error(), select(), fetch() … |
$this->db |
Database – eigene Abfragen |
$this->record |
Record – das Dataset, das das Ereignis auslöst (load(), save() …) |
$this->areaId |
Bereich, für den das Template geladen wurde. Ist es mehreren Bereichen zugewiesen, nehmen Sie den Bereich aus $data['area_id'] |
$this->temporary |
true beim Zwischenspeichern einer Arbeitskopie (Reiterwechsel in der Eingabemaske) |
$this->email, $this->pagination … |
die Dienste, die auch Templates kennen |
$cms['record_id'] |
Nummer des auslösenden Datensatzes – nur sie, für die CMS-Befehle; in onSave() die seiner Arbeitskopie (siehe Verknüpfte Inhalte abfragen) |
Ereignisse
| Methode | Wann |
|---|---|
onCreate(array &$data): void |
ein neuer Datensatz wird vorbelegt – Werte in $data erscheinen in der leeren Maske |
onLoaded(array &$data): void |
nach jedem Laden eines Datensatzes (Eingabemaske, Stand vor dem Speichern …) – nicht bei CMS:SELECT im Frontend |
onSave(array &$affectedFields, array &$data, array &$previousData): void |
vor der Prüfung und dem Schreiben. Ein Feld, das die Methode neu setzt, muss sie in $affectedFields eintragen |
onSaved(array &$affectedFields, array &$data, array &$previousData): void |
nach dem Schreiben, vor dem Abschluss der Transaktion |
onSoftDelete(array $data): void |
der Datensatz kommt in den Papierkorb |
onRestore(array $data): void |
der Datensatz kommt aus dem Papierkorb zurück |
onDelete(array $data): void |
endgültiges Löschen, vor dem Löschen in der Datenbank |
onDeleted(array $data): void |
endgültiges Löschen, danach |
onPrivacyAction(string $action, array &$data, array $context): void |
Aktion einer Lösch-Policy – siehe unten |
$previousData ist der gespeicherte Stand vor der Änderung (leer bei einem neuen
Datensatz).
Abbrechen
Alle Ereignisse außer onCreate und onLoaded laufen in der Transaktion des Vorgangs.
Eine Ausnahme bricht ihn ab und macht ihn vollständig rückgängig; die Meldung erscheint
in der Eingabemaske bzw. als Fehler der API:
function onSave(array &$affectedFields, array &$data, array &$previousData): void
{
if (($data['ende'] ?? '') !== '' && $data['ende'] < ($data['beginn'] ?? '')) {
throw new \Exception('Das Ende liegt vor dem Beginn.');
}
}
Zwischenspeichern
Wechselt ein Redakteur in der Eingabemaske den Reiter, speichert camindo CMS den
Zwischenstand der Arbeitskopie – auch dabei laufen onSave und onSaved, mit
$this->temporary = true. Aufwendige oder nach außen wirkende Schritte (E-Mails,
Schnittstellen) gehören hinter if ($this->temporary) return;.
Verschieben
Wird ein Datensatz in einen anderen Bereich verschoben, gelten die Trigger-Templates
des Zielbereichs. $data enthält dort schon die Felder des Zielbereichs,
$previousData den Stand im Herkunftsbereich mit dessen Feldnamen.
Wiedereintritt
Speichert ein Template in onSaved() denselben Datensatz erneut, liefe es endlos.
Ein Ereignis wird deshalb je Template für denselben Datensatz nicht verschachtelt
ausgeführt: Beim erneuten Speichern laufen die übrigen Trigger-Templates des Bereichs,
das gerade laufende nicht. Andere Datensätze – auch desselben Bereichs – lösen ihre
Trigger normal aus.
Verknüpfte Inhalte abfragen
Die Inhalte des auslösenden Datensatzes stehen in $data (neuer Stand) und
$previousData (vorheriger Stand) – mehrsprachige Felder mit allen Sprachen. Dort lesen
und ändern Sie sie, nicht über CMS:TEXT.
Für andere Datensätze, etwa die verknüpften, können Sie CMS:SELECT wie in
jedem Template verwenden. Während eine Methode läuft, enthält $cms nur
record_id, die Nummer des auslösenden Datensatzes. Ein CMS:SELECT mit relation
braucht deshalb kein src_id – und sollte keines haben (siehe unten):
function onSave(array &$affectedFields, array &$data, array &$previousData): void
{
$isVariant = false;
?>
(CMS:SELECT
relation=variante_von
state=any archive=true future=true
resulttemplate=shop.trigger_variante_gefunden
)
<?php
$data['ist_variante'] = $isVariant ? 'ja' : '';
if (!in_array('ist_variante', $affectedFields, true)) $affectedFields[] = 'ist_variante';
}
Das Ergebnis-Template (shop.trigger_variante_gefunden) läuft im Rumpf der Methode: Es
sieht deren Variablen und $this und setzt hier nur $isVariant = true;. Es darf
nichts ausgeben.
Beachten Sie die Grenzen:
-
Neuer Stand der Verknüpfungen über
$cms['record_id']. Während ein vorhandener Datensatz bearbeitet wird, hängen seine Verknüpfungen – ausgehende wie eingehende – an einer Arbeitskopie; erst nach dem Speichern gehen sie auf den Datensatz über. InonSave()– und inonSaved()beim Zwischenspeichern – enthält$cms['record_id']deshalb die Nummer der Arbeitskopie:CMS:SELECT relation=…und CMS:RELATION ohne Quellangabe sehen so schon die geänderten Verknüpfungen, ebenso die Gegenrichtung („Wer verweist auf diesen Datensatz?“) mitdest_id="{$cms['record_id']}"bzw.filter[feld]="{$cms['record_id']}".$data['record_id']ist dagegen immer die Nummer des Datensatzes selbst – damit erhalten Sie inonSave()den bisherigen Stand. InonSaved()nach dem endgültigen Speichern und in allen übrigen Methoden sind beide Nummern gleich. -
Eingebettete Inhalte beim Zwischenspeichern: Verknüpfungen eines Feldes mit eingebetteten Inhalten zeigen beim Zwischenspeichern auf deren Arbeitskopien, die
CMS:SELECTnicht findet. Beim endgültigen Speichern sind diese Inhalte schon übernommen und werden gefunden. -
Veröffentlichung:
CMS:SELECTfindet auch hier nur, was die Website zeigen würde – freigegeben, Erscheinungsdatum erreicht, nicht abgelaufen. Geht es um die Struktur („Ist überhaupt ein Datensatz verknüpft?“), setzen Siestate=any archive=true future=true; sonst fehlen gesperrte, vordatierte und abgelaufene Datensätze. Datensätze im Papierkorb findetCMS:SELECTnie. -
Rechte: Die Abfrage sieht, was der Auslöser sehen darf. Das Ergebnis kann sich deshalb unterscheiden, je nachdem, wer speichert:
Auslöser Was CMS:SELECTfindetBenutzer im Backend oder über die API nur Inhalte der Instanzen, Bereiche und Sprachen, die seine Benutzergruppe lesen darf (Superuser: alle) Website ( CMS:SAVEDATA,CMS:USER_…)Inhalte der Instanz und ihrer lesbaren Instanzen Zeitplaner (auch später fällige Blöcke einer Lösch-Policy) alle -
Sprache: Mehrsprachige Felder der gefundenen Datensätze erscheinen in einer Sprache – auf der Website in der Sprache der Seite, im Backend in der Inhaltssprache des Benutzers. Alle Sprachen liefert
$this->record->load($id).
onPrivacyAction: Lösch-Policies
Eine Lösch-Policy legt fest, was beim Löschen eines Datensatzes geschieht. Was sich mit „Einzelne Felder überschreiben“ nicht ausdrücken lässt – verknüpfte Datensätze, Texte, die nur teilweise personenbezogen sind, mehrsprachige Inhalte je Sprache –, erledigt das Trigger-Template mit zwei Aktionen:
| Aktion der Policy | Aufruf |
|---|---|
| „Trigger-Template auf den Datensatz anwenden“ | einmal, $data ist der Datensatz |
| „Trigger-Template auf das Audit-Log anwenden“ | einmal je Eintrag im Audit-Log des Datensatzes, $data ist der Eintrag |
Jeder solche Block hat eine Kennung (Kleinbuchstaben, Ziffern, _). Sie kommt
als $action an – daran unterscheidet das Template die Blöcke, etwa einen sofortigen
Schritt und einen nach einem Jahr.
function onPrivacyAction(string $action, array &$data, array $context): void
$context |
|
|---|---|
target |
record oder audit |
record_id |
der gelöschte Datensatz |
user_id |
der Website-User, dem er gehört (record.user_id), sonst 0 |
area_id |
sein Bereich |
source |
Auslöser: record (Datensatz wird gelöscht), profile (UGC: Profil wurde gelöscht) oder area (Bereich wurde gelöscht) |
policy_id, job_id |
Policy und Auftrag |
deleted |
nur bei record: true, wenn der Datensatz zu dem Zeitpunkt im Papierkorb liegt |
Auf den Datensatz
$data enthält alle Felder des Datensatzes – mehrsprachige Felder als Array
language_id => Wert, Dateifelder mit ihren Metadaten. Was die Methode an $data
ändert, speichert camindo CMS anschließend:
- im Policy-Modus: ohne Pflichtfeld- und Formatprüfung und ohne Rechte der Benutzergruppe – „unique“ gilt weiter;
- Dateifelder,
record_id,area_id,user_idund die übrigen Kennungen nicht. Eine Datei entfernen Sie mit „Einzelne Felder überschreiben“ (bild = "") im selben oder einem späteren Block; - im Audit-Log als Aktion „nach Lösch-Policy angepasst“ (
privacy), ohne die vorherigen Werte.
Liegt der Datensatz im Papierkorb ($context['deleted']), läuft die Methode ebenfalls
– etwa um verknüpfte Daten aufzuräumen –, gespeichert wird am Datensatz dann aber
nichts. Andere Datensätze bearbeitet die Methode selbst über $this->record oder
$this->db.
Auf das Audit-Log
Für jeden Eintrag im Verlauf des Datensatzes wird die Methode mit dem Eintrag aufgerufen:
| Schlüssel | |
|---|---|
id, t |
Nummer und Zeitpunkt – bleiben unverändert |
action |
create, update, delete, restore, purge, privacy … |
actor |
type (admin, user, system), admin_id, user_id, ip, origin – Handelnde nur als Nummer, ohne Namen |
label |
Bezeichnung des Datensatzes zu dem Zeitpunkt |
changes |
Liste der Änderungen: k Feldname (mehrsprachig feld@de_DE), l Beschriftung, t Feldtyp, g Sprache, b vorher, a nachher |
Geänderte Einträge schreibt camindo CMS zurück – in die Datei unter var/audit/ mit
einem neuen Kontrollpunkt der Prüfsummenkette, label auch in die Datenbank. Einträge, die erst später entstehen (etwa beim endgültigen Löschen),
durchlaufen die Methode nicht; planen Sie dafür einen späteren Block.
Hat der Bereich mehrere Trigger-Templates mit onPrivacyAction(), laufen sie
nacheinander mit derselben Kennung; jedes prüft selbst, ob $action es betrifft.
Fehler
Hat keines der Trigger-Templates des Bereichs onPrivacyAction() oder wirft die
Methode eine Ausnahme, scheitert der Auftrag: Er steht mit der Meldung im Log privacy und
wird beim nächsten Lauf der Zeitplaner-Aufgabe „Lösch-Policies anwenden“ erneut versucht –
höchstens 20-mal. Scheitert ein sofort fälliger Block, bricht das Löschen nicht ab:
Die übrigen Blöcke laufen, der gescheiterte folgt mit dem nächsten Lauf.
CMS:DATE
Gibt den Datumsteil eines Datumswertes formatiert aus.
(CMS:DATE field [parameter=wert …])
Methode: Cms::cmsDate()
Ohne Schlüssel: der erste Wert ohne = gilt als field
Mindestens einer von: field, useval
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
id |
Datensatz-Id oder Slug |
(aktueller Datensatz) |
Datensatz, aus dem gelesen wird. Reine Ziffernfolge = record_id, alles andere = Slug - dieselbe Regel wie in der URL. | |
field ¹ |
Feld |
– | ² | Name eines Datensatzfeldes, dessen Inhalt als Quelle genutzt wird. |
useval |
Datumswert |
– | ² | Wert in Textform, der als Quelle genutzt wird. |
format |
Formatangaben |
– | ICU-Muster, z. B. "dd.MM.y". Ohne Angabe entscheidet die Sprache der Seite über die Schreibweise. | |
timezone |
true / false |
false |
true: Der Name der Zeitzone wird angehängt (z. B. "Europe/Berlin"). | |
replace[…] |
Text?Text |
– | Kommt der Text vor dem Fragezeichen im formatierten Wert vor, wird er durch den Text nach dem Fragezeichen ersetzt (z. B. "Jan?Januar"). | |
ifempty |
Text |
– | Text, der ausgegeben wird, falls die Quelle ('field' oder 'useval') leer ist. | |
instead |
Text |
– | Text, der anstelle der Quelle ausgegeben wird, sofern diese nicht leer ist. | |
prefix |
Text |
– | Text, der vorangestellt wird, sofern der formatierte Wert nicht leer ist. | |
suffix |
Text |
– | Text, der angehängt wird, sofern der formatierte Wert nicht leer ist. | |
set |
Array-Index von $set |
– | Der formatierte Wert wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
² mindestens einer der mit ² markierten Parameter muss angegeben werden.
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Gibt den Datumsteil eines Datums-/Zeitwertes aus. Quelle ist ein Datums- oder
Datum-Zeit-Feld (Metafelder online_at, created_at, changed_at oder eigene Felder).
Ohne format entscheidet die Sprache der Seite über die Schreibweise:
(CMS:DATE online_at) 07.03.2024 (de)
(CMS:DATE online_at format="EEEE, d. MMMM y") Donnerstag, 7. März 2024
(CMS:DATE online_at format="y-MM-dd") 2024-03-07
Typisch in Listen:
<time datetime="(CMS:DATE online_at format='y-MM-dd')">(CMS:DATE online_at)</time>
CMS:DATE, CMS:DATETIME und CMS:TIME sind derselbe Befehl mit unterschiedlichem
Ausschnitt; alle drei kennen dieselben Parameter (CMS:DATETIME zusätzlich autotime).
Parameter im Einzelnen
id
id liest aus einem anderen Datensatz statt aus dem laufenden - record_id oder Slug,
genau wie in der URL:
(CMS:DATE online_at id=1977)
(CMS:DATE online_at id=impressum)
field
field ist der Hauptparameter und darf ohne Schlüssel stehen - beide Zeilen sind gleich:
(CMS:DATE field=online_at)
(CMS:DATE online_at)
useval
useval nimmt statt eines Feldes einen Wert direkt aus dem Template - meist eine
PHP-Variable oder ein zuvor mit set abgelegter Wert:
(CMS:DATE useval="2024-12-24")
(CMS:DATE useval="{$set['wert']}")
(CMS:DATE useval="{$set['user']['created_at']}")
format
format ist ein ICU-Muster (d Tag, MM Monat zweistellig, MMMM Monatsname, y Jahr,
HH:mm Uhrzeit, EEEE Wochentag). Ohne format entscheidet die Sprache der Seite (im Backend die Locale des Benutzers):
(CMS:DATE online_at format="d. MMMM y")
Ausgabe: 7. März 2024
timezone
(CMS:DATE online_at timezone=true)
Ausgabe: 07.03.2024 Europe/Berlin
replace
replace ersetzt Teiltexte im fertig formatierten Wert - Kurzform mit ? als
Trenner oder ein Eintrag je Suchtext:
(CMS:DATE online_at format="MMM y" replace="Jan.?Januar")
(CMS:DATE online_at format="MMM y" replace[Jan.]="Januar" replace[x]="y")
Ausgabe: Januar 2026 statt Jan. 2026
ifempty
ifempty ersetzt die gesamte Ausgabe, wenn die Quelle leer ist; prefix und
suffix entfallen dann:
(CMS:DATE offline_at prefix="<p>" suffix="</p>" ifempty="unbegrenzt")
Als leer gelten auch die Platzhalter für „kein Datum“ und „kein Ende“, mit denen
camindo CMS etwa ein unbegrenztes offline_at speichert (vor dem 2. Januar 1000 bzw. ab
dem 31. Dezember 8999) – mit und ohne format.
instead
instead tauscht den Wert aus, sobald die Quelle nicht leer ist - nützlich, wenn
nur das Vorhandensein zählt:
(CMS:DATE offline_at instead="befristet" ifempty="-")
prefix
prefix / suffix umschließen den Wert und erscheinen nur, wenn er nicht leer ist -
so entsteht kein leeres veröffentlicht am:
(CMS:DATE online_at prefix="veröffentlicht am " suffix="")
suffix
(CMS:DATE online_at suffix=" (Erstveröffentlichung)")
Siehe prefix.
set
set gibt nichts aus, sondern legt das Ergebnis unter $set['date'] für das
umgebende PHP oder einen späteren Befehl ab:
(CMS:DATE online_at set=date)
<?php if ($set['date'] !== '') { ?> … <?php echo $set['date']; ?> … <?php } ?>
CMS:DATETIME
Gibt Datum und Uhrzeit eines Datumswertes formatiert aus.
(CMS:DATETIME field [parameter=wert …])
Methode: Cms::cmsDatetime()
Ohne Schlüssel: der erste Wert ohne = gilt als field
Mindestens einer von: field, useval
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
id |
Datensatz-Id oder Slug |
(aktueller Datensatz) |
Datensatz, aus dem gelesen wird. Reine Ziffernfolge = record_id, alles andere = Slug - dieselbe Regel wie in der URL. | |
field ¹ |
Feld |
– | ² | Name eines Datensatzfeldes, dessen Inhalt als Quelle genutzt wird. |
useval |
Datumswert |
– | ² | Wert in Textform, der als Quelle genutzt wird. |
format |
Formatangaben |
– | ICU-Muster, z. B. "dd.MM.y HH:mm". Ohne Angabe entscheidet die Sprache der Seite über die Schreibweise. | |
autotime |
true / false |
false |
true: Wenn keine Uhrzeit vorhanden ist (00:00), wird diese nicht angezeigt. | |
timezone |
true / false |
false |
true: Der Name der Zeitzone wird angehängt (z. B. "Europe/Berlin"). | |
replace[…] |
Text?Text |
– | Kommt der Text vor dem Fragezeichen im formatierten Wert vor, wird er durch den Text nach dem Fragezeichen ersetzt (z. B. "Jan?Januar"). | |
ifempty |
Text |
– | Text, der ausgegeben wird, falls die Quelle ('field' oder 'useval') leer ist. | |
instead |
Text |
– | Text, der anstelle der Quelle ausgegeben wird, sofern diese nicht leer ist. | |
prefix |
Text |
– | Text, der vorangestellt wird, sofern der formatierte Wert nicht leer ist. | |
suffix |
Text |
– | Text, der angehängt wird, sofern der formatierte Wert nicht leer ist. | |
set |
Array-Index von $set |
– | Der formatierte Wert wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
² mindestens einer der mit ² markierten Parameter muss angegeben werden.
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Gibt Datum und Uhrzeit aus. Wie CMS:DATE, nur mit Zeitanteil:
(CMS:DATETIME changed_at) 07.03.2024, 14:30
(CMS:DATETIME changed_at format="dd.MM.y HH:mm") 07.03.2024 14:30
(CMS:DATETIME changed_at format="dd.MM.y HH:mm" autotime=true)
Mit autotime=true verschwindet die Uhrzeit, wenn im Wert keine gesetzt war
(00:00:00) - für Termine „ganztägig“.
Parameter im Einzelnen
id
id liest aus einem anderen Datensatz statt aus dem laufenden - record_id oder Slug,
genau wie in der URL:
(CMS:DATETIME changed_at id=1977)
(CMS:DATETIME changed_at id=impressum)
field
field ist der Hauptparameter und darf ohne Schlüssel stehen - beide Zeilen sind gleich:
(CMS:DATETIME field=changed_at)
(CMS:DATETIME changed_at)
useval
useval nimmt statt eines Feldes einen Wert direkt aus dem Template - meist eine
PHP-Variable oder ein zuvor mit set abgelegter Wert:
(CMS:DATETIME useval="2024-03-07 14:30:00")
(CMS:DATETIME useval="{$set['wert']}")
format
format ist ein ICU-Muster (d Tag, MM Monat zweistellig, MMMM Monatsname, y Jahr,
HH:mm Uhrzeit, EEEE Wochentag). Ohne format entscheidet die Sprache der Seite (im Backend die Locale des Benutzers):
(CMS:DATETIME online_at format="dd.MM.y HH:mm")
Ausgabe: 07.03.2024 14:30
autotime
Nur zusammen mit format: Enthält der Wert keine Uhrzeit (00:00:00), wird der
Zeitanteil aus der Ausgabe entfernt:
(CMS:DATETIME event_at format="dd.MM.y HH:mm" autotime=true)
Ausgabe bei 2024-03-07 00:00:00: 07.03.2024 - bei 2024-03-07 14:30:00: 07.03.2024 14:30
timezone
timezone=true hängt den Namen der Zeitzone an, in der der Wert angezeigt wird:
(CMS:DATETIME online_at timezone=true)
Ausgabe: 07.03.2024, 14:30 Europe/Berlin
replace
replace ersetzt Teiltexte im fertig formatierten Wert - Kurzform mit ? als
Trenner oder ein Eintrag je Suchtext:
(CMS:DATETIME changed_at replace=", ? um ")
(CMS:DATETIME changed_at replace[, ]=" um " replace[x]="y")
Ausgabe: 07.03.2024 um 14:30
ifempty
ifempty ersetzt die gesamte Ausgabe, wenn die Quelle leer ist; prefix und
suffix entfallen dann:
(CMS:DATETIME event_at prefix="<p>" suffix="</p>" ifempty="Termin folgt")
Als leer gelten auch die Platzhalter für „kein Datum“ und „kein Ende“, mit denen
camindo CMS etwa ein unbegrenztes offline_at speichert (vor dem 2. Januar 1000 bzw. ab
dem 31. Dezember 8999) – mit und ohne format.
instead
instead tauscht den Wert aus, sobald die Quelle nicht leer ist - nützlich, wenn
nur das Vorhandensein zählt:
(CMS:DATETIME event_at instead="Termin steht fest" ifempty="-")
prefix
prefix / suffix umschließen den Wert und erscheinen nur, wenn er nicht leer ist -
so entsteht kein leeres Stand::
(CMS:DATETIME changed_at prefix="Stand: " suffix="")
suffix
(CMS:DATETIME changed_at suffix=" Uhr")
Siehe prefix.
set
set gibt nichts aus, sondern legt das Ergebnis unter $set['changed'] für das
umgebende PHP oder einen späteren Befehl ab:
(CMS:DATETIME changed_at set=changed)
<?php if ($set['changed'] !== '') { ?> … <?php echo $set['changed']; ?> … <?php } ?>
CMS:EMAIL
Versendet eine E-Mail. Prüft auf Wunsch vorab die Pflichtfelder eines Formulars.
(CMS:EMAIL [parameter=wert …])
Methode: Email::cmsEmail()
Mindestens einer von: subject, subjecttemplate, body, bodytemplate
PHP-Schnipsel: include_and_stop (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
to[…] |
E-Mail-Adresse |
– | ja | Empfänger-E-Mail-Adresse. Mehrere Empfänger als to[]=… oder kommasepariert (höchstens 20). |
toname |
Text |
– | Empfängername (nur bei genau einem Empfänger). | |
from |
E-Mail-Adresse |
email.noreply bzw. email.reply |
Absender-E-Mail-Adresse. | |
fromname |
Text |
– | Absendername. | |
contenttype |
text/plain oder text/html |
text/plain |
MIME-Type der E-Mail. | |
subject |
Text |
(no subject) |
² | Betreff der E-Mail. |
subjecttemplate |
Template-Name |
– | ² | Template für den Betreff (Tags und Zeilenumbrüche fallen weg). Hat Vorrang vor subject. |
body |
Text |
– | ² | Nachrichtentext (Text oder HTML). |
bodytemplate |
Template-Name |
– | ² | Template für den E-Mail-Body. Hat Vorrang vor body. |
paths_absolute |
true / false |
false |
true: Alle serverrelativen Pfade im E-Mail-Body werden in absolute Pfade umgewandelt (nur bei contenttype="text/html"). |
|
sender_url |
(http://)Domain |
Protokoll und Hauptdomain der Instanz | (Optional mit Protokoll) + Domain, um serverrelative Pfade in absolute Pfade wandeln zu können. | |
debug |
true / false |
false |
true: E-Mail wird in das Logfile var/log/{Datum}_mail-debug.log geschrieben, aber nicht versendet. |
|
requiredfields |
Feldnamen, kommasepariert |
– | Angabe der Pflichtfelder. Feldnamen (Index-Namen) im Request-Array data[…] (siehe Parameter data). |
|
data |
PHP-Variablenname |
data |
Name des Request-Arrays, das die von requiredfields erwarteten Daten enthält (Standard data, also $_POST['data']). |
|
submit |
Textwert |
– | ja | Der Befehl läuft nur, wenn submit einen Wert enthält. Ansonsten wird der CMS:EMAIL-Befehl ignoriert. |
successtemplate |
Template-Name |
– | Template, das nach erfolgreichem Absenden eingebunden wird; danach endet die Seite (außer mit CMS:CONTINUE). |
|
message[…] |
Text |
– | Fehlermeldung. Im Index: Anwendungsfall (siehe Meldungen); im Wert: Meldungstext. | |
attachment[…] |
Pfad |
– | Dateien, die angehängt werden sollen. Pfad relativ zum var-Verzeichnis, nur aus download/, upload/ und modules/. |
|
throttle |
true / Bucket-Name |
– | Drosselung je Herkunft (IP): true = höchstens fünf E-Mails je Stunde (user.throttle.email_ip), sonst eine eigene Grenze aus user.throttle. Gedrosselt: Meldung throttled, keine E-Mail. |
|
formguard |
true / false / Sekunden |
false |
true: Formularschutz gegen Robots – das Formular muss die Felder aus (CMS:USER_TOKEN formguard=true) enthalten und frühestens nach 3 Sekunden (user.formguard_min_seconds) abgeschickt werden. Eine Zahl setzt die Mindestzeit für dieses Formular (formguard=5), 0 schaltet ab. Sonst Meldung throttled. |
² mindestens einer der mit ² markierten Parameter muss angegeben werden.
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Kontaktformular mit Pflichtfeldprüfung und Bestätigungsseite:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:EMAIL
submit="$submit"
to="info@example.com"
fromname="Kontaktformular"
subject="Kontaktanfrage"
bodytemplate=contact_mail
requiredfields="name,email,message"
successtemplate=contact_thanks
)
(CMS:MESSAGE prefix="<div class=alert>" suffix="</div>")
<form method="post">
<input name="data[name]"> <input name="data[email]" type="email">
<textarea name="data[message]"></textarea>
<button name="submit" value="1">Senden</button>
</form>
Der Ablauf:
- Beim ersten Aufruf ist
submitleer – der Befehl tut nichts, das Formular erscheint. - Nach dem Absenden prüft er die Pflichtfelder. Fehlt eines, wird nicht gesendet;
CMS:MESSAGEzeigt die Meldung, und das Template läuft weiter – das Formular erscheint erneut. - Sonst baut er die E-Mail, versendet sie und bindet
successtemplateein. Die Seite endet danach. Ohnesuccesstemplateläuft das Template einfach weiter.
Versendet wird über das in der Konfiguration eingetragene SMTP-Postfach
(email.smtp), ersatzweise über die PHP-Funktion mail() – wie alle E-Mails des CMS.
Scheitert der Versand, steht der Grund in var/log/{Datum}_mail.log (Modul
Logdateien, Art „Nicht versendete E-Mails“).
Was der Befehl hinterlässt
| Variable | Inhalt |
|---|---|
$set['email_sent'] |
true nach erfolgreichem Versand (bzw. mit debug=true nach dem Protokollieren), sonst false |
$set['email_missing'] |
Namen der leeren Pflichtfelder, z. B. um sie im Formular hervorzuheben |
<input name="data[email]" class="<?php echo in_array('email', $set['email_missing'] ?? []) ? 'error' : ''; ?>">
Meldungen
Fehler landen bei CMS:MESSAGE. Den Text bestimmt message[index],
sonst gilt der Standardtext:
| Index | Fall | Standardtext |
|---|---|---|
required |
ein Pflichtfeld ist leer | Bitte füllen Sie alle Pflichtfelder aus. |
to |
kein oder ein ungültiger Empfänger | Die E-Mail hat keinen gültigen Empfänger. |
from |
ungültiger Absender (auch: keiner konfiguriert) | Die E-Mail hat keinen gültigen Absender. |
content |
subjecttemplate/bodytemplate fehlt oder bricht ab |
Der Inhalt der E-Mail konnte nicht erzeugt werden. |
attachment |
ein Anhang fehlt oder liegt außerhalb der erlaubten Verzeichnisse | Ein Anhang der E-Mail wurde nicht gefunden. |
send |
der Versand ist gescheitert | Die E-Mail konnte nicht versendet werden. |
throttled |
Drosselung (throttle) oder Formularschutz (formguard) |
Zu viele Anfragen. Bitte versuchen Sie es später erneut. |
Die Standardtexte erscheinen in der Sprache der Seite.
Sicherheit
- Empfänger nie aus dem Formular übernehmen. Steht in
toeine Eingabe des Besuchers, kann jeder über Ihr Formular E-Mails an beliebige Adressen schicken. Ist eine der Adressen intoungültig, sendet der Befehl gar nicht. - Kontaktformulare drosseln:
throttle=truebegrenzt die E-Mails je Besucher,formguard=true(mit(CMS:USER_TOKEN formguard=true)im Formular) hält Robots auf:(CMS:EMAIL submit="$submit" to="info@example.com" … throttle=true formguard=true). Bestellbestätigungen und andere E-Mails, deren Empfänger Sie selbst bestimmen, brauchen das nicht. - Absender: Absender ist immer der Websitebetreiber, nie der Besucher: Viele
Mailserver nehmen nur E-Mails an, deren Absender zur eigenen Domain passt. Setzen Sie die Adresse des Besuchers deshalb besser in den Text als
in
from; ohnefromgilt die Absenderadresse aus der Konfiguration. - Zeilenumbrüche in Betreff, Namen und Adressen werden entfernt – eingeschleuste
Kopfzeilen (etwa ein zusätzliches
Bcc:) sind damit ausgeschlossen. - HTML-E-Mails: Eingaben des Besuchers im Body-Template mit
(CMS:TEXT useval="{$_POST['data']['…']}" specialchars=true)ausgeben. - Anhänge nur aus
var/download/,var/upload/undvar/modules/– Konfiguration, Protokolle und Sicherungen lassen sich nicht anhängen.
Parameter im Einzelnen
to
(CMS:EMAIL to="info@example.com" …)
(CMS:EMAIL to[]="info@example.com" to[]="vertrieb@example.com" …)
toname
Gilt nur, wenn genau ein Empfänger angegeben ist:
(CMS:EMAIL to="info@example.com" toname="Beispiel GmbH" …)
from
(CMS:EMAIL from="shop@example.com" …)
Ohne Angabe die Absenderadresse aus der Konfiguration (email.noreply, ersatzweise
email.reply). Ist keine davon eingetragen, kann ohne from nicht gesendet werden.
fromname
(CMS:EMAIL fromname="Kontaktformular Beispiel GmbH" …)
contenttype
(CMS:EMAIL contenttype="text/html" bodytemplate=newsletter_html …)
Bei text/html erzeugt das CMS zusätzlich eine Textfassung für E-Mail-Programme ohne
HTML-Darstellung.
subject
(CMS:EMAIL subject="Kontaktanfrage" …)
subjecttemplate
Betreff aus einem Template erzeugen (z. B. mit Feldern des laufenden Datensatzes). Tags, Entities und Zeilenumbrüche fallen weg:
(CMS:EMAIL subjecttemplate=order_subject …)
body
(CMS:EMAIL body="Neue Anfrage von {$_POST['data']['name']}" …)
bodytemplate
Nachrichtentext aus einem Template – dort stehen alle CMS-Befehle zur Verfügung, der
laufende Datensatz, $set und die Formulardaten in $_POST. Variablen, die Sie im
aufrufenden Template mit PHP gesetzt haben, sieht das Body-Template nicht; legen Sie
sie bei Bedarf in $set ab.
(CMS:EMAIL contenttype="text/html" bodytemplate=contact_mail …)
contact_mail:
<p>Neue Anfrage von (CMS:TEXT useval="{$_POST['data']['name']}" specialchars=true)
(CMS:TEXT useval="{$_POST['data']['email']}" specialchars=true):</p>
<p>(CMS:TEXT useval="{$_POST['data']['message']}" specialchars=true)</p>
specialchars=true gehört zu HTML-Mails. In einer Textmail (text/plain, Standard)
kann Besucher-Text nichts ausführen; specialchars würde dort nur Entities wie &
sichtbar machen.
Template-Namen lassen sich wie bei CMS:INCLUDE mit dem Bereich
angeben (bereich.name).
paths_absolute
Serverrelative Pfade (/media/…) in src, href, action, background, poster
und url(…) des HTML-Bodys absolut machen:
(CMS:EMAIL contenttype="text/html" bodytemplate=newsletter_html paths_absolute=true …)
sender_url
Domain für paths_absolute, wenn nicht Protokoll und Hauptdomain der Instanz gelten
sollen; ohne Protokoll gilt das der Instanz:
(CMS:EMAIL paths_absolute=true sender_url="https://www.example.com" …)
debug
E-Mail nur ins Logfile var/log/{Datum}_mail-debug.log schreiben, nicht senden – zum
Testen eines Formulars. Der Befehl verhält sich sonst wie beim echten Versand
(Pflichtfelder, successtemplate):
(CMS:EMAIL debug=true …)
requiredfields
Pflichtfelder im Request-Array data[…] (bzw. dem über data benannten Array); fehlt
eines, wird nicht gesendet und CMS:MESSAGE zeigt die Meldung. Bei Feldern mit
mehreren Werten (Checkbox-Gruppe data[themen][]) genügt ein ausgewählter Wert:
(CMS:EMAIL requiredfields="name,email,message" …)
data
Name des Request-Arrays mit den Formulardaten (Standard data, also $_POST['data']):
<input name="form[name]">
(CMS:EMAIL data=form requiredfields="name" …)
submit
Der Befehl läuft nur, wenn submit einen Wert hat – beim ersten Aufruf des Formulars also nicht:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:EMAIL submit="$submit" …)
<button name="submit" value="1">Senden</button>
successtemplate
Nach erfolgreichem Versand wird dieses Template eingebunden und die Seite beendet (ohne CMS:CONTINUE):
(CMS:EMAIL successtemplate=contact_thanks …)
message
Eigene Meldungstexte je Fehlerfall (Indizes siehe Meldungen):
(CMS:EMAIL message[required]="Bitte alle Pflichtfelder ausfüllen." …)
attachment
Dateien relativ zum var-Verzeichnis anhängen – nur aus download/, upload/ und
modules/:
(CMS:EMAIL attachment[]="download/agb.pdf" …)
throttle
(CMS:EMAIL throttle=true …)
(CMS:EMAIL throttle=newsletter …)
Ein eigener Name braucht eine Grenze in der Konfiguration (user.throttle.newsletter,
Systemadministrator); fehlt sie, gilt die von true.
formguard
(CMS:EMAIL formguard=true …)
<form method="post">(CMS:USER_TOKEN formguard=true) …</form>
CMS:MEDIA
Erzeugt ein Image-Tag oder gibt den Inhalt einer Mediendatei in Textform aus.
(CMS:MEDIA field [parameter=wert …])
Methode: Cms::cmsMedia()
Ohne Schlüssel: der erste Wert ohne = gilt als field
Mindestens einer von: field, useval
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
id |
Datensatz-Id oder Slug |
(aktueller Datensatz) |
Datensatz, aus dem gelesen wird. Reine Ziffernfolge = record_id, alles andere = Slug - dieselbe Regel wie in der URL. | |
field ¹ |
– | – | ² | Name eines Upload- oder Thumbnail-Feldes. |
subfield |
path / file / filename / mime_type / extension / size / width / height / version |
– | Einzelwert statt IMG-Tag: path (Pfad ab DOCUMENT_ROOT für src/href, mit sprechendem Dateinamen; bei auth/download/user_auth über file.php), file (absoluter Pfad im Dateisystem), filename (ursprünglicher Dateiname), mime_type, extension (ohne Punkt), size (Bytes; mit autoformat lesbar), width / height (Pixel, 0 wenn unbekannt), version (Versionszähler der Datei). Der Einzelwert wird nicht maskiert. | |
auth |
true / false |
false |
true: Der Verweis zeigt auf /file.php statt ins Medienverzeichnis. Für Felder mit "Nur für angemeldete Frontend-User" (user_auth) gilt das automatisch - file.php prüft dort die Frontend-Session. | |
download |
Text |
– | Datei wird über /file.php unter dem angegebenen Dateinamen zum Download angeboten (Dateiendung wird automatisch ergänzt). | |
width |
Text |
– | Breite des IMG-Tags, mit oder ohne CSS-Einheit (Standard: px). | |
height |
Text |
– | Höhe des IMG-Tags, mit oder ohne CSS-Einheit (Standard: px). | |
attr[…] |
Array |
– | Zusätzliche Attribute für das IMG-Tag (Schlüssel: Attributname, Wert: Attributwert). Die Werte werden automatisch mit htmlspecialchars maskiert. | |
autoformat |
true / false |
true |
true: Der via 'subfield' gewählte Inhalt wird je nach Typ formatiert (z. B. Dateigrößen in Bytes/KB/MB dynamisch nach Wert). | |
instead |
Text |
– | Überschreibt den formatierten Wert, sofern dieser nicht leer ist. | |
ifempty |
Text |
– | Alternativtext, falls der formatierte Wert leer (oder "0") ist. | |
prefix |
Text |
– | Text, der vorangestellt wird, sofern der formatierte Wert nicht leer (oder "0") ist. | |
suffix |
Text |
– | Text, der angehängt wird, sofern der formatierte Wert nicht leer (oder "0") ist. | |
useval |
Array |
– | ² | Datei-Array (etwa aus CMS:SAVEDATA) samt field_id und record_id - nur per PHP-Aufruf, ohne field (siehe Beispiel). |
set |
Array-Index von $set |
– | Der formatierte Wert wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
² mindestens einer der mit ² markierten Parameter muss angegeben werden.
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Quelle ist ein Upload-Feld. Ohne subfield entsteht ein <img>-Tag mit den
gespeicherten Maßen, alt ist der ursprüngliche Dateiname:
(CMS:MEDIA image)
Ausgabe: <img src="/media/1/12/1977,strand.jpg?v=3" alt="strand.jpg" width="1200" height="800">
Der Pfad trägt den ursprünglichen Dateinamen hinter einem Komma (sprechende URL für
Browser und Suchmaschinen, als Slug ohne Endung); die Datei selbst heißt 1977.jpg –
eine Rewrite-Regel im Medienverzeichnis wirft den Namensteil ungeprüft weg. ?v= trägt
die Versionsnummer der Datei: nach einem neuen Upload ändert sich die Adresse, der
Browser lädt die Datei neu.
Mit subfield wird ein Einzelwert der Datei ausgegeben - so lassen sich beliebige
Tags bauen oder Downloads verlinken:
<a href="(CMS:MEDIA document subfield=path)">
(CMS:MEDIA document subfield=filename) ((CMS:MEDIA document subfield=size))
</a>
Ausgabe: <a href="/media/1/13/1977,agb.pdf?v=3">agb.pdf (123.5 KB)</a>
Kein Bild hochgeladen? ifempty liefert den Ersatz - prefix/suffix entfallen dann:
(CMS:MEDIA image width=300 height="" prefix="<figure>" suffix="</figure>" ifempty="<img src='/static/placeholder.png'>")
Verfügbare subfield-Werte
| subfield | Inhalt |
|---|---|
path |
Pfad ab DOCUMENT_ROOT für src/href: /media/{instanz}/{feld}/{datensatz},{name}.{endung} - bei geschützten Feldern, auth=true und download stattdessen der /file.php-Aufruf |
file |
absoluter Pfad im Dateisystem des Servers (…/public/media/1/12/1977.jpg) - für Dateioperationen in PHP, nie für Links |
filename |
ursprünglicher Dateiname (vor dem Upload) |
mime_type |
z. B. image/jpeg |
extension |
Dateiendung ohne Punkt, z. B. jpg |
size |
Dateigröße; mit autoformat (Standard) lesbar (123.5 KB), mit autoformat=false in Bytes |
width / height |
Bildmaße in Pixeln, 0 wenn nicht ermittelbar |
version |
Versionszähler der Datei - steigt bei jedem neuen Upload in dasselbe Feld |
url ist ein Alias für path (Schreibweise aus C9-Vorabversionen). Bei mehrsprachigen
Upload-Feldern liefert der Befehl die Datei der aktuellen Sprache bzw. die der
Hauptsprache als Fallback (1977_2,strand.jpg).
Geschützte Dateien (Feldoption „nur für angemeldete Frontend-User“): Der Verweis zeigt
automatisch auf /file.php, das die Frontend-Session prüft. auth=true erzwingt diesen
Weg auch für ungeschützte Felder, download="name" bietet die Datei zum Herunterladen an.
Parameter im Einzelnen
id
id liest aus einem anderen Datensatz statt aus dem laufenden - record_id oder Slug,
genau wie in der URL:
(CMS:MEDIA image id=1977)
(CMS:MEDIA image id=impressum)
field
Name des Upload-Feldes; darf ohne Schlüssel stehen:
(CMS:MEDIA field=image)
(CMS:MEDIA image)
subfield
Einzelwert statt <img>-Tag (Liste siehe oben):
<img src="(CMS:MEDIA image subfield=path)" alt="(CMS:TEXT title specialchars=true)" loading="lazy">
Dateityp: (CMS:MEDIA document subfield=mime_type)
Einzelwerte über subfield werden nicht maskiert. path enthält nur
URL-taugliche Zeichen und kann direkt in src oder href stehen. Den ursprünglichen
Dateinamen (filename) wählt der Hochladende – in einem Attribut maskieren Sie ihn
über CMS:TEXT:
(CMS:MEDIA document subfield=filename set=dateiname)
<a href="(CMS:MEDIA document subfield=path)" title="(CMS:TEXT useval="{$set['dateiname']}" specialchars=true lbrconvert=false)">…</a>
path und file unterscheiden sich im Zweck - Verweis für den Browser gegenüber
Zugriff durch PHP:
(CMS:MEDIA document subfield=path) /media/1/13/1977,agb.pdf?v=3
(CMS:MEDIA document subfield=file) /var/www/example/public/media/1/13/1977.pdf
(CMS:MEDIA document subfield=file set=file)
<?php echo filesize($set['file']) > 5e6 ? 'große Datei' : ''; ?>
auth
Verweis über /file.php erzwingen - etwa, damit eine Vorschau denselben Zugriffsweg
nimmt wie das geschützte Original:
<p>Vorschau: (CMS:MEDIA thumb)</p>
<p>Original (nur angemeldet): (CMS:MEDIA image auth=true)</p>
Bei Feldern mit der Option „nur für angemeldete Frontend-User“ gilt das automatisch.
download
Datei zum Herunterladen anbieten; download ist der Dateiname beim Speichern, die
Endung wird ergänzt:
<a href="(CMS:MEDIA document subfield=path download="AGB-2026")">AGB herunterladen</a>
Ausgabe: <a href="/file.php?file=13/1977&v=2&download=AGB-2026">AGB herunterladen</a>
width
Breite des <img>-Tags; ohne Angabe die gespeicherte Bildbreite. Ein leerer Wert
lässt das Attribut weg, der Browser rechnet dann proportional:
(CMS:MEDIA image width=300 height="")
Ausgabe: <img src="/media/1/12/1977,strand.jpg?v=3" alt="strand.jpg" width="300">
height
(CMS:MEDIA image width="" height=200)
(CMS:MEDIA image width="100%" height="")
attr
Weitere Attribute; Schlüssel = Attributname. Vorhandene (src, alt, width,
height) lassen sich damit überschreiben:
(CMS:TEXT title striptags=true ampconvert=false lbrconvert=false set=title)
(CMS:MEDIA image attr[alt]="{$set['title']}" attr[class]="hero" attr[loading]="lazy")
CMS:MEDIA maskiert alle Attributwerte des <img>-Tags selbst mit
htmlspecialchars – auch die aus attr. Übergeben Sie deshalb unmaskierten Text,
ohne specialchars=true; sonst wird doppelt maskiert und aus & wird sichtbar
&. Aus dem Attributnamen entfernt der Befehl alle Zeichen außer Buchstaben,
Ziffern, _ und -.
autoformat
Betrifft nur subfield=size: Standardmäßig lesbar (123.5 KB), mit false in Bytes:
(CMS:MEDIA document subfield=size) 123.5 KB
(CMS:MEDIA document subfield=size autoformat=false) 126464
instead
Fester Text, wenn eine Datei vorhanden ist (das Tag bzw. der Einzelwert selbst wird nicht ausgegeben):
Bild: (CMS:MEDIA image instead="vorhanden" ifempty="fehlt")
ifempty
Ersatz, wenn keine Datei vorhanden ist (bzw. der Einzelwert leer oder 0):
(CMS:MEDIA image ifempty="<img src='/static/placeholder.png' alt=''>")
(CMS:MEDIA image subfield=width ifempty="unbekannt")
prefix
prefix / suffix umschließen den Wert und erscheinen nur, wenn er nicht leer ist -
so entsteht kein leeres <figure></figure>:
(CMS:MEDIA image prefix="<figure>" suffix="</figure>")
suffix
(CMS:MEDIA image suffix="</figure>")
Siehe prefix.
useval
Statt eines Feldes ein Datei-Array übergeben - z. B. aus einem mit CMS:SAVEDATA
gesicherten Datensatz. Parameterwerte im Template sind immer Strings; ein Array lässt
sich deshalb nur per direktem PHP-Aufruf der Methode übergeben. field darf dabei
nicht angegeben sein – es hätte Vorrang und läse das Feld des laufenden Datensatzes.
Weil das Datei-Array die Feld-Id nicht enthält, gehören field_id und record_id
ausdrücklich hinein:
(CMS:SAVEDATA author id="{$cms['author_id']}")
<?php $this->cms->cmsMedia([
'useval' => $GLOBALS['author']['portrait'] + ['record_id' => $GLOBALS['author']['record_id'], 'field_id' => 17],
'width' => 80, 'height' => '',
]); ?>
17 steht hier für die Id des Upload-Feldes portrait. Einfacher ist fast immer
(CMS:MEDIA portrait id="{$cms['author_id']}") - id lädt den fremden Datensatz
direkt und findet das Feld über seinen Namen.
set
set gibt nichts aus, sondern legt das Ergebnis unter $set['img'] für das
umgebende PHP oder einen späteren Befehl ab:
(CMS:MEDIA image subfield=path set=img)
<?php if ($set['img'] !== '') { ?> … <?php echo $set['img']; ?> … <?php } ?>
CMS:MESSAGE
Erzeugt eine Textausgabe aus aktuellen Fehlermeldungen.
(CMS:MESSAGE useval [parameter=wert …])
Methode: Cms::cmsMessage()
Ohne Schlüssel: der erste Wert ohne = gilt als useval
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
useval ¹ |
Text |
– | Wert in Textform, der als Quelle genutzt wird. | |
prefix |
Text |
– | Text, der vor den Meldungen ausgegeben wird, sofern es welche gibt. | |
suffix |
Text |
– | Text, der nach den Meldungen ausgegeben wird, sofern es welche gibt. | |
ifempty |
Text |
– | Text, falls keine Meldung vorhanden ist. | |
instead |
Text |
– | Text, der anstelle der Meldungen ausgegeben wird. | |
set |
Array-Index von $set |
– | Die Meldung wird nicht angezeigt, sondern in die Variable $set geschrieben. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Gibt die Meldungen aus, die Befehle dieser Seite abgelegt haben - typischerweise
Feldfehler von CMS:USER_INPUT, CMS:USER_LOGIN oder CMS:USER_REGISTER. Ohne Meldung
bleibt der Befehl still:
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=welcome)
<p class="error">(CMS:MESSAGE)</p>
Mehrere Meldungen werden mit <br> getrennt. Eine Umrahmung gibt es nur über
prefix/suffix – sie erscheint nur, wenn es Meldungen gibt:
(CMS:MESSAGE prefix="<div class=alert>" suffix="</div>")
Parameter im Einzelnen
useval
Eigenen Text statt der Systemmeldungen ausgeben (die Meldungen werden dann ignoriert;
ist useval leer, erscheinen wieder die Meldungen):
(CMS:MESSAGE useval="{$set['hinweis']}" prefix="<p class=hint>" suffix="</p>")
Der Text wird unverändert ausgegeben. Setzen Sie hier keine Werte aus dem Request
($_GET, $_POST) ein – dafür gibt es (CMS:TEXT useval="…" specialchars=true).
prefix
(CMS:MESSAGE prefix="<div class=alert>" suffix="</div>")
Ohne Angabe werden die Meldungen ohne Umrahmung ausgegeben.
suffix
(CMS:MESSAGE prefix="<ul class=errors><li>" suffix="</li></ul>")
ifempty
Text, wenn keine Meldung vorliegt:
(CMS:MESSAGE ifempty="Alles in Ordnung.")
instead
Fester Text, sobald eine Meldung vorliegt - wenn der Wortlaut nicht angezeigt werden soll:
(CMS:MESSAGE instead="Bitte prüfen Sie Ihre Eingaben.")
set
Meldungen für PHP bereitstellen, etwa um sie neben dem jeweiligen Feld zu zeigen:
(CMS:MESSAGE set=errors)
<?php if ($set['errors'] !== '') { ?><div class="alert"><?php echo $set['errors']; ?></div><?php } ?>
CMS:NUMBER
Gibt einen Zahlenwert formatiert aus.
(CMS:NUMBER field [parameter=wert …])
Methode: Cms::cmsNumber()
Ohne Schlüssel: der erste Wert ohne = gilt als field
Mindestens einer von: field, useval
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
id |
Datensatz-Id oder Slug |
(aktueller Datensatz) |
Datensatz, aus dem gelesen wird. Reine Ziffernfolge = record_id, alles andere = Slug - dieselbe Regel wie in der URL. | |
field ¹ |
Feld |
– | ² | Name eines Datensatzfeldes, dessen Inhalt als Quelle genutzt wird. |
useval |
Zahl |
– | ² | Wert in Textform, der als Quelle genutzt wird. |
ifempty |
Text |
– | Text, der ausgegeben wird, wenn die Quelle leer ist oder der Zahlenwert (nach mul/add) 0 ergibt. | |
instead |
Zahl |
– | Zahl, die zur Quelle wird, sofern die ursprüngliche Quelle nicht leer ist. | |
mul |
Zahl |
1 |
Zahl, mit der die Quelle multipliziert wird. | |
add |
Zahl |
0 |
Zahl, die zur Quelle addiert wird. | |
format |
Formatierung |
– | Formatierung analog zum PHP-Befehl sprintf(). | |
decimals |
Zahl 0-10 |
– | Feste Anzahl Nachkommastellen (gerundet); decimal und thousands_separator wirken weiter. Ohne Angabe so viele, wie die Zahl hat. |
|
decimal |
Zeichen |
. |
Zeichen, durch das der Dezimalpunkt ersetzt wird. | |
thousands_separator |
Zeichen |
– | Zeichen, das als Tausendertrennzeichen (alle drei Stellen von rechts nach links) eingefügt wird. | |
nullcent |
Text |
– | Text, durch den ein fehlender Nachkommawert ersetzt wird. | |
replace[…] |
Text?Text |
– | Kommt der Text vor dem Fragezeichen im formatierten Wert vor, wird er durch den Text nach dem Fragezeichen ersetzt (z. B. ".00?" entfernt leere Nachkommastellen). | |
prefix |
Text |
– | Text, der dem formatierten Wert vorangestellt wird, sofern dieser nicht "0" oder leer ist. | |
suffix |
Text |
– | Text, der dem formatierten Wert angehängt wird, sofern dieser nicht "0" oder leer ist. | |
set |
Array-Index von $set |
– | Der formatierte Wert wird nicht ausgegeben, sondern stattdessen in der Variable $set gespeichert. | |
sizefactor |
Anzahl Bytes eines Kilobytes |
– | 1000 oder 1024. Wenn > 0, wird eine lesbare Dateigröße berechnet (z. B. 15.2 GB). |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
² mindestens einer der mit ² markierten Parameter muss angegeben werden.
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Gibt einen Zahlenwert formatiert aus. Ohne weitere Parameter erscheint die Zahl so, wie sie gespeichert ist - mit Punkt als Dezimaltrenner und ohne Tausendertrennzeichen:
(CMS:NUMBER price) 1234.5
(CMS:NUMBER price decimal="," thousands_separator=".") 1.234,5
(CMS:NUMBER price decimals=2 decimal="," thousands_separator="." suffix=" €") 1.234,50 €
Rechnen vor der Ausgabe (mul, add), Dateigrößen lesbar machen (sizefactor):
(CMS:NUMBER price mul=1.19 decimals=2 decimal="," suffix=" € inkl. MwSt.")
(CMS:NUMBER filesize sizefactor=1024) 15.2 MB
Ergibt die Rechnung 0, erscheint 0 (formatiert, z. B. 0,00 mit decimals=2):
(CMS:NUMBER useval="{$a}" add=-1) 0 (bei $a = 1)
Sobald prefix, suffix oder ifempty angegeben ist, gilt der Wert 0 dagegen als
leer - eine Preisangabe mit 0 bleibt also ohne €. Maßgeblich ist der Zahlenwert nach
mul/add, nicht der formatierte Text: auch 0.00 (mit decimals oder format) gilt
dann als leer. Eine leere Quelle (Feld ohne Inhalt, leeres useval) ergibt immer eine
leere Ausgabe - auch mit mul/add.
Parameter im Einzelnen
id
id liest aus einem anderen Datensatz statt aus dem laufenden - record_id oder Slug,
genau wie in der URL:
(CMS:NUMBER price id=1977)
(CMS:NUMBER price id=impressum)
field
field ist der Hauptparameter und darf ohne Schlüssel stehen - beide Zeilen sind gleich:
(CMS:NUMBER field=price)
(CMS:NUMBER price)
useval
useval nimmt statt eines Feldes einen Wert direkt aus dem Template - meist eine
PHP-Variable oder ein zuvor mit set abgelegter Wert:
(CMS:NUMBER useval="{$this->db->num_rows_unlimited}")
(CMS:NUMBER useval="{$set['wert']}")
Komma als Dezimaltrenner wird verstanden: useval="3,5" ergibt 3.5.
ifempty
Mit ifempty gilt auch der Zahlenwert 0 als leer - egal, wie er formatiert würde:
(CMS:NUMBER stock ifempty="ausverkauft" suffix=" Stück")
Ausgabe bei stock = 0: ausverkauft - bei 12: 12 Stück
instead
instead ersetzt die Quelle (nicht das Ergebnis), wenn sie nicht leer ist -
Rechnung und Formatierung laufen danach auf dem neuen Wert:
(CMS:NUMBER price instead=99 decimal=",")
mul
(CMS:NUMBER price mul=1.19 decimals=2 decimal="," suffix=" €") Bruttopreis
(CMS:NUMBER share mul=100 suffix=" %") 0.25 → 25 %
add
(CMS:NUMBER price add=4.90 decimals=2 decimal="," suffix=" € inkl. Versand")
mul wird vor add angewendet: Ergebnis = Quelle × mul + add.
format
sprintf()-Muster; es bestimmt die Ausgabe allein, decimal, thousands_separator,
decimals und nullcent bleiben dann außen vor. Für Preise mit Dezimalkomma und
Tausenderpunkt ist deshalb decimals der richtige Parameter, format für Sonderfälle
wie führende Nullen:
(CMS:NUMBER price format="%.2f") 1234.50
(CMS:NUMBER position format="%03d") 007
decimals
Feste Anzahl Nachkommastellen, kaufmännisch gerundet - die Trennzeichen aus decimal
und thousands_separator gelten weiter:
(CMS:NUMBER price decimals=2 decimal="," thousands_separator=".")
Ausgabe bei 1234.5: 1.234,50 - bei 19.999: 20,00
Ein Betrag in Cent (Feldtyp Währung) wird mit mul=0.01 zu Euro:
(CMS:NUMBER cur_preis mul=0.01 decimals=2 decimal="," suffix=" €")
decimal
(CMS:NUMBER price decimal=",")
Ausgabe: 1234,5
thousands_separator
(CMS:NUMBER views thousands_separator=".")
(CMS:NUMBER views thousands_separator=" ")
Ausgabe: 1.234.567 bzw. 1 234 567
nullcent
Ersatz für fehlende Nachkommastellen (ohne Angabe: keiner) - für Preise, die immer zwei Stellen zeigen sollen:
(CMS:NUMBER price decimal="," nullcent="00" suffix=" €")
Ausgabe bei 1234: 1234,00 € - bei 1234.5 bleibt 1234,5 € (nur fehlende
Nachkommastellen werden ersetzt; für festes Runden decimals=2 nutzen).
replace
replace ersetzt Teiltexte im fertig formatierten Wert - Kurzform mit ? als
Trenner oder ein Eintrag je Suchtext:
(CMS:NUMBER price replace=".00?")
(CMS:NUMBER price replace[.00]="" replace[x]="y")
Ausgabe bei 12.00: 12
prefix
Erscheint nur, wenn der formatierte Wert weder leer noch 0 ist:
(CMS:NUMBER discount prefix="- " suffix=" %")
suffix
(CMS:NUMBER price suffix=" €")
Siehe prefix.
set
set gibt nichts aus, sondern legt das Ergebnis unter $set['price'] für das
umgebende PHP oder einen späteren Befehl ab:
(CMS:NUMBER price set=price)
<?php if ($set['price'] !== '') { ?> … <?php echo $set['price']; ?> … <?php } ?>
Der gespeicherte Wert ist ein String in der formatierten Schreibweise.
sizefactor
Dateigröße lesbar machen (Bytes → KB / MB / GB); 1024 für binäre, 1000 für
dezimale Einheiten:
(CMS:NUMBER filesize sizefactor=1024) 15.2 MB
(CMS:NUMBER filesize sizefactor=1000) 15.9 MB
Für Upload-Felder geht das auch direkt mit (CMS:MEDIA image subfield=size).
CMS:RAW
Gibt einen Inhalt unformatiert aus.
(CMS:RAW field [parameter=wert …])
Methode: Cms::cmsRaw()
Ohne Schlüssel: der erste Wert ohne = gilt als field
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
id |
Datensatz-Id oder Slug |
(aktueller Datensatz) |
Datensatz, aus dem gelesen wird. Reine Ziffernfolge = record_id, alles andere = Slug - dieselbe Regel wie in der URL. | |
field ¹ |
Feld |
– | ja | Name des Feldes, dessen Inhalt ausgegeben wird. |
ifempty |
Text |
– | Text, der ausgegeben wird, falls das Feld leer ist. | |
instead |
Text |
– | Text, der anstelle des Feldinhalts ausgegeben wird, sofern dieser nicht leer ist. | |
prefix |
Text |
– | Text, der vorangestellt wird, sofern der Feldinhalt nicht leer ist. | |
suffix |
Text |
– | Text, der angehängt wird, sofern der Feldinhalt nicht leer ist. | |
set |
Array-Index von $set |
– | Der Inhalt wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Gibt den Feldinhalt ohne jede Aufbereitung aus - kein Maskieren, keine Umwandlung von Zeilenumbrüchen, keine Entities. Gedacht für Felder, die fertiges HTML enthalten (Editor-Felder) oder für Werte, die unverändert in PHP/JavaScript landen sollen:
<div class="content">(CMS:RAW html_text)</div>
Dem Inhalt muss man vertrauen: Was ein Redakteur ins Feld schreibt, landet unverändert
auf der Seite – bis auf aktive Inhalte (<script>, <iframe>, Event-Handler,
javascript:-Links), die camindo CMS schon beim Speichern und beim Lesen aus der
Datenbank entfernt. Für Texte aus Benutzereingaben (ugc-Bereiche) CMS:TEXT mit
specialchars=true verwenden.
Die Ausgabeparameter ifempty, instead, prefix, suffix und set wirken wie bei
allen Ausgabebefehlen.
Parameter im Einzelnen
id
id liest aus einem anderen Datensatz statt aus dem laufenden - record_id oder Slug,
genau wie in der URL:
(CMS:RAW html_text id=1977)
(CMS:RAW html_text id=impressum)
field
field ist der Hauptparameter und darf ohne Schlüssel stehen - beide Zeilen sind gleich:
(CMS:RAW field=html_text)
(CMS:RAW html_text)
ifempty
ifempty ersetzt die gesamte Ausgabe, wenn die Quelle leer ist; prefix und
suffix entfallen dann:
(CMS:RAW html_text prefix="<p>" suffix="</p>" ifempty="<p>Kein Inhalt.</p>")
instead
instead tauscht den Wert aus, sobald die Quelle nicht leer ist - nützlich, wenn
nur das Vorhandensein zählt:
(CMS:RAW html_text instead="vorhanden" ifempty="-")
prefix
prefix / suffix umschließen den Wert und erscheinen nur, wenn er nicht leer ist -
so entsteht kein leeres <section></section>:
(CMS:RAW html_text prefix="<section>" suffix="</section>")
suffix
(CMS:RAW html_text suffix="</section>")
Siehe prefix.
set
set gibt nichts aus, sondern legt das Ergebnis unter $set['html'] für das
umgebende PHP oder einen späteren Befehl ab:
(CMS:RAW html_text set=html)
<?php if ($set['html'] !== '') { ?> … <?php echo $set['html']; ?> … <?php } ?>
CMS:SNIPPET
Gibt ein Snippet (Textbaustein) in der Sprache des Aufrufs aus.
(CMS:SNIPPET key [parameter=wert …])
Methode: Cms::cmsSnippet()
Ohne Schlüssel: der erste Wert ohne = gilt als key
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
key ¹ |
Snippet-Schlüssel |
– | ja | Schlüssel des Snippets, z. B. custom.weiterlesen (Snippet-Set der Instanz) oder ein Core-Snippet ohne Präfix. |
param[…] |
Text |
– | Werte für Platzhalter: param[name]=Wert ersetzt {name} im Snippet und füllt ICU-Parameter (plural, select). |
|
specialchars |
true / false |
false |
true: Sonderzeichen werden in ihr HTML-Äquivalent umgewandelt (für Attribute). | |
ifempty |
Text |
– | Text, der ausgegeben wird, wenn es das Snippet nicht gibt (ohne ifempty erscheint der Schlüssel). |
|
prefix |
Text |
– | Text, der vorangestellt wird, sofern das Snippet nicht leer ist. | |
suffix |
Text |
– | Text, der angehängt wird, sofern das Snippet nicht leer ist. | |
set |
Array-Index von $set |
– | Der Text wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Feste Beschriftungen einer Website – „Weiterlesen“, „In den Warenkorb“, Überschriften
von Listen – pflegt die Redaktion als Snippets der Instanz in allen Sprachen
(Modul Snippets, siehe Handbuch für Administratoren).
Das Template gibt sie mit CMS:SNIPPET aus, immer in der Sprache des Aufrufs; fehlt eine
Übersetzung, erscheint der englische Text (en_US):
<a class="more" href="(CMS:URL)">(CMS:SNIPPET custom.weiterlesen)</a>
custom. meint das Snippet-Set der Instanz. Ohne Präfix sind die Snippets des Cores
gemeint – im Frontend nur die allgemeinen, nicht die der Backend-Oberfläche.
In einem Attribut maskieren Sie den Text mit specialchars=true:
<button aria-label="(CMS:SNIPPET custom.menue specialchars=true)">…</button>
Platzhalter. param[name] ersetzt {name} im Snippet. So bleibt die Wortstellung
Sache der Übersetzung – etwa das Währungszeichen vor oder hinter dem Betrag:
Snippet custom.preis: en "€{betrag}" de "{betrag} €"
(CMS:NUMBER preis mul=0.01 format="%.2f" set=betrag)
(CMS:SNIPPET custom.preis param[betrag]="{$set['betrag']}")
Beginnt ein Snippet mit einer ICU-Nachricht ({anzahl, plural, one {# Artikel} other {# Artikel}}),
füllt param deren Werte:
(CMS:SNIPPET custom.treffer param[anzahl]="{$set['count']}")
Schlüssel aus dem Datensatz. Der Schlüssel darf PHP-Variablen enthalten – etwa, um einen Auswahlwert zu übersetzen:
(CMS:SNIPPET "custom.status_{$cms['status']}" ifempty="–")
Gibt es das Snippet nicht, steht der Schlüssel selbst auf der Seite – so fällt ein
fehlender Text beim Entwickeln auf. ifempty gibt stattdessen einen Ersatz aus.
Innerhalb eines Parameters eines anderen Befehls lässt sich CMS:SNIPPET nicht
verschachteln; dort liefert {$this->cms->snippet('custom.weiterlesen')} den Text:
(CMS:TEXT teaser suffix=" {$this->cms->snippet('custom.weiterlesen')}")
CMS:TEXT
Gibt einen Text formatiert aus.
(CMS:TEXT field [parameter=wert …])
Methode: Cms::cmsText()
Ohne Schlüssel: der erste Wert ohne = gilt als field
Mindestens einer von: field, useval, id
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
id |
Datensatz-Id oder Slug |
– | ² | Datensatz, aus dem gelesen wird. Reine Ziffernfolge = record_id, alles andere = Slug - dieselbe Regel wie in der URL. Ohne 'field' wird das Feld "text" gelesen. |
field ¹ |
Feld |
– | ² | Name eines Datensatzfeldes, dessen Inhalt als Quelle genutzt wird. |
useval |
Text |
– | ² | Wert in Textform, der als Quelle genutzt wird. |
ifempty |
Text |
– | Text, der zur Quelle wird, wenn die ursprüngliche Quelle leer ist (weitere Parameter, außer 'set', werden dann ignoriert). | |
instead |
Text |
– | Text, der zur Quelle wird, wenn die ursprüngliche Quelle nicht leer ist. | |
entitydecode |
true / false |
true |
true: Entities im Format &...; werden zurück in Unicode-Zeichen gewandelt. | |
trim |
true / false |
false |
true: Leerzeichen am Textanfang und -ende werden entfernt. | |
striptags |
true / false |
false |
true: HTML- und Script-Tags werden entfernt. | |
if[…] |
Text?Text |
– | Wenn die Quelle dem Text vor dem Fragezeichen entspricht, wird der Text nach dem Fragezeichen zur neuen Quelle. | |
else |
Text |
– | Trifft keine der vorherigen IF-Abfragen zu, wird dieser Wert zur Quelle. | |
replace[…] |
Text?Text |
– | Kommt der Text vor dem Fragezeichen in der Quelle vor, wird er durch den Text nach dem Fragezeichen ersetzt. | |
limit |
Zahl >=0 |
– | Kürzt den Text auf die angegebene Anzahl an Zeichen und hängt "…" an; mit angehängtem s (z. B. 200s) wird am letzten Wortende gekürzt. | |
lbrconvert |
true / false |
true |
true: Zeilenumbrüche werden in HTML-Zeilenumbrüche (<br>-Tags) umgewandelt. | |
specialchars |
true / false |
false |
true: Sonderzeichen werden in ihr HTML-Äquivalent umgewandelt. | |
urlconvert |
true / false |
false |
true: Internet-Adressen werden automatisch in klickbare Links umgewandelt. | |
emailconvert |
true / false / encrypt |
false |
true: E-Mail-Adressen werden verlinkt. encrypt: Die Links werden mittels JavaScript leicht verschlüsselt. | |
emaillimit |
>= 0 |
– | Kürzt automatisch verlinkte E-Mail-Adressen auf x sichtbare Zeichen. | |
emailtext |
Text |
– | Verlinkter Text, der anstelle einer E-Mail-Adresse angezeigt wird (nur bei aktiviertem "emailconvert"). | |
target |
Fenster / Frame |
_self |
Setzt das Link-Target für automatisch erzeugte Links (urlconvert, emailconvert). | |
markdown |
true / false |
false |
true: Der Text ist Markdown und wird in HTML umgewandelt (HTML im Text wird maskiert); die übrigen Umwandlungen entfallen. | |
urlencode |
true / false |
false |
true: Der Text wird nach RFC 3986 kodiert (rawurlencode) – für URL-Bestandteile, auch in Attributen ohne weitere Maskierung. | |
urllimit |
Anzahl Zeichen |
80 |
Maximale Länge des sichtbaren Textes für automatisch erzeugte Links (nur für externe Links geeignet!). | |
ampconvert |
true / false |
true |
true: Ein alleinstehendes & wird automatisch in das HTML-Entity & umgewandelt. | |
prefix |
Text |
– | Text, der der Quelle vorangestellt wird, sofern diese nicht leer ist. | |
suffix |
Text |
– | Text, der an die Quelle angehängt wird, sofern diese nicht leer ist. | |
iso |
true / false |
false |
true: Die Ausgabe wird in den Zeichensatz ISO-8859-1 konvertiert. | |
charset |
Text |
utf-8 |
Zeichensatz, in den die Ausgabe konvertiert werden soll. | |
set |
Array-Index von $set |
– | Der formatierte Wert wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
² mindestens einer der mit ² markierten Parameter muss angegeben werden.
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Der Standardbefehl für Feldinhalte. Er liest ein Feld des laufenden Datensatzes,
wandelt Zeilenumbrüche in <br>, maskiert alleinstehende & und gibt das Ergebnis aus:
<h1>(CMS:TEXT title)</h1>
<p>(CMS:TEXT text)</p>
Ein Feld nur ausgeben, wenn es gefüllt ist - prefix/suffix erscheinen nur bei
nicht leerem Wert:
(CMS:TEXT teaser prefix="<p class=teaser>" suffix="</p>")
Wert für PHP zwischenspeichern statt ausgeben:
(CMS:TEXT title set=title)
<title><?php echo $set['title']; ?> – Beispiel GmbH</title>
Statuswert in Text übersetzen:
(CMS:TEXT status_id if[0]="online" if[1]="gesperrt" if[2]="unbestätigt" else="unbekannt")
Reihenfolge der Verarbeitung: Quelle → if/else → replace → trim → striptags
→ limit → (markdown bzw. urlencode beenden hier) → entitydecode → specialchars bzw.
ampconvert → urlconvert / emailconvert → lbrconvert → iso/charset →
ifempty / instead / prefix / suffix → Ausgabe oder set.
Parameter im Einzelnen
id
id liest aus einem anderen Datensatz - record_id oder Slug, wie in der URL. Nur bei
CMS:TEXT darf id allein stehen: dann wird das Feld text gelesen. Wie in der URL
muss der Datensatz veröffentlicht sein (freigegeben, Veröffentlichungsdatum
erreicht; ein abgelaufener bleibt lesbar), sonst ist der Wert leer – auch in der
Vorschau des Backends.
(CMS:TEXT title id=1977) Titel des Datensatzes 1977
(CMS:TEXT id=impressum) Feld "text" des Datensatzes mit dem Slug "impressum"
field
field ist der Hauptparameter und darf ohne Schlüssel stehen - beide Zeilen sind gleich:
(CMS:TEXT field=title)
(CMS:TEXT title)
useval
useval nimmt statt eines Feldes einen Wert direkt aus dem Template - meist eine
PHP-Variable oder ein zuvor mit set abgelegter Wert:
(CMS:TEXT useval="{$_GET['q']}" specialchars=true)
(CMS:TEXT useval="{$set['wert']}")
Werte aus dem Request ($_GET, $_POST) immer mit specialchars=true ausgeben – sonst
kann ein präparierter Link beliebiges HTML und Script in die Seite setzen.
Damit lassen sich die Text-Filter (striptags, specialchars, limit …) auch auf
fremde Werte anwenden.
ifempty
ifempty ersetzt die gesamte Ausgabe, wenn die Quelle leer ist; prefix und
suffix entfallen dann:
(CMS:TEXT teaser prefix="<p>" suffix="</p>" ifempty="Kein Teaser vorhanden.")
Achtung: Ist die Quelle leer, werden auch alle Umwandlungen übersprungen - der
ifempty-Text wird unverändert ausgegeben.
instead
instead tauscht den Wert aus, sobald die Quelle nicht leer ist - nützlich, wenn
nur das Vorhandensein zählt:
(CMS:TEXT image instead="✔ Bild vorhanden" ifempty="-")
entitydecode
Standardmäßig werden Entities (&, ä, –) zurück in Zeichen gewandelt.
Für Inhalte, die die Entities behalten sollen (z. B. für XML-Feeds):
(CMS:TEXT text entitydecode=false)
trim
(CMS:TEXT title trim=true prefix="[" suffix="]")
Ausgabe: [Hallo Welt] statt [ Hallo Welt ]
striptags
HTML- und Script-Tags entfernen, etwa für Teaser aus einem HTML-Feld oder für
Meta-Tags. Jedes Tag wird durch ein Leerzeichen ersetzt (aus <p>a</p><p>b</p> wird
a b), der Text zwischen den Tags bleibt stehen:
<meta name="description" content="(CMS:TEXT text striptags=true limit=160 lbrconvert=false)">
if
Wertetabelle: Trifft der Wert einen Schlüssel, wird der Eintrag zur neuen Quelle. Zwei Schreibweisen - Unterschlüssel oder Kurzform mit Fragezeichen:
(CMS:TEXT status_id if[0]="online" if[1]="gesperrt")
(CMS:TEXT status_id if="0?online" if[]="1?gesperrt")
Nicht getroffene Werte bleiben unverändert - außer else ist angegeben.
Enthält der Ersatztext eines Ziffern-Schlüssels selbst ein Fragezeichen, nehmen Sie
die Kurzform: if[]="1?Wirklich?". Bei if[1]="Wirklich?" wäre nicht zu erkennen, ob
1 der Suchwert ist oder die Kurzform Wirklich → `gemeint war (in PHP sind beide Schreibweisen der Schlüssel1). Für Text-Schlüssel (if[aktiv]="Ja?"`) gilt das nicht.
else
(CMS:TEXT status_id if[0]="online" else="nicht online")
Ausgabe bei status_id = 2: nicht online
replace
Teiltexte in der Quelle ersetzen - Kurzform mit ? oder ein Eintrag je Suchtext:
(CMS:TEXT text replace="\n?<br><br>" lbrconvert=false)
(CMS:TEXT title replace[GmbH]="Gesellschaft mbH" replace[&]="und")
Wie bei if gilt: Ersatztexte mit Fragezeichen für einen Ziffern-Suchtext in der
Kurzform schreiben (replace[]="0?null?").
limit
Text auf eine Zeichenzahl kürzen; ein angehängtes s kürzt am letzten Wortende
(„soft“). Angehängt wird das Snippet ellipsis (…):
<p>(CMS:TEXT text striptags=true limit=200s)</p>
<td>(CMS:TEXT title limit=30)</td>
lbrconvert
Standardmäßig werden Zeilenumbrüche zu <br>. Für Inhalte in <pre>, in Attributen
oder in JavaScript abschalten:
<pre>(CMS:TEXT code lbrconvert=false)</pre>
specialchars
Alle Sonderzeichen maskieren (< → <, " → " …) - für Attribute und
für Inhalte, die kein HTML enthalten dürfen:
<input value="(CMS:TEXT title specialchars=true lbrconvert=false)">
urlconvert
Adressen (http://…, https://…, www.…) im Text klickbar machen:
(CMS:TEXT text urlconvert=true target=_blank urllimit=24)
Ausgabe: <a href="https://example.com/sehr/lange/adresse" target="_blank">https://example.com/sehr…</a>
emailconvert
E-Mail-Adressen verlinken; encrypt setzt den Link per JavaScript zusammen, sodass
die Adresse nicht im Quelltext steht. Das <script> dafür trägt die Nonce der Seite und
läuft deshalb auch unter einer durchgesetzten Script-Policy (CMS:CSP):
(CMS:TEXT contact emailconvert=true)
(CMS:TEXT contact emailconvert=encrypt emailtext="E-Mail schreiben")
emaillimit
Sichtbaren Teil einer verlinkten Adresse kürzen:
(CMS:TEXT contact emailconvert=true emaillimit=12)
Ausgabe: <a href="mailto:redaktion@example.com">redaktion@ex…</a>
emailtext
Fester Linktext statt der Adresse (nur mit emailconvert):
(CMS:TEXT contact emailconvert=true emailtext="Kontakt aufnehmen")
target
Link-Target für die von urlconvert erzeugten Links:
(CMS:TEXT text urlconvert=true target=_blank)
markdown
Gibt ein Feld aus, das Markdown enthält – etwa eines mit der Extension Markdown (visuell) – und wandelt es in HTML um:
<div class="text">(CMS:TEXT text markdown=true)</div>
Umgewandelt wird mit Parsedown; HTML im Text wird maskiert, Links auf javascript: und
ähnliche Ziele werden entschärft. Zusätzlich zum üblichen Markdown versteht der
Parameter die Formate der Extension Markdown (visuell):
| Markdown | Ergebnis |
|---|---|
++Text++ |
unterstrichen (<u>) |
::: center … ::: |
die eingeschlossenen Absätze zentriert (left, center, right; als <div style="text-align: …">) |
<br> |
Zeilenumbruch, auch in Tabellenzellen |
­, … |
bleiben Sonderzeichen |
markdown beendet die Verarbeitung: entitydecode, specialchars, lbrconvert,
urlconvert usw. entfallen. prefix, suffix, ifempty und set wirken wie
gewohnt.
urlencode
Den Wert als URL-Bestandteil kodieren. Alle weiteren Umwandlungen entfallen dann:
<a href="/suche?q=(CMS:TEXT title urlencode=true)">ähnliche Artikel</a>
Ausgabe: /suche?q=Hallo%20Welt
urllimit
Sichtbare Länge automatisch verlinkter Adressen (Standard 80 Zeichen, 0 = ungekürzt):
(CMS:TEXT text urlconvert=true urllimit=30)
ampconvert
Standardmäßig wird ein alleinstehendes & zu &, vorhandene Entities bleiben.
Abschalten, wenn der Inhalt bereits vollständig maskiert ist:
(CMS:TEXT html_snippet ampconvert=false)
prefix
prefix / suffix umschließen den Wert und erscheinen nur, wenn er nicht leer ist -
so entsteht kein leeres <p class=teaser></p>:
(CMS:TEXT teaser prefix="<p class=teaser>" suffix="</p>")
suffix
(CMS:TEXT author_name suffix=" (Autor)")
Siehe prefix.
iso
Ausgabe nach ISO-8859-1 umkodieren (Kurzform für charset=ISO-8859-1), etwa für
alte Schnittstellen:
(CMS:TEXT title iso=true)
charset
(CMS:TEXT title charset=windows-1252)
set
set gibt nichts aus, sondern legt das Ergebnis unter $set['title'] für das
umgebende PHP oder einen späteren Befehl ab:
(CMS:TEXT title set=title)
<?php if ($set['title'] !== '') { ?> … <?php echo $set['title']; ?> … <?php } ?>
CMS:TIME
Gibt die Uhrzeit eines Datums- oder Zeitwertes formatiert aus.
(CMS:TIME field [parameter=wert …])
Methode: Cms::cmsTime()
Ohne Schlüssel: der erste Wert ohne = gilt als field
Mindestens einer von: field, useval
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
id |
Datensatz-Id oder Slug |
(aktueller Datensatz) |
Datensatz, aus dem gelesen wird. Reine Ziffernfolge = record_id, alles andere = Slug - dieselbe Regel wie in der URL. | |
field ¹ |
Feld |
– | ² | Name eines Datensatzfeldes, dessen Inhalt als Quelle genutzt wird. |
useval |
Zeitwert |
– | ² | Wert in Textform, der als Quelle genutzt wird. |
format |
Formatangaben |
– | ICU-Muster, z. B. "HH:mm". Ohne Angabe entscheidet die Sprache der Seite über die Schreibweise; volle Minuten werden ohne Sekunden gezeigt. | |
timezone |
true / false |
false |
true: Der Name der Zeitzone wird angehängt (z. B. "Europe/Berlin"). | |
replace[…] |
Text?Text |
– | Kommt der Text vor dem Fragezeichen im formatierten Wert vor, wird er durch den Text nach dem Fragezeichen ersetzt (z. B. "Jan?Januar"). | |
ifempty |
Text |
– | Text, der ausgegeben wird, falls die Quelle ('field' oder 'useval') leer ist. | |
instead |
Text |
– | Text, der anstelle der Quelle ausgegeben wird, sofern diese nicht leer ist. | |
prefix |
Text |
– | Text, der vorangestellt wird, sofern der formatierte Wert nicht leer ist. | |
suffix |
Text |
– | Text, der angehängt wird, sofern der formatierte Wert nicht leer ist. | |
set |
Array-Index von $set |
– | Der formatierte Wert wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
² mindestens einer der mit ² markierten Parameter muss angegeben werden.
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Gibt nur die Uhrzeit eines Datums-/Zeitwertes aus:
(CMS:TIME event_at) 14:30
(CMS:TIME event_at format="HH:mm:ss") 14:30:00
(CMS:TIME event_at suffix=" Uhr") 14:30 Uhr
Ohne format werden volle Minuten ohne Sekunden gezeigt.
Parameter im Einzelnen
id
id liest aus einem anderen Datensatz statt aus dem laufenden - record_id oder Slug,
genau wie in der URL:
(CMS:TIME event_at id=1977)
(CMS:TIME event_at id=impressum)
field
field ist der Hauptparameter und darf ohne Schlüssel stehen - beide Zeilen sind gleich:
(CMS:TIME field=event_at)
(CMS:TIME event_at)
useval
useval nimmt statt eines Feldes einen Wert direkt aus dem Template - meist eine
PHP-Variable oder ein zuvor mit set abgelegter Wert:
(CMS:TIME useval="2024-03-07 14:30:00")
(CMS:TIME useval="{$set['wert']}")
format
format ist ein ICU-Muster (d Tag, MM Monat zweistellig, MMMM Monatsname, y Jahr,
HH:mm Uhrzeit, EEEE Wochentag). Ohne format entscheidet die Sprache der Seite (im Backend die Locale des Benutzers):
(CMS:TIME online_at format="HH:mm")
Ausgabe: 14:30
h:mm a ergibt die 12-Stunden-Schreibweise (2:30 PM).
timezone
(CMS:TIME event_at timezone=true)
Ausgabe: 14:30 Europe/Berlin
replace
replace ersetzt Teiltexte im fertig formatierten Wert - Kurzform mit ? als
Trenner oder ein Eintrag je Suchtext:
(CMS:TIME event_at replace=":?.")
(CMS:TIME event_at replace[:]="." replace[x]="y")
Ausgabe: 14.30
ifempty
ifempty ersetzt die gesamte Ausgabe, wenn die Quelle leer ist; prefix und
suffix entfallen dann:
(CMS:TIME event_at prefix="<p>" suffix="</p>" ifempty="ganztägig")
instead
instead tauscht den Wert aus, sobald die Quelle nicht leer ist - nützlich, wenn
nur das Vorhandensein zählt:
(CMS:TIME event_at instead="mit Uhrzeit" ifempty="-")
prefix
prefix / suffix umschließen den Wert und erscheinen nur, wenn er nicht leer ist -
so entsteht kein leeres ab Uhr:
(CMS:TIME event_at prefix="ab " suffix=" Uhr")
suffix
(CMS:TIME event_at suffix=" Uhr")
Siehe prefix.
set
set gibt nichts aus, sondern legt das Ergebnis unter $set['time'] für das
umgebende PHP oder einen späteren Befehl ab:
(CMS:TIME event_at set=time)
<?php if ($set['time'] !== '') { ?> … <?php echo $set['time']; ?> … <?php } ?>
CMS:URL
Erzeugt die URL eines Datensatzes.
(CMS:URL id [parameter=wert …])
Methode: Cms::cmsUrl()
Ohne Schlüssel: der erste Wert ohne = gilt als id
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
id ¹ |
Datensatz-Id oder Slug |
(aktueller Datensatz) |
Datensatz, auf den verwiesen werden soll. Reine Ziffernfolge = record_id, alles andere = Slug - dieselbe Regel wie in der URL. | |
area |
Bereichsname oder -Id |
– | Verweis auf die Übersicht des Bereichs: die Adresse aus seinem Slug-Präfix (/de/termine), die sein Übersichts-Template zeigt. |
|
template |
Templatename |
– | Template, mit dem der Datensatz gezeigt werden soll (zweiter URL-Teil nach dem Komma): Name oder bereich.name. Ohne Angabe das Template, das der Bereich zuweist. | |
lang |
Sprachkürzel |
(aktuelle Sprache) |
Sprachversion, auf die verwiesen werden soll (standardmäßig die des laufenden Aufrufs). Führt die Instanz mehr als eine Sprache, steht das Kürzel immer in der URL; bei nur einer Sprache nie. Ein mehrsprachiger Slug wird in der Zielsprache eingesetzt. Ein Kürzel, das die Website nicht führt, wird übergangen. | |
protocol |
Übertragungsprotokoll |
(Protokoll der Instanz) |
http oder https, nur mit absolute bzw. host. Ohne Angabe das Protokoll der Instanz, sonst das des Aufrufs. | |
host |
Hostname |
– | z. B. example.com für den Wechsel der Domain, über die die Website aufgerufen wird. | |
absolute |
true / false |
false |
true: Der Pfad wird inkl. Protokoll und Domain erzeugt. false: Der Pfad ist serverrelativ und beginnt mit einem Slash. | |
set |
Array-Index von $set |
– | Die URL wird nicht ausgegeben, sondern in $set eingetragen. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Erzeugt die Frontend-URL eines Datensatzes - mit Slug, wenn er einen hat, sonst mit der record_id; bei mehrsprachigen Auftritten immer mit Sprachsegment:
<a href="(CMS:URL)">(CMS:TEXT title)</a>
Ausgabe: <a href="/de/hallo-welt">Hallo Welt</a>
Auf eine andere Ansicht desselben Datensatzes verweisen (template), auf einen anderen
Datensatz (id) oder in eine andere Sprache (lang):
(CMS:URL template=print) /de/hallo-welt,print
(CMS:URL 1977) /de/1977 (Datensatz ohne Slug)
(CMS:URL impressum lang=en) /en/legal-notice (Slug der Zielsprache)
(CMS:URL farben/blau) /de/farben/blau (Bereich mit Slug-Prefix)
Führt der Inhaltsbereich des Datensatzes einen Slug-Prefix (Einstellung des
Bereichs, z. B. farben), steht er vor dem Slug: /de/farben/blau. Der Präfix gilt
je Sprache – mit lang also der Präfix der Zielsprache. Ein id mit Slug meint den
Pfad, wie er in der Adresse steht (id=farben/blau); id=blau findet nur einen
Datensatz eines Bereichs ohne Präfix.
Sprachumschalter - in einem CMS:SELECT_LANGVERSION-Listentemplate. Die Zeilen dort
sind Sprachen, keine Datensätze; CMS:URL ohne id verweist deshalb auf den
Datensatz der Seite:
<a href="(CMS:URL lang="{$cms['code']}")">(CMS:TEXT endonym)</a>
Absolute URL für Feeds, E-Mails oder og:url:
<link rel="canonical" href="(CMS:URL absolute=true)">
Parameter im Einzelnen
id
Datensatz, auf den verwiesen wird; ohne Angabe der laufende. Darf ohne Schlüssel stehen:
(CMS:URL 1977)
(CMS:URL id=impressum)
(CMS:URL "{$cms['parent_id']}") Verweis auf den Elternknoten
area
Verweist auf die Übersicht eines Bereichs: die Adresse, die nur aus seinem
Slug-Präfix besteht, in der Sprache des Aufrufs bzw. aus lang. Sie zeigt das
Übersichts-Template des Bereichs (Einstellung des Bereichs, „Template-Zuweisung“):
<a href="(CMS:URL area=termine)">Alle Termine</a> /de/termine
(CMS:URL area=termine lang=en) /en/events
Hat der Bereich keinen Slug-Präfix, bleibt die Ausgabe leer. Im Übersichts-Template
selbst gibt es keinen laufenden Datensatz; CMS:URL ohne id verweist dort auf
dieselbe Übersicht – ein Sprachumschalter mit CMS:SELECT_LANGVERSION funktioniert
deshalb wie auf jeder Datensatzseite.
template
Zweiter URL-Teil (nach dem Komma): das Template, mit dem der Datensatz gezeigt werden
soll - Name oder bereich.name:
(CMS:URL template=print)
(CMS:URL template=artikeltemplates.print)
Ausgabe: /de/hallo-welt,print
lang
Sprachversion, auf die der Link zeigt (Kürzel aus der Sprachverwaltung); ohne Angabe die Sprache des laufenden Aufrufs:
(CMS:URL lang=en)
Führt der Auftritt nur eine Sprache, steht nie ein Sprachsegment in der URL. Ein Kürzel, das die Website nicht führt, wird übergangen – die URL behält die Sprache des Aufrufs.
Ist das Feld slug des Bereichs mehrsprachig, hat jede Sprachversion ihren eigenen
Slug - und die Ziel-URL muss den Slug der Zielsprache tragen, denn aufgelöst wird er in
der Sprache der URL. CMS:URL setzt ihn deshalb bei lang selbst ein:
Seite /de/esstisch-nord-180
(CMS:URL lang=en) /en/dining-table-nord-180
(CMS:URL lang=sv) /sv/matbord-nord-180
Ist der Slug in der Zielsprache nicht übersetzt, steht dort der Slug der Hauptsprache.
Führt die Instanz des Datensatzes die Zielsprache nicht, gilt ihre Hauptsprache. Findet
sich kein Slug, steht die record_id in der URL (bei canonical = 1 leitet die Seite
von dort auf den Slug um). Je Datensatz und Sprache kostet das eine Datenbankabfrage,
die für den Aufruf gemerkt wird; ohne lang oder bei einem einsprachigen Slug-Feld
keine.
protocol
Nur zusammen mit absolute bzw. host:
(CMS:URL absolute=true protocol=https)
Ausgabe: https://www.example.com/de/hallo-welt
host
Andere Domain; impliziert absolute=true:
(CMS:URL host=shop.example.com)
Ausgabe: https://shop.example.com/de/hallo-welt (Protokoll der Instanz)
absolute
Vollständige URL mit Protokoll und Haupt-Domain der Instanz:
<link rel="canonical" href="(CMS:URL absolute=true)">
Ausgabe: <link rel="canonical" href="https://www.example.com/de/hallo-welt">
set
set gibt nichts aus, sondern legt das Ergebnis unter $set['url'] für das
umgebende PHP oder einen späteren Befehl ab:
(CMS:URL set=url)
<?php if ($set['url'] !== '') { ?> … <?php echo $set['url']; ?> … <?php } ?>
CMS:XML
Erzeugt eine XML-Deklaration.
(CMS:XML encoding [parameter=wert …])
Methode: Cms::cmsXml()
Ohne Schlüssel: der erste Wert ohne = gilt als encoding
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
encoding ¹ |
encoding |
utf-8 |
Zeichenkodierung, die verwendet werden soll. | |
version |
version |
1.0 |
XML-Version. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
encoding und version werden mit htmlspecialchars maskiert ausgegeben.
Anwendung
Erzeugt die XML-Deklaration <?xml version="1.0" encoding="utf-8"?>. Sie muss das
erste Zeichen der Ausgabe sein - der Befehl steht deshalb in der ersten Zeile eines
Feed-Templates, ohne Leerzeile davor:
(CMS:XML)
<rss version="2.0">
<channel>
<title>(CMS:TEXT title specialchars=true lbrconvert=false)</title>
(CMS:SELECT filter[area]=artikel limit=20 resulttemplate=rss_item)
</channel>
</rss>
Dazu passend (CMS:HEADER "Content-Type: application/rss+xml; charset=utf-8") – er
gibt selbst nichts aus und steht deshalb vor (CMS:XML) in derselben Zeile bzw.
ohne Leerzeile dazwischen:
(CMS:HEADER "Content-Type: application/rss+xml; charset=utf-8")(CMS:XML)
<rss version="2.0">
Die Deklaration in ein Template zu tippen ginge nicht: <?xml würde PHP als
öffnendes Tag lesen.
Parameter im Einzelnen
encoding
(CMS:XML encoding=ISO-8859-1)
Ausgabe: <?xml version="1.0" encoding="ISO-8859-1"?> - die Inhalte dann mit charset=ISO-8859-1 ausgeben.
version
(CMS:XML version=1.1)
CMS:ZIP
Bietet die Dateien mehrerer Datensätze als ZIP-Archiv zum Download an und beendet die Seite.
(CMS:ZIP field=feld[,feld…] filter[…]=wert | relation=feld | ids=… | source=basket [parameter=wert …])
Methode: CmsZip::cmsZip() (nachgeladener Dienst, siehe unten)
Ohne Schlüssel: der erste Wert ohne = gilt als field
Pflicht: field und eine Auswahl: filter, relation, ids oder source
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
field ¹ |
Feldname(n), kommasepariert |
– | ja | Upload- oder Thumbnail-Feld(er). Je Datensatz gilt das erste, das der Besucher abrufen darf. |
filter[…] |
Wert |
– | ² | Auswahl der Datensätze wie bei CMS:SELECT. |
filter_op[…] |
Operator |
= |
Vergleichsoperator wie bei CMS:SELECT. | |
filter_mode[…] |
AND / OR |
OR |
Verknüpfung mehrerer Werte wie bei CMS:SELECT. | |
relation |
Feldname |
– | ² | Die verknüpften Datensätze eines Datensatzes. |
src_id |
Datensatz-Id |
aktueller Datensatz | Quell-Datensatz für relation. Leer oder 0: keine Dateien. |
|
ids |
Datensatz-Ids, kommasepariert |
– | ² | Ausdrücklich genannte Datensätze. |
source |
basket |
– | ² | Die Datensätze im Warenkorb der Session – etwa ein Leuchtkasten. |
category |
Zahl |
0 |
Warenkorb-Kategorie für source=basket. |
|
sort[…] |
Feldname |
– | Reihenfolge wie bei CMS:SELECT; ohne Angabe die von ids bzw. des Warenkorbs. |
|
order[…] |
asc / desc |
asc |
Sortierrichtung. | |
name |
Text |
download |
Name des Archivs ohne .zip. |
|
filenames |
Muster |
{slug}.{extension} |
Dateinamen im Archiv (siehe unten). | |
credits |
Feldnamen, kommasepariert |
– | Legt eine CREDITS.txt bei: je Datei ihr Name und die Inhalte dieser Felder. |
|
maxfiles |
Zahl |
Einstellung | Höchstzahl der Dateien – nur kleiner als die Einstellung user.zip.max_files. |
|
maxsize |
MB |
Einstellung | Höchstgröße in MB – nur kleiner als die Einstellung user.zip.max_mb. |
|
future |
true / false |
false |
Auch vordatierte Datensätze (wie bei CMS:SELECT). | |
archive |
true / false |
false |
Auch abgelaufene Datensätze (wie bei CMS:SELECT). | |
cookiename |
Cookiename |
usess |
Session-Cookie, wie bei CMS:USER_LOGIN. | |
message[…] |
Text |
Core-Snippet | Eigene Meldungen: message[empty], message[limit], message[throttled]. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
² mindestens einer der mit ² markierten Parameter muss angegeben werden.
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Pressebilder einer Meldung, alle Motive eines Modells, die Auswahl aus einem
Leuchtkasten: CMS:ZIP packt die Dateien mehrerer Datensätze in ein Archiv und
schickt es dem Besucher als Download. Die Seite endet damit – was danach im Template
steht, erscheint nur, wenn kein Archiv entstanden ist.
Der Befehl gehört deshalb an den Anfang eines eigenen Templates, vor jede Ausgabe (das Frontend puffert nicht; nach der ersten Ausgabe meldet der Befehl einen Fehler). Ein eigenständiges Template, das ohne Datensatz aufgerufen wird, eignet sich gut:
(CMS:NUMBER useval="{$_GET['id']}" set=zip_id)
(CMS:ZIP relation=bilder src_id="{$set['zip_id']}" sort="_custom asc"
field="fil_druck,fil_web" name="Pressebilder" credits="titel,bildunterschrift,bildnachweis")
(CMS:INCLUDE layout_kopf)
(CMS:MESSAGE prefix="<div class='fehler'>" suffix="</div>")
(CMS:INCLUDE layout_fuss)
Verlinkt wird es mit der Id des Datensatzes, dessen Bilder gemeint sind:
(CMS:URL id=0 template=pressebilder_zip set=zip_url)
<a href="<?php echo $set['zip_url']; ?>?id=(CMS:TEXT record_id)">Alle Bilder als ZIP</a>
CMS:NUMBER macht aus dem Adressparameter eine Zahl. Fehlt er, ist src_id leer –
dann gibt es keine Dateien, nie „alle“.
Welche Datensätze. Die Auswahl folgt denselben Regeln wie CMS:SELECT:
filter, relation mit src_id oder ids. Es zählen nur veröffentlichte Datensätze
aus den Instanzen der Website (future und archive wie bei CMS:SELECT). Eine Angabe
ist Pflicht – ohne Auswahl entsteht kein Archiv.
Mit source=basket kommen die Datensätze aus dem Warenkorb der Session
(CMS:USER_BASKET). Eine eigene Kategorie trennt den Leuchtkasten vom
Einkauf:
(CMS:USER_BASKET add variable=bilder category=2) im Template des Leuchtkastens
(CMS:ZIP source=basket category=2 field=fil_web name=Auswahl) im Download-Template
Welche Datei. field nennt ein oder mehrere Datei-Felder. Je Datensatz kommt die
erste Datei ins Archiv, die der Besucher auch einzeln abrufen dürfte – dieselbe Prüfung
wie beim Download über file.php. Ist ein Feld nur für angemeldete Besucher
freigegeben, erhält es nur, wer mit einem passenden Profil angemeldet ist; alle anderen
bekommen das nächste Feld der Liste. So liefert field="fil_druck,fil_web" der
angemeldeten Presse die Druckversion, allen anderen die Web-Version. Datensätze ohne
erlaubte Datei werden übergangen.
Dateinamen. filenames legt die Namen im Archiv fest:
| Platzhalter | Inhalt |
|---|---|
{slug} |
Slug des Datensatzes (ohne Slug seine Id) |
{id} |
Id des Datensatzes |
{filename} |
ursprünglicher Dateiname ohne Endung |
{extension} |
Endung der Datei |
{feldname} |
Inhalt eines Feldes des Datensatzes |
Die Endung wird ergänzt, wenn das Muster sie nicht enthält; doppelte Namen erhalten
-2, -3 …
Bildnachweise. credits legt eine CREDITS.txt bei – je Datei ihr Name und
darunter die Inhalte der genannten Felder, etwa Titel, Bildunterschrift und Fotograf.
Grenzen. Ein Archiv enthält höchstens user.zip.max_files Dateien (Standard 100)
mit zusammen user.zip.max_mb MB (Standard 500). maxfiles und maxsize können diese
Werte für ein Template nur verkleinern. Überschreitet die Auswahl die Größe, entsteht
kein Archiv.
Fertige Archive bleiben user.zip.retention_hours Stunden (Standard 24) im Zwischenspeicher;
dieselbe Auswahl wird aus dem Speicher geliefert. Neue Archive sind je Herkunft gedrosselt
(user.throttle.zip_ip, Standard 30 je Stunde).
Meldungen. Entsteht kein Archiv, läuft das Template weiter. CMS:MESSAGE gibt die
Meldung aus, $set['zip_error'] nennt den Grund:
| Grund | Bedeutung |
|---|---|
empty |
keine Datei, die der Besucher abrufen darf |
limit |
Auswahl größer als die erlaubte Größe |
throttled |
zu viele Archive in kurzer Zeit |
Die Texte kommen aus den Core-Snippets zip_error_empty, zip_error_limit und
zip_error_throttled; message[empty]=… usw. ersetzt sie im Template.
CMS:RELATION
Wichtig: CMS:RELATION ist nur für Templates des Backends gedacht (Listen-, Trigger-Templates). Es prüft keine Zugriffsrechte: Es liefert die verknüpften Datensätze unabhängig von Instanz- und Bereichsrechten. Wie bei
CMS:SELECTzählen aber nur veröffentlichte Ziele (freigegeben, Veröffentlichungsdatum erreicht, Ablaufdatum nicht erreicht) – auch im Backend und in der Vorschau;future,archiveundstatelockern das. Datensätze im Papierkorb und Arbeitskopien erscheinen nie.
Gibt das Hauptfeld (Titel) aller Datensätze aus, die über ein Verknüpfungsfeld mit dem
aktuellen Datensatz verbunden sind – in der bevorzugten Sprachversion des Backends
(je Instanz; führt eine Instanz sie nicht, deren Hauptsprache). Datensätze mit leerem
Hauptfeld erscheinen nicht.
Für Frontend-Templates nutzen Sie (CMS:SELECT relation=…)
(CMS:RELATION field [parameter=wert …])
Methode: Cms::cmsRelation()
Ohne Schlüssel: der erste Wert ohne = gilt als field
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
id |
Datensatz-Id oder Slug |
(aktueller Datensatz) |
Datensatz, dessen Verweise ausgegeben werden. Reine Ziffernfolge = record_id, alles andere = Slug - dieselbe Regel wie in der URL. | |
field ¹ |
Feld |
– | Name des Verweisfeldes (Feldtyp relation, pointer oder link), dessen Ziele ausgegeben werden. Ein anderer Feldtyp ergibt eine Fehlermeldung. | |
limit |
Zahl >0 |
1000 |
Maximale Anzahl der einbezogenen Datensätze in der Ergebnismenge. | |
future |
true / false |
false |
true: Auch vordatierte Ziele (online_at noch nicht erreicht) werden ausgegeben (wie bei CMS:SELECT). | |
archive |
true / false |
false |
true: Auch abgelaufene Ziele (offline_at erreicht) werden ausgegeben (wie bei CMS:SELECT). | |
state |
Status-Id oder any |
0 |
Ohne Angabe nur freigegebene Ziele (status_id 0). Eine Status-Id > 0 nur Ziele mit diesem Status, "any" Ziele jeden Status (wie bei CMS:SELECT). | |
lang |
Sprachkürzel |
(aktuelle Sprache) |
Kürzel der Sprachversion, deren Inhalte angefordert werden (wirkt sich auf mehrsprachige Felder aus). Noch nicht umgesetzt - derzeit gilt immer die Sprache des Aufrufs. | |
separator |
Trennzeichen |
\| |
Text, der zwischen den Ergebnissen stehen soll. | |
prefix |
Text |
– | Text, der der Quelle vorangestellt wird, sofern diese nicht leer ist. | |
suffix |
Text |
– | Text, der an die Quelle angehängt wird, sofern diese nicht leer ist. | |
ifempty |
Text |
– | Text, der zur Quelle wird, wenn die ursprüngliche Quelle leer ist (weitere Parameter, außer 'set', werden ignoriert). | |
instead |
Text |
– | Überschreibt die Ausgabe, falls diese nicht leer ist. | |
set |
Array-Index von $set |
– | Die Ausgabe der Quelle wird umgeleitet und in $set eingetragen. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Gibt das Hauptfeld (Titel) aller Datensätze aus, auf die ein Verweisfeld des laufenden Datensatzes zeigt - in der im Backend festgelegten Reihenfolge:
Themen: (CMS:RELATION topics separator=", ")
Ausgabe: Themen: Reisen, Fotografie, Island
Der Befehl liefert nur die Titel. Sollen die verknüpften Datensätze selbst ausgegeben
werden (mit Bild, Link, Datum …), ist CMS:SELECT relation=topics der richtige Weg:
(CMS:SELECT relation=topics resulttemplate=topic_badge)
Rückwärts-Verknüpfungen (welche Artikel zeigen auf dieses Thema?) gehen über
CMS:SELECT filter[topics]="{$cms['record_id']}".
Parameter im Einzelnen
field
Name des Verweisfeldes; darf ohne Schlüssel stehen:
(CMS:RELATION field=topics)
(CMS:RELATION topics)
id
Die Verweise eines anderen Datensatzes - per record_id oder Slug, wie in der URL. Der
Datensatz muss wie in der URL veröffentlicht sein, sonst bleibt die Ausgabe leer:
(CMS:RELATION topics id=1977)
(CMS:RELATION topics id=island-reise)
limit
(CMS:RELATION topics limit=3 separator=", ")
Es werden höchstens drei Titel ausgegeben - die ersten drei in der Reihenfolge des Verweisfeldes.
future
Auch Ziele, deren Veröffentlichungsdatum noch nicht erreicht ist:
(CMS:RELATION termine future=true separator=", ")
archive
Auch abgelaufene Ziele:
(CMS:RELATION topics archive=true)
state
Alle Ziele unabhängig vom Status – etwa in einem Listen-Template des Backends:
(CMS:RELATION topics state=any future=true archive=true)
lang
Noch nicht umgesetzt - es gilt die Sprache des Aufrufs.
(CMS:RELATION topics lang=en)
separator
(CMS:RELATION topics separator=", ")
(CMS:RELATION topics separator="</li><li>" prefix="<ul><li>" suffix="</li></ul>")
Die zweite Zeile ergibt eine Liste: <ul><li>Reisen</li><li>Fotografie</li></ul>.
prefix
prefix / suffix umschließen den Wert und erscheinen nur, wenn er nicht leer ist -
so entsteht kein leeres Themen::
(CMS:RELATION topics prefix="Themen: " suffix="")
suffix
(CMS:RELATION topics suffix=".")
Siehe prefix.
ifempty
ifempty ersetzt die gesamte Ausgabe, wenn die Quelle leer ist; prefix und
suffix entfallen dann:
(CMS:RELATION topics prefix="<p>" suffix="</p>" ifempty="keine Themen zugeordnet")
instead
instead tauscht den Wert aus, sobald die Quelle nicht leer ist - nützlich, wenn
nur das Vorhandensein zählt:
(CMS:RELATION topics instead="Themen vorhanden" ifempty="-")
set
set gibt nichts aus, sondern legt das Ergebnis unter $set['topics'] für das
umgebende PHP oder einen späteren Befehl ab:
(CMS:RELATION topics set=topics)
<?php if ($set['topics'] !== '') { ?> … <?php echo $set['topics']; ?> … <?php } ?>
CMS:SELECT
Durchsucht die Datenbank anhand der angegebenen (Filter-)Kriterien und liefert die gefundenen Datensätze zurück.
(CMS:SELECT resulttemplate [parameter=wert …])
Methode: Cms::cmsSelect()
Ohne Schlüssel: der erste Wert ohne = gilt als resulttemplate
Mindestens einer von: filter, relation
PHP-Schnipsel: push_result (davor) · include, pop_result (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
resulttemplate ¹ |
Templatename |
– | ja | Listentemplate, das zur Verarbeitung der Ergebnisse eingebunden wird. |
noresulttemplate |
Templatename |
– | Detailtemplate, das eingebunden wird, falls die Abfrage keine Ergebnisse liefert. | |
noresult |
Text |
– | Text, der unmittelbar ausgegeben wird, falls die Abfrage keine Ergebnisse liefert. | |
filter[…][…] |
Wert oder EMPTY |
– | ² | Im Index: Name des zu durchsuchenden Feldes (relation_any für ein beliebiges Verweisfeld; area für den Namen des Inhaltsbereichs, eine Zahl gilt dort als area_id). Im Wert: der zu suchende Wert. |
filter_mode[…] |
AND / OR |
OR |
Modus, mit dem mehrere Werte desselben Feldes verknüpft werden (filter_mode[feld]=AND). | |
filter_op[…][…] |
=, ==, !=, !!, >, <, >>, <<, >=, <= |
= |
Vergleichsoperator für das angegebene Feld (filter_op[feld]=">"). Alternativ steht der Operator zwischen Schlüssel und Wert (filter[preis] > 100) oder direkt vor dem Wert (filter[preis]=">100"). | |
skip |
Zahl >=0 |
0 |
Anzahl der übersprungenen Datensätze in der Ergebnismenge. Wirkt nur zusammen mit limit. | |
limit |
Zahl >0 |
– | Maximale Anzahl der einbezogenen Datensätze in der Ergebnismenge - zugleich die Seitengröße für CMS:JUMP_LIST, CMS:JUMP_PREV und CMS:JUMP_NEXT. Ohne Angabe oder 0: unbegrenzt, keine Blätterleiste. | |
sort[…] |
Feldname(n), kommasepariert |
changed_at |
Feld, nach dem sortiert wird (vgl. order); mehrere kommasepariert oder als sort[]=…; area sortiert nach dem Namen des Inhaltsbereichs, verweisfeld.feld nach einem Feld des verknüpften Datensatzes. | |
order[…] |
desc / asc |
asc |
Reihenfolge, in der sortiert wird (vgl. sort). Ohne sort gilt changed_at desc. | |
lang |
Sprachkürzel |
(aktuelle Sprache) |
Kürzel der Sprachversion, deren Inhalte angefordert werden (wirkt sich auf mehrsprachige Felder aus). Noch nicht umgesetzt - derzeit gilt immer die Sprache des Aufrufs. | |
relation |
Feldname(n) kommasepariert oder 'ANY' |
– | ² | Feldnamen von Beziehungs-/Verweisfeldern, in denen die gesuchten Datensätze gespeichert sind (mit src_id); mit dest_id die Felder, über die die gesuchten Datensätze auf dest_id verweisen. ANY: irgendein Verweisfeld. |
src_id |
Datensatz-ID(s) |
aktueller Datensatz |
Quell-Datensatz, dessen Verknüpfungen ausgewertet werden (ohne Angabe: aktueller Datensatz). | |
dest_id |
Datensatz-ID |
– | Rückrichtung zu relation: die Datensätze, die über relation auf diesen Datensatz verweisen (relation=ANY: über irgendein Verweisfeld). Ohne src_id wird dann kein aktueller Datensatz als Quelle eingesetzt. | |
nextof |
Datensatz-ID |
– | Nur mit relation: Nachbarn des angegebenen Datensatzes in der Verweisliste - die Einträge hinter ihm (order=asc) bzw. vor ihm (order=desc), in der Reihenfolge der Liste; mit limit=1 der nächste bzw. vorige Datensatz. | |
count |
true / false |
false |
true: Die Anzahl der Datensätze in der Ergebnismenge wird ermittelt, unabhängig von "limit" und "skip" (im resulttemplate in $this->db->num_rows_unlimited; Grundlage der JUMP-Befehle). | |
hits |
true / false |
true |
false: Es wird nur gezählt (schließt count=true ein), es werden keine Datensätze geladen und kein resulttemplate eingebunden. Die Gesamtzahl steht danach im aufrufenden Template in $this->db->num_rows_unlimited; noresult/noresulttemplate gelten, wenn sie 0 ist. | |
archive |
true / false |
false |
true: Auch abgelaufene Datensätze (offline_at erreicht) werden in die Abfrage einbezogen. | |
future |
true / false |
false |
true: Auch vordatierte Datensätze (online_at noch nicht erreicht) werden in die Abfrage einbezogen. | |
debug |
true / false |
false |
true: Der erzeugte Datenbank-Query wird für Support-Analysen ausgegeben. | |
sql |
– | – | Raw SQL-Zusatz in der WHERE-Klausel (Achtung: nur für den Support!). | |
state |
Status-Id oder any |
0 |
Ohne Angabe nur freigegebene Datensätze (status_id 0). Eine Status-Id > 0 findet nur Datensätze mit diesem Status, "any" Datensätze jeden Status. | |
request |
Kommaliste |
– | Übernimmt Filter- und Sortierwerte aus der URL – Kommaliste oder Array der erlaubten Schlüssel: filter[feld], filter_op[feld], filter_mode[feld], skip, limit, sort, order (z. B. "filter[titel],sort,order,skip"). filter nur mit Feldname. Im Befehl gesetzte Werte haben immer Vorrang; für ein im Befehl gefiltertes Feld fügt die URL nichts hinzu. | |
set |
Array-Index von $set |
– | Die gesamte Ausgabe (samt eingebundenem Template) wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
² mindestens einer der mit ² markierten Parameter muss angegeben werden.
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Fragt Datensätze ab und bindet für die Treffer das resulttemplate ein; darin läuft
CMS:RESULT_START … CMS:RESULT_END über die Zeilen. Der Befehl ist der Kern jeder
Liste.
Gefunden werden nur veröffentlichte Datensätze: freigegeben (status_id 0),
Erscheinungsdatum (online_at) erreicht, Ende (offline_at) nicht erreicht. Die
Parameter future, archive und state lockern das je Abfrage - auch in der
Vorschau aus dem Backend, die zwar einen unveröffentlichten Datensatz zeigt, aber
darin die Listen so, wie die Website sie zeigt.
<h2>Neueste Artikel</h2>
(CMS:SELECT artikel_teaser
filter[area]=artikel
sort=online_at order=desc
limit=10
noresult="<p>Noch keine Artikel.</p>"
)
Listentemplate artikel_teaser:
<ul>
(CMS:RESULT_START)
<li><a href="(CMS:URL)">(CMS:TEXT title)</a> – (CMS:DATE online_at)</li>
(CMS:RESULT_END)
</ul>
Häufige Muster:
Ein Datensatz nach Slug: (CMS:SELECT detail filter[slug]=impressum limit=1)
Nur ein Thema: (CMS:SELECT liste filter[area]=artikel filter[topics]=42)
Suche im Titel: (CMS:SELECT liste filter[area]=artikel filter[title]="*{$_GET['q']}*")
Zeitraum: (CMS:SELECT liste filter[area]=artikel filter[online_at] >= 2026-01-01)
Verknüpfte Datensätze: (CMS:SELECT topic_badge relation=topics)
Kinder im Baum: (CMS:SELECT nav filter[parent_id]="{$cms['record_id']}" filter[depth]=1 sort=position order=asc)
Blätterbare Liste: (CMS:SELECT liste filter[area]=artikel limit=20 skip="{$_REQUEST['skip']}" count=true)
Suchformular mit Sortierung: (CMS:SELECT liste filter[area]=artikel limit=20 count=true request="filter[title],sort,order,skip")
Mehrere Werte für dasselbe Feld werden mit OR verknüpft (filter_mode[feld]=AND
schaltet um); verschiedene Felder immer mit AND. Vergleichsoperatoren stehen zwischen
Schlüssel und Wert (filter[preis] > 100, filter[farbe] != rot) oder direkt vor dem
Wert (">100"); Platzhalter und (empty) gehören in den Wert (Hen*) - die
vollständige Filtersyntax steht in den Grundlagen.
Ein CMS:SELECT darf innerhalb eines Listentemplates stehen (verschachtelte Listen);
das äußere Ergebnis wird automatisch beiseitegelegt und danach fortgesetzt.
Parameter im Einzelnen
resulttemplate
Listentemplate für die Treffer; darf ohne Schlüssel stehen. Name oder bereich.name:
(CMS:SELECT resulttemplate=artikel_teaser filter[area]=artikel)
(CMS:SELECT artikel_teaser filter[area]=artikel)
(CMS:SELECT artikeltemplates.artikel_teaser filter[area]=artikel)
noresulttemplate
Template ohne Treffer - etwa mit einem Suchformular oder Hinweistext:
(CMS:SELECT suchergebnis filter[area]=artikel filter[title]="*{$_GET['q']}*" noresulttemplate=suche_leer)
noresult
Kurzer Text ohne Treffer, ohne eigenes Template:
(CMS:SELECT liste filter[area]=artikel noresult="<p>Keine Einträge.</p>")
filter
Schlüssel = Feldname (eigene Felder, Metafelder wie record_id, status_id,
online_at, parent_id, user_id, das virtuelle slug, _default für das
Hauptfeld, area für den Bereichsnamen), Wert = Suchwert mit optionalem Operator:
filter[area]=artikel Bereich nach Name (Zahl = area_id)
filter[area]=artikel,news zwei Bereiche (OR)
filter[title]="Hen*" beginnt mit
filter[price] > 100 größer als
filter[price]=">100" dasselbe, Operator im Wert
filter[price]=">100 AND <500" Ausdruck in einem Feld
filter[online_at]=2026 das ganze Jahr
filter[termin] >= today ab heute (0 Uhr, Zeitzone des Aufrufs)
filter[online_at] > utc_now-7d die letzten sieben Tage, in UTC gerechnet
filter[teaser]="(empty)" leer / nicht übersetzt
filter[bild]="(empty)" Datei-Feld ohne Datei
filter[topics]=42 Verweisfeld zeigt auf Datensatz 42
filter[topics]="(empty)" Verweisfeld ohne Verweis
filter[relation_any]=42 irgendein Verweisfeld zeigt auf 42
filter[topics.title]="Island" Feld des verknüpften Datensatzes
filter[tags]=a filter[tags]=b zwei Werte, OR (siehe filter_mode)
filter[parent_id]=17 filter[depth]=1 direkte Kinder von 17
filter[parent_id]=null filter[depth]=1 oberste Ebene (Datensätze ohne Elterndatensatz)
filter[depth] ist kein Filter auf die Einrückung eines Datensatzes, sondern gibt
an, wie viele Ebenen unterhalb von filter[parent_id] durchsucht werden: 1 = nur
direkte Kinder, 2 = bis zu den Enkeln, ohne Angabe = alle Nachkommen. Ohne
filter[parent_id] bleibt es wirkungslos (Hinweis beim Speichern des Templates bzw. mit debug=true); filter[depth]=0 liefert keine Datensätze.
Der Wert unter „Einzurückende Stellen“ in der Datensatzinfo (0 = oberste Ebene) ist
dagegen absolut – die oberste Ebene finden Sie mit filter[parent_id]=null filter[depth]=1.
Textfelder werden nach den Regeln der Sprache verglichen und sortiert, in der
gesucht wird: Groß-/Kleinschreibung und Akzente spielen keine Rolle, ß gleich ss,
im Deutschen außerdem ä gleich ae (filter[title]="Mue*" findet „Müller“), im
Schwedischen dagegen ist ö ein eigener Buchstabe (o findet kein ö). Die Regeln
ergeben sich aus dem Sprachkürzel; siehe Grundlagen.
Ein leerer Wert hebt den Filter auf - praktisch für optionale Filter aus dem
Request, gefährlich bei Tippfehlern: filter[kategorie]="{$cms['id']}" (den Schlüssel
id gibt es nicht, richtig ist record_id) liefert alle Datensätze.
filter_mode
Verknüpfung mehrerer Werte desselben Feldes (Standard OR):
(CMS:SELECT liste filter[area]=artikel filter[tags]=reisen filter[tags]=island filter_mode[tags]=AND)
Findet Artikel, die beide Tags tragen.
filter_op
Vergleichsoperator getrennt vom Wert - gleichwertig zur Schreibweise im Wert und zum Operator zwischen Schlüssel und Wert:
(CMS:SELECT liste filter[area]=artikel filter[price]=100 filter_op[price]=">")
(CMS:SELECT liste filter[area]=artikel filter[price]=">100")
(CMS:SELECT liste filter[area]=artikel filter[price] > 100)
skip
Offset für die Blätterfunktion; meist aus dem Request. skip wirkt nur zusammen mit
limit – ohne limit wird es ignoriert, die Abfrage liefert dann alle Treffer ab dem
ersten:
(CMS:SELECT liste filter[area]=artikel limit=20 skip="{$_REQUEST['skip']}" count=true)
Die Blätterleiste (CMS:JUMP_LIST) steht im resulttemplate liste.
limit
(CMS:SELECT teaser filter[area]=artikel limit=3)
Höchstens drei Treffer. Ohne limit (oder mit 0) liefert die Abfrage alle
Treffer – bei großen Bereichen gehört deshalb immer ein limit dazu. limit ist
zugleich die Seitengröße der Blätterbefehle CMS:JUMP_LIST,
CMS:JUMP_PREV und CMS:JUMP_NEXT im resulttemplate;
ohne limit gibt es keine Blätterleiste.
sort
Sortierfeld, Richtung über order oder direkt im Wert:
sort=online_at order=desc
sort="title asc"
sort=position order=asc Reihenfolge aus dem Backend (Baum/Liste)
sort=_default Hauptfeld des Bereichs
sort=area nach Bereichsname
sort=author.name Feld des verknüpften Datensatzes
sort[]=status_id sort[]=title order[]=asc order[]=asc
order
Richtung zu sort: asc (Standard, sobald sort angegeben ist) oder desc. Ohne
sort sortiert der Befehl nach changed_at desc – die zuletzt geänderten zuerst.
(CMS:SELECT liste filter[area]=artikel sort=title order=asc)
lang
Noch nicht umgesetzt - es gilt die Sprache des Aufrufs.
relation
Treffer sind die Datensätze, auf die das Verweisfeld des Quell-Datensatzes zeigt -
ohne src_id die des laufenden Datensatzes. Die Reihenfolge des Verweisfeldes
liefert sort=_custom; ohne sort gilt die Standardsortierung (changed_at desc):
(CMS:SELECT topic_badge relation=topics)
(CMS:SELECT galerie relation=bilder sort="_custom asc")
Mehrere Feldnamen, kommagetrennt, gelten als ODER; ANY meint irgendein Verweisfeld.
sort=_custom und nextof setzen genau einen Feldnamen voraus.
(CMS:SELECT teaser relation="topics,tags")
(CMS:SELECT teaser relation=any)
Die umgekehrte Richtung - Datensätze, die auf den laufenden zeigen:
(CMS:SELECT produkt_liste relation=rel_produkte)
(CMS:SELECT produkt_liste filter[area]=produkte filter[kategorie]="{$cms['record_id']}")
(CMS:SELECT verweis_liste relation=any dest_id="{$cms['record_id']}")
Die erste Zeile gilt für Beziehungsfelder (relation): Sie haben im Zielbereich ein
Gegenfeld rel_{bereichsname}. Verweisfelder (pointer) haben keins - ihr Gegenstück
ist der Filter auf das Verweisfeld (zweite Zeile). Siehe
Jeder Datensatz ist eine Seite.
src_id
Quell-Datensatz für relation, wenn nicht der laufende gemeint ist:
(CMS:SELECT topic_badge relation=topics src_id=1977)
(CMS:SELECT topic_badge relation=topics src_id="{$set['user_id']}")
Ein leerer Wert oder 0 meint ebenfalls den laufenden Datensatz. Gibt es keinen – etwa
in einem eigenständigen Template ohne Datensatz –, findet die Abfrage nichts (und nicht
etwa alle Datensätze des Bereichs).
dest_id
Die Rückrichtung zu relation: Treffer sind die Datensätze, die über das Verweisfeld
– mit relation=any über irgendeines – auf dest_id zeigen, etwa „Wo wird dieses
Bild verwendet?":
(CMS:SELECT verweis_liste relation=any dest_id="{$cms['record_id']}")
(CMS:SELECT artikel_liste relation=topics dest_id=1977)
Mit dest_id setzt der Befehl ohne src_id keinen laufenden Datensatz als Quelle ein.
Dasselbe als Filterwert, auch für mehrere Ziele: filter[relation_any]="1977,1978".
nextof
Nur zusammen mit relation: statt aller verknüpften Datensätze die Nachbarn des
Datensatzes nextof in derselben Verweisliste - die Einträge hinter ihm bei
order=asc (Standard), die vor ihm bei order=desc, jeweils in der Reihenfolge der
Liste (die Sortierung ist damit festgelegt, sort wird ignoriert). Mit limit=1
ist das der nächste bzw. vorige Datensatz - etwa für „weiter“/„zurück“ in einer
Bildergalerie, deren Bilder am Artikel 2422 hängen:
(CMS:SELECT galerie_weiter relation=bilder src_id=2422 nextof="{$cms['record_id']}" limit=1)
(CMS:SELECT galerie_zurueck relation=bilder src_id=2422 nextof="{$cms['record_id']}" order=desc limit=1)
Steht nextof nicht in der Liste, gibt es keine Treffer.
count
Gesamtzahl unabhängig von limit/skip ermitteln - Grundlage für CMS:JUMP_* und für Angaben wie „Treffer 21–40 von 137“:
(CMS:SELECT liste filter[area]=artikel limit=20 skip="{$_REQUEST['skip']}" count=true)
Im resulttemplate liste stehen dann zwei Zahlen: $this->db->num_rows sind die
Treffer dieser Seite (höchstens limit), $this->db->num_rows_unlimited alle
Treffer der Abfrage:
(CMS:NUMBER useval="{$this->db->num_rows_unlimited}") Treffer
(CMS:RESULT_START) … (CMS:RESULT_END)
(CMS:JUMP_LIST)
Beide Zahlen gelten nur im resulttemplate: Nach dem CMS:SELECT ist wieder
das umgebende Ergebnis aktiv – dieselbe Regel, die verschachtelte Listen möglich macht.
Wird nur die Zahl gebraucht, zählt hits=false im aufrufenden Template.
hits
hits=false zählt nur (count=true ist damit eingeschlossen): Es werden keine
Datensätze geladen und kein resulttemplate eingebunden – der Name muss trotzdem
angegeben werden. Die Gesamtzahl steht danach im aufrufenden Template in
$this->db->num_rows_unlimited:
(CMS:SELECT x filter[area]=artikel filter[topics]=42 hits=false)
(CMS:NUMBER useval="{$this->db->num_rows_unlimited}") Artikel zu diesem Thema
Ist die Zahl 0, gelten noresult bzw. noresulttemplate wie gewohnt:
(CMS:SELECT x filter[area]=artikel filter[topics]=42 hits=false noresult="Noch keine Artikel zu diesem Thema.")
Steht das zählende CMS:SELECT in einem resulttemplate, ersetzt die Zahl dort die
Gesamtzahl des umgebenden Ergebnisses; $this->db->num_rows und die laufende
Ergebnisschleife bleiben unberührt. Eine Blätterleiste
(CMS:JUMP_LIST) des umgebenden Ergebnisses gehört dann vor das
zählende CMS:SELECT.
archive
Abgelaufene Datensätze (offline_at erreicht) einbeziehen - z. B. für ein Archiv
vergangener Veranstaltungen:
(CMS:SELECT archiv filter[area]=events archive=true sort=online_at order=desc)
Ein abgelaufener Datensatz bleibt übrigens unter seiner eigenen Adresse erreichbar;
er fehlt nur in Listen ohne archive.
future
Vordatierte Datensätze (online_at noch nicht erreicht) einbeziehen - z. B. eine
Vorschau auf kommende Beiträge:
(CMS:SELECT demnaechst filter[area]=artikel future=true filter[online_at] > now)
debug
SQL und Bind-Werte der Abfrage ausgeben (nur zur Entwicklung). Darüber stehen verworfene
Filter mit Begründung – etwa ein unbekannter Feldname oder filter[depth] ohne
filter[parent_id]:
(CMS:SELECT liste filter[area]=artikel debug=true)
sql
Roh-SQL für die WHERE-Klausel - nur für den Support, keine Benutzereingaben hineinreichen:
(CMS:SELECT liste filter[area]=artikel sql="AND `record`.`record_id` % 2 = 0")
set
Die gesamte Ausgabe – das Listentemplate mit allen Treffern, bei noresult dessen
Text – landet in $set statt auf der Seite. So lässt sich etwa eine Adresse aus einer
Abfrage in einem anderen Befehl weiterverwenden:
(CMS:SELECT url_only filter[area]=seiten filter[kennung]=warenkorb limit=1 set=warenkorb_url)
<a class="cart" href="<?php echo $set['warenkorb_url']; ?>">…</a>
(CMS:TEXT titel prefix="<a href='{$set['warenkorb_url']}'>" suffix="</a>")
Auch die Ausgabe von debug=true und PHP-Hinweise aus dem Listentemplate stehen dann
in $set und nicht auf der Seite – zum Untersuchen set vorübergehend weglassen.
Endet die Seite innerhalb der Abfrage (etwa durch ein Success-Template), wird das bis
dahin Erzeugte normal ausgegeben.
state
Ohne Angabe nur freigegebene Datensätze (status_id 0). Eine Status-Id findet nur
Datensätze mit genau diesem Status, any hebt den Statusfilter auf:
(CMS:SELECT liste filter[area]=artikel state=2)
(CMS:SELECT liste filter[area]=artikel state=any)
request
Übernimmt Filter, Sortierung und Blättern aus der URL (GET oder POST) – für Suchformulare und sortierbare Listen, ohne jeden Wert einzeln in den Befehl zu schreiben. Angegeben wird, was übernommen werden darf, als Kommaliste oder Array:
request="sort,order,skip" nur Sortierung und Blättern
request="filter[title],filter[price],filter_op[price]" Suche in Titel und Preis
request[]=sort request[]="filter[title]" dasselbe als Array
Erlaubt sind skip, limit, sort, order sowie filter, filter_op und
filter_mode nur mit Feldname (filter[title]). Ein filter ohne Feldname und
all werden ignoriert und als Fehler gemeldet – sonst dürfte ein Besucher nach jedem
Feld der gefundenen Datensätze suchen, auch nach Feldern, die die Seite gar nicht
anzeigt, etwa einer internen Notiz.
In der URL heißen die Schlüssel wie im Befehl, nur filterop und filtermode ohne
Unterstrich: ?filter[title]=Hen*&sort=title&order=desc&skip=20.
(CMS:SELECT liste filter[area]=artikel limit=20 count=true request="filter[title],sort,order,skip")
Die URL kann die Abfrage nur einschränken, nie erweitern:
- Werte im Befehl haben Vorrang. Für ein Feld, das der Befehl filtert, übernimmt
requestweder Werte nochfilter_op/filter_mode–?filter[area]=internoder?filterop[area]=!=ändern anfilter[area]=artikelnichts. Felder eines verknüpften Datensatzes zählen zum Verweisfeld (autor.namegehört zuautor),areaundarea_idgelten als dasselbe Feld. skip,limit,sortundorderaus dem Befehl ersetzen den Wert der URL vollständig.limitaus der URL gilt nur als positive Zahl – „unbegrenzt“ lässt sich nicht anfordern.- Verschlüsselte Felder finden nur den exakten Wert; Platzhalter (
*) und Größer/Kleiner liefern keine Treffer. - Passwortfelder lassen sich grundsätzlich weder filtern noch sortieren.
CMS:SELECT_LANGVERSION
Liefert die verfügbaren Sprachen als Datensätze zurück.
(CMS:SELECT_LANGVERSION resulttemplate)
Methode: Cms::cmsSelectLanguages()
Ohne Schlüssel: der erste Wert ohne = gilt als resulttemplate
PHP-Schnipsel: push_result (davor) · include, pop_result (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
resulttemplate ¹ |
Templatename |
– | ja | Listentemplate, das zur Verarbeitung der Ergebnisse eingebunden wird. |
set |
Array-Index von $set |
– | Die gesamte Ausgabe (samt eingebundenem Template) wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Stellt die Sprachen des Auftritts als Ergebnismenge bereit - Hauptsprache zuerst -
und bindet das resulttemplate ein. Jede Zeile trägt language_id, code, label,
endonym und current (1 = Sprache des laufenden Aufrufs). Damit entsteht ein
Sprachumschalter:
<ul class="lang">
(CMS:SELECT_LANGVERSION lang_item)
</ul>
Listentemplate lang_item:
(CMS:RESULT_START)
<li<?php if ($cms['current']) echo ' class="active"'; ?>>
<a href="(CMS:URL lang="{$cms['code']}")" hreflang="(CMS:TEXT code)">(CMS:TEXT endonym)</a>
</li>
(CMS:RESULT_END)
Die Zeilen sind keine Datensätze; CMS:URL ohne id verweist deshalb auf den
äußeren Datensatz - den der Seite bzw. die Zeile der umgebenden Ergebnisschleife -,
nur in der jeweils anderen Sprache. Ist das Feld slug mehrsprachig, setzt CMS:URL
den Slug der Zielsprache ein (/de/esstisch-nord-180 → /en/dining-table-nord-180),
siehe CMS:URL. Bei nur einer Sprache enthält die Ergebnismenge genau
eine Zeile.
Auf einer Seite ohne Datensatz (eigenständiges Template, /de/0,suche) gibt es
keinen äußeren Datensatz; das Template gehört dann ausdrücklich in den Verweis:
<a href="(CMS:URL lang="{$cms['code']}" template=suche)">(CMS:TEXT endonym)</a>
Parameter im Einzelnen
resulttemplate
Listentemplate; darf ohne Schlüssel stehen:
(CMS:SELECT_LANGVERSION resulttemplate=lang_item)
(CMS:SELECT_LANGVERSION lang_item)
CMS:RESULT_START
Markiert den Beginn einer Schleife zur Verarbeitung von CMS:SELECT-Ergebnissen.
(CMS:RESULT_START)
Kein Methodenaufruf – die Wirkung steckt allein im PHP-Schnipsel
PHP-Schnipsel: loop_start (danach)
Parameter
Dieser Befehl hat keine Parameter.
Anwendung
Öffnet die Schleife über die Treffer eines CMS:SELECT (bzw. CMS:SELECT_LANGVERSION,
CMS:USER_BASKET function=load). Steht im Listentemplate; alles zwischen
CMS:RESULT_START und CMS:RESULT_END wird je Treffer ausgeführt, die Befehle darin
lesen aus der aktuellen Zeile:
<table>
(CMS:RESULT_START)
<tr>
<td><?php echo $this->cms->row_no + 1; ?></td>
<td><a href="(CMS:URL)">(CMS:TEXT title)</a></td>
<td>(CMS:DATE online_at)</td>
</tr>
(CMS:RESULT_END)
</table>
Innerhalb der Schleife stehen zur Verfügung: $cms (die aktuelle Zeile als Array),
$this->cms->row_no (0-basierte Zeilennummer), $this->db->num_rows (Treffer auf
dieser Seite) und $this->db->num_rows_unlimited (Gesamtzahl bei count=true).
Der Befehl hat keine Parameter und erzeugt keinen Methodenaufruf - er wird direkt zu
einer for-Schleife kompiliert.
CMS:RESULT_END
Markiert das Ende einer Schleife zur Verarbeitung von CMS:SELECT-Ergebnissen.
(CMS:RESULT_END)
Kein Methodenaufruf – die Wirkung steckt allein im PHP-Schnipsel
PHP-Schnipsel: loop_end (danach)
Parameter
Dieser Befehl hat keine Parameter.
Anwendung
Schließt die mit CMS:RESULT_START geöffnete Schleife:
(CMS:RESULT_START)
<li>(CMS:TEXT title)</li>
(CMS:RESULT_END)
Jedes CMS:RESULT_START braucht genau ein CMS:RESULT_END im selben Template - der
Compiler erzeugt daraus { und }; ein fehlendes Ende führt zu einem PHP-Syntaxfehler
beim Aufruf der Seite.
CMS:JUMP_LIST
Erzeugt eine Blätter-Navigation (Pagination), z. B. 1 … 5 | 6 | 7 | 8 | 9 … 15.
(CMS:JUMP_LIST separator [parameter=wert …])
Methode: Pagination::cmsJumpList()
Ohne Schlüssel: der erste Wert ohne = gilt als separator
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
skip |
Zahl >= 0 |
$_REQUEST[skip] |
Versatz der aktuellen Seite (Anzahl übersprungener Ergebnisse). Ohne Angabe gilt der Request-Parameter skip. | |
maximum |
Zahl >= 1 |
7 |
Anzahl der gleichzeitig angezeigten (mitwandernden) Seitenverweise, ohne die Verweise auf erste und letzte Seite. | |
boundaries |
true / false |
true |
true: Verweise auf die erste und die letzte Seite werden immer angezeigt, sobald sie außerhalb des mitwandernden Bereichs liegen. | |
separator ¹ |
Trennzeichen |
(Leerzeichen) | Text zwischen zwei Seitenverweisen. Vor und hinter der Auslassung ('ellipsis') wird er nicht eingefügt. | |
tpl_link |
HTML-Vorlage |
<a href="{href}">{page}</a> |
Vorlage für einen Seitenverweis. {page} wird durch die Seitennummer ersetzt, {href} durch die Seiten-URL. Die URL ist HTML-maskiert (& statt &), ' und \ sind prozentkodiert – {href} kann direkt in einem Attribut stehen. |
|
tpl_active |
HTML-Vorlage |
<strong>{page}</strong> |
Vorlage für die aktuelle Seite. {page} wird durch die Seitennummer ersetzt, {href} durch die Seiten-URL. | |
ellipsis |
Code |
… |
Platzhalter für ausgelassene Seiten - erscheint zwischen einem Boundary-Verweis und dem mitwandernden Bereich. | |
replace |
Text?Text |
– | Einfache Textersetzung im fertigen HTML. Trenner ist das Fragezeichen: "suchen?ersetzen". | |
prefix |
Text |
– | Text vor der Navigation, nur wenn eine Navigation ausgegeben wird. | |
suffix |
Text |
– | Text nach der Navigation, nur wenn eine Navigation ausgegeben wird. | |
ifempty |
Text |
– | Ausgabe, wenn das Ergebnis auf eine einzige Seite passt oder das CMS:SELECT kein limit hat und deshalb keine Navigation nötig ist. | |
set |
Array-Index von $set |
– | Die Navigation wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Blätterleiste zu einem CMS:SELECT mit count=true. Die Seitengröße ist das limit
der Abfrage; ohne limit gibt es keine Seiten und der Befehl gibt nur ifempty
aus. Der aktuelle Versatz kommt aus $_REQUEST['skip'].
Der Befehl steht im resulttemplate des CMS:SELECT (vor oder hinter der
Ergebnisschleife): Nur dort gelten Trefferzahl und limit dieser Abfrage – hinter dem
CMS:SELECT im aufrufenden Template ist wieder das äußere Ergebnis aktiv.
(CMS:SELECT liste filter[area]=artikel limit=20 skip="{$_REQUEST['skip']}" count=true)
Template liste:
(CMS:RESULT_START)
<h2>(CMS:TEXT titel)</h2>
(CMS:RESULT_END)
<nav class="pager">
(CMS:JUMP_PREV) (CMS:JUMP_LIST separator=" | ") (CMS:JUMP_NEXT)
</nav>
Ausgabe: ❮❮ 1 … 5 | 6 | 7 | 8 | 9 … 15 ❯❯
Die Links behalten alle übrigen Parameter der aktuellen URL (Suchbegriff, Filter) und
setzen nur skip neu. Passt das Ergebnis auf eine Seite, wird nichts ausgegeben (bzw.
ifempty).
Bootstrap-artige Leiste:
(CMS:JUMP_LIST
prefix="<ul class=pagination>" suffix="</ul>"
tpl_link="<li><a href='{href}'>{page}</a></li>"
tpl_active="<li class=active><span>{page}</span></li>"
ellipsis="<li class=disabled><span>…</span></li>"
separator=""
)
Parameter im Einzelnen
skip
Versatz der aktuellen Seite, falls er nicht aus $_REQUEST['skip'] kommt:
(CMS:JUMP_LIST skip="{$offset}")
maximum
Anzahl der mitwandernden Seitenverweise (ohne erste/letzte Seite):
(CMS:JUMP_LIST maximum=5)
Ausgabe auf Seite 7 von 15: 1 … 5 6 7 8 9 … 15
boundaries
Verweise auf erste und letzte Seite unterdrücken:
(CMS:JUMP_LIST boundaries=false)
Ausgabe: 5 6 7 8 9 statt 1 … 5 6 7 8 9 … 15
separator
Text zwischen zwei Seitenverweisen; darf ohne Schlüssel stehen:
(CMS:JUMP_LIST " | ")
(CMS:JUMP_LIST separator=" · ")
tpl_link
Vorlage je Seitenverweis; {href} und {page} werden ersetzt:
(CMS:JUMP_LIST tpl_link="<a class=page href='{href}'>{page}</a>")
tpl_active
Vorlage für die aktuelle Seite:
(CMS:JUMP_LIST tpl_active="<span class='page current'>{page}</span>")
ellipsis
Platzhalter für ausgelassene Seiten (zwischen erster/letzter Seite und dem mitwandernden Bereich):
(CMS:JUMP_LIST ellipsis=" ··· ")
replace
Einfache Ersetzung im fertigen HTML, Trenner ? (nur ein Paar):
(CMS:JUMP_LIST replace="<strong>?<strong class=current>")
prefix
Erscheint nur, wenn eine Leiste ausgegeben wird:
(CMS:JUMP_LIST prefix="<nav class=pager>" suffix="</nav>")
suffix
(CMS:JUMP_LIST suffix="</nav>" prefix="<nav>")
ifempty
Ausgabe, wenn alles auf eine Seite passt (oder das CMS:SELECT kein limit hat):
(CMS:JUMP_LIST ifempty="<span class=muted>alle Einträge</span>")
set
(CMS:JUMP_LIST set=pager)
<?php if ($set['pager'] !== '') { ?><nav><?php echo $set['pager']; ?></nav><?php } ?>
CMS:JUMP_NEXT
Erzeugt einen Weiterblättern-Verweis.
(CMS:JUMP_NEXT tpl_link [parameter=wert …])
Methode: Pagination::cmsJumpNext()
Ohne Schlüssel: der erste Wert ohne = gilt als tpl_link
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
skip |
Zahl >= 0 |
$_REQUEST[skip] |
Versatz der aktuellen Seite (Anzahl übersprungener Ergebnisse). Ohne Angabe gilt der Request-Parameter skip. | |
tpl_link ¹ |
HTML-Vorlage |
<a href="{href}"><strong>❯❯</strong></a> |
Vorlage für den Verweis, solange es eine nächste Seite gibt. {href} wird durch die Ziel-URL ersetzt. Die URL ist HTML-maskiert (& statt &), ' und \ sind prozentkodiert – {href} kann direkt in einem Attribut stehen. |
|
tpl_inactive |
HTML-Vorlage |
<span>❯❯</span> |
Vorlage für den nicht anwählbaren Verweis, wenn es keine nächste Seite gibt. {href} wird ebenfalls ersetzt. | |
replace |
Text?Text |
– | Einfache Textersetzung im fertigen HTML. Trenner ist das Fragezeichen: "suchen?ersetzen". | |
prefix |
Text |
– | Text vor dem Verweis, nur wenn ein Verweis ausgegeben wird. | |
suffix |
Text |
– | Text nach dem Verweis, nur wenn ein Verweis ausgegeben wird. | |
ifempty |
Text |
– | Ausgabe, wenn das CMS:SELECT kein limit hat (dann gibt es keine Seiten). Passt alles auf eine Seite, erscheint tpl_inactive. |
|
set |
Array-Index von $set |
– | Der Verweis wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Verweis auf die nächste Seite einer blätterbaren Liste (CMS:SELECT mit count=true
und limit). Der Befehl steht wie CMS:JUMP_LIST im resulttemplate
der Abfrage, hier bei limit=20:
(CMS:JUMP_PREV) Seite <?php echo intdiv((int)($_REQUEST['skip'] ?? 0), 20) + 1; ?> (CMS:JUMP_NEXT)
Ausgabe: ❮❮ Seite 3 ❯❯
Gibt es keine nächste Seite – auch wenn alles auf eine Seite passt –, erscheint
tpl_inactive (Standard: das Symbol ohne Link). Die Seitengröße ist das limit des
CMS:SELECT; ohne limit erscheint nur ifempty.
Parameter im Einzelnen
skip
(CMS:JUMP_NEXT skip="{$offset}")
tpl_link
Vorlage mit Link; {href} wird ersetzt. Darf ohne Schlüssel stehen:
(CMS:JUMP_NEXT "<a class=next href='{href}'>weiter ›</a>")
tpl_inactive
Vorlage ohne nächste Seite ({href} wird auch hier ersetzt – mit der Adresse, die die
nächste Seite hätte):
(CMS:JUMP_NEXT tpl_link="<a href='{href}'>weiter ›</a>" tpl_inactive="<span class=disabled>weiter ›</span>")
replace
(CMS:JUMP_NEXT replace="❯❯?→")
prefix
(CMS:JUMP_NEXT prefix="<li class=next>" suffix="</li>")
suffix
(CMS:JUMP_NEXT suffix="</li>" prefix="<li>")
ifempty
(CMS:JUMP_NEXT ifempty="")
Greift nur, wenn das CMS:SELECT kein limit hat; Standard ist leer.
set
(CMS:JUMP_NEXT set=next)
CMS:JUMP_PREV
Erzeugt einen Zurückblättern-Verweis.
(CMS:JUMP_PREV tpl_link [parameter=wert …])
Methode: Pagination::cmsJumpPrev()
Ohne Schlüssel: der erste Wert ohne = gilt als tpl_link
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
skip |
Zahl >= 0 |
$_REQUEST[skip] |
Versatz der aktuellen Seite (Anzahl übersprungener Ergebnisse). Ohne Angabe gilt der Request-Parameter skip. | |
tpl_link ¹ |
HTML-Vorlage |
<a href="{href}"><strong>❮❮</strong></a> |
Vorlage für den Verweis, solange es eine vorherige Seite gibt. {href} wird durch die Ziel-URL ersetzt. Die URL ist HTML-maskiert (& statt &), ' und \ sind prozentkodiert – {href} kann direkt in einem Attribut stehen. |
|
tpl_inactive |
HTML-Vorlage |
<span>❮❮</span> |
Vorlage für den nicht anwählbaren Verweis, wenn es keine vorherige Seite gibt. {href} wird ebenfalls ersetzt. | |
replace |
Text?Text |
– | Einfache Textersetzung im fertigen HTML. Trenner ist das Fragezeichen: "suchen?ersetzen". | |
prefix |
Text |
– | Text vor dem Verweis, nur wenn ein Verweis ausgegeben wird. | |
suffix |
Text |
– | Text nach dem Verweis, nur wenn ein Verweis ausgegeben wird. | |
ifempty |
Text |
– | Ausgabe, wenn das CMS:SELECT kein limit hat (dann gibt es keine Seiten). Passt alles auf eine Seite, erscheint tpl_inactive. |
|
set |
Array-Index von $set |
– | Der Verweis wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Verweis auf die vorherige Seite - Gegenstück zu CMS:JUMP_NEXT, wie dieses im
resulttemplate eines CMS:SELECT mit limit (das limit ist die Seitengröße):
<nav>(CMS:JUMP_PREV "<a href='{href}'>‹ zurück</a>" tpl_inactive="") (CMS:JUMP_NEXT "<a href='{href}'>weiter ›</a>" tpl_inactive="")</nav>
Auf der ersten Seite erscheint tpl_inactive (hier leer), sonst der Link – auch wenn alles
auf eine Seite passt.
Parameter im Einzelnen
skip
(CMS:JUMP_PREV skip="{$offset}")
tpl_link
(CMS:JUMP_PREV "<a class=prev href='{href}'>‹ zurück</a>")
tpl_inactive
(CMS:JUMP_PREV tpl_inactive="<span class=disabled>‹ zurück</span>")
replace
(CMS:JUMP_PREV replace="❮❮?←")
prefix
(CMS:JUMP_PREV prefix="<li class=prev>" suffix="</li>")
suffix
(CMS:JUMP_PREV suffix="</li>" prefix="<li>")
ifempty
(CMS:JUMP_PREV ifempty="")
Greift nur, wenn das CMS:SELECT kein limit hat; Standard ist leer.
set
(CMS:JUMP_PREV set=prev)
CMS:CONTINUE
Setzt die Template-Abarbeitung trotz eines Success-Templates fort.
(CMS:CONTINUE)
Methode: Cms::cmsContinue()
Parameter
Dieser Befehl hat keine Parameter.
Anwendung
Befehle mit successtemplate (CMS:USER_LOGIN, CMS:USER_LOGIN_CODE,
CMS:USER_INPUT, CMS:USER_REGISTER, CMS:USER_REGISTER_CONFIRM, CMS:USER_PW,
CMS:USER_DELETE, CMS:EMAIL) binden im Erfolgsfall ihr Template ein und beenden
danach die Seite; ebenso CMS:USER_PROTECT und CMS:USER_PW mit ihrem
errortemplate und CMS:USER_LOGIN mit dem codetemplate. Steht CMS:CONTINUE vor dem
Befehl, läuft das Template nach dem eingebundenen Template stattdessen weiter:
(CMS:CONTINUE)
(CMS:USER_INPUT
container=comments
submit="$submit"
successtemplate=comment_saved
)
<h2>Alle Kommentare</h2>
(CMS:SELECT comment_list
filter[area]=comments
)
Ohne CMS:CONTINUE würde nach comment_saved nichts mehr ausgegeben - mit ihm
erscheint die Bestätigung oberhalb der Liste. Das Flag gilt genau einmal: Der nächste
dieser Befehle, der tatsächlich ein Template einbindet, verbraucht es.
CMS:CSP
Ändert die Script-Policy (Content-Security-Policy) dieser Seite: weitere Script-Quellen, Formularziele oder ein anderer Modus.
(CMS:CSP [parameter=wert …])
Methode: Cms::cmsCsp()
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
script |
Quellen |
– | Weitere Script-Quellen, durch Leerzeichen oder Komma getrennt. | |
form |
Quellen |
– | Weitere Formularziele (schaltet form-action ein). |
|
mode |
off / report / enforce |
– | Modus für diese Seite; leer: Einstellung der Website. |
Anwendung
Für jede Website lässt sich im Backend eine Script-Policy einschalten (Instanz, Abschnitt „Script-Policy (CSP)“). Dann laufen nur noch Scripts,
- die die Nonce der Seite tragen (ein Zufallswert je Aufruf) – im Template
schreiben Sie dafür
<script(CMS:NONCE)>, siehe CMS:NONCE, oder - die von der eigenen Website oder aus den eingetragenen Quellen geladen werden
(
<script src="/js/app.js">).
Code, der über Inhalte in die Seite gerät (ein Kommentar mit <script>, ein Bild mit
onerror), trägt keine Nonce und läuft nicht. Voreinstellung ist „aus“; „Nur melden“
lässt alles laufen und schreibt Verstöße ins Protokoll csp – so sieht man vor dem
Durchsetzen, was blockiert würde.
CMS:CSP passt die Policy für eine Seite an, etwa die Kasse mit dem Script des
Zahlungsanbieters:
(CMS:CSP script="https://js.stripe.com" form="https://checkout.stripe.com")
Der Befehl muss vor der ersten Ausgabe stehen, am besten ganz oben im Template; steht er
später oder ist mode unbekannt, legt er eine Fehlermeldung ab (CMS:MESSAGE).
Mehrere Aufrufe sammeln Quellen, getrennt durch Leerzeichen oder Komma. Erlaubt sind
Host-Angaben (https://js.example.com,
*.example.com, mit Port oder Pfad); Schlüsselwörter wie 'unsafe-inline' und reine
Schemata wie https: werden verworfen – sie höben die Policy auf.
Was die Policy blockiert und wie es richtig geht:
| blockiert | stattdessen |
|---|---|
<button onclick="…">, <form onsubmit="…"> |
data-…-Attribut und addEventListener in einem <script> des Templates |
<a href="javascript:…"> |
<a href="#" data-…> + Listener |
<script> ohne Nonce |
<script(CMS:NONCE)> |
echo '<script>…</script>' in PHP |
(CMS:NONCE set=nonce) und echo '<script' . $set['nonce'] . '>' – siehe CMS:NONCE |
| fremde Scripts (Tag-Manager, Widgets) | Quelle in der Instanz eintragen oder CMS:CSP script=…; lädt ein Script weitere nach, „Nachgeladene Scripts erlauben“ einschalten |
Beim Speichern eines Templates meldet der Editor jedes <script> ohne (CMS:NONCE),
jedes on…=-Attribut und jedes javascript: als Hinweis; auf der Startseite sammelt sie
das Widget „Template-Meldungen“ – auch für Templates, die im Dateisystem bearbeitet
werden.
Wer eine ganz eigene Policy will (auch für Styles, Bilder, Verbindungen), sendet sie mit
CMS:HEADER und holt die Nonce mit (CMS:NONCE format=value).
Parameter im Einzelnen
script
(CMS:CSP script="https://maps.example.com https://cdn.example.com")
form
(CMS:CSP form="https://www.paypal.com")
mode
Eine Seite mit einem alten Widget vorübergehend ausnehmen oder nur melden lassen:
(CMS:CSP mode=report)
(CMS:CSP mode=off)
CMS:HEADER
Sendet einen HTTP-Header.
(CMS:HEADER text [parameter=wert …])
Methode: Cms::cmsHeader()
Ohne Schlüssel: der erste Wert ohne = gilt als text
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
text ¹ |
Text |
– | Textzeile, die als Header an den Browser gesendet werden soll. Ohne Wert (Name:) wird der Header entfernt. |
|
overwrite |
true / false |
true |
false: Ein vorausgegangener Header desselben Typs wird nicht überschrieben; es wird ein neuer Header hinzugefügt. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Sendet einen HTTP-Header. Muss vor der ersten Ausgabe stehen - also am Anfang des Templates, vor jedem HTML und ohne Leerzeile davor:
(CMS:HEADER "Content-Type: application/json; charset=utf-8")
{"title": (CMS:TEXT title specialchars=true lbrconvert=false prefix='"' suffix='"')}
Weitere typische Fälle:
(CMS:HEADER "Content-Type: application/rss+xml; charset=utf-8")
(CMS:HEADER "Cache-Control: no-store")
(CMS:HEADER "HTTP/1.1 404 Not Found")
Weiterleitung – CMS-Befehle lassen sich nicht ineinander schachteln; die Adresse kommt
deshalb erst per set in $set und dann in den Header:
(CMS:URL id=startseite set=ziel)
(CMS:HEADER "Location: {$set['ziel']}")
Liefert ein Template CSS, Javascript oder JSON aus, gehört der passende Content-Type
an den Anfang – camindo CMS sendet X-Content-Type-Options: nosniff, der Browser führt
ein Script mit dem Standardtyp text/html deshalb nicht aus:
(CMS:HEADER "Content-Type: text/javascript; charset=utf-8")
Ist schon etwas ausgegeben, wird eine Fehlermeldung abgelegt (CMS:MESSAGE) und der
Header nicht gesendet.
Parameter im Einzelnen
text
Die Header-Zeile; darf ohne Schlüssel stehen:
(CMS:HEADER text="X-Robots-Tag: noindex")
(CMS:HEADER "X-Robots-Tag: noindex")
overwrite
Standardmäßig ersetzt ein Header einen vorherigen desselben Namens; overwrite=false hängt einen weiteren an:
(CMS:HEADER "Link: </style.css>; rel=preload; as=style")
(CMS:HEADER "Link: </app.js>; rel=preload; as=script" overwrite=false)
Voreinstellungen ersetzen oder entfernen
camindo CMS sendet vor dem Template einige Sicherheits-Header:
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Cache-Control: private, no-cache
Die Seite lässt sich damit nur innerhalb der eigenen Website in einen Rahmen legen (Hintergrund und Fehlerbilder: Grundlagen). Jeder dieser Header lässt sich ersetzen; ohne Wert wird er entfernt. Eine Seite, die ein Partner einbetten darf:
(CMS:HEADER "X-Frame-Options:")
(CMS:HEADER "Content-Security-Policy: frame-ancestors 'self' https://partner.example")
Ist für die Website eine Script-Policy eingeschaltet (Backend, Instanz), sendet
camindo stattdessen script-src 'self' 'nonce-…' samt frame-ancestors 'self'; für
einzelne Seiten passen Sie sie mit CMS:CSP an.
Eine eigene Content-Security-Policy ersetzt die Voreinstellung – nehmen Sie
frame-ancestors dann mit auf (die Nonce der Seite liefert CMS:NONCE):
(CMS:HEADER "Content-Security-Policy: frame-ancestors 'self'; script-src 'self' https://cdn.example")
Zwischenspeichern (Cache)
Seiten werden bei jedem Aufruf neu erzeugt. Mit der Voreinstellung
Cache-Control: private, no-cache speichert kein Proxy und kein CDN die Seite, und
der Browser fragt vor jeder Anzeige erneut beim Server an – geänderte Inhalte erscheinen
sofort. Die Zurück-Schaltfläche des Browsers bleibt trotzdem schnell.
Gehört eine Seite einem einzelnen Besucher – es läuft eine Sitzung eines Frontend-Users
(auch ein Warenkorb ohne Anmeldung, siehe CMS:USER_BASKET) oder das
Frontend-Editing ist eingeschaltet –, sendet camindo stattdessen
Cache-Control: private, no-store: Die Seite wird nirgends gespeichert.
Eine Seite, die sich selten ändert, darf länger zwischengespeichert werden:
(CMS:HEADER "Cache-Control: public, max-age=3600")
Setzen Sie den Header an den Template-Anfang. Läuft danach noch ein CMS:USER_*-Befehl mit
einer Sitzung, gilt wieder private, no-store – persönliche Seiten landen so in keinem
Zwischenspeicher.
CMS:INCLUDE
Bindet ein Template ein.
(CMS:INCLUDE template)
(CMS:INCLUDE template caching=5m cacheparams=$params)
Methode: Cms::cmsInclude()
Ohne Schlüssel: der erste Wert ohne = gilt als template
PHP-Schnipsel: include_cached (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
template ¹ |
Templatename |
– | ja | Detailtemplate, das eingebunden wird. |
set |
Array-Index von $set |
– | Die gesamte Ausgabe (samt eingebundenem Template) wird nicht ausgegeben, sondern in der Variable $set gespeichert. | |
caching |
Sekunden, 30s, 5m, 1h, 1d oder infinite |
– | Die Ausgabe des Templates wird so lange zwischengespeichert und danach ohne erneute Abarbeitung ausgeliefert. Ohne Angabe, leer oder 0: kein Cache. |
|
cacheparams |
null, bool, int, string oder Array |
– | Alles, wovon die Ausgabe abhängt (Datensatz, Seite, Kunde …): je Wert entsteht eine eigene Cache-Datei. Instanz und Sprache zählen von selbst mit. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Bindet ein anderes Template an dieser Stelle ein - für Kopf- und Fußbereiche, Navigationen, wiederkehrende Blöcke:
(CMS:INCLUDE header)
<main>
<h1>(CMS:TEXT title)</h1>
(CMS:INCLUDE text_image_block)
</main>
(CMS:INCLUDE footer)
Das eingebundene Template sieht denselben Datensatz ($cms), dieselben $set-Werte
und, innerhalb einer Ergebnisschleife, dieselbe Zeile. Ein Name ohne Bereich muss im
Zugriffsbereich der Instanz eindeutig sein, sonst bereich.name:
(CMS:INCLUDE templates.header)
Die Verschachtelungstiefe ist durch admin.max_include_depth (Standard 20) begrenzt -
ein Template, das sich selbst einbindet, bricht mit einer Fehlermeldung ab.
Parameter im Einzelnen
template
Name oder bereich.name (keine Id); darf ohne Schlüssel stehen. Im Backend-Editor ist der Wert ein Link auf das Template:
(CMS:INCLUDE template=header)
(CMS:INCLUDE header)
(CMS:INCLUDE templates.header)
(CMS:INCLUDE "{$set['layout']}") dynamisch
set
Die Ausgabe des eingebundenen Templates landet in $set statt auf der Seite – etwa um
einen Baustein zweimal zu verwenden oder an einen anderen Befehl weiterzugeben:
(CMS:INCLUDE preis set=preis)
<p class="preis"><?php echo $set['preis']; ?></p>
<meta property="product:price" content="(CMS:TEXT useval="{$set['preis']}" striptags=true specialchars=true)">
Mit caching kommt auch der abgefangene Text aus dem Cache.
caching
Schaltet den Ausgabe-Cache für diesen Aufruf ein: Beim ersten Aufruf läuft das
Template wie gewohnt, seine Ausgabe wird unter var/cache/include/ abgelegt. Jeder
weitere Aufruf mit denselben Angaben liefert innerhalb der Frist diese Datei aus – das
Template, seine Abfragen und eingebundenen Templates laufen dann nicht.
| Wert | Frist |
|---|---|
300 oder 300s |
300 Sekunden |
5m |
5 Minuten |
1h |
1 Stunde |
1d |
1 Tag |
infinite |
unbegrenzt – bis das Template geändert wird |
leer oder 0 |
kein Cache (etwa um ihn per Variable abzuschalten) |
(CMS:INCLUDE navigation caching=1h)
(CMS:INCLUDE footer caching=1d)
(CMS:INCLUDE teaser caching="{$set['cache_time']}")
Ein ungültiger Wert (caching=5x) erscheint als Fehlermeldung; das Template läuft dann
ohne Cache.
cacheparams
Der Cache unterscheidet von selbst nach Website und Sprache. Hängt die
Ausgabe von weiteren Dingen ab – dem Datensatz, der Seite einer Liste, einem Filter, dem
angemeldeten Kunden –, gehören sie in cacheparams. Für jeden anderen Wert entsteht
eine eigene Cache-Datei.
Erlaubt sind null, true/false, Zahlen, Texte und Arrays. Eine Variable ohne
Anführungszeichen wird unverändert übergeben, auch ein Array:
<?php $params = ['id' => $cms['record_id'], 'seite' => (int)($_GET['skip'] ?? 0)]; ?>
(CMS:INCLUDE produktliste caching=10m cacheparams=$params)
(CMS:INCLUDE produkt_box caching=1h cacheparams=$cms['record_id'])
(CMS:INCLUDE produkt_box caching=1h cacheparams="produkt-{$cms['record_id']}")
Arrays werden als Ganzes verglichen – dieselben Schlüssel in anderer Reihenfolge ergeben eine andere Cache-Datei.
Was der Cache beachtet
- Wird das eingebundene Template geändert, gelten seine Cache-Dateien sofort nicht mehr. Ändern sich dagegen Inhalte im Backend, sieht der Besucher sie erst nach Ablauf der Frist. Wählen Sie die Frist danach, wie schnell Änderungen sichtbar werden müssen.
- Ein Template, das selbst Templates einbindet, legt deren Ausgabe mit ab. Ändert sich ein solches inneres Template, bleibt die gecachte Ausgabe bis zum Ablauf der Frist.
- Gespeichert wird nur die Ausgabe. Was das Template sonst tut – Werte in
$setsetzen,CMS:HEADER,CMS:CSP, Cookies, E-Mails –, geschieht bei einem Treffer nicht. Solche Befehle gehören ins aufrufende Template. - Die Nonce der Script-Policy (
CMS:NONCE) ist in jeder Auslieferung die aktuelle. - Formulare mit Sicherheits-Token (
CMS:USER_TOKEN,CMS:USER_INPUT) und persönliche Inhalte angemeldeter Kunden nicht cachen – oder den Kunden incacheparamsaufnehmen. - Meldet ein Befehl beim Erzeugen einen Fehler, wird die Ausgabe nicht abgelegt.
- In der Vorschau aus dem Backend und bei eingeschaltetem Frontend-Editing gilt kein Cache.
- Rufen viele Besucher gleichzeitig ein abgelaufenes Template auf, erzeugt es nur einer; die anderen warten kurz und bekommen dann das Ergebnis.
Aufgeräumt werden abgelaufene Dateien von der Zeitplaner-Aufgabe
„Frontend-Cache aufräumen“; ganz geleert wird der Cache mit
php Core/cron/camindo.php cache include. Beides richtet der Systemadministrator ein.
CMS:NONCE
Gibt die Nonce der Seite aus – für jedes Script im Template.
(CMS:NONCE [parameter=wert …])
Methode: Cms::cmsNonce()
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
format |
"attribute" / "value" |
"attribute" |
attribute: nonce="…" (mit führendem Leerzeichen, für ein <script>-Tag). value: nur der Wert. |
|
set |
Array-Index von $set |
– | Die Ausgabe wird nicht ausgegeben, sondern in $set gespeichert. |
Anwendung
Ist die Script-Policy der Website durchgesetzt (siehe CMS:CSP), laufen nur Scripts mit der Nonce der Seite – einem Zufallswert, der mit jedem Aufruf wechselt. Schreiben Sie deshalb jedes Script im Template so:
<script(CMS:NONCE)>
document.querySelectorAll('[data-confirm]').forEach(function (form) {
form.addEventListener('submit', function (ev) {
if (!confirm(form.dataset.confirm)) ev.preventDefault();
});
});
</script>
Auch eingebundene Dateien bekommen sie, wenn sie nicht von der eigenen Website kommen:
<script src="https://cdn.example.com/lib.js"(CMS:NONCE)></script>
camindo CMS setzt die Nonce nicht selbst ein. Fehlt sie, meldet der Template-Editor beim Speichern einen Hinweis („script without (CMS:NONCE)“), ebenso das Widget „Template-Meldungen“ auf der Startseite. Solange die Script-Policy aus ist, schadet das Attribut nicht – schreiben Sie es trotzdem gleich mit, dann lässt sich die Policy später einschalten, ohne die Templates zu ändern.
onclick="…", onsubmit="…" und href="javascript:…" laufen unter der Policy gar nicht,
auch nicht mit Nonce: verlegen Sie den Code in ein <script(CMS:NONCE)> mit
addEventListener (Beispiel oben).
Erzeugt PHP das Tag, holen Sie die Nonce mit set:
(CMS:NONCE set=nonce)
<?php echo '<script' . $set['nonce'] . '>var shop = ' . json_encode($config) . ';</script>'; ?>
Für eine eigene Policy per CMS:HEADER:
(CMS:NONCE format=value set=nonce)
(CMS:HEADER "Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$set['nonce']}'")
Eine Seite, die ein Proxy oder Cache mit fester Nonce ausliefert, schützt nicht mehr – Seiten mit Script-Policy nicht als Ganzes cachen.
Parameter im Einzelnen
format
<script(CMS:NONCE)>…</script>
(CMS:NONCE format=value)
set
(CMS:NONCE set=nonce)
CMS:SAVEDATA
Speichert die Daten des aktuellen Datensatzes in der angegebenen Variable.
(CMS:SAVEDATA phpvar [parameter=wert …])
Methode: Cms::cmsSaveData()
Ohne Schlüssel: der erste Wert ohne = gilt als phpvar
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
phpvar ¹ |
Index innerhalb $GLOBALS |
– | ja | Variable, in der die Daten des aktuellen Datensatzes abgelegt werden sollen. |
id |
Datensatz-Id oder Slug |
– | Datensatz, dessen Daten geladen werden sollen. Reine Ziffernfolge = record_id, alles andere = Slug - dieselbe Regel wie in der URL. Ohne Angabe der aktuell geladene Datensatz. | |
lang |
Sprachversions-Kürzel |
$lang |
Gibt an, in welcher Sprache die Inhalte mehrsprachiger Felder geladen werden sollen. Noch nicht umgesetzt - derzeit gilt immer die Sprache des Aufrufs. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Legt einen kompletten Datensatz als Array in einer globalen PHP-Variable ab - für Logik, die mit den Ausgabebefehlen allein nicht zu schreiben ist:
(CMS:SAVEDATA article)
<?php if ($article['price'] > 100 && $article['stock'] > 0) { ?>
<span class="badge">Premium</span>
<?php } ?>
Mit id einen anderen Datensatz laden, etwa den Elternknoten, um mit seinen Werten
zu rechnen:
(CMS:SAVEDATA parent id="{$cms['parent_id']}")
<?php if ($parent['price'] > $cms['price']) { ?> … <?php } ?>
Geht es nur um die Ausgabe von Feldern eines anderen Datensatzes, brauchen Sie
CMS:SAVEDATA nicht – die Ausgabebefehle lesen mit id selbst:
<a href="(CMS:URL "{$cms['parent_id']}")">(CMS:TEXT title id="{$cms['parent_id']}")</a>
<footer>(CMS:TEXT footer_text id=konfiguration)</footer>
Das Array enthält die Metafelder (record_id, area_id, online_at …) und die
Felder des Bereichs in der aktuellen Sprache; Upload-Felder als Datei-Array (filename,
mime_type, size …). Verweisfelder fehlen – ihre Ziele liefert
CMS:SELECT relation=…, ihre Anzahl (CMS:SELECT x relation=topics hits=false) –
danach steht sie in $this->db->num_rows_unlimited.
Gibt es den Datensatz nicht, ist das Array leer.
Parameter im Einzelnen
phpvar
Name der Variable; darf ohne Schlüssel stehen. Nur Buchstaben, Ziffern und _.
Das Array steht danach direkt als $article im Template – auch in mit
CMS:INCLUDE eingebundenen Templates – und zusätzlich in $GLOBALS['article'],
etwa für eigene PHP-Funktionen:
(CMS:SAVEDATA phpvar=article)
(CMS:SAVEDATA article)
<?php echo $article['record_id']; ?>
Namen, die mit _ beginnen, sowie this und GLOBALS sind nur über
$GLOBALS[…] erreichbar.
id
Anderer Datensatz - record_id oder Slug:
(CMS:SAVEDATA parent id="{$cms['parent_id']}")
(CMS:SAVEDATA site id=konfiguration)
lang
Noch nicht umgesetzt - es gilt die Sprache des Aufrufs.
CMS:FED
Erzeugt einen Verweis zum Bearbeiten eines Datensatzes, wenn das Frontend-Editing aktiviert ist.
(CMS:FED id [parameter=wert …])
Methode: Cms::cmsFed()
Ohne Schlüssel: der erste Wert ohne = gilt als id
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
label |
Text |
[»] |
Text, der verlinkt wird. | |
id ¹ |
Datensatz-Id oder Slug |
(aktueller Datensatz) |
Datensatz, auf den verwiesen wird. Reine Ziffernfolge = record_id, alles andere = Slug - dieselbe Regel wie in der URL. | |
info |
true / false |
false |
true: Der Verweis öffnet die Detailansicht des Datensatzes statt der Eingabemaske. | |
prefix |
Text |
– | Text, der (unverlinkt) vor dem Verweis ausgegeben wird. | |
suffix |
Text |
– | Text, der (unverlinkt) nach dem Verweis ausgegeben wird. | |
set |
Array-Index von $set |
– | Der Verweis wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Bearbeiten-Verweis ins Backend - nur sichtbar, wenn der Betrachter das
Frontend-Editing in seinen persönlichen Backend-Einstellungen eingeschaltet hat
(Cookie fed). Für Besucher ohne Cookie gibt der Befehl nichts aus, die Seite bleibt
unverändert:
<h1>(CMS:TEXT title) (CMS:FED)</h1>
Ausgabe (mit Cookie): <h1>Hallo Welt <a href="#" data-c9-fed="1977" class="c9-fed"><span style="color:#f00">[»]</span></a></h1> – den Klick wertet (CMS:FED_SCRIPT) aus (einmal je Seite) und öffnet das Backend in einem neuen Tab, bei fester Backend-Domain unter deren Adresse.
In einer Liste je Zeile, mit eigenem Text:
(CMS:RESULT_START)
<li>(CMS:TEXT title) (CMS:FED label="✎" prefix=" " )</li>
(CMS:RESULT_END)
Das Backend verlangt weiterhin die Anmeldung - der Verweis öffnet bei Bedarf den Anmeldedialog über der Eingabemaske.
Parameter im Einzelnen
label
Linktext (HTML erlaubt); Standard ist ein rotes [»] (<span style="color:#f00">[»]</span>):
(CMS:FED label="bearbeiten")
(CMS:FED label="<img src='/static/edit.svg' alt='bearbeiten'>")
id
Anderer Datensatz; darf ohne Schlüssel stehen:
(CMS:FED 1977)
(CMS:FED id="{$cms['parent_id']}" label="Eltern bearbeiten")
info
Detailansicht statt Eingabemaske öffnen:
(CMS:FED info=true label="Details")
prefix
Unverlinkter Text vor dem Verweis - nur, wenn der Verweis ausgegeben wird:
(CMS:FED prefix="<span class=edit>" suffix="</span>")
suffix
(CMS:FED suffix="</span>" prefix="<span class=edit>")
set
(CMS:FED set=edit)
<?php if ($set['edit'] !== '') { ?><div class="toolbar"><?php echo $set['edit']; ?></div><?php } ?>
CMS:FED_CODE
Blendet HTML-Code ein, wenn das Frontend-Editing aktiviert ist.
(CMS:FED_CODE code [parameter=wert …])
Methode: Cms::cmsFedCode()
Ohne Schlüssel: der erste Wert ohne = gilt als code
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
code ¹ |
HTML-Code |
– | HTML-Code, der ausgegeben werden soll, wenn das Frontend-Editing eingeschaltet ist. | |
instead |
HTML-Code |
– | Alternative Ausgabe, wenn das Frontend-Editing ausgeschaltet ist. | |
set |
Array-Index von $set |
– | Der Code wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Beliebiges HTML, das nur bei eingeschaltetem Frontend-Editing erscheint - etwa eine Werkzeugleiste oder ein Hinweis auf den Bearbeitungsmodus:
(CMS:FED_CODE "<div class=fed-bar>Bearbeitungsmodus – <a href='/backend/'>Backend öffnen</a></div>")
Mit instead eine Alternative für Besucher ohne Frontend-Editing:
<body class="(CMS:FED_CODE code="editing" instead="public")">
Parameter im Einzelnen
code
HTML bei eingeschaltetem Frontend-Editing; darf ohne Schlüssel stehen:
(CMS:FED_CODE code="<link rel=stylesheet href='/static/fed.css'>")
(CMS:FED_CODE "<link rel=stylesheet href='/static/fed.css'>")
instead
Ausgabe für Besucher ohne Frontend-Editing:
(CMS:FED_CODE code="<p>Sie sehen die Redaktionsansicht.</p>" instead="")
set
(CMS:FED_CODE code=1 instead=0 set=fed)
<?php if ($set['fed']) { ?> … <?php } ?>
CMS:FED_SCRIPT
Stellt das Script für die Bearbeiten-Verweise aus CMS:FED bereit – einmal je Seite.
(CMS:FED_SCRIPT [set])
Methode: Cms::cmsFedScript()
Ohne Schlüssel: der erste Wert ohne = gilt als set
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
set ¹ |
Array-Index von $set |
– | Der Script-Block wird nicht ausgegeben, sondern in der Variable $set gespeichert. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Die Verweise aus CMS:FED enthalten kein Ziel und keinen Javascript-Code. Den Klick
wertet erst das Script aus CMS:FED_SCRIPT aus und öffnet die Eingabemaske (bzw. mit
info=true die Detailansicht) im Backend in einem neuen Tab – bei fester
Backend-Domain unter deren Adresse. Ohne CMS:FED_SCRIPT auf der Seite bleibt ein
Klick auf den Verweis wirkungslos.
Schreiben Sie den Befehl einmal in ein Template, das jede Seite einbindet, üblicherweise
in den Footer vor </body>:
(CMS:FED_SCRIPT)
</body>
</html>
Die Reihenfolge zu den CMS:FED-Verweisen spielt keine Rolle. Wie CMS:FED gibt der
Befehl nur etwas aus, wenn der Betrachter das Frontend-Editing eingeschaltet hat;
für Besucher bleibt die Seite unverändert. Steht der Befehl mehrfach auf einer Seite
(etwa über mehrere CMS:INCLUDE), erscheint das Script trotzdem nur einmal.
Das <script>-Tag trägt die Nonce der Seite und läuft deshalb auch unter einer
durchgesetzten Script-Policy (siehe CMS:CSP).
Parameter im Einzelnen
set
set gibt nichts aus, sondern legt den Script-Block unter $set['fed'] ab, etwa um
ihn an einer anderen Stelle auszugeben:
(CMS:FED_SCRIPT set=fed)
<?php echo $set['fed']; ?>
CMS:USER_2FA
Zweiten Faktor des angemeldeten Frontend-Users anzeigen, einrichten (Authenticator-App oder Code per E-Mail) und abschalten.
(CMS:USER_2FA [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsTwoFactor()
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
function |
status / totp_setup / totp_enable / code_enable / disable / recovery_renew |
status |
Was der Befehl tut (siehe unten). | |
submit |
Textwert |
– | Schreibende Funktionen laufen nur, wenn in diesem Feld ein Text enthalten ist. | |
data |
PHP-Variablenname |
data |
Name des PHP-Arrays mit den Eingaben. | |
codefield |
Feldname |
code |
Eingabefeld für den App-Code in data[…]. |
|
currentfield |
Feldname |
current_password |
Eingabefeld für das Passwort in data[…]. |
|
message[…] |
Standardmeldung |
– | Fehlermeldungen. Im Index: Anwendungsfall; im Wert: Meldungstext. | |
cookiename |
Cookiename |
usess |
Name des Session-Cookies [a-z0-9]. | |
csrf |
true / false |
true |
true: Schreibende Funktionen verlangen ein Token aus CMS:USER_TOKEN. |
Anwendung
Der Befehl gehört auf eine geschützte Seite des Kundenkontos (nach
CMS:USER_PROTECT). Er wirkt nur, wenn der Profil-Bereich einen zweiten
Faktor zulässt (Einstellung „Zweiter Faktor“ nicht „aus“, sonst Meldung policy).
function |
Wirkung | Bestätigt mit |
|---|---|---|
status |
nur lesen | – |
totp_setup |
neuen Schlüssel für die Authenticator-App erzeugen – gespeichert wird noch nichts: $set['totp_setup'] = token, qr (fertiges HTML mit QR-Code), secret (zum Abtippen, in Vierergruppen), uri (otpauth://…, öffnet auf dem Telefon direkt die App), qr_script (Adresse der QR-Bibliothek) |
– |
totp_enable |
Schlüssel speichern, zehn Wiederherstellungscodes erzeugen ($set['recovery_codes'], nur jetzt im Klartext) |
data[token] und dem ersten Code aus der App (data[code]) |
code_enable |
Code per E-Mail bei jeder Anmeldung | Passwort (data[current_password]); ist eine App eingerichtet, dem App-Code |
disable |
abschalten – verlangt der Bereich immer einen zweiten Faktor, wird aus der App der Code per E-Mail | App-Code oder Wiederherstellungscode bzw. Passwort |
recovery_renew |
neue Wiederherstellungscodes, die alten verfallen | App-Code |
Nach jeder Funktion stehen in $set: second_factor ('', totp, email),
second_factor_policy (Einstellung des Bereichs: none, optional, new_device,
required), second_factor_since (UTC), recovery_count; nach einer Änderung
second_factor_changed (totp_enabled, totp_disabled, code_enabled,
code_disabled, recovery_renewed) – etwa für eine Hinweis-E-Mail an den Frontend-User.
Zum Einrichten scannt der Frontend-User den QR-Code mit der App; er enthält den Schlüssel
(nicht die Codes – die erzeugt die App danach selbst, je sechs Ziffern). Alternativ tippt
er den Schlüssel ab oder öffnet auf dem Telefon den Link. $set['totp_setup']['qr']
bringt alles mit: ein Element mit dem QR-Code, die Bibliothek qrcode-generator (dieselbe
wie im Backend, aus dessen Assets) und ein kleines Script, beide mit der Nonce der Seite –
es läuft also auch unter einer durchgesetzten Script-Policy. Geben Sie es nur aus
(echo), die Größe steuern Sie per CSS (.c9-qr { width: 200px }).
<?php
$isPost = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
$action = $isPost ? (string)($_POST['action'] ?? '') : (isset($_GET['setup']) ? 'totp_setup' : 'status');
$submit = $isPost ? '1' : '';
?>
(CMS:USER_PROTECT errortemplate=u_login)
(CMS:USER_2FA function="$action" submit="$submit")
<p class="error">(CMS:MESSAGE)</p>
<?php if (!empty($set['recovery_codes'])) { ?>
<p>Ihre Wiederherstellungscodes (bitte sicher aufbewahren):</p>
<ul><?php foreach ($set['recovery_codes'] as $c) echo '<li>' . htmlspecialchars($c) . '</li>'; ?></ul>
<?php } ?>
<?php if (!empty($set['totp_setup'])) { $s = $set['totp_setup']; ?>
<?php echo $s['qr']; ?>
<p>Schlüssel für Ihre Authenticator-App: <code><?php echo htmlspecialchars($s['secret']); ?></code>
– oder <a href="<?php echo htmlspecialchars($s['uri']); ?>">in der App öffnen</a>.</p>
<form method="post">
(CMS:USER_TOKEN)
<input type="hidden" name="action" value="totp_enable">
<input type="hidden" name="data[token]" value="<?php echo htmlspecialchars($s['token']); ?>">
<input name="data[code]" autocomplete="one-time-code" required>
<button>Einschalten</button>
</form>
<?php } elseif ($set['second_factor'] === '') { ?>
<p><a href="?setup=1">Authenticator-App einrichten</a></p>
<?php } ?>
Meldungs-Indizes: auth (nicht angemeldet), policy, code (App-Code falsch),
current (Passwort falsch), setup (Einrichtung abgelaufen – 15 Minuten), blocked
(zu viele Fehlversuche), token. Falsche Codes und Passwörter zählen wie bei der
Anmeldung.
Hat ein Frontend-User Telefon und Wiederherstellungscodes verloren, setzt der Betreiber den zweiten Faktor im Backend zurück (Datensatz des Profils, Menü „Zweiter Faktor“).
Parameter im Einzelnen
function
(CMS:USER_2FA function=status)
(CMS:USER_2FA function="$action" submit="$submit")
submit
Nur schreibende Funktionen brauchen ihn – sie gehören in ein POST-Formular:
<?php $submit = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' ? '1' : ''; ?>
data, codefield, currentfield
(CMS:USER_2FA function=disable submit="$submit" data=sec codefield=otp currentfield=pw)
message
(CMS:USER_2FA function="$action" submit="$submit" message[code]="Der Code stimmt nicht.")
CMS:USER_AUTH
Prüft eine Session-ID auf Gültigkeit.
(CMS:USER_AUTH [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsAuth()
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
sessionid |
Session-ID |
– | Session des Frontend-Users anhand der angegebenen sessionid wieder aufnehmen anstatt über ein Cookie. | |
filter[…][…] |
vgl. Parameter "filter[]" von CMS:SELECT |
– | Filterkriterien für den Profil-Datensatz, die beim angemeldeten Frontend-User erfüllt sein müssen. | |
mode[…] |
AND / OR |
OR |
Modus, mit dem Werte verknüpft werden, die dasselbe Feld betreffen. | |
cookiename |
Cookiename |
usess |
Name des Session-Cookies [a-z0-9]. |
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Nimmt die Frontend-Session auf und stellt den angemeldeten Frontend-User bereit, ohne die
Seite zu sperren - für Seiten, die angemeldeten und anonymen Besuchern
unterschiedliche Inhalte zeigen. Ergebnis in $set:
$set[…] |
Inhalt |
|---|---|
user_id |
record_id des Profils, 0 = nicht angemeldet |
user |
der Profil-Datensatz als Array ([] wenn nicht angemeldet) |
user_session_id |
Session-Id, '' = keine Session |
password_reset |
1 nach einer Anmeldung über den Link aus CMS:USER_PW, solange ein neues Passwort ohne das bisherige gesetzt werden darf, sonst 0 |
(CMS:USER_AUTH)
<?php if ($set['user_id']) { ?>
Hallo <?php echo htmlspecialchars($set['user']['nickname']); ?> – <a href="/0,u_logout">abmelden</a>
<?php } else { ?>
<a href="/0,u_login">anmelden</a>
<?php } ?>
Der Befehl gehört an den Anfang des Templates (vor jeder Ausgabe), damit das
Session-Cookie gesetzt werden kann. Für Seiten, die nur angemeldete Frontend-User sehen dürfen,
ist CMS:USER_PROTECT der passende Befehl.
Parameter im Einzelnen
sessionid
Session per Parameter statt Cookie aufnehmen - für Aufrufe aus Apps oder von anderen Domains:
(CMS:USER_AUTH sessionid="{$_GET['sid']}")
filter
Zusätzliche Bedingung an das Profil, Syntax wie CMS:SELECT. Trifft sie nicht zu, gilt der Frontend-User als nicht angemeldet:
(CMS:USER_AUTH filter[role]=editor)
(CMS:USER_AUTH filter[status_id]=0 filter[newsletter]=1)
mode
Verknüpfung mehrerer Werte desselben Feldes in filter (Standard OR):
(CMS:USER_AUTH filter[role]=editor filter[role]=admin mode[role]=OR)
cookiename
Name des Session-Cookies (Standard usess), etwa für zwei getrennte Anmeldungen auf einer Domain:
(CMS:USER_AUTH cookiename=shop)
Muss bei allen USER_*-Befehlen derselbe sein.
CMS:USER_BASKET
Bearbeiten eines Warenkorbs.
(CMS:USER_BASKET function [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsBasket()
Ohne Schlüssel: der erste Wert ohne = gilt als function
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
function ¹ |
"load" / "add" / "update" / "update_addon" |
"load" |
ja | Funktion, die ausgeführt werden soll. |
overwrite |
false / true |
false |
function=add + overwrite=true: Die Anzahl der bereits gespeicherten Artikel wird beim Hinzufügen aktualisiert. | |
category |
Kategorie-ID |
0 |
Warenkorb-Kategorie. | |
container |
Container-IDs oder -Namen, kommasepariert |
– | Nur Datensätze aus diesen Containern kommen in den Warenkorb. Ohne Angabe: veröffentlichte Datensätze aus Containern vom Typ "record" oder "ugc" (nie Profile). | |
variable |
Variablenname |
– | Name der Variable, in der Artikel-IDs als Array gespeichert sind. | |
duration |
Anzahl Tage |
30 |
Anzahl der Tage, die der Warenkorb gespeichert bleibt, sofern keine Änderungen durchgeführt werden. | |
basket_addon |
Text |
– | Text (serialisierte Daten), die gespeichert werden sollen. | |
csrf |
true / false |
true |
true: Der Request muss ein gültiges Token aus CMS:USER_TOKEN enthalten (Schutz gegen CSRF). false schaltet die Prüfung für diesen Befehl ab – nur mit gutem Grund. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Warenkorb je Frontend-Session - funktioniert auch ohne Anmeldung (anonyme Session).
Ergebnis in $set:
$set[…] |
Inhalt |
|---|---|
basket |
record_id => Menge |
basket_count |
Summe der Mengen |
basket_addon |
mit update_addon gespeicherter Text der Kategorie |
Artikel hinzufügen (Formular mit items[]), Warenkorb anzeigen:
<?php $f = $_POST['f'] ?? 'load'; ?>
(CMS:USER_BASKET function="$f" variable=items)
(CMS:USER_BASKET load)
<h2>Warenkorb (<?php echo (int)$set['basket_count']; ?> Stück)</h2>
<form method="post"><input type="hidden" name="f" value="update">
(CMS:USER_TOKEN)
(CMS:RESULT_START)
<p>(CMS:TEXT title): <input name="items[(CMS:NUMBER record_id)]" value="<?php echo (int)$set['basket'][$cms['record_id']]; ?>" size="3"></p>
(CMS:RESULT_END)
<button>Mengen aktualisieren (0 = entfernen)</button>
</form>
Nur nach load stehen die Artikel als Ergebnismenge bereit - CMS:RESULT_START läuft
über sie wie nach einem CMS:SELECT. Nach add, update und update_addon ist $set
aktuell, für die Liste folgt deshalb ein zweiter Aufruf mit load (Beispiel oben).
Ohne gültiges Token aus CMS:USER_TOKEN ändert eine schreibende Funktion nichts. Ein „In den Warenkorb“-Formular in einer
Artikelliste:
<form method="post" action="/0,u_basket">
(CMS:USER_TOKEN)
<input type="hidden" name="f" value="add">
<button name="items[]" value="(CMS:TEXT record_id)">In den Warenkorb</button>
</form>
Parameter im Einzelnen
function
Darf ohne Schlüssel stehen:
(CMS:USER_BASKET load) Inhalt lesen, Artikel als Ergebnismenge
(CMS:USER_BASKET add variable=items) Ids aus $_REQUEST['items'] hinzufügen
(CMS:USER_BASKET update variable=items) Mengen setzen (0 entfernt)
(CMS:USER_BASKET update_addon basket_addon="{$note}")
overwrite
Bei add: Menge setzen statt addieren:
(CMS:USER_BASKET add variable=items overwrite=true)
Mit items[5]=2 liegt Artikel 5 danach genau 2-mal im Korb, auch wenn er schon drin war.
category
Mehrere Warenkörbe je Session, z. B. Merkliste neben dem Einkaufskorb:
(CMS:USER_BASKET add variable=items category=1)
(CMS:USER_BASKET load category=1)
container
Beschränkt den Warenkorb auf bestimmte Inhaltsbereiche (Namen oder Ids,
kommasepariert). Ohne Angabe sind veröffentlichte Datensätze aus Bereichen vom Typ
„record“ oder „ugc“ erlaubt, nie Profile. Die Prüfung gilt auch beim Lesen: Artikel
außerhalb der Bereiche erscheinen weder in $set['basket'] noch in der Ergebnismenge.
(CMS:USER_BASKET add variable=items container=produkte,zubehoer)
(CMS:USER_BASKET load container=produkte,zubehoer)
variable
Name des Request-Parameters mit den Artikel-Ids - als Liste (items[]=5, Menge 1 je Nennung) oder mit Mengen (items[5]=2):
<input type="hidden" name="cart[<?php echo $id; ?>]" value="1">
(CMS:USER_BASKET add variable=cart)
duration
Tage, die der Warenkorb ohne Änderung erhalten bleibt (Standard 30):
(CMS:USER_BASKET add variable=items duration=7)
basket_addon
Freitext zur Kategorie ablegen (Lieferhinweis, Gutscheincode, serialisierte Daten):
(CMS:USER_BASKET update_addon basket_addon="{$_POST['note']}")
<?php echo htmlspecialchars($set['basket_addon']); ?>
CMS:USER_DELETE
Löscht einen Datensatz, der dem angemeldeten Frontend-User gehört.
(CMS:USER_DELETE id [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsDelete()
Ohne Schlüssel: der erste Wert ohne = gilt als id
PHP-Schnipsel: include_and_stop (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
id ¹ |
Datensatz-ID |
– | ja | Datensatz, der gelöscht werden soll. |
ignoreuser |
true / false |
false |
true: Der zu löschende Datensatz muss nicht dem angemeldeten Frontend-User gehören (Achtung: Sicherheitshinweise beachten!). | |
successtemplate |
Templatename |
– | ja | Template, das eingebunden wird, wenn der Eintrag erfolgreich gelöscht wurde. |
message_error |
Text |
Zugriff verweigert! |
Fehlermeldung, falls der Benutzer keinen Zugriff auf den Datensatz hat. | |
csrf |
true / false |
true |
true: Der Request muss ein gültiges Token aus CMS:USER_TOKEN enthalten (Schutz gegen CSRF). false schaltet die Prüfung für diesen Befehl ab – nur mit gutem Grund. |
¹ kann ohne Schlüssel als erster Wert angegeben werden.
Anwendung
Löscht einen Datensatz, der dem angemeldeten Frontend-User gehört (record.user_id) - oder
das eigene Profil, dann werden alle Sessions und bekannten Geräte des Users entfernt.
Was „löschen“ heißt, bestimmt die Lösch-Policy des Bereichs mit ihren Blöcken für den
Auslöser „Datensatz wird gelöscht“ – derselbe wie beim Löschen im Backend: in den
Papierkorb, endgültig, einzelne Felder überschreiben (etwa Status auf „deaktiviert“ und
Anmeldename user_{record_id}), auch zeitversetzt. Löscht der User sein Profil, werden
zugleich alle seine Beiträge nach deren Policy gelöscht (Auslöser „UGC: Profil wurde
gelöscht“). Hat die Policy keinen Block für diesen Auslöser, gibt der Befehl eine
Fehlermeldung aus. Siehe Lösch-Policies.
Ein schon zum Löschen vorgemerkter Datensatz kann nicht erneut gelöscht werden.
<?php $id = (int)($_POST['id'] ?? 0); if ($id > 0) { ?>
(CMS:USER_DELETE id="$id" successtemplate=u_deleted message_error="Dieser Eintrag gehört Ihnen nicht.")
<?php } ?>
(CMS:MESSAGE)
<form method="post">
(CMS:USER_TOKEN)
<input type="hidden" name="id" value="<?php echo (int)$_GET['id']; ?>">
<button>Eintrag endgültig löschen</button>
</form>
Der Befehl kennt kein submit: Er läuft bei jedem Aufruf und meldet ohne gültige id
einen Fehler - deshalb die PHP-Bedingung um den Befehl (CMS-Befehle werden zu
PHP-Code, ein umgebendes if ist erlaubt). Nach Erfolg wird successtemplate
eingebunden und die Seite beendet (außer mit CMS:CONTINUE).
Parameter im Einzelnen
id
Datensatz-Id; darf ohne Schlüssel stehen. Ohne gültige Id erscheint die Meldung aus
message_error:
(CMS:USER_DELETE "{$_POST['id']}" successtemplate=u_deleted)
(CMS:USER_DELETE id="{$set['user_id']}" successtemplate=u_account_closed) eigenes Profil löschen
ignoreuser
Besitzprüfung und Anmeldung übergehen - das Template trägt dann die Verantwortung (z. B. Löschen per geheimem Link):
(CMS:USER_DELETE id="{$id}" ignoreuser=true successtemplate=u_deleted)
successtemplate
(CMS:USER_DELETE id="$id" successtemplate=u_deleted)
u_deleted kann z. B. „Eintrag gelöscht“ ausgeben und per CMS:INCLUDE die Navigation zeigen.
message_error
Text, wenn der Datensatz nicht existiert, nicht dem Frontend-User gehört oder niemand angemeldet ist (Ausgabe über CMS:MESSAGE). Fehlt das Token aus CMS:USER_TOKEN, erscheint stattdessen die Standardmeldung für ein abgelaufenes Formular:
(CMS:USER_DELETE id="$id" successtemplate=u_deleted message_error="Löschen nicht möglich.")
CMS:USER_INPUT
Erzeugt oder aktualisiert einen Datensatz aus Daten, die ein Frontend-User angegeben hat.
(CMS:USER_INPUT [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsInput()
PHP-Schnipsel: include_and_stop (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
successtemplate |
Templatename |
– | Template, das eingebunden wird, wenn der Eintrag erfolgreich gespeichert wurde. | |
data |
PHP-Variablenname |
data |
Name des PHP-Arrays, das die zu speichernden Daten enthält. | |
overwrite[…] |
Schlüssel-Wert-Paare |
– | Explizites Überschreiben der Eingaben des Frontend-Users (auch Metafelder, außer record_id, area_id, instance_id, tmp, deleted, origin_id, owner_id, created_by, changed_by). Hier keine Eingaben des Frontend-Users weiterreichen! | |
container |
Container-ID oder -Name |
– | ja | ID oder Name des Content-Containers, in dem der neue Datensatz gespeichert werden soll. |
message[…] |
Text |
Standardtext |
Fehlermeldung abhängig vom Index (siehe Tabelle). | |
submit |
Textwert |
– | ja | Der Befehl wird nur ausgeführt, wenn in diesem Feld ein Text enthalten ist. |
fields |
kommaseparierte Liste |
– | Namen der Eingabefelder, die geschrieben werden dürfen. Leer = alle definierten Felder des Containers. | |
id |
Datensatz-ID |
– | Falls angegeben, wird versucht diesen Datensatz zu aktualisieren (UPDATE). Andernfalls wird ein neuer erstellt (CREATE). | |
ignoreuser |
true / false |
false |
true: Der anzupassende Datensatz muss nicht dem angemeldeten Frontend-User gehören (Achtung: Sicherheitshinweise beachten!). | |
forceupdate |
true / false |
false |
true: Es wird erzwungen, den über Parameter "id" angegebenen Datensatz zu aktualisieren. Wirft im Fehlerfall eine Meldung. | |
masterid |
Datensatz-ID |
– | Nur beim Anlegen in Bereichen mit Baumstruktur: Elternknoten des neuen Datensatzes (parent_id). | |
owner |
Benutzer-ID |
– | Nur beim Anlegen: admin_id des Backend-Owners (record.owner_id, created_by). Ohne Angabe gilt user.default_owner_id. | |
removefiles |
kommaseparierte Liste |
– | Namen der Upload-Felder, deren Inhalt gelöscht werden soll. | |
currentfield |
Feldname |
current_password |
Nur Profil-Container: Name des Formularfeldes data[…] mit dem bisherigen Passwort. Ein neues Passwort wird nur mit dem bisherigen gespeichert – außer direkt nach der Anmeldung über den Reset-Link. Meldung current. |
|
csrf |
true / false |
true |
true: Der Request muss ein gültiges Token aus CMS:USER_TOKEN enthalten (Schutz gegen CSRF). false schaltet die Prüfung für diesen Befehl ab – nur mit gutem Grund. |
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Speichert Formulareingaben als Datensatz - neu anlegen oder (mit id) einen eigenen
Datensatz ändern. Die Felder heißen data[feldname], genau wie in der
Backend-Eingabemaske; Uploads laufen über dasselbe Formular. Nach Erfolg steht
$set['record_id'], das successtemplate wird eingebunden und die Seite beendet:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_INPUT container=comments submit="$submit" successtemplate=u_saved)
(CMS:USER_AUTH)
<p class="error">(CMS:MESSAGE)</p>
<form method="post" enctype="multipart/form-data">
(CMS:USER_TOKEN)
<textarea name="data[comment]">(CMS:TEXT useval="{$_POST['data']['comment']}" specialchars=true lbrconvert=false)</textarea>
<input type="file" name="data[photo]">
<button name="submit" value="1">Speichern</button>
</form>
Eigenen Datensatz ändern:
<?php $submit = $_POST['submit'] ?? ''; $id = (int)($_POST['id'] ?? 0); ?>
(CMS:USER_INPUT container=comments id="$id" forceupdate=true submit="$submit" successtemplate=u_saved)
Kontaktformular ohne Anmeldung (ignoreuser), Absender-IP in ein eigenes Feld
remote_ip des Bereichs schreiben:
(CMS:USER_INPUT container=anfragen ignoreuser=true submit="$submit" fields="name,email,message"
overwrite[remote_ip]="{$_SERVER['REMOTE_ADDR']}" successtemplate=anfrage_danke)
Wer schreiben darf: In ugc-Bereichen angemeldete Frontend-User, deren Profil-Bereich beim
Bereich freigeschaltet ist; in profile-Bereichen nur das eigene Profil (mit id, ein
neues Profil legt nur CMS:USER_REGISTER an); in gewöhnliche Datensatzbereiche nur mit
ignoreuser=true. Alle Prüfungen
des Bereichs (Pflichtfelder, eindeutige Werte, Dateitypen) gelten wie im Backend;
Fehler erscheinen über CMS:MESSAGE. Meldungs-Indizes für message[…]: container,
access, notfound, save, current, blocked, owner (kein gültiger Owner beim
Anlegen), token.
Passwort ändern
Ein neues Passwort im eigenen Profil speichert der Befehl nur zusammen mit dem
bisherigen Passwort im Feld data[current_password] – eine fremde Person, die an
eine offene Sitzung gelangt, kann das Konto so nicht übernehmen. Ausnahme: direkt nach
der Anmeldung über den Link aus CMS:USER_PW; dann steht
$set['password_reset'] auf 1, und das Feld entfällt (eine Stunde lang, bis das neue
Passwort gespeichert ist). Nach dem Speichern werden alle anderen Anmeldungen des
Profils beendet, und $set['password_changed'] ist 1 – das successtemplate kann den
Inhaber per CMS:EMAIL benachrichtigen.
(CMS:USER_PROTECT errortemplate=u_denied)
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_INPUT container=members id="{$set['user_id']}" forceupdate=true fields=password
submit="$submit" successtemplate=u_pw_saved)
<p class="error">(CMS:MESSAGE)</p>
<form method="post">
(CMS:USER_TOKEN)
<?php if (empty($set['password_reset'])) { ?>
<input type="password" name="data[current_password]" autocomplete="current-password">
<?php } ?>
<input type="password" name="data[password]" autocomplete="new-password"
data-c9-password="(CMS:USER_PASSWORD_RULES container=members)">
<button name="submit" value="1">Speichern</button>
</form>
Trägt das Passwort-Feld im Bereich die Extension Passwort-Richtlinie, prüft der Server Mindestlänge und Zeichenklassen; mit CMS:USER_PASSWORD_RULES zeigt das Formular dieselben Regeln schon beim Tippen an.
Parameter im Einzelnen
successtemplate
Nach dem Speichern eingebunden; darin ist der gespeicherte Datensatz der laufende ($cms), $set['record_id'] seine Id:
(CMS:USER_INPUT container=comments submit="$submit" successtemplate=u_saved)
Ohne successtemplate läuft das Template nach dem Speichern normal weiter.
data
Name des Request-Arrays (Standard data):
<input name="form[comment]">
(CMS:USER_INPUT container=comments data=form submit="$submit")
overwrite
Werte fest vorgeben - auch Metafelder - unabhängig davon, was der Frontend-User schickt. Keine Eingaben des Frontend-Users hier einsetzen. Ausgenommen sind record_id, area_id, instance_id, tmp, deleted, origin_id, owner_id, created_by und changed_by; ohne Angabe setzt der Befehl needs_review auf 1:
(CMS:USER_INPUT container=comments submit="$submit"
overwrite[article_id]="{$cms['record_id']}"
overwrite[status_id]=2
)
container
Bereich (Name oder Id), in dem gespeichert wird - Typ ugc oder profile, mit
ignoreuser=true auch ein gewöhnlicher Datensatzbereich:
(CMS:USER_INPUT container=comments …)
(CMS:USER_INPUT container=58 …)
message
Eigene Meldungstexte:
(CMS:USER_INPUT container=comments submit="$submit"
message[access]="Bitte melden Sie sich an, um zu kommentieren."
message[save]="Der Kommentar konnte nicht gespeichert werden."
)
submit
Der Befehl läuft nur, wenn submit einen Wert hat - beim ersten Aufruf des Formulars also nicht:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_INPUT container=comments submit="$submit" …)
<button name="submit" value="1">Speichern</button>
fields
Nur diese Felder aus den Eingaben übernehmen - alles andere im Formular wird ignoriert:
(CMS:USER_INPUT container=members id="{$set['user_id']}" fields="nickname,avatar" submit="$submit")
id
Vorhandenen Datensatz ändern; ohne forceupdate wird bei unbekannter/fremder Id ein neuer angelegt:
(CMS:USER_INPUT container=comments id="{$_POST['id']}" submit="$submit")
ignoreuser
Keine Anmeldung und keine Besitzprüfung - für Kontakt- und Gästebuchformulare. Das Template trägt die Verantwortung (Spam-Schutz, Feldliste per fields):
(CMS:USER_INPUT container=gaestebuch ignoreuser=true fields="name,message" submit="$submit" successtemplate=gb_danke)
forceupdate
Mit id: Statt bei fremder/unbekannter Id einen neuen Datensatz anzulegen, Meldung notfound ausgeben:
(CMS:USER_INPUT container=comments id="$id" forceupdate=true submit="$submit")
masterid
Elternknoten in einem Bereich mit Baumstruktur (wird zu parent_id):
(CMS:USER_INPUT container=forum masterid="{$cms['record_id']}" submit="$submit") Antwort unter dem aktuellen Beitrag
currentfield
Anderer Name für das Feld mit dem bisherigen Passwort (siehe Passwort ändern):
(CMS:USER_INPUT container=members id="{$set['user_id']}" fields=password currentfield=old_pw …)
<input type="password" name="data[old_pw]">
removefiles
Upload-Felder leeren - z. B. über eine Checkbox „Foto entfernen“:
<label><input type="checkbox" name="remove_photo" value="photo"> Foto entfernen</label>
(CMS:USER_INPUT container=comments id="$id" removefiles="{$_POST['remove_photo']}" submit="$submit")
CMS:USER_LOGIN
Überprüft die Anmeldedaten eines Frontend-Users und erzeugt eine neue Session.
(CMS:USER_LOGIN [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsLogin()
PHP-Schnipsel: include_and_stop (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
successtemplate |
– | – | ja | Detailtemplate, das nach einer erfolgreichen Anmeldung eingebunden wird. |
container |
Container-ID oder -Name |
– | ja | ID oder Name des Profil-Containers, in dem die Daten der Frontend-User gespeichert sind. |
data |
PHP-Variablenname |
data |
Name des PHP-Arrays, in dem die Anmeldedaten enthalten sind. | |
message[…] |
Standardmeldung |
– | Fehlermeldungen. Im Index: Anwendungsfall; im Wert: Meldungstext (vgl. Tabelle). | |
hash |
Hash |
– | Ein Login kann auch mittels Hash durchgeführt werden (Token aus CMS:USER_PW, Passwort vergessen). | |
trim |
true / false |
false |
true: Leerzeichen am Anfang und Ende der Eingaben entfernen. | |
cookie |
true / false |
true |
true: Es wird standardmäßig ein Cookie gesetzt, um die Anmeldung über mehrere Seiten hinweg aufrechtzuerhalten. | |
submit |
Textwert |
– | Der Befehl wird nur ausgeführt, wenn in diesem Feld ein Text enthalten ist. Entfällt bei der Anmeldung per "hash". | |
cookiename |
Cookiename |
usess |
Name des Session-Cookies [a-z0-9]. | |
csrf |
true / false |
true |
true: Der Request muss ein gültiges Token aus CMS:USER_TOKEN enthalten (Schutz gegen CSRF). false schaltet die Prüfung für diesen Befehl ab – nur mit gutem Grund. | |
codetemplate |
– | – | Template für den zweiten Schritt der Anmeldung, wenn der Profil-Bereich einen zweiten Faktor verlangt. Pflicht, sobald der Bereich einen einsetzt. |
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Prüft Anmeldename und Passwort gegen den Profil-Bereich und legt eine Session an
(Cookie). Die Felder sind die im Bereich als Anmeldename und Passwort festgelegten
(data[nickname], data[password]); nach Erfolg wird successtemplate eingebunden,
darin ist das Profil der laufende Datensatz und $set['user_id'] / $set['user']
sind gefüllt:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=u_welcome)
<h1>Anmeldung</h1>
<p class="error">(CMS:MESSAGE)</p>
<form method="post">
(CMS:USER_TOKEN)
<input name="data[nickname]" value="(CMS:TEXT useval="{$_POST['data']['nickname']}" specialchars=true)">
<input type="password" name="data[password]">
<button name="submit" value="1">Anmelden</button>
</form>
Anmeldung über den Link aus CMS:USER_PW (Passwort vergessen):
(CMS:USER_LOGIN container=members hash="{$_GET['hash']}" successtemplate=u_newpw)
Der Befehl gehört an den Anfang des Templates (Cookie vor der Ausgabe). Fehlversuche
werden gezählt (Brute-Force-Schutz). Meldungs-Indizes: failed, blocked, disabled
(Status 1), unconfirmed (Status 2), hash, container, token. Gezählt wird je Anmeldename und
zusätzlich je Herkunft (IP-Adresse): wer von einem Anschluss aus viele Konten mit je einem
Passwort durchprobiert, wird ebenfalls gesperrt (blocked).
Zweiter Faktor
Im Profil-Bereich legt der Administrator fest, ob eine Anmeldung in zwei Schritten
nötig ist (Einstellung „Zweiter Faktor“: aus, freiwillig, Code bei neuem Gerät, immer).
Braucht eine Anmeldung einen Code, meldet USER_LOGIN nach dem richtigen Passwort noch
niemanden an, sondern bindet codetemplate ein. Dort stehen:
$set[…] |
Inhalt |
|---|---|
second_factor |
totp (Code aus der Authenticator-App) oder email (Code per E-Mail) |
login_pending |
1 |
pending_user |
das Profil (auch der laufende Datensatz) – etwa für die E-Mail-Adresse |
login_code |
beim Code per E-Mail: der sechsstellige Code, den Sie verschicken |
Das codetemplate verschickt den Code mit CMS:EMAIL und zeigt das Formular
für CMS:USER_LOGIN_CODE (Beispiel dort). Wie alle CMS:USER_*-Befehle
verschickt USER_LOGIN selbst keine E-Mail – das tut immer das Template. Auch die Anmeldung über den Link aus
„Passwort vergessen“ (hash) verlangt den Code, wenn der Frontend-User eine Authenticator-App
eingerichtet hat; die Einrichtung selbst übernimmt CMS:USER_2FA.
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=u_welcome codetemplate=u_code_mail)
Anmeldung von einem neuen Gerät
camindo merkt sich die Geräte (Browser), mit denen sich ein Profil anmeldet. Kommt die
Anmeldung von einem unbekannten Gerät und hat das Profil schon andere, steht im
successtemplate $set['new_device'] auf 1; $set['device'] enthält label
(z. B. „Firefox / macOS“), ip und time (UTC). Den Hinweis an den Inhaber schicken
Sie selbst – vor der Weiterleitung, mit CMS:EMAIL:
<?php
$to = (string)($set['user']['email'] ?? '');
$send = (!empty($set['new_device']) && $to !== '') ? '1' : '';
$body = "Neue Anmeldung: {$set['device']['label']}, IP {$set['device']['ip']}, {$set['device']['time']} UTC."
. " Waren Sie das nicht, setzen Sie bitte ein neues Passwort.";
?>
(CMS:EMAIL submit="$send" to="$to" subject="Neue Anmeldung" body="$body")
Die allererste Anmeldung eines Profils löst keinen Hinweis aus. Ob stattdessen eine SMS geht, legt der Systemadministrator fest (siehe Handbuch für Administratoren, Betrieb).
Parameter im Einzelnen
successtemplate
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=u_welcome)
In u_welcome: Hallo (CMS:TEXT nickname) - das Profil ist dort der laufende Datensatz.
container
Profil-Bereich (Name oder Id):
(CMS:USER_LOGIN container=members …)
data
Name des Request-Arrays mit den Anmeldedaten (Standard data):
<input name="login[nickname]"> <input type="password" name="login[password]">
(CMS:USER_LOGIN container=members data=login submit="$submit" successtemplate=u_welcome)
message
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=u_welcome
message[failed]="Name oder Passwort falsch."
message[unconfirmed]="Bitte bestätigen Sie zuerst Ihre E-Mail-Adresse."
message[blocked]="Zu viele Fehlversuche – bitte später erneut versuchen."
)
hash
Anmeldung per Einmal-Token aus CMS:USER_PW statt mit Passwort; submit entfällt:
(CMS:USER_LOGIN container=members hash="{$_GET['hash']}" successtemplate=u_newpw)
In u_newpw setzt der Frontend-User per CMS:USER_INPUT container=members id="{$set['user_id']}" fields=password ein neues Passwort.
Nach dieser Anmeldung ist $set['password_reset'] 1: eine Stunde lang (Einstellung
user.password_token_expiration) braucht er dafür das bisherige Passwort nicht – siehe
CMS:USER_INPUT, Passwort ändern.
codetemplate
Template für den zweiten Schritt (siehe oben):
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=u_welcome codetemplate=u_code_mail)
(CMS:USER_LOGIN container=members hash="{$_GET['hash']}" successtemplate=u_newpw codetemplate=u_code_mail)
Fehlt es, obwohl der Bereich einen zweiten Faktor verlangt, scheitert die Anmeldung mit
failed und einem Eintrag im Protokoll user.
trim
Leerzeichen um Name und Passwort entfernen (Kopierfehler auf Mobilgeräten):
(CMS:USER_LOGIN container=members trim=true submit="$submit" successtemplate=u_welcome)
cookie
Kein Cookie setzen - die Session-Id steht dann nur in $set['user_session_id'] (für Apps, die sie selbst weiterreichen):
(CMS:USER_LOGIN container=members cookie=false submit="$submit" successtemplate=api_token)
submit
Nur ausführen, wenn das Formular abgeschickt wurde:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=u_welcome)
cookiename
(CMS:USER_LOGIN container=members cookiename=shop submit="$submit" successtemplate=u_welcome)
Muss bei allen USER_*-Befehlen gleich sein.
CMS:USER_LOGIN_CODE
Zweiter Schritt der Anmeldung: prüft den Code aus der Authenticator-App, per E-Mail oder einen Wiederherstellungscode und meldet den Frontend-User an.
(CMS:USER_LOGIN_CODE [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsLoginCode()
PHP-Schnipsel: include_and_stop (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
successtemplate |
– | – | ja | Detailtemplate, das nach der erfolgreichen Anmeldung eingebunden wird. |
submit |
Textwert |
– | Der Code wird nur geprüft, wenn in diesem Feld ein Text enthalten ist. | |
resend |
true / false |
false |
true: neuen Code per E-Mail erzeugen (gedrosselt) und codetemplate einbinden. |
|
codetemplate |
– | – | Template, das nach resend den neuen Code mit CMS:EMAIL verschickt – dasselbe wie bei CMS:USER_LOGIN. |
|
data |
PHP-Variablenname |
data |
Name des PHP-Arrays mit den Eingaben. | |
codefield |
Feldname |
code |
Eingabefeld für den Code in data[…]. |
|
message[…] |
Standardmeldung |
– | Fehlermeldungen. Im Index: Anwendungsfall; im Wert: Meldungstext. | |
sessionid |
Session-Id |
– | Wartende Anmeldung ohne Cookie (Apps) – $set['user_session_id'] nach CMS:USER_LOGIN. |
|
cookiename |
Cookiename |
usess |
Name des Session-Cookies [a-z0-9]. | |
csrf |
true / false |
true |
true: Der Request muss ein gültiges Token aus CMS:USER_TOKEN enthalten. |
Anwendung
Verlangt der Profil-Bereich einen zweiten Faktor, bindet
CMS:USER_LOGIN nach dem richtigen Passwort das codetemplate ein. Die
Anmeldung wartet dann in einer Session ohne Frontend-User; USER_LOGIN_CODE schließt sie ab.
Verteilen Sie die Arbeit auf zwei Templates:
u_code_mailverschickt den Code ($set['login_code']) mit CMS:EMAIL und bindet dann das Formular ein. Es ist dascodetemplatevonCMS:USER_LOGINund vonCMS:USER_LOGIN_CODE(für „Neuen Code senden“).u_codeenthältCMS:USER_LOGIN_CODEund das Formular; es ist das Ziel des Formulars.
Ohne submit und resend füllt der Befehl nur $set, und das Template läuft weiter.
Innerhalb eines Aufrufs wertet er submit/resend nur beim ersten Mal aus – wenn
u_code_mail nach dem Nachsenden wieder u_code einbindet, passiert nichts doppelt.
Geben Sie den Schaltflächen eigene Namen: das Anmeldeformular hat schon submit=1
mitgeschickt.
u_code_mail:
<?php
$to = (string)($set['pending_user']['email'] ?? '');
$send = (!empty($set['login_code']) && $to !== '') ? '1' : '';
$body = "Ihr Anmeldecode: {$set['login_code']} (gültig 10 Minuten)";
?>
(CMS:EMAIL submit="$send" to="$to" subject="Ihr Anmeldecode" body="$body")
(CMS:INCLUDE u_code)
u_code:
<?php
$isPost = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
$submit = ($isPost && !empty($_POST['code_submit'])) ? '1' : '';
$resend = ($isPost && !empty($_POST['code_resend'])) ? 'true' : 'false';
?>
(CMS:USER_LOGIN_CODE submit="$submit" resend="$resend" successtemplate=u_welcome codetemplate=u_code_mail)
<h1>Anmeldung bestätigen</h1>
<p class="error">(CMS:MESSAGE)</p>
<?php if (!empty($set['code_resent'])) { ?><p>Ein neuer Code ist unterwegs.</p><?php } ?>
<?php if (!empty($set['login_pending'])) { ?>
<form method="post" action="/0,u_code">
(CMS:USER_TOKEN)
<input name="data[code]" autocomplete="one-time-code" required>
<button name="code_submit" value="1">Bestätigen</button>
</form>
<?php if ($set['second_factor'] === 'email') { ?>
<form method="post" action="/0,u_code">
(CMS:USER_TOKEN)
<button name="code_resend" value="1">Neuen Code senden</button>
</form>
<?php } ?>
<?php } else { ?>
<p><a href="/0,u_login">Zur Anmeldung</a></p>
<?php } ?>
Für einen Code per SMS schreiben Sie in u_code_mail statt CMS:EMAIL den Aufruf Ihres
SMS-Dienstes – der Core bleibt davon unberührt.
Der Befehl gehört an den Anfang des Templates (Cookie vor der Ausgabe).
$set[…] |
Inhalt |
|---|---|
login_pending |
1, solange eine Anmeldung auf den Code wartet |
second_factor |
totp oder email |
pending_user |
das Profil der wartenden Anmeldung |
login_code |
nach resend: der neue Code (im codetemplate) |
code_resent |
1 nach resend |
| nach Erfolg | wie USER_LOGIN (user_id, user, new_device …), dazu second_factor_used (totp, email, recovery) und recovery_remaining |
Sicherheit: Ein Code per E-Mail gilt 10 Minuten (user.login_code_expiration).
Jeder falsche Code zählt wie ein falsches Passwort; nach drei falschen Codes verfällt die
wartende Anmeldung (Meldung expired) und der Frontend-User beginnt wieder mit dem Passwort.
Ein Wiederherstellungscode gilt nur einmal. „Neuen Code senden“ ist gedrosselt (drei je
10 Minuten).
Meldungs-Indizes: code (falsch), expired (abgelaufen oder zu viele Fehlversuche),
blocked, throttled (zu oft neuer Code), token.
Parameter im Einzelnen
successtemplate
Wie bei USER_LOGIN – das Profil ist dort der laufende Datensatz:
(CMS:USER_LOGIN_CODE submit="$submit" successtemplate=u_welcome)
submit
<?php $submit = !empty($_POST['code_submit']) ? '1' : ''; ?>
(CMS:USER_LOGIN_CODE submit="$submit" successtemplate=u_welcome)
resend, codetemplate
<?php $resend = !empty($_POST['code_resend']) ? 'true' : 'false'; ?>
(CMS:USER_LOGIN_CODE resend="$resend" submit="$submit" successtemplate=u_welcome codetemplate=u_code_mail)
data, codefield
<input name="login[otp]">
(CMS:USER_LOGIN_CODE data=login codefield=otp submit="$submit" successtemplate=u_welcome)
message
(CMS:USER_LOGIN_CODE submit="$submit" successtemplate=u_welcome
message[code]="Der Code stimmt nicht."
message[expired]="Bitte melden Sie sich erneut an."
)
sessionid, cookiename
Wie bei CMS:USER_AUTH.
CMS:USER_LOGOUT
Beendet und löscht die Session eines angemeldeten Frontend-Users.
(CMS:USER_LOGOUT [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsLogout()
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
submit |
Textwert |
– | Ist der Parameter angegeben, meldet der Befehl nur ab, wenn er einen Text enthält (z. B. submit="$logout" aus dem Abmelde-Formular) – leer passiert nichts, auch keine Meldung token. Ohne den Parameter meldet der Befehl bei jedem Aufruf ab (gültiges Token vorausgesetzt). |
|
deletecookie |
true / false |
true |
false: Nur die serverseitige Session wird gelöscht. true: Auch der Browser-Cookie wird gelöscht. | |
cookiename |
Cookiename |
usess |
Name des Session-Cookies [a-z0-9]. | |
csrf |
true / false |
true |
true: Der Request muss ein gültiges Token aus CMS:USER_TOKEN enthalten (Schutz gegen CSRF). false schaltet die Prüfung für diesen Befehl ab – nur mit gutem Grund. |
Anwendung
Beendet die Session des angemeldeten Users (auch den Warenkorb der Session) und löscht
das Cookie. Danach ist $set['user_id'] = 0:
(CMS:USER_LOGOUT)
(CMS:INCLUDE header)
<p>Sie wurden abgemeldet. <a href="/0,u_login">Erneut anmelden</a></p>
Die Abmeldung wird per POST-Formular mit Token ausgelöst – ein einfacher Link genügt
nicht, sonst könnte jede fremde Seite Ihre Besucher abmelden. Ohne gültiges Token meldet
der Befehl nicht ab und legt die Meldung token ab (CMS:MESSAGE):
<form method="post" action="/0,u_logout">
(CMS:USER_TOKEN)
<button name="logout" value="1">Abmelden</button>
</form>
Damit der direkte Aufruf der Abmeldeseite keine Meldung token erzeugt, geben Sie
submit an – leer (die Seite wurde nur aufgerufen) tut der Befehl dann nichts:
<?php $logout = $_POST['logout'] ?? ''; ?>
(CMS:USER_LOGOUT submit="$logout")
Auch dieser Befehl gehört vor die erste Ausgabe, damit das Cookie gelöscht werden kann.
Parameter im Einzelnen
submit
Nur abmelden, wenn das Abmelde-Formular abgeschickt wurde:
<?php $logout = $_POST['logout'] ?? ''; ?>
(CMS:USER_LOGOUT submit="$logout")
deletecookie
Nur die serverseitige Session löschen, das Cookie stehen lassen:
(CMS:USER_LOGOUT deletecookie=false)
cookiename
(CMS:USER_LOGOUT cookiename=shop)
CMS:USER_PASSWORD_RULES
Gibt die Regeln der Passwort-Richtlinie eines Profil-Containers samt Texten aus – für die Anzeige im Formular (Stärke-Balken und Sprechblase mit den Anforderungen).
(CMS:USER_PASSWORD_RULES container=… [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsPasswordRules()
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
container |
Container-ID oder -Name |
– | ja | Profil-Container, dessen Passwort-Feld gemeint ist. |
field |
Feldname |
Anmelde-Passwortfeld | Passwort-Feld des Containers. Trägt es die Extension Passwort-Richtlinie, gelten deren Regeln, sonst die Vorgaben des Administrators (der Server prüft dann nicht). | |
set |
Array-Index von $set |
– | Die Ausgabe wird nicht ausgegeben, sondern in $set gespeichert. |
Die Ausgabe ist bereits mit htmlspecialchars für ein HTML-Attribut maskiert – nicht zusätzlich maskieren.
Anwendung
Beim Registrieren und beim Ändern des Passworts prüft der Server die Passwort- Richtlinie (Mindestlänge, Sonderzeichen, Groß- und Kleinbuchstaben), wenn der Administrator dem Passwort-Feld die Extension „Passwort-Richtlinie“ zugewiesen hat. Damit Ihre Besucher die Anforderungen schon beim Tippen sehen, gibt es dazu ein kleines Script – dasselbe, das im Backend läuft. Es zeigt unter dem Feld einen Balken (rot = schwach, orange = mittel, grün = stark) und eine Sprechblase, in der erfüllte Anforderungen abgehakt werden.
1. Script und CSS einbinden. Kopieren Sie die beiden Dateien der Extension
unverändert in Ihr Template – als <style> und <script(CMS:NONCE)> (die Nonce braucht
das Script unter einer Script-Policy, siehe CMS:NONCE), am besten in ein
eigenes Include, das Sie am Ende der Seite einbinden:
Core/Extensions/Password/public/css/password.ext.css → <style> … </style>
Core/Extensions/Password/public/js/password.ext.js → <script(CMS:NONCE)> … </script>
Die Dateien brauchen nichts vom Backend und keine Bibliothek. Nach einem Update von camindo CMS kopieren Sie sie neu, falls sich die Regeln geändert haben.
2. Das Feld markieren. Das Attribut data-c9-password bekommt die Regeln aus diesem
Befehl – die Texte stehen darin in der Sprache der Seite:
<input type="password" name="data[password]" autocomplete="new-password"
data-c9-password="(CMS:USER_PASSWORD_RULES container=members)">
(CMS:INCLUDE password_assets)
Das Script stattet beim Laden der Seite jedes Feld mit data-c9-password aus. Für
Felder, die erst später per Javascript entstehen: C9Password.attach(feld, optionen).
Wichtig: Die Anzeige ist eine Hilfe, keine Prüfung. Maßgeblich ist der Server – ohne die Extension am Feld nimmt er jedes Passwort an, auch wenn das Formular die Regeln zeigt. Die Fehlermeldung bei zu schwachem Passwort erscheint wie andere Feldfehler über CMS:MESSAGE.
Ein vollständiges Formular zum Ändern des Passworts steht bei CMS:USER_INPUT.
Parameter im Einzelnen
container
Name oder ID des Profil-Containers:
(CMS:USER_PASSWORD_RULES container=members)
field
Ein anderes Passwort-Feld als das zur Anmeldung festgelegte:
(CMS:USER_PASSWORD_RULES container=members field=pin)
set
Die Ausgabe nicht ausgeben, sondern in $set['pwrules'] ablegen (bereits für ein
HTML-Attribut maskiert):
(CMS:USER_PASSWORD_RULES container=members set=pwrules)
CMS:USER_REGISTER
Erzeugt einen Profil-Datensatz aus den angegebenen Daten und setzt den Status auf status=2.
(CMS:USER_REGISTER [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsRegister()
PHP-Schnipsel: include_and_stop (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
successtemplate |
– | – | ja | Detailtemplate, das nach erfolgreicher Registrierung eingebunden wird. |
container |
Container-ID oder -Name |
– | ja | ID oder Name des Profil-Containers, in dem die Daten der Frontend-User gespeichert werden sollen. |
emailfield |
Feldname |
– | ja | Feldname, in dem die E-Mail des Frontend-Users gespeichert ist (dorthin wird der Freischalt-Link gesendet). |
fields |
kommaseparierte Liste |
– | Namen der Eingabefelder, die geschrieben werden dürfen. Leer = alle definierten Felder des Containers. | |
overwrite[…] |
Schlüssel-Wert-Paare |
– | Explizites Überschreiben der Eingaben des Frontend-Users (auch Metafelder, außer record_id, area_id, instance_id, tmp, deleted, origin_id, owner_id, created_by, changed_by). Hier keine Eingaben des Frontend-Users weiterreichen! | |
message[…] |
Meldung |
– | Fehlermeldung. Im Index: Anwendungsfall; im Wert: Meldungstext (vgl. Tabelle bei CMS:USER_INPUT). | |
submit |
Textwert |
– | ja | Der Befehl wird nur ausgeführt, wenn in diesem Feld ein Text enthalten ist. |
owner |
Benutzer-ID |
– | admin_id des Backend-Owners des neuen Profils (record.owner_id, created_by). Ohne Angabe gilt user.default_owner_id. | |
data |
PHP-Variablenname |
data |
Name des PHP-Arrays, das die zu speichernden Daten enthält. | |
csrf |
true / false |
true |
true: Der Request muss ein gültiges Token aus CMS:USER_TOKEN enthalten (Schutz gegen CSRF). false schaltet die Prüfung für diesen Befehl ab – nur mit gutem Grund. | |
formguard |
true / false / Sekunden |
false |
true: Formularschutz gegen Robots – das Formular muss die Felder aus (CMS:USER_TOKEN formguard=true) enthalten und frühestens nach 3 Sekunden (user.formguard_min_seconds) abgeschickt werden. Eine Zahl setzt die Mindestzeit für dieses Formular (formguard=5), 0 schaltet ab. Sonst Meldung throttled. |
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Legt ein Profil an (Status 2 = unbestätigt) und erzeugt einen Freischalt-Token in
$set['hash']; das successtemplate verschickt den Bestätigungslink, dessen Zielseite
CMS:USER_REGISTER_CONFIRM aufruft. Anmeldename und E-Mail müssen im Bereich noch frei
sein, emailfield muss eine gültige Adresse enthalten. Anmeldenamen, die mit user_
beginnen, sind reserviert – so heißen anonymisierte Profile (user_{record_id}, siehe
Lösch-Policies); die Registrierung
lehnt sie ab:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_REGISTER container=members emailfield=kontakt_email fields="nickname,kontakt_email,password"
submit="$submit" successtemplate=u_registered)
<h1>Registrieren</h1>
<p class="error">(CMS:MESSAGE)</p>
<form method="post">
(CMS:USER_TOKEN)
<input name="data[nickname]"> <input name="data[kontakt_email]" type="email">
<input name="data[password]" type="password">
<button name="submit" value="1">Konto anlegen</button>
</form>
u_registered verschickt den Link mit CMS:EMAIL. Die Adresse steht dort
in $set['email'] – immer unter diesem Namen, gleich wie das Feld aus emailfield
heißt (hier kontakt_email). Unter dem Feldnamen findet sie sich nur im Profil,
$set['user']['kontakt_email']:
(CMS:URL id=0 template=u_confirm absolute=true set=link)
(CMS:EMAIL submit="1" to="{$set['email']}" subject="Bitte bestätigen"
body="Konto freischalten: {$set['link']}?hash={$set['hash']}")
(CMS:MESSAGE)
<p>Bitte bestätigen Sie Ihre E-Mail-Adresse.</p>
Meldungs-Indizes: container, email, exists, save, throttled, owner (kein gültiger Owner, siehe owner), token sowie Feldfehler des Bereichs.
Drosselung und Formularschutz
Jede Registrierung schickt eine E-Mail an die eingegebene Adresse. camindo begrenzt das
(Drosselung): fünf Registrierungen je Herkunft (IP-Adresse) und Stunde, drei je
E-Mail-Adresse und Tag, 200 je Website und Stunde. Gezählt werden Registrierungen und
Versuche mit einem schon vergebenen Namen – ein Tippfehler bei der Passwort-Richtlinie
zählt nicht. Meldung dann: throttled, $set['throttled'] = 1. Wie bei
CMS:USER_PW schützt formguard=true (mit
(CMS:USER_TOKEN formguard=true) im Formular) vor Robots.
Die Meldung exists verrät, dass es einen Namen oder eine Adresse schon gibt; die
Drosselung begrenzt, wie oft sich das ausprobieren lässt.
Nie bestätigte Registrierungen löscht camindo nach sieben Tagen
(user.unconfirmed_days): wer die Adresse eines anderen einträgt, blockiert sie so nur
kurz.
Parameter im Einzelnen
successtemplate
Nach dem Anlegen eingebunden. Darin stehen:
$set[…] |
Inhalt |
|---|---|
hash |
Freischalt-Token für den Bestätigungslink |
email |
die geprüfte E-Mail-Adresse – unabhängig vom Namen des Feldes aus emailfield |
user_id |
record_id des neuen Profils |
user |
das Profil als Array (Feldnamen als Schlüssel); es ist auch der laufende Datensatz |
(CMS:USER_REGISTER container=members emailfield=email submit="$submit" successtemplate=u_registered)
container
(CMS:USER_REGISTER container=members …)
emailfield
Feld mit der E-Mail-Adresse - wird geprüft und in $set['email'] abgelegt (unter
diesem festen Namen, nicht unter dem Feldnamen):
(CMS:USER_REGISTER container=members emailfield=kontakt_email …)
fields
Nur diese Felder aus dem Formular übernehmen:
(CMS:USER_REGISTER container=members emailfield=email fields="nickname,email,password" …)
overwrite
Feste Werte, auch Metafelder - keine Usereingaben. Ausgenommen sind record_id,
area_id, instance_id, tmp, deleted, origin_id, owner_id, created_by und
changed_by; der Status (status_id 2) lässt sich hier überschreiben – dann entfällt die
Bestätigung per Link:
(CMS:USER_REGISTER container=members emailfield=email overwrite[role]=member overwrite[source]=web …)
message
(CMS:USER_REGISTER container=members emailfield=email submit="$submit" successtemplate=u_registered
message[exists]="Name oder E-Mail sind bereits vergeben."
message[email]="Bitte eine gültige E-Mail-Adresse angeben."
)
submit
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_REGISTER … submit="$submit")
data
<input name="reg[nickname]"> …
(CMS:USER_REGISTER container=members emailfield=email data=reg submit="$submit" successtemplate=u_registered)
CMS:USER_REGISTER_CONFIRM
Schaltet einen Profil-Datensatz frei und setzt status=0.
(CMS:USER_REGISTER_CONFIRM [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsRegisterConfirm()
PHP-Schnipsel: include_and_stop (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
successtemplate |
– | – | ja | Detailtemplate, das eingebunden wird, wenn die Registrierung erfolgreich abgeschlossen werden konnte. |
hash |
Hash-ID |
– | ja | Hash-ID, die mittels CMS:USER_REGISTER erzeugt und per E-Mail versendet wurde. |
message[…] |
Meldung |
– | Fehlermeldung. Im Index: Anwendungsfall; im Wert: Meldungstext (vgl. Tabelle). |
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Löst den Token aus CMS:USER_REGISTER ein und schaltet das Profil frei (Status 0).
Steht auf der Seite, auf die der Bestätigungslink zeigt:
(CMS:USER_REGISTER_CONFIRM hash="{$_GET['hash']}" successtemplate=u_confirmed
message[hash]="Der Link ist ungültig oder abgelaufen.")
(CMS:INCLUDE header)
<p class="error">(CMS:MESSAGE)</p>
u_confirmed:
<p>Ihr Konto ist freigeschaltet – <a href="/0,u_login">jetzt anmelden</a>.</p>
Tokens verfallen nach 24 Stunden (user.token_expiration). Nach Erfolg sind
$set['user_id'] und $set['user'] gefüllt; angemeldet ist der Frontend-User damit noch nicht.
Parameter im Einzelnen
successtemplate
(CMS:USER_REGISTER_CONFIRM hash="{$_GET['hash']}" successtemplate=u_confirmed)
hash
Token aus dem Link. Ist er leer, unbekannt, abgelaufen oder schon eingelöst, erscheint
die Meldung hash – der Befehl kennt kein submit:
(CMS:USER_REGISTER_CONFIRM hash="{$_GET['hash']}" successtemplate=u_confirmed)
message
(CMS:USER_REGISTER_CONFIRM hash="{$_GET['hash']}" successtemplate=u_confirmed message[hash]="Dieser Link ist nicht mehr gültig.")
CMS:USER_PROTECT
Schützt ein Template vor dem Zugriff nicht angemeldeter Website-Besucher.
(CMS:USER_PROTECT [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsProtect()
PHP-Schnipsel: include_and_stop (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
errortemplate |
Templatename |
– | ja | Detailtemplate, das eingebunden wird, falls der Frontend-User keinen Zugriff auf den Container hat oder nicht angemeldet ist. |
filter[…][…] |
vgl. Parameter "filter[]" von CMS:SELECT |
– | Filterkriterien für den Profil-Datensatz, die beim angemeldeten Frontend-User erfüllt sein müssen. | |
sessionid |
Session-ID |
– | Session des Frontend-Users anhand der angegebenen sessionid wieder aufnehmen anstatt über ein Cookie. | |
cookiename |
Cookiename |
usess |
Name des Session-Cookies [a-z0-9]. |
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Sperrt ein Template für nicht angemeldete Besucher: Ist niemand angemeldet (oder trifft
filter nicht zu), wird errortemplate eingebunden und die Seite beendet. Sonst läuft
das Template weiter, $set['user_id'] und $set['user'] sind gefüllt:
(CMS:USER_PROTECT errortemplate=u_login)
(CMS:INCLUDE header)
<h1>Mein Konto</h1>
<p>Angemeldet als <?php echo htmlspecialchars($set['user']['nickname']); ?></p>
Nur für eine Gruppe von Frontend-Usern (Profil-Feld role):
(CMS:USER_PROTECT errortemplate=u_denied filter[role]=editor)
Das errortemplate ist typischerweise das Anmeldeformular selbst - so landet der
Besucher nach der Anmeldung wieder auf der geschützten Seite.
Parameter im Einzelnen
errortemplate
Template für nicht angemeldete Besucher (oder wenn filter nicht zutrifft):
(CMS:USER_PROTECT errortemplate=u_login)
(CMS:USER_PROTECT errortemplate=templates.u_denied filter[role]=editor)
filter
Zusätzliche Bedingung an das Profil, Syntax wie CMS:SELECT:
(CMS:USER_PROTECT errortemplate=u_denied filter[role]=editor)
(CMS:USER_PROTECT errortemplate=u_denied filter[paid_until] >= "{$today}")
sessionid
(CMS:USER_PROTECT errortemplate=api_denied sessionid="{$_GET['sid']}")
cookiename
(CMS:USER_PROTECT errortemplate=u_login cookiename=shop)
CMS:USER_PW
Bereitet das Zurücksetzen eines Passworts vor.
(CMS:USER_PW [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsPw()
PHP-Schnipsel: include_and_stop (danach)
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
successtemplate |
– | – | ja | Detailtemplate, das eingebunden wird, falls "uniquevalue" im Feld "uniquefield" einem Profil zugeordnet werden konnte. |
errortemplate |
– | – | ja | Detailtemplate, das eingebunden wird, falls "uniquevalue" angegeben wurde, aber keinem Profil zugeordnet werden konnte. |
container |
Container-ID oder -Name |
– | ja | ID oder Name des Profil-Containers, in dem die abzugleichenden Profile gespeichert sind. |
uniquefield |
Feldname |
– | ja | Name des Feldes, über das gesucht wird (Feldtyp chars, text oder encrypted, z. B. "email"). Der Wert muss genau ein Profil treffen. |
uniquevalue |
Text |
– | Gesuchter Wert im Feld "uniquefield", z. B. $_POST[email]. | |
submit |
Textwert |
– | ja | Der Befehl wird nur ausgeführt, wenn in diesem Feld ein Text enthalten ist. |
message[…] |
Text |
– | Fehlermeldung. Im Index: Anwendungsfall; im Wert: Meldungstext (vgl. Tabelle). | |
csrf |
true / false |
true |
true: Der Request muss ein gültiges Token aus CMS:USER_TOKEN enthalten (Schutz gegen CSRF). false schaltet die Prüfung für diesen Befehl ab – nur mit gutem Grund. | |
formguard |
true / false / Sekunden |
false |
true: Formularschutz gegen Robots – das Formular muss die Felder aus (CMS:USER_TOKEN formguard=true) enthalten und frühestens nach 3 Sekunden (user.formguard_min_seconds) abgeschickt werden. Eine Zahl setzt die Mindestzeit für dieses Formular (formguard=5), 0 schaltet ab. Sonst Meldung throttled. |
[…] – der Parameter nimmt Unterschlüssel an, z. B. name[schlüssel]=wert.
Anwendung
Erster Schritt „Passwort vergessen“: Sucht das Profil über ein eindeutiges Feld
(meist die E-Mail-Adresse) und erzeugt einen Einmal-Token in $set['hash']. Das
successtemplate verschickt den Link; die Zielseite meldet den Frontend-User mit
(CMS:USER_LOGIN hash=…) an, danach setzt er per CMS:USER_INPUT ein neues Passwort:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_PW container=members uniquefield=email uniquevalue="{$_POST['data']['email']}"
submit="$submit" successtemplate=u_pwsent errortemplate=u_pwfail)
<h1>Passwort vergessen</h1>
<form method="post">
(CMS:USER_TOKEN)
<input name="data[email]" type="email">
<button name="submit" value="1">Link anfordern</button>
</form>
u_pwsent verschickt den Link mit CMS:EMAIL:
<?php $link = 'https://www.example.com/0,u_hashlogin?hash=' . rawurlencode($set['hash']); ?>
(CMS:EMAIL submit="1" to="{$set['user']['email']}" subject="Neues Passwort"
body="Link (eine Stunde gültig): $link")
(CMS:MESSAGE)
<p>Wir haben Ihnen einen Link geschickt.</p>
Meldungs-Indizes: notfound, container, throttled, token.
Drosselung und Formularschutz
Jede Anfrage verschickt eine E-Mail an eine Adresse, die jemand eingetippt hat. Damit
niemand über Ihr Formular fremde Postfächer flutet, zählt camindo die Anfragen
(Drosselung): höchstens drei je eingegebener Adresse und Stunde, zehn je
Herkunft (IP-Adresse) und 200 je Website (Systemadministrator: user.throttle). Gezählt wird
der eingegebene Wert – ob es das Konto gibt, spielt keine Rolle. Ist eine Grenze
erreicht, entsteht kein Link, und der Befehl bindet das errortemplate ein – mit
$set['throttled'] = 1 und der Meldung throttled. Zeigen Sie dort denselben Text wie
im Erfolgsfall, verrät die Seite weder, ob es das Konto gibt, noch, dass gedrosselt wurde.
Gegen Robots, die jedes Formular blind füllen, hilft zusätzlich der
Formularschutz: formguard=true am Befehl und (CMS:USER_TOKEN formguard=true)
im Formular. Wer das Formular schneller als in drei Sekunden abschickt oder das
unsichtbare Köderfeld füllt, bekommt dieselbe Antwort wie bei der Drosselung:
(CMS:USER_PW container=members formguard=true uniquefield=email uniquevalue="{$_POST['data']['email']}"
submit="$submit" successtemplate=u_pwsent errortemplate=u_pwfail)
…
<form method="post">
(CMS:USER_TOKEN formguard=true)
…
</form>
Der Link gilt eine Stunde und nur einmal (Einstellung
user.password_token_expiration). Er funktioniert auf jedem Gerät – wer sein Passwort
vergessen hat, sitzt oft gerade an einem neuen. Kommt die Anmeldung über den Link von
einem unbekannten Gerät, meldet CMS:USER_LOGIN das wie
jede andere Anmeldung.
Zeigen Sie im errortemplate denselben Text wie im successtemplate („Falls zu
dieser Adresse ein Konto existiert, haben wir einen Link geschickt."). Sonst kann jeder
ausprobieren, welche E-Mail-Adressen bei Ihnen ein Konto haben.
Parameter im Einzelnen
successtemplate
Eingebunden, wenn ein Profil gefunden wurde; darin $set['hash'], $set['user'], und das Profil ist der laufende Datensatz:
(CMS:USER_PW container=members uniquefield=email uniquevalue="$email" submit="$submit" successtemplate=u_pwsent errortemplate=u_pwfail)
errortemplate
Eingebunden, wenn uniquevalue angegeben war, aber kein Profil passt:
(CMS:USER_PW … errortemplate=u_pwfail)
container
(CMS:USER_PW container=members …)
uniquefield
Feld, über das gesucht wird (Feldtyp chars, text oder encrypted). Verglichen wird
wörtlich, ohne Groß-/Kleinschreibung; trifft der Wert mehr als ein Profil, gilt das wie
„nicht gefunden“ – markieren Sie das Feld deshalb im Bereich als eindeutig:
(CMS:USER_PW container=members uniquefield=email …)
(CMS:USER_PW container=members uniquefield=nickname …)
uniquevalue
Gesuchter Wert, meist aus dem Formular. Leer = der Befehl tut nichts:
(CMS:USER_PW container=members uniquefield=email uniquevalue="{$_POST['data']['email']}" …)
submit
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_PW … submit="$submit")
message
(CMS:USER_PW … message[notfound]="Zu dieser Adresse gibt es kein Konto.")
CMS:USER_TOKEN
Gibt das Sicherheits-Token (CSRF-Token) für ein Formular aus, das einen schreibenden
CMS:USER_*-Befehl auslöst.
(CMS:USER_TOKEN [parameter=wert …])
Methode: Modules\Record\RecordUser::cmsToken()
Parameter
| Parameter | Wert | Standard | Pflicht | Beschreibung |
|---|---|---|---|---|
format |
"field" / "value" |
"field" |
field: verstecktes Eingabefeld <input type="hidden" name="_token">, der Wert mit htmlspecialchars maskiert. value: nur das Token (für Ajax), unmaskiert. |
|
set |
Array-Index von $set |
– | Die Ausgabe wird nicht ausgegeben, sondern in $set gespeichert. Das Token steht zusätzlich immer in $set['user_token']. |
|
formguard |
true / false |
false |
true: zusätzlich Zeitstempel und ein unsichtbares Köderfeld ausgeben (Formularschutz gegen Robots). |
Anwendung
Das Token verhindert, dass eine fremde Website im Namen Ihrer Besucher Formulare
abschickt (Cross-Site Request Forgery): Anmelden, Registrieren, Passwort-Reset,
Beiträge speichern oder löschen, Abmelden, Warenkorb ändern. Diese Befehle prüfen
das Token und brechen ohne gültiges Token mit einer Meldung (Index token) ab:
| Befehl | geprüft |
|---|---|
USER_LOGIN, USER_REGISTER, USER_PW, USER_INPUT, USER_2FA |
wenn submit gefüllt ist (die Anmeldung per hash nicht) |
USER_LOGIN_CODE |
wenn submit oder resend gesetzt ist |
USER_DELETE |
immer |
USER_LOGOUT |
immer – mit submit nur, wenn submit gefüllt ist |
USER_BASKET |
bei add, update, update_addon – load nicht; ohne gültiges Token wird nur gelesen |
Das Token hängt an einem Cookie. Der erste Aufruf auf einer Seite muss deshalb
vor jeder Ausgabe stehen – am Anfang des Templates (bzw. im Kopf-Include vor
<!doctype>), mit set, damit dort nichts ausgegeben wird. Danach schreiben Sie
(CMS:USER_TOKEN) in jedes Formular, das einen dieser Befehle auslöst:
(CMS:USER_TOKEN set=token)
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=u_welcome)
<!doctype html>
…
<form method="post">
(CMS:USER_TOKEN)
<input name="data[nickname]"> <input type="password" name="data[password]">
<button name="submit" value="1">Anmelden</button>
</form>
Formularschutz gegen Robots
(CMS:USER_TOKEN formguard=true) gibt zusätzlich einen signierten Zeitstempel und ein
Köderfeld aus, das Besucher nicht sehen. Befehle mit formguard=true
(CMS:USER_REGISTER, CMS:USER_PW,
CMS:EMAIL) weisen das Formular ab, wenn das Köderfeld gefüllt ist oder es
schneller als nach drei Sekunden bzw. später als nach einer Stunde abgeschickt wurde.
Beide Zeiten stellt der Systemadministrator ein (user.formguard_min_seconds,
formguard_max_seconds); für ein einzelnes Formular setzt formguard=5 am Befehl die
Mindestzeit auf fünf Sekunden – etwa für ein langes Formular, das niemand in drei
Sekunden ausfüllt.
Das hält Robots auf, die blind jedes Formular füllen – kein Captcha, kein fremder Dienst.
Ein externes Captcha kann der Systemadministrator zusätzlich anbinden
(security.form_challenge); dessen Widget binden Sie dann selbst ein.
<form method="post">
(CMS:USER_TOKEN formguard=true)
…
</form>
Für Ajax-Aufrufe schicken Sie das Token als POST-Feld _token oder im Header
X-Camindo-Token mit. Ein FormData aus einem Formular mit (CMS:USER_TOKEN) enthält
es bereits:
<script>
fetch('/0,basket_api', {
method: 'POST',
headers: { 'X-Camindo-Token': '(CMS:USER_TOKEN format=value)' },
body: new URLSearchParams({ product: 42 })
});
</script>
Steht der erste Aufruf zu spät, gibt es kein Token: an Stelle des Feldes steht ein
HTML-Kommentar mit Hinweis, und das Formular scheitert mit der Meldung token.
Das Cookie (ucsrf) gilt für alle Formulare der Website und überlebt die Anmeldung. Im Query-String
(GET) wird es nicht angenommen – schreibende Aktionen gehören in POST-Formulare.
Die Prüfung lässt sich je Befehl mit csrf=false abschalten, der Systemadministrator
kann sie mit user.csrf = false global abschalten. Beides öffnet die Befehle für
fremde Websites und ist nur für Übergangszeiten gedacht.
Stand: camindo CMS 9.1.3