Skip to the content
Documentation
Help Start
Reading in:
  • German: 9,059 of 9,059 pages
About the help in other languages
All help pages

Backend services over ezjscore (expservices)

Read this page if you build a remote admin app (GTK, Qt, KDE/GNOME, iOS, Android, shell) or a JavaScript frontend on top of Exponential. extension/expservices turns the ezjscore server functions into a library of remote…

Download as PDF

Address: https://doc.exponential.earth/exphelp/viewer/bc/6.0/backend_ezjscore_services.md

On this page

Backend services over ezjscore (expservices)

Read this page if you build a remote admin app (GTK, Qt, KDE/GNOME, iOS, Android, shell) or a JavaScript frontend on top of Exponential. extension/expservices turns the ezjscore server functions into a library of remote services that offer the features of Exponential, with discovery, one answer envelope, policies per service and personal API tokens. Version 0.1.0 (first draft, 2026-10-02).

In short

What changedNew extension expservices: one class per domain (exp<Domain>Services), registered as [ezjscServer_exp<domain>], called as <root>/ezjscore/call/exp<domain>::<function>. Discovery through expservices::catalog. Personal API tokens (table expservices_token). Clients in shell, Python and snippets for other platforms.
Who is affectedNobody until the extension is activated. To use API tokens on an existing installation, the token table must be created once. Behind Apache with mod_proxy_fcgi, the Authorization header may not reach PHP; use X-Exp-Token.
How to checkcurl -s 'https://www.example.com/ezjscore/call/expsystem::version?ContentType=json' and php vendor/bin/phpunit tests/tests/extension/expservices/
How to fixActivate the extension, regenerate autoloads (php bin/php/ezpgenerateautoloads.php -e), create the token table from extension/expservices/sql/<engine>/schema.sql.

