The print dialog can also save the manual as PDF. Each chapter starts on a new page.

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

  1. 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
  2. Template sets – encapsulating functions, keeping them portable, passing them on and adapting them
  3. Trigger templates – PHP methods for the events of the records of an area (saving, deleting …) and for the actions of the deletion policies
  4. 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:

  1. the instance – from the domain,
  2. the language – from the language code in the address, otherwise from the browser's language setting, otherwise the primary language,
  3. the record – from the slug or the number (record_id) in the address; without one, the start record of the instance,
  4. 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] >= 100 is the same as filter[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 $cms only where a command needs a value as a parameter, for example filter[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 &amp;:

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 state is 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:

  1. Linking: Lists link every record with CMS:URL – the category just as the product.
  2. 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).
  3. 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 key id. 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

  1. Import (module “Contents” on the start page → menu ⋮ above the record list → “Import”), preferably first in a test environment.
  2. Test with the preview – in particular links that the import log reports as “not assigned”.
  3. Adjust the header and footer, or replace them with your own.
  4. If the template set is a whole website: enter the start page ID or start page template in the instance.
  5. 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 $data or $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) – use does 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. In onSave() – and in onSaved() 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?”) with dest_id="{$cms['record_id']}" or filter[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 in onSave(). In onSaved() 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:SELECT does not find. On final saving these contents are already taken over and are found.

  • Publication: CMS:SELECT finds 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?”), set state=any archive=true future=true; otherwise blocked, pre-dated and expired records are missing. CMS:SELECT never 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:SELECT finds
    User 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_id and 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 } ?>

← Overview

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 } ?>

← Overview

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:

  1. On the first call, submit is empty – the command does nothing, the form appears.
  2. After submission, it checks the required fields. If one is missing, nothing is sent; CMS:MESSAGE shows the message, and the template continues – the form appears again.
  3. Otherwise it builds the email, sends it and includes successtemplate. The page ends afterwards. Without successtemplate, 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 to is an input of the visitor, anyone can send emails to arbitrary addresses through your form. If one of the addresses in to is invalid, the command does not send at all.
  • Throttle contact forms: throttle=true limits 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; without from, 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/ and var/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 &amp; 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>

← Overview

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 &amp;. 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 } ?>

← Overview

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 } ?>

← Overview

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).


← Overview

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 } ?>

← Overview

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')}")

← Overview

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 (&amp;, &auml;, &#8211;) 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 (< → &lt;, " → &quot; …) – 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
&shy;, &nbsp; … 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 &amp;; 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 } ?>

← Overview

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 } ?>

← Overview

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 } ?>

← Overview

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)

← Overview

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.


← Overview

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, archive and state relax 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 } ?>

← Overview

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, request takes over neither values nor filter_op/filter_mode – ?filter[area]=internal or ?filterop[area]=!= change nothing about filter[area]=articles. Fields of a linked record count as the reference field (author.name belongs to author); area and area_id count as the same field.
  • skip, limit, sort and order from the command replace the URL value completely. limit from 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.

← Overview

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)

← Overview

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.


← Overview

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.


← Overview

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 (&amp; 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=" · ")

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 } ?>

← Overview

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 (&amp; 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}")

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)

← Overview

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 (&amp; 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}")
(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)

← Overview

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.


← Overview

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)

← Overview

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.


← Overview

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 in cacheparams.
  • 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.


← Overview

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)

← Overview

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.


← Overview

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">[&raquo;]</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">[&raquo;]</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 } ?>

← Overview

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 } ?>

← Overview

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']; ?>

← Overview

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.")

← Overview

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.


← Overview

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']); ?>

← Overview

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.")

← Overview

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")

← Overview

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)

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.


← Overview

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_mail sends the code ($set['login_code']) with CMS:EMAIL and then includes the form. It is the codetemplate of CMS:USER_LOGIN and of CMS:USER_LOGIN_CODE (for “Send new code”).
  • u_code contains CMS:USER_LOGIN_CODE and 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.


← Overview

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)

← Overview

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)

← Overview

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)

← Overview

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.")

← Overview

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)

← Overview

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.")

← Overview

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.


← Overview

As of camindo CMS 9.1.3