Admin Extensions

Contents

Admin extensions are dynamic endpoints that plug into into existing admin tools at their named interfaces.

Introduction

The most common usage is rendering an embedded UI fragment, but an extension is basically a dynamic HTTP endpoint — it can equally well serve JSON, binary assets, or any other payload the host tool agrees to consume.

  • Contributed by an app and discovered by the host admin tool through a string identifier called an interface.

  • Mounts only into tools that publish a matching interface name. The reserved name generic matches any tool.

  • Is served by the admin service through the admin:extension dispatcher API at /admin/<app>/<tool>/_/admin:extension/<app>:<extension-name>/.

  • Returns whatever HTTP payload the host tool expects — HTML fragments for UI widgets, JSON for data feeds, binary assets, etc.

Usage

To declare an extension, place a descriptor and an implementation under src/main/resources/admin/extensions/<extension-name>/:

src/main/resources/admin/extensions/
  my-extension/
    my-extension.yaml   (1)
    my-extension.ts     (2)
    my-extension.svg    (3)
1 Descriptor — see below.
2 HTTP function implementation for the extension URL.
3 Optional SVG icon, served by the discovery endpoint.

The folder name, descriptor base name, and implementation base name must all match <extension-name>.

Descriptor

The descriptor is YAML only — .yaml and .yml are both accepted.

src/main/resources/admin/extensions/<extension-name>/<extension-name>.yaml
kind: "AdminExtension"
title:                                (1)
  text: "My extension"
  i18n: "i18n.my-extension.title"
description: "My extension"           (2)
interfaces:                           (3)
  - "com.example.app.sidebar"
  - "com.example.myinterface"
allow:                                (4)
  - "role:system.authenticated"
config:                               (5)
  property_1: "value_1"
1 title — string, or { text, i18n } for localization. Used wherever the host tool renders an extension list.
2 description — string, or { text, i18n }.
3 interfaces — names of interface points the extension plugs into. The host tool must publish a matching name in its own interfaces: list. Use the reserved value "generic" to make the extension mountable from any admin tool.
4 allow — principal keys granted access. role:system.admin is always permitted in addition to anything listed here. Required to expose the extension to non-admins.
5 config — free-form key/value map readable from the implementation and surfaced in the discovery endpoint.

Implementation

The implementation lives at src/main/resources/admin/extensions/<extension-name>/<extension-name>.ts and exports an HTTP function for each method the extension responds to. The response shape is determined by the host tool — UI widgets typically return HTML fragments, but a data-feed extension might return JSON, and an asset-serving extension might return binary content with the appropriate contentType.

Sample my-extension.ts
exports.GET = (req) => {
    const contentId = req.params.contentId;
    return {
        body: `<div>Inspecting content ${contentId}</div>`,
        contentType: 'text/html'
    };
};

For anything beyond a couple of static endpoints, use the router library for method dispatch, parameter extraction, and pattern matching.

Extension URL

The URL at which an extension is reachable is a platform internal and may change between XP releases — do not hard-code it. Use extensionUrl() from lib-admin; it resolves the URL based on the current admin tool context.

const adminLib = require('/lib/xp/admin');

const url = adminLib.extensionUrl({
    application: 'com.example.app',
    extension: 'my-extension',
    params: { contentId: '123' }
});

(widgetUrl() exists for backward compatibility but is deprecated; use extensionUrl() in new code.)

Two preconditions still apply to every invocation:

  • The host tool’s descriptor must include "admin:extension" in its apis: list, so the dispatcher is mounted under that tool.

  • The interface match rule: unless the extension’s interfaces: list contains "generic", at least one of its interfaces must also be present in the host tool’s interfaces: list — otherwise the request returns 404 Not Found.

Sub-paths and static assets

Everything after the descriptor segment of the extension URL is routed to the same implementation, and req.contextPath holds the extension URL itself. This is the extension’s only address space: an extension is served by the admin:extension Universal API, which is mounted by the host tool. Which APIs are mounted under a tool is decided by the tool’s own descriptor, and an extension cannot add to that list. In particular, an extension cannot get its own app’s asset API mounted under the host tool, so assetUrl() from lib-asset or lib-portal produces URLs that return 404 Not Found there.

Serve CSS, JS, images, and other static files from a sub-path of the extension URL instead, using lib-static together with the router library:

Sample my-extension.ts serving its own assets
const router = require('/lib/router')();
const staticLib = require('/lib/enonic/static');

router.get('', (req) => ({
    contentType: 'text/html',
    body: `<link rel="stylesheet" href="${req.contextPath}/_static/styles/my-extension.css"/>
<div class="my-extension">Hello</div>`
}));

router.get('/_static/{path:.*}', (req) => staticLib.requestHandler(req, {
    root: '/assets',
    relativePath: staticLib.mappedRelativePath('/_static'),
    index: false
}));

exports.all = (req) => router.dispatch(req);

Files placed under src/main/resources/assets/ are then reachable at <extension URL>/_static/<path>, behind the same access control as the extension itself. Client-side code in the extension can call back to the same URL space for data, or to any Universal API the host tool has mounted.

Response processors

XPXP8.1.08.1.0 An extension that needs the host tool page to allow external resources, or to load a script of its own, registers a Java response processor for its key. It runs after the tool controller and may adjust the page’s Content Security Policy or add page contributions. See Extension response processors.

Access control

Two checks are enforced:

  1. The caller must hold role:system.admin.login.

  2. The extension’s descriptor must permit the caller — role:system.admin is always permitted; otherwise the caller must match at least one principal in allow:.

Discovery

The host tool typically queries ?interface=<name> to find which extensions are available to the current user at a given interface point. The response is a JSON array, one entry per allowed extension:

[
  {
    "key": "com.example.app:my-extension",
    "title": "My extension",
    "description": "My extension",
    "iconUrl": "/admin/.../_/admin:extension?icon&app=com.example.app&extension=my-extension&v=...",
    "url": "/admin/.../_/admin:extension/com.example.app:my-extension",
    "interfaces": ["com.example.app.sidebar"],
    "config": { "property_1": "value_1" }
  }
]

The listing is filtered by the caller’s principals — extensions the caller cannot access are omitted.

Interfaces

Interface names are free-form strings, not a closed enum. The contract is purely a string match between the extension’s interfaces: and the host tool’s interfaces:. To define a new interface point in your own admin tool, declare a stable name (e.g. <app-key>.<purpose>) in the tool’s descriptor and document the request/response shape your tool expects from extensions.

Each consuming admin tool defines its own set of interface names along with the request/response shape extensions must conform to — consult the host tool’s documentation for the contract.


Contents

Contents