Host names in the examples are placeholders (https://www.example.com); use your own. The shipped shell client takes the address from -u or EXPSERVICES_URL; always set one of them.

Overview

What an ezjscore server function is

ezjscore (extension/ezjscore) lets a page call PHP over HTTP. A server function is a public static method of a class that is registered in ezjscore.ini as a block [ezjscServer_<name>] (Class=<class>). It is called as

<root>/ezjscore/call/<name>::<function>[::<arg>[::<arg>...]]

(the admin siteaccess has the same URL below /admin/). The router (ezjscServerRouter) refuses any class that has no block in ezjscore.ini, calls <class>::<function>( array $args ) and answers in the format of the Accept header or ?ContentType=json|xml|text. ezjscore/call itself does not check a policy: every function checks for itself. Several calls can be sent in one request: POST ezjscServer_function_arguments with the calls joined by @SEPARATOR$.

The ezjscore answer wraps the return value: {"error_text": "", "content": <what the function returned>}. expservices functions return the envelope below as content.

The classes: one domain, one class, one block

Every domain is a class exp<Domain>Services extending expServiceBase, registered in extension/expservices/settings/ezjscore.ini.append.php as [ezjscServer_exp<domain>], so a service of the domain system is called ezjscore/call/expsystem::version. Each public static method is a service and is declared in the class's public static $services:

'version' => array( 'summary' => '...', 'access' => 'public' | 'user' | array( 'setup', 'system_info' ),
                    'write' => false, 'args' => array( 'node_id' => 'int' ), 'returns' => '...' )

ezjscServerRouter::call() hands every class that extends expServiceBase to expServiceBase::invoke(), which checks that the service is declared, calls it, and turns every exception into the error envelope.

Declare the list as the property's value in the class body, never by an assignment after the class (expFooServices::$services = ...;). A persistent Velocity worker puts the statics of a class it first loaded during a request back to their declared value before its next request, and include_once does not run such an assignment again: from the second request on, every service of the class answered "not declared" (404) on that worker, and the extension point survey listed them as missing. The ten users and access domains did this until 2026-10-12 (207 services; expUsersDeclarationsTest::testTheServicesSurviveAResetToTheDeclaredValue guards it now).

Discovery

CallAnswers
expservices::catalog[::<domain>]every service: domain, method, summary, access, write, ai, args, returns, call
expservices::schema::<domain>the services of one domain
expservices::service::<domain>::<method>one descriptor
expservices::domainsthe domains with their class and service count
expservices::versionthe expservices, envelope and Exponential versions

The domain is the part after exp of the block name (session, system, ini, cache, ...). The calls to the discovery functions are public; every other service checks its own access.

The envelope

{ "ok": true,  "data": <the result>, "meta": { ... } }
{ "ok": false, "error": { "code": 403, "message": "No access to setup/managecache" } }

Lists are paged: data is the items and meta is total, offset, limit, count, has_more.

Authentication

Sign in once with expsession::login (POST username, password); the response sets the session cookie, which a non-browser client keeps in a cookie jar and sends with every call. expsession::whoami says who the session is, expsession::token returns the form token needed for writes, expsession::ping keeps the session alive, expsession::logout ends it. Clients that cannot keep a cookie use a personal API token instead, see below.

curl -c jar -d 'username=admin' --data-urlencode 'password=...' https://site/ezjscore/call/expsession::login
curl -b jar https://site/ezjscore/call/expsession::whoami

Authentication with an API token

For apps, scripts and servers a session cookie is the wrong tool: it expires, it needs the sign-in flow and a form token for every write. A personal API token is the alternative. A signed-in user (session login, not a token) creates one with expsession::tokenCreate (POST name, expires_in_days 1 to 365, default 90); the answer carries the token once (data.token, expt_ and 48 hex characters). Only its SHA-256 hash is stored (table expservices_token), so a lost token cannot be read back, only revoked (expsession::tokenRevoke, POST id) and replaced. expsession::tokenList lists the caller's own tokens: id, name, a hint (the first characters), created, last used, expiry, revoked, valid, never the token.

curl -b jar -H 'X-CSRF-Token: <form token>' --data-urlencode 'name=backup script' https://site/ezjscore/call/expsession::tokenCreate?ContentType=json
curl -H 'Authorization: Bearer expt_...' https://site/ezjscore/call/expsession::whoami?ContentType=json
curl -H 'X-Exp-Token: expt_...' --data-urlencode 'id=7' https://site/ezjscore/call/expsession::tokenRevoke?ContentType=json
  • Sending it. Authorization: Bearer <token>, or the header X-Exp-Token: <token>. Use X-Exp-Token where a proxy or FastCGI setup does not hand the Authorization header to PHP (Apache with mod_proxy_fcgi needs CGIPassAuth On or SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1; on the reference installation Authorization does not arrive and X-Exp-Token is the header that works). The shipped clients send both.
  • What it does. Before any guard looks at the user, the base signs the request in as the token's user, for this one request only: the session is not touched, so no cookie is created and nothing stays signed in afterwards. Every other guard is unchanged: the same policies and limitations as for that user, POST for writes, the audit event of every write.
  • Writes need no form token with a bearer token (but still POST). The form token protects a browser, which attaches its cookie to a request a hostile page makes on its own; a request that carries the token in a header it must set itself cannot be forged that way, so there is nothing for the form token to prove. A request without the token header falls back to the cookie and the form token as before.
  • Refusals. An unknown, revoked or expired token, or a token whose user is disabled, answers 401 on every service, public ones too (a client must notice that its token died, not silently become anonymous). A token cannot create tokens (403): that needs the session login. Tokens of other users are invisible (404).
  • Audit. access.expservices.token.create, access.expservices.token.revoke and access.expservices.token.failed (a refused token, with the reason) in the access channel; the taxonomy branch expServicesAuditBranch is registered in extension/expservices/settings/audit.ini.append.php. Uses are not logged one by one; the token's last_used is updated at most once a minute.
  • Install. The table is in extension/expservices/sql/{mysql,postgresql,sqlite}/schema.sql (the extension has no share/db_schema.dba); create it on an existing installation by running the schema of your database engine once, for example mysql -u YOUR_USER -p YOUR_DATABASE < extension/expservices/sql/mysql/schema.sql (it is a plain CREATE TABLE: skip it when the table exists).
  • Only a token's hint is ever shown again; keep the token in the platform's secret store (Keychain, Keystore, KWallet, libsecret), never in source code or a URL.

Access

'access' of a service is public (anybody), user (any signed-in user) or array( module, function ), a policy of the user (eZUser::hasAccessTo; a limited policy passes the check, and the service then checks the object, e.g. canRead() of a node). A missing login answers 401, a missing policy 403.

A classic server function (an [ezjscServer_<name>] block of ezjscore.ini) is guarded by its Functions[], checked against the ezjscore/call policy with the FunctionList limitation. RequirePolicy sets how: not set, Functions[] is checked and a refused call is answered with its error text in a 200 answer, as before; disabled, it is not checked; enabled, it is checked and a request whose calls are all refused is answered with 403. ezjscnode ships with Functions[]=ezjscnode and RequirePolicy=disabled; new installations write enabled (see Roles and policies, section 8). ezjscServerRouter::getInstance() reports why it returned null in an optional fourth argument (invalid, access, policy-required). The idea of guarding ezjscnode by a policy comes from Mugo Web's LoveStack distribution (commit aa307a7ff3), with thanks.

Paging

Services that list take limit and offset arguments (expsystem::phpextensions::10::20 is 10 items from 20). The default and the maximum are in expservices.ini [Paging] (25 and 200).

Errors

CodeMeaning
400bad or missing arguments
401login needed
403no policy, no POST, or a wrong form token
404no such service, node, object ...
409conflict
422the data is not valid
500the service failed (logged to the debug output, never a PHP fatal)

Writes

A service with 'write' => true needs a POST, the form token (field ezxform_token or header X-CSRF-Token; get it from expsession::token), and its policy; arguments come in the POST body (expServiceBase::post()), not in the URL. Every successful write records the audit event service.<domain>.<method> (expAudit), besides the events the kernel operation writes itself. expservices.ini [Writes] can switch the token check (tests only) and the audit off. A request signed in by an API token needs POST but no form token (see "Authentication with an API token").

Writing a service

class expFooServices extends expServiceBase
{
    public static $services = array( 'bar' => array( 'summary' => '...', 'access' => array( 'content', 'read' ),
        'write' => false, 'args' => array( 'node_id' => 'int' ), 'returns' => '...' ) );

    public static function bar( $args )
    {
        self::guard( __FUNCTION__ );
        $node = self::node( self::arg( $args, 0, 'int' ) );
        return self::ok( array( 'name' => $node->attribute( 'name' ) ) );
    }
}

Helpers: arg(), post(), paging()/pageOf(), ok(), page(), guard(), audit(), node(), can(). Register the class in ezjscore.ini.append.php, run php bin/php/ezpgenerateautoloads.php -e.

One declaration, three channels

The same declaration serves HTTP (ezjscore/call/exp<domain>::<method>), MCP (the tools exp_svc_<domain>_<method> an MCP server generates from the catalogue) and, when the service opts in, the assistant of the extension expai. A read service opts in with 'ai' => true, or with 'ai' => array( 'summary' => '...' ) to give the model its own description; services are private to the assistant by default, and a service with 'write' => true is never offered. The catalogue reports the flag as ai. The assistant calls the service in process as the logged-in editor, with the service's own access check, the policy ezjscore/call and expai's policy expai/services (limitation Domain); expai's data policy filters the answer. Opted in here: expnode::childrenNames, childrenCount, breadcrumb, tree, latest, find; expsection::list, ofNode; expstate::groups, ofObject; exptags::roots, children, tree, search, ofnode. A site can opt in more without code changes in expai.ini [ServiceTools] (see the documentation of expai, doc/service-tools.md).

Testing a call

extension/expservices/bin/expservices-client.sh -u https://www.example.com call expsystem::version   # from a shell
php vendor/bin/phpunit tests/tests/extension/expservices/

Domains

Catalogue (expservices, 5 services)

Discovery of the API; every call is public.

CallAccessWriteSummary
expservices::catalog::<domain>publicEvery service of every domain: domain, method, summary, access, write, args, returns
expservices::schema::<domain>publicThe services of one domain
expservices::service::<domain>::<method>publicOne service descriptor
expservices::domainspublicThe domains with their class and service count
expservices::versionpublicThe version of expservices, the envelope version and the Exponential version

Session and authentication (expsession, 11 services)

Remote clients sign in once and keep the session cookie, or use a personal API token; see the Authentication sections.

CallAccessWriteSummary
expsession::login::<username>::<password>publicSigns in with POST username and password; the session cookie of the response is the session
expsession::logoutuserPOSTSigns out (POST with the form token)
expsession::whoamipublicThe current user: id, login, name, email, registered, groups
expsession::tokenpublicThe form token of this session for writes (field ezxform_token or header X-CSRF-Token)
expsession::pingpublicKeeps the session alive and tells whether it is signed in
expsession::access::<module>::<function>userWhether the user has module/function (accessWord yes, no or limited)
expsession::rolesuserThe roles of the user with the policies of each (module, function)
expsession::groupsuserThe user groups of the user
expsession::tokenCreateuserPOSTCreates a personal API token for the signed-in user (session login, not by another token); name, expires_in_days. The token is in the answer once, only its hash is stored
expsession::tokenListuserThe own API tokens: id, name, hint, created, last used, expiry, revoked, valid (never the token)
expsession::tokenRevokeuserPOSTRevokes an own API token (id)

System information (expsystem, 16 services)

What the installation is and runs on; the secrets of the installation are never part of an answer.

CallAccessWriteSummary
expsystem::versionpublicThe Exponential version, release, state and the PHP version
expsystem::infosetup/system_infoSite name, URLs, siteaccess, database type, locale
expsystem::settingspublicThe public site settings: site name, default access, locale, design
expsystem::timepublicThe server time, time zone and the time of the request
expsystem::phpsetup/system_infoPHP version, SAPI, limits and OPcache state
expsystem::phpextensions::<limit>::<offset>setup/system_infoThe loaded PHP extensions with their versions
expsystem::databasesetup/system_infoThe database type, name, server version and connection state (no credentials)
expsystem::siteaccessessetup/system_infoThe available siteaccesses
expsystem::siteaccesspublicThe siteaccess of this request
expsystem::languagesuserThe content languages of the installation
expsystem::locales::<limit>::<offset>userThe locales the installation knows
expsystem::healthsetup/system_infoQuick checks: database connection, var and cache directories writable
expsystem::directoriessetup/system_infoThe var, storage and cache directories (relative to the installation) and whether they are writable
expsystem::statisticssetup/system_infoCounts: content objects, nodes, classes, users, sessions
expsystem::loadsetup/system_infoThe server load average and the PHP memory use of this request
expsystem::urlspublicHost, root URL, index file and server URL of this request

Settings (read-only) (expini, 10 services)

Reads the INI settings as the siteaccess of the request sees them. Values of secrets (password, token, key ...) are masked exactly as exp:ini masks them.

CallAccessWriteSummary
expini::files::<limit>::<offset>setup/setupThe INI files of the installation (kernel, override and extension settings)
expini::groups::<file>::<limit>::<offset>setup/setupThe groups (blocks) of an INI file as the current siteaccess reads it
expini::variables::<file>::<group>setup/setupThe variables of one group with their values (secrets masked)
expini::get::<file>::<group>::<variable>setup/setupOne setting value as the current siteaccess reads it (secrets masked)
expini::has::<file>::<group>::<variable>setup/setupWhether a group or a variable exists
expini::search::<file>::<text>::<limit>::<offset>setup/setupFinds variables whose name contains a text, in one INI file (secrets masked)
expini::siteaccessessetup/setupThe siteaccesses that can have their own settings
expini::scopessetup/setupThe settings scopes exp:ini knows (global, siteaccesses, extensions)
expini::secret::<variable>setup/setupWhether a variable name is treated as a secret (masked)
expini::activeextensionssetup/setupThe active extensions in the order the settings read them

Caches (expcache, 14 services)

The clear services are writes: POST, the form token, setup/managecache, an audit event. Pass dry_run=1 to see what would be cleared.

CallAccessWriteSummary
expcache::list::<limit>::<offset>setup/managecacheEvery cache with its id, name, tags and how it is cleared
expcache::tagssetup/managecacheThe cache tags with the cache ids each clears
expcache::describe::<id>setup/managecacheOne cache by id, with its path and the size of its files
expcache::phpsetup/managecacheThe PHP level caches: OPcache and APCu state
expcache::velocitysetup/managecacheVelocity response cache: where it is, how many files, when cleared
expcache::httpsetup/managecacheThe HTTP cache: whether it is enabled and its status
expcache::staticcachesetup/managecacheThe static cache: whether it is enabled and its status
expcache::querysetup/managecacheThe database query cache state
expcache::clear::<by>::<names>::<dry_run>setup/managecachePOSTClears caches: POST by=all/tag/id, names=comma list, dry_run=1 to only report
expcache::cleartag::<tag>::<dry_run>setup/managecachePOSTClears the caches of one tag (POST tag, dry_run)
expcache::clearid::<id>::<dry_run>setup/managecachePOSTClears one cache by id (POST id, dry_run)
expcache::clearnode::<node_id>setup/managecachePOSTClears the view cache of a node (POST node_id)
expcache::clearvelocity::<dry_run>setup/managecachePOSTClears the Velocity response cache (POST dry_run)
expcache::clearopcache::<dry_run>setup/managecachePOSTResets OPcache of this PHP process pool (POST dry_run)

Cronjobs (expcronjob, 7 services)

CallAccessWriteSummary
expcronjob::partssetup/managecronjobsThe cronjob parts (groups run by runcronjobs.php <part>) with their script count
expcronjob::scripts::<part>setup/managecronjobsThe scripts of one cronjob part
expcronjob::all::<limit>::<offset>setup/managecronjobsEvery part with its scripts
expcronjob::settingssetup/managecronjobsThe cronjob settings: script directories, execution time limit, admin console settings
expcronjob::statussetup/managecronjobsThe cronjob log files: whether they exist, their size and last change
expcronjob::deferredsetup/managecronjobsThe workflow processes deferred to cron: how many wait
expcronjob::runnables::<limit>::<offset>setup/managecronjobsThe cronjobs implemented as runnables (kernel and extension)

Extensions (expextension, 9 services)

CallAccessWriteSummary
expextension::list::<limit>::<offset>setup/system_infoThe active extensions with version, license and website where known
expextension::available::<limit>::<offset>setup/system_infoThe extensions found in the extension directories (active or not)
expextension::info::<name>setup/system_infoThe ezinfo of one extension: name, version, author, license, website
expextension::isactive::<name>setup/system_infoWhether an extension is active
expextension::accessextensionssetup/system_infoThe extensions active for the current siteaccess (ActiveAccessExtensions)
expextension::designextensionssetup/system_infoThe extensions that supply designs (DesignExtensions)
expextension::settingsfiles::<name>setup/system_infoThe settings files an extension ships
expextension::directoriessetup/system_infoThe extension root directories
expextension::countsetup/system_infoThe number of active and available extensions

Packages (exppackage, 7 services)

CallAccessWriteSummary
exppackage::list::<limit>::<offset>::<type>::<repository>package/listThe packages: name, version, vendor, type, summary, installed
exppackage::view::<name>::<repository>package/readOne package: description, vendor, maintainers, dependencies, state
exppackage::files::<name>::<limit>::<offset>package/readThe file count and the simple file list of a package
exppackage::typespackage/listThe package types
exppackage::statespackage/listThe package states
exppackage::repositoriespackage/listThe package repositories
exppackage::countpackage/listThe number of packages and of installed packages

Workflows and triggers (expworkflow, 10 services)

CallAccessWriteSummary
expworkflow::list::<limit>::<offset>setup/administrateThe workflows (published versions)
expworkflow::view::<id>setup/administrateOne workflow with its events
expworkflow::events::<id>setup/administrateThe events of a workflow in order
expworkflow::eventtypessetup/administrateThe registered workflow event types
expworkflow::groupssetup/administrateThe workflow groups
expworkflow::triggers::<limit>::<offset>::<module>setup/administrateThe triggers: module, function, connect type and the workflow
expworkflow::trigger::<id>setup/administrateOne trigger
expworkflow::processes::<limit>::<offset>setup/administrateThe running workflow processes
expworkflow::process::<id>setup/administrateOne workflow process
expworkflow::statusessetup/administrateThe workflow status codes and names

Velocity (expvelocity, 5 services)

Paths of the server stay on the server and are not part of the answers.

CallAccessWriteSummary
expvelocity::status::<engine>setup/system_infoWhether Velocity runs: processes, listening ports, engine
expvelocity::enginessetup/system_infoThe engines Velocity knows and the configured default
expvelocity::urlssetup/system_infoThe URLs the server answers on
expvelocity::cachesetup/system_infoThe Velocity response cache: files, bytes, last cleared
expvelocity::installedsetup/system_infoWhether Velocity is part of this installation

Extension point survey (rad) (exprad, 7 services)

CallAccessWriteSummary
exprad::summarysetup/system_infoThe survey counts: ini files, settings, modules, operators, runnables ...
exprad::counts::<name>setup/system_infoOne count of the survey by name
exprad::groupssetup/system_infoThe extension point groups with their counts
exprad::runnables::<limit>::<offset>::<kind>setup/system_infoThe runnables (commands, cronjobs, views) with kind and owner
exprad::files::<limit>::<offset>setup/system_infoThe ini files the survey reads
exprad::inicommandsetup/system_infoThe exp:ini actions and scope providers
exprad::contentjobtypessetup/system_infoThe content job types

Debug summary (expdebug, 6 services)

CallAccessWriteSummary
expdebug::summarysetup/setupTime, SQL, memory, templates and warnings of this request with their levels (the debug bar summary)
expdebug::enabledsetup/setupWhether debug output is enabled for this request
expdebug::levelsetup/setupThe debug level settings of the site
expdebug::thresholdssetup/setupThe warn and high thresholds of the debug bar
expdebug::engineuserWhich engine serves this request (Apache, Velocity, FrankenPHP, CLI)
expdebug::phpversionuserThe PHP version and SAPI of this request

Media and other services: 218 services in 14 domains

Written by the media/other part of expservices: extension/expservices/classes/media/ (images, files, media, tags) and classes/misc/ (layouts, audit, subitems, newsletters, sitemaps, statistics, designs, languages, links, PDF). Domains of extensions answer 404 ... is not available when the extension is inactive. Tests: tests/tests/extension/expservices/media/ and misc/ (live database, test content under the Media root node 43 removed in tearDown, never a test database); the services were also checked over HTTP.

Images and aliases (expimage, 18 services)

Image aliases (image.ini), original image information and the URL of any alias of a node image. Reads follow content/read on the node; purge needs content/edit.

CallAccessWriteSummary
expimage::aliasespublicThe configured image aliases (image.ini AliasList) with size and filters
expimage::alias::<name>publicOne configured image alias
expimage::aliasnamespublicOnly the names of the aliases
expimage::formatspublicThe image MIME types the converters support
expimage::filterspublicThe image filters (image.ini [ImageMagick]/[GD]) known to the manager
expimage::qualitypublicThe configured output quality per MIME type
expimage::attributes::<node>content/readThe image attributes of a node with their original file
expimage::info::<node>::<attribute>content/readThe original image of a node attribute: file name, size, dimensions, MIME type, alt text
expimage::url::<node>::<attribute>::<alias>content/readThe URL of one alias of a node image (generated when missing)
expimage::urls::<node>::<attribute>content/readThe URLs of every configured alias of a node image
expimage::generate::<node>::<attribute>::<alias>content/readMakes sure an alias file exists for a node image (derived data, no content change)
expimage::alt::<node>::<attribute>content/readThe alternative text of a node image
expimage::files::<node>::<limit>::<offset>content/readThe image files registered for the object of a node (ezimagefile)
expimage::list::<parent>::<limit>::<offset>::<alias>content/readThe image objects below a parent node with their original and one alias
expimage::search::<text>::<parent>::<limit>::<offset>content/readImage objects by name below a parent
expimage::statscontent/readHow many image files and images the installation holds
expimage::bymimecontent/readImage file counts per MIME type of the original files
expimage::purge::<node>::<attribute>content/editPOSTRemoves the alias files of a node image (they are generated again on demand)

Files and downloads (expfile, 16 services)

Binary files: information, the content/download URL (the download itself checks content/read), time limited signed links (HMAC over node, attribute and expiry with the site secret; verify is public) and download statistics.

CallAccessWriteSummary
expfile::attributes::<node>content/readThe file attributes of a node
expfile::info::<node>::<attribute>content/readOne file of a node: names, MIME type, size, download count
expfile::downloadurl::<node>::<attribute>content/readThe content/download URL of a node file (the download itself checks content/read)
expfile::signedurl::<node>::<attribute>::<seconds>content/readA time limited signed link to a node file, for clients without a session
expfile::verify::<node>::<attribute_id>::<expires>::<signature>publicWhether a signed link is genuine and not expired
expfile::exists::<node>::<attribute>content/readWhether the stored file of a node attribute exists on disk
expfile::downloads::<node>::<attribute>content/readThe download count of a node file
expfile::list::<parent>::<limit>::<offset>content/readThe file objects below a parent node with their file information
expfile::search::<text>::<parent>::<limit>::<offset>content/readFile objects by name below a parent
expfile::byname::<filename>::<limit>::<offset>content/readThe stored files with an original or stored file name (exact)
expfile::top::<limit>content/readThe most downloaded files
expfile::statscontent/readNumber of files, total downloads, and counts per MIME group
expfile::bymimecontent/readFile counts per MIME type
expfile::mimegroup::<mime>publicThe MIME group (application, image, ...) of a MIME type
expfile::safename::<filename>userWhether a file name is acceptable for upload and its safe form
expfile::resetdownloads::<node>::<attribute>content/editPOSTSets the download count of a node file back to zero

Audio and video (expmedia, 10 services)

The ezmedia datatype: player settings, file information, URL, listings and statistics.

CallAccessWriteSummary
expmedia::attributes::<node>content/readThe media attributes of a node
expmedia::info::<node>::<attribute>content/readOne media file of a node: file, MIME type, player dimensions and flags
expmedia::player::<node>::<attribute>content/readOnly the player settings (width, height, controls, autoplay, loop, quality)
expmedia::url::<node>::<attribute>content/readThe download/stream URL of a node media file
expmedia::mimegroup::<mime>publicThe MIME group (audio, video, ...) of a MIME type
expmedia::safename::<filename>userWhether a media file name is acceptable and its safe form
expmedia::list::<parent>::<limit>::<offset>content/readThe media objects below a parent node
expmedia::byobject::<object>content/readThe media rows stored for an object (all versions)
expmedia::statscontent/readNumber of media files and per MIME group
expmedia::bymimecontent/readMedia file counts per MIME type

Tags (exptags, 27 services)

The eztags tree with the eztags policies (tags/read, add, edit, delete, addsynonym). Answers 404 "not available" when eztags is inactive. attach and detach change the tags attribute of a node (content/edit on the node).

CallAccessWriteSummary
exptags::availablepublicWhether the eztags extension is active, and how many tags there are
exptags::roots::<limit>::<offset>tags/readThe top level tags
exptags::get::<id>tags/readOne tag with counts
exptags::children::<id>::<limit>::<offset>tags/readThe children of a tag
exptags::childrencount::<id>tags/readHow many children a tag has
exptags::tree::<id>::<depth>tags/readA tag subtree as nested nodes down to a depth (at most 5)
exptags::path::<id>tags/readThe path of a tag from the root
exptags::parent::<id>tags/readThe parent of a tag
exptags::search::<text>::<limit>::<offset>tags/readTags whose keyword contains the text
exptags::suggest::<text>::<limit>tags/readAutocomplete: tags whose keyword starts with the text
exptags::bykeyword::<keyword>tags/readTags with exactly this keyword
exptags::byremote::<remote>tags/readThe tag with a remote id
exptags::bypath::<path>tags/readThe tag at a path string (/1/5/7/)
exptags::synonyms::<id>tags/readThe synonyms of a tag
exptags::translations::<id>tags/readThe translations (keyword per locale) of a tag
exptags::related::<id>::<limit>::<offset>tags/readThe objects tagged with a tag that the user may read
exptags::relatedcount::<id>tags/readHow many published objects carry a tag
exptags::ofnode::<node>content/readThe tags attached to a node
exptags::popular::<limit>tags/readThe most used tags
exptags::recent::<limit>tags/readThe most recently modified tags
exptags::statstags/readCounts of tags, synonyms, translations and attachments
exptags::add::<parent>tags/addPOSTCreates a tag below a parent (0: top level)
exptags::rename::<id>tags/editPOSTChanges the keyword of a tag in a locale
exptags::addsynonym::<id>tags/addsynonymPOSTAdds a synonym to a tag
exptags::delete::<id>tags/deletePOSTDeletes a tag with its children and synonyms
exptags::attach::<node>::<attribute>content/editPOSTAttaches an existing tag to a node tags attribute
exptags::detach::<node>::<attribute>content/editPOSTRemoves a tag from a node tags attribute

Layouts (explayout, 27 services)

explayouts: layouts, zones (a linked zone answers the blocks of its shared layout), blocks, rules, and the layout a node or path resolves to (computed without touching the resolver cache). Policies explayouts/read and explayouts/edit; the writes use the layout service of explayouts_core.

CallAccessWriteSummary
explayout::availablepublicWhether the explayouts extension is active, and the number of published layouts
explayout::layouts::<limit>::<offset>::<status>explayouts/readThe published layouts
explayout::sharedexplayouts/readThe shared layouts (header, footer, ...)
explayout::draftsexplayouts/readThe draft layouts
explayout::view::<id>explayouts/readOne layout with its zones and block counts
explayout::byidentifier::<identifier>::<status>explayouts/readA layout by identifier (published unless status is 1)
explayout::typesexplayouts/readThe layout types (explayouts.ini LayoutType_*) with their zones
explayout::blocktypes::<category>explayouts/readThe block definitions (explayouts.ini BlockDefinition_*)
explayout::zones::<id>explayouts/readThe zones of a layout, with their link to a shared layout
explayout::zone::<id>::<zone>explayouts/readOne zone of a layout
explayout::blocks::<id>::<zone>explayouts/readThe blocks of a zone (a linked zone answers the blocks of the shared layout)
explayout::block::<id>explayouts/readOne block with its parameters
explayout::blockchildren::<id>explayouts/readThe child blocks of a container block
explayout::blockparameters::<id>explayouts/readThe parameters of a block as name => value
explayout::linkedzonesexplayouts/readEvery zone that inherits its blocks from a shared layout
explayout::rules::<limit>::<offset>explayouts/readThe layout rules in priority order
explayout::rule::<id>explayouts/readOne rule with its targets and conditions
explayout::rulesfor::<id>explayouts/readThe rules that show a layout
explayout::resolve::<node>explayouts/readThe layout a content node gets (first matching enabled rule, else the default); changes nothing
explayout::resolvepath::<path>explayouts/readThe layout a URL path gets
explayout::statsexplayouts/readCounts of layouts, zones, blocks, rules and block types in use
explayout::createdraft::<id>explayouts/editPOSTCreates (or returns) the draft of a published layout
explayout::publish::<id>explayouts/editPOSTPublishes the draft of a layout
explayout::discard::<id>explayouts/editPOSTDiscards the draft of a layout
explayout::enablerule::<id>explayouts/editPOSTEnables a rule
explayout::disablerule::<id>explayouts/editPOSTDisables a rule
explayout::clearcacheexplayouts/editPOSTClears the layout resolver cache

Audit (read only) (expaudit, 19 services)

Policy audit/read, narrowed to the channels of its Channel limitation; every use is recorded as system.audit.read. Figures need the audit index (409 when it is not usable). Nothing here verifies, exports or changes the audit.

CallAccessWriteSummary
expaudit::availablepublicWhether the audit classes are loaded, whether audit is on and the index usable
expaudit::channelsaudit/readThe channels the user may read, with record counts of today
expaudit::recent::<channel>::<limit>::<offset>audit/readThe latest events, newest first, optionally of one channel
expaudit::search::<q>::<name>::<login>::<result>::<from>::<to>::<limit>::<offset>audit/readEvents by text, name pattern (access.*), login, result and time range
expaudit::event::<id>audit/readOne event in full by id
expaudit::related::<id>::<limit>audit/readThe events of the same request as an event
expaudit::byrequest::<request>::<limit>::<offset>audit/readThe events of one request id
expaudit::bylogin::<login>::<limit>::<offset>audit/readThe events of one login
expaudit::byobject::<object>::<limit>::<offset>audit/readThe events about one object (type or type:id, node:275)
expaudit::refused::<limit>::<offset>audit/readThe latest refused events
expaudit::failed::<limit>::<offset>audit/readThe latest failed events
expaudit::namesaudit/readThe event names in the index (the name filter suggestions)
expaudit::volumeaudit/readEvents of today and of the last 7 days per channel
expaudit::perday::<days>audit/readEvents per local day for the last N days (at most 30)
expaudit::resultsaudit/readCounts of success, refused and failed for today and the week
expaudit::topactors::<limit>audit/readThe most active logins of today
expaudit::failedloginsaudit/readFailed logins of the last 24 hours: total and by address
expaudit::chainsaudit/readThe hash chain state of each channel as last verified (nothing is verified here)
expaudit::summaryaudit/readThe dashboard figures in one answer: volume, results, security, chains

Subitems columns (expsubitems_svc, 12 services)

The column catalogue and rows of the admin subitems list in the expservices envelope. The block is named expsubitems_svc because expsubitems is the original ezjscore server function of the admin list, which stays as it is. rows takes the columns as an argument (comma list of non built-in column keys).

CallAccessWriteSummary
expsubitems_svc::cataloguecontent/readEvery column known to the registry (built-in and INI defined), without the per-parent attribute columns
expsubitems_svc::builtincontent/readThe built-in columns
expsubitems_svc::describe::<key>::<parent>content/readOne column by key
expsubitems_svc::columns::<parent>content/readThe columns the user may see under a parent, defaults, presets and the saved choice
expsubitems_svc::defaults::<parent>content/readThe default column keys under a parent
expsubitems_svc::presetscontent/readThe column presets of the INI
expsubitems_svc::pagesizescontent/readThe page sizes the list offers
expsubitems_svc::sortfields::<parent>content/readThe sort keys a list accepts
expsubitems_svc::groups::<parent>content/readThe columns grouped by their Group under a parent
expsubitems_svc::preference::<parent>content/readThe user's own saved column choice for the parent's navigation part
expsubitems_svc::rows::<parent>::<columns>::<limit>::<offset>::<sort>::<ascending>::<filter>content/readThe children of a parent with the columns asked for (comma list)
expsubitems_svc::settingscontent/readThe registry settings: attribute columns, CSV export, CSV limit

Newsletters (expnewsletter, 13 services)

cjw_newsletter: lists, editions, subscriber counts and a user's own subscriptions. subscribe and unsubscribe act on the signed-in user's own account e-mail only; a new subscription starts pending and follows the extension's confirmation rules. 404 "not available" when the extension is inactive.

CallAccessWriteSummary
expnewsletter::availablepublicWhether cjw_newsletter is active, and how many lists and newsletter users exist
expnewsletter::statusespublicThe subscription status codes and names
expnewsletter::lists::<limit>::<offset>content/readThe newsletter lists the user may read
expnewsletter::list::<node>content/readOne newsletter list by node id
expnewsletter::subscriberscount::<node>content/readThe number of approved subscribers of a list
expnewsletter::statistics::<node>newsletter/subscription_listSubscriptions of a list by status
expnewsletter::editions::<node>::<limit>::<offset>content/readThe editions (issues) below a list node
expnewsletter::edition::<node>content/readOne edition by node id
expnewsletter::mysubscriptionsuserThe logged-in user's own subscriptions (by their account e-mail)
expnewsletter::subscribe::<node>userPOSTSubscribes the logged-in user's own e-mail to a list
expnewsletter::unsubscribe::<node>userPOSTRemoves the logged-in user's own subscription to a list
expnewsletter::users::<limit>::<offset>::<search>newsletter/user_listThe newsletter users (administration)
expnewsletter::usercountnewsletter/user_listThe number of newsletter users

Sitemaps (expsitemap, 11 services)

Entries of the content tree as a sitemap lists them (loc, lastmod, changefreq, priority; settings of bcgooglesitemaps.ini), the sitemap index and the generated files.

CallAccessWriteSummary
expsitemap::configcontent/readThe sitemap settings (root node, protocol, file name, class filter)
expsitemap::availablepublicWhether the sitemap extensions are active
expsitemap::rootnodecontent/readThe node the sitemap starts from
expsitemap::entries::<node>::<limit>::<offset>content/readSitemap entries of a subtree: loc, lastmod, changefreq, priority (class filter of the settings applied)
expsitemap::count::<node>content/readHow many entries a subtree has
expsitemap::entry::<node>content/readThe sitemap entry of one node
expsitemap::recent::<node>::<limit>content/readThe most recently changed entries (a news sitemap)
expsitemap::index::<node>content/readThe sitemap index: the pages of 50000 entries with their entry window
expsitemap::filessetup/administrateThe generated sitemap files (name, size, modified)
expsitemap::classescontent/readThe classes the sitemap includes or excludes
expsitemap::robotspublicThe robots.txt Sitemap line for the generated sitemap

Statistics (expstats, 16 services)

Search phrases, content counts, growth, languages, users and sessions. Content figures follow content/read, the rest needs setup/administrate.

CallAccessWriteSummary
expstats::searchtop::<limit>::<offset>setup/administrateThe most frequent search phrases with their average result count
expstats::searchtotalsetup/administrateNumber of distinct search phrases and of searches
expstats::searchnoresults::<limit>setup/administrateSearch phrases that found nothing
expstats::searchphrase::<phrase>setup/administrateThe statistics of one search phrase
expstats::contenttotalscontent/readCounts of objects, nodes, versions and classes
expstats::classcounts::<limit>content/readPublished objects per class
expstats::sectioncountscontent/readPublished objects per section
expstats::statecountscontent/readObjects per object state
expstats::recentpublished::<limit>content/readThe most recently published objects
expstats::recentmodified::<limit>content/readThe most recently modified objects
expstats::growth::<days>content/readObjects published per day for the last N days (at most 90)
expstats::topowners::<limit>setup/administrateThe users who own most published objects
expstats::languagecountscontent/readPublished object translations per language
expstats::userssetup/administrateNumber of user accounts, enabled and disabled
expstats::sessionssetup/administrateActive sessions (not expired) and how many are logged-in users
expstats::overviewcontent/readThe headline figures in one answer

Designs and templates (expdesign, 13 services)

Read only, setup/administrate: designs, bases, the overrides of override.ini, the templates a design resolves, and the source of a template inside a design directory (path checked, 512 KB at most).

CallAccessWriteSummary
expdesign::designssetup/administrateThe design directories of the site and of the extensions
expdesign::currentsetup/administrateThe standard design, the site design and the additional designs of this siteaccess
expdesign::basessetup/administrateThe design bases in priority order (first wins)
expdesign::extensionssetup/administrateThe extensions that provide designs for this siteaccess
expdesign::overrides::<limit>::<offset>::<source>setup/administrateThe template overrides of override.ini
expdesign::override::<name>setup/administrateOne override by name
expdesign::overridecountsetup/administrateHow many overrides there are
expdesign::overridesfor::<source>setup/administrateThe overrides of one template source (node/view/full.tpl)
expdesign::templates::<limit>::<offset>::<prefix>setup/administrateThe templates of the resolved design, with the design base each comes from
expdesign::templatecountsetup/administrateHow many templates the design resolves
expdesign::resolve::<template>setup/administrateWhich file a template path resolves to and its overrides
expdesign::source::<path>setup/administrateThe source of a template of a design directory (design/<name>/templates/..., read only)
expdesign::cachestatesetup/administrateTemplate cache settings: compile, cache, override cache and design location cache

Languages and translations (explanguage, 14 services)

Content languages, language masks, locales, and the progress of the interface translations in share/translations.

CallAccessWriteSummary
explanguage::languagespublicThe content languages of the installation with their object counts
explanguage::language::<locale>publicOne content language by locale
explanguage::prioritizedpublicThe languages in the priority order of this siteaccess
explanguage::toppublicThe top priority language of this siteaccess
explanguage::decode::<mask>publicThe languages in a language mask
explanguage::maskfor::<locales>::<always_available>publicThe language mask of some locales
explanguage::locales::<limit>::<offset>publicThe locales the system knows (share/locale)
explanguage::localeinfo::<locale>publicNames, formats and currency of a locale
explanguage::countriespublicThe country codes known to the locales
explanguage::translationsuserThe interface translations present in share/translations and their size
explanguage::translationstats::<locale>userMessages, finished, unfinished and obsolete of one interface translation
explanguage::translationcontexts::<locale>::<limit>::<offset>userThe translation contexts (files) of one interface translation with message counts
explanguage::siteaccesspublicThe language settings of the current siteaccess
explanguage::objectcountscontent/readPublished object names per language

The links stored by URL attributes: lists, search, validity, the objects using a link. check requests a stored link with HEAD, without redirects, only for http and https addresses that resolve to public hosts (loopback, private, link-local and credentials are refused with 422).

CallAccessWriteSummary
expurl::list::<limit>::<offset>::<validity>url/listThe stored links, newest first
expurl::get::<id>url/viewOne link with its check state
expurl::count::<validity>url/listNumber of stored links
expurl::statsurl/listLinks by validity: valid, invalid, never checked
expurl::invalid::<limit>::<offset>url/listThe links found invalid
expurl::unchecked::<limit>::<offset>url/listThe links never checked
expurl::search::<text>::<limit>::<offset>url/listLinks whose address contains the text
expurl::byurl::<url>url/viewThe stored link with exactly this address
expurl::objects::<id>::<limit>::<offset>url/viewThe objects (readable by the user) that use a link
expurl::bydomain::<limit>url/listLink counts per host name
expurl::check::<id>url/editPOSTRequests a stored link (HEAD) and records whether it is valid; only public http/https hosts
expurl::setvalid::<id>url/editPOSTSets the validity of a stored link by hand

PDF (exppdf, 10 services)

PDF links of nodes (content/pdf/<node>, which checks the policy again when the link is followed) and the PDF export definitions of the pdf module.

CallAccessWriteSummary
exppdf::availablepublicWhether PDF export is available (classes present) and how many export definitions exist
exppdf::canpdf::<node>content/readWhether the user may export a node as PDF
exppdf::link::<node>::<language>content/readThe URL that downloads the PDF of a node (optional language)
exppdf::links::<nodes>content/readPDF links for several nodes at once (those the user may export)
exppdf::exports::<limit>::<offset>pdf/editThe PDF export definitions
exppdf::export::<id>pdf/editOne PDF export definition
exppdf::exportcountpdf/editHow many PDF export definitions exist
exppdf::exportsfor::<node>pdf/editThe export definitions whose source node is a node or one of its ancestors
exppdf::exportfile::<id>pdf/editWhether the generated file of a stored export exists and how large it is
exppdf::statusespublicThe export modes: created once or on the fly

Clients

Every service is a plain HTTP call, so any language with an HTTP client and a cookie jar is a client. The contract all the examples below follow:

  1. Wire format. GET|POST <site>/ezjscore/call/<service>[::arg...]?ContentType=json with Accept: application/json. ezjscore wraps the answer: { "error_text": "", "content": <envelope> }. error_text is set only when the call itself could not be routed; the service's own result (or its error) is content, see "The envelope". Arguments in the URL are percent-encoded and joined with ::; a write sends its arguments as POST form fields.
  2. Auth. POST expsession::login with username and password (form encoded). The answer's Set-Cookie is the session; keep it in a cookie jar. The answer also carries the new session's form token (data.token), which replaces any token fetched before the login. Anonymous calls work for the services whose access is public. Or use a personal API token (expsession::tokenCreate, see "Authentication with an API token"): send Authorization: Bearer <token> and X-Exp-Token: <token> with every call, keep no cookie, fetch no form token; writes are still POST.
  3. Writes. With an API token a write is only a POST (no cookie, so no form token). With the session: fetch expsession::token (data.token, data.field = ezxform_token, data.header = X-CSRF-Token), then POST with the token as the field ezxform_token and as the header X-CSRF-Token. A missing or wrong token is a 403.
  4. Errors. HTTP 200 does not mean success: look at content.ok. content.error.code is 400/401/403/404/409/422/500 (table in "Errors"). A 401 means sign in again, a 403 a missing policy or token (fetch a fresh token once and retry once, never loop), 404/409/422 are for the user to see. A non-200 HTTP status or a body that is not JSON is a transport fault (proxy, TLS, server down): retry with backoff, do not interpret.
  5. Paging. List services take offset and limit as their last two URL arguments and answer meta.total, meta.offset, meta.limit, meta.has_more. Loop while has_more is true (or offset + count < total); keep limit at or below expservices.ini [Paging] MaxLimit (200).
  6. Discovery. expservices::catalog gives every service with its args, access, write; generate a typed client from it, or call expservices::service::<domain>::<method> for one descriptor.

Toolchains and what was compiled (checked 2026-10-02 on the build machine): only bash+curl, python3 (with requests and PyGObject gi) and g++ without Qt exist. The shell and Python clients and the GTK/Python snippet (both variants) were extracted from this guide and run against the live site. The Kotlin, Swift, Objective-C and Qt/C++ snippets could not be compiled here (no kotlinc, swiftc, clang/gobjc or Qt headers); each of them says so, and they are written against the documented APIs of OkHttp 4, URLSession, NSURLSession and Qt 5/6 Network and reviewed by hand. Compile them in your project before relying on them.

In the examples $SITE is the base URL, e.g. https://www.example.com (or https://www.example.com/<siteaccess> when the installation needs a siteaccess in the path), expnode::children is a paged read and expforum::reply stands for any write service (take a real one from the catalogue). The shell and Python clients are in extension/expservices/bin/ and are tested against the live services; the other snippets follow the same contract and are meant as the starting point of an app.

Shell (bash + curl): extension/expservices/bin/expservices-client.sh (tested here)

export EXPSERVICES_URL=https://www.example.com
expservices-client.sh whoami                          # anonymous or signed in, pretty printed
expservices-client.sh catalog session                 # the services of one domain
expservices-client.sh call expsystem::version         # any read call; arguments follow, joined with ::
expservices-client.sh login editor                    # asks for the password (or EXPSERVICES_PASSWORD); cookie jar kept
expservices-client.sh page expnode::children 2 --limit 50 --max 500   # every item of a paged service, one JSON per line
expservices-client.sh post expforum::reply topic=123 'message=Hello' # write: token fetched and sent as field and header
expservices-client.sh --raw call expsession::ping     # the answer as received
EXPSERVICES_TOKEN=expt_... expservices-client.sh post expforum::reply topic=123 message=Hi   # API token: no login, no form token

The cookie jar is ${XDG_CACHE_HOME:-~/.cache}/expservices/cookies.txt (--jar or EXPSERVICES_JAR change it). Exit status: 0 ok, 1 service error (error 401: ... on stderr), 2 transport fault. It needs bash and curl, plus jq (or python3) for the JSON.

Python: extension/expservices/bin/expservices_client.py (tested here)

Standard library only; a module and a command line (expservices_client.py call expsystem::version, page, post, login...).

from expservices_client import Client, ServiceError, TransportError

c = Client("https://www.example.com", jar="expservices.cookies")   # jar optional; keeps the session between runs
c.login("editor", "secret")                                         # POST, stores the new token
version = c.call("expsystem::version")                              # read: returns data
for node in c.pages("expnode::children", 2, limit=50):              # paged read: yields every item
    print(node["name"])
try:
    c.post("expforum::reply", {"topic": 123, "message": "Hello"})   # write: field + header token
except ServiceError as e:                                           # the envelope's error
    print(e.code, e)                                                # 401 login again, 403 policy/token, 422 data
except TransportError as e:                                         # HTTP/network/JSON fault
    print("retry later:", e)

t = Client("https://www.example.com", api_token="expt_...")        # API token variant: no login, no jar, no form token
t.post("expforum::reply", {"topic": 123, "message": "Hello"})       # a POST with the token headers; command line: --token or EXPSERVICES_TOKEN

Kotlin / Android (OkHttp)

Not compiled here (no Kotlin toolchain on the build box).

// build.gradle: implementation("com.squareup.okhttp3:okhttp:4.12.0"); org.json comes with Android
class Expservices(val site: String) {
    private val jar = HashMap<String, List<Cookie>>()                 // use PersistentCookieJar for a real app
    private val http = OkHttpClient.Builder().cookieJar(object : CookieJar {
        override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) { jar[url.host] = cookies }
        override fun loadForRequest(url: HttpUrl) = jar[url.host] ?: emptyList()
    }).build()

    class ServiceException(val code: Int, message: String) : Exception(message)

    private fun url(service: String, args: List<Any> = emptyList()) =
        "$site/ezjscore/call/$service" + args.joinToString("") { "::" + Uri.encode(it.toString()) } + "?ContentType=json"

    private fun send(req: Request): JSONObject {                      // returns the envelope, throws on error
        http.newCall(req).execute().use { r ->
            if (!r.isSuccessful) throw IOException("HTTP ${r.code}")
            val outer = JSONObject(r.body!!.string())
            if (outer.optString("error_text").isNotEmpty()) throw ServiceException(500, outer.getString("error_text"))
            val env = outer.getJSONObject("content")
            if (!env.getBoolean("ok")) env.getJSONObject("error").let { throw ServiceException(it.getInt("code"), it.getString("message")) }
            return env
        }
    }

    // auth
    fun login(user: String, pass: String): JSONObject = send(Request.Builder().url(url("expsession::login"))
        .post(FormBody.Builder().add("username", user).add("password", pass).build()).build())

    // read, with paging
    fun children(nodeId: Int, offset: Int = 0, limit: Int = 25): JSONObject =
        send(Request.Builder().url(url("expnode::children", listOf(nodeId, offset, limit))).build())
    // loop: do { val e = children(2, off); ...; off += 25 } while (e.getJSONObject("meta").getBoolean("has_more"))

    // write with the form token
    fun write(service: String, fields: Map<String, String>): JSONObject {
        val t = send(Request.Builder().url(url("expsession::token")).build()).getJSONObject("data").getString("token")
        val body = FormBody.Builder().add("ezxform_token", t)
        fields.forEach { (k, v) -> body.add(k, v) }
        return send(Request.Builder().url(url(service)).header("X-CSRF-Token", t).post(body.build()).build())
    }
}
// error handling: catch ServiceException (401: show the sign-in screen; 403: refresh the token once; 422: show the message)
// and IOException (offline: retry with backoff). Run it off the main thread (coroutines Dispatchers.IO).

