Manual for template programmers
camindo CMS delivers content by means of templates, for example HTML on a website. CMS commands prepare the content and insert it dynamically into the template. Example of a greatly simplified page template:
<html>
<body>
<h1>(CMS:TEXT title)</h1>
<div>(CMS:TEXT text)</div>
<div>(CMS:MEDIA field=image attr[id]=image1)</div>
<div>created: (CMS:DATE created_at format="dd.MM.y")</div>
</body>
</html>
CMS commands are not merely placeholders. They represent complex encapsulated functionality that can be adapted with parameters and combined with one another. This saves a lot of development time. Moreover, they are proven and guarantee stability.
Examples of what CMS commands can do:
- determine content matching certain criteria from the database
- process content
- check, save or send user input
- registration of website visitors, sign-in and sign-out
- include further templates
Output formats: In addition to websites with HTML pages, further output media and publication channels are possible. Content can, for example, be formatted as XML files, PDF files, emails … and made available for retrieval via a URL, written as files to the file system, or pushed over a network.
Template set: Nobody needs to reinvent the wheel. Content areas (their configuration incl. input fields, optionally with content) and the associated templates can be exported in XML format. When imported into another camindo installation, the relations between areas and templates remain intact. The complete application part is immediately ready for use. This enables the easy integration of ready-made kits (forum, news feed, ...) or the export of self-developed applications for reuse in later projects.
Contents
- Basics – how a page is created, addresses, syntax, lists, links, the concept “every record has a page”, filters, multilingualism, security headers (delivering CSS/JS/JSON, embedding in partner websites), scheduled templates, templates in the file system
- Template sets – encapsulating functions, keeping them portable, passing them on and adapting them
- Trigger templates – PHP methods for the events of the records of an area (saving, deleting …) and for the actions of the deletion policies
- Command reference – one page per command (below)
How templates are organized in the back end and assigned to content areas is described in the Manual for administrators.
Command reference
(CMS:COMMAND value parameter=value parameter[key]="value" …)
Command names and parameter names are not case-sensitive. The first value without =
counts as the main parameter of the command ((CMS:TEXT title) =
(CMS:TEXT field=title)). Values in quotation marks may contain PHP expressions,
as is customary in a PHP string ("{$set['x']}").
The output parameters are the same for most commands:
ifempty (replacement for an empty value),
instead (replacement for a non-empty value),
prefix / suffix (only for a non-empty value),
set (result is written to the variable $set[…] instead of to the page),
id (another record as the source),
replace (replace partial texts) for TEXT, DATE, DATETIME, TIME and NUMBER.
Output
| Command | |
|---|---|
| CMS:DATE | Output date |
| CMS:DATETIME | Output date and time |
| CMS:EMAIL | Send email, check required form fields |
| CMS:MEDIA | Output image or media file |
| CMS:MESSAGE | Output error messages |
| CMS:NUMBER | Output numeric content |
| CMS:RAW | Output content unformatted |
| CMS:SNIPPET | Output snippet (text module) in the language of the call |
| CMS:TEXT | Output text content |
| CMS:TIME | Output time |
| CMS:URL | Create the URL of a record |
| CMS:XML | Output the XML declaration |
| CMS:ZIP | Download the files of several records as a ZIP archive |
Relations
| Command | |
|---|---|
| CMS:RELATION | Output the main field of linked records (for internal preview only!) |
Queries
| Command | |
|---|---|
| CMS:SELECT | Query records |
| CMS:SELECT_LANGVERSION | Languages of the instance as a result set |
| CMS:RESULT_START | Start of the result loop |
| CMS:RESULT_END | End of the result loop |
Navigation
| Command | |
|---|---|
| CMS:JUMP_LIST | Create page navigation |
| CMS:JUMP_NEXT | Create a link to the next page |
| CMS:JUMP_PREV | Create a link to the previous page |
Control
| Command | |
|---|---|
| CMS:CONTINUE | Continue after a success template |
| CMS:CSP | Adapt the script policy (CSP) of the page |
| CMS:HEADER | Send HTTP headers |
| CMS:INCLUDE | Include a template, optionally with output cache |
| CMS:NONCE | Nonce for scripts generated by PHP |
| CMS:SAVEDATA | Store a record in a PHP variable |
Frontend editing
| Command | |
|---|---|
| CMS:FED | Edit link for frontend editing |
| CMS:FED_CODE | Output HTML only with frontend editing |
| CMS:FED_SCRIPT | Script for the edit links (once per page) |
Community
| Command | |
|---|---|
| CMS:USER_2FA | Manage the second factor |
| CMS:USER_AUTH | Check the session of the frontend user |
| CMS:USER_BASKET | Edit the shopping basket |
| CMS:USER_DELETE | Delete the user's record |
| CMS:USER_INPUT | Save a record from user input |
| CMS:USER_LOGIN | Sign in the frontend user |
| CMS:USER_LOGIN_CODE | Complete sign-in with the second factor |
| CMS:USER_LOGOUT | Sign out the frontend user |
| CMS:USER_PASSWORD_RULES | Password rules for display in the form |
| CMS:USER_REGISTER | Register a frontend user (create a profile record) |
| CMS:USER_REGISTER_CONFIRM | Activate registration |
| CMS:USER_PROTECT | Lock a template for visitors who are not signed in |
| CMS:USER_PW | Prepare password reset |
| CMS:USER_TOKEN | Security token for forms |
The administrator sets up the community functions (see Manual for administrators).
PDF add-on module
The PDF add-on module (camindo/module-pdf) supplements the commands (CMS:PDF_…), with
which templates create PDF documents – page by page, if desired on a
PDF template such as letterhead paper, with texts, HTML, images, lines and tables:
(CMS:PDF_INIT margins=25,20,30,20)
(CMS:PDF_PAGE letterhead.pdf)
(CMS:PDF_FONT helvetica style=B size=16)
(CMS:PDF_TEXT "Invoice {$cms['number']}" y=50)
(CMS:PDF_PRINT invoice.pdf)
The complete command list is part of the documentation of the module.
1 Basics of template programming
What a template is
A template is a text document – usually HTML – that contains CMS commands. The commands query records, insert contents, include other templates, evaluate forms and control the flow. PHP code may also appear in between.
(CMS:INCLUDE header)
<h1>(CMS:TEXT title)</h1>
<div>(CMS:TEXT text)</div>
<div>(CMS:MEDIA field=image_small attr[id]=image1)</div>
<div>created: (CMS:DATE created_at format="MM/dd/y")</div>
(CMS:INCLUDE footer)
When you save, camindo CMS translates the template into PHP. At runtime only this PHP is executed – there is no template engine that evaluates the text again on every call.
How a page is created
When a visitor opens an address, camindo CMS determines:
- the instance – from the domain,
- the language – from the language code in the address, otherwise from the browser's language setting, otherwise the primary language,
- the record – from the slug or the number (
record_id) in the address; without one, the start record of the instance, - the template – from the address, from the record, or the default template of its content area.
The contents of the requested record are available in the template. By default, all output
commands refer to this record – see (CMS:TEXT title) above. If the template runs a query
with CMS:SELECT, the contents of the records found are available in the same way in the
result template; see Lists, SELECT and the result loop below.
Addresses
[/{language}]/{slug or number}[,{template}]
| Address | Meaning |
|---|---|
/ |
Start page of the instance |
/company-party-2026 |
Record with this slug, default template |
/events/company-party-2026 |
Record with the slug company-party-2026 in the area with the slug prefix events |
/events |
Overview of the area with the slug prefix events – its overview template, without a record |
/1758 |
Record number 1758 |
/en/summer-party-2026 |
English language version |
/company-party-2026,print |
Record with the template print – only if it is released as a URL override for the area |
/0,search |
Template search without a record – only if it is marked as “Standalone” |
Pure digit sequences are always record numbers; anything else is a slug. If the first part of the address is the slug prefix of a content area, the rest is searched only in this area; otherwise the whole path is searched in the areas without a prefix. The language code is part of the address as soon as the instance has more than one language. Generate addresses with CMS:URL – never by hand, then language, slug and domain always match.
If the address consists only of the language code (/en, without a trailing slash),
camindo CMS redirects to the start page of the language (/en/ or /en_US/ –
depending on how it is set up). If the site has only one language and a record has exactly
this slug, the record is shown instead. Addresses for which there is nothing – unknown record,
unknown language, template not released – respond with the status 404.
Syntax of CMS commands
(CMS:COMMAND value parameter=value parameter[key]="value" …)
- Command names and parameter names are not case-sensitive.
- Parameters are separated by spaces; a command may span several lines.
- The order in which a command processes its parameters is determined by the command itself, not by the order in which the parameters are written.
- The first value without
=is the main parameter of the command:(CMS:TEXT title)is the same as(CMS:TEXT field=title). - Values that contain spaces are enclosed in quotation marks, single or double:
(CMS:TEXT field="title")is the same as(CMS:TEXT field='title'). - Subkeys in square brackets:
filter[area]=news. Empty brackets append to a list:sort[]=date sort[]=title. If the same subkey is given several times, a list is created as well (in contrast to PHP array syntax,[]is optional). - Instead of
=, a comparison operator may stand between parameter and value –==,!=,!,!!,<,>,<=,>=,<<,>>:filter[price] >= 100is the same asfilter[price]=">=100". The value then applies unchanged; an operator at its beginning is no longer evaluated:filter[code] < ">2"compares with the text “>2”. With=it stays as before –filter[code] = ">2"means “greater than 2” (see Filter syntax). - The line break after a CMS command is swallowed, in the same way as after a
<?php … ?>block. - Command parameters in double quotation marks may contain PHP variables:
attr[alt]="{$myvar}"(parameters without quotation marks are handled internally like parameters in double quotation marks).
Errors – an unknown command, an unknown parameter, a missing bracket – do not prevent saving. The detail view of the template shows them with the line number. They also appear on the start page in the widget “Template messages” – in case templates are edited through the file system.
Combining CMS commands and PHP code
$set <?php … ?> blocks may appear freely in the template, but CMS commands inside
PHP blocks lead to errors! To use the contents of CMS commands in PHP blocks, use the
parameter set (for example set = myvar) and read the content in the PHP block from $set
($set['myvar']).
$cms The contents of the requested record are in $cms; so are the contents of records
found with CMS:SELECT.
For output, always use the CMS output commands (such as
CMS:TEXT). These commands take care of correct formatting; a<?php echo $cms[…] ?>does none of this. Use$cmsonly where a command needs a value as a parameter, for examplefilter[category]="{$cms['record_id']}".
Common parameters of the output commands
| Parameter | Effect |
|---|---|
ifempty |
replacement text if the value is empty |
instead |
replacement text if the value is not empty |
prefix, suffix |
text before and after – only if the value is not empty |
set |
do not output the value, but store it in $set[…] |
id |
read from another record (number or slug), for example (CMS:TEXT title id=imprint) – only published records, as in the URL |
replace |
replace partial texts (replace="old?new") – for TEXT, NUMBER, DATE, DATETIME, TIME |
(CMS:TEXT subtitle prefix="<h2>" suffix="</h2>")
(CMS:NUMBER price format="%.2f" suffix=" $" ifempty="on request")
Values in attributes and JavaScript
camindo CMS removes script, event handlers (onclick …), iframe, object, embed and
javascript: links from all record contents – when saving and again when outputting.
HTML formatting remains, which is why CMS:TEXT outputs contents unescaped.
If you insert a value into an HTML attribute, escape it with specialchars=true –
otherwise a quotation mark in the content would end the attribute:
<img src="…" alt="(CMS:TEXT title specialchars=true)">
<a href="(CMS:URL)" title="(CMS:TEXT teaser specialchars=true striptags=true lbrconvert=false)">…</a>
The same applies to form input that you put back into the field after submitting:
<input name="data[nickname]" value="(CMS:TEXT useval="{$_POST['data']['nickname']}" specialchars=true)">
Commands that escape themselves: Some commands already escape their output with
htmlspecialchars. For them, specialchars is omitted – an already escaped value would be
escaped a second time, and an & would appear as &:
| Command | Escapes |
|---|---|
| CMS:MEDIA | all attributes of the <img> tag: src, alt, width, height and attr[…] – not the individual values via subfield |
| CMS:USER_TOKEN | the value of the hidden field |
| CMS:USER_PASSWORD_RULES | the entire output (JSON for an attribute) |
| CMS:XML | version and encoding |
| CMS:JUMP_LIST, CMS:JUMP_PREV, CMS:JUMP_NEXT | the URL in {href} |
What you pass to one of these commands must therefore be unescaped text – without HTML tags and entities:
(CMS:TEXT title striptags=true ampconvert=false lbrconvert=false set=title)
(CMS:MEDIA image attr[alt]="{$set['title']}")
Never escaped are prefix, suffix, ifempty and instead – they come from the
template and may contain HTML. Only CMS:TEXT and CMS:SNIPPET know
specialchars. Values of other commands, such as CMS:MEDIA … subfield=filename,
CMS:RELATION or CMS:RAW, store them with set for an attribute
and output them escaped via CMS:TEXT:
(CMS:MEDIA document subfield=filename set=filename)
<a href="(CMS:MEDIA document subfield=path)" title="(CMS:TEXT useval="{$set['filename']}" specialchars=true lbrconvert=false)">…</a>
CMS:URL and CMS:MEDIA … subfield=path produce only URL-safe characters, and so does
CMS:TEXT … urlencode=true – they can stand in href and src without escaping.
If JavaScript needs a value, output it as a data- attribute and read it there – so
that no content ever appears in the script code itself:
<div id="product" data-title="(CMS:TEXT title specialchars=true)"></div>
<script>var title = document.getElementById('product').dataset.title;</script>
Lists, SELECT and the result loop
Lists always consist of two templates: the calling template queries with CMS:SELECT and names a list template. The list template runs through the hits between CMS:RESULT_START and CMS:RESULT_END.
Calling template:
<h2>Latest news</h2>
(CMS:SELECT news_list
filter[area]=news
sort=date
order=desc
limit=10
noresult="<p>No news.</p>"
)
List template news_list:
<ul>
(CMS:RESULT_START)
<li><a href="(CMS:URL)">(CMS:TEXT title)</a> – (CMS:DATE date)</li>
(CMS:RESULT_END)
</ul>
Inside the loop, the current row is the running record. A list template may itself contain
CMS:SELECT – for example the tags of each article.
Paging: count=true counts all hits; CMS:JUMP_LIST,
CMS:JUMP_PREV and CMS:JUMP_NEXT turn this into the navigation.
Publication: Templates find only published records – released, publication date
reached, expiry date not yet reached. The parameters future and archive relax this per
query, for example for an archive or a preview of upcoming events.
The parameter
stateis intended exclusively for views inside the back end.
Addressing areas
Content areas are addressed by their name or by their number (area_id):
filter[area]=news, filter[area]=12 or filter[area_id]=12. Separate several areas with
commas. Because field names are uniform across the system, several areas can be filtered and
sorted together:
(CMS:SELECT timeline
filter[area]=news,events,blog
filter[date] >= 2026-01-01
sort=date
order=desc
)
sort=area sorts by area; _default stands in filters and sorting for the main field of each
area, whatever it is called there.
Overview page of an area
The address consisting only of the slug prefix (/events) calls the overview template
of the area – without a record. In its place stand the details of the area:
| Value | Contents |
|---|---|
area_id |
number of the area |
area |
name of the area |
area_label |
label of the area (snippets in the language of the call) |
area_type, area_icon |
type and icon of the area |
instance_id, language_id |
instance of the area and language of the call |
slug_prefix |
slug prefix in the language of the call |
record_id |
always 0 – there is no current record |
overview |
true |
In this way, an overview template lists the records of its area without having their name written in it – and can be used for several areas:
<h1>(CMS:TEXT area_label specialchars=true)</h1>
(CMS:SELECT event_list
filter[area]="{$cms['area_id']}"
sort=date order=asc
)
Linked records
With relation, CMS:SELECT retrieves the records that are linked with the current record
(or with the record named in src_id) via a link field.
(CMS:SELECT gallery_list
relation=see_also
sort="created_at desc"
)
With sort="_custom asc", linked records appear in the order that the editorial team set
in the lookup field (using the arrow buttons):
(CMS:SELECT gallery_list
relation=images
sort="_custom asc"
)
To sort and filter by fields of linked records, use dot notation: sort=author.last_name.
If there is no current record and src_id is not given or invalid, the search with the
parameter relation finds nothing.
The concept: every record has its own page
In camindo CMS, every record has its own page in the frontend – not only articles and
products, but also every category, every tag, every material. A website therefore needs
no parameters such as ?cat=12 or ?tag=oak:
- Linking: Lists link every record with CMS:URL – the category just as the product.
- Default template per area: Each content area gets a default template – a category
page, a tag page, a product page. With a slug prefix, each kind of page gets its own address
space (
/en/category/living,/en/tag/oak). - Listing linked contents: On the page, the record is in
$cms; the template queries its linked records directly through it.
Category page (default template of the area categories):
<h1>(CMS:TEXT title)</h1>
(CMS:TEXT text prefix="<p>" suffix="</p>")
(CMS:SELECT product_list
relation=rel_products
sort=position
order=asc
)
List template product_list – the products link back to their own page:
<ul>
(CMS:RESULT_START)
<li><a href="(CMS:URL)">(CMS:TEXT title)</a></li>
(CMS:RESULT_END)
</ul>
Navigation is just as simple – a list of all categories with links:
(CMS:RESULT_START)
<a href="(CMS:URL)">(CMS:TEXT title)</a>
(CMS:RESULT_END)
Which query returns the linked records depends on from which page the link starts:
| Link | Query on the page of the record |
|---|---|
| starts from the record (its own link field) | (CMS:SELECT list relation=fieldname) |
relation field (relation) of another area points to it |
(CMS:SELECT list relation=rel_areaname) – via the counterpart field |
pointer field (pointer) of another area points to it |
(CMS:SELECT list filter[area]=areaname filter[fieldname]="{$cms['record_id']}") |
Relation fields (relation, relation1) are linked in both directions: when you create
them, camindo CMS creates in the target area a counterpart field named
rel_{areaname} (for example rel_products), which leads the link from the other side. The
category page therefore queries its products with relation=rel_products, and the product page
queries its categories with relation=categories.
Pointer fields (pointer, pointer1) are directed – they can point to several areas.
The target record does not know its pointers; the counterpart is a filter on the pointer field
with the number of the current record:
(CMS:SELECT product_list
filter[area]=products
filter[category]="{$cms['record_id']}"
)
Note: The number of the current record is in
$cms['record_id']– there is no keyid. An empty filter value cancels the filter:filter[category]="{$cms['id']}"therefore does not find no products, but all products.
Searches and free filter combinations remain parameters (?q=…), because they are not
records. Everything that is a record, on the other hand, has its own address – and with it
its page title, description and frontend editing like any other page.
Filter syntax
The values in filter[…] follow the same syntax as the search mask in the back end:
| Value | Meaning |
|---|---|
Berlin |
equal |
Berlin,Hamburg |
either of them (with filter_mode[field]=AND: both) |
Hen*, *man, *berg* |
wildcards |
!Berlin, !*man |
not equal |
==Star* |
exactly equal, * without wildcard effect |
>100, >=100, <100, <=100 |
comparisons – also for dates |
>2 AND <4, Hen* OR Her* |
combine conditions within the same field |
2026, 2026-06 |
date: the whole year or the whole month |
now, today, now-2h, today+7d |
date relative to the call, in the time zone of the user or the language, like a typed date; today without an hour offset is the whole day. Offset in s, m, h, d |
utc_now, utc_today, utc_now-15m |
the same in UTC – for comparisons with points in time that apply regardless of the visitor (blocking periods, time window of a scheduler run) |
(empty), !(empty) |
without its own value (empty or not translated) or with one |
The operator may also stand between key and value – handy when the value comes from a variable:
(CMS:SELECT embargo_mail
filter[area] = press_releases
filter[online_at] > {$set['last_run']}
filter[online_at] <= {$set['this_run']}
filter_mode[online_at] = AND
future = true
)
is the same as filter[online_at]=">{$set['last_run']}" and
filter[online_at]="<={$set['this_run']}". With such an operator, the value itself is free of
operators: if the content of the variable begins with <, > or !, it is compared as text.
A date value that cannot be evaluated (such as a Unix timestamp like
$_SERVER['REQUEST_TIME']) is skipped without a message – the list then shows all
records, as if the filter were not there. For analyses in your development environment,
debug=true helps: the generated SQL query is displayed.
Text comparisons by language: Text and character fields (including _default) are
compared and sorted according to the rules of the language version in which you search –
derived from the language code, nothing needs to be set. Always valid: no distinction between
upper and lower case and accents (cafe finds “Café”), ß equals ss. In addition, the rules
of individual languages apply:
| Language | Rule | Example |
|---|---|---|
| German | ä = ae, ö = oe, ü = ue (phone-book rule) |
Mue* finds “Müller”, Mul* does not |
| Swedish | å, ä, ö are letters of their own |
o does not find ö; ö sorts after z |
| Danish, Norwegian | æ, ø, å are letters of their own, aa = å |
Aa* finds “Ålesund” |
| Turkish | i/İ and ı/I are different |
Bulgarian, Esperanto, Estonian, Galician, Icelandic, Croatian and Bosnian, Latvian, Lithuanian, Polish, Romanian, Russian, Serbian (Latin), Slovak, Slovenian, Spanish, Czech, Hungarian and Vietnamese have rules of their own as well. All other languages – English included – follow the general rules.
This applies equally to =, wildcards and comparisons such as >, and to full-text fields
(Mueller finds “Müller”, Muller does not). In the back end, the language that is set for
the instance of the searched area applies; if the instances of one search follow different
rules, only the general rules apply.
Different filters are always combined with AND. Full-text fields understand +must -mustnot;
encrypted fields only the exact value.
In areas with a tree structure, filter[parent_id]=42 returns all descendants of record 42;
together with filter[depth]=1 only the direct children – the basis for navigations.
Including and organizing templates
CMS:INCLUDE includes another template – for example the header and footer of the page:
(CMS:INCLUDE layout.header)
<main>…</main>
(CMS:INCLUDE layout.footer)
Template names only have to be unique within their template area. If a name exists in several
areas, refer to it as areaname.templatename. A name that is not unique is reported by the page
as an error.
Typical division:
| Type | Task |
|---|---|
| Page templates | default templates of the content areas: output a record as a complete page |
| List templates | run through the hits of a CMS:SELECT |
| Building blocks | header, footer, navigation, teaser – included via CMS:INCLUDE |
| Standalone templates | pages without a record: search, sitemap, forms |
| Scheduled templates | run as a task of the scheduler, without visitors (see below) |
| List templates (back end) | design the record list of the editorial team (see below) |
Caching
Expensive building blocks – a navigation made of many queries, a product list – can be cached
by CMS:INCLUDE with caching: (CMS:INCLUDE layout.navigation caching=1h) creates the
navigation at most once an hour per website and language. What else the output depends on
belongs in cacheparams – see CMS:INCLUDE.
Multilingualism
A template automatically outputs the language version of the call – if a translation is
missing, the primary language. Links with CMS:URL keep the language; with the parameter lang,
you link explicitly to another language version. To build a language switcher, use
CMS:SELECT_LANGVERSION.
Date and number formats follow the language version; so does the time zone.
Fixed labels (“Read more”, “Search”) are maintained by the editorial team as separate records
(for example in an area named “Other texts”) in all frontend languages. This area should have a
single-language slug field and a multilingual text field. Then CMS:TEXT works as a
snippet provider; see CMS:TEXT with the id parameter.
<a href="(CMS:URL)">(CMS:TEXT id=labels.readmore)</a>
Frontend editing
CMS:FED places an edit marker on a content that only users with frontend editing switched on can see. A click opens the input form of the record in the back end. CMS:FED_CODE outputs HTML only in this case – for example a stylesheet for the markers.
Security headers: CSS, JavaScript, JSON and embedding
camindo CMS sends a few HTTP headers before each template, which the browser applies as protection:
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'
Referrer-Policy: strict-origin-when-cross-origin
For ordinary HTML pages you do not need to do anything. Two cases do have an effect – and the cause is not obvious at first glance:
Templates that deliver CSS, JavaScript or JSON
A template is delivered as text/html unless it says otherwise. Because of nosniff, the
browser then does not accept the content as a stylesheet or script, even if it is correct. The
page looks unstyled, the script does not run – and the browser console reports something like
“refused to execute script … MIME type ('text/html') is not executable” or
“… not a supported stylesheet MIME type”.
Therefore set the appropriate type at the beginning of each such template – before any output, without a blank line in front:
(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")
Embedding output in other websites
A page of the website can only be shown in an <iframe> within the same website. This
protects sign-in, profile and shopping cart from a foreign page overlaying them invisibly and
slipping clicks to visitors (clickjacking). If a partner website embeds a page via <iframe>,
the frame therefore stays empty or shows an error page of the browser; the console reports
“Refused to frame … because an ancestor violates … frame-ancestors” or
“… X-Frame-Options to 'sameorigin'”.
If a template should be allowed to be embedded – a widget, an event calendar for partners –
release it only in this template: remove X-Frame-Options by giving it no value, and name
the permitted websites in frame-ancestors:
(CMS:HEADER "X-Frame-Options:")
(CMS:HEADER "Content-Security-Policy: frame-ancestors 'self' https://www.partner.example https://*.partner.example")
Both lines belong together: a browser that still sees X-Frame-Options refuses the frame despite
frame-ancestors. Name the partners individually – frame-ancestors * allows every website to
embed the page – including the pages with forms. Other kinds of embedding are not affected:
images, downloads, data loaded by JavaScript (JSON) and links to your website work without
release.
More on the header lines: CMS:HEADER.
Templates in the file system
Each template is stored as a file at
var/templates/{instance_id}/{area_name}/{template_name}.cms.php. You can edit it there with a
code editor and manage it with Git. camindo CMS applies changes every minute (or immediately via
“Refresh” in the module Templates) and recompiles the template. Rename templates in the back end –
in the file system, a rename counts as deleting and creating anew, and the assignments are lost.
Error messages appear in the widget “Template messages”.
Editor support
For Visual Studio Code there is the extension “camindo CMS Templates”
(github.com/dirkintelligence/camindo-vscode,
also in the Visual Studio Marketplace and on Open VSX). In *.cms.php files it colors the
CMS commands purple and the PHP expressions inside them orange – like the view in the
backend –, completes the command names after (CMS: and then the parameters of the command,
and shows their description on hover, in the same language as the backend.
The extension knows the commands of the core and the standard modules. If your modules bring their own commands, write the command list of your project with
php Core/cron/camindo.php template commands --all --out=var/vscode
and enter the directory in the VS Code setting camindo.commandsFile.
Scheduled templates
A template with the usage “Scheduled (scheduler)” (tab “Access” of the template) does not run as a page, but as a task of the scheduler (task type “Template”; see Handbook for administrators). It can never be reached via an address.
It runs like a page call without a record: in the website of its template area, in the language that the task names – with its texts, snippets, date formats and time zone. There is no visitor, hence no sign-in and no shopping cart. The task passes the time window since the last successful run:
| Variable | Contents |
|---|---|
$set['last_run'] |
start of the time window (exclusive), time zone of the language |
$set['this_run'] |
end of the time window (inclusive), time zone of the language |
$set['last_run_utc'], $set['this_run_utc'] |
the same in UTC |
On the first run, the time window is empty. If a run fails, the next one catches up on the whole
period – nothing is lost and nothing is processed twice. The time window fits directly into a
filter; two conditions for the same field are combined with AND via filter_mode:
(CMS:SELECT embargo_mail
filter[area]=press_releases
filter[online_at] > {$set['last_run']}
filter[online_at] <= {$set['this_run']}
filter_mode[online_at]=AND
future=true
)
The result template then sends, for example with CMS:EMAIL (submit=1), one message
per hit; CMS:URL with absolute=true returns the complete address for links. If a command
reports an error, the run counts as failed. On request, the task saves what the template outputs
under var/custom/ – this is how a sitemap or a feed is created as a file.
Preview
The preview from the back end also shows unpublished records with their template. Lists in it
still show only published records, as do id=… and CMS:RELATION. In the preview,
$this->isPreview() returns true.
List templates for the back end
A content area can have a list template that is used only in the back end: for an additional
column of the record list and for the detailed preview in lookup fields. In the template, the
record of the row is available as $cms; the output commands CMS:TEXT, CMS:MEDIA … work as in
the frontend:
(CMS:MEDIA image_small width=60)
<small>(CMS:NUMBER price format="%.2f" suffix=" $")</small>
CMS:RELATION is helpful here to list the main field of linked records.
2 Template sets
Encapsulated functionality
A template set bundles one function of the website: templates, content areas, possibly records and snippets. Examples:
| Template set | Content areas | Templates |
|---|---|---|
| News | News items, categories | Overview with paging, detail page, category page, archive, teaser for the start page, RSS feed |
| Events | Dates, venues | Calendar, detail page with directions, upcoming events preview |
| Members area | Members (profile), posts (UGC) | Registration, confirmation, sign-in, forgotten password, edit profile, protected pages |
| Page builder | Pages, text block, image block, gallery (dependent areas) | Page, one building block per block type |
A good template set is ready to use right away: import it, enter contents, done. And it is adaptable to the individual structure of a website without the function having to be developed anew. It is passed on by XML export with the option “Optimize templates for import into a foreign database” (see Handbook for administrators).
Rules for portable template sets
One folder, one template area
Place the content areas and the template area of a set together in one folder. In this way, one click exports the whole set, and on import it is clear what belongs together.
Names and IDs directly in the command parameter
Templates, content areas and records are addressed in the templates by their ID or their name (or slug). In a foreign database these may already be taken; then the importer assigns new ones and adjusts the templates accordingly. The adjustment succeeds only if IDs, names and slugs stand directly in the command parameters. If a parameter contains a PHP variable that receives its value only at runtime, the adjustment fails.
Consistent field names
For your own template sets: use the same field names as in the rest of the website (title,
date, text, image, slug). Then cross-area lists – for example a timeline of news and
events – work without adjustment, and a template set can easily be combined with existing areas.
For template sets in other databases: use the name prefixes, so that the import does not produce type conflicts (see Reserved name prefixes).
Separate logic and design
Keep the logic (queries, filters, paging, form evaluation) together in a few templates, and the design in separate, small building blocks:
news_overview (CMS:SELECT news_list filter[area]=news … count=true)
news_list (CMS:RESULT_START) (CMS:INCLUDE news_teaser) (CMS:RESULT_END)
(CMS:JUMP_LIST …)
news_teaser <article class="teaser"> … (CMS:TEXT title) … </article>
Whoever adapts the set to a different design changes only news_teaser and the other design
building blocks; the function remains untouched.
Including the website layout
Use fixed header and footer templates; these can easily be replaced by existing ones.
Texts in a separate content area
Fixed labels (“Read more”, “Back to overview”, error messages from forms) belong in a separate
content area (for example “Labels”). They are included with CMS:TEXT, for example
(CMS:TEXT id=labels.readmore). In this way they can easily be adapted and translated for the
later website.
Labels as custom snippets
To keep area names and input field labels multilingual as well, custom snippets can be created (see Snippets module). The snippets used in the template set are automatically included in the export file.
Settings as a record
If the editorial team is to control the behavior of a set – number of entries per page, title of the overview page, an introductory text – create for this a small content area “Settings” with a single record and read it via its slug:
(CMS:TEXT intro id=news-settings)
(CMS:NUMBER entries_per_page id=news-settings set=limit)
(CMS:SELECT news_list filter[area]=news limit="{$set['limit']}" count=true)
Shipping sample content
A few sample records show right after the import how the set looks and works. Export them along with their images.
Documenting
Add a short description to the set – as a note in the folder or as a record: which templates are to be assigned as default templates, which layout templates are expected, which snippets and converters the set uses.
CSS as a template
Include the required CSS as a template – in this way the template set appears immediately in the intended design. The user can move the CSS into a file at any time. Note, however, that this template needs special header instructions; see Templates that deliver CSS, JavaScript or JSON.
Using a template set
- Import (module “Contents” on the start page → menu ⋮ above the record list → “Import”), preferably first in a test environment.
- Test with the preview – in particular links that the import log reports as “not assigned”.
- Adjust the header and footer, or replace them with your own.
- If the template set is a whole website: enter the start page ID or start page template in the instance.
- Clarify legal aspects; adjust texts and deletion policies if necessary.
3 Trigger templates
A trigger template contains PHP methods that camindo CMS calls on events of the records of a content area – when creating, saving, deleting, restoring, and for the actions of a deletion policy. In this way, contents can be prepared on saving (combine several fields into one, check values), linked data can be updated, or personal data can be anonymized according to your own rules.
The methods run regardless of where the change comes from: input form, frontend (CMS:SAVEDATA,
CMS:USER_…), API, import, batch processing, the trash cleanup job, and deletion policies.
Assignment
A trigger template is an ordinary template in a template area. You assign it in the settings of the content area under “Trigger templates” (see Handbook for administrators). An area can have several trigger templates, and the same template may be assigned to several areas. In this way, reusable building blocks can be formed – for example a template that maintains slugs in all areas, and alongside it one with the rules of a single area.
Order
The templates of an area run one after another in the displayed order for each event; you change the order with the arrow buttons next to each entry.
- A template without the method of the event is skipped.
- All of them receive the same variables: what one template changes in
$dataor$affectedFields, the next one already sees. - If a template throws an exception, the following ones no longer run, and the process is cancelled (see Cancelling).
- Each template is an object of its own: properties and helper methods with the same name in
different templates do not interfere with each other. Exchange data between templates via
$data.
Structure
The template contains method definitions only – no output, no statements outside the methods. camindo CMS places the compiled code into the body of a class; each method thus becomes a method of an object:
<?php
// Trigger template "members_trigger" for the area Members
function onSave(array &$affectedFields, array &$data, array &$previousData): void
{
// Display name from first and last name
$data['name'] = trim(($data['first_name'] ?? '') . ' ' . ($data['last_name'] ?? ''));
if (!in_array('name', $affectedFields, true)) $affectedFields[] = 'name';
}
private function normalize(string $text): string
{
return trim(preg_replace('/\s+/', ' ', $text));
}
- All event methods are optional. If one is missing, nothing happens for that event.
- Your own helper methods (
private function …), properties and constants are allowed. - CMS:SELECT is allowed to determine contents of linked records (note the remarks below!).
- Address classes by their full name (
\Camindo\Core\Cms,\DateTimeImmutable) –usedoes not work, because the code sits in the middle of a class. - A template that cannot be loaded (syntax error, output outside the methods) aborts the process with an error message. Saving records without the intended processing would be worse.
What is available in the template
$this->cms |
Cms – snippet(), error(), select(), fetch() … |
$this->db |
Database – your own queries |
$this->record |
Record – the record that triggers the event (load(), save() …) |
$this->areaId |
Area for which the template was loaded. If it is assigned to several areas, take the area from $data['area_id'] |
$this->temporary |
true when a working copy is saved temporarily (tab change in the input form) |
$this->email, $this->pagination … |
the services that templates also know |
$cms['record_id'] |
number of the triggering record – only this, for the CMS commands; in onSave() that of its working copy (see Querying linked contents) |
Events
| Method | When |
|---|---|
onCreate(array &$data): void |
a new record is prefilled – values in $data appear in the empty mask |
onLoaded(array &$data): void |
after each loading of a record (input form, state before saving …) – not with CMS:SELECT in the frontend |
onSave(array &$affectedFields, array &$data, array &$previousData): void |
before the check and the writing. A field that the method sets anew must be added to $affectedFields |
onSaved(array &$affectedFields, array &$data, array &$previousData): void |
after writing, before the transaction is completed |
onSoftDelete(array $data): void |
the record goes into the trash |
onRestore(array $data): void |
the record comes back from the trash |
onDelete(array $data): void |
permanent deletion, before the record is deleted in the database |
onDeleted(array $data): void |
permanent deletion, after it |
onPrivacyAction(string $action, array &$data, array $context): void |
action of a deletion policy – see below |
$previousData is the saved state before the change (empty for a new record).
Cancelling
All events except onCreate and onLoaded run in the transaction of the process. An exception
cancels it and reverts it completely; the message appears in the input form or as an error of the
API:
function onSave(array &$affectedFields, array &$data, array &$previousData): void
{
if (($data['end'] ?? '') !== '' && $data['end'] < ($data['start'] ?? '')) {
throw new \Exception('The end is before the start.');
}
}
Temporary saving
If an editor switches tabs in the input form, camindo CMS saves the interim state of the working
copy – onSave and onSaved also run then, with $this->temporary = true. Time-consuming or
outward-facing steps (emails, interfaces) belong after if ($this->temporary) return;.
Moving
If a record is moved to another area, the trigger templates of the target area apply.
$data already contains the fields of the target area there, $previousData the state in the
source area with its field names.
Re-entry
If a template saves the same record again in onSaved(), it would run endlessly. An event is
therefore not nested for the same record within the same template: when saving again, the other
trigger templates of the area run, but the one currently running does not. Other records – also of
the same area – trigger their triggers normally.
Querying linked contents
The contents of the triggering record are in $data (new state) and $previousData (previous
state) – multilingual fields with all languages. Read and change them there, not via CMS:TEXT.
For other records, such as the linked ones, you can use CMS:SELECT as in any template.
While a method runs, $cms contains only record_id, the number of the triggering record. A
CMS:SELECT with relation therefore needs no src_id – and should not have one (see below):
function onSave(array &$affectedFields, array &$data, array &$previousData): void
{
$isVariant = false;
?>
(CMS:SELECT
relation=variant_of
state=any archive=true future=true
resulttemplate=shop.trigger_variant_found
)
<?php
$data['is_variant'] = $isVariant ? 'yes' : '';
if (!in_array('is_variant', $affectedFields, true)) $affectedFields[] = 'is_variant';
}
The result template (shop.trigger_variant_found) runs in the body of the method: it sees the
variables of the method and $this, and here only sets $isVariant = true;. It must not output
anything.
Observe the limits:
-
New state of the links via
$cms['record_id']. While an existing record is being edited, its links – outgoing as well as incoming – hang on a working copy; only after saving do they pass over to the record. InonSave()– and inonSaved()during temporary saving –$cms['record_id']therefore contains the number of the working copy:CMS:SELECT relation=…and CMS:RELATION without a source specification already see the changed links, and so does the reverse direction (“Who refers to this record?”) withdest_id="{$cms['record_id']}"orfilter[field]="{$cms['record_id']}".$data['record_id'], on the other hand, is always the number of the record itself – with it you get the previous state inonSave(). InonSaved()after the final saving, and in all other methods, both numbers are the same. -
Embedded contents during temporary saving: Links of a field with embedded contents point, during temporary saving, to their working copies, which
CMS:SELECTdoes not find. On final saving these contents are already taken over and are found. -
Publication:
CMS:SELECTfinds here only what the website would show – released, publication date reached, not expired. If the question concerns the structure (“Is any record linked at all?”), setstate=any archive=true future=true; otherwise blocked, pre-dated and expired records are missing.CMS:SELECTnever finds records in the trash. -
Rights: The query sees what the triggering user may see. The result can therefore differ depending on who saves:
Trigger What CMS:SELECTfindsUser in the back end or via the API only contents of the instances, areas and languages that their user group may read (superusers: all) Website ( CMS:SAVEDATA,CMS:USER_…)contents of the instance and its readable instances Scheduler (also due blocks of a deletion policy later on) all -
Language: Multilingual fields of the records found appear in one language – on the website in the language of the page, in the back end in the content language of the user. All languages are returned by
$this->record->load($id).
onPrivacyAction: deletion policies
A deletion policy determines what happens when a record is deleted. What cannot be expressed with “Overwrite individual fields” – linked records, texts that are only partly personal, multilingual contents per language – is handled by the trigger template with two actions:
| Action of the policy | Call |
|---|---|
| “Apply trigger template to the record” | once, $data is the record |
| “Apply trigger template to the audit log” | once for each entry in the audit log of the record, $data is the entry |
Each such block has an identifier (lowercase letters, digits, _). It arrives as $action –
the template uses it to tell the blocks apart, for example an immediate step from one after a year.
function onPrivacyAction(string $action, array &$data, array $context): void
$context |
|
|---|---|
target |
record or audit |
record_id |
the deleted record |
user_id |
the website user to whom it belongs (record.user_id), otherwise 0 |
area_id |
its area |
source |
trigger: record (record is being deleted), profile (UGC: profile was deleted) or area (area was deleted) |
policy_id, job_id |
policy and job |
deleted |
only with record: true if the record lies in the trash at that time |
On the record
$data contains all fields of the record – multilingual fields as an array language_id => value,
file fields with their metadata. camindo CMS then saves what the method changes in $data:
- in policy mode: without required-field and format checks and without the rights of the user group – “unique” still applies;
- not file fields,
record_id,area_id,user_idand the other identifiers. To remove a file, use “Overwrite individual fields” (image = "") in the same or a later block; - in the audit log as the action “adjusted by deletion policy” (
privacy), without the previous values.
If the record lies in the trash ($context['deleted']), the method runs as well – for example to tidy up
linked data – but nothing is saved on the record then. Other records the method processes itself via
$this->record or $this->db.
On the audit log
For each entry in the history of the record, the method is called with the entry:
| Key | |
|---|---|
id, t |
number and time – remain unchanged |
action |
create, update, delete, restore, purge, privacy … |
actor |
type (admin, user, system), admin_id, user_id, ip, origin – acting persons only as numbers, without names |
label |
designation of the record at that time |
changes |
list of the changes: k field name (multilingual field@en_US), l label, t field type, g language, b before, a after |
camindo CMS writes changed entries back – into the file under var/audit/ with a new checkpoint of the
checksum chain, and label also into the database. Entries that arise only later (for example during
permanent deletion) do not pass through the method; plan a later block for them.
If the area has several trigger templates with onPrivacyAction(), they run one after another with the
same identifier; each checks itself whether $action concerns it.
Errors
If none of the trigger templates of the area has onPrivacyAction(), or if the method throws an exception,
the job fails: it appears in the log privacy with the message and is retried at the next run of the
scheduler task “Apply deletion policies” – at most 20 times. If a block that is due immediately fails,
the deletion does not abort: the remaining blocks run, and the failed one follows with the next run.
CMS:DATE
Outputs the date part of a date value, formatted.
(CMS:DATE field [parameter=value …])
Method: Cms::cmsDate()
Without key: the first value without = is the field
At least one of: field, useval
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
id |
record ID or slug |
(current record) |
Record to read from. Pure digit sequence = record_id, anything else = slug – the same rule as in the URL. | |
field ¹ |
field |
– | ² | Name of a record field whose content is used as the source. |
useval |
date value |
– | ² | Value in text form that is used as the source. |
format |
format specification |
– | ICU pattern, e.g. "dd.MM.y". Without specification, the language of the page determines the notation. | |
timezone |
true / false |
false |
true: The name of the time zone is appended (e.g. "Europe/Berlin"). | |
replace[…] |
text?text |
– | If the text before the question mark occurs in the formatted value, it is replaced by the text after the question mark (e.g. "Jan?January"). | |
ifempty |
text |
– | Text that is output if the source ('field' or 'useval') is empty. | |
instead |
text |
– | Text that is output instead of the source, if it is not empty. | |
prefix |
text |
– | Text that is placed before the formatted value, if it is not empty. | |
suffix |
text |
– | Text that is appended if the formatted value is not empty. | |
set |
array index of $set |
– | The formatted value is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
² at least one of the parameters marked with ² must be given.
[…] – the parameter accepts subkeys, for example name[key]=value.
Usage
Outputs the date part of a date/time value. The source is a date or date-time field
(meta fields online_at, created_at, changed_at or your own fields).
Without format, the language of the page determines the notation:
(CMS:DATE online_at) 07.03.2024 (de)
(CMS:DATE online_at format="EEEE, d MMMM y") Thursday, 7 March 2024
(CMS:DATE online_at format="y-MM-dd") 2024-03-07
Typical in lists:
<time datetime="(CMS:DATE online_at format='y-MM-dd')">(CMS:DATE online_at)</time>
CMS:DATE, CMS:DATETIME and CMS:TIME are the same command with a different
section; all three know the same parameters (CMS:DATETIME additionally autotime).
Parameters in detail
id
id reads from another record instead of the current one – record_id or slug,
exactly as in the URL:
(CMS:DATE online_at id=1977)
(CMS:DATE online_at id=imprint)
field
field is the main parameter and may be given without a key – both lines are the same:
(CMS:DATE field=online_at)
(CMS:DATE online_at)
useval
Instead of a field, useval takes a value directly from the template – usually a
PHP variable or a value stored earlier with set:
(CMS:DATE useval="2024-12-24")
(CMS:DATE useval="{$set['value']}")
(CMS:DATE useval="{$set['user']['created_at']}")
format
format is an ICU pattern (d day, MM month with two digits, MMMM month name, y year,
HH:mm time, EEEE weekday). Without format, the language of the page decides (in the back end, the locale of the user):
(CMS:DATE online_at format="d MMMM y")
Output: 7 March 2024
timezone
(CMS:DATE online_at timezone=true)
Output: 07.03.2024 Europe/Berlin
replace
replace replaces parts of text in the already formatted value – short form with ? as
separator, or one entry per search text:
(CMS:DATE online_at format="MMM y" replace="Jan.?January")
(CMS:DATE online_at format="MMM y" replace[Jan.]="January" replace[x]="y")
Output: January 2026 instead of Jan. 2026
ifempty
ifempty replaces the entire output if the source is empty; prefix and
suffix are then omitted:
(CMS:DATE offline_at prefix="<p>" suffix="</p>" ifempty="unlimited")
The placeholders for “no date” and “no end” that camindo CMS stores for an unlimited
offline_at (before 2 January 1000 or from 31 December 8999) also count as empty – with and
without format.
instead
instead replaces the value as soon as the source is not empty – useful if
only the presence matters:
(CMS:DATE offline_at instead="limited" ifempty="-")
prefix
prefix / suffix surround the value and appear only if it is not empty –
so no empty published on:
(CMS:DATE online_at prefix="published on " suffix="")
suffix
(CMS:DATE online_at suffix=" (first publication)")
See prefix.
set
set outputs nothing, but stores the result under $set['date'] for the
surrounding PHP or a later command:
(CMS:DATE online_at set=date)
<?php if ($set['date'] !== '') { ?> … <?php echo $set['date']; ?> … <?php } ?>
CMS:DATETIME
Outputs date and time of a date value, formatted.
(CMS:DATETIME field [parameter=value …])
Method: Cms::cmsDatetime()
Without key: the first value without = is the field
At least one of: field, useval
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
id |
record ID or slug |
(current record) |
Record to read from. Pure digit sequence = record_id, anything else = slug – the same rule as in the URL. | |
field ¹ |
field |
– | ² | Name of a record field whose content is used as the source. |
useval |
date value |
– | ² | Value in text form that is used as the source. |
format |
format specification |
– | ICU pattern, e.g. "dd.MM.y HH:mm". Without specification, the language of the page determines the notation. | |
autotime |
true / false |
false |
true: If no time is present (00:00), it is not displayed. | |
timezone |
true / false |
false |
true: The name of the time zone is appended (e.g. "Europe/Berlin"). | |
replace[…] |
text?text |
– | If the text before the question mark occurs in the formatted value, it is replaced by the text after the question mark (e.g. "Jan?January"). | |
ifempty |
text |
– | Text that is output if the source ('field' or 'useval') is empty. | |
instead |
text |
– | Text that is output instead of the source, if it is not empty. | |
prefix |
text |
– | Text that is placed before the formatted value, if it is not empty. | |
suffix |
text |
– | Text that is appended if the formatted value is not empty. | |
set |
array index of $set |
– | The formatted value is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
² at least one of the parameters marked with ² must be given.
[…] – the parameter accepts subkeys, for example name[key]=value.
Usage
Outputs date and time. Like CMS:DATE, but with the time part:
(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)
With autotime=true, the time disappears if none was set in the value
(00:00:00) – for appointments that last “all day”.
Parameters in detail
id
id reads from another record instead of the current one – record_id or slug,
exactly as in the URL:
(CMS:DATETIME changed_at id=1977)
(CMS:DATETIME changed_at id=imprint)
field
field is the main parameter and may be given without a key – both lines are the same:
(CMS:DATETIME field=changed_at)
(CMS:DATETIME changed_at)
useval
Instead of a field, useval takes a value directly from the template – usually a
PHP variable or a value stored earlier with set:
(CMS:DATETIME useval="2024-03-07 14:30:00")
(CMS:DATETIME useval="{$set['value']}")
format
format is an ICU pattern (d day, MM month with two digits, MMMM month name, y year,
HH:mm time, EEEE weekday). Without format, the language of the page decides (in the back end, the locale of the user):
(CMS:DATETIME online_at format="dd.MM.y HH:mm")
Output: 07.03.2024 14:30
autotime
Only together with format: if the value contains no time (00:00:00), the
time part is removed from the output:
(CMS:DATETIME event_at format="dd.MM.y HH:mm" autotime=true)
Output for 2024-03-07 00:00:00: 07.03.2024 – for 2024-03-07 14:30:00: 07.03.2024 14:30
timezone
timezone=true appends the name of the time zone in which the value is displayed:
(CMS:DATETIME online_at timezone=true)
Output: 07.03.2024, 14:30 Europe/Berlin
replace
replace replaces parts of text in the already formatted value – short form with ? as
separator, or one entry per search text:
(CMS:DATETIME changed_at replace=", ? at ")
(CMS:DATETIME changed_at replace[, ]=" at " replace[x]="y")
Output: 07.03.2024 at 14:30
ifempty
ifempty replaces the entire output if the source is empty; prefix and
suffix are then omitted:
(CMS:DATETIME event_at prefix="<p>" suffix="</p>" ifempty="Date to follow")
The placeholders for “no date” and “no end” that camindo CMS stores for an unlimited
offline_at (before 2 January 1000 or from 31 December 8999) also count as empty – with and
without format.
instead
instead replaces the value as soon as the source is not empty – useful if
only the presence matters:
(CMS:DATETIME event_at instead="Date confirmed" ifempty="-")
prefix
prefix / suffix surround the value and appear only if it is not empty –
so no empty Updated::
(CMS:DATETIME changed_at prefix="Updated: " suffix="")
suffix
(CMS:DATETIME changed_at suffix=" o'clock")
See prefix.
set
set outputs nothing, but stores the result under $set['changed'] for the
surrounding PHP or a later command:
(CMS:DATETIME changed_at set=changed)
<?php if ($set['changed'] !== '') { ?> … <?php echo $set['changed']; ?> … <?php } ?>
CMS:EMAIL
Sends an email. On request, checks the required fields of a form in advance.
(CMS:EMAIL [parameter=value …])
Method: Email::cmsEmail()
At least one of: subject, subjecttemplate, body, bodytemplate
PHP snippet: include_and_stop (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
to[…] |
email address |
– | yes | Recipient email address. Several recipients as to[]=… or comma-separated (at most 20). |
toname |
text |
– | Recipient name (only with exactly one recipient). | |
from |
email address |
email.noreply or email.reply |
Sender email address. | |
fromname |
text |
– | Sender name. | |
contenttype |
text/plain or text/html |
text/plain |
MIME type of the email. | |
subject |
text |
(no subject) |
² | Subject of the email. |
subjecttemplate |
template name |
– | ² | Template for the subject (tags and line breaks are removed). Takes precedence over subject. |
body |
text |
– | ² | Message text (text or HTML). |
bodytemplate |
template name |
– | ² | Template for the email body. Takes precedence over body. |
paths_absolute |
true / false |
false |
true: All server-relative paths in the email body are converted into absolute paths (only with contenttype="text/html"). |
|
sender_url |
(http://)domain |
protocol and main domain of the instance | (Optionally with protocol) + domain, so that server-relative paths can be converted into absolute paths. | |
debug |
true / false |
false |
true: The email is written to the log file var/log/{date}_mail-debug.log, but not sent. |
|
requiredfields |
field names, comma-separated |
– | Specification of the required fields. Field names (index names) in the request array data[…] (see parameter data). |
|
data |
PHP variable name |
data |
Name of the request array that contains the data expected by requiredfields (default data, i.e. $_POST['data']). |
|
submit |
text value |
– | yes | The command runs only if submit contains a value. Otherwise the CMS:EMAIL command is ignored. |
successtemplate |
template name |
– | Template that is included after successful sending; the page then ends (unless with CMS:CONTINUE). |
|
message[…] |
text |
– | Error message. In the index: use case (see Messages); in the value: message text. | |
attachment[…] |
path |
– | Files to be attached. Path relative to the var directory, only from download/, upload/ and modules/. |
|
throttle |
true / bucket name |
– | Throttling per origin (IP): true = at most five emails per hour (user.throttle.email_ip), otherwise a separate limit from user.throttle. Throttled: message throttled, no email. |
|
formguard |
true / false / seconds |
false |
true: Form protection against robots – the form must contain the fields from (CMS:USER_TOKEN formguard=true) and may be sent no earlier than after 3 seconds (user.formguard_min_seconds). A number sets the minimum time for this form (formguard=5), 0 switches it off. Otherwise message throttled. |
² at least one of the parameters marked with ² must be given.
[…] – the parameter accepts subkeys, for example name[key]=value.
Usage
Contact form with a required-field check and confirmation page:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:EMAIL
submit="$submit"
to="info@example.com"
fromname="Contact form"
subject="Contact request"
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">Send</button>
</form>
The process:
- On the first call,
submitis empty – the command does nothing, the form appears. - After submission, it checks the required fields. If one is missing, nothing is sent;
CMS:MESSAGEshows the message, and the template continues – the form appears again. - Otherwise it builds the email, sends it and includes
successtemplate. The page ends afterwards. Withoutsuccesstemplate, the template simply continues to run.
Sending is done via the SMTP mailbox entered in the configuration (email.smtp), or
alternatively via the PHP function mail() – like all emails of the CMS.
If sending fails, the reason is stored in var/log/{date}_mail.log (module
Log files, type “Undelivered emails”).
What the command leaves behind
| Variable | Content |
|---|---|
$set['email_sent'] |
true after successful sending (or with debug=true after logging), otherwise false |
$set['email_missing'] |
Names of the empty required fields, e.g. to highlight them in the form |
<input name="data[email]" class="<?php echo in_array('email', $set['email_missing'] ?? []) ? 'error' : ''; ?>">
Messages
Errors end up in CMS:MESSAGE. message[index] determines the text,
otherwise the default text applies:
| Index | Case | Default text |
|---|---|---|
required |
a required field is empty | Please fill in all required fields. |
to |
no or an invalid recipient | The email has no valid recipient. |
from |
invalid sender (also: none configured) | The email has no valid sender. |
content |
subjecttemplate/bodytemplate missing or aborts |
The content of the email could not be generated. |
attachment |
an attachment is missing or lies outside the permitted directories | An attachment of the email was not found. |
send |
sending has failed | The email could not be sent. |
throttled |
throttling (throttle) or form protection (formguard) |
Too many requests. Please try again later. |
The default texts appear in the language of the page.
Security
- Never take the recipient from the form. If an address in
tois an input of the visitor, anyone can send emails to arbitrary addresses through your form. If one of the addresses intois invalid, the command does not send at all. - Throttle contact forms:
throttle=truelimits the emails per visitor,formguard=true(with(CMS:USER_TOKEN formguard=true)in the form) keeps robots at bay:(CMS:EMAIL submit="$submit" to="info@example.com" … throttle=true formguard=true). Order confirmations and other emails whose recipient you determine yourself do not need this. - Sender: The sender is always the website operator, never the visitor: many
mail servers accept only emails whose sender matches their own domain. It is therefore better to put the visitor's address into the text than
into
from; withoutfrom, the sender address from the configuration applies. - Line breaks in subject, names and addresses are removed – injected
headers (for example an additional
Bcc:) are thereby excluded. - HTML emails: output the visitor's input in the body template with
(CMS:TEXT useval="{$_POST['data']['…']}" specialchars=true). - Attachments only from
var/download/,var/upload/andvar/modules/– configuration, logs and backups cannot be attached.
Parameters in detail
to
(CMS:EMAIL to="info@example.com" …)
(CMS:EMAIL to[]="info@example.com" to[]="sales@example.com" …)
toname
Applies only if exactly one recipient is specified:
(CMS:EMAIL to="info@example.com" toname="Example Ltd." …)
from
(CMS:EMAIL from="shop@example.com" …)
Without specification, the sender address from the configuration (email.noreply, alternatively
email.reply). If none of these is entered, nothing can be sent without from.
fromname
(CMS:EMAIL fromname="Contact form Example Ltd." …)
contenttype
(CMS:EMAIL contenttype="text/html" bodytemplate=newsletter_html …)
With text/html, the CMS additionally creates a plain-text version for email programs without
HTML display.
subject
(CMS:EMAIL subject="Contact request" …)
subjecttemplate
Creates the subject from a template (e.g. with fields of the current record). Tags, entities and line breaks are removed:
(CMS:EMAIL subjecttemplate=order_subject …)
body
(CMS:EMAIL body="New request from {$_POST['data']['name']}" …)
bodytemplate
Message text from a template – there all CMS commands are available, the current
record, $set and the form data in $_POST. Variables that you have set with PHP in the
calling template are not visible to the body template; store them in $set if necessary.
(CMS:EMAIL contenttype="text/html" bodytemplate=contact_mail …)
contact_mail:
<p>New request from (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 belongs with HTML emails. In a plain-text email (text/plain, default),
visitor text cannot execute anything; specialchars would only make entities such as &
visible there.
Template names can be specified with the area as with CMS:INCLUDE
(area.name).
paths_absolute
Makes server-relative paths (/media/…) in src, href, action, background, poster
and url(…) of the HTML body absolute:
(CMS:EMAIL contenttype="text/html" bodytemplate=newsletter_html paths_absolute=true …)
sender_url
Domain for paths_absolute, if the protocol and main domain of the instance should not apply;
without a protocol, that of the instance applies:
(CMS:EMAIL paths_absolute=true sender_url="https://www.example.com" …)
debug
Writes the email only to the log file var/log/{date}_mail-debug.log, without sending it – for
testing a form. Otherwise the command behaves as with real sending
(required fields, successtemplate):
(CMS:EMAIL debug=true …)
requiredfields
Required fields in the request array data[…] (or the array named via data); if one
is missing, nothing is sent and CMS:MESSAGE shows the message. For fields with several
values (checkbox group data[topics][]), one selected value is sufficient:
(CMS:EMAIL requiredfields="name,email,message" …)
data
Name of the request array with the form data (default data, i.e. $_POST['data']):
<input name="form[name]">
(CMS:EMAIL data=form requiredfields="name" …)
submit
The command runs only if submit has a value – so not on the first call of the form:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:EMAIL submit="$submit" …)
<button name="submit" value="1">Send</button>
successtemplate
After successful sending, this template is included and the page ends (without CMS:CONTINUE):
(CMS:EMAIL successtemplate=contact_thanks …)
message
Own message texts per error case (indexes see Messages):
(CMS:EMAIL message[required]="Please fill in all required fields." …)
attachment
Attaches files relative to the var directory – only from download/, upload/ and
modules/:
(CMS:EMAIL attachment[]="download/terms.pdf" …)
throttle
(CMS:EMAIL throttle=true …)
(CMS:EMAIL throttle=newsletter …)
A separate name needs a limit in the configuration (user.throttle.newsletter,
system administrator); if it is missing, the limit of true applies.
formguard
(CMS:EMAIL formguard=true …)
<form method="post">(CMS:USER_TOKEN formguard=true) …</form>
CMS:MEDIA
Creates an image tag or outputs the content of a media file in text form.
(CMS:MEDIA field [parameter=value …])
Method: Cms::cmsMedia()
Without key: the first value without = is the field
At least one of: field, useval
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
id |
record ID or slug |
(current record) |
Record to read from. Pure digit sequence = record_id, anything else = slug – the same rule as in the URL. | |
field ¹ |
– | – | ² | Name of an upload or thumbnail field. |
subfield |
path / file / filename / mime_type / extension / size / width / height / version |
– | Single value instead of an IMG tag: path (path from DOCUMENT_ROOT for src/href, with a readable file name; with auth/download/user_auth via file.php), file (absolute path in the file system), filename (original file name), mime_type, extension (without dot), size (bytes; readable with autoformat), width / height (pixels, 0 if unknown), version (version counter of the file). The single value is not escaped. | |
auth |
true / false |
false |
true: The link points to /file.php instead of the media directory. For fields with "Only for signed-in frontend users" (user_auth) this applies automatically – file.php checks the frontend session there. | |
download |
text |
– | The file is offered for download via /file.php under the given file name (the file extension is added automatically). | |
width |
text |
– | Width of the IMG tag, with or without CSS unit (default: px). | |
height |
text |
– | Height of the IMG tag, with or without CSS unit (default: px). | |
attr[…] |
array |
– | Additional attributes for the IMG tag (key: attribute name, value: attribute value). The values are automatically escaped with htmlspecialchars. | |
autoformat |
true / false |
true |
true: The content selected via 'subfield' is formatted depending on its type (e.g. file sizes in bytes/KB/MB dynamically according to the value). | |
instead |
text |
– | Overrides the formatted value, if it is not empty. | |
ifempty |
text |
– | Alternative text if the formatted value is empty (or "0"). | |
prefix |
text |
– | Text that is placed before the formatted value, if it is not empty (or "0"). | |
suffix |
text |
– | Text that is appended if the formatted value is not empty (or "0"). | |
useval |
array |
– | ² | File array (e.g. from CMS:SAVEDATA) together with field_id and record_id – only via a direct PHP call, without field (see example). |
set |
array index of $set |
– | The formatted value is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
² at least one of the parameters marked with ² must be given.
[…] – the parameter accepts subkeys, for example name[key]=value.
Usage
The source is an upload field. Without subfield, an <img> tag is created with the
stored dimensions; alt is the original file name:
(CMS:MEDIA image)
Output: <img src="/media/1/12/1977,beach.jpg?v=3" alt="beach.jpg" width="1200" height="800">
The path carries the original file name after a comma (a readable URL for
browsers and search engines, as a slug without extension); the file itself is called 1977.jpg –
a rewrite rule in the media directory discards the name part without checking it. ?v= carries
the version number of the file: after a new upload, the address changes and the
browser reloads the file.
With subfield, a single value of the file is output – this way, arbitrary
tags can be built or downloads linked:
<a href="(CMS:MEDIA document subfield=path)">
(CMS:MEDIA document subfield=filename) ((CMS:MEDIA document subfield=size))
</a>
Output: <a href="/media/1/13/1977,terms.pdf?v=3">terms.pdf (123.5 KB)</a>
No image uploaded? ifempty provides the replacement – prefix/suffix are then omitted:
(CMS:MEDIA image width=300 height="" prefix="<figure>" suffix="</figure>" ifempty="<img src='/static/placeholder.png'>")
Available subfield values
| subfield | Content |
|---|---|
path |
Path from DOCUMENT_ROOT for src/href: /media/{instance}/{field}/{record},{name}.{extension} – for protected fields, auth=true and download, the /file.php call instead |
file |
absolute path in the server's file system (…/public/media/1/12/1977.jpg) – for file operations in PHP, never for links |
filename |
original file name (before the upload) |
mime_type |
e.g. image/jpeg |
extension |
file extension without dot, e.g. jpg |
size |
file size; readable with autoformat (default) (123.5 KB), in bytes with autoformat=false |
width / height |
image dimensions in pixels, 0 if not determinable |
version |
version counter of the file – increases with every new upload into the same field |
url is an alias for path (spelling from C9 preview versions). For multilingual
upload fields, the command delivers the file of the current language, or that of the
main language as a fallback (1977_2,beach.jpg).
Protected files (field option “Signed-in frontend users only”): the link points
automatically to /file.php, which checks the frontend session. auth=true forces this
route also for unprotected fields, download="name" offers the file for download.
Parameters in detail
id
id reads from another record instead of the current one – record_id or slug,
exactly as in the URL:
(CMS:MEDIA image id=1977)
(CMS:MEDIA image id=imprint)
field
Name of the upload field; may be given without a key:
(CMS:MEDIA field=image)
(CMS:MEDIA image)
subfield
Single value instead of an <img> tag (list see above):
<img src="(CMS:MEDIA image subfield=path)" alt="(CMS:TEXT title specialchars=true)" loading="lazy">
File type: (CMS:MEDIA document subfield=mime_type)
Single values via subfield are not escaped. path contains only
URL-safe characters and can stand directly in src or href. The original
file name (filename) is chosen by the uploader – in an attribute, escape it
via CMS:TEXT:
(CMS:MEDIA document subfield=filename set=filename)
<a href="(CMS:MEDIA document subfield=path)" title="(CMS:TEXT useval="{$set['filename']}" specialchars=true lbrconvert=false)">…</a>
path and file differ in purpose – a link for the browser versus access by PHP:
(CMS:MEDIA document subfield=path) /media/1/13/1977,terms.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 ? 'large file' : ''; ?>
auth
Force the link via /file.php – for example, so that a preview takes the same access
route as the protected original:
<p>Preview: (CMS:MEDIA thumb)</p>
<p>Original (signed-in only): (CMS:MEDIA image auth=true)</p>
For fields with the option “Signed-in frontend users only”, this applies automatically.
download
Offer the file for download; download is the file name when saving,
the extension is added:
<a href="(CMS:MEDIA document subfield=path download="GTC-2026")">Download terms</a>
Output: <a href="/file.php?file=13/1977&v=2&download=GTC-2026">Download terms</a>
width
Width of the <img> tag; without specification, the stored image width. An empty value
omits the attribute, and the browser then calculates proportionally:
(CMS:MEDIA image width=300 height="")
Output: <img src="/media/1/12/1977,beach.jpg?v=3" alt="beach.jpg" width="300">
height
(CMS:MEDIA image width="" height=200)
(CMS:MEDIA image width="100%" height="")
attr
Further attributes; key = attribute name. Existing ones (src, alt, width,
height) can be overridden this way:
(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 escapes all attribute values of the <img> tag itself with
htmlspecialchars – including those from attr. Therefore pass unescaped text,
without specialchars=true; otherwise escaping happens twice and & becomes visible as
&. From the attribute name, the command removes all characters except letters,
digits, _ and -.
autoformat
Affects only subfield=size: readable by default (123.5 KB), in bytes with false:
(CMS:MEDIA document subfield=size) 123.5 KB
(CMS:MEDIA document subfield=size autoformat=false) 126464
instead
Fixed text if a file is present (the tag or the single value itself is not output):
Image: (CMS:MEDIA image instead="present" ifempty="missing")
ifempty
Replacement if no file is present (or the single value is empty or 0):
(CMS:MEDIA image ifempty="<img src='/static/placeholder.png' alt=''>")
(CMS:MEDIA image subfield=width ifempty="unknown")
prefix
prefix / suffix surround the value and appear only if it is not empty –
so no empty <figure></figure>:
(CMS:MEDIA image prefix="<figure>" suffix="</figure>")
suffix
(CMS:MEDIA image suffix="</figure>")
See prefix.
useval
Passes a file array instead of a field – e.g. from a record saved with CMS:SAVEDATA.
Parameter values in the template are always strings; an array can therefore only be passed
via a direct PHP call of the method. field must not be specified in this case –
it would take precedence and read the field of the current record.
Because the file array does not contain the field id, field_id and record_id
belong in it explicitly:
(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 stands here for the id of the upload field portrait. Almost always simpler is
(CMS:MEDIA portrait id="{$cms['author_id']}") – id loads the other record
directly and finds the field by its name.
set
set outputs nothing, but stores the result under $set['img'] for the
surrounding PHP or a later command:
(CMS:MEDIA image subfield=path set=img)
<?php if ($set['img'] !== '') { ?> … <?php echo $set['img']; ?> … <?php } ?>
CMS:MESSAGE
Produces a text output from current error messages.
(CMS:MESSAGE useval [parameter=value …])
Method: Cms::cmsMessage()
Without key: the first value without = is the useval
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
useval ¹ |
text |
– | Value in text form that is used as the source. | |
prefix |
text |
– | Text that is output before the messages, if there are any. | |
suffix |
text |
– | Text that is output after the messages, if there are any. | |
ifempty |
text |
– | Text if no message is present. | |
instead |
text |
– | Text that is output instead of the messages. | |
set |
array index of $set |
– | The message is not displayed, but written to the variable $set. |
¹ can be given without a key as the first value.
Usage
Outputs the messages that commands on this page have stored – typically
field errors from CMS:USER_INPUT, CMS:USER_LOGIN or CMS:USER_REGISTER. Without a message,
the command stays silent:
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=welcome)
<p class="error">(CMS:MESSAGE)</p>
Several messages are separated by <br>. A frame is only possible through
prefix/suffix – it appears only if there are messages:
(CMS:MESSAGE prefix="<div class=alert>" suffix="</div>")
Parameters in detail
useval
Output your own text instead of the system messages (the messages are then ignored;
if useval is empty, the messages appear again):
(CMS:MESSAGE useval="{$set['hint']}" prefix="<p class=hint>" suffix="</p>")
The text is output unchanged. Do not insert values from the request
($_GET, $_POST) here – for that, use (CMS:TEXT useval="…" specialchars=true).
prefix
(CMS:MESSAGE prefix="<div class=alert>" suffix="</div>")
Without specification, the messages are output without a frame.
suffix
(CMS:MESSAGE prefix="<ul class=errors><li>" suffix="</li></ul>")
ifempty
Text if no message is present:
(CMS:MESSAGE ifempty="Everything is in order.")
instead
Fixed text as soon as a message is present – if the wording should not be shown:
(CMS:MESSAGE instead="Please check your entries.")
set
Provide messages for PHP, for example to show them next to the respective field:
(CMS:MESSAGE set=errors)
<?php if ($set['errors'] !== '') { ?><div class="alert"><?php echo $set['errors']; ?></div><?php } ?>
CMS:NUMBER
Outputs a numeric value, formatted.
(CMS:NUMBER field [parameter=value …])
Method: Cms::cmsNumber()
Without key: the first value without = is the field
At least one of: field, useval
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
id |
record ID or slug |
(current record) |
Record to read from. Pure digit sequence = record_id, anything else = slug – the same rule as in the URL. | |
field ¹ |
field |
– | ² | Name of a record field whose content is used as the source. |
useval |
number |
– | ² | Value in text form that is used as the source. |
ifempty |
text |
– | Text that is output if the source is empty or the numeric value (after mul/add) is 0. | |
instead |
number |
– | Number that becomes the source, if the original source is not empty. | |
mul |
number |
1 |
Number by which the source is multiplied. | |
add |
number |
0 |
Number that is added to the source. | |
format |
format |
– | Formatting analogous to the PHP command sprintf(). | |
decimals |
number 0-10 |
– | Fixed number of decimal places (rounded); decimal and thousands_separator still apply. Without specification, as many as the number has. |
|
decimal |
character |
. |
Character that replaces the decimal point. | |
thousands_separator |
character |
– | Character inserted as thousands separator (every three digits from right to left). | |
nullcent |
text |
– | Text that replaces a missing decimal value. | |
replace[…] |
text?text |
– | If the text before the question mark occurs in the formatted value, it is replaced by the text after the question mark (e.g. ".00?" removes empty decimal places). | |
prefix |
text |
– | Text that is placed before the formatted value, if it is not "0" or empty. | |
suffix |
text |
– | Text that is appended to the formatted value, if it is not "0" or empty. | |
set |
array index of $set |
– | The formatted value is not output, but stored in the variable $set instead. | |
sizefactor |
number of bytes in a kilobyte |
– | 1000 or 1024. If > 0, a readable file size is calculated (e.g. 15.2 GB). |
¹ can be given without a key as the first value.
² at least one of the parameters marked with ² must be given.
[…] – the parameter accepts subkeys, for example name[key]=value.
Usage
Outputs a numeric value, formatted. Without further parameters, the number appears as it is stored – with a point as decimal separator and without thousands separator:
(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 €
Calculating before output (mul, add), making file sizes readable (sizefactor):
(CMS:NUMBER price mul=1.19 decimals=2 decimal="," suffix=" € incl. VAT")
(CMS:NUMBER filesize sizefactor=1024) 15.2 MB
If the calculation results in 0, 0 is output (formatted, e.g. 0.00 with decimals=2):
(CMS:NUMBER useval="{$a}" add=-1) 0 (with $a = 1)
As soon as prefix, suffix or ifempty is given, however, the value 0 counts as
empty – a price of 0 therefore stays without €. What counts is the numeric value after
mul/add, not the formatted text: even 0.00 (with decimals or format) then counts as
empty. An empty source (field without content, empty useval) always results in empty
output – also with mul/add.
Parameters in detail
id
id reads from another record instead of the current one – record_id or slug,
exactly as in the URL:
(CMS:NUMBER price id=1977)
(CMS:NUMBER price id=imprint)
field
field is the main parameter and may be given without a key – both lines are the same:
(CMS:NUMBER field=price)
(CMS:NUMBER price)
useval
Instead of a field, useval takes a value directly from the template – usually a
PHP variable or a value stored earlier with set:
(CMS:NUMBER useval="{$this->db->num_rows_unlimited}")
(CMS:NUMBER useval="{$set['value']}")
A decimal comma is understood: useval="3,5" gives 3.5.
ifempty
With ifempty, the numeric value 0 also counts as empty – regardless of how it would be formatted:
(CMS:NUMBER stock ifempty="sold out" suffix=" pcs")
Output for stock = 0: sold out – for 12: 12 pcs
instead
instead replaces the source (not the result) if it is not empty –
calculation and formatting then run on the new value:
(CMS:NUMBER price instead=99 decimal=",")
mul
(CMS:NUMBER price mul=1.19 decimals=2 decimal="," suffix=" €") gross price
(CMS:NUMBER share mul=100 suffix=" %") 0.25 → 25 %
add
(CMS:NUMBER price add=4.90 decimals=2 decimal="," suffix=" € incl. shipping")
mul is applied before add: result = source × mul + add.
format
sprintf() pattern; it alone determines the output, so decimal, thousands_separator,
decimals and nullcent are then ignored. For prices with a decimal comma and
thousands point, decimals is therefore the right parameter; use format for special cases
such as leading zeros:
(CMS:NUMBER price format="%.2f") 1234.50
(CMS:NUMBER position format="%03d") 007
decimals
Fixed number of decimal places, rounded commercially – the separators from decimal
and thousands_separator still apply:
(CMS:NUMBER price decimals=2 decimal="," thousands_separator=".")
Output for 1234.5: 1.234,50 – for 19.999: 20,00
An amount in cents (field type currency amount) becomes euros with mul=0.01:
(CMS:NUMBER cur_price mul=0.01 decimals=2 decimal="," suffix=" €")
decimal
(CMS:NUMBER price decimal=",")
Output: 1234,5
thousands_separator
(CMS:NUMBER views thousands_separator=".")
(CMS:NUMBER views thousands_separator=" ")
Output: 1.234.567 or 1 234 567
nullcent
Replacement for missing decimal places (without specification: none) – for prices that should always show two digits:
(CMS:NUMBER price decimal="," nullcent="00" suffix=" €")
Output for 1234: 1234,00 € – for 1234.5 it remains 1234,5 € (only missing decimal places
are replaced; use decimals=2 for fixed rounding).
replace
replace replaces parts of text in the already formatted value – short form with ? as
separator, or one entry per search text:
(CMS:NUMBER price replace=".00?")
(CMS:NUMBER price replace[.00]="" replace[x]="y")
Output for 12.00: 12
prefix
Appears only if the formatted value is neither empty nor 0:
(CMS:NUMBER discount prefix="- " suffix=" %")
suffix
(CMS:NUMBER price suffix=" €")
See prefix.
set
set outputs nothing, but stores the result under $set['price'] for the
surrounding PHP or a later command:
(CMS:NUMBER price set=price)
<?php if ($set['price'] !== '') { ?> … <?php echo $set['price']; ?> … <?php } ?>
The stored value is a string in the formatted notation.
sizefactor
Makes a file size readable (bytes → KB / MB / GB); 1024 for binary, 1000 for
decimal units:
(CMS:NUMBER filesize sizefactor=1024) 15.2 MB
(CMS:NUMBER filesize sizefactor=1000) 15.9 MB
For upload fields, this also works directly with (CMS:MEDIA image subfield=size).
CMS:RAW
Outputs the content unformatted.
(CMS:RAW field [parameter=value …])
Method: Cms::cmsRaw()
Without key: the first value without = is the field
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
id |
record ID or slug |
(current record) |
Record to read from. Pure digit sequence = record_id, anything else = slug – the same rule as in the URL. | |
field ¹ |
field |
– | yes | Name of the field whose content is output. |
ifempty |
text |
– | Text that is output if the field is empty. | |
instead |
text |
– | Text that is output instead of the field content, if it is not empty. | |
prefix |
text |
– | Text that is placed before the field content, if it is not empty. | |
suffix |
text |
– | Text that is appended if the field content is not empty. | |
set |
array index of $set |
– | The content is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
Usage
Outputs the field content without any processing – no escaping, no conversion of line breaks, no entities. Intended for fields that contain finished HTML (editor fields) or for values that are to be passed unchanged to PHP/JavaScript:
<div class="content">(CMS:RAW html_text)</div>
You must trust the content: whatever an editor writes into the field ends up on the
page unchanged – except for active content (<script>, <iframe>, event handlers,
javascript: links), which camindo CMS already removes when saving and when reading
from the database. For texts from user input (ugc areas) use CMS:TEXT with
specialchars=true.
The output parameters ifempty, instead, prefix, suffix and set work as with
all output commands.
Parameters in detail
id
id reads from another record instead of the current one – record_id or slug,
exactly as in the URL:
(CMS:RAW html_text id=1977)
(CMS:RAW html_text id=imprint)
field
field is the main parameter and may be given without a key – both lines are the same:
(CMS:RAW field=html_text)
(CMS:RAW html_text)
ifempty
ifempty replaces the entire output if the source is empty; prefix and
suffix are then omitted:
(CMS:RAW html_text prefix="<p>" suffix="</p>" ifempty="<p>No content.</p>")
instead
instead replaces the value as soon as the source is not empty – useful if
only the presence matters:
(CMS:RAW html_text instead="present" ifempty="-")
prefix
prefix / suffix surround the value and appear only if it is not empty –
so no empty <section></section>:
(CMS:RAW html_text prefix="<section>" suffix="</section>")
suffix
(CMS:RAW html_text suffix="</section>")
See prefix.
set
set outputs nothing, but stores the result under $set['html'] for the
surrounding PHP or a later command:
(CMS:RAW html_text set=html)
<?php if ($set['html'] !== '') { ?> … <?php echo $set['html']; ?> … <?php } ?>
CMS:SNIPPET
Outputs a snippet (text module) in the language of the call.
(CMS:SNIPPET key [parameter=value …])
Method: Cms::cmsSnippet()
Without key: the first value without = is the key
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
key ¹ |
snippet key |
– | yes | Key of the snippet, e.g. custom.read_more (snippet set of the instance) or a core snippet without prefix. |
param[…] |
text |
– | Values for placeholders: param[name]=value replaces {name} in the snippet and fills ICU parameters (plural, select). |
|
specialchars |
true / false |
false |
true: Special characters are converted into their HTML equivalent (for attributes). | |
ifempty |
text |
– | Text that is output if the snippet does not exist (without ifempty, the key appears). |
|
prefix |
text |
– | Text that is placed before the snippet, if it is not empty. | |
suffix |
text |
– | Text that is appended if the snippet is not empty. | |
set |
array index of $set |
– | The text is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
[…] – the parameter accepts subkeys, for example name[key]=value.
Usage
Fixed labels of a website – “Read more”, “Add to cart”, headings
of lists – are maintained by the editorial team as snippets of the instance in all languages
(module Snippets, see Manual for administrators).
The template outputs them with CMS:SNIPPET, always in the language of the call; if a
translation is missing, the English text (en_US) appears:
<a class="more" href="(CMS:URL)">(CMS:SNIPPET custom.read_more)</a>
custom. refers to the snippet set of the instance. Without a prefix, the snippets of the core
are meant – on the front end only the general ones, not those of the back-end interface.
In an attribute, mask the text with specialchars=true:
<button aria-label="(CMS:SNIPPET custom.menu specialchars=true)">…</button>
Placeholders. param[name] replaces {name} in the snippet. This leaves the word order
to the translation – for example, the currency sign before or after the amount:
Snippet custom.price: en "€{amount}" de "{amount} €"
(CMS:NUMBER price mul=0.01 format="%.2f" set=amount)
(CMS:SNIPPET custom.price param[amount]="{$set['amount']}")
If a snippet begins with an ICU message ({count, plural, one {# item} other {# items}}),
param fills in its values:
(CMS:SNIPPET custom.hits param[count]="{$set['count']}")
Key from the record. The key may contain PHP variables – for example, to translate a selection value:
(CMS:SNIPPET "custom.status_{$cms['status']}" ifempty="–")
If the snippet does not exist, the key itself appears on the page – so a missing
text is noticed during development. ifempty outputs a replacement instead.
Inside a parameter of another command, CMS:SNIPPET cannot be nested; there,
{$this->cms->snippet('custom.read_more')} returns the text:
(CMS:TEXT teaser suffix=" {$this->cms->snippet('custom.read_more')}")
CMS:TEXT
Outputs a text with formatting applied.
(CMS:TEXT field [parameter=value …])
Method: Cms::cmsText()
Without key: the first value without = is the field
At least one of: field, useval, id
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
id |
record ID or slug |
– | ² | Record to read from. Pure digit sequence = record_id, anything else = slug – the same rule as in the URL. Without 'field', the field "text" is read. |
field ¹ |
field |
– | ² | Name of a record field whose content is used as the source. |
useval |
text |
– | ² | Value in text form that is used as the source. |
ifempty |
text |
– | Text that becomes the source if the original source is empty (further parameters, except 'set', are then ignored). | |
instead |
text |
– | Text that becomes the source if the original source is not empty. | |
entitydecode |
true / false |
true |
true: Entities in the format &...; are converted back to Unicode characters. | |
trim |
true / false |
false |
true: Spaces at the beginning and end of the text are removed. | |
striptags |
true / false |
false |
true: HTML and script tags are removed. | |
if[…] |
text?text |
– | If the source corresponds to the text before the question mark, the text after the question mark becomes the new source. | |
else |
text |
– | If none of the previous IF queries applies, this value becomes the source. | |
replace[…] |
text?text |
– | If the text before the question mark occurs in the source, it is replaced by the text after the question mark. | |
limit |
number >=0 |
– | Shortens the text to the given number of characters and appends "…"; with an appended s (e.g. 200s) the text is shortened at the last word end. | |
lbrconvert |
true / false |
true |
true: Line breaks are converted into HTML line breaks (<br> tags). | |
specialchars |
true / false |
false |
true: Special characters are converted into their HTML equivalent. | |
urlconvert |
true / false |
false |
true: Internet addresses are automatically converted into clickable links. | |
emailconvert |
true / false / encrypt |
false |
true: Email addresses are linked. encrypt: The links are lightly encrypted by means of JavaScript. | |
emaillimit |
>= 0 |
– | Automatically shortens linked email addresses to x visible characters. | |
emailtext |
text |
– | Linked text that is shown instead of an email address (only if "emailconvert" is enabled). | |
target |
window / frame |
_self |
Sets the link target for automatically generated links (urlconvert, emailconvert). | |
markdown |
true / false |
false |
true: The text is Markdown and is converted into HTML (HTML in the text is escaped); the other conversions do not apply. | |
urlencode |
true / false |
false |
true: The text is encoded according to RFC 3986 (rawurlencode) – for URL components, also in attributes without further escaping. | |
urllimit |
number of characters |
80 |
Maximum length of the visible text for automatically generated links (only suitable for external links!). | |
ampconvert |
true / false |
true |
true: A standalone & is automatically converted into the HTML entity &. | |
prefix |
text |
– | Text that is placed before the source, if the source is not empty. | |
suffix |
text |
– | Text that is appended to the source, if the source is not empty. | |
iso |
true / false |
false |
true: The output is converted into the character set ISO-8859-1. | |
charset |
text |
utf-8 |
Character set into which the output should be converted. | |
set |
array index of $set |
– | The formatted value is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
² at least one of the parameters marked with ² must be given.
[…] – the parameter accepts subkeys, for example name[key]=value.
Usage
The standard command for field contents. It reads a field of the current record, converts line
breaks into <br>, escapes standalone & and outputs the result:
<h1>(CMS:TEXT title)</h1>
<p>(CMS:TEXT text)</p>
Output a field only if it is filled – prefix/suffix appear only for a non-empty value:
(CMS:TEXT teaser prefix="<p class=teaser>" suffix="</p>")
Cache a value for PHP instead of outputting it:
(CMS:TEXT title set=title)
<title><?php echo $set['title']; ?> – Example Ltd.</title>
Translate a status value into text:
(CMS:TEXT status_id if[0]="online" if[1]="blocked" if[2]="unconfirmed" else="unknown")
Order of processing: source → if/else → replace → trim → striptags
→ limit → (markdown or urlencode end here) → entitydecode → specialchars or
ampconvert → urlconvert / emailconvert → lbrconvert → iso/charset →
ifempty / instead / prefix / suffix → output or set.
Parameters in detail
id
id reads from another record – record_id or slug, as in the URL. Only with CMS:TEXT may id
stand alone: then the field text is read. As in the URL, the record must be published
(released, publication date reached; an expired one remains readable), otherwise the value is
empty – also in the preview of the back end.
(CMS:TEXT title id=1977) Title of record 1977
(CMS:TEXT id=imprint) Field "text" of the record with the slug "imprint"
field
field is the main parameter and may stand without a key – both lines are the same:
(CMS:TEXT field=title)
(CMS:TEXT title)
useval
useval takes a value directly from the template instead of a field – usually a PHP variable or
a value previously stored with set:
(CMS:TEXT useval="{$_GET['q']}" specialchars=true)
(CMS:TEXT useval="{$set['value']}")
Always output values from the request ($_GET, $_POST) with specialchars=true – otherwise
a manipulated link can place arbitrary HTML and script into the page.
This also allows the text filters (striptags, specialchars, limit …) to be applied to
foreign values.
ifempty
ifempty replaces the entire output if the source is empty; prefix and suffix are then
omitted:
(CMS:TEXT teaser prefix="<p>" suffix="</p>" ifempty="No teaser available.")
Note: if the source is empty, all conversions are skipped as well – the ifempty text is output
unchanged.
instead
instead replaces the value as soon as the source is not empty – useful when only the presence
counts:
(CMS:TEXT image instead="✔ image available" ifempty="-")
entitydecode
By default, entities (&, ä, –) are converted back into characters. For content
that should keep the entities (for example for XML feeds):
(CMS:TEXT text entitydecode=false)
trim
(CMS:TEXT title trim=true prefix="[" suffix="]")
Output: [Hello world] instead of [ Hello world ]
striptags
Removes HTML and script tags, for example for teasers from an HTML field or for meta tags. Each
tag is replaced by a space (from <p>a</p><p>b</p> comes a b); the text between the tags remains:
<meta name="description" content="(CMS:TEXT text striptags=true limit=160 lbrconvert=false)">
if
Value table: if the value matches a key, the entry becomes the new source. Two notations – subkey or short form with a question mark:
(CMS:TEXT status_id if[0]="online" if[1]="blocked")
(CMS:TEXT status_id if="0?online" if[]="1?blocked")
Values that do not match remain unchanged – unless else is given.
If the replacement text of a numeric key itself contains a question mark, use the short form:
if[]="1?Really?". With if[1]="Really?" it would not be clear whether 1 is the search value or
whether the short form Really → empty was meant (in PHP both notations give the key 1). For text
keys (if[active]="Yes?") this does not apply.
else
(CMS:TEXT status_id if[0]="online" else="not online")
Output for status_id = 2: not online
replace
Replace partial texts in the source – short form with ? or one entry per search text:
(CMS:TEXT text replace="\n?<br><br>" lbrconvert=false)
(CMS:TEXT title replace[Ltd.]="Limited" replace[&]="and")
As with if, write replacement texts with a question mark for a numeric search text in the short
form (replace[]="0?zero?").
limit
Shortens a text to a number of characters; an appended s shortens at the last word end (“soft”).
The snippet ellipsis (…) is appended:
<p>(CMS:TEXT text striptags=true limit=200s)</p>
<td>(CMS:TEXT title limit=30)</td>
lbrconvert
By default, line breaks become <br>. Switch this off for content in <pre>, in attributes or in
JavaScript:
<pre>(CMS:TEXT code lbrconvert=false)</pre>
specialchars
Escapes all special characters (< → <, " → " …) – for attributes and for content
that must not contain HTML:
<input value="(CMS:TEXT title specialchars=true lbrconvert=false)">
urlconvert
Makes addresses (http://…, https://…, www.…) in the text clickable:
(CMS:TEXT text urlconvert=true target=_blank urllimit=24)
Output: <a href="https://example.com/a/very/long/address" target="_blank">https://example.com/a/very…</a>
emailconvert
Links email addresses; encrypt assembles the link by means of JavaScript, so that the address
does not appear in the source code. The <script> for this carries the nonce of the page and
therefore also runs under an enforced script policy (CMS:CSP):
(CMS:TEXT contact emailconvert=true)
(CMS:TEXT contact emailconvert=encrypt emailtext="Write an email")
emaillimit
Shortens the visible part of a linked address:
(CMS:TEXT contact emailconvert=true emaillimit=12)
Output: <a href="mailto:editorial@example.com">editorial@ex…</a>
emailtext
Fixed link text instead of the address (only with emailconvert):
(CMS:TEXT contact emailconvert=true emailtext="Get in touch")
target
Link target for the links generated by urlconvert:
(CMS:TEXT text urlconvert=true target=_blank)
markdown
Outputs a field that contains Markdown – for example one with the extension Markdown (visual) – and converts it into HTML:
<div class="text">(CMS:TEXT text markdown=true)</div>
Conversion is done with Parsedown; HTML in the text is escaped, and links to javascript: and
similar targets are neutralized. In addition to the usual Markdown, the parameter understands the
formats of the extension Markdown (visual):
| Markdown | Result |
|---|---|
++Text++ |
underlined (<u>) |
::: center … ::: |
the enclosed paragraphs centered (left, center, right; as <div style="text-align: …">) |
<br> |
line break, also in table cells |
­, … |
remain special characters |
markdown ends the processing: entitydecode, specialchars, lbrconvert, urlconvert and so on
do not apply. prefix, suffix, ifempty and set work as usual.
urlencode
Encodes the value as a URL component. All further conversions do not apply then:
<a href="/search?q=(CMS:TEXT title urlencode=true)">similar articles</a>
Output: /search?q=Hello%20world
urllimit
Visible length of automatically linked addresses (default 80 characters, 0 = not shortened):
(CMS:TEXT text urlconvert=true urllimit=30)
ampconvert
By default a standalone & becomes &; existing entities remain. Switch off if the content is
already fully escaped:
(CMS:TEXT html_snippet ampconvert=false)
prefix
prefix / suffix surround the value and appear only if it is not empty – so no empty
<p class=teaser></p> is created:
(CMS:TEXT teaser prefix="<p class=teaser>" suffix="</p>")
suffix
(CMS:TEXT author_name suffix=" (author)")
See prefix.
iso
Re-encodes the output as ISO-8859-1 (short form for charset=ISO-8859-1), for example for older
interfaces:
(CMS:TEXT title iso=true)
charset
(CMS:TEXT title charset=windows-1252)
set
set outputs nothing, but stores the result under $set['title'] for the surrounding PHP or a later
command:
(CMS:TEXT title set=title)
<?php if ($set['title'] !== '') { ?> … <?php echo $set['title']; ?> … <?php } ?>
CMS:TIME
Outputs the time of a date or time value, formatted.
(CMS:TIME field [parameter=value …])
Method: Cms::cmsTime()
Without key: the first value without = is the field
At least one of: field, useval
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
id |
record ID or slug |
(current record) |
Record to read from. Pure digit sequence = record_id, anything else = slug – the same rule as in the URL. | |
field ¹ |
field |
– | ² | Name of a record field whose content is used as the source. |
useval |
time value |
– | ² | Value in text form that is used as the source. |
format |
format specification |
– | ICU pattern, e.g. "HH:mm". Without specification, the language of the page determines the notation; full minutes are shown without seconds. | |
timezone |
true / false |
false |
true: The name of the time zone is appended (e.g. "Europe/Berlin"). | |
replace[…] |
text?text |
– | If the text before the question mark occurs in the formatted value, it is replaced by the text after the question mark (e.g. "Jan?January"). | |
ifempty |
text |
– | Text that is output if the source ('field' or 'useval') is empty. | |
instead |
text |
– | Text that is output instead of the source, if it is not empty. | |
prefix |
text |
– | Text that is placed before the formatted value, if it is not empty. | |
suffix |
text |
– | Text that is appended if the formatted value is not empty. | |
set |
array index of $set |
– | The formatted value is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
² at least one of the parameters marked with ² must be given.
[…] – the parameter accepts subkeys, for example name[key]=value.
Usage
Outputs only the time of a date/time value:
(CMS:TIME event_at) 14:30
(CMS:TIME event_at format="HH:mm:ss") 14:30:00
(CMS:TIME event_at suffix=" o'clock") 14:30 o'clock
Without format, full minutes are shown without seconds.
Parameters in detail
id
id reads from another record instead of the current one – record_id or slug,
exactly as in the URL:
(CMS:TIME event_at id=1977)
(CMS:TIME event_at id=imprint)
field
field is the main parameter and may be given without a key – both lines are the same:
(CMS:TIME field=event_at)
(CMS:TIME event_at)
useval
Instead of a field, useval takes a value directly from the template – usually a
PHP variable or a value stored earlier with set:
(CMS:TIME useval="2024-03-07 14:30:00")
(CMS:TIME useval="{$set['value']}")
format
format is an ICU pattern (d day, MM month with two digits, MMMM month name, y year,
HH:mm time, EEEE weekday). Without format, the language of the page decides (in the back end, the locale of the user):
(CMS:TIME online_at format="HH:mm")
Output: 14:30
h:mm a gives the 12-hour notation (2:30 PM).
timezone
(CMS:TIME event_at timezone=true)
Output: 14:30 Europe/Berlin
replace
replace replaces parts of text in the already formatted value – short form with ? as
separator, or one entry per search text:
(CMS:TIME event_at replace=":?.")
(CMS:TIME event_at replace[:]="." replace[x]="y")
Output: 14.30
ifempty
ifempty replaces the entire output if the source is empty; prefix and
suffix are then omitted:
(CMS:TIME event_at prefix="<p>" suffix="</p>" ifempty="all day")
instead
instead replaces the value as soon as the source is not empty – useful if
only the presence matters:
(CMS:TIME event_at instead="with time" ifempty="-")
prefix
prefix / suffix surround the value and appear only if it is not empty –
so no empty from o'clock:
(CMS:TIME event_at prefix="from " suffix=" o'clock")
suffix
(CMS:TIME event_at suffix=" o'clock")
See prefix.
set
set outputs nothing, but stores the result under $set['time'] for the
surrounding PHP or a later command:
(CMS:TIME event_at set=time)
<?php if ($set['time'] !== '') { ?> … <?php echo $set['time']; ?> … <?php } ?>
CMS:URL
Creates the URL of a record.
(CMS:URL id [parameter=value …])
Method: Cms::cmsUrl()
Without key: the first value without = is the id
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
id ¹ |
record ID or slug |
(current record) |
Record to link to. Pure digit sequence = record_id, anything else = slug – the same rule as in the URL. | |
area |
area name or ID |
– | Link to the overview of the area: the address made of its slug prefix (/en/events), which its overview template shows. |
|
template |
template name |
– | Template with which the record is to be shown (second part of the URL after the comma): name or area.name. Without specification, the template that the area assigns. | |
lang |
language code |
(current language) |
Language version to link to (by default that of the current call). If the instance runs more than one language, the code always appears in the URL; with only one language, never. A multilingual slug is inserted in the target language. A code that the website does not run is ignored. | |
protocol |
transfer protocol |
(protocol of the instance) |
http or https, only with absolute or host. Without specification, the protocol of the instance, otherwise that of the call. | |
host |
host name |
– | e.g. example.com to change the domain through which the website is called. | |
absolute |
true / false |
false |
true: The path is generated including protocol and domain. false: The path is server-relative and begins with a slash. | |
set |
array index of $set |
– | The URL is not output, but stored in $set. |
¹ can be given without a key as the first value.
Usage
Creates the front-end URL of a record – with the slug if it has one, otherwise with the record_id; for multilingual websites always with a language segment:
<a href="(CMS:URL)">(CMS:TEXT title)</a>
Output: <a href="/en/hello-world">Hello world</a>
Link to another view of the same record (template), to another record (id)
or into another language (lang):
(CMS:URL template=print) /en/hello-world,print
(CMS:URL 1977) /en/1977 (record without slug)
(CMS:URL imprint lang=de) /de/impressum (slug of the target language)
(CMS:URL colors/blue) /en/colors/blue (area with slug prefix)
If the content area of the record has a slug prefix (setting of the
area, e.g. colors), it precedes the slug: /en/colors/blue. The prefix applies
per language – with lang, therefore, the prefix of the target language. An id with a slug means
the path as it appears in the address (id=colors/blue); id=blue finds only a
record of an area without a prefix.
Language switcher – in a CMS:SELECT_LANGVERSION list template. The rows there
are languages, not records; CMS:URL without id therefore points to the
record of the page:
<a href="(CMS:URL lang="{$cms['code']}")">(CMS:TEXT endonym)</a>
Absolute URL for feeds, emails or og:url:
<link rel="canonical" href="(CMS:URL absolute=true)">
Parameters in detail
id
Record to link to; without specification, the current one. May be given without a key:
(CMS:URL 1977)
(CMS:URL id=imprint)
(CMS:URL "{$cms['parent_id']}") link to the parent node
area
Links to the overview of an area: the address that consists only of its
slug prefix, in the language of the call or from lang. It shows the overview template of the
area (setting of the area, “Template assignment”):
<a href="(CMS:URL area=events)">All events</a> /en/events
(CMS:URL area=events lang=de) /de/termine
If the area has no slug prefix, the output remains empty. In the overview template
itself there is no current record; CMS:URL without id therefore points there to
the same overview – a language switcher with CMS:SELECT_LANGVERSION therefore works
as on every record page.
template
Second part of the URL (after the comma): the template with which the record is to be
shown – name or area.name:
(CMS:URL template=print)
(CMS:URL template=articletemplates.print)
Output: /en/hello-world,print
lang
Language version to which the link points (code from language management); without specification, the language of the current call:
(CMS:URL lang=de)
If the website runs only one language, a language segment never appears in the URL. A code that the website does not run is ignored – the URL keeps the language of the call.
If the field slug of the area is multilingual, each language version has its own
slug – and the target URL must carry the slug of the target language, because it is resolved in
the language of the URL. CMS:URL therefore inserts it itself with lang:
Page /en/dining-table-nord-180
(CMS:URL lang=de) /de/esstisch-nord-180
(CMS:URL lang=sv) /sv/matbord-nord-180
If the slug is not translated into the target language, the slug of the main language appears there.
If the instance of the record does not run the target language, its main language applies. If
no slug is found, the record_id appears in the URL (with canonical = 1, the page
redirects from there to the slug). This costs one database query per record and language,
which is remembered for the call; without lang, or with a single-language slug field,
there is no query.
protocol
Only together with absolute or host:
(CMS:URL absolute=true protocol=https)
Output: https://www.example.com/en/hello-world
host
Another domain; implies absolute=true:
(CMS:URL host=shop.example.com)
Output: https://shop.example.com/en/hello-world (protocol of the instance)
absolute
Complete URL with protocol and main domain of the instance:
<link rel="canonical" href="(CMS:URL absolute=true)">
Output: <link rel="canonical" href="https://www.example.com/en/hello-world">
set
set outputs nothing, but stores the result under $set['url'] for the
surrounding PHP or a later command:
(CMS:URL set=url)
<?php if ($set['url'] !== '') { ?> … <?php echo $set['url']; ?> … <?php } ?>
CMS:XML
Creates an XML declaration.
(CMS:XML encoding [parameter=value …])
Method: Cms::cmsXml()
Without key: the first value without = is the encoding
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
encoding ¹ |
encoding |
utf-8 |
Character encoding to be used. | |
version |
version |
1.0 |
XML version. |
¹ can be given without a key as the first value.
encoding and version are output escaped with htmlspecialchars.
Usage
Creates the XML declaration <?xml version="1.0" encoding="utf-8"?>. It must be the
first character of the output – the command therefore stands in the first line of a
feed template, without a blank line before it:
(CMS:XML)
<rss version="2.0">
<channel>
<title>(CMS:TEXT title specialchars=true lbrconvert=false)</title>
(CMS:SELECT filter[area]=articles limit=20 resulttemplate=rss_item)
</channel>
</rss>
Matching this, (CMS:HEADER "Content-Type: application/rss+xml; charset=utf-8") – it
outputs nothing itself and therefore stands before (CMS:XML) on the same line, or
without a blank line in between:
(CMS:HEADER "Content-Type: application/rss+xml; charset=utf-8")(CMS:XML)
<rss version="2.0">
Typing the declaration into a template directly would not work: <?xml would be read by PHP
as an opening tag.
Parameters in detail
encoding
(CMS:XML encoding=ISO-8859-1)
Output: <?xml version="1.0" encoding="ISO-8859-1"?> – then output the content with charset=ISO-8859-1.
version
(CMS:XML version=1.1)
CMS:ZIP
Offers the files of several records as a ZIP archive for download and ends the page.
(CMS:ZIP field=field[,field…] filter[…]=value | relation=field | ids=… | source=basket [parameter=value …])
Method: CmsZip::cmsZip() (service loaded on demand, see below)
Without key: the first value without = is the field
Required: field and a selection: filter, relation, ids or source
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
field ¹ |
field name(s), comma-separated |
– | yes | Upload or thumbnail field(s). For each record, the first one that the visitor may retrieve applies. |
filter[…] |
value |
– | ² | Selection of the records as with CMS:SELECT. |
filter_op[…] |
operator |
= |
Comparison operator as with CMS:SELECT. | |
filter_mode[…] |
AND / OR |
OR |
Combination of several values as with CMS:SELECT. | |
relation |
field name |
– | ² | The linked records of a record. |
src_id |
record ID |
current record | Source record for relation. Empty or 0: no files. |
|
ids |
record IDs, comma-separated |
– | ² | Explicitly named records. |
source |
basket |
– | ² | The records in the basket of the session – for example a light box. |
category |
number |
0 |
Basket category for source=basket. |
|
sort[…] |
field name |
– | Order as with CMS:SELECT; without specification, that of ids or of the basket. |
|
order[…] |
asc / desc |
asc |
Sort direction. | |
name |
text |
download |
Name of the archive without .zip. |
|
filenames |
pattern |
{slug}.{extension} |
File names in the archive (see below). | |
credits |
field names, comma-separated |
– | Adds a CREDITS.txt: for each file its name and the contents of these fields. |
|
maxfiles |
number |
setting | Maximum number of files – only smaller than the setting user.zip.max_files. |
|
maxsize |
MB |
setting | Maximum size in MB – only smaller than the setting user.zip.max_mb. |
|
future |
true / false |
false |
Also pre-dated records (as with CMS:SELECT). | |
archive |
true / false |
false |
Also expired records (as with CMS:SELECT). | |
cookiename |
cookie name |
usess |
Session cookie, as with CMS:USER_LOGIN. | |
message[…] |
text |
core snippet | Own messages: message[empty], message[limit], message[throttled]. |
¹ can be given without a key as the first value.
² at least one of the parameters marked with ² must be given.
[…] – the parameter accepts subkeys, for example name[key]=value.
Usage
Press pictures of a release, all motifs of a model, the selection from a
light box: CMS:ZIP packs the files of several records into an archive and
sends it to the visitor as a download. The page ends with it – what follows in the template
appears only if no archive was created.
The command therefore belongs at the beginning of its own template, before any output (the frontend does not buffer; after the first output, the command reports an error). A stand-alone template that is called without a record is well suited:
(CMS:NUMBER useval="{$_GET['id']}" set=zip_id)
(CMS:ZIP relation=images src_id="{$set['zip_id']}" sort="_custom asc"
field="fil_print,fil_web" name="Press pictures" credits="title,caption,photo_credit")
(CMS:INCLUDE layout_head)
(CMS:MESSAGE prefix="<div class='error'>" suffix="</div>")
(CMS:INCLUDE layout_foot)
It is linked with the id of the record whose pictures are meant:
(CMS:URL id=0 template=press_pictures_zip set=zip_url)
<a href="<?php echo $set['zip_url']; ?>?id=(CMS:TEXT record_id)">All pictures as ZIP</a>
CMS:NUMBER turns the address parameter into a number. If it is missing, src_id is empty –
then there are no files, never “all”.
Which records. The selection follows the same rules as CMS:SELECT:
filter, relation with src_id or ids. Only published records count
from the instances of the website (future and archive as with CMS:SELECT). One
specification is required – without a selection, no archive is created.
With source=basket, the records come from the basket of the session
(CMS:USER_BASKET). A separate category separates the light box from
shopping:
(CMS:USER_BASKET add variable=images category=2) in the template of the light box
(CMS:ZIP source=basket category=2 field=fil_web name=Selection) in the download template
Which file. field names one or more file fields. For each record, the
first file that the visitor may also retrieve individually goes into the archive – the same check
as for a download via file.php. If a field is released only for signed-in visitors,
only those who are signed in with a suitable profile receive it; everyone else receives the
next field in the list. Thus field="fil_print,fil_web" gives the
signed-in press the print version, and everyone else the web version. Records without an
allowed file are skipped.
File names. filenames determines the names in the archive:
| Placeholder | Content |
|---|---|
{slug} |
Slug of the record (without slug, its id) |
{id} |
Id of the record |
{filename} |
original file name without extension |
{extension} |
extension of the file |
{fieldname} |
content of a field of the record |
The extension is added if the pattern does not contain it; duplicate names receive
-2, -3 …
Image credits. credits adds a CREDITS.txt – for each file its name and below
it the contents of the named fields, for example title, caption and photographer.
Limits. An archive contains at most user.zip.max_files files (default 100)
with a total of user.zip.max_mb MB (default 500). maxfiles and maxsize can only
reduce these values for a template. If the selection exceeds the size, no archive
is created.
Finished archives remain in the cache for user.zip.retention_hours hours (default 24);
the same selection is delivered from the cache. New archives are throttled per origin
(user.throttle.zip_ip, default 30 per hour).
Messages. If no archive is created, the template continues to run. CMS:MESSAGE outputs the
message, $set['zip_error'] names the reason:
| Reason | Meaning |
|---|---|
empty |
no file that the visitor may retrieve |
limit |
selection larger than the permitted size |
throttled |
too many archives in a short time |
The texts come from the core snippets zip_error_empty, zip_error_limit and
zip_error_throttled; message[empty]=… etc. replaces them in the template.
CMS:RELATION
Important: CMS:RELATION is intended only for back-end templates (list and trigger templates). It does not check access rights: it returns the linked records regardless of instance and area rights. As with
CMS:SELECT, however, only published targets count (released, publication date reached, expiry date not reached) – also in the back end and in the preview;future,archiveandstaterelax this. Records in the trash and working copies never appear.
Outputs the main field (title) of all records that are linked to the current record via a
relation field – in the preferred language version of the back end
(per instance; if an instance does not offer it, its main language). Records with an
empty main field do not appear.
For front-end templates, use (CMS:SELECT relation=…)
(CMS:RELATION field [parameter=value …])
Method: Cms::cmsRelation()
Without key: the first value without = is the field
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
id |
record ID or slug |
(current record) |
Record whose relations are output. Pure digit sequence = record_id, anything else = slug – the same rule as in the URL. | |
field ¹ |
field |
– | Name of the relation field (field type relation, pointer or link) whose targets are output. Another field type results in an error message. | |
limit |
number >0 |
1000 |
Maximum number of records included in the result set. | |
future |
true / false |
false |
true: Targets not yet published (online_at not yet reached) are also output (as with CMS:SELECT). | |
archive |
true / false |
false |
true: Expired targets (offline_at reached) are also output (as with CMS:SELECT). | |
state |
state ID or any |
0 |
Without specification, only released targets (status_id 0). A status ID > 0 only targets with this status, "any" targets of any status (as with CMS:SELECT). | |
lang |
language code |
(current language) |
Code of the language version whose contents are requested (affects multilingual fields). Not yet implemented – currently the language of the call always applies. | |
separator |
separator |
\| |
Text that is to appear between the results. | |
prefix |
text |
– | Text that is placed before the source, if it is not empty. | |
suffix |
text |
– | Text that is appended to the source, if it is not empty. | |
ifempty |
text |
– | Text that becomes the source if the original source is empty (further parameters, except 'set', are ignored). | |
instead |
text |
– | Overrides the output, if it is not empty. | |
set |
array index of $set |
– | The output of the source is redirected and stored in $set. |
¹ can be given without a key as the first value.
Usage
Outputs the main field (title) of all records to which a relation field of the current record points – in the order defined in the back end:
Topics: (CMS:RELATION topics separator=", ")
Output: Topics: Travel, Photography, Iceland
The command returns only the titles. If the linked records themselves are to be output
(with image, link, date …), CMS:SELECT relation=topics is the right way:
(CMS:SELECT relation=topics resulttemplate=topic_badge)
Reverse relations (which articles point to this topic?) work via
CMS:SELECT filter[topics]="{$cms['record_id']}".
Parameters in detail
field
Name of the relation field; may be given without a key:
(CMS:RELATION field=topics)
(CMS:RELATION topics)
id
The relations of another record – by record_id or slug, as in the URL. The
record must be published as in the URL, otherwise the output remains empty:
(CMS:RELATION topics id=1977)
(CMS:RELATION topics id=island-trip)
limit
(CMS:RELATION topics limit=3 separator=", ")
At most three titles are output – the first three in the order of the relation field.
future
Also targets whose publication date has not yet been reached:
(CMS:RELATION dates future=true separator=", ")
archive
Also expired targets:
(CMS:RELATION topics archive=true)
state
All targets regardless of status – for example in a list template of the back end:
(CMS:RELATION topics state=any future=true archive=true)
lang
Not yet implemented – the language of the call applies.
(CMS:RELATION topics lang=en)
separator
(CMS:RELATION topics separator=", ")
(CMS:RELATION topics separator="</li><li>" prefix="<ul><li>" suffix="</li></ul>")
The second line produces a list: <ul><li>Travel</li><li>Photography</li></ul>.
prefix
prefix / suffix surround the value and appear only if it is not empty –
so no empty Topics::
(CMS:RELATION topics prefix="Topics: " suffix="")
suffix
(CMS:RELATION topics suffix=".")
See prefix.
ifempty
ifempty replaces the entire output if the source is empty; prefix and
suffix are then omitted:
(CMS:RELATION topics prefix="<p>" suffix="</p>" ifempty="no topics assigned")
instead
instead replaces the value as soon as the source is not empty – useful if
only the presence matters:
(CMS:RELATION topics instead="Topics present" ifempty="-")
set
set outputs nothing, but stores the result under $set['topics'] for the
surrounding PHP or a later command:
(CMS:RELATION topics set=topics)
<?php if ($set['topics'] !== '') { ?> … <?php echo $set['topics']; ?> … <?php } ?>
CMS:SELECT
Searches the database using the given (filter) criteria and returns the records found.
(CMS:SELECT resulttemplate [parameter=value …])
Method: Cms::cmsSelect()
Without key: the first value without = is the resulttemplate
At least one of: filter, relation
PHP fragments: push_result (before) · include, pop_result (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
resulttemplate ¹ |
templatename |
– | yes | List template that is included to process the results. |
noresulttemplate |
templatename |
– | Detail template that is included if the query returns no results. | |
noresult |
text |
– | Text that is output directly if the query returns no results. | |
filter[…][…] |
value or EMPTY |
– | ² | In the key: name of the field to search (relation_any for any link field; area for the name of the content area, a number there counts as area_id). In the value: the value to search for. |
filter_mode[…] |
AND / OR |
OR |
Mode in which several values of the same field are combined (filter_mode[field]=AND). | |
filter_op[…][…] |
=, ==, !=, !!, >, <, >>, <<, >=, <= |
= |
Comparison operator for the given field (filter_op[field]=">"). Alternatively the operator stands between key and value (filter[price] > 100) or directly before the value (filter[price]=">100"). | |
skip |
number >=0 |
0 |
Number of records skipped in the result set. Only works together with limit. | |
limit |
number >0 |
– | Maximum number of records included in the result set – at the same time the page size for CMS:JUMP_LIST, CMS:JUMP_PREV and CMS:JUMP_NEXT. Without a value or 0: unlimited, no page navigation. | |
sort[…] |
field name(s), comma-separated |
changed_at |
Field to sort by (see order); several comma-separated or as sort[]=…; area sorts by the name of the content area, pointerfield.field by a field of the linked record. | |
order[…] |
desc / asc |
asc |
Order in which to sort (see sort). Without sort, changed_at desc applies. | |
lang |
language code |
(current language) |
Code of the language version whose contents are requested (affects multilingual fields). Not yet implemented – currently always the language of the call applies. | |
relation |
field name(s) comma-separated or 'ANY' |
– | ² | Field names of relation/pointer fields in which the sought records are stored (with src_id); with dest_id the fields via which the sought records point to dest_id. ANY: any pointer field. |
src_id |
record ID(s) |
current record |
Source record whose links are evaluated (without a value: current record). | |
dest_id |
record ID |
– | Reverse direction to relation: the records that point to this record via relation (relation=ANY: via any pointer field). Without src_id, no current record is then used as the source. | |
nextof |
record ID |
– | Only with relation: neighbors of the given record in the link list – the entries after it (order=asc) or before it (order=desc), in the order of the list; with limit=1 the next or previous record. | |
count |
true / false |
false |
true: The number of records in the result set is determined, regardless of "limit" and "skip" (in the resulttemplate in $this->db->num_rows_unlimited; basis of the JUMP commands). | |
hits |
true / false |
true |
false: Only counts (includes count=true); no records are loaded and no resulttemplate is included. The total is then available in the calling template in $this->db->num_rows_unlimited; noresult/noresulttemplate apply if it is 0. | |
archive |
true / false |
false |
true: Expired records (offline_at reached) are included in the query as well. | |
future |
true / false |
false |
true: Pre-dated records (online_at not yet reached) are included in the query as well. | |
debug |
true / false |
false |
true: The generated database query is output for support analysis. | |
sql |
– | – | Raw SQL addition in the WHERE clause (caution: for support only!). | |
state |
status ID or any |
0 |
Without a value only released records (status_id 0). A status ID > 0 finds only records with this status; "any" finds records of every status. | |
request |
comma list |
– | Takes filter and sort values from the URL – comma list or array of the permitted keys: filter[field], filter_op[field], filter_mode[field], skip, limit, sort, order (e.g. "filter[title],sort,order,skip"). filter only with a field name. Values set in the command always take precedence; for a field filtered in the command, the URL adds nothing. | |
set |
array index of $set |
– | The entire output (including the included template) is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
² at least one of the parameters marked with ² must be given.
[…] – the parameter accepts subkeys, for example name[key]=value.
Usage
Queries records and includes the resulttemplate for the hits; in it,
CMS:RESULT_START … CMS:RESULT_END runs over the rows. The command is the core of every
list.
Only published records are found: released (status_id 0), publication date (online_at)
reached, end (offline_at) not reached. The parameters future, archive and state relax
this per query – also in the preview from the back end, which shows an unpublished record but
shows the lists in it the way the website shows them.
<h2>Latest articles</h2>
(CMS:SELECT article_teaser
filter[area]=articles
sort=online_at order=desc
limit=10
noresult="<p>No articles yet.</p>"
)
List template article_teaser:
<ul>
(CMS:RESULT_START)
<li><a href="(CMS:URL)">(CMS:TEXT title)</a> – (CMS:DATE online_at)</li>
(CMS:RESULT_END)
</ul>
Common patterns:
A record by slug: (CMS:SELECT detail filter[slug]=imprint limit=1)
Only one topic: (CMS:SELECT list filter[area]=articles filter[topics]=42)
Search in the title: (CMS:SELECT list filter[area]=articles filter[title]="*{$_GET['q']}*")
Time period: (CMS:SELECT list filter[area]=articles filter[online_at] >= 2026-01-01)
Linked records: (CMS:SELECT topic_badge relation=topics)
Children in the tree: (CMS:SELECT nav filter[parent_id]="{$cms['record_id']}" filter[depth]=1 sort=position order=asc)
Pageable list: (CMS:SELECT list filter[area]=articles limit=20 skip="{$_REQUEST['skip']}" count=true)
Search form with sorting: (CMS:SELECT list filter[area]=articles limit=20 count=true request="filter[title],sort,order,skip")
Several values for the same field are combined with OR (filter_mode[field]=AND switches
this); different fields are always combined with AND. Comparison operators stand between
key and value (filter[price] > 100, filter[color] != red) or directly before the value
(">100"); wildcards and (empty) belong in the value (Hen*). The complete filter syntax is
in the Basics.
A CMS:SELECT may stand inside a list template (nested lists); the outer result is set aside
automatically and resumed afterwards.
Parameters in detail
resulttemplate
List template for the hits; may stand without a key. Name or area.name:
(CMS:SELECT resulttemplate=article_teaser filter[area]=articles)
(CMS:SELECT article_teaser filter[area]=articles)
(CMS:SELECT articletemplates.article_teaser filter[area]=articles)
noresulttemplate
Template for no hits – for example with a search form or a note:
(CMS:SELECT search_result filter[area]=articles filter[title]="*{$_GET['q']}*" noresulttemplate=search_empty)
noresult
Short text for no hits, without its own template:
(CMS:SELECT list filter[area]=articles noresult="<p>No entries.</p>")
filter
Key = field name (own fields, meta fields such as record_id, status_id, online_at,
parent_id, user_id, the virtual slug, _default for the main field, area for the area
name); value = search value with an optional operator:
filter[area]=articles area by name (number = area_id)
filter[area]=articles,news two areas (OR)
filter[title]="Hen*" begins with
filter[price] > 100 greater than
filter[price]=">100" the same, operator in the value
filter[price]=">100 AND <500" expression in one field
filter[online_at]=2026 the whole year
filter[date] >= today from today on (0:00, time zone of the call)
filter[online_at] > utc_now-7d the last seven days, calculated in UTC
filter[teaser]="(empty)" empty / not translated
filter[image]="(empty)" file field without a file
filter[topics]=42 link field points to record 42
filter[topics]="(empty)" link field without a link
filter[relation_any]=42 any link field points to 42
filter[topics.title]="Iceland" field of the linked record
filter[tags]=a filter[tags]=b two values, OR (see filter_mode)
filter[parent_id]=17 filter[depth]=1 direct children of 17
filter[parent_id]=null filter[depth]=1 top level (records without a parent record)
filter[depth] is not a filter on a record’s indentation; it states how many levels
below filter[parent_id] are searched: 1 = direct children only, 2 = down to the
grandchildren, omitted = all descendants. Without filter[parent_id] it has no effect (a note when saving the template, or with debug=true);
filter[depth]=0 returns no records. The value under “Indent levels” in the record info
(0 = top level), by contrast, is absolute – you find the top level with
filter[parent_id]=null filter[depth]=1.
Text fields are compared and sorted according to the rules of the language in which
you search: upper and lower case and accents do not matter, ß equals ss; in German, in
addition, ä equals ae (filter[title]="Mue*" finds “Müller”); in Swedish, by contrast,
ö is a letter of its own (o does not find ö). The rules follow from the language code;
see Basics.
An empty value cancels the filter – handy for optional filters from the request, risky
with typos: filter[category]="{$cms['id']}" (there is no key id; the correct key is
record_id) returns all records.
filter_mode
Combination of several values of the same field (default OR):
(CMS:SELECT list filter[area]=articles filter[tags]=travel filter[tags]=iceland filter_mode[tags]=AND)
Finds articles that carry both tags.
filter_op
Comparison operator separate from the value – equivalent to writing it in the value and to the operator between key and value:
(CMS:SELECT list filter[area]=articles filter[price]=100 filter_op[price]=">")
(CMS:SELECT list filter[area]=articles filter[price]=">100")
(CMS:SELECT list filter[area]=articles filter[price] > 100)
skip
Offset for paging; usually from the request. skip only works together with limit – without
limit it is ignored, and the query then returns all hits from the first one on:
(CMS:SELECT list filter[area]=articles limit=20 skip="{$_REQUEST['skip']}" count=true)
The page navigation (CMS:JUMP_LIST) stands in the resulttemplate list.
limit
(CMS:SELECT teaser filter[area]=articles limit=3)
At most three hits. Without limit (or with 0) the query returns all hits – for large
areas, a limit therefore always belongs. limit is at the same time the page size of the
paging commands CMS:JUMP_LIST, CMS:JUMP_PREV and
CMS:JUMP_NEXT in the resulttemplate; without limit there is no page
navigation.
sort
Sort field; direction via order or directly in the value:
sort=online_at order=desc
sort="title asc"
sort=position order=asc order from the back end (tree/list)
sort=_default main field of the area
sort=area by area name
sort=author.name field of the linked record
sort[]=status_id sort[]=title order[]=asc order[]=asc
order
Direction for sort: asc (default as soon as sort is given) or desc. Without sort, the
command sorts by changed_at desc – the most recently changed first.
(CMS:SELECT list filter[area]=articles sort=title order=asc)
lang
Not yet implemented – the language of the call applies.
relation
Hits are the records to which the link field of the source record points – without src_id those
of the current record. The order of the link field is given by sort=_custom; without sort, the
default sorting applies (changed_at desc):
(CMS:SELECT topic_badge relation=topics)
(CMS:SELECT gallery relation=images sort="_custom asc")
Several field names, comma-separated, count as OR; ANY means any link field.
sort=_custom and nextof require exactly one field name.
(CMS:SELECT teaser relation="topics,tags")
(CMS:SELECT teaser relation=any)
The reverse direction – records that point to the current one:
(CMS:SELECT product_list relation=rel_products)
(CMS:SELECT product_list filter[area]=products filter[category]="{$cms['record_id']}")
(CMS:SELECT reference_list relation=any dest_id="{$cms['record_id']}")
The first line applies to relation fields (relation): they have a counterpart field
rel_{areaname} in the target area. Pointer fields (pointer) have none – their counterpart is
the filter on the pointer field (second line). See
Every record has a page.
src_id
Source record for relation, if it is not the current record that is meant:
(CMS:SELECT topic_badge relation=topics src_id=1977)
(CMS:SELECT topic_badge relation=topics src_id="{$set['user_id']}")
An empty value or 0 also means the current record. If there is none – for example in a
standalone template without a record – the query finds nothing (and not all records of the area).
dest_id
The reverse direction to relation: hits are the records that point to dest_id via the
pointer field – with relation=any via any of them – for example “Where is this image used?”:
(CMS:SELECT reference_list relation=any dest_id="{$cms['record_id']}")
(CMS:SELECT article_list relation=topics dest_id=1977)
With dest_id, the command does not use a current record as the source without src_id.
The same as a filter value, also for several targets: filter[relation_any]="1977,1978".
nextof
Only together with relation: instead of all linked records, the neighbors of the record
nextof in the same link list – the entries after it with order=asc (default), those before it
with order=desc, in each case in the order of the list (the sorting is thereby fixed, sort is
ignored). With limit=1 this is the next or previous record – for example for “next”/“back” in an
image gallery whose images hang on article 2422:
(CMS:SELECT gallery_next relation=images src_id=2422 nextof="{$cms['record_id']}" limit=1)
(CMS:SELECT gallery_back relation=images src_id=2422 nextof="{$cms['record_id']}" order=desc limit=1)
If nextof is not in the list, there are no hits.
count
Determine the total regardless of limit/skip – the basis for CMS:JUMP_* and for information
such as “hits 21–40 of 137”:
(CMS:SELECT list filter[area]=articles limit=20 skip="{$_REQUEST['skip']}" count=true)
In the resulttemplate list there are then two numbers: $this->db->num_rows are the hits of
this page (at most limit), $this->db->num_rows_unlimited are all hits of the query:
(CMS:NUMBER useval="{$this->db->num_rows_unlimited}") hits
(CMS:RESULT_START) … (CMS:RESULT_END)
(CMS:JUMP_LIST)
Both numbers apply only in the resulttemplate: after the CMS:SELECT, the surrounding
result is active again – the same rule that makes nested lists possible. If only the number is
needed, count in the calling template with hits=false.
hits
hits=false only counts (count=true is thereby included): no records are loaded and no
resulttemplate is included – the name must nevertheless be given. The total is then available
in the calling template in $this->db->num_rows_unlimited:
(CMS:SELECT x filter[area]=articles filter[topics]=42 hits=false)
(CMS:NUMBER useval="{$this->db->num_rows_unlimited}") articles on this topic
If the number is 0, noresult or noresulttemplate apply as usual:
(CMS:SELECT x filter[area]=articles filter[topics]=42 hits=false noresult="No articles on this topic yet.")
If the counting CMS:SELECT stands in a resulttemplate, the number there replaces the total of
the surrounding result; $this->db->num_rows and the running result loop remain untouched. A page
navigation (CMS:JUMP_LIST) of the surrounding result then belongs before the
counting CMS:SELECT.
archive
Include expired records (offline_at reached) – for example for an archive of past events:
(CMS:SELECT archive filter[area]=events archive=true sort=online_at order=desc)
An expired record remains reachable at its own address, by the way; it is only missing from lists
without archive.
future
Include pre-dated records (online_at not yet reached) – for example a preview of upcoming posts:
(CMS:SELECT upcoming filter[area]=articles future=true filter[online_at] > now)
debug
Output the SQL and bind values of the query (for development only). Above them, dropped
filters are listed with the reason – for example an unknown field name or filter[depth]
without filter[parent_id]:
(CMS:SELECT list filter[area]=articles debug=true)
sql
Raw SQL for the WHERE clause – for support only; do not pass user input into it:
(CMS:SELECT list filter[area]=articles sql="AND `record`.`record_id` % 2 = 0")
set
The entire output – the list template with all hits, for noresult its text – ends up in
$set instead of on the page. In this way, an address from a query can be reused in another command:
(CMS:SELECT url_only filter[area]=pages filter[identifier]=cart limit=1 set=cart_url)
<a class="cart" href="<?php echo $set['cart_url']; ?>">…</a>
(CMS:TEXT title prefix="<a href='{$set['cart_url']}'>" suffix="</a>")
The output of debug=true and PHP notices from the list template also end up in $set and not on
the page – to investigate, temporarily leave out set. If the page ends within the query (for
example through a success template), what has been generated up to that point is output normally.
state
Without a value only released records (status_id 0). A status ID finds only records with exactly
this status; any removes the status filter:
(CMS:SELECT list filter[area]=articles state=2)
(CMS:SELECT list filter[area]=articles state=any)
request
Takes filters, sorting and paging from the URL (GET or POST) – for search forms and sortable lists, without writing every value into the command. You state what may be taken over, as a comma list or an array:
request="sort,order,skip" only sorting and paging
request="filter[title],filter[price],filter_op[price]" search in title and price
request[]=sort request[]="filter[title]" the same as an array
Permitted are skip, limit, sort, order as well as filter, filter_op and
filter_mode only with a field name (filter[title]). A filter without a field
name and all are ignored and reported as errors – otherwise a visitor could search by any
field of the records found, including fields the page does not even show, such as an
internal note.
In the URL the keys are named as in the command, only filterop and filtermode without
the underscore: ?filter[title]=Hen*&sort=title&order=desc&skip=20.
(CMS:SELECT list filter[area]=articles limit=20 count=true request="filter[title],sort,order,skip")
The URL can only narrow the query, never widen it:
- Values in the command take precedence. For a field the command filters on,
requesttakes over neither values norfilter_op/filter_mode–?filter[area]=internalor?filterop[area]=!=change nothing aboutfilter[area]=articles. Fields of a linked record count as the reference field (author.namebelongs toauthor);areaandarea_idcount as the same field. skip,limit,sortandorderfrom the command replace the URL value completely.limitfrom the URL only counts as a positive number – “unlimited” cannot be requested.- Encrypted fields only find the exact value; wildcards (
*) and greater/less yield no hits. - Password fields can never be filtered or sorted.
CMS:SELECT_LANGVERSION
Returns the available languages as records.
(CMS:SELECT_LANGVERSION resulttemplate)
Method: Cms::cmsSelectLanguages()
Without key: the first value without = is the resulttemplate
PHP snippet: push_result (before) · include, pop_result (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
resulttemplate ¹ |
template name |
– | yes | List template that is included to process the results. |
set |
array index of $set |
– | The entire output (including the included template) is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
Usage
Provides the languages of the website as a result set – main language first –
and includes the resulttemplate. Each row contains language_id, code, label,
endonym and current (1 = language of the current call). This creates a
language switcher:
<ul class="lang">
(CMS:SELECT_LANGVERSION lang_item)
</ul>
List template 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)
The rows are not records; CMS:URL without id therefore points to the outer record –
the record of the page or the row of the surrounding result loop –
only in the respective other language. If the field slug is multilingual, CMS:URL
inserts the slug of the target language (/de/esstisch-nord-180 → /en/dining-table-nord-180),
see CMS:URL. With only one language, the result set contains exactly
one row.
On a page without a record (stand-alone template, /de/0,suche) there is
no outer record; the template must then be specified explicitly in the link:
<a href="(CMS:URL lang="{$cms['code']}" template=suche)">(CMS:TEXT endonym)</a>
Parameters in detail
resulttemplate
List template; may be given without a key:
(CMS:SELECT_LANGVERSION resulttemplate=lang_item)
(CMS:SELECT_LANGVERSION lang_item)
CMS:RESULT_START
Marks the beginning of a loop for processing CMS:SELECT results.
(CMS:RESULT_START)
No method call – the effect lies solely in the PHP snippet
PHP snippet: loop_start (after)
Parameters
This command has no parameters.
Usage
Opens the loop over the hits of a CMS:SELECT (or CMS:SELECT_LANGVERSION,
CMS:USER_BASKET function=load). It belongs in the list template; everything between
CMS:RESULT_START and CMS:RESULT_END is executed for each hit, and the commands in it
read from the current row:
<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>
Within the loop, the following are available: $cms (the current row as an array),
$this->cms->row_no (zero-based row number), $this->db->num_rows (hits on
this page) and $this->db->num_rows_unlimited (total number with count=true).
The command has no parameters and creates no method call – it is compiled directly into a
for loop.
CMS:RESULT_END
Marks the end of a loop for processing CMS:SELECT results.
(CMS:RESULT_END)
No method call – the effect lies solely in the PHP snippet
PHP snippet: loop_end (after)
Parameters
This command has no parameters.
Usage
Closes the loop opened with CMS:RESULT_START:
(CMS:RESULT_START)
<li>(CMS:TEXT title)</li>
(CMS:RESULT_END)
Every CMS:RESULT_START needs exactly one CMS:RESULT_END in the same template – the
compiler turns them into { and }; a missing end leads to a PHP syntax error
when the page is called.
CMS:JUMP_LIST
Creates a page navigation (pagination), for example 1 … 5 | 6 | 7 | 8 | 9 … 15.
(CMS:JUMP_LIST separator [parameter=value …])
Method: Pagination::cmsJumpList()
Without key: the first value without = is the separator
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
skip |
number >= 0 |
$_REQUEST[skip] |
Offset of the current page (number of skipped results). Without a value, the request parameter skip applies. | |
maximum |
number >= 1 |
7 |
Number of page links shown at the same time (moving along), not counting the links to the first and last page. | |
boundaries |
true / false |
true |
true: Links to the first and the last page are always shown as soon as they lie outside the moving range. | |
separator ¹ |
separator |
(space) | Text between two page links. It is not inserted before and after the omission ('ellipsis'). | |
tpl_link |
HTML template |
<a href="{href}">{page}</a> |
Template for a page link. {page} is replaced by the page number, {href} by the page URL. The URL is HTML-escaped (& instead of &), ' and \ are percent-encoded – {href} can stand directly in an attribute. |
|
tpl_active |
HTML template |
<strong>{page}</strong> |
Template for the current page. {page} is replaced by the page number, {href} by the page URL. | |
ellipsis |
code |
… |
Placeholder for omitted pages – appears between a boundary link and the moving range. | |
replace |
text?text |
– | Simple text replacement in the finished HTML. The separator is the question mark: "search?replace". | |
prefix |
text |
– | Text before the navigation, only if a navigation is output. | |
suffix |
text |
– | Text after the navigation, only if a navigation is output. | |
ifempty |
text |
– | Output if the result fits on a single page, or if the CMS:SELECT has no limit and therefore no navigation is needed. | |
set |
array index of $set |
– | The navigation is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
Usage
Page navigation for a CMS:SELECT with count=true. The page size is the limit of the query;
without limit there are no pages and the command outputs only ifempty. The current offset
comes from $_REQUEST['skip'].
The command stands in the resulttemplate of the CMS:SELECT (before or after the result
loop): only there the hit count and limit of this query apply – after the CMS:SELECT in the
calling template, the outer result is active again.
(CMS:SELECT list filter[area]=articles limit=20 skip="{$_REQUEST['skip']}" count=true)
Template list:
(CMS:RESULT_START)
<h2>(CMS:TEXT title)</h2>
(CMS:RESULT_END)
<nav class="pager">
(CMS:JUMP_PREV) (CMS:JUMP_LIST separator=" | ") (CMS:JUMP_NEXT)
</nav>
Output: ❮❮ 1 … 5 | 6 | 7 | 8 | 9 … 15 ❯❯
The links keep all other parameters of the current URL (search term, filters) and only set skip
anew. If the result fits on one page, nothing is output (or ifempty).
Bootstrap-style bar:
(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=""
)
Parameters in detail
skip
Offset of the current page, if it does not come from $_REQUEST['skip']:
(CMS:JUMP_LIST skip="{$offset}")
maximum
Number of moving page links (not counting the first and last page):
(CMS:JUMP_LIST maximum=5)
Output on page 7 of 15: 1 … 5 6 7 8 9 … 15
boundaries
Suppress links to the first and last page:
(CMS:JUMP_LIST boundaries=false)
Output: 5 6 7 8 9 instead of 1 … 5 6 7 8 9 … 15
separator
Text between two page links; may stand without a key:
(CMS:JUMP_LIST " | ")
(CMS:JUMP_LIST separator=" · ")
tpl_link
Template for each page link; {href} and {page} are replaced:
(CMS:JUMP_LIST tpl_link="<a class=page href='{href}'>{page}</a>")
tpl_active
Template for the current page:
(CMS:JUMP_LIST tpl_active="<span class='page current'>{page}</span>")
ellipsis
Placeholder for omitted pages (between the first/last page and the moving range):
(CMS:JUMP_LIST ellipsis=" ··· ")
replace
Simple replacement in the finished HTML, separator ? (only one pair):
(CMS:JUMP_LIST replace="<strong>?<strong class=current>")
prefix
Appears only if a navigation is output:
(CMS:JUMP_LIST prefix="<nav class=pager>" suffix="</nav>")
suffix
(CMS:JUMP_LIST suffix="</nav>" prefix="<nav>")
ifempty
Output if everything fits on one page (or the CMS:SELECT has no limit):
(CMS:JUMP_LIST ifempty="<span class=muted>all entries</span>")
set
(CMS:JUMP_LIST set=pager)
<?php if ($set['pager'] !== '') { ?><nav><?php echo $set['pager']; ?></nav><?php } ?>
CMS:JUMP_NEXT
Creates a link to the next page.
(CMS:JUMP_NEXT tpl_link [parameter=value …])
Method: Pagination::cmsJumpNext()
Without key: the first value without = is the tpl_link
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
skip |
number >= 0 |
$_REQUEST[skip] |
Offset of the current page (number of skipped results). Without a value, the request parameter skip applies. | |
tpl_link ¹ |
HTML template |
<a href="{href}"><strong>❯❯</strong></a> |
Template for the link, as long as there is a next page. {href} is replaced by the target URL. The URL is HTML-escaped (& instead of &), ' and \ are percent-encoded – {href} can stand directly in an attribute. |
|
tpl_inactive |
HTML template |
<span>❯❯</span> |
Template for the link that cannot be selected, if there is no next page. {href} is replaced here as well. | |
replace |
text?text |
– | Simple text replacement in the finished HTML. The separator is the question mark: "search?replace". | |
prefix |
text |
– | Text before the link, only if a link is output. | |
suffix |
text |
– | Text after the link, only if a link is output. | |
ifempty |
text |
– | Output if the CMS:SELECT has no limit (then there are no pages). If everything fits on one page, tpl_inactive appears. |
|
set |
array index of $set |
– | The link is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
Usage
Link to the next page of a pageable list (CMS:SELECT with count=true and limit). Like
CMS:JUMP_LIST, the command stands in the resulttemplate of the query, here with
limit=20:
(CMS:JUMP_PREV) Page <?php echo intdiv((int)($_REQUEST['skip'] ?? 0), 20) + 1; ?> (CMS:JUMP_NEXT)
Output: ❮❮ Page 3 ❯❯
If there is no next page – also when everything fits on one page –, tpl_inactive appears (default:
the symbol without a link). The page size is the limit of the CMS:SELECT; without limit, only
ifempty appears.
Parameters in detail
skip
(CMS:JUMP_NEXT skip="{$offset}")
tpl_link
Template with link; {href} is replaced. May stand without a key:
(CMS:JUMP_NEXT "<a class=next href='{href}'>next ›</a>")
tpl_inactive
Template without a next page ({href} is replaced here too – with the address the next page would
have):
(CMS:JUMP_NEXT tpl_link="<a href='{href}'>next ›</a>" tpl_inactive="<span class=disabled>next ›</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="")
Applies only if the CMS:SELECT has no limit; the default is empty.
set
(CMS:JUMP_NEXT set=next)
CMS:JUMP_PREV
Creates a link to the previous page.
(CMS:JUMP_PREV tpl_link [parameter=value …])
Method: Pagination::cmsJumpPrev()
Without key: the first value without = is the tpl_link
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
skip |
number >= 0 |
$_REQUEST[skip] |
Offset of the current page (number of skipped results). Without a value, the request parameter skip applies. | |
tpl_link ¹ |
HTML template |
<a href="{href}"><strong>❮❮</strong></a> |
Template for the link, as long as there is a previous page. {href} is replaced by the target URL. The URL is HTML-escaped (& instead of &), ' and \ are percent-encoded – {href} can stand directly in an attribute. |
|
tpl_inactive |
HTML template |
<span>❮❮</span> |
Template for the link that cannot be selected, if there is no previous page. {href} is replaced here as well. | |
replace |
text?text |
– | Simple text replacement in the finished HTML. The separator is the question mark: "search?replace". | |
prefix |
text |
– | Text before the link, only if a link is output. | |
suffix |
text |
– | Text after the link, only if a link is output. | |
ifempty |
text |
– | Output if the CMS:SELECT has no limit (then there are no pages). If everything fits on one page, tpl_inactive appears. |
|
set |
array index of $set |
– | The link is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
Usage
Link to the previous page – the counterpart to CMS:JUMP_NEXT, which, like it, stands in the
resulttemplate of a CMS:SELECT with limit (the limit is the page size):
<nav>(CMS:JUMP_PREV "<a href='{href}'>‹ previous</a>" tpl_inactive="") (CMS:JUMP_NEXT "<a href='{href}'>next ›</a>" tpl_inactive="")</nav>
On the first page, tpl_inactive appears (here empty), otherwise the link – also when everything
fits on one page.
Parameters in detail
skip
(CMS:JUMP_PREV skip="{$offset}")
tpl_link
(CMS:JUMP_PREV "<a class=prev href='{href}'>‹ previous</a>")
tpl_inactive
(CMS:JUMP_PREV tpl_inactive="<span class=disabled>‹ previous</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="")
Applies only if the CMS:SELECT has no limit; the default is empty.
set
(CMS:JUMP_PREV set=prev)
CMS:CONTINUE
Continues template processing despite a success template.
(CMS:CONTINUE)
Method: Cms::cmsContinue()
Parameters
This command has no parameters.
Usage
Commands with 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) include their template in case of success and then end
the page; likewise CMS:USER_PROTECT and CMS:USER_PW with their
errortemplate, and CMS:USER_LOGIN with the codetemplate. If CMS:CONTINUE stands before the
command, the template continues to run after the included template instead:
(CMS:CONTINUE)
(CMS:USER_INPUT
container=comments
submit="$submit"
successtemplate=comment_saved
)
<h2>All comments</h2>
(CMS:SELECT comment_list
filter[area]=comments
)
Without CMS:CONTINUE, nothing would be output after comment_saved – with it,
the confirmation appears above the list. The flag applies exactly once: the next
of these commands that actually includes a template uses it up.
CMS:CSP
Changes the script policy (Content Security Policy) of this page: additional script sources, form targets or a different mode.
(CMS:CSP [parameter=value …])
Method: Cms::cmsCsp()
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
script |
sources |
– | Additional script sources, separated by spaces or commas. | |
form |
sources |
– | Additional form targets (enables form-action). |
|
mode |
off / report / enforce |
– | Mode for this page; empty: setting of the website. |
Usage
For each website, a script policy can be switched on in the back end (instance, section “Script policy (CSP)”). Then only scripts run that
- carry the nonce of the page (a random value for each request) – in the template,
write
<script(CMS:NONCE)>for this, see CMS:NONCE, or - are loaded from your own website or from the listed sources
(
<script src="/js/app.js">).
Code that gets into the page through content (a comment with <script>, an image with
onerror) carries no nonce and does not run. The default is “off”; “Report only”
lets everything run and writes violations to the log csp – so you can see what would
be blocked before you enforce the policy.
CMS:CSP adapts the policy for one page, for example the checkout with the script
of the payment provider:
(CMS:CSP script="https://js.stripe.com" form="https://checkout.stripe.com")
The command must come before the first output, ideally at the very top of the template; if it
comes later or mode is unknown, it stores an error message (CMS:MESSAGE).
Several calls collect sources, separated by spaces or commas. Host specifications are allowed
(https://js.example.com,
*.example.com, with port or path); keywords such as 'unsafe-inline' and bare
schemes such as https: are discarded – they would override the policy.
What the policy blocks and how to do it correctly:
| blocked | instead |
|---|---|
<button onclick="…">, <form onsubmit="…"> |
data-… attribute and addEventListener in a <script> of the template |
<a href="javascript:…"> |
<a href="#" data-…> + listener |
<script> without nonce |
<script(CMS:NONCE)> |
echo '<script>…</script>' in PHP |
(CMS:NONCE set=nonce) and echo '<script' . $set['nonce'] . '>' – see CMS:NONCE |
| external scripts (tag managers, widgets) | enter the source in the instance or use CMS:CSP script=…; if a script loads further scripts, switch on “Allow scripts loaded by trusted scripts” |
When you save a template, the editor reports every <script> without (CMS:NONCE),
every on…= attribute and every javascript: as a note; on the start page, the
widget “Template messages” collects them – also for templates that are edited in the
file system.
If you want a completely custom policy (also for styles, images, connections), send it with
CMS:HEADER and get the nonce with (CMS:NONCE format=value).
Parameters in detail
script
(CMS:CSP script="https://maps.example.com https://cdn.example.com")
form
(CMS:CSP form="https://www.paypal.com")
mode
Temporarily exempt a page with an old widget or let it only report:
(CMS:CSP mode=report)
(CMS:CSP mode=off)
CMS:HEADER
Sends an HTTP header.
(CMS:HEADER text [parameter=value …])
Method: Cms::cmsHeader()
Without key: the first value without = is the text
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
text ¹ |
text |
– | Line of text that is sent as a header to the browser. Without a value (Name:), the header is removed. |
|
overwrite |
true / false |
true |
false: A previous header of the same type is not overwritten; a new header is added. |
¹ can be given without a key as the first value.
Usage
Sends an HTTP header. It must come before the first output – so at the beginning of the template, before any HTML and without a blank line in front:
(CMS:HEADER "Content-Type: application/json; charset=utf-8")
{"title": (CMS:TEXT title specialchars=true lbrconvert=false prefix='"' suffix='"')}
Further typical cases:
(CMS:HEADER "Content-Type: application/rss+xml; charset=utf-8")
(CMS:HEADER "Cache-Control: no-store")
(CMS:HEADER "HTTP/1.1 404 Not Found")
Redirect – CMS commands cannot be nested within each other, so the address is first
passed to $set with set and then placed into the header:
(CMS:URL id=home set=target)
(CMS:HEADER "Location: {$set['target']}")
If a template delivers CSS, JavaScript or JSON, the matching Content-Type belongs at
the beginning – camindo CMS sends X-Content-Type-Options: nosniff, so the browser does not
execute a script with the default type text/html:
(CMS:HEADER "Content-Type: text/javascript; charset=utf-8")
If something has already been output, an error message is stored (CMS:MESSAGE) and the
header is not sent.
Parameters in detail
text
The header line; may be given without a key:
(CMS:HEADER text="X-Robots-Tag: noindex")
(CMS:HEADER "X-Robots-Tag: noindex")
overwrite
By default, a header replaces a previous header of the same name; overwrite=false adds another one:
(CMS:HEADER "Link: </style.css>; rel=preload; as=style")
(CMS:HEADER "Link: </app.js>; rel=preload; as=script" overwrite=false)
Replacing or removing the defaults
camindo CMS sends some security headers before the template:
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
This means the page may only be placed in a frame within the website itself (background information: Basics). Each of these headers can be replaced; without a value, it is removed. A page that a partner may embed:
(CMS:HEADER "X-Frame-Options:")
(CMS:HEADER "Content-Security-Policy: frame-ancestors 'self' https://partner.example")
If a script policy is switched on for the website (back end, instance), camindo
sends script-src 'self' 'nonce-…' together with frame-ancestors 'self' instead; for
individual pages, adapt it with CMS:CSP.
A custom Content-Security-Policy replaces the default – then include
frame-ancestors as well (the nonce of the page is provided by CMS:NONCE):
(CMS:HEADER "Content-Security-Policy: frame-ancestors 'self'; script-src 'self' https://cdn.example")
Caching
Pages are generated anew on every request. With the default
Cache-Control: private, no-cache, no proxy and no CDN stores the page, and the
browser asks the server again before every display – changed content appears
immediately. The browser's back button stays fast all the same.
If a page belongs to a single visitor – a session of a frontend user is running (also a
basket without sign-in, see CMS:USER_BASKET) or frontend editing is
switched on – camindo sends Cache-Control: private, no-store instead: the page
is not stored anywhere.
A page that rarely changes may be cached longer:
(CMS:HEADER "Cache-Control: public, max-age=3600")
Put the header at the top of the template. If a CMS:USER_* command with a session runs
afterwards, private, no-store applies again – so personal pages never end up in a cache.
CMS:INCLUDE
Includes a template.
(CMS:INCLUDE template)
(CMS:INCLUDE template caching=5m cacheparams=$params)
Method: Cms::cmsInclude()
Without key: the first value without = is the template
PHP snippet: include_cached (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
template ¹ |
template name |
– | yes | Detail template that is included. |
set |
array index of $set |
– | The entire output (including the included template) is not output, but stored in the variable $set. | |
caching |
seconds, 30s, 5m, 1h, 1d or infinite |
– | The output of the template is cached for this long and then delivered without processing again. Without specification, empty or 0: no cache. |
|
cacheparams |
null, bool, int, string or array |
– | Everything the output depends on (record, page, customer …): each value creates its own cache file. Instance and language are taken into account automatically. |
¹ can be given without a key as the first value.
Usage
Includes another template at this point – for header and footer areas, navigation, recurring blocks:
(CMS:INCLUDE header)
<main>
<h1>(CMS:TEXT title)</h1>
(CMS:INCLUDE text_image_block)
</main>
(CMS:INCLUDE footer)
The included template sees the same record ($cms), the same $set values
and, within a result loop, the same row. A name without an area must be unique within the
access area of the instance, otherwise use area.name:
(CMS:INCLUDE templates.header)
The nesting depth is limited by admin.max_include_depth (default 20) –
a template that includes itself aborts with an error message.
Parameters in detail
template
Name or area.name (no id); may be given without a key. In the back-end editor, the value is a link to the template:
(CMS:INCLUDE template=header)
(CMS:INCLUDE header)
(CMS:INCLUDE templates.header)
(CMS:INCLUDE "{$set['layout']}") dynamic
set
The output of the included template ends up in $set instead of on the page – for example to
use a block twice or to pass it on to another command:
(CMS:INCLUDE price set=price)
<p class="price"><?php echo $set['price']; ?></p>
<meta property="product:price" content="(CMS:TEXT useval="{$set['price']}" striptags=true specialchars=true)">
With caching, the intercepted text also comes from the cache.
caching
Switches on the output cache for this call: the first time, the template runs as usual,
and its output is stored under var/cache/include/. Every further call with the same
parameters delivers this file within the time limit – the template, its queries and included
templates then do not run.
| Value | Time limit |
|---|---|
300 or 300s |
300 seconds |
5m |
5 minutes |
1h |
1 hour |
1d |
1 day |
infinite |
unlimited – until the template is changed |
empty or 0 |
no cache (for example to switch it off via a variable) |
(CMS:INCLUDE navigation caching=1h)
(CMS:INCLUDE footer caching=1d)
(CMS:INCLUDE teaser caching="{$set['cache_time']}")
An invalid value (caching=5x) appears as an error message; the template then runs
without a cache.
cacheparams
The cache distinguishes by website and language on its own. If the
output depends on further things – the record, the page of a list, a filter, the
signed-in customer –, they belong in cacheparams. Each other value creates
its own cache file.
Permitted are null, true/false, numbers, texts and arrays. A variable without
quotation marks is passed unchanged, including an array:
<?php $params = ['id' => $cms['record_id'], 'page' => (int)($_GET['skip'] ?? 0)]; ?>
(CMS:INCLUDE product_list caching=10m cacheparams=$params)
(CMS:INCLUDE product_box caching=1h cacheparams=$cms['record_id'])
(CMS:INCLUDE product_box caching=1h cacheparams="product-{$cms['record_id']}")
Arrays are compared as a whole – the same keys in a different order produce a different cache file.
What the cache takes into account
- If the included template is changed, its cache files immediately no longer apply. If content changes in the back end, however, visitors see it only after the time limit has expired. Choose the time limit according to how quickly changes must become visible.
- A template that itself includes templates stores their output with it. If such an inner template changes, the cached output remains until the time limit expires.
- Only the output is stored. What the template otherwise does – setting values in
$set,CMS:HEADER,CMS:CSP, cookies, emails – does not happen on a cache hit. Such commands belong in the calling template. - The nonce of the script policy (
CMS:NONCE) is the current one in every delivery. - Do not cache forms with security tokens (
CMS:USER_TOKEN,CMS:USER_INPUT) and personal content of signed-in customers – or include the customer incacheparams. - If a command reports an error while generating the output, the output is not stored.
- In the preview from the back end and when frontend editing is switched on, no cache applies.
- If many visitors call an expired template at the same time, only one of them generates it; the others wait briefly and then get the result.
Expired files are cleaned up by the scheduler task
“Clean up frontend cache”; the cache is emptied completely with
php Core/cron/camindo.php cache include. The system administrator sets up both.
CMS:NONCE
Outputs the nonce of the page – for every script in the template.
(CMS:NONCE [parameter=value …])
Method: Cms::cmsNonce()
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
format |
"attribute" / "value" |
"attribute" |
attribute: nonce="…" (with a leading space, for a <script> tag). value: only the value. |
|
set |
array index of $set |
– | The output is not output, but stored in $set. |
Usage
If the script policy of the website is enforced (see CMS:CSP), only scripts with the nonce of the page run – a random value that changes with every request. Therefore, write every script in the template like this:
<script(CMS:NONCE)>
document.querySelectorAll('[data-confirm]').forEach(function (form) {
form.addEventListener('submit', function (ev) {
if (!confirm(form.dataset.confirm)) ev.preventDefault();
});
});
</script>
Included files get it as well if they do not come from your own website:
<script src="https://cdn.example.com/lib.js"(CMS:NONCE)></script>
camindo CMS does not add the nonce by itself. If it is missing, the template editor reports a note when you save (“script without (CMS:NONCE)”), as does the widget “Template messages” on the start page. As long as the script policy is off, the attribute does no harm – nevertheless, add it right away, so the policy can be switched on later without changing the templates.
onclick="…", onsubmit="…" and href="javascript:…" do not run under the policy at all,
not even with a nonce: move the code into a <script(CMS:NONCE)> with
addEventListener (example above).
If PHP generates the tag, get the nonce with set:
(CMS:NONCE set=nonce)
<?php echo '<script' . $set['nonce'] . '>var shop = ' . json_encode($config) . ';</script>'; ?>
For a custom policy via CMS:HEADER:
(CMS:NONCE format=value set=nonce)
(CMS:HEADER "Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$set['nonce']}'")
A page that a proxy or cache delivers with a fixed nonce no longer offers protection – do not cache pages with a script policy as a whole.
Parameters in detail
format
<script(CMS:NONCE)>…</script>
(CMS:NONCE format=value)
set
(CMS:NONCE set=nonce)
CMS:SAVEDATA
Stores the data of the current record in the specified variable.
(CMS:SAVEDATA phpvar [parameter=value …])
Method: Cms::cmsSaveData()
Without key: the first value without = is the phpvar
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
phpvar ¹ |
index within $GLOBALS |
– | yes | Variable in which the data of the current record is to be stored. |
id |
record ID or slug |
– | Record whose data is to be loaded. Pure digit sequence = record_id, anything else = slug – the same rule as in the URL. Without specification, the currently loaded record. | |
lang |
language version code |
$lang |
Specifies in which language the contents of multilingual fields are to be loaded. Not yet implemented – currently always the language of the call applies. |
¹ can be given without a key as the first value.
Usage
Stores a complete record as an array in a global PHP variable – for logic that cannot be written with the output commands alone:
(CMS:SAVEDATA article)
<?php if ($article['price'] > 100 && $article['stock'] > 0) { ?>
<span class="badge">Premium</span>
<?php } ?>
With id, load a different record, for example the parent node, in order to calculate
with its values:
(CMS:SAVEDATA parent id="{$cms['parent_id']}")
<?php if ($parent['price'] > $cms['price']) { ?> … <?php } ?>
If you only need to output fields of another record, you do not need
CMS:SAVEDATA – the output commands read with id themselves:
<a href="(CMS:URL "{$cms['parent_id']}")">(CMS:TEXT title id="{$cms['parent_id']}")</a>
<footer>(CMS:TEXT footer_text id=configuration)</footer>
The array contains the meta fields (record_id, area_id, online_at …) and the
fields of the area in the current language; upload fields as a file array (filename,
mime_type, size …). Relation fields are missing – their targets are provided by
CMS:SELECT relation=…, their count by (CMS:SELECT x relation=topics hits=false) –
afterwards it is available in $this->db->num_rows_unlimited.
If the record does not exist, the array is empty.
Parameters in detail
phpvar
Name of the variable; may be given without a key. Only letters, digits and _.
The array is then available directly as $article in the template – also in templates
included with CMS:INCLUDE – and additionally in $GLOBALS['article'], for example
for your own PHP functions:
(CMS:SAVEDATA phpvar=article)
(CMS:SAVEDATA article)
<?php echo $article['record_id']; ?>
Names starting with _, as well as this and GLOBALS, are only accessible via
$GLOBALS[…].
id
Another record – record_id or slug:
(CMS:SAVEDATA parent id="{$cms['parent_id']}")
(CMS:SAVEDATA site id=configuration)
lang
Not yet implemented – the language of the call applies.
CMS:FED
Creates a link for editing a record when frontend editing is enabled.
(CMS:FED id [parameter=value …])
Method: Cms::cmsFed()
Without key: the first value without = is the id
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
label |
text |
[»] |
Text that is linked. | |
id ¹ |
record ID or slug |
(current record) |
Record the link points to. Pure digit sequence = record_id, anything else = slug – the same rule as in the URL. | |
info |
true / false |
false |
true: The link opens the detail view of the record instead of the input mask. | |
prefix |
text |
– | Text that is output before the link (not linked). | |
suffix |
text |
– | Text that is output after the link (not linked). | |
set |
array index of $set |
– | The link is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
Usage
Edit link into the back end – only visible if the visitor has switched on the
frontend editing in their personal back-end settings (cookie fed). For visitors without
the cookie, the command outputs nothing, and the page remains unchanged:
<h1>(CMS:TEXT title) (CMS:FED)</h1>
Output (with cookie): <h1>Hello world <a href="#" data-c9-fed="1977" class="c9-fed"><span style="color:#f00">[»]</span></a></h1> – the click is handled by (CMS:FED_SCRIPT) (once per page), which opens the back end in a new tab, or at its address if a fixed back-end domain is set.
In a list, one link per row, with its own text:
(CMS:RESULT_START)
<li>(CMS:TEXT title) (CMS:FED label="✎" prefix=" " )</li>
(CMS:RESULT_END)
The back end still requires sign-in – if necessary, the link opens the sign-in dialog above the input mask.
Parameters in detail
label
Link text (HTML allowed); the default is a red [»] (<span style="color:#f00">[»]</span>):
(CMS:FED label="edit")
(CMS:FED label="<img src='/static/edit.svg' alt='edit'>")
id
Another record; may be given without a key:
(CMS:FED 1977)
(CMS:FED id="{$cms['parent_id']}" label="Edit parent")
info
Open the detail view instead of the input mask:
(CMS:FED info=true label="Details")
prefix
Unlinked text before the link – only output if the link is output:
(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
Outputs HTML code when frontend editing is enabled.
(CMS:FED_CODE code [parameter=value …])
Method: Cms::cmsFedCode()
Without key: the first value without = is the code
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
code ¹ |
HTML code |
– | HTML code that is output when frontend editing is enabled. | |
instead |
HTML code |
– | Alternative output when frontend editing is disabled. | |
set |
array index of $set |
– | The code is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
Usage
Any HTML that appears only when frontend editing is enabled – for example a toolbar or a hint about the editing mode:
(CMS:FED_CODE "<div class=fed-bar>Editing mode – <a href='/backend/'>Open back end</a></div>")
With instead, an alternative for visitors without frontend editing:
<body class="(CMS:FED_CODE code="editing" instead="public")">
Parameters in detail
code
HTML when frontend editing is enabled; may be given without a key:
(CMS:FED_CODE code="<link rel=stylesheet href='/static/fed.css'>")
(CMS:FED_CODE "<link rel=stylesheet href='/static/fed.css'>")
instead
Output for visitors without frontend editing:
(CMS:FED_CODE code="<p>You see the editorial view.</p>" instead="")
set
(CMS:FED_CODE code=1 instead=0 set=fed)
<?php if ($set['fed']) { ?> … <?php } ?>
CMS:FED_SCRIPT
Provides the script for the edit links from CMS:FED – once per page.
(CMS:FED_SCRIPT [set])
Method: Cms::cmsFedScript()
Without key: the first value without = is the set
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
set ¹ |
array index of $set |
– | The script block is not output, but stored in the variable $set. |
¹ can be given without a key as the first value.
Usage
The links from CMS:FED contain no target and no JavaScript code. The click is
handled by the script from CMS:FED_SCRIPT, which opens the input mask (or the detail view
with info=true) in the back end in a new tab – or at its address if a fixed
back-end domain is set. Without CMS:FED_SCRIPT on the page, clicking the link has no effect.
Write the command once into a template that every page includes, usually
in the footer before </body>:
(CMS:FED_SCRIPT)
</body>
</html>
The position relative to the CMS:FED links does not matter. Like CMS:FED, the
command outputs something only if the visitor has switched on frontend editing;
for other visitors, the page remains unchanged. If the command appears several times on a
page (for example via several CMS:INCLUDE), the script still appears only once.
The <script> tag carries the nonce of the page and therefore also runs under an
enforced script policy (see CMS:CSP).
Parameters in detail
set
set outputs nothing, but stores the script block under $set['fed'], for example to
output it at a different place:
(CMS:FED_SCRIPT set=fed)
<?php echo $set['fed']; ?>
CMS:USER_2FA
Shows the second factor of the signed-in frontend user, sets it up (authenticator app or code by email) and switches it off.
(CMS:USER_2FA [parameter=value …])
Method: Modules\Record\RecordUser::cmsTwoFactor()
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
function |
status / totp_setup / totp_enable / code_enable / disable / recovery_renew |
status |
What the command does (see below). | |
submit |
text value |
– | Writing functions run only if this field contains a text. | |
data |
PHP variable name |
data |
Name of the PHP array with the inputs. | |
codefield |
field name |
code |
Input field for the app code in data[…]. |
|
currentfield |
field name |
current_password |
Input field for the password in data[…]. |
|
message[…] |
default message |
– | Error messages. In the index: use case; in the value: message text. | |
cookiename |
cookie name |
usess |
Name of the session cookie [a-z0-9]. | |
csrf |
true / false |
true |
true: Writing functions require a token from CMS:USER_TOKEN. |
Usage
The command belongs on a protected page of the customer account (after
CMS:USER_PROTECT). It takes effect only if the profile area allows a second
factor (setting “Second factor” is not “off”; otherwise the message policy).
function |
Effect | Confirmed with |
|---|---|---|
status |
read only | – |
totp_setup |
creates a new key for the authenticator app – nothing is saved yet. $set['totp_setup'] contains token, qr (ready-made HTML with the QR code), secret (to type in, in groups of four), uri (otpauth://…, which opens the app directly on the phone) and qr_script (address of the QR library) |
– |
totp_enable |
saves the key and creates ten recovery codes ($set['recovery_codes'], shown in plain text only at this moment) |
data[token] and the first code from the app (data[code]) |
code_enable |
code by email at every sign-in | password (data[current_password]); if an app is set up, the app code |
disable |
switches off. If the area always requires a second factor, the code is then sent by email instead of from the app | app code or recovery code, or password |
recovery_renew |
new recovery codes; the old ones expire | app code |
After each function, $set contains: second_factor ('', totp, email),
second_factor_policy (setting of the area: none, optional, new_device, required),
second_factor_since (UTC) and recovery_count. After a change, second_factor_changed is set
(totp_enabled, totp_disabled, code_enabled, code_disabled, recovery_renewed) – for
example for a notice email to the frontend user.
To set up the app, the frontend user scans the QR code with the app. It contains the key,
not the codes – the app generates those itself afterwards, six digits each. Alternatively, the
user types in the key or opens the link on the phone. $set['totp_setup']['qr'] brings
everything: an element with the QR code, the qrcode-generator library (the same as in the back
end, taken from its assets) and a small script. Both use the nonce of the page, so the setup also
runs under an enforced script policy. Output it only with echo, and control the size with 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>Your recovery codes (please keep them safe):</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>Key for your authenticator app: <code><?php echo htmlspecialchars($s['secret']); ?></code>
– or <a href="<?php echo htmlspecialchars($s['uri']); ?>">open it in the app</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>Switch on</button>
</form>
<?php } elseif ($set['second_factor'] === '') { ?>
<p><a href="?setup=1">Set up authenticator app</a></p>
<?php } ?>
Message indexes: auth (not signed in), policy, code (app code wrong), current (password
wrong), setup (setup expired – 15 minutes), blocked (too many failed attempts), token.
Wrong codes and passwords count in the same way as at sign-in.
If a frontend user has lost their phone and their recovery codes, an administrator resets the second factor in the back end (profile record, menu “Second factor”).
Parameters in detail
function
(CMS:USER_2FA function=status)
(CMS:USER_2FA function="$action" submit="$submit")
submit
Only writing functions need it – they belong in a POST form:
<?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]="The code is not correct.")
CMS:USER_AUTH
Checks a session ID for validity.
(CMS:USER_AUTH [parameter=value …])
Method: Modules\Record\RecordUser::cmsAuth()
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
sessionid |
session ID |
– | Resumes the session of the frontend user with the given session ID instead of using a cookie. | |
filter[…][…] |
see parameter "filter[]" of CMS:SELECT |
– | Filter criteria for the profile record that the signed-in frontend user must meet. | |
mode[…] |
AND / OR |
OR |
Mode in which values that concern the same field are combined. | |
cookiename |
cookie name |
usess |
Name of the session cookie [a-z0-9]. |
[…] – the parameter accepts sub-keys, e.g. name[key]=value.
Usage
Takes up the frontend session and provides the signed-in frontend user, without blocking the
page – for pages that show different content to signed-in and anonymous visitors. Result in
$set:
$set[…] |
Content |
|---|---|
user_id |
record_id of the profile; 0 = not signed in |
user |
the profile record as an array ([] if not signed in) |
user_session_id |
session ID; '' = no session |
password_reset |
1 after a sign-in via the link from CMS:USER_PW, as long as a new password may be set without the previous one; otherwise 0 |
(CMS:USER_AUTH)
<?php if ($set['user_id']) { ?>
Hello <?php echo htmlspecialchars($set['user']['nickname']); ?> – <a href="/0,u_logout">sign out</a>
<?php } else { ?>
<a href="/0,u_login">sign in</a>
<?php } ?>
The command belongs at the start of the template (before any output), so that the session
cookie can be set. For pages that only signed-in frontend users may see,
CMS:USER_PROTECT is the appropriate command.
Parameters in detail
sessionid
Takes up the session by parameter instead of by cookie – for calls from apps or from other domains:
(CMS:USER_AUTH sessionid="{$_GET['sid']}")
filter
An additional condition on the profile, with the same syntax as CMS:SELECT. If it does not
apply, the frontend user counts as not signed in:
(CMS:USER_AUTH filter[role]=editor)
(CMS:USER_AUTH filter[status_id]=0 filter[newsletter]=1)
mode
Combines several values of the same field in filter (default OR):
(CMS:USER_AUTH filter[role]=editor filter[role]=admin mode[role]=OR)
cookiename
Name of the session cookie (default usess), for example for two separate sign-ins on one
domain:
(CMS:USER_AUTH cookiename=shop)
Must be the same for all USER_* commands.
CMS:USER_BASKET
Edits a shopping basket.
(CMS:USER_BASKET function [parameter=value …])
Method: Modules\Record\RecordUser::cmsBasket()
Without a key: the first value without = counts as function
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
function ¹ |
"load" / "add" / "update" / "update_addon" |
"load" |
yes | Function to be executed. |
overwrite |
false / true |
false |
function=add + overwrite=true: The number of items already saved is updated when adding. | |
category |
category ID |
0 |
Shopping basket category. | |
container |
container IDs or names, comma-separated |
– | Only records from these containers get into the basket. Without specification: published records from containers of type “record” or “ugc” (never profiles). | |
variable |
variable name |
– | Name of the variable in which the item IDs are stored as an array. | |
duration |
number of days |
30 |
Number of days the basket stays saved, provided no changes are made. | |
basket_addon |
text |
– | Text (serialized data) to be saved. | |
csrf |
true / false |
true |
true: The request must contain a valid token from CMS:USER_TOKEN (protection against CSRF). false switches off the check for this command – only with good reason. |
¹ can be given without a key as the first value.
Usage
Shopping basket per frontend session – it also works without signing in (anonymous session).
Result in $set:
$set[…] |
Content |
|---|---|
basket |
record_id => quantity |
basket_count |
total of the quantities |
basket_addon |
text of the category saved with update_addon |
Add items (form with items[]), display the shopping basket:
<?php $f = $_POST['f'] ?? 'load'; ?>
(CMS:USER_BASKET function="$f" variable=items)
(CMS:USER_BASKET load)
<h2>Shopping basket (<?php echo (int)$set['basket_count']; ?> pieces)</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>Update quantities (0 = remove)</button>
</form>
Only after load are the items available as a result set – CMS:RESULT_START runs over them just
as after a CMS:SELECT. After add, update and update_addon, $set is up to date, so the
list needs a second call with load (example above). Without a valid token from
CMS:USER_TOKEN, a writing function changes nothing. An “Add to basket” form in a list of items:
<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)">Add to basket</button>
</form>
Parameters in detail
function
May stand without a key:
(CMS:USER_BASKET load) read the content, items as result set
(CMS:USER_BASKET add variable=items) add the IDs from $_REQUEST['items']
(CMS:USER_BASKET update variable=items) set quantities (0 removes)
(CMS:USER_BASKET update_addon basket_addon="{$note}")
overwrite
With add: set the quantity instead of adding to it:
(CMS:USER_BASKET add variable=items overwrite=true)
With items[5]=2, item 5 is then in the basket exactly twice, even if it was already in it.
category
Several baskets per session, e.g. a wish list next to the shopping basket:
(CMS:USER_BASKET add variable=items category=1)
(CMS:USER_BASKET load category=1)
container
Restricts the basket to certain content areas (names or IDs, comma-separated). Without a
specification, published records from areas of type “record” or “ugc” are allowed, never
profiles. The check also applies when reading: items outside these areas appear neither in
$set['basket'] nor in the result set.
(CMS:USER_BASKET add variable=items container=products,accessories)
(CMS:USER_BASKET load container=products,accessories)
variable
Name of the request parameter with the item IDs – as a list (items[]=5, quantity 1 per
mention) or with quantities (items[5]=2):
<input type="hidden" name="cart[<?php echo $id; ?>]" value="1">
(CMS:USER_BASKET add variable=cart)
duration
Days the basket is kept without changes (default 30):
(CMS:USER_BASKET add variable=items duration=7)
basket_addon
Store free text for the category (delivery note, voucher code, serialized data):
(CMS:USER_BASKET update_addon basket_addon="{$_POST['note']}")
<?php echo htmlspecialchars($set['basket_addon']); ?>
CMS:USER_DELETE
Deletes a record that belongs to the signed-in frontend user.
(CMS:USER_DELETE id [parameter=value …])
Method: Modules\Record\RecordUser::cmsDelete()
Without a key: the first value without = counts as id
PHP snippet: include_and_stop (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
id ¹ |
record ID |
– | yes | Record to be deleted. |
ignoreuser |
true / false |
false |
true: The record to be deleted need not belong to the signed-in frontend user (caution: observe the security notes!). | |
successtemplate |
template name |
– | yes | Template that is included when the entry has been deleted successfully. |
message_error |
text |
Access denied! |
Error message if the user has no access to the record. | |
csrf |
true / false |
true |
true: The request must contain a valid token from CMS:USER_TOKEN (protection against CSRF). false switches off the check for this command – only with good reason. |
¹ can be given without a key as the first value.
Usage
Deletes a record that belongs to the signed-in frontend user (record.user_id) – or the user's own
profile. In the latter case, all sessions and known devices of the user are removed as well.
What “delete” means is determined by the deletion policy of the area, with its blocks for the
trigger “record is deleted”. This is the same as when deleting in the back end: into the trash,
permanently, or overwriting individual fields (for example setting the status to “Disabled” and
the sign-in name to user_{record_id}), also with a time delay. If the user deletes their profile,
all of their contributions are deleted at the same time according to their policy (trigger “UGC:
profile was deleted”). If the policy has no block for this trigger, the command outputs an error
message. See deletion policies.
A record that is already marked for deletion cannot be deleted again.
<?php $id = (int)($_POST['id'] ?? 0); if ($id > 0) { ?>
(CMS:USER_DELETE id="$id" successtemplate=u_deleted message_error="This entry does not belong to you.")
<?php } ?>
(CMS:MESSAGE)
<form method="post">
(CMS:USER_TOKEN)
<input type="hidden" name="id" value="<?php echo (int)$_GET['id']; ?>">
<button>Permanently delete entry</button>
</form>
The command has no submit: it runs on every call and reports an error if there is no valid id.
That is why the PHP condition surrounds the command (CMS commands are turned into PHP code, so an
enclosing if is allowed). After success, successtemplate is included and the page ends (except
with CMS:CONTINUE).
Parameters in detail
id
The record ID; it may stand without a key. Without a valid ID, the message from message_error
appears:
(CMS:USER_DELETE "{$_POST['id']}" successtemplate=u_deleted)
(CMS:USER_DELETE id="{$set['user_id']}" successtemplate=u_account_closed) delete own profile
ignoreuser
Skips the ownership check and the sign-in. The template is then responsible (for example for deletion via a secret link):
(CMS:USER_DELETE id="{$id}" ignoreuser=true successtemplate=u_deleted)
successtemplate
(CMS:USER_DELETE id="$id" successtemplate=u_deleted)
u_deleted can, for example, output “Entry deleted” and show the navigation via CMS:INCLUDE.
message_error
Text shown if the record does not exist, does not belong to the frontend user, or nobody is signed
in (output via CMS:MESSAGE). If the token from CMS:USER_TOKEN is missing, the standard message
for an expired form appears instead:
(CMS:USER_DELETE id="$id" successtemplate=u_deleted message_error="Deletion is not possible.")
CMS:USER_INPUT
Creates or updates a record from data that a frontend user has entered.
(CMS:USER_INPUT [parameter=value …])
Method: Modules\Record\RecordUser::cmsInput()
PHP snippet: include_and_stop (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
successtemplate |
template name |
– | Template that is included when the entry has been saved successfully. | |
data |
PHP variable name |
data |
Name of the PHP array that contains the data to be saved. | |
overwrite[…] |
key-value pairs |
– | Explicitly overwrites the input of the frontend user (including meta fields, except record_id, area_id, instance_id, tmp, deleted, origin_id, owner_id, created_by, changed_by). Do not pass on any input of the frontend user here! | |
container |
container ID or name |
– | yes | ID or name of the content area in which the new record is to be saved. |
message[…] |
text |
default text |
Error message depending on the index (see table). | |
submit |
text value |
– | yes | The command runs only if this field contains a text. |
fields |
comma-separated list |
– | Names of the input fields that may be written. Empty = all fields defined for the container. | |
id |
record ID |
– | If given, an attempt is made to update this record (UPDATE). Otherwise a new one is created (CREATE). | |
ignoreuser |
true / false |
false |
true: The record to be adjusted need not belong to the signed-in frontend user (caution: observe the security notes!). | |
forceupdate |
true / false |
false |
true: Forces the update of the record given via the parameter “id”. On error, a message is output. | |
masterid |
record ID |
– | Only when creating in areas with a tree structure: parent node of the new record (parent_id). | |
owner |
user ID |
– | Only when creating: admin_id of the back-end owner (record.owner_id, created_by). If not given, user.default_owner_id applies. | |
removefiles |
comma-separated list |
– | Names of the upload fields whose content is to be deleted. | |
currentfield |
field name |
current_password |
Only profile containers: name of the form field data[…] with the current password. A new password is saved only together with the current one – except directly after signing in via the reset link. Message current. |
|
csrf |
true / false |
true |
true: The request must contain a valid token from CMS:USER_TOKEN (protection against CSRF). false switches off the check for this command – only with good reason. |
[…] – the parameter accepts sub-keys, e.g. name[key]=value.
Usage
Saves form input as a record – creates a new one or changes one of the frontend user's own
records (with id). The fields are called data[fieldname], exactly as in the back-end input
mask; uploads go through the same form. After success, $set['record_id'] is available, the
successtemplate is included, and the page ends:
<?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">Save</button>
</form>
Change one's own record:
<?php $submit = $_POST['submit'] ?? ''; $id = (int)($_POST['id'] ?? 0); ?>
(CMS:USER_INPUT container=comments id="$id" forceupdate=true submit="$submit" successtemplate=u_saved)
Contact form without sign-in (ignoreuser), writing the sender's IP address into its own field
remote_ip of the area:
(CMS:USER_INPUT container=inquiries ignoreuser=true submit="$submit" fields="name,email,message"
overwrite[remote_ip]="{$_SERVER['REMOTE_ADDR']}" successtemplate=inquiry_thanks)
Who may write: in ugc areas, signed-in frontend users whose profile area is enabled for the
area; in profile areas only one's own profile (with id – a new profile is created only by
CMS:USER_REGISTER); in ordinary record areas only with ignoreuser=true. All checks of the
area (required fields, unique values, file types) apply as in the back end; errors appear via
CMS:MESSAGE. Message indexes for message[…]: container, access, notfound, save,
current, blocked, owner (no valid owner when creating), token.
Changing the password
A new password in one's own profile is saved by the command only together with the
current password in the field data[current_password]. A stranger who gets hold of an
open session cannot take over the account this way. The exception is directly after signing in
via the link from CMS:USER_PW: then $set['password_reset'] is 1 and the field is
omitted (for one hour, until the new password has been saved). After saving, all other
sign-ins of the profile are ended, and $set['password_changed'] is 1 – the successtemplate
can notify the owner by CMS:EMAIL.
(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">Save</button>
</form>
If the password field in the area carries the extension “Password policy”, the server checks the minimum length and the character classes. CMS:USER_PASSWORD_RULES shows the same rules in the form while the user types.
Parameters in detail
successtemplate
Included after saving. In it, the saved record is the current one ($cms), and
$set['record_id'] is its ID:
(CMS:USER_INPUT container=comments submit="$submit" successtemplate=u_saved)
Without successtemplate, the template continues normally after saving.
data
Name of the request array (default data):
<input name="form[comment]">
(CMS:USER_INPUT container=comments data=form submit="$submit")
overwrite
Set values in advance – including meta fields – regardless of what the frontend user sends.
No input of the frontend user belongs here. Excluded are record_id, area_id,
instance_id, tmp, deleted, origin_id, owner_id, created_by and changed_by. Without
a specification, the command sets needs_review to 1:
(CMS:USER_INPUT container=comments submit="$submit"
overwrite[article_id]="{$cms['record_id']}"
overwrite[status_id]=2
)
container
Area (name or ID) in which saving takes place – type ugc or profile; with ignoreuser=true
also an ordinary record area:
(CMS:USER_INPUT container=comments …)
(CMS:USER_INPUT container=58 …)
message
Your own message texts:
(CMS:USER_INPUT container=comments submit="$submit"
message[access]="Please sign in to comment."
message[save]="The comment could not be saved."
)
submit
The command runs only if submit has a value – so not on the first call of the form:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_INPUT container=comments submit="$submit" …)
<button name="submit" value="1">Save</button>
fields
Take only these fields from the inputs – everything else in the form is ignored:
(CMS:USER_INPUT container=members id="{$set['user_id']}" fields="nickname,avatar" submit="$submit")
id
Change an existing record. Without forceupdate, a new record is created if the ID is unknown
or belongs to someone else:
(CMS:USER_INPUT container=comments id="{$_POST['id']}" submit="$submit")
ignoreuser
No sign-in and no ownership check – for contact and guestbook forms. The template is responsible
(spam protection, field list via fields):
(CMS:USER_INPUT container=guestbook ignoreuser=true fields="name,message" submit="$submit" successtemplate=gb_thanks)
forceupdate
With id: instead of creating a new record for an unknown or foreign ID, output the message
notfound:
(CMS:USER_INPUT container=comments id="$id" forceupdate=true submit="$submit")
masterid
Parent node in an area with a tree structure (becomes parent_id):
(CMS:USER_INPUT container=forum masterid="{$cms['record_id']}" submit="$submit") Reply below the current post
currentfield
Another name for the field with the current password (see Changing the password):
(CMS:USER_INPUT container=members id="{$set['user_id']}" fields=password currentfield=old_pw …)
<input type="password" name="data[old_pw]">
removefiles
Empty upload fields – for example via a checkbox “Remove photo”:
<label><input type="checkbox" name="remove_photo" value="photo"> Remove photo</label>
(CMS:USER_INPUT container=comments id="$id" removefiles="{$_POST['remove_photo']}" submit="$submit")
CMS:USER_LOGIN
Checks the sign-in data of a frontend user and creates a new session.
(CMS:USER_LOGIN [parameter=value …])
Method: Modules\Record\RecordUser::cmsLogin()
PHP snippet: include_and_stop (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
successtemplate |
– | – | yes | Detail template that is included after a successful sign-in. |
container |
container ID or name |
– | yes | ID or name of the profile container in which the data of the frontend users is stored. |
data |
PHP variable name |
data |
Name of the PHP array that contains the sign-in data. | |
message[…] |
default message |
– | Error messages. In the index: use case; in the value: message text (see the table). | |
hash |
hash |
– | A sign-in can also be carried out by means of a hash (token from CMS:USER_PW, forgotten password). | |
trim |
true / false |
false |
true: remove spaces at the beginning and end of the input. | |
cookie |
true / false |
true |
true: a cookie is set by default to keep the sign-in across several pages. | |
submit |
text value |
– | The command runs only if this field contains a text. Not applicable to sign-in by “hash”. | |
cookiename |
cookie name |
usess |
Name of the session cookie [a-z0-9]. | |
csrf |
true / false |
true |
true: The request must contain a valid token from CMS:USER_TOKEN (protection against CSRF). false switches off the check for this command – only with good reason. | |
codetemplate |
– | – | Template for the second step of the sign-in if the profile area requires a second factor. Required as soon as the area uses one. |
[…] – the parameter accepts sub-keys, e.g. name[key]=value.
Usage
Checks sign-in name and password against the profile area and creates a session (cookie).
The fields are those defined in the area as sign-in name and password (data[nickname],
data[password]). After success, successtemplate is included; in it the profile is the
current record, and $set['user_id'] and $set['user'] are filled:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=u_welcome)
<h1>Sign in</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">Sign in</button>
</form>
Sign-in via the link from CMS:USER_PW (forgotten password):
(CMS:USER_LOGIN container=members hash="{$_GET['hash']}" successtemplate=u_newpw)
The command belongs at the start of the template (the cookie must be set before any output).
Failed attempts are counted (brute-force protection). Message indexes: failed, blocked,
disabled (status 1), unconfirmed (status 2), hash, container, token. Counting is
done per sign-in name and additionally per origin (IP address): anyone who tries many accounts
with one password each from the same connection is blocked as well (blocked).
Second factor
In the profile area the administrator determines whether a two-step sign-in is required
(setting “Second factor”: off, optional, code for new device, always). If a sign-in needs a
code, USER_LOGIN does not sign anyone in after the correct password. Instead, it includes
codetemplate. There you find:
$set[…] |
Content |
|---|---|
second_factor |
totp (code from the authenticator app) or email (code by email) |
login_pending |
1 |
pending_user |
the profile (also the current record) – for example for the email address |
login_code |
with a code by email: the six-digit code that you send |
The codetemplate sends the code with CMS:EMAIL and shows the form for
CMS:USER_LOGIN_CODE (example there). As with all CMS:USER_* commands,
USER_LOGIN does not send any email itself – the template always does that. The sign-in via
the link from “Forgot password” (hash) also requires the code if the frontend user has set
up an authenticator app. The setup itself is handled by CMS:USER_2FA.
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=u_welcome codetemplate=u_code_mail)
Sign-in from a new device
camindo records the devices (browsers) with which a profile signs in. If a sign-in comes from
an unknown device and the profile already has other devices, $set['new_device'] is 1
in the successtemplate. $set['device'] contains label (e.g. “Firefox / macOS”), ip and
time (UTC). You send the notice to the owner yourself – before the redirect, with
CMS:EMAIL:
<?php
$to = (string)($set['user']['email'] ?? '');
$send = (!empty($set['new_device']) && $to !== '') ? '1' : '';
$body = "New sign-in: {$set['device']['label']}, IP {$set['device']['ip']}, {$set['device']['time']} UTC."
. " If this was not you, please set a new password.";
?>
(CMS:EMAIL submit="$send" to="$to" subject="New sign-in" body="$body")
The very first sign-in of a profile does not trigger a notice. Whether an SMS is sent instead is determined by the system administrator (see the manual for administrators, Operation).
Parameters in detail
successtemplate
(CMS:USER_LOGIN container=members submit="$submit" successtemplate=u_welcome)
In u_welcome: Hello (CMS:TEXT nickname) – the profile is the current record there.
container
Profile area (name or ID):
(CMS:USER_LOGIN container=members …)
data
Name of the request array with the sign-in data (default 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 or password is incorrect."
message[unconfirmed]="Please confirm your email address first."
message[blocked]="Too many failed attempts – please try again later."
)
hash
Sign-in with the one-time token from CMS:USER_PW instead of a password; submit does not
apply:
(CMS:USER_LOGIN container=members hash="{$_GET['hash']}" successtemplate=u_newpw)
In u_newpw the frontend user sets a new password with
CMS:USER_INPUT container=members id="{$set['user_id']}" fields=password. After this sign-in,
$set['password_reset'] is 1: for one hour (setting user.password_token_expiration) the user
does not need the previous password for this – see
CMS:USER_INPUT, changing the password.
codetemplate
Template for the second step (see above):
(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)
If it is missing although the area requires a second factor, the sign-in fails with failed
and an entry in the user log.
trim
Remove spaces around name and password (copy errors on mobile devices):
(CMS:USER_LOGIN container=members trim=true submit="$submit" successtemplate=u_welcome)
cookie
Do not set a cookie – the session ID is then only in $set['user_session_id'] (for apps that
pass it on themselves):
(CMS:USER_LOGIN container=members cookie=false submit="$submit" successtemplate=api_token)
submit
Run only when the form has been submitted:
<?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)
Must be the same for all USER_* commands.
CMS:USER_LOGIN_CODE
Second step of the sign-in: checks the code from the authenticator app, the code by email or a recovery code, and signs in the frontend user.
(CMS:USER_LOGIN_CODE [parameter=value …])
Method: Modules\Record\RecordUser::cmsLoginCode()
PHP snippet: include_and_stop (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
successtemplate |
– | – | yes | Detail template that is included after the successful sign-in. |
submit |
text value |
– | The code is checked only if this field contains a text. | |
resend |
true / false |
false |
true: creates a new code by email (throttled) and includes codetemplate. |
|
codetemplate |
– | – | Template that sends the new code with CMS:EMAIL after resend – the same as with CMS:USER_LOGIN. |
|
data |
PHP variable name |
data |
Name of the PHP array with the inputs. | |
codefield |
field name |
code |
Input field for the code in data[…]. |
|
message[…] |
default message |
– | Error messages. In the index: use case; in the value: message text. | |
sessionid |
session ID |
– | Waiting sign-in without a cookie (apps) – $set['user_session_id'] after CMS:USER_LOGIN. |
|
cookiename |
cookie name |
usess |
Name of the session cookie [a-z0-9]. | |
csrf |
true / false |
true |
true: The request must contain a valid token from CMS:USER_TOKEN. |
Usage
If the profile area requires a second factor, CMS:USER_LOGIN includes the
codetemplate after the correct password. The sign-in then waits in a session without a
frontend user; USER_LOGIN_CODE completes it.
Divide the work between two templates:
u_code_mailsends the code ($set['login_code']) with CMS:EMAIL and then includes the form. It is thecodetemplateofCMS:USER_LOGINand ofCMS:USER_LOGIN_CODE(for “Send new code”).u_codecontainsCMS:USER_LOGIN_CODEand the form; it is the target of the form.
Without submit and resend, the command only fills $set, and the template continues. Within
one call, it evaluates submit and resend only the first time – if u_code_mail includes
u_code again after a resend, nothing happens twice. Give the buttons their own names: the
sign-in form has already sent submit=1.
u_code_mail:
<?php
$to = (string)($set['pending_user']['email'] ?? '');
$send = (!empty($set['login_code']) && $to !== '') ? '1' : '';
$body = "Your sign-in code: {$set['login_code']} (valid for 10 minutes)";
?>
(CMS:EMAIL submit="$send" to="$to" subject="Your sign-in code" 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>Confirm sign-in</h1>
<p class="error">(CMS:MESSAGE)</p>
<?php if (!empty($set['code_resent'])) { ?><p>A new code is on its way.</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">Confirm</button>
</form>
<?php if ($set['second_factor'] === 'email') { ?>
<form method="post" action="/0,u_code">
(CMS:USER_TOKEN)
<button name="code_resend" value="1">Send new code</button>
</form>
<?php } ?>
<?php } else { ?>
<p><a href="/0,u_login">To sign-in</a></p>
<?php } ?>
For a code by SMS, write the call to your SMS service in u_code_mail instead of CMS:EMAIL –
the core is not affected.
The command belongs at the start of the template (the cookie must be set before any output).
$set[…] |
Content |
|---|---|
login_pending |
1 as long as a sign-in is waiting for the code |
second_factor |
totp or email |
pending_user |
the profile of the waiting sign-in |
login_code |
after resend: the new code (in the codetemplate) |
code_resent |
1 after resend |
| after success | as with USER_LOGIN (user_id, user, new_device …), plus second_factor_used (totp, email, recovery) and recovery_remaining |
Security: A code by email is valid for 10 minutes (user.login_code_expiration). Every
wrong code counts like a wrong password. After three wrong codes the waiting sign-in expires
(message expired), and the frontend user starts again with the password. A recovery code works
only once. “Send new code” is throttled (three times per 10 minutes).
Message indexes: code (wrong code), expired (expired or too many failed attempts), blocked,
throttled (new code requested too often), token.
Parameters in detail
successtemplate
As with USER_LOGIN – the profile is the current record there:
(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]="The code is not correct."
message[expired]="Please sign in again."
)
sessionid, cookiename
As with CMS:USER_AUTH.
CMS:USER_LOGOUT
Ends and deletes the session of a signed-in frontend user.
(CMS:USER_LOGOUT [parameter=value …])
Method: Modules\Record\RecordUser::cmsLogout()
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
submit |
text value |
– | If the parameter is given, the command signs out only if it contains a text (e.g. submit="$logout" from the sign-out form). If it is empty, nothing happens – not even a token message. Without the parameter, the command signs out on every call (a valid token is assumed). |
|
deletecookie |
true / false |
true |
false: Only the server-side session is deleted. true: The browser cookie is deleted as well. | |
cookiename |
cookie name |
usess |
Name of the session cookie [a-z0-9]. | |
csrf |
true / false |
true |
true: The request must contain a valid token from CMS:USER_TOKEN (protection against CSRF). false switches off the check for this command – only with good reason. |
Usage
Ends the session of the signed-in user (including the shopping basket of the session) and deletes
the cookie. Afterwards, $set['user_id'] = 0:
(CMS:USER_LOGOUT)
(CMS:INCLUDE header)
<p>You have been signed out. <a href="/0,u_login">Sign in again</a></p>
The sign-out is triggered by a POST form with a token – a simple link is not enough, because
otherwise any foreign page could sign out your visitors. Without a valid token, the command does
not sign out and sets the message token (CMS:MESSAGE):
<form method="post" action="/0,u_logout">
(CMS:USER_TOKEN)
<button name="logout" value="1">Sign out</button>
</form>
So that a direct call of the sign-out page creates no token message, specify submit. If it is
empty (the page was only called), the command does nothing:
<?php $logout = $_POST['logout'] ?? ''; ?>
(CMS:USER_LOGOUT submit="$logout")
This command, too, belongs before the first output, so that the cookie can be deleted.
Parameters in detail
submit
Sign out only if the sign-out form has been submitted:
<?php $logout = $_POST['logout'] ?? ''; ?>
(CMS:USER_LOGOUT submit="$logout")
deletecookie
Delete only the server-side session and leave the cookie in place:
(CMS:USER_LOGOUT deletecookie=false)
cookiename
(CMS:USER_LOGOUT cookiename=shop)
CMS:USER_PASSWORD_RULES
Outputs the rules of the password policy of a profile container, including their texts – for display in the form (strength bar and speech bubble with the requirements).
(CMS:USER_PASSWORD_RULES container=… [parameter=value …])
Method: Modules\Record\RecordUser::cmsPasswordRules()
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
container |
container ID or name |
– | yes | Profile container whose password field is meant. |
field |
field name |
sign-in password field | Password field of the container. If it carries the extension “Password policy”, its rules apply; otherwise the settings of the administrator apply (the server then does not check). | |
set |
$set array index |
– | The output is not printed, but stored in $set. |
The output is already masked with htmlspecialchars for an HTML attribute – do not mask it again.
Usage
When registering and when changing the password, the server checks the password policy (minimum length, special characters, upper- and lowercase letters) if the administrator has assigned the extension “Password policy” to the password field. So that your visitors see the requirements while typing, there is a small script for this – the same one that runs in the back end. It shows a bar under the field (red = weak, orange = medium, green = strong) and a speech bubble in which the fulfilled requirements are ticked off.
1. Include the script and CSS. Copy the two files of the extension unchanged into your
template – as <style> and <script(CMS:NONCE)>. The nonce is needed by the script under a script
policy (see CMS:NONCE). It is best placed in a separate include that you add at the end
of the page:
Core/Extensions/Password/public/css/password.ext.css → <style> … </style>
Core/Extensions/Password/public/js/password.ext.js → <script(CMS:NONCE)> … </script>
The files need nothing from the back end and no library. After an update of camindo CMS, copy them again if the rules have changed.
2. Mark the field. The attribute data-c9-password takes the rules from this command – the texts
are in the language of the page:
<input type="password" name="data[password]" autocomplete="new-password"
data-c9-password="(CMS:USER_PASSWORD_RULES container=members)">
(CMS:INCLUDE password_assets)
When the page loads, the script equips every field with data-c9-password. For fields that are only
created later by JavaScript, use C9Password.attach(field, options).
Important: the display is an aid, not a check. The server is decisive – without the extension on the field, it accepts any password, even if the form shows the rules. The error message for a password that is too weak appears like other field errors, via CMS:MESSAGE.
A complete form for changing the password is shown at CMS:USER_INPUT.
Parameters in detail
container
Name or ID of the profile container:
(CMS:USER_PASSWORD_RULES container=members)
field
A password field other than the one set for sign-in:
(CMS:USER_PASSWORD_RULES container=members field=pin)
set
Does not output the rules, but stores them in $set['pwrules'] (already masked for an HTML
attribute):
(CMS:USER_PASSWORD_RULES container=members set=pwrules)
CMS:USER_REGISTER
Creates a profile record from the data supplied and sets the status to status=2.
(CMS:USER_REGISTER [parameter=value …])
Method: Modules\Record\RecordUser::cmsRegister()
PHP snippet: include_and_stop (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
successtemplate |
– | – | yes | Detail template that is included after a successful registration. |
container |
container ID or name |
– | yes | ID or name of the profile container in which the data of the frontend users is to be saved. |
emailfield |
field name |
– | yes | Name of the field in which the email address of the frontend user is saved (the confirmation link is sent there). |
fields |
comma-separated list |
– | Names of the input fields that may be written. Empty = all fields defined for the container. | |
overwrite[…] |
key-value pairs |
– | Explicitly overwrites the input of the frontend user (including meta fields, except record_id, area_id, instance_id, tmp, deleted, origin_id, owner_id, created_by, changed_by). Do not pass on any input of the frontend user here! | |
message[…] |
message |
– | Error message. In the index: use case; in the value: message text (see the table at CMS:USER_INPUT). | |
submit |
text value |
– | yes | The command runs only if this field contains a text. |
owner |
user ID |
– | admin_id of the back-end owner of the new profile (record.owner_id, created_by). If not given, user.default_owner_id applies. | |
data |
PHP variable name |
data |
Name of the PHP array that contains the data to be saved. | |
csrf |
true / false |
true |
true: The request must contain a valid token from CMS:USER_TOKEN (protection against CSRF). false switches off the check for this command – only with good reason. | |
formguard |
true / false / seconds |
false |
true: form protection against robots – the form must contain the fields from (CMS:USER_TOKEN formguard=true) and may not be submitted earlier than 3 seconds (user.formguard_min_seconds). A number sets the minimum time for this form (formguard=5), 0 switches it off. Otherwise the message throttled. |
[…] – the parameter accepts sub-keys, e.g. name[key]=value.
Usage
Creates a profile (status 2 = unconfirmed) and generates a confirmation token in
$set['hash']; the successtemplate sends the confirmation link, whose target page calls
CMS:USER_REGISTER_CONFIRM. The sign-in name and email address must still be free in the
area, and emailfield must contain a valid address. Sign-in names that begin with user_
are reserved – that is how anonymized profiles are named (user_{record_id}, see
deletion policies);
registration rejects them:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_REGISTER container=members emailfield=contact_email fields="nickname,contact_email,password"
submit="$submit" successtemplate=u_registered)
<h1>Register</h1>
<p class="error">(CMS:MESSAGE)</p>
<form method="post">
(CMS:USER_TOKEN)
<input name="data[nickname]"> <input name="data[contact_email]" type="email">
<input name="data[password]" type="password">
<button name="submit" value="1">Create account</button>
</form>
u_registered sends the link with CMS:EMAIL. The address is there in
$set['email'] – always under this name, regardless of what the field from
emailfield is called (here contact_email). Under the field name it is found only in the
profile, $set['user']['contact_email']:
(CMS:URL id=0 template=u_confirm absolute=true set=link)
(CMS:EMAIL submit="1" to="{$set['email']}" subject="Please confirm"
body="Activate your account: {$set['link']}?hash={$set['hash']}")
(CMS:MESSAGE)
<p>Please confirm your email address.</p>
Message indexes: container, email, exists, save, throttled, owner (no valid owner,
see owner), token, and field errors of the area.
Throttling and form protection
Each registration sends an email to the address entered. camindo limits this
(throttling): five registrations per origin (IP address) and hour, three per email
address and day, 200 per website and hour. Registrations and attempts with a name that is
already taken are counted – a typo in the password policy does not count. The message is
then throttled, and $set['throttled'] = 1. As with
CMS:USER_PW, formguard=true (with
(CMS:USER_TOKEN formguard=true) in the form) protects against robots.
The message exists reveals that a name or an address already exists; the throttling limits
how often this can be tried.
Registrations that are never confirmed are deleted by camindo after seven days
(user.unconfirmed_days): anyone who enters the address of someone else only blocks it for a
short time.
Parameters in detail
successtemplate
Included after the profile has been created. It contains:
$set[…] |
Content |
|---|---|
hash |
confirmation token for the confirmation link |
email |
the checked email address – independent of the name of the field from emailfield |
user_id |
record_id of the new profile |
user |
the profile as an array (field names as keys); it is also the current record |
(CMS:USER_REGISTER container=members emailfield=email submit="$submit" successtemplate=u_registered)
container
(CMS:USER_REGISTER container=members …)
emailfield
Field with the email address – it is checked and stored in $set['email'] (under this fixed
name, not under the field name):
(CMS:USER_REGISTER container=members emailfield=contact_email …)
fields
Take only these fields from the form:
(CMS:USER_REGISTER container=members emailfield=email fields="nickname,email,password" …)
overwrite
Fixed values, including meta fields – no user input. Excluded are record_id, area_id,
instance_id, tmp, deleted, origin_id, owner_id, created_by and changed_by. The
status (status_id 2) can be overwritten here – then the confirmation by link is skipped:
(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 or email address is already taken."
message[email]="Please enter a valid email address."
)
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
Activates a profile record and sets status=0.
(CMS:USER_REGISTER_CONFIRM [parameter=value …])
Method: Modules\Record\RecordUser::cmsRegisterConfirm()
PHP snippet: include_and_stop (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
successtemplate |
– | – | yes | Detail template that is included if the registration could be completed successfully. |
hash |
hash ID |
– | yes | Hash ID created by CMS:USER_REGISTER and sent by email. |
message[…] |
message |
– | Error message. In the index: use case; in the value: message text (see table). |
[…] – the parameter accepts sub-keys, e.g. name[key]=value.
Usage
Redeems the token from CMS:USER_REGISTER and activates the profile (status 0). It stands on the
page to which the confirmation link points:
(CMS:USER_REGISTER_CONFIRM hash="{$_GET['hash']}" successtemplate=u_confirmed
message[hash]="The link is invalid or has expired.")
(CMS:INCLUDE header)
<p class="error">(CMS:MESSAGE)</p>
u_confirmed:
<p>Your account has been activated – <a href="/0,u_login">sign in now</a>.</p>
Tokens expire after 24 hours (user.token_expiration). After success, $set['user_id'] and
$set['user'] are filled. The frontend user is not yet signed in by this.
Parameters in detail
successtemplate
(CMS:USER_REGISTER_CONFIRM hash="{$_GET['hash']}" successtemplate=u_confirmed)
hash
Token from the link. If it is empty, unknown, expired or already redeemed, the message hash appears.
The command has no submit:
(CMS:USER_REGISTER_CONFIRM hash="{$_GET['hash']}" successtemplate=u_confirmed)
message
(CMS:USER_REGISTER_CONFIRM hash="{$_GET['hash']}" successtemplate=u_confirmed message[hash]="This link is no longer valid.")
CMS:USER_PROTECT
Protects a template from access by visitors who are not signed in.
(CMS:USER_PROTECT [parameter=value …])
Method: Modules\Record\RecordUser::cmsProtect()
PHP snippet: include_and_stop (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
errortemplate |
template name |
– | yes | Detail template that is included if the frontend user has no access to the container or is not signed in. |
filter[…][…] |
see parameter "filter[]" of CMS:SELECT |
– | Filter criteria for the profile record that the signed-in frontend user must meet. | |
sessionid |
session ID |
– | Resumes the session of the frontend user with the given session ID instead of using a cookie. | |
cookiename |
cookie name |
usess |
Name of the session cookie [a-z0-9]. |
[…] – the parameter accepts sub-keys, e.g. name[key]=value.
Usage
Blocks a template for visitors who are not signed in. If nobody is signed in (or filter does not
apply), errortemplate is included and the page ends. Otherwise the template continues, and
$set['user_id'] and $set['user'] are filled:
(CMS:USER_PROTECT errortemplate=u_login)
(CMS:INCLUDE header)
<h1>My account</h1>
<p>Signed in as <?php echo htmlspecialchars($set['user']['nickname']); ?></p>
Only for a group of frontend users (profile field role):
(CMS:USER_PROTECT errortemplate=u_denied filter[role]=editor)
The errortemplate is typically the sign-in form itself – so that the visitor lands back on the
protected page after signing in.
Parameters in detail
errortemplate
Template for visitors who are not signed in (or when filter does not apply):
(CMS:USER_PROTECT errortemplate=u_login)
(CMS:USER_PROTECT errortemplate=templates.u_denied filter[role]=editor)
filter
An additional condition on the profile, with the same syntax as 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
Prepares the resetting of a password.
(CMS:USER_PW [parameter=value …])
Method: Modules\Record\RecordUser::cmsPw()
PHP snippet: include_and_stop (after)
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
successtemplate |
– | – | yes | Detail template that is included if “uniquevalue” could be assigned to a profile in the field “uniquefield”. |
errortemplate |
– | – | yes | Detail template that is included if “uniquevalue” was given but could not be assigned to a profile. |
container |
container ID or name |
– | yes | ID or name of the profile container in which the profiles to be matched are stored. |
uniquefield |
field name |
– | yes | Name of the field to search by (field type chars, text or encrypted, e.g. “email”). The value must match exactly one profile. |
uniquevalue |
text |
– | Value sought in the field “uniquefield”, e.g. $_POST[email]. |
|
submit |
text value |
– | yes | The command runs only if this field contains a text. |
message[…] |
text |
– | Error message. In the index: use case; in the value: message text (see table). | |
csrf |
true / false |
true |
true: The request must contain a valid token from CMS:USER_TOKEN (protection against CSRF). false switches off the check for this command – only with good reason. | |
formguard |
true / false / seconds |
false |
true: form protection against robots – the form must contain the fields from (CMS:USER_TOKEN formguard=true) and may not be submitted earlier than 3 seconds (user.formguard_min_seconds). A number sets the minimum time for this form (formguard=5), 0 switches it off. Otherwise the message throttled. |
[…] – the parameter accepts sub-keys, e.g. name[key]=value.
Usage
First step of “Forgot password”: searches for the profile via a unique field (usually the email
address) and creates a one-time token in $set['hash']. The successtemplate sends the link. The
target page signs the frontend user in with (CMS:USER_LOGIN hash=…); afterwards, the user sets a
new password with CMS:USER_INPUT:
<?php $submit = $_POST['submit'] ?? ''; ?>
(CMS:USER_PW container=members uniquefield=email uniquevalue="{$_POST['data']['email']}"
submit="$submit" successtemplate=u_pwsent errortemplate=u_pwfail)
<h1>Forgot password</h1>
<form method="post">
(CMS:USER_TOKEN)
<input name="data[email]" type="email">
<button name="submit" value="1">Request link</button>
</form>
u_pwsent sends the link with CMS:EMAIL:
<?php $link = 'https://www.example.com/0,u_hashlogin?hash=' . rawurlencode($set['hash']); ?>
(CMS:EMAIL submit="1" to="{$set['user']['email']}" subject="New password"
body="Link (valid for one hour): $link")
(CMS:MESSAGE)
<p>We have sent you a link.</p>
Message indexes: notfound, container, throttled, token.
Throttling and form protection
Each request sends an email to an address that someone has typed in. So that nobody can flood
foreign mailboxes through your form, camindo counts the requests (throttling): at most three
per entered address and hour, ten per origin (IP address) and 200 per website (system administrator:
user.throttle). What is counted is the entered value – whether an account exists does not matter.
Once a limit is reached, no link is created, and the command includes the errortemplate, with
$set['throttled'] = 1 and the message throttled. If you show the same text there as in the success
case, the page reveals neither whether an account exists nor that throttling took place.
Against robots that fill in every form blindly, the form protection helps additionally:
formguard=true on the command and (CMS:USER_TOKEN formguard=true) in the form. Anyone who submits
the form faster than three seconds, or fills the invisible honeypot field, receives the same response as
with throttling:
(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>
The link is valid for one hour and only once (setting user.password_token_expiration). It works
on any device – someone who has forgotten their password is often sitting at a new device. If the
sign-in via the link comes from an unknown device, CMS:USER_LOGIN
reports it like any other sign-in.
Show the same text in the errortemplate as in the successtemplate (“If an account exists for
this address, we have sent a link.”). Otherwise anyone can try out which email addresses have an
account with you.
Parameters in detail
successtemplate
Included if a profile was found. In it, $set['hash'] and $set['user'] are available, and the profile
is the current record:
(CMS:USER_PW container=members uniquefield=email uniquevalue="$email" submit="$submit" successtemplate=u_pwsent errortemplate=u_pwfail)
errortemplate
Included if uniquevalue was given but no profile matches:
(CMS:USER_PW … errortemplate=u_pwfail)
container
(CMS:USER_PW container=members …)
uniquefield
Field to search by (field type chars, text or encrypted). The comparison is literal and not
case-sensitive. If the value matches more than one profile, that counts as “not found”. Therefore,
mark the field as unique in the area:
(CMS:USER_PW container=members uniquefield=email …)
(CMS:USER_PW container=members uniquefield=nickname …)
uniquevalue
Value sought, usually from the form. If empty, the command does nothing:
(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]="There is no account for this address.")
CMS:USER_TOKEN
Outputs the security token (CSRF token) for a form that triggers a writing CMS:USER_* command.
(CMS:USER_TOKEN [parameter=value …])
Method: Modules\Record\RecordUser::cmsToken()
Parameters
| Parameter | Value | Default | Required | Description |
|---|---|---|---|---|
format |
"field" / "value" |
"field" |
field: hidden input field <input type="hidden" name="_token">, with the value masked by htmlspecialchars. value: only the token (for Ajax), unmasked. |
|
set |
$set array index |
– | The output is not printed, but stored in $set. The token is additionally always in $set['user_token']. |
|
formguard |
true / false |
false |
true: additionally outputs a timestamp and an invisible honeypot field (form protection against robots). |
Usage
The token prevents a foreign website from submitting forms in the name of your visitors (cross-site
request forgery): signing in, registering, resetting a password, saving or deleting contributions,
signing out, changing the shopping basket. These commands check the token. Without a valid token, they
abort with a message (index token):
| Command | Checked |
|---|---|
USER_LOGIN, USER_REGISTER, USER_PW, USER_INPUT, USER_2FA |
when submit is filled (not for sign-in by hash) |
USER_LOGIN_CODE |
when submit or resend is set |
USER_DELETE |
always |
USER_LOGOUT |
always – with submit only if submit is filled |
USER_BASKET |
for add, update and update_addon – not for load; without a valid token, only reading takes place |
The token is held in a cookie. The first call on a page must therefore stand before any output –
at the start of the template (or in the head include before <!doctype>), with set, so that nothing
is output there. After that, write (CMS:USER_TOKEN) into every form that triggers one of these
commands:
(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">Sign in</button>
</form>
Form protection against robots
(CMS:USER_TOKEN formguard=true) additionally outputs a signed timestamp and a honeypot field that
visitors do not see. Commands with formguard=true (CMS:USER_REGISTER,
CMS:USER_PW, CMS:EMAIL) reject the form if the honeypot field is filled, or if
the form was submitted faster than three seconds or later than one hour after it was served. The system
administrator sets both times (user.formguard_min_seconds, formguard_max_seconds). For an individual
form, formguard=5 on the command sets the minimum time to five seconds – for example for a long form
that nobody completes within three seconds.
This stops robots that fill in every form blindly – no captcha, no third-party service. The system
administrator can additionally connect an external captcha (security.form_challenge); you then embed
its widget yourself.
<form method="post">
(CMS:USER_TOKEN formguard=true)
…
</form>
For Ajax calls, send the token as the POST field _token or in the header X-Camindo-Token. A
FormData object from a form with (CMS:USER_TOKEN) already contains it:
<script>
fetch('/0,basket_api', {
method: 'POST',
headers: { 'X-Camindo-Token': '(CMS:USER_TOKEN format=value)' },
body: new URLSearchParams({ product: 42 })
});
</script>
If the first call comes too late, there is no token: in place of the field there is an HTML comment
with a note, and the form fails with the message token.
The cookie (ucsrf) applies to all forms of the website and survives sign-in. It is not accepted in
the query string (GET). Writing actions belong in POST forms.
The check can be switched off per command with csrf=false. The system administrator can switch it off
globally with user.csrf = false. Both open the commands to foreign websites and are meant only for
transitional periods.
As of camindo CMS 9.1.3