// API token variant: no cookie jar, no login, no form token. Keep the token in the Android Keystore / EncryptedSharedPreferences.
class ExpservicesToken(val site: String, private val token: String) {
    private val http = OkHttpClient()                                   // nothing to keep between calls
    private fun url(service: String, args: List<Any> = emptyList()) =
        "$site/ezjscore/call/$service" + args.joinToString("") { "::" + Uri.encode(it.toString()) } + "?ContentType=json"
    private fun auth(b: Request.Builder) = b.header("Authorization", "Bearer $token").header("X-Exp-Token", token)
    private fun send(req: Request): JSONObject = http.newCall(req).execute().use { r ->
        if (!r.isSuccessful) throw IOException("HTTP ${r.code}")
        val env = JSONObject(r.body!!.string()).getJSONObject("content")
        if (!env.getBoolean("ok")) env.getJSONObject("error").let { throw Expservices.ServiceException(it.getInt("code"), it.getString("message")) }
        env                                                            // 401 here: the token is revoked or expired, ask for a new one
    }
    fun read(service: String, vararg args: Any) = send(auth(Request.Builder().url(url(service, args.toList()))).build())
    fun write(service: String, fields: Map<String, String>): JSONObject {   // POST only, no form token
        val body = FormBody.Builder(); fields.forEach { (k, v) -> body.add(k, v) }
        return send(auth(Request.Builder().url(url(service))).post(body.build()).build())
    }
}

Swift / iOS (URLSession)

Not compiled here (no Swift toolchain on the build box).

struct ServiceError: Error { let code: Int; let message: String }

final class Expservices {
    let site: URL                                    // https://www.example.com
    let session = URLSession(configuration: .default)   // the default configuration keeps cookies (HTTPCookieStorage.shared)
    init(site: URL) { self.site = site }

    private func url(_ service: String, _ args: [Any] = []) -> URL {
        let tail = args.map { "::" + "\($0)".addingPercentEncoding(withAllowedCharacters: .alphanumerics)! }.joined()
        return URL(string: "\(site)/ezjscore/call/\(service)\(tail)?ContentType=json")!
    }
    private func envelope(_ req: URLRequest) async throws -> [String: Any] {
        let (data, resp) = try await session.data(for: req)
        guard (resp as? HTTPURLResponse)?.statusCode == 200,
              let outer = try JSONSerialization.jsonObject(with: data) as? [String: Any],
              let env = outer["content"] as? [String: Any] else { throw ServiceError(code: 0, message: "transport fault") }
        if env["ok"] as? Bool != true {
            let e = env["error"] as? [String: Any] ?? [:]
            throw ServiceError(code: e["code"] as? Int ?? 500, message: e["message"] as? String ?? "error")
        }
        return env
    }
    private func form(_ url: URL, _ fields: [String: String], token: String? = nil) -> URLRequest {
        var r = URLRequest(url: url); r.httpMethod = "POST"
        var f = fields; if let t = token { f["ezxform_token"] = t; r.setValue(t, forHTTPHeaderField: "X-CSRF-Token") }
        var c = URLComponents(); c.queryItems = f.map { URLQueryItem(name: $0.key, value: $0.value) }
        r.httpBody = c.percentEncodedQuery?.data(using: .utf8)
        r.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type"); return r
    }

    func login(_ user: String, _ pass: String) async throws -> [String: Any] {          // auth
        try await envelope(form(url("expsession::login"), ["username": user, "password": pass]))
    }
    func children(_ node: Int, offset: Int = 0, limit: Int = 25) async throws -> [String: Any] {   // read, paged
        try await envelope(URLRequest(url: url("expnode::children", [node, offset, limit])))     // meta.has_more drives the next page
    }
    func write(_ service: String, _ fields: [String: String]) async throws -> [String: Any] {     // write
        let t = ((try await envelope(URLRequest(url: url("expsession::token"))))["data"] as! [String: Any])["token"] as! String
        return try await envelope(form(url(service), fields, token: t))
    }
}
// Errors: catch ServiceError (401 sign in, 403 refresh token once, 422 show message); other errors are transport faults.

// API token variant: no cookies, no login, no form token. Keep the token in the Keychain.
final class ExpservicesToken {
    let site: URL, token: String
    let session = URLSession(configuration: { let c = URLSessionConfiguration.ephemeral; c.httpCookieStorage = nil; return c }())
    init(site: URL, token: String) { self.site = site; self.token = token }

    private func request(_ service: String, _ args: [Any] = [], form: [String: String]? = nil) -> URLRequest {
        let tail = args.map { "::" + "\($0)".addingPercentEncoding(withAllowedCharacters: .alphanumerics)! }.joined()
        var r = URLRequest(url: URL(string: "\(site)/ezjscore/call/\(service)\(tail)?ContentType=json")!)
        r.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        r.setValue(token, forHTTPHeaderField: "X-Exp-Token")
        if let f = form {                                              // a write: POST, no form token
            var c = URLComponents(); c.queryItems = f.map { URLQueryItem(name: $0.key, value: $0.value) }
            r.httpMethod = "POST"; r.httpBody = c.percentEncodedQuery?.data(using: .utf8)
            r.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        }
        return r
    }
    func call(_ service: String, _ args: [Any] = [], form: [String: String]? = nil) async throws -> [String: Any] {
        let (data, resp) = try await session.data(for: request(service, args, form: form))
        guard (resp as? HTTPURLResponse)?.statusCode == 200,
              let env = (try JSONSerialization.jsonObject(with: data) as? [String: Any])?["content"] as? [String: Any]
        else { throw ServiceError(code: 0, message: "transport fault") }
        if env["ok"] as? Bool != true {                               // 401: the token is revoked or expired
            let e = env["error"] as? [String: Any] ?? [:]
            throw ServiceError(code: e["code"] as? Int ?? 500, message: e["message"] as? String ?? "error")
        }
        return env
    }
}

Objective-C (NSURLSession)

Not compiled here (no Objective-C compiler or Foundation on the build box; gcc has no Objective-C front end).

// The shared session keeps cookies. Wrap in a method taking a completion block; shown here for one write.
- (void)post:(NSString *)service fields:(NSDictionary<NSString *, NSString *> *)fields
  completion:(void (^)(NSDictionary *data, NSError *error))done {
    NSString *base = @"https://www.example.com/ezjscore/call/";
    NSURL *tokenURL = [NSURL URLWithString:[base stringByAppendingString:@"expsession::token?ContentType=json"]];
    [[NSURLSession.sharedSession dataTaskWithURL:tokenURL completionHandler:^(NSData *d, NSURLResponse *r, NSError *e) {
        NSDictionary *env = [NSJSONSerialization JSONObjectWithData:d options:0 error:nil][@"content"];   // d nil = transport fault
        if (e || ![env[@"ok"] boolValue]) { done(nil, e ?: [self errorFrom:env]); return; }
        NSString *token = env[@"data"][@"token"];
        NSMutableArray *pairs = [NSMutableArray arrayWithObject:[@"ezxform_token=" stringByAppendingString:token]];
        [fields enumerateKeysAndObjectsUsingBlock:^(NSString *k, NSString *v, BOOL *s) {
            [pairs addObject:[NSString stringWithFormat:@"%@=%@", k,
                [v stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.alphanumericCharacterSet]]];
        }];
        NSString *u = [NSString stringWithFormat:@"%@%@?ContentType=json", base, service];
        NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:u]];
        req.HTTPMethod = @"POST"; req.HTTPBody = [[pairs componentsJoinedByString:@"&"] dataUsingEncoding:NSUTF8StringEncoding];
        [req setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
        [req setValue:token forHTTPHeaderField:@"X-CSRF-Token"];
        [[NSURLSession.sharedSession dataTaskWithRequest:req completionHandler:^(NSData *d2, NSURLResponse *r2, NSError *e2) {
            NSDictionary *env2 = d2 ? [NSJSONSerialization JSONObjectWithData:d2 options:0 error:nil][@"content"] : nil;
            done([env2[@"ok"] boolValue] ? env2[@"data"] : nil, e2 ?: ([env2[@"ok"] boolValue] ? nil : [self errorFrom:env2]));
        }] resume];
    }] resume];
}
- (NSError *)errorFrom:(NSDictionary *)env {      // { ok:false, error:{ code, message } } -> NSError (domain "expservices")
    NSDictionary *err = env[@"error"];
    return [NSError errorWithDomain:@"expservices" code:[err[@"code"] integerValue]
                           userInfo:@{NSLocalizedDescriptionKey: err[@"message"] ?: @"transport fault"}];
}
// Login: the same POST without token to expsession::login with username and password (cookies are kept by the session).
// Read with paging: GET .../expnode::children::2::<offset>::<limit>, continue while env[@"meta"][@"has_more"] is true.

// API token variant: no cookies, no token fetch; the same POST without ezxform_token and with the token headers.
- (NSMutableURLRequest *)requestFor:(NSString *)service token:(NSString *)token {
    NSString *u = [NSString stringWithFormat:@"https://www.example.com/ezjscore/call/%@?ContentType=json", service];
    NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:u]];
    req.HTTPShouldHandleCookies = NO;                                       // the token is the only credential
    [req setValue:[@"Bearer " stringByAppendingString:token] forHTTPHeaderField:@"Authorization"];
    [req setValue:token forHTTPHeaderField:@"X-Exp-Token"];
    return req;       // for a write set HTTPMethod = @"POST", the form body and the Content-Type as above (no ezxform_token)
}
// Run it with [[NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.ephemeralSessionConfiguration] dataTaskWithRequest:...];
// keep the token in the Keychain. A 401 envelope means the token is revoked or expired.

Qt / C++ (QNetworkAccessManager)

Not compiled here (no Qt headers or qmake on the build box).

// QT += network. One QNetworkAccessManager per client; set a QNetworkCookieJar (the default one keeps session cookies).
class Expservices : public QObject {
    Q_OBJECT
    QNetworkAccessManager net; QString site = "https://www.example.com"; QString token;
    QUrl url(const QString &svc, const QStringList &args = {}) const {
        QString u = site + "/ezjscore/call/" + svc;
        for (const QString &a : args) u += "::" + QString::fromUtf8(QUrl::toPercentEncoding(a));
        return QUrl(u + "?ContentType=json");
    }
    // Parses the answer; emits failed(code, message) for { ok:false }, for a non-200 status or a body that is not JSON.
    void handle(QNetworkReply *r, std::function<void(const QJsonObject &)> ok) {
        connect(r, &QNetworkReply::finished, this, [=] {
            const QByteArray body = r->readAll(); const int http = r->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
            r->deleteLater();
            const QJsonObject env = QJsonDocument::fromJson(body).object().value("content").toObject();
            if (r->error() != QNetworkReply::NoError && http != 200) { emit failed(0, r->errorString()); return; }   // transport
            if (!env.value("ok").toBool()) { const auto e = env.value("error").toObject();
                emit failed(e.value("code").toInt(500), e.value("message").toString()); return; }                  // service
            ok(env);
        });
    }
public:
    void login(const QString &user, const QString &pass) {                        // auth: POST, no token needed
        QNetworkRequest rq(url("expsession::login"));
        rq.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
        QUrlQuery q; q.addQueryItem("username", user); q.addQueryItem("password", pass);
        handle(net.post(rq, q.toString(QUrl::FullyEncoded).toUtf8()), [this](const QJsonObject &env) {
            token = env["data"].toObject()["token"].toString(); emit signedIn(env["data"].toObject()); });
    }
    void children(int node, int offset, int limit) {                              // read, paged
        handle(net.get(QNetworkRequest(url("expnode::children", {QString::number(node), QString::number(offset), QString::number(limit)}))),
               [this](const QJsonObject &env) { emit page(env["data"].toArray(), env["meta"].toObject()["has_more"].toBool()); });
    }
    void write(const QString &svc, const QMap<QString, QString> &fields) {         // write: field + header token
        QNetworkRequest rq(url(svc));
        rq.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
        rq.setRawHeader("X-CSRF-Token", token.toUtf8());
        QUrlQuery q; q.addQueryItem("ezxform_token", token);
        for (auto it = fields.begin(); it != fields.end(); ++it) q.addQueryItem(it.key(), it.value());
        handle(net.post(rq, q.toString(QUrl::FullyEncoded).toUtf8()), [this](const QJsonObject &env) { emit written(env["data"]); });
    }
signals: void failed(int code, const QString &message); void signedIn(const QJsonObject &); void page(const QJsonArray &, bool more); void written(const QJsonValue &);
};
// The token comes from the login answer; after a 403 fetch expsession::token once and retry once.

// API token variant: no login, no cookie jar needed, no form token. Keep the token in KWallet / libsecret.
class ExpservicesToken : public Expservices {
    QString apiToken;
public:
    explicit ExpservicesToken(const QString &t) : apiToken(t) {}
    QNetworkRequest authorised(QNetworkRequest rq) const {
        rq.setRawHeader("Authorization", "Bearer " + apiToken.toUtf8());
        rq.setRawHeader("X-Exp-Token", apiToken.toUtf8());
        return rq;
    }
    // read: net.get(authorised(QNetworkRequest(url("expsession::whoami"))));
    // write: rq = authorised(QNetworkRequest(url(svc))); content type form-urlencoded; net.post(rq, body) with the fields only (no ezxform_token)
    // a { ok:false, error:{ code:401 } } answer means the token is revoked or expired: ask the user for a new one
};

GTK / Python (requests, or libsoup) (tested here: both variants run against the live site)

With PyGObject, run the calls off the main loop (GLib.idle_add or a thread) and hand the data to the widgets. requests is the simplest transport; the shell/Python client above has the same logic without it.

import http.cookiejar
import requests
from gi.repository import GLib

class Api:
    def __init__(self, site): self.site, self.s, self.token = site, requests.Session(), None   # Session keeps cookies
    def _url(self, svc, args=()): return f"{self.site}/ezjscore/call/{svc}" + "".join("::" + requests.utils.quote(str(a), safe="") for a in args) + "?ContentType=json"
    def _env(self, r):
        r.raise_for_status()                                  # transport fault: HTTPError
        env = r.json()["content"]
        if not env["ok"]: raise RuntimeError(f'{env["error"]["code"]}: {env["error"]["message"]}')
        return env
    def login(self, u, p): env = self._env(self.s.post(self._url("expsession::login"), data={"username": u, "password": p})); self.token = env["data"]["token"]; return env["data"]
    def read(self, svc, *args): return self._env(self.s.get(self._url(svc, args)))            # paged: args end with offset, limit
    def write(self, svc, **fields):
        self.token = self.token or self.read("expsession::token")["data"]["token"]
        return self._env(self.s.post(self._url(svc), data={"ezxform_token": self.token, **fields}, headers={"X-CSRF-Token": self.token}))

# in a handler: threading.Thread(target=lambda: GLib.idle_add(fill_list, api.read("expnode::children", 2, 0, 25)["data"])).start()

class TokenApi(Api):                                    # API token variant: no login, no cookies, no form token
    def __init__(self, site, token):
        super().__init__(site)
        self.s.cookies.set_policy(http.cookiejar.DefaultCookiePolicy(allowed_domains=[]))   # keep no cookie
        self.s.headers.update({"Authorization": "Bearer " + token, "X-Exp-Token": token})
    def write(self, svc, **fields): return self._env(self.s.post(self._url(svc), data=fields))   # POST, no ezxform_token

libsoup (Soup 3): Soup.Session() keeps cookies when a Soup.CookieJar feature is added (session.add_feature(Soup.CookieJar())); Soup.Message.new_from_encoded_form("POST", url, Soup.form_encode(...)) builds the POST, msg.get_request_headers().append("X-CSRF-Token", token) adds the header, and session.send_and_read_async(msg, ...) returns the bytes of the same { error_text, content } JSON.

KDE and GNOME integration notes

  • KDE (Qt/KF6). Use the Qt client above. Store the password in KWallet (KWallet::Wallet::openWallet) and never in the config file; only the session cookie lives in memory (or QNetworkCookieJar persisted to ~/.cache). Show the services of the catalogue as a KRunner plugin (query expsearch::search), a Plasma widget (read services on a timer, expsession::ping keeps the session), or KIO slave-like browsing of expnode::children. KJob/QNetworkReply give the async model; KNotification for 401 ("sign in again").
  • GNOME (GTK4/libadwaita). Use the GTK/Python client or Soup 3 from C/Vala/Rust. Store the password with libsecret (Secret.password_store_sync); show the services in a GNOME Shell search provider (D-Bus org.gnome.Shell.SearchProvider2 backed by expsearch::search), a libadwaita app with an Adw.NavigationView per node level, Gio.ListStore filled page by page. Gio.Task/GLib.idle_add keep the UI responsive; GNotification for 401.
  • Both. Discover with expservices::catalog at start (cache by expservices::version), build forms from the args of a write service, send writes only after the user's action, and treat every 403 as "refresh the token once". Online accounts: KDE (KAccounts) and GNOME (GOA) have no generic "cookie session" provider, so keep the account in the app.

Tests

The shell and Python clients were run against a live site (read-only calls; the write path up to the 401 an anonymous session gets). The API token flow was checked over HTTP (create, bearer read and write, revoke, 401), the shell and Python clients were driven with a token, and the GTK/Python snippets of this guide were extracted and run.

Frontend designs: React and wireframe

Both are designs of the extension (extension/expservices/design/), shown by test siteaccesses and reached by URI:

DesignSiteaccess / URLWhat
expportal_react/portalreact/the portal in React 18 with React Bootstrap 2.10 (Bootstrap 5.3), hash routes #/news, #/shop, #/shop/basket, #/forums, #/media, #/feeds, #/search, #/login, #/profile
expportal_wireframe/portalwireframe/low-fidelity screens (grey boxes) with an annotation per block naming the service that feeds it, a phone/desktop frame toggle and a notes toggle; with the catalogue readable each service name is marked present or missing

No build step; vendored libraries. The React design needs no Node, JSX or bundler: its code is ES modules (javascript/portal/) using React.createElement (h). The libraries are vendored into the design (javascript/vendor/, stylesheets/vendor/) so the site works offline and the versions cannot move under it: React and ReactDOM 18.3.1 (UMD) and React Bootstrap 2.10.2 from cdnjs.cloudflare.com/ajax/libs/ and Bootstrap 5.3.3 CSS. To upgrade, download the same file names from cdnjs at the new version. Layout: services.js (the client: fetch, session cookie, form token, envelope unwrapping, ServiceError), api.js (one function per screen need, the only file that names services), ui.js (kit shortcuts, useAsync, pager, error and loading states), pages/*.js (one component per page), app.js (shell and hash router).

Siteaccesses. portalreact and portalwireframe are directories settings/siteaccess/<name>/site.ini.append.php (SiteDesign = the design, IndexPage=user/login so the shell renders with a 200) plus the entry AvailableSiteAccessList[]=<name> in the global override, added with exp:ini add site/SiteAccessSettings/AvailableSiteAccessList[] <name> override. Reverse it with exp:ini rem for the same entry and removing the directory; the live site siteaccess is not touched. The extension registers its designs with settings/design.ini.append.php (DesignExtensions[]=expservices); without that file an extension design is not found.

Frontends: jQuery 4 reference and reactive jQuery

Two portal designs in extension/expservices/design/ consume the services from the browser. Both are corporate portals (home, news list and article, shop catalogue, product and basket, forums with topics and replies, media gallery, feeds, search, login and profile), rendered client-side, with no build step, on the jQuery 4 that ezjscore ships (ezjsc::jquery). Both are accessible (skip link, landmarks, aria-live announcements, focus moved to the page on navigation, labelled forms, native <dialog> for the lightbox), responsive (mobile first, one breakpoint at 48em) and light or dark (prefers-color-scheme, plus a toggle remembered in localStorage).

DesignSiteaccess (test)URLIdea
expportal_jqueryportaljq/portaljq/the reference standard: small modules, one script per feature, pages build DOM directly
expportal_reactiveportalreactive/portalreactive/the same features with a React-like model: components, store, one-way flow

Architecture (expportal_jquery)

templates/pagelayout.tpl is the only template. It ignores the module result, prints the page frame, passes the configuration as data- attributes of <body> (siteaccess URL, ezjscore/call URL, content root node ids from extension/expservices/settings/expportal.ini), loads jQuery 4 through ezscript_load( 'ezjsc::jquery' ) and then the scripts in javascript/portal/, one per feature:

FileRole
core.jsconfig, the service table (ExpPortal.services), api.call(), DOM helpers (text is always set as text), UI states, hash router, theme
home.js, news.js, shop.js, forums.js, media.js, feeds.js, search.js, account.jsone feature each: registers its routes with router.add( '/news/:id', handler )
app.jsboot: chrome, service catalogue, who is logged in, basket count, router

The address is the hash route (#/news/12), so every portal URL is served by the one pagelayout and the portal can be dropped under any siteaccess. api.call( feature, args, post ) builds <siteaccess>/ezjscore/call/<service>::<arg>::<arg>?ContentType=json, sends reads as GET and writes as POST with the form token (expsession::token, field ezxform_token, header X-CSRF-Token; the login is the one write sent without it), unwraps { error_text, content } and the service envelope { ok, data, meta }, and rejects with { code, message }. At boot it reads expservices::catalog: a service that is not in the catalogue is not called and its page shows a clear "service not available" state instead of an error; if the catalogue itself cannot be read, calls are tried and a 404 or "no such function" answer gets the same state.

The reactive variant (expportal_reactive)

It falls back to expportal_jquery (AdditionalSiteDesignList), so core.js (api, services, router, theme) and portal.css are shared. Its own scripts in javascript/reactive/ implement the pattern, documented in each file:

state --(components)--> description --(vdom: keyed diff)--> DOM
  ^                                                            |
  |   dispatch( action ) <--------- event handlers ------------+
  +--- reducer( state, action ) -> new state;  effects run after the reducer and call services
FileRole
vdom.jsh( tag, props, ...children ) returns a description; mount( container, view ) renders and re-renders it. Keyed list reconciliation (nodes move, keep focus and typed text), one delegating listener per event type (handlers are replaced, never stacked), ref callback, no way to inject HTML
store.jscreateStore( reducer ): getState, dispatch, subscribe (only on a changed state), effect; combine() for slices
components.jsevery component is a pure function ( state, dispatch ) -> description; the route table (pattern, page, the resources the page needs)
effects.jsthe only place that calls services: load( key, feature, args ) dispatches res/request, res/success or res/failure; form posts, login, basket
app.jsthe reducers (route, user, basket, res, form, lightbox, feedOpen), the store and the mounts (#main, #nav, #account-slot, #basket-count)

A page never fetches: the route reducer stores the route, the effect reads the route table's needs and loads the missing resources into state.res[ key ] (loading, ok, error, unavailable), the component reads that and draws.

How each page maps to services

The one table is ExpPortal.services in core.js; ExpPortal.argMap turns the portal's ( node, offset, limit ) into the positional order of a service. To follow a renamed service, change the table only.

Page (route)Services
Home /expnode::children (news), expproduct::list (shop), expimage::list (media)
News /news, article /news/:idexpnode::children, expnode::get
Shop /shop, product /shop/:id, basket /basketexpproduct::list, expproduct::view, expbasket::view, expbasket::add, expbasket::remove
Forums /forums, topics /forums/:id, topic /forums/topic/:idexpnode::children (forums), expforum::topics, expforum::replies, expforum::reply, expforum::create_topic
Media /mediaexpimage::list
Feeds /feedsexpfeed::list, expfeed::items
Search /search?q=expsearch::search
Login /login, profile /profile, headerexpsession::login, expsession::logout, expsession::whoami, expsession::token, expuser::profile
every pageexpservices::catalog (which services exist)

Forum, feed, search and profile services were not yet in the catalogue when the designs were written; their names are the contract names and those pages show the "not available" state until the services exist. Content root node ids (news, shop, forums, media) are in expportal.ini; the defaults are the content root (2) and Media (43).

How to run

The test siteaccesses are local settings (not committed): create settings/siteaccess/portaljq and portalreactive and add them to [SiteAccessSettings] AvailableSiteAccessList of the global override with exp:ini (reversible: delete the two directories, and php bin/php/console exp:ini rem 'site/SiteAccessSettings/AvailableSiteAccessList[]' <name> override). They match by URI, so open https://www.example.com/portaljq/ and https://www.example.com/portalreactive/; the live site siteaccess and its design are untouched. DefaultPage is user/login because that module answers 200 to anonymous users and the pagelayout replaces its output; Velocity workers need a restart to see a new siteaccess, Apache/FPM do not.

Tests

A browser test runs both portals at 960 px with device scale 2 and at 390 px: every page renders its heading and no unfinished state, the menu (and the phone menu toggle) navigates, search and the theme toggle work, no horizontal scroll, no console error and no jQuery Migrate warning (the code is jQuery 4 clean: no $.isArray, $.trim and the like). In-page unit tests cover the core (node shapes, list folding, URL building, unavailable handling, text safety) and, for the reactive design, keyed reordering with node identity kept, handler replacement, text safety, the store (reducer, subscribe on change only, effects) and the route table. expui's browser harness (/expui/test) is an admin page tied to admin sessions, so the portal tests run in the Playwright page instead.

Users and access (nine domains, 195 services)

Classes in extension/expservices/classes/users/, tests in tests/tests/extension/expservices/users/ (113 tests, run against the live database; they create a test group, test users, a test role, test sessions and collaboration items and remove them again). Users are addressed by content object id, user groups by node id, roles and policies by id.

Rules that hold for every service of these domains:

  • Nothing returns a password hash, activation or reset key, session key or session data. Login, e-mail and account state of a user are only returned for oneself or for a user the caller may edit (content/edit on the user object); other callers get the public part (id, name, node, class).
  • Writes are POST with the form token (expsession::token), checked against the policy shown in the table, and recorded as service.<domain>.<method> plus, where the kernel records one, its own access.* event (role, policy, user, password and reset events are written by the kernel code the services call).
  • Roles are edited like the role editor does: exprole::draftCreate, draftAddPolicy, draftRemovePolicy, publish (or draftDiscard); exppolicy changes the published role directly and validates module, function and limitation names against the module definition.
  • expaccount carries the public flows. The reset and activation keys travel by e-mail only; an activation key expires after 24 hours, a reset key after site.ini [UserSettings] ForgotPasswordKeyLifetime (one hour as shipped), both through the services as on the forgot-password page; forgotRequest answers the same for known and unknown addresses. Where a write is open to visitors its access is the login or registration policy of the user module (the catalogue allows no public write).
  • A collaboration item is visible to its participants only (everybody else gets 404); approve and deny need the approver role and a waiting approval, and leave the workflow to resume exactly as the kernel's own view does.
  • expsessionadmin names a session by a short derived identifier; listing and ending other sessions needs setup/administrate. Session counts read the ezsession table, which is empty when the PHP file session handler is used (expsessionadmin::settings says which handler is active). With great power comes great responsibility.

expuser (42 services)

ServiceAccessWriteArgumentsSummary
expuser::currentpublicThe current user: id, name, groups, and the private data of oneself
expuser::fetchuserid:intA user by content object id; login, e-mail and state only for oneself or users the caller may edit
expuser::byLoginuserlogin:stringA user by login name
expuser::byEmailuseremail:stringA user by e-mail address, only when the caller may see that user's private data
expuser::existspubliclogin:stringWhether a login name is taken
expuser::searchuserquery:string, limit:int, offset:intSearch users by login or name (2 characters or more)
expuser::listAlluserlimit:int, offset:intAll users, paged
expuser::countuserThe number of users
expuser::listByGroupusergroup:int, limit:int, offset:intThe users directly in a user group (group node id), paged
expuser::loggedInsetup/administratelimit:int, offset:intThe users with an active session
expuser::loggedInCountsetup/administrateLogged in registered and anonymous sessions
expuser::isOnlineuserid:intWhether a user has an active session
expuser::profileuserid:intThe content attributes of a user (not the account), oneself when no id
expuser::updateProfileuserPOSTid:int, fields:jsonUpdate profile attributes of a user the caller may edit (POST id, fields JSON)
expuser::createuserPOSTgroup:int, login:string, email:string, password:string, fields:json, enabled:bool, class:stringCreate a user in a group (POST group, login, email, password, fields, enabled, class)
expuser::removeuserPOSTid:intMove a user to the trash (POST id)
expuser::enableuserPOSTid:intEnable an account (POST id)
expuser::disableuserPOSTid:intDisable an account and end its sessions (POST id)
expuser::unlockuserPOSTid:intReset the failed login counter (POST id)
expuser::setPassworduserPOSTid:int, password:stringSet another user's password, ends their sessions (POST id, password)
expuser::changeEmailuserPOSTid:int, email:stringChange the e-mail address of a user the caller may edit (POST id, email)
expuser::changeLoginuserPOSTid:int, login:stringChange the login name of a user the caller may edit (POST id, login)
expuser::loginInfouserid:intLogin count, last visit, failed attempts, lock and online state
expuser::lastVisituserid:intThe last visit of a user
expuser::settingsuserid:intAccount settings: enabled and the maximum number of logins
expuser::setMaxLoginuserPOSTid:int, max_login:intChange the maximum number of simultaneous logins (POST id, max_login)
expuser::validateLoginpubliclogin:stringCheck a login name against the validation rules and whether it is taken
expuser::validatePasswordpublicpassword:stringCheck a password against the length rule
expuser::passwordPolicypublicThe password and e-mail rules of the site
expuser::generatePassworduserlength:intA generated password
expuser::rolesOfrole/listid:intThe roles a user has, directly and through groups
expuser::groupsOfuserid:intThe groups a user is a member of
expuser::effectivePoliciesrole/listid:int, limit:int, offset:intThe effective module/function access of a user, paged
expuser::limitationsrole/listid:intThe limited role assignments of a user
expuser::hasAccesspublicmodule:string, function:stringWhether the current user has access to a module and function
expuser::hasAccessOfrole/listid:int, module:string, function:stringWhether a given user has access to a module and function
expuser::canNodepublicnode:intThe current user's rights on a node: read, edit, remove, create, move, hide, translate
expuser::accessSummaryuserThe modules and functions the current user has any access to
expuser::canLoginTorole/listid:int, siteaccess:stringWhether a user may log in to a siteaccess
expuser::hashTypesuserThe password hash types and the default
expuser::userClassesuserThe content class identifiers of users and of user groups
expuser::anonymouspublicThe id of the anonymous user

expusergroup (20 services)

ServiceAccessWriteArgumentsSummary
expusergroup::rootuserThe users root node, the default placement and the group classes
expusergroup::treeusergroup:int, depth:intThe group tree below a node, to a depth (default the users root, depth 3)
expusergroup::fetchusergroup:intA user group by node id
expusergroup::childrenusergroup:int, limit:int, offset:intThe subgroups of a group, paged
expusergroup::subgroupCountusergroup:intThe number of subgroups
expusergroup::pathusergroup:intThe path from the root to a group
expusergroup::parentusergroup:intThe parent group
expusergroup::searchuserquery:string, limit:int, offset:intSearch groups by name
expusergroup::membersusergroup:int, limit:int, offset:intThe users directly in a group, paged
expusergroup::memberCountusergroup:intThe number of users directly in a group
expusergroup::isMemberuseruser:int, group:intWhether a user is directly in a group
expusergroup::ofUseruserid:intThe groups of a user
expusergroup::rolesOfrole/listgroup:intThe roles assigned to a group
expusergroup::createuserPOSTparent:int, name:string, class:string, fields:jsonCreate a group (POST parent, name, class, fields)
expusergroup::renameuserPOSTgroup:int, name:stringRename a group (POST group, name)
expusergroup::removeuserPOSTgroup:int, force:boolMove a group to the trash (POST group, force for a group with children)
expusergroup::addMemberuserPOSTgroup:int, user:intAdd a user to a group, a new location (POST group, user)
expusergroup::removeMemberuserPOSTgroup:int, user:intRemove a user from a group, never the last one (POST group, user)
expusergroup::moveMemberuserPOSTuser:int, from:int, to:intMove a user from one group to another (POST user, from, to)
expusergroup::addMembersuserPOSTgroup:int, users:listAdd up to 100 users to a group (POST group, users)

exprole (26 services)

ServiceAccessWriteArgumentsSummary
exprole::listAllrole/listlimit:int, offset:intAll roles, paged
exprole::countrole/listThe number of roles
exprole::fetchrole/listid:intA role by id
exprole::viewrole/viewid:intA role with its policies and assignments
exprole::byNamerole/listname:stringA role by name
exprole::existsrole/listname:stringWhether a role name exists
exprole::policiesrole/viewid:int, limit:int, offset:intThe policies of a role, paged
exprole::policyCountrole/viewid:intThe number of policies of a role
exprole::assignmentsrole/viewid:int, limit:int, offset:intWho a role is assigned to, with limitations
exprole::assignmentsOfrole/viewobject:intThe roles assigned to a user or group object
exprole::byLimitationrole/viewidentifier:string, value:stringRoles assigned with a limitation (Subtree/Section, value)
exprole::createrole/editPOSTname:stringCreate an empty role (POST name)
exprole::renamerole/editPOSTid:int, name:stringRename a role (POST id, name)
exprole::copyrole/editPOSTid:int, name:stringCopy a role with its policies (POST id, optional name)
exprole::removerole/editPOSTid:int, force:boolRemove a role (POST id, force when it is assigned)
exprole::draftCreaterole/editPOSTid:intStart editing: create (or return) the draft of a role (POST id)
exprole::draftrole/editid:intThe draft of a role
exprole::draftRenamerole/editPOSTid:int, name:stringRename the draft (POST id, name)
exprole::draftAddPolicyrole/editPOSTid:int, module:string, function:string, limitations:jsonAdd a policy to the draft (POST id, module, function, limitations JSON)
exprole::draftRemovePolicyrole/editPOSTid:int, policy:intRemove a policy from the draft (POST id, policy)
exprole::draftDiscardrole/editPOSTid:intThrow the draft away (POST id)
exprole::publishrole/editPOSTid:intPublish the draft: it replaces the policies of the role (POST id)
exprole::assignrole/assignPOSTid:int, object:int, limit:string, limit_value:stringAssign a role to a user or group (POST id, object, limit subtree/section, limit_value)
exprole::unassignrole/assignPOSTid:int, object:int, assignment:intRemove an assignment (POST id, object, optional assignment id)
exprole::assignedrole/viewid:intThe objects (users, groups) a role is assigned to
exprole::usersWithrole/viewid:int, limit:int, offset:intThe users and groups holding a role

exppolicy (16 services)

ServiceAccessWriteArgumentsSummary
exppolicy::fetchrole/viewid:intA policy with its limitations
exppolicy::listOfRolerole/viewid:int, limit:int, offset:intThe policies of a role, paged
exppolicy::limitationsrole/viewid:intThe limitations of a policy, identifier => values
exppolicy::summaryrole/viewid:intA policy as one line of text
exppolicy::addrole/editPOSTrole:int, module:string, function:string, limitations:jsonAdd a policy to a role, validated against the module (POST role, module, function, limitations)
exppolicy::removerole/editPOSTid:intRemove a policy (POST id)
exppolicy::setLimitationsrole/editPOSTid:int, limitations:jsonReplace all limitations of a policy (POST id, limitations)
exppolicy::addLimitationrole/editPOSTid:int, identifier:string, values:listAdd one limitation (POST id, identifier, values)
exppolicy::removeLimitationrole/editPOSTid:int, identifier:stringRemove one limitation (POST id, identifier)
exppolicy::copyrole/editPOSTid:int, role:intCopy a policy into another role (POST id, role)
exppolicy::modulesrole/viewThe modules a policy can name
exppolicy::functionsrole/viewmodule:stringThe functions of a module
exppolicy::availableLimitationsrole/viewmodule:string, function:stringThe limitation identifiers of a module function
exppolicy::limitationValuesrole/viewmodule:string, function:string, identifier:string, limit:int, offset:intThe values a limitation can take, paged
exppolicy::ofModulerole/viewmodule:string, limit:int, offset:intThe policies naming a module, over all roles
exppolicy::findByLimitationrole/viewidentifier:string, value:string, limit:int, offset:intThe policies with a limitation identifier and value

expsessionadmin (16 services)

ServiceAccessWriteArgumentsSummary
expsessionadmin::ownpublicThe current session: user, started, cookie, short identifier
expsessionadmin::ownListuserlimit:int, offset:intThe active sessions of the current user (other devices)
expsessionadmin::ownCountuserThe number of active sessions of the current user
expsessionadmin::ownRemoveuserPOSTid:stringEnd one of your own sessions (POST id)
expsessionadmin::ownRemoveOthersuserPOSTEnd all your sessions except this one (POST)
expsessionadmin::regenerateuserPOSTIssue a new session key for this session (POST)
expsessionadmin::listAllsetup/administratelimit:int, offset:int, registered:boolThe active sessions of the installation, paged
expsessionadmin::byUsersetup/administrateid:int, limit:int, offset:intThe sessions of a user
expsessionadmin::countsetup/administrateActive, expired and total sessions
expsessionadmin::statssetup/administrateRegistered and anonymous sessions and expired ones
expsessionadmin::expiredCountsetup/administrateThe number of expired sessions
expsessionadmin::removeExpiredsetup/administratePOSTDelete expired sessions (POST)
expsessionadmin::removesetup/administratePOSTid:stringEnd a session by its identifier (POST id)
expsessionadmin::removeByUsersetup/administratePOSTid:intEnd all sessions of a user but your own (POST id)
expsessionadmin::settingssetup/administrateSession handler and timeouts
expsessionadmin::handlersetup/administrateThe session handler class and PHP save handler

exppreferences (13 services)

ServiceAccessWriteArgumentsSummary
exppreferences::getusername:stringOne preference of the current user
exppreferences::getManyusernames:listSeveral preferences of the current user (comma separated names)
exppreferences::listAlluserlimit:int, offset:intAll preferences of the current user, paged
exppreferences::byPrefixuserprefix:string, limit:int, offset:intThe preferences whose name starts with a prefix
exppreferences::existsusername:stringWhether a preference is set
exppreferences::countuserThe number of preferences of the current user
exppreferences::setuserPOSTname:string, value:stringSet a preference (POST name, value)
exppreferences::setManyuserPOSTvalues:jsonSet up to 100 preferences (POST values JSON)
exppreferences::removeuserPOSTname:stringRemove a preference (POST name)
exppreferences::incrementuserPOSTname:string, by:intAdd to a numeric preference (POST name, by)
exppreferences::listOfsetup/administrateid:int, limit:int, offset:intThe preferences of another user
exppreferences::getOfsetup/administrateid:int, name:stringOne preference of another user
exppreferences::setOfsetup/administratePOSTuser:int, name:string, value:stringSet a preference of another user (POST user, name, value)

expnotification (21 services)

ServiceAccessWriteArgumentsSummary
expnotification::settingsnotification/useDigest settings, subscription count and collaboration types of the current user
expnotification::digestTypesnotification/useThe digest types
expnotification::setDigestnotification/usePOSTreceive_digest:bool, digest_type:int, day:string, time:stringSet the digest settings (POST receive_digest, digest_type, day, time)
expnotification::subscriptionsnotification/uselimit:int, offset:intThe nodes the current user is subscribed to, paged
expnotification::subscriptionCountnotification/useThe number of subscriptions
expnotification::isSubscribednotification/usenode:intWhether the current user is subscribed to a node
expnotification::subscribenotification/usePOSTnode:int, use_digest:boolSubscribe to a node and its subtree (POST node, use_digest)
expnotification::unsubscribenotification/usePOSTnode:intUnsubscribe from a node (POST node)
expnotification::unsubscribeAllnotification/usePOSTRemove all subscriptions of the current user (POST)
expnotification::setDigestForSubscriptionnotification/usePOSTnode:int, use_digest:boolTurn the digest on or off for one subscription (POST node, use_digest)
expnotification::collaborationTypesnotification/useThe collaboration notification types the current user receives
expnotification::subscribeCollaborationnotification/usePOSTtype:stringReceive notifications of a collaboration type (POST type)
expnotification::unsubscribeCollaborationnotification/usePOSTtype:stringStop receiving a collaboration type (POST type)
expnotification::handlersnotification/administrateThe notification handlers
expnotification::eventTypesnotification/administrateThe available notification event types
expnotification::subscriptionsOfnotification/administrateid:int, limit:int, offset:intThe subscriptions of a user
expnotification::subscribeUsernotification/administratePOSTuser:int, node:int, use_digest:boolSubscribe a user to a node (POST user, node, use_digest)
expnotification::unsubscribeUsernotification/administratePOSTuser:int, node:intUnsubscribe a user from a node (POST user, node)
expnotification::subscribersnotification/administratenode:int, limit:int, offset:intThe users subscribed to a node
expnotification::eventsnotification/administratelimit:int, offset:intThe notification events, newest first, paged
expnotification::queuenotification/administrateThe size of the notification queue

expcollaboration (22 services)

ServiceAccessWriteArgumentsSummary
expcollaboration::itemsuserlimit:int, offset:int, status:string, is_read:bool, group:intThe collaboration items of the current user, paged (status active/inactive/archive, is_read, group)
expcollaboration::itemCountuserstatus:string, is_read:bool, group:intThe number of items, same filters
expcollaboration::summaryuserActive, unread, archived and pending approval counts
expcollaboration::fetchuserid:intOne item, for participants
expcollaboration::messagesuserid:int, limit:int, offset:intThe messages of an item, paged
expcollaboration::messageCountuserid:intThe message and unread message count of an item
expcollaboration::addMessageuserPOSTitem:int, text:stringAdd a message to an item (POST item, text)
expcollaboration::participantsuserid:intThe participants of an item with type and role
expcollaboration::markReaduserPOSTitem:intMark an item read (POST item)
expcollaboration::setActiveuserPOSTitem:int, active:boolShow or hide an item in the current user's list (POST item, active)
expcollaboration::handlersuserThe active collaboration handlers
expcollaboration::groupsuserlimit:int, offset:intThe collaboration groups of the current user
expcollaboration::groupInfouserid:intOne collaboration group
expcollaboration::groupItemsuserid:int, limit:int, offset:intThe items of a group, paged
expcollaboration::groupCreateuserPOSTtitle:string, parent:intCreate a group (POST title, parent)
expcollaboration::groupRenameuserPOSTgroup:int, title:stringRename a group (POST group, title)
expcollaboration::groupRemoveuserPOSTgroup:intRemove an empty group (POST group)
expcollaboration::moveToGroupuserPOSTitem:int, group:intPut an item in a group (POST item, group)
expcollaboration::pendingApprovalsuserlimit:int, offset:intThe approvals waiting for the current user
expcollaboration::approvalStatususerid:intThe state of an approval item and whether the current user may decide
expcollaboration::approveuserPOSTitem:int, comment:stringApprove a waiting item as an approver (POST item, comment)
expcollaboration::denyuserPOSTitem:int, comment:stringDeny a waiting item as an approver (POST item, comment)

expaccount (19 services)

ServiceAccessWriteArgumentsSummary
expaccount::registrationOptionspublicWhether registration is open and how it is verified
expaccount::registeruser/registerPOSTlogin:string, email:string, password:string, fields:jsonRegister a new account (POST login, email, password, fields)
expaccount::activationKeyValidpublickey:stringWhether an activation key is valid
expaccount::activateuser/loginPOSTkey:stringActivate an account with the key from the activation mail (POST key)
expaccount::forgotRequestuser/loginPOSTemail:stringMail a password reset link, the same answer for known and unknown addresses (POST email)
expaccount::forgotKeyValidpublickey:stringWhether a reset key is valid
expaccount::resetuser/loginPOSTkey:string, password:stringSet a new password with a reset key (POST key, password)
expaccount::changePassworduserPOSTold_password:string, new_password:string, confirm_password:stringChange your password with the old one (POST old_password, new_password, confirm_password)
expaccount::changeEmailuserPOSTpassword:string, email:stringChange your e-mail address, confirmed with the password (POST password, email)
expaccount::meuserYour account: profile data, groups, role count
expaccount::myRolesuserYour roles
expaccount::myGroupsuserYour groups
expaccount::myLoginInfouserYour login count, last visit and failed attempts
expaccount::updateMyProfileuserPOSTfields:jsonUpdate your own profile attributes (POST fields)
expaccount::unactivatedrole/listlimit:int, offset:intThe accounts waiting for activation, paged
expaccount::unactivatedCountrole/listThe number of accounts waiting for activation
expaccount::activateUseruserPOSTid:intActivate an account by hand (POST id)
expaccount::pendingResetsrole/listThe number of open password reset requests
expaccount::cancelResetuserPOSTid:intCancel the open reset requests of a user (POST id)

Total: 195 services.

Commerce, community and feeds (16 domains, 256 services)

Classes in extension/expservices/classes/commerce/ and classes/community/, tests in tests/tests/extension/expservices/commerce/ and community/ (151 tests, run against the live database like the other domains: test products, forums, polls, forms and exports are created in a test folder under the Media root (node 43) or as named test rows and removed in tearDown; baskets exist only for a test session key; the checkout tests create the temporary order of a test basket and cancel it, no order is completed and no payment is created; there is never a test database). Run: php vendor/bin/phpunit tests/tests/extension/expservices/commerce/ tests/tests/extension/expservices/community/.

Rules that hold for every service of these domains:

  • The shop works on the kernel. Products are content objects whose class has a price attribute (ezprice, ezmultiprice; expproduct::classes). Prices come with and without VAT, with the discount of the product and, when a currency is asked for (USD, EUR ...), converted with the rates of the currency table (in_currency; null when a rate is missing). Basket, wish list, orders, VAT, currency, discount and payment services call eZBasket, eZOrder, eZVatType/eZVatRule, eZCurrencyData, eZDiscountRule, eZWishList, eZPaymentObject and the shop operations (addtobasket, confirmorder, checkout), so workflows, triggers and the kernel's own audit events (commerce.order.status, .archive, .unarchive, .delete, commerce.vat.change, .currency.change, .discount.change, .payment.approve, .basket.checkout) behave exactly as in the shop views.
  • The basket belongs to the session. A remote app keeps its session cookie (expsession::login); without one, reads answer an empty basket and writes answer 401. Checkout is in steps: checkoutStatus (what blocks), startCheckout (the temporary order, nothing charged), review (shipping and VAT as the confirm step), cancelCheckout, and, only when the client means it, placeOrder, which runs the checkout operation: it activates the order or hands over to the payment gateway (redirect_url). Nothing else completes an order.
  • Orders: a customer reads their own (exporder::mine, myOrder, myReceipt); the shop administrators (shop/administrate) list, view, search, archive, unarchive and delete any order, change the status within their shop/setstatus policy (FromStatus/ToStatus limitations are applied by statusOptions and setStatus) and read statistics, customers and the dashboard. Account data is returned by exporder::account for administrators only; card or account numbers are never stored by the kernel and never returned.
  • Money values are numbers rounded to four decimals, VAT percentages numbers, currencies three-letter codes, times ISO 8601 in UTC.
  • Forums, topics, replies, comments, reviews and polls are content services over the classes configured in settings/expservices.ini [Community] (ForumClass=forum, TopicClass=forum_topic, ReplyClass=forum_reply, CommentClass=comment, PollClass=poll, ReviewClass=review, TopicStickyAttribute=sticky), so a site with other class identifiers changes the INI, not the code. Create, edit and remove follow the content policies of the caller (the owner limitation lets members edit and remove their own posts); the text fields of a class are written as POST fields of the attribute's name or as one fields JSON object (rich text attributes take plain text and store paragraphs); unknown fields are refused (422), required ones checked. Moderation is hide/unhide (policy content/hide), hidden lists the hidden comments, a sticky topic stays on top, a topic can be moved to another forum. Removal goes to the trash.
  • Collected forms and polls use the information collection of the kernel. expinfocollection::submit does what content/collectinformation does (validation by the datatype, anonymous collection and unique/overwrite/multiple handling from collect.ini) for text, number, boolean and choice fields; reading, summary, CSV export and removal need the infocollector/read policy (the CSV neutralises formulas). A poll vote is one collection: exppoll::vote answers the results; the poll class is unique per user in the standard collect.ini.
  • Feeds: expfeed::exports and output serve the RSS exports of the site as the reader gets them (RSS 1.0, 2.0, Atom, OPML, iTunes; data.content_type and data.content), rss, atom and json build RSS 2.0, Atom 1.0 and JSON Feed 1.1 of the newest content below any node the caller may read (classes limits the classes, limit is capped at 100), discover lists the feed addresses for a node. Export management (rss/edit) creates active exports directly (the draft/publish copy of the admin editor is not used); RSS imports are read (imports, importStatus, importCheck) and switched (importSetActive); runImport (rss/edit, POST) runs one import with the code of the rssimport cronjob (fetch, create and publish the new items below the destination, once per item, known by the item's link or guid) and answers found, new, created and one line per item (imported, would_import, exists, no_identifier, skipped_max_items) plus the log the cronjob would print. With dry_run=1 nothing is created: it only reports what would be imported. max_items limits the new items of one run (0 = all). The caller also needs the right to create below the destination node, a draft import cannot run (409), and importCheck and runImport fetch only http/https addresses. A real run records data.import.rss as the cronjob does, besides service.feed.runimport.
  • Writes are POST with the form token (expsession::token) and recorded as service.<domain>.<method>; where the kernel records its own event for the change, it is recorded as well.
  • Known kernel fault, worked around: eZVatRule::removeVatRule() calls an instance method statically, which PHP 8 refuses; expvat::removeRule and removeType remove the rules themselves (with the same audit event).

Shop

Products (expproduct, 19 services)
ServiceAccessArgumentsWhat it does
classespublicThe product classes: content classes with a price attribute
listpublicparent_node_id:int, limit:int, offset:int, currency:string, sort:string name/published/priceThe products below a node, newest or by name, with prices (optionally in a currency)
countpublicparent_node_id:intHow many products are below a node
searchpublictext:string, limit:int, offset:int, currency:stringProducts by words in name, number or description
viewpublicnode_id:int, currency:stringOne product by node id with its fields, prices, options
viewByObjectpublicobject_id:int, currency:stringOne product by object id
isProductpublicnode_id:intWhether a node is a product
pricepublicnode_id:int, currency:stringThe price of a product, optionally converted into a currency
pricespublicnode_id:intThe price of a product in every active currency with a rate
optionspublicnode_id:intThe options (ezoption/ezmultioption) a buyer can choose, with additional prices
variationspublicnode_id:int, limit:intEvery combination of the option groups with its resulting price
optionPricepublicnode_id:int, choices:jsonThe price of a product with chosen options (option attribute id => choice id as JSON)
latestpubliclimit:int, currency:stringThe most recently published products
categoriespublicparent_node_id:intThe product categories: nodes below a node that hold products, with their product count
priceRangepublicparent_node_id:intThe lowest, highest and average price below a node
byNumberpublicnumber:string, currency:stringA product by its product number
relatedpublicnode_id:int, limit:intOther products of the same parent
vatpublicnode_id:intThe VAT of a product: the type selected and the percentage
setPrice (POST)content/editnode_id:int, price:string POSTSets the price of a product (publishes a new version)
Basket and checkout (expbasket, 25 services)
ServiceAccessArgumentsWhat it does
viewpublicThe basket of this session: lines, options, totals per VAT rate, currency
itemspublicThe lines of the basket
itempublicitem_id:intOne line of the basket
countpublicNumber of lines and units in the basket
totalspublicTotals with and without VAT, VAT per rate, shipping
isEmptypublicWhether the basket has no lines
currencypublicThe currency of the basket and the preferred one of the user
canAddpublicobject_id:intWhether a product can be put in this basket (type and currency compatible)
add (POST)shop/buyobject_id:int POST, quantity:int POST, options:json POSTPuts a product in the basket (POST fields object_id, quantity, options as JSON attribute id => choice id)
addNode (POST)shop/buynode_id:int POST, quantity:int POST, options:json POSTPuts a product in the basket by its node id
update (POST)shop/buyitem_id:int POST, quantity:int POSTSets the quantity of one line (0 removes it)
updateMany (POST)shop/buyquantities:json POSTSets the quantities of several lines: JSON object item id => quantity
remove (POST)shop/buyitem_id:int POSTRemoves one line
empty (POST)shop/buyRemoves every line
refreshPrices (POST)shop/buyBrings the prices of the lines up to date with the products
setCurrency (POST)usercurrency:string POSTSets the preferred currency of the user for new lines
checkoutStatuspublicWhat is needed before the checkout: lines, login, country, account handler, VAT known
accountInfoshop/buyThe account information of the current checkout (the temporary order)
startCheckout (POST)shop/buyCreates the temporary order of the basket (step 1); nothing is charged
reviewshop/buyThe temporary order with shipping and VAT as the confirm step shows it
cancelCheckout (POST)shop/buyCancels the temporary order and returns to the basket
placeOrder (POST)shop/buyRuns the checkout operation of the temporary order: activates it or hands over to the payment gateway
adminListshop/administratelimit:int, offset:intBaskets of all sessions with their size and age
adminViewshop/administratebasket_id:intOne basket of any session
adminCleanup (POST)shop/administratedays:int POSTRemoves the baskets of sessions that are gone and older than the given days
Orders (exporder, 24 services)
ServiceAccessArgumentsWhat it does
mineuserlimit:int, offset:intThe orders of the logged-in customer, newest first
myOrderuserorder_id:intOne order of the logged-in customer with lines, extra order items and status history
myCountuserHow many orders the logged-in customer has
myReceiptuserorder_id:intThe receipt link and totals of one of the customer's orders
statusesshop/administrateall:boolThe order statuses
listshop/administratelimit:int, offset:int, show:string normal/archived/all, sort:string created/id/total/user_name, order:string asc/desc, status_id:intOrders of all customers, filterable by status, archived and sorted
countshop/administrateshow:string normal/archived/allNumber of orders
viewshop/administrateorder_id:intOne order by id with lines, order items and status history
byNumbershop/administrateorder_nr:intOne order by its order number
itemsshop/administrateorder_id:intThe product lines of an order
orderItemsshop/administrateorder_id:intThe extra items of an order: shipping, discounts
historyshop/administrateorder_id:intThe status history of an order
accountshop/administrateorder_id:intThe customer account information of an order
statusOptionsshop/setstatusorder_id:intThe statuses the current user may set an order to (policy FromStatus/ToStatus)
setStatus (POST)shop/setstatusorder_id:int POST, status_id:int POSTChanges the status of an order
archive (POST)shop/administrateorder_id:int POSTMoves an order to the archive
unarchive (POST)shop/administrateorder_id:int POSTBrings an archived order back
remove (POST)shop/administrateorder_id:int POSTDeletes an order with its lines and history (audited)
searchshop/administratetext:string, limit:int, offset:intOrders by customer email or account name
statisticsshop/administrateyear:int, month:intProducts sold in a year or month with totals
customersshop/administratelimit:int, offset:intThe customers (distinct emails) with their order count and sum
customerOrdersshop/administrateuser_id:int, email:stringThe orders of one customer by user id
customerProductsshop/administrateuser_id:int, email:stringThe products one customer bought
dashboardshop/administrateOrder counts per status and the latest orders
VAT (expvat, 19 services)
ServiceAccessArgumentsWhat it does
typespublicskip_dynamic:boolThe VAT types with their percentage
typepublicid:intOne VAT type
typeUsageshop/administrateid:intHow many products, product classes and rules use a VAT type
createType (POST)shop/administratename:string POST, percentage:string POSTCreates a VAT type
updateType (POST)shop/administrateid:int POST, name:string POST, percentage:string POSTChanges the name or percentage of a VAT type
removeType (POST)shop/administrateid:int POSTRemoves a VAT type, its rules; products fall back to the default of their class
rulesshop/administratelimit:int, offset:intThe VAT charging rules
ruleshop/administrateid:intOne VAT charging rule
createRule (POST)shop/administratecountry_code:string POST, vat_type:int POST, categories:list POSTCreates a rule: country code (or Any), VAT type id, product category ids
updateRule (POST)shop/administrateid:int POST, country_code:string POST, vat_type:int POST, categories:list POSTChanges a rule
removeRule (POST)shop/administrateid:int POSTRemoves a rule
categoriesshop/administrateThe product categories the rules refer to, with their product count
createCategory (POST)shop/administratename:string POSTCreates a product category
removeCategory (POST)shop/administrateid:int POSTRemoves a product category and its references
forProductpublicnode_id:int, country:stringThe VAT percent of a product for a country (dynamic VAT handler), or the product's own
settingspublicThe VAT settings of the shop: dynamic charging, handler, country requirement
userCountrypublicThe country used for the VAT of the current user
setUserCountry (POST)publiccountry:string POSTSets the preferred country of the current session for the VAT
countriespublicThe country codes with VAT rules and the countries of the installation
Currencies (expcurrency, 19 services)
ServiceAccessArgumentsWhat it does
listpubliconly_active:boolThe currencies with symbol, status and rates
viewpubliccode:stringOne currency by its code
countpublicHow many currencies exist
codespublicThe codes of the active currencies
existspubliccode:stringWhether a currency code exists
baseCurrencypublicThe base currency of the exchange rates and the shop's default
ratepubliccode:stringThe rate of a currency against the base
crossRatepublicfrom:string, to:stringThe rate between two currencies
convertpublicamount:string, from:string, to:stringConverts an amount from one currency to another with the rates and rounding of the shop
roundingpublicThe rounding settings of conversions
preferredpublicThe preferred currency of the current user
setPreferred (POST)usercode:string POSTSets the preferred currency of the current user
create (POST)shop/setupcode:string POST, symbol:string POST, locale:string POST, custom_rate:string POST, rate_factor:string POST, status:string POST active/inactiveCreates a currency
update (POST)shop/setupcode:string POST, symbol:string POST, locale:string POST, custom_rate:string POST, rate_factor:string POST, status:string POSTChanges symbol, locale, rates, factor or status of a currency
setStatus (POST)shop/setupcode:string POST, status:string POST active/inactiveActivates or deactivates a currency
remove (POST)shop/setupcode:string POSTRemoves a currency
updateRates (POST)shop/setupFetches the automatic rates from the configured provider and stores them
updateAutoprices (POST)shop/setupRecalculates the automatic prices of multi price products from the rates
providersshop/setupThe exchange rate settings: provider, server, base
Discounts (expdiscount, 16 services)
ServiceAccessArgumentsWhat it does
groupsshop/administratelimit:int, offset:intThe discount groups with their rule and member counts
groupshop/administrateid:intOne discount group with its rules and members
createGroup (POST)shop/administratename:string POSTCreates a discount group
renameGroup (POST)shop/administrateid:int POST, name:string POSTRenames a discount group
removeGroup (POST)shop/administrateid:int POSTRemoves a discount group with its rules and memberships
rulesshop/administrategroup_id:intThe rules of a discount group
ruleshop/administrateid:intOne rule with its limitations
createRule (POST)shop/administrategroup_id:int POST, name:string POST, percent:string POST, classes:list POST, sections:list POST, products:list POSTCreates a rule in a group: percent and optional class/section/product limits
updateRule (POST)shop/administrateid:int POST, name:string POST, percent:string POST, classes:list POST, sections:list POST, products:list POSTChanges name, percent or limits of a rule
removeRule (POST)shop/administrateid:int POSTRemoves a rule
membersshop/administrategroup_id:intThe users and user groups of a discount group
addMember (POST)shop/administrategroup_id:int POST, object_id:int POSTAdds a user or user group (object id) to a discount group
removeMember (POST)shop/administrategroup_id:int POST, object_id:int POSTRemoves a user or user group from a discount group
groupsOfUsershop/administrateobject_id:intThe discount groups a user or user group belongs to
forUsershop/administrateuser_id:int, node_id:intThe best discount percent of a user for a product (class, section, object)
mineusernode_id:intThe discount percent the logged-in user gets for a product
Wish lists (expwishlist, 11 services)
ServiceAccessArgumentsWhat it does
viewuserThe wish list of the logged-in user with its lines
itemsuserlimit:int, offset:intThe lines of the wish list
countuserHow many lines the wish list has
containsuserobject_id:intWhether a product is on the wish list
add (POST)userobject_id:int POST, options:json POSTPuts a product on the wish list (options as JSON attribute id => choice id)
remove (POST)useritem_id:int POSTRemoves one line
empty (POST)userRemoves every line
moveToBasket (POST)shop/buyitem_id:int POST, quantity:int POSTPuts a wish list line in the basket and removes it from the list
adminListshop/administratelimit:int, offset:intThe wish lists of all users with their size
adminViewshop/administrateuser_id:intThe wish list of one user by object id
popularshop/administratelimit:intThe products on most wish lists
Shipping (expshipping, 8 services)
ServiceAccessArgumentsWhat it does
statuspublicWhether shipping is configured: handler, simple shipping workflow
handlershop/administrateThe shipping handler of shop.ini and where it is searched
simpleShippingpublicThe cost and description of the simple shipping workflow
basketpublicThe shipping info of the current basket
ordershop/administrateorder_id:intThe shipping line of an order (order items of shipping type)
basketInfoHandlerpublicThe basket info handler that calculates totals and shipping
vatOfShippingpublicThe VAT split of a shipping cost (the VAT of the products it is spread over)
settingsshop/administrateThe ShippingSettings and BasketInfoSettings of shop.ini
Payments (exppayment, 10 services)
ServiceAccessArgumentsWhat it does
listshop/administratelimit:int, offset:int, status:string all/pending/approvedThe payment objects, newest first, optionally only pending or approved
viewshop/administrateid:intOne payment object by id
byOrdershop/administrateorder_id:intThe payment of an order
myStatususerorder_id:intThe payment status of one of the logged-in customer's orders
countsshop/administrateHow many payments are pending and approved
gatewaysshop/administrateThe payment gateways available and the directories searched
workflowEventsshop/administrateThe workflow events of the payment gateway type, with their gateway
handlerspublicThe account handler and confirm order handler of the checkout
methodspublicWhat a client can offer as payment: enabled gateways, or the manual method
approve (POST)shop/administrateid:int POSTApproves a payment and continues its workflow (audited as commerce.payment.approve)

Forms, polls and community

Collected forms (expinfocollection, 12 services)
ServiceAccessArgumentsWhat it does
formsinfocollector/readlimit:int, offset:intThe content objects that have collected data, with the number of collections
collectionsinfocollector/readobject_id:int, limit:int, offset:intThe collections of one form, newest first
viewinfocollector/readcollection_id:intOne collection with its values
countinfocollector/readobject_id:intHow many collections a form (or all forms) has
fieldspublicnode_id:intThe fields of a form: the attributes that collect information, with type and requirement
summaryinfocollector/readobject_id:intPer field of a form: how many values and, for choice and number fields, the distribution
mineuserlimit:int, offset:intThe collections the logged-in user submitted
exportinfocollector/readobject_id:intAll collections of a form as CSV text (audited as a data export by the kernel when enabled)
submit (POST)publicobject_id:int POST, fields:json POSTSubmits a form: POST object_id and fields as JSON identifier => value (text, number, boolean, choice id)
remove (POST)infocollector/readcollection_id:int POSTRemoves one collection (audited as data.infocollection.remove)
removeAll (POST)infocollector/readobject_id:int POSTRemoves every collection of a form
settingspublicnode_id:intHow collection is handled for a form: anonymous allowed, unique/overwrite/multiple, template type
Polls (exppoll, 11 services)
ServiceAccessArgumentsWhat it does
listpublicparent_node_id:int, limit:int, offset:intThe polls below a node with their question and number of votes
viewpublicnode_id:intOne poll: question, choices, votes per choice
resultspublicnode_id:intThe results of a poll: votes and percent per choice
choicespublicnode_id:intThe choices of a poll
canVotepublicnode_id:intWhether the current visitor may vote now, and why not
myVotepublicnode_id:intThe choice the current visitor voted for
vote (POST)publicnode_id:int POST, choice:int POSTVotes for a choice (POST node_id, choice)
latestpubliclimit:intThe newest polls
create (POST)content/createparent_node_id:int POST, name:string POST, question:string POST, choices:list POSTCreates a poll under a node: name, question and choices
remove (POST)content/removenode_id:int POSTRemoves a poll (to the trash) with its votes
resetVotes (POST)infocollector/readnode_id:int POSTRemoves every vote of a poll
Forums (expforum, 13 services)
ServiceAccessArgumentsWhat it does
containerspubliclimit:int, offset:intThe forum containers (the "forums" class) of the site
listpublicparent_node_id:int, limit:int, offset:intThe forums below a node (a container, or the content root: all forums)
viewpublicnode_id:intOne forum with its topic and reply counts and the latest topic
statspublicnode_id:intTopics, replies and the latest activity of a forum
latestpubliclimit:intThe latest topics over all forums
searchpublictext:string, limit:int, offset:intWords in forum names and descriptions
pathpublicnode_id:intThe path (container, forum) of a node for a breadcrumb
fieldspublicThe fields a forum has and the classes in use
create (POST)content/createparent_node_id:int POST, fields:json POSTCreates a forum in a container (fields as JSON or POST fields: name, description)
edit (POST)content/editnode_id:int POST, fields:json POSTChanges the fields of a forum
remove (POST)content/removenode_id:int POSTRemoves a forum with its topics to the trash
hide (POST)content/hidenode_id:int POSTHides a forum from visitors
unhide (POST)content/hidenode_id:int POSTShows a hidden forum again
Topics (exptopic, 17 services)
ServiceAccessArgumentsWhat it does
listpublicforum_node_id:int, limit:int, offset:intThe topics of a forum, sticky ones first, then newest first
viewpublicnode_id:intOne topic with its text and reply count
countpublicforum_node_id:intHow many topics a forum has
latestpubliclimit:int, forum_node_id:intThe latest topics, in one forum or everywhere
stickypublicforum_node_id:intThe sticky topics of a forum
byUserpublicuser_id:int, limit:int, offset:intThe topics a user started
mineuserlimit:int, offset:intThe topics the logged-in user started
searchpublictext:string, forum_node_id:int, limit:int, offset:intWords in topic subjects and texts
lastReplypublicnode_id:intThe newest reply of a topic
canCreatepublicforum_node_id:intWhether the current user may start a topic in a forum
create (POST)content/createforum_node_id:int POST, fields:json POSTStarts a topic in a forum (fields subject, message, sticky as JSON or POST fields)
edit (POST)content/editnode_id:int POST, fields:json POSTChanges subject or message of a topic
remove (POST)content/removenode_id:int POSTRemoves a topic with its replies to the trash
setSticky (POST)content/editnode_id:int POST, sticky:bool POSTMakes a topic sticky or not (moderation)
move (POST)content/editnode_id:int POST, forum_node_id:int POSTMoves a topic to another forum
hide (POST)content/hidenode_id:int POSTHides a topic with its replies from visitors (moderation)
unhide (POST)content/hidenode_id:int POSTShows a hidden topic again
Replies (expreply, 13 services)
ServiceAccessArgumentsWhat it does
listpublictopic_node_id:int, limit:int, offset:int, newest_first:boolThe replies of a topic, oldest first
viewpublicnode_id:intOne reply
countpublictopic_node_id:intHow many replies a topic has
latestpubliclimit:int, parent_node_id:intThe latest replies, everywhere or below a node
byUserpublicuser_id:int, limit:int, offset:intThe replies a user wrote
mineuserlimit:int, offset:intThe replies the logged-in user wrote
searchpublictext:string, topic_node_id:int, limit:int, offset:intWords in reply subjects and texts
quotepublicnode_id:intA reply as a quote to start an answer from: "Re: subject" and the quoted text
create (POST)content/createtopic_node_id:int POST, fields:json POSTWrites a reply to a topic (fields subject, message as JSON or POST fields)
edit (POST)content/editnode_id:int POST, fields:json POSTChanges a reply
remove (POST)content/removenode_id:int POSTRemoves a reply to the trash
hide (POST)content/hidenode_id:int POSTHides a reply (moderation)
unhide (POST)content/hidenode_id:int POSTShows a hidden reply again
Comments and reviews (expcomment, 17 services)
ServiceAccessArgumentsWhat it does
listpublicnode_id:int, limit:int, offset:int, newest_first:boolThe comments below a node, oldest first or newest first
viewpublicnode_id:intOne comment
countpublicnode_id:intHow many comments a node has
latestpubliclimit:int, parent_node_id:intThe latest comments, everywhere or below a node
byUserpublicuser_id:int, limit:int, offset:intThe comments a user wrote
mineuserlimit:int, offset:intThe comments the logged-in user wrote
searchpublictext:string, parent_node_id:int, limit:int, offset:intWords in comment subjects, authors and texts
hiddencontent/hideparent_node_id:int, limit:int, offset:intThe hidden comments below a node (moderation queue)
canCommentpublicnode_id:intWhether the current user may comment on a node
create (POST)content/createnode_id:int POST, fields:json POSTComments on a node (fields subject, author, message as JSON or POST fields)
edit (POST)content/editnode_id:int POST, fields:json POSTChanges a comment
remove (POST)content/removenode_id:int POSTRemoves a comment to the trash
hide (POST)content/hidenode_id:int POSTHides a comment (moderation)
unhide (POST)content/hidenode_id:int POSTShows a hidden comment again
reviewspublicnode_id:int, limit:int, offset:intThe reviews of a product with the average rating
ratingSummarypublicnode_id:intAverage rating and the count per star of a product
addReview (POST)content/createnode_id:int POST, fields:json POSTReviews a product (fields title, rating 1-5, author, body)

Feeds

Feeds (expfeed, 22 services)
ServiceAccessArgumentsWhat it does
exportspubliclimit:int, offset:int, only_active:boolThe RSS exports with their address, format and number of items
exportpublicid:intOne RSS export with its sources
exportByUrlpublicaccess_url:stringOne RSS export by its access URL name
outputpublicaccess_url:stringThe feed document of an RSS export as the reader gets it (RSS 1.0, 2.0, Atom, OPML, iTunes)
formatspublicThe export formats and their names
rsspublicnode_id:int, limit:int, classes:listAn RSS 2.0 feed of the latest content below a node
atompublicnode_id:int, limit:int, classes:listAn Atom 1.0 feed of the latest content below a node
jsonpublicnode_id:int, limit:int, classes:listA JSON Feed 1.1 of the latest content below a node
itemspublicnode_id:int, limit:int, offset:int, classes:listThe feed items of a node as plain data (title, url, date, summary)
discoverpublicnode_id:intThe feed addresses a site offers for a node: the generic ones and the exports that cover it
createExport (POST)rss/edittitle:string POST, access_url:string POST, source_node_id:int POST, rss_version:string POST 1.0/2.0/ATOM, number_of_objects:int POST, description:string POSTCreates an active RSS export of a subtree
updateExport (POST)rss/editid:int POST, title:string POST, description:string POST, number_of_objects:int POST, rss_version:string POST, main_node_only:bool POSTChanges title, description, size or format of an export
setActive (POST)rss/editid:int POST, active:bool POSTSwitches an export on or off
addSource (POST)rss/editid:int POST, source_node_id:int POST, subnodes:bool POSTAdds a source subtree to an export
removeSource (POST)rss/editid:int POST, source_id:int POSTRemoves a source from an export
removeExport (POST)rss/editid:int POSTRemoves an export with its sources
importsrss/editlimit:int, offset:intThe RSS imports with their source URL, destination and state
importrss/editid:intOne RSS import
importStatusrss/editid:intWhat an import brought in: objects created by it, the newest one and when
importCheckrss/editid:int, fetch:boolWhether the source of an import is an address the server may fetch, and optionally its feed version (fetches the source)
importSetActive (POST)rss/editid:int POST, active:bool POSTSwitches an import on or off (the rssimport cronjob runs the active ones)
runImport (POST)rss/editid:int POST, dry_run:bool POST, max_items:int POSTRuns one RSS import the way the rssimport cronjob does; dry_run only reports what would be imported

Total: 256 services in 16 domains (84 writes).

Content services: 350 services in 15 domains (112 writes)

Written by the content part of expservices: extension/expservices/classes/content/ (one class per domain over the shared expContentServiceBase: fetch helpers with the access checks, the exporters that turn nodes, objects, versions, classes, sections and states into plain arrays, attribute values per datatype, sort and filter parsing, and the now-or-job routing). Tests: tests/tests/extension/expservices/content/ (live database like the other domains: reads on the stable nodes, writes on test content under the Media root node 43 and on test classes, class groups, sections, state groups, aliases and wildcards that tearDown removes; never a test database). The sample calls were also verified over HTTP on Apache with a signed-in admin session (reads, a refused write without the token, then create, update and remove of a test folder).

How the content services work, beyond the rules above:

  • Policies with limitations. The access column is the policy the call needs at all (content/read, content/edit, ...); inside, every call checks the node or object the way the kernel does (canRead, canEdit, canRemove, canMoveFrom, canCreateClassList, canAssignSectionToObject, the allowed state list ...), so a limitation by class, section, subtree, owner or state applies to the services as it does in the admin. Lists use the kernel fetch functions with their permission SQL, so a user only counts and pages what they may read.
  • Identifiers. Nodes, objects, versions, sections and states are addressed by number; classes by id or identifier; state groups by id or identifier; languages by locale code (eng-GB).
  • Attribute input. Writes take attribute values as a JSON object identifier => string, in the string form the datatype reads (fromString(): see expattribute::string). Datatypes whose string form is a file path or a credential (ezimage, ezbinaryfile, ezmedia, ezuser, ezpassword) are refused with 422: a remote caller must never make the server read a local path. Required attributes must be given on create. Values come back per datatype (text, number, boolean, date with ISO form, XML text as xml and html, relations as ids, images with their original URL).
  • Lists answer the paged envelope (data items, meta total, offset, limit, count, has_more). Node lists take sort (path, published, modified, section, depth, class_identifier, class_name, priority, name, modified_subnode, node_id, contentobject_id), order (asc, desc), limit, offset and a JSON filter: class[], exclude_class[], section, state[], owner, name (prefix), language, from and to (published), depth, hidden.
  • Now or job. Large operations (move, copy of a subtree, hide, reveal, remove of subtrees, section and state of a subtree, add and remove locations) take the POST field mode: now runs in the request (refused above content.ini NowLimit nodes, answer 422), job creates a content job and starts its worker, auto (the default) picks by SynchronousLimit. A job answer is { job_id, type, state, spawned } with meta.mode = job; follow it with expcontentjob::progress::<job_id>. A subtree locked by a running job answers 409 to a synchronous write.
  • Drafts of classes are the temporary version of the class, the one the admin class editor works on: editDraft or createDraft, change, then publishDraft (the class edit handler updates existing objects) or discardDraft.
  • Removal. remove takes move_to_trash (default 1); the root nodes cannot be removed; exptrash::purge, purgeMany and emptyTrash (POST confirm=yes) remove for good through the kernel trash service. Classes with objects, sections in use, state groups with the ez prefix and the last location of an object are refused with 409 or 403.
  • Audit. Every successful write records service.<domain>.<method>; the kernel records its own content.* events for the operations the services call (create, publish, move, remove, section, state, translation, class, URL alias).

Errors: 400 bad argument, 401 not signed in, 403 denied (policy, method or form token), 404 not found, 409 conflict (exists, locked, in use), 422 not valid (input refused by a datatype or a rule).

Example: create an article, change it, and follow a large move as a job (shell, after expsession::login):

curl -b jar -c jar --data-urlencode 'class=article' --data-urlencode 'attributes={"title":"Hello","intro":"First"}' \
     --data-urlencode "ezxform_token=$TOKEN" "$BASE/ezjscore/call/expnode::create::43?ContentType=json"
curl -b jar -c jar --data-urlencode 'attributes={"title":"Hello again"}' --data-urlencode "ezxform_token=$TOKEN" \
     "$BASE/ezjscore/call/expnode::update::<node>?ContentType=json"
curl -b jar -c jar --data-urlencode 'mode=job' --data-urlencode "ezxform_token=$TOKEN" \
     "$BASE/ezjscore/call/expnode::move::<node>::<new-parent>?ContentType=json"      # { job_id: ... }
curl -b jar "$BASE/ezjscore/call/expcontentjob::progress::<job_id>?ContentType=json"

Nodes (expnode, 49 services, 16 writes)

The content tree: fetch by id, remote id, path and object; children and subtrees paged, sorted and filtered; counts, ancestors, siblings, data maps, rights; create, update, move, copy, hide, swap, remove, sort and priority.

CallAccessWriteSummary
expnode::get::<node_id>content/readA node with its object
expnode::getMany::<node_ids>content/readSeveral nodes by id (unreadable and missing ones are left out)
expnode::getByRemoteId::<remote_id>content/readA node by its remote id
expnode::getByPath::<path>content/readA node by its URL alias, e.g. Media/Images
expnode::getByObject::<object_id>content/readThe main node of an object
expnode::exists::<node_id>userWhether a node exists and is readable
expnode::children::<node_id>::<sort>::<order>::<limit>::<offset>::<filter>content/readChildren of a node, paged, sorted and filtered
expnode::childrenCount::<node_id>::<filter>content/readNumber of children
expnode::childrenNames::<node_id>::<limit>::<offset>content/readLight list of children: node id, object id, class, name
expnode::subtree::<node_id>::<sort>::<order>::<limit>::<offset>::<filter>content/readDescendants of a node, paged, sorted and filtered (filter.depth limits the depth)
expnode::subtreeCount::<node_id>::<filter>content/readNumber of descendants
expnode::byClass::<node_id>::<classes>::<limit>::<offset>content/readDescendants of one or more classes
expnode::countByClass::<node_id>::<depth>content/readFacet: number of descendants per class (needs unrestricted read)
expnode::latest::<node_id>::<limit>::<classes>content/readMost recently published descendants
expnode::modifiedSince::<node_id>::<since>::<limit>::<offset>content/readDescendants published since a time (timestamp or date)
expnode::find::<node_id>::<name>::<limit>::<offset>content/readDescendants whose name starts with a text
expnode::hidden::<node_id>::<limit>::<offset>content/readHidden nodes below a node
expnode::tree::<node_id>::<depth>::<per_level>content/readNested children down to a depth, limited per level
expnode::path::<node_id>content/readThe ancestors of a node from the root, with the node itself
expnode::breadcrumb::<node_id>content/readLight ancestors list: node id, name, url alias
expnode::parent::<node_id>content/readThe parent node
expnode::siblings::<node_id>::<limit>::<offset>content/readOther children of the parent, paged
expnode::neighbours::<node_id>content/readPrevious and next sibling in the parent sort order
expnode::dataMap::<node_id>::<language>content/readThe attributes of the node object with their values
expnode::attribute::<node_id>::<identifier>::<language>content/readOne attribute of the node object
expnode::names::<node_id>content/readThe node name in every language
expnode::url::<node_id>content/readURL alias and system URL of a node
expnode::sortInfo::<node_id>content/readSort field and order of the children, and the names allowed
expnode::visibility::<node_id>content/readHidden and invisible flags with the nearest hidden ancestor
expnode::rights::<node_id>userWhat the current user can do with the node
expnode::creatableClasses::<node_id>content/createClasses the current user can create below the node
expnode::locations::<node_id>content/readAll nodes of the same object
expnode::pendingDrafts::<node_id>::<limit>::<offset>content/readDrafts waiting to be published below a node
expnode::create::<parent_node_id>content/createPOSTCreates and publishes an object below a node. POST: class, attributes (json identifier => string), language, remote_id
expnode::update::<node_id>content/editPOSTChanges attributes of the node object and publishes a new version. POST: attributes (json), language
expnode::rename::<node_id>content/editPOSTRenames the object of a node. POST: name
expnode::move::<node_id>::<new_parent_node_id>content/movePOSTMoves a node and its subtree. POST: mode
expnode::copy::<node_id>::<new_parent_node_id>content/createPOSTCopies one node (object) below a node
expnode::copySubtree::<node_id>::<new_parent_node_id>content/createPOSTCopies a node with all its descendants. POST: mode, all_versions, keep_creator, keep_time
expnode::hide::<node_id>content/hidePOSTHides a node and its subtree. POST: mode
expnode::reveal::<node_id>content/hidePOSTReveals a hidden node and its subtree. POST: mode
expnode::toggleHide::<node_id>content/hidePOSTHides a visible node, reveals a hidden one
expnode::swap::<node_id>::<other_node_id>content/editPOSTSwaps two nodes (their objects change places)
expnode::remove::<node_id>content/removePOSTRemoves a node and its subtree. POST: move_to_trash (default 1), mode
expnode::removeManycontent/removePOSTRemoves several nodes. POST: node_ids, move_to_trash, mode
expnode::setSort::<node_id>::<field>::<order>content/editPOSTSets how the children are sorted
expnode::setPriority::<node_id>::<priority>content/editPOSTSets the priority of a node
expnode::setPriorities::<parent_node_id>content/editPOSTSets the priorities of children. POST: priorities (json node id => priority)
expnode::setRemoteId::<node_id>::<remote_id>content/editPOSTSets the remote id of a node

Objects (expobject, 36 services, 11 writes)

Content objects: lists by class and owner, data maps and attributes with their values, names, owner, class, section, states, locations, languages; create, update (a new published version), rename, owner, languages, copy, remove.

CallAccessWriteSummary
expobject::get::<object_id>content/readAn object with its languages and node ids
expobject::getByRemoteId::<remote_id>content/readAn object by its remote id
expobject::exists::<object_id>userWhether an object exists and is readable
expobject::list::<sort>::<order>::<limit>::<offset>::<filter>content/readObjects (main nodes) paged, sorted and filtered
expobject::count::<filter>content/readNumber of objects matching a filter
expobject::byClass::<class>::<limit>::<offset>content/readObjects of a class
expobject::byOwner::<owner_id>::<limit>::<offset>content/readObjects owned by an object (user) id
expobject::recent::<limit>::<classes>content/readMost recently published objects
expobject::recentlyModified::<limit>::<classes>content/readMost recently modified objects
expobject::dataMap::<object_id>::<version>::<language>content/readThe attributes of an object version with their values
expobject::attributes::<object_id>::<language>content/readThe attributes of an object without values
expobject::attribute::<object_id>::<identifier>::<language>content/readOne attribute of an object
expobject::name::<object_id>::<language>content/readThe name of an object in a language
expobject::names::<object_id>content/readThe name of an object in every language
expobject::owner::<object_id>content/readThe owner object of an object
expobject::class::<object_id>content/readThe class of an object with its attributes
expobject::section::<object_id>content/readThe section of an object
expobject::states::<object_id>content/readThe states of an object
expobject::nodes::<object_id>content/readAll nodes (locations) of an object
expobject::mainNode::<object_id>content/readThe main node of an object
expobject::languages::<object_id>content/readLanguages the object exists in, initial and always available
expobject::versionCount::<object_id>content/readNumber of versions
expobject::rights::<object_id>userWhat the current user can do with the object
expobject::url::<object_id>content/readURL aliases of the main node of an object
expobject::summary::<object_id>content/readLight card of an object: id, name, class, main node, modified
expobject::createcontent/createPOSTCreates and publishes an object. POST: parent_node_id, class, attributes (json), language, remote_id
expobject::update::<object_id>content/editPOSTChanges attributes and publishes a new version. POST: attributes (json), language
expobject::rename::<object_id>content/editPOSTRenames an object. POST: name
expobject::setRemoteId::<object_id>::<remote_id>content/editPOSTSets the remote id of an object
expobject::setOwner::<object_id>::<owner_id>content/editPOSTChanges the owner of an object (needs unrestricted edit access)
expobject::setInitialLanguage::<object_id>::<language>content/translatePOSTChanges the initial language of an object
expobject::setAlwaysAvailable::<object_id>::<value>content/translatePOSTSets whether the object is shown in languages it has no translation for
expobject::copy::<object_id>::<new_parent_node_id>content/createPOSTCopies the object of a main node below a node
expobject::remove::<object_id>content/removePOSTRemoves an object with all its nodes. POST: move_to_trash (default 1), mode
expobject::expireCache::<object_id>content/editPOSTClears the view caches of an object
expobject::cleanupDrafts::<object_id>content/editPOSTRemoves the current user's internal drafts of an object

Classes (expclass, 37 services, 12 writes)

Content class definitions and the class editor as services: reads of classes, attributes, groups, usage and datatypes; draft workflow (createDraft or editDraft, change the draft and its attributes, publishDraft or discardDraft), copy and remove.

CallAccessWriteSummary
expclass::list::<group_id>::<limit>::<offset>class/*Content classes, optionally of one group
expclass::countclass/*Number of classes
expclass::identifiersuserLight map of class id => identifier and name
expclass::get::<class>class/*A class with its attributes and groups (id or identifier)
expclass::getByRemoteId::<remote_id>class/*A class by its remote id
expclass::search::<text>::<limit>::<offset>class/*Classes whose name or identifier contains a text
expclass::attributes::<class>class/*The attributes of a class in order
expclass::attribute::<attribute_id>class/*One class attribute by id
expclass::attributeByIdentifier::<class>::<identifier>class/*One class attribute by class and identifier
expclass::searchableAttributes::<class>class/*The searchable attributes of a class
expclass::requiredAttributes::<class>class/*The required attributes of a class
expclass::collectorAttributes::<class>class/*The information collector attributes of a class
expclass::groups::<class>class/*The groups a class is in
expclass::objectCount::<class>class/*Number of objects of a class
expclass::usage::<limit>::<offset>class/*Classes with their object counts, most used first
expclass::containersclass/*Classes that are containers
expclass::names::<class>class/*The name of a class in every language
expclass::languages::<class>class/*Languages a class has names in
expclass::patterns::<class>class/*Object name and URL alias patterns of a class
expclass::removable::<class>class/*Whether the class can be removed and what blocks it
expclass::canInstantiateuserClasses the current user may create objects of
expclass::draftsclass/*Class drafts (temporary versions) being edited
expclass::draft::<class_id>class/*A class draft with its attributes
expclass::dataTypesclass/*The datatypes available for class attributes
expclass::sortFieldsuserThe names accepted for the default sort of children
expclass::createDraftclass/*POSTStarts a new class draft. POST: name, identifier, group_id, language, description, is_container, always_available, object_name_pattern, url_alias_pattern, sort_field, sort_order
expclass::editDraft::<class_id>class/*POSTStarts or continues a draft of an existing class
expclass::updateDraft::<class_id>class/*POSTChanges the class draft fields (same POST fields as createDraft)
expclass::addAttributeDraft::<class_id>class/*POSTAdds an attribute to the draft. POST: data_type, identifier, name, description, is_required, is_searchable, can_translate, is_information_collector, language
expclass::updateAttributeDraft::<class_id>::<attribute_id>class/*POSTChanges an attribute of the draft (same POST fields except data_type)
expclass::removeAttributeDraft::<class_id>::<attribute_id>class/*POSTRemoves an attribute from the draft
expclass::moveAttributeDraft::<class_id>::<attribute_id>::<direction>class/*POSTMoves an attribute of the draft: up, down, top or bottom
expclass::publishDraft::<class_id>class/*POSTPublishes the draft as the class definition
expclass::discardDraft::<class_id>class/*POSTDiscards the class draft
expclass::copy::<class_id>class/*POSTStarts a draft that is a copy of a class (publishDraft makes it a class)
expclass::remove::<class_id>class/*POSTRemoves a class that has no objects
expclass::setName::<class_id>::<language>::<name>class/*POSTSets the name of a class in a language (through a draft that is published)

Class groups (expclassgroup, 13 services, 5 writes)

The groups classes are filed in: list, members, create, rename, remove (empty groups only), add a class to a group, take it out.

CallAccessWriteSummary
expclassgroup::list::<limit>::<offset>class/*All class groups with their class counts
expclassgroup::countclass/*Number of class groups
expclassgroup::get::<group_id>class/*A class group
expclassgroup::getByName::<name>class/*A class group by name
expclassgroup::classes::<group_id>::<limit>::<offset>class/*The classes in a group
expclassgroup::classCount::<group_id>class/*Number of classes in a group
expclassgroup::ofClass::<class>class/*The groups a class is in
expclassgroup::isEmpty::<group_id>class/*Whether a group has no classes (and can be removed)
expclassgroup::create::<name>class/*POSTCreates a class group
expclassgroup::rename::<group_id>::<name>class/*POSTRenames a class group
expclassgroup::remove::<group_id>class/*POSTRemoves an empty class group
expclassgroup::addClass::<group_id>::<class>class/*POSTFiles a class in a group
expclassgroup::removeClass::<group_id>::<class>class/*POSTTakes a class out of a group (a class keeps at least one group)

Attributes and datatypes (expattribute, 21 services, 5 writes)

The attributes of content objects (values per datatype, strings, titles, history, languages) and the datatype catalogue; set one or several attributes of an object (a new published version) or of a draft.

CallAccessWriteSummary
expattribute::get::<object_id>::<identifier>::<language>content/readOne attribute of an object with its value
expattribute::getById::<attribute_id>::<version>content/readAn attribute by its id and version
expattribute::value::<object_id>::<identifier>::<language>content/readOnly the value of an attribute
expattribute::string::<object_id>::<identifier>::<language>content/readThe attribute as its string form (the format writes accept)
expattribute::title::<object_id>::<identifier>::<language>content/readThe title text of an attribute
expattribute::hasContent::<object_id>::<identifier>::<language>content/readWhether the attribute has content
expattribute::dataType::<object_id>::<identifier>content/readThe datatype of an attribute
expattribute::classAttribute::<object_id>::<identifier>content/readThe class attribute definition of an attribute
expattribute::history::<object_id>::<identifier>::<limit>content/readThe value of an attribute in every version, newest first
expattribute::languages::<object_id>::<identifier>content/readThe value of an attribute in every language
expattribute::missingRequired::<object_id>::<version>content/readRequired attributes without content in a version
expattribute::byDataType::<datatype>::<limit>::<offset>content/readAttributes of a datatype across current objects (readable ones)
expattribute::dataTypesuserThe datatype catalogue
expattribute::dataTypeInfo::<datatype>userOne datatype: name, flags and the classes using it
expattribute::dataTypeClasses::<datatype>class/*The classes that have an attribute of a datatype
expattribute::set::<object_id>::<identifier>content/editPOSTSets one attribute and publishes a new version. POST: value, language
expattribute::setMany::<object_id>content/editPOSTSets several attributes and publishes a new version. POST: values (json identifier => string), language
expattribute::clear::<object_id>::<identifier>content/editPOSTEmpties an attribute and publishes a new version
expattribute::setDraft::<object_id>::<version>::<identifier>content/editPOSTSets one attribute of a draft version (not published). POST: value, language
expattribute::setDraftMany::<object_id>::<version>content/editPOSTSets several attributes of a draft version. POST: values (json), language
expattribute::validateDraft::<object_id>::<version>content/editChecks a draft: required attributes filled, input valid

Versions and drafts (expversion, 25 services, 8 writes)

The version history of an object, drafts of a user and of all users, compare two versions; create a draft (in a language), publish, discard, remove old versions, revert.

CallAccessWriteSummary
expversion::list::<object_id>::<limit>::<offset>::<status>content/versionreadThe versions of an object, newest first, paged
expversion::count::<object_id>content/versionreadNumber of versions of an object
expversion::get::<object_id>::<version>content/versionreadOne version
expversion::current::<object_id>content/versionreadThe current (published) version
expversion::published::<object_id>content/readThe published version number
expversion::status::<object_id>::<version>content/versionreadThe status of a version
expversion::creator::<object_id>::<version>content/versionreadThe creator object of a version
expversion::dataMap::<object_id>::<version>::<language>content/versionreadThe attributes of a version with their values
expversion::translations::<object_id>::<version>content/versionreadThe languages of a version
expversion::nodeAssignments::<object_id>::<version>content/versionreadThe locations a version is assigned to
expversion::compare::<object_id>::<version_a>::<version_b>::<language>content/diffDifferences between two versions, attribute by attribute
expversion::drafts::<object_id>content/versionreadThe drafts of an object
expversion::myDrafts::<limit>::<offset>content/editThe current user's drafts of all objects, paged
expversion::allDrafts::<limit>::<offset>content/versionreadDrafts of all users (needs unrestricted version access), paged
expversion::pending::<limit>::<offset>content/pendinglistVersions waiting in the publishing queue, paged
expversion::hasConflicts::<object_id>::<version>::<language>content/editWhether a draft is older than the published version of its language
expversion::viewUrl::<object_id>::<version>::<language>content/versionreadThe relative URL that previews a version
expversion::createDraft::<object_id>content/editPOSTCreates a draft from the current or a given version. POST: language, copy_from_version
expversion::createDraftIn::<object_id>::<language>content/editPOSTCreates a draft for a new or existing language. POST: copy_from_language
expversion::publish::<object_id>::<version>content/publishPOSTPublishes a draft
expversion::discard::<object_id>::<version>content/editPOSTDiscards a draft
expversion::remove::<object_id>::<version>content/versionremovePOSTRemoves an archived or draft version (never the published one)
expversion::removeArchived::<object_id>::<keep>content/versionremovePOSTRemoves archived versions of an object, keeping the newest N
expversion::revert::<object_id>::<version>content/editPOSTCreates a draft from an old version (publish it to bring the old content back). POST: language
expversion::cleanupDrafts::<hours>content/versionremovePOSTRemoves internal (unsaved) drafts older than a number of hours from all objects. Needs unrestricted version removal

Translations (exptranslation, 19 services, 5 writes)

The site languages and the translations of objects: what exists, what is missing, status of a subtree; translate, copy a language, remove a translation, add and remove a site language.

CallAccessWriteSummary
exptranslation::languagesuserThe languages of the site
exptranslation::language::<locale>userOne site language by locale code
exptranslation::prioritizedpublicThe language codes of the siteaccess in priority order
exptranslation::topPrioritypublicThe first language of the siteaccess
exptranslation::statscontent/translationsObjects and classes per site language
exptranslation::knownLocales::<text>::<limit>::<offset>content/translationsLocales that can be added as site languages
exptranslation::ofObject::<object_id>content/readThe translations of an object with names and initial flag
exptranslation::missing::<object_id>content/readSite languages an object has no translation in
exptranslation::content::<object_id>::<language>content/readThe attributes of an object in one language
exptranslation::names::<object_id>content/readThe object name in every language
exptranslation::nodeNames::<node_id>content/readThe names of a node in every language
exptranslation::classNames::<class>userThe name of a class in every language
exptranslation::canTranslate::<object_id>userWhether the current user can translate an object, and into which languages
exptranslation::status::<node_id>content/readTranslation completeness of a node subtree: objects per language
exptranslation::translate::<object_id>::<language>content/translatePOSTCreates or changes a translation and publishes it. POST: attributes (json), copy_from_language
exptranslation::copyLanguage::<object_id>::<from>::<to>content/translatePOSTCopies the content of one language into another and publishes
exptranslation::remove::<object_id>::<language>content/translatePOSTRemoves one translation of an object
exptranslation::addLanguage::<locale>content/translationsPOSTAdds a site language (locale code). POST: name
exptranslation::removeLanguage::<locale>content/translationsPOSTRemoves a site language that no object or class uses

Relations (exprelation, 15 services, 4 writes)

Relations between objects in both directions by type (common, embed, link, attribute), counts, broken relations; add, remove and replace common relations.

CallAccessWriteSummary
exprelation::related::<object_id>::<type>::<limit>::<offset>content/readObjects an object relates to (of the current version), paged. type: common, embed, link, attribute or all
exprelation::reverse::<object_id>::<type>::<limit>::<offset>content/readObjects that relate to an object, paged
exprelation::counts::<object_id>content/readRelation counts per type and direction
exprelation::relatedCount::<object_id>::<type>content/readNumber of relations from an object
exprelation::reverseCount::<object_id>::<type>content/readNumber of objects relating to an object
exprelation::reverseCountForNodes::<node_ids>content/readReverse relation counts for a list of nodes
exprelation::byAttribute::<object_id>::<identifier>content/readObjects related through one attribute
exprelation::exists::<from_object_id>::<to_object_id>content/readWhether a relation between two objects exists, and its types
exprelation::neighbours::<object_id>::<limit>content/readBoth directions at once: related and reverse related, light
exprelation::broken::<object_id>content/readRelations of an object that point at objects that are gone or not published
exprelation::typesuserThe relation type names and their codes
exprelation::add::<from_object_id>::<to_object_id>content/editPOSTAdds a relation from an object to another. POST: type (common, embed, link; default common), version
exprelation::remove::<from_object_id>::<to_object_id>content/editPOSTRemoves a relation. POST: type, version
exprelation::removeAll::<object_id>content/editPOSTRemoves every common relation of an object (embed, link and attribute relations belong to the content)
exprelation::replace::<object_id>content/editPOSTMakes the common relations of an object exactly the given list. POST: object_ids

Locations (explocation, 15 services, 5 writes)

The places an object lives at: list, main location, assignments, candidates and checks; add, remove, set the main location (large changes as jobs).

CallAccessWriteSummary
explocation::list::<object_id>content/readAll locations (nodes) of an object
explocation::count::<object_id>content/readNumber of locations of an object
explocation::main::<object_id>content/readThe main location of an object
explocation::parents::<object_id>content/readThe parent nodes of the locations of an object
explocation::assignments::<object_id>content/readThe node assignments of an object (current version)
explocation::isMain::<node_id>content/readWhether a node is the main location of its object
explocation::canAdd::<object_id>::<parent_node_id>userWhether the current user can add the object to a node
explocation::canRemove::<node_id>userWhether a location can be removed (it is not the last one and has no children)
explocation::candidates::<node_id>::<class>::<limit>::<offset>content/readContainer nodes below a node where an object of a class can be added
explocation::orphans::<limit>::<offset>content/readPublished objects that have no node (needs unrestricted read)
explocation::add::<object_id>::<parent_node_id>content/manage_locationsPOSTAdds a location of an object below a node
explocation::addMany::<target_node_id>content/manage_locationsPOSTAdds the objects of several nodes below one node. POST: node_ids, mode
explocation::remove::<node_id>content/manage_locationsPOSTRemoves one location of an object (not the last one)
explocation::removeManycontent/manage_locationsPOSTRemoves several locations. POST: node_ids, mode
explocation::setMain::<node_id>content/manage_locationsPOSTMakes a location the main one

Trash (exptrash, 12 services, 4 writes)

Trashed objects: list, filter, original parent; restore, purge one or several, empty the trash (confirm field).

CallAccessWriteSummary
exptrash::list::<sort>::<order>::<limit>::<offset>::<filter>content/restoreTrashed objects, paged and sorted (sort: name, class_name, published, modified, section)
exptrash::countcontent/restoreNumber of trashed objects
exptrash::get::<object_id>content/restoreOne trashed object
exptrash::isTrashed::<object_id>content/restoreWhether an object is in the trash
exptrash::originalParent::<object_id>content/restoreThe node a trashed object was removed from, when it still exists in the same place
exptrash::byClass::<class>::<limit>::<offset>content/restoreTrashed objects of a class
exptrash::since::<since>::<limit>::<offset>content/restoreObjects trashed since a time (timestamp or date)
exptrash::canEmptyuserWhether the current user may purge from the trash
exptrash::restore::<object_id>content/restorePOSTRestores an object to its original parent, or to POST parent_node_id
exptrash::purge::<object_id>content/cleantrashPOSTRemoves one object from the trash for good
exptrash::purgeManycontent/cleantrashPOSTPurges several objects. POST: object_ids
exptrash::emptyTrashcontent/cleantrashPOSTEmpties the whole trash. POST: confirm=yes

Sections (expsection, 17 services, 5 writes)

Sections and what is in them; create, change, remove; assign an object or a subtree (jobs for large subtrees).

CallAccessWriteSummary
expsection::list::<limit>::<offset>section/viewAll sections
expsection::countsection/viewNumber of sections
expsection::get::<section_id>section/viewA section by id
expsection::getByIdentifier::<identifier>section/viewA section by identifier
expsection::objects::<section_id>::<limit>::<offset>content/readObjects (main nodes) in a section, paged
expsection::objectCount::<section_id>section/viewNumber of objects in a section
expsection::usagesection/viewSections with their object counts
expsection::canRemove::<section_id>section/viewWhether a section can be removed and what blocks it
expsection::assignableuserSections the current user may assign
expsection::navigationPartssection/viewThe navigation parts a section can use
expsection::ofObject::<object_id>content/readThe section of an object
expsection::ofNode::<node_id>content/readThe section of a node
expsection::createsection/editPOSTCreates a section. POST: name, identifier, navigation_part
expsection::update::<section_id>section/editPOSTChanges a section (same POST fields)
expsection::remove::<section_id>section/editPOSTRemoves an unused section
expsection::assign::<object_id>::<section_id>section/assignPOSTAssigns an object to a section, with the objects below it. POST: scope (subtree, inherited or object)
expsection::assignSubtree::<node_id>::<section_id>section/assignPOSTAssigns a node and its subtree to a section. POST: mode, scope (subtree, inherited or object)

Object states (expstate, 23 services, 9 writes)

State groups and states, the states of an object, objects in a state; create, change, remove, order; assign to an object or a subtree (jobs for large subtrees).

CallAccessWriteSummary
expstate::groups::<limit>::<offset>state/administrateThe state groups with their states
expstate::groupCountstate/administrateNumber of state groups
expstate::group::<group>state/administrateA state group by id or identifier, with its states
expstate::states::<group>state/administrateThe states of a group in order
expstate::state::<state_id>state/administrateOne state by id
expstate::stateByIdentifier::<group>::<identifier>state/administrateOne state by group and identifier
expstate::translations::<state_id>state/administrateThe names and descriptions of a state in every language
expstate::groupTranslations::<group>state/administrateThe names and descriptions of a group in every language
expstate::defaultState::<group>state/administrateThe first (default) state of a group
expstate::ofObject::<object_id>content/readThe states of an object
expstate::allowedForObject::<object_id>content/readThe states the current user may assign to an object
expstate::objects::<state_id>::<limit>::<offset>content/readObjects (main nodes) in a state, paged
expstate::objectCount::<state_id>state/administrateNumber of objects in a state
expstate::limitationsstate/administrateThe state limitations policies can use
expstate::createGroupstate/administratePOSTCreates a state group. POST: identifier, name, description, language
expstate::updateGroup::<group>state/administratePOSTChanges a state group. POST: identifier, name, description, language
expstate::removeGroup::<group>state/administratePOSTRemoves a state group with its states
expstate::createState::<group>state/administratePOSTCreates a state in a group. POST: identifier, name, description, language
expstate::updateState::<state_id>state/administratePOSTChanges a state. POST: identifier, name, description, language
expstate::removeState::<state_id>state/administratePOSTRemoves a state (its objects get the group default)
expstate::reorderStates::<group>state/administratePOSTOrders the states of a group. POST: state_ids (the group's state ids in the new order)
expstate::assign::<object_id>::<state_id>state/assignPOSTAssigns a state to an object
expstate::assignSubtree::<node_id>::<state_id>state/assignPOSTAssigns a state to a node and its subtree. POST: mode

URL aliases and wildcards (expurlalias, 24 services, 6 writes)

The nice URLs: aliases of nodes, resolving a path, normalising text, global aliases, redirects, wildcards; create and remove custom aliases and wildcards.

CallAccessWriteSummary
expurlalias::forNode::<node_id>::<type>::<limit>::<offset>content/readThe URL aliases of a node. type: alias (custom), name (generated) or all
expurlalias::forObject::<object_id>content/readThe generated URL of every location of an object
expurlalias::path::<node_id>content/readThe URL path of a node (generated alias)
expurlalias::resolve::<path>userResolves a URL path to its action and node
expurlalias::exists::<path>content/readWhether a path is taken by an alias
expurlalias::children::<path>::<limit>::<offset>content/urltranslatorThe alias elements directly below a path (empty path: the top)
expurlalias::normalize::<text>userTurns text into the form the URL transformation rules give it
expurlalias::normalizePath::<path>userNormalises every element of a path
expurlalias::list::<type>::<limit>::<offset>::<text>content/urltranslatorAll custom aliases (type alias), name or all, paged
expurlalias::count::<type>content/urltranslatorNumber of aliases of a type
expurlalias::redirects::<limit>::<offset>content/urltranslatorAliases that redirect to the real URL, paged
expurlalias::byAction::<action>content/urltranslatorThe aliases of an action such as eznode:43 or module:search
expurlalias::pathPrefixpublicThe path prefix of the siteaccess
expurlalias::wildcards::<limit>::<offset>content/urltranslatorThe URL wildcards, paged
expurlalias::wildcardCountcontent/urltranslatorNumber of wildcards
expurlalias::wildcard::<wildcard_id>content/urltranslatorOne wildcard by id
expurlalias::wildcardBySource::<source>content/urltranslatorA wildcard by its source URL
expurlalias::wildcardMatches::<path>content/urltranslatorWhether a path is caught by any wildcard
expurlalias::createAlias::<node_id>content/urltranslatorPOSTAdds a custom URL alias to a node. POST: alias, language, parent_is_root, redirects
expurlalias::removeAlias::<node_id>content/urltranslatorPOSTRemoves custom aliases of a node. POST: elements (list of parent.md5.language from forNode)
expurlalias::removeAllAliases::<node_id>content/urltranslatorPOSTRemoves every custom alias of a node
expurlalias::createWildcardcontent/urltranslatorPOSTAdds a wildcard. POST: source, destination, type (forward or direct)
expurlalias::removeWildcard::<wildcard_id>content/urltranslatorPOSTRemoves a wildcard
expurlalias::removeWildcardscontent/urltranslatorPOSTRemoves several wildcards. POST: ids

Search (expsearch, 18 services, 3 writes)

Full text search with paging, class, section, subtree and date filters and facets; suggestions, index status and statistics; index and unindex an object.

CallAccessWriteSummary
expsearch::search::<text>::<limit>::<offset>::<filter>content/readSearches the content. filter: class[], section, subtree[], date (day, week, month, 3months, year)
expsearch::count::<text>::<filter>content/readNumber of matches of a search
expsearch::facets::<text>::<filter>content/readCounts of the matches per class and per section (over the first 500 matches)
expsearch::byClass::<text>::<classes>::<limit>::<offset>content/readSearches within classes
expsearch::inSubtree::<text>::<node_id>::<limit>::<offset>content/readSearches below a node
expsearch::bySection::<text>::<section_id>::<limit>::<offset>content/readSearches within a section
expsearch::byAttribute::<text>::<class_attribute_id>::<limit>::<offset>content/readSearches the values of one class attribute
expsearch::similar::<object_id>::<limit>content/readObjects similar to an object (searches with its name)
expsearch::suggest::<prefix>::<limit>content/readWords of the index that start with a prefix, most used first
expsearch::normalize::<text>userThe text as the search engine normalises it
expsearch::engineuserThe search engine in use and its flags
expsearch::searchableClassesuserClasses with searchable attributes
expsearch::indexStatus::<object_id>content/readWhether an object has words in the index
expsearch::statssetup/administrateIndex size: words, links, objects with words
expsearch::topPhrases::<limit>::<offset>setup/administrateThe most frequent search phrases of the site, with their average hits
expsearch::reindex::<object_id>content/editPOSTIndexes an object again
expsearch::removeFromIndex::<object_id>content/editPOSTRemoves an object from the index
expsearch::clearPhrasessetup/administratePOSTClears the stored search phrase statistics. POST: confirm=yes

Content jobs (expcontentjob, 26 services, 14 writes)

The background jobs for large operations: list and follow them, estimate whether an operation runs now or as a job, create a job of every type, cancel, resume, start.

CallAccessWriteSummary
expcontentjob::list::<state>::<limit>::<offset>content/jobsThe current user's jobs, newest first. state: queued, running, done, failed, cancelled or all
expcontentjob::listAll::<state>::<limit>::<offset>setup/administrateThe jobs of all users (needs setup/administrate)
expcontentjob::activecontent/jobsThe current user's queued and running jobs
expcontentjob::summarycontent/jobsCounts of the current user's jobs per state
expcontentjob::get::<job_id>::<log_lines>content/jobsOne job with its progress, result and the last log lines
expcontentjob::progress::<job_id>content/jobsOnly the progress of a job: done, total, percent, phase, message
expcontentjob::log::<job_id>::<lines>content/jobsThe last lines of the job log
expcontentjob::result::<job_id>content/jobsThe result of a finished job
expcontentjob::typescontent/jobsThe registered job types with their parameters
expcontentjob::settingscontent/jobsThe limits that decide now or job: SynchronousLimit, NowLimit, BatchSize
expcontentjob::estimate::<type>::<params>content/jobsHow many nodes an operation touches and whether it runs now or as a job. params as in create
expcontentjob::lockOf::<node_id>content/jobsThe job that locks a node, if any
expcontentjob::create::<type>content/jobsPOSTCreates a job and starts its worker. POST: params (json, see types), spawn (default 1)
expcontentjob::removecontent/removePOSTJob: removes nodes. POST: node_ids, move_to_trash, spawn
expcontentjob::copycontent/createPOSTJob: copies a subtree. POST: source_node_id, destination_node_id, all_versions, keep_creator, keep_time, spawn
expcontentjob::movecontent/movePOSTJob: moves a subtree. POST: node_id, new_parent_node_id, spawn
expcontentjob::hidecontent/hidePOSTJob: hides a subtree. POST: node_id, spawn
expcontentjob::revealcontent/hidePOSTJob: reveals a subtree. POST: node_id, spawn
expcontentjob::sectionsection/assignPOSTJob: assigns a section to a subtree. POST: node_id, section_id, spawn
expcontentjob::statestate/assignPOSTJob: assigns a state to a subtree. POST: node_id, state_id, spawn
expcontentjob::addLocationcontent/manage_locationsPOSTJob: adds locations. POST: node_ids, target_node_id, spawn
expcontentjob::removeLocationcontent/manage_locationsPOSTJob: removes locations. POST: node_ids, spawn
expcontentjob::cancel::<job_id>content/jobsPOSTCancels a job (a running job stops after its current batch)
expcontentjob::resume::<job_id>content/jobsPOSTResumes a failed job or one whose worker died
expcontentjob::spawn::<job_id>content/jobsPOSTStarts the worker of a queued job
expcontentjob::purgeFinished::<days>setup/administratePOSTRemoves finished jobs older than a number of days (needs setup/administrate)

Total: 350 content services in 15 domains (112 writes).

Demo mode. With the environment variable EXP_DEMO_MODE=1 (a public demo) the account and user services refuse to change or reset an administrator's password: HTTP 403 with "This installation is a public demo: the passwords of administrators cannot be changed or reset." for expaccount.reset, expaccount.changePassword and expuser.setPassword; no key and no mail for an administrator from expaccount.forgotRequest. Demo mode.

Something wrong on this page? Report an error

Documentation license

This documentation is free documentation, licensed under the GNU Free Documentation License 1.3 or later.

Exponential Copyright © 1998-2026 Exponential Foundation and 7x