Skip to content

Releases: AdminBolt/plugin-sdk

1.0.7: Give a plugin the transport it talks to everything else with

Choose a tag to compare

@bobicloudvision bobicloudvision released this 11 Sep 16:36
An integration plugin has two halves. admin() and client() cover the one
that faces the panel, signing and retrying every call. The other half is
the service the plugin exists to connect the panel to, and the SDK had
nothing to say about it: a plugin built its own curl handle, which meant
the half most worth testing could not be reached from a test.

Plugin::http() hands back the injected client, or a default curl one
built from the configured timeout. A test that passes Plugin::create() a
fake now sees both halves. Its two arguments are for the case that
differs from the default, a service behind a certificate nothing in the
trust store signed, and are ignored when a client was injected so that
asking for them in production code cannot defeat a fake in a test.

FakeHttpClient moves out of the SDK's own tests and ships. Every plugin
needs exactly it, and writing it again in each one is how plugins end up
with slightly different ideas of what an HTTP failure looks like.

Manifest::directory() is the plugin's own root, for reading a file it
ships rather than one the panel wrote. Resolved through realpath,
because it ends up in front of an operator as a path to type: a manifest
discovered from public/index.php would otherwise hand back a path with
"/public/.." in the middle of it.

Also corrects what UiResponse::redirect() says about itself. It claimed
an external URL opens in a new tab. The panel follows only its own
paths and logs that it refused anything else, so a link out belongs in a
Text::markdown() component, where the viewer can see where it goes
before they take it.

1.0.6: Let a manifest say where a plugin belongs and what it looks like

Choose a tag to compare

@bobicloudvision bobicloudvision released this 11 Sep 16:26
Two fields the panel's plugin list now reads. "category" is one word from a
fixed vocabulary and becomes the tab a plugin is found under; a word the panel
does not know is filed under other rather than refused, which is the bargain
the rest of the manifest already strikes. "screenshots" is up to six pictures
shipped inside the plugin, optionally captioned.

A screenshot is a file in the plugin: png, jpg, webp, avif or gif, on a
relative path that stays inside it. Not SVG, which can carry script, and never
a remote URL - the panel serves these itself rather than sending every
administrator's browser to fetch an address a manifest chose.

Say nothing and the panel still looks in screenshots/ and then at a
screenshot.png in the root, so a repository that already has one needs no
change. Themes get the paragraph they need: the list draws a theme as its
picture, because what a theme is cannot be written in a sentence, and a theme
that names no category is filed under appearance.

The docs also now say what the panel will not read from a manifest: who wrote
the plugin. That badge comes from a list the panel ships.

1.0.5: Return the panel's word for which account a call is about

Choose a tag to compare

@bobicloudvision bobicloudvision released this 11 Sep 12:24
A plugin's API key is an admin key, because a client-panel page is drawn for
whichever account is looking. Left at that, a plugin could name any account on
the server in X-Hosting-Account, which is not what the approval screen offered
when it said "for the account viewing the plugin".

So the panel no longer lets a plugin choose. It puts a signed grant naming the
account in the envelope it sends, and the client API acts only on what that
grant says. This returns it. clientFor() is the only thing a plugin needs to
know about: it already scoped a call to the account a request concerns, and it
now carries the grant along with the username, because the two are the same
fact and a plugin should not be able to send one without the other.

forAccount() takes the grant as a second argument and sends it when there is
one. Nothing is invented when there is not: a plugin naming an account by hand
is the case the panel refuses, and making that call look authorised here would
only move the refusal further from the mistake.

Hook deliveries read the grant from the same place in their context, ready for
the panel to start sending it. A plugin acting on a hook that carries none is
refused, exactly as a page would be.

Needs the matching panel release: a panel without it ignores the header, and a
panel with it refuses a plugin that does not send one.

1.0.4: Let a plugin ship a stylesheet the panel can wear

Choose a tag to compare

@bobicloudvision bobicloudvision released this 11 Sep 10:27
A theme is a manifest block: a name, a stylesheet inside the plugin, and the
panels it applies to. The panel loads its own stylesheet first and this one
after it, so a theme is a file of token overrides rather than a rebuild of
everything, and docs/themes.md says which tokens are worth starting from.

The path is checked like a path, because this is the one thing a plugin ships
that reaches a browser as the plugin wrote it: relative, inside the plugin,
and a .css file. The panel serves it as text/css and will serve nothing else.

A plugin that ships only a theme now needs no runtime block and no entrypoint.
It is not a program: there is no listener for the panel to call, no secret to
hold, and an entrypoint would be a PHP file that never runs. Anything the
panel does have to call still has to declare one, which is what the rest of
that rule says.

1.0.3: Give plugins a place to draw and a vocabulary for where

Choose a tag to compare

@bobicloudvision bobicloudvision released this 11 Sep 09:26
A plugin's pages waited for somebody to go and look at them. Slots are the
other half: a line in the footer, a note under the navigation, a warning above
every admin page. $plugin->slot() answers one, and what it returns is a page
description, so there is nothing new to learn about what a slot may contain.

SlotPosition is the catalogue of where. It is written out here, all sixty-two
of them, rather than being whatever the panel happens to render with: a plugin
that declares "footer" should keep drawing in the footer for as long as the
panel has one, whoever built the layer underneath by then. The panel keeps the
map from these names to its own view layer, and a change of view layer moves
that map rather than every plugin in the wild.

Names are added and never repurposed, exactly as hook names are. Three kinds
of position are deliberately missing: the document head, the script block and
the stylesheet block take markup and nothing else, and a plugin sends a
description rather than markup.

The manifest validates a slot the way it validates a page, and a position with
a typo in it is told what was probably meant, because "sidebar.nav.ends" is a
mistake somebody will make more than once.

1.0.2: Let a page say how big its frame should be

Choose a tag to compare

@bobicloudvision bobicloudvision released this 10 Sep 23:50
A frame the panel sizes for the plugin is a frame that is wrong for half of
them. A console wants everything the window has; a settings page wants to be
as tall as its content and no taller.

Three fields, and all three only mean something for a page the plugin draws
itself, so declaring one on a declarative page is refused rather than accepted
and ignored: the panel draws those with its own components and decides how
tall they are.

height: "auto" is the one worth explaining. The frame is sandboxed out of the
panel's origin, which is what keeps a plugin's scripts away from the panel's
DOM and cookies, and it means the panel cannot measure the frame's document
either. So the front end reports its own height, and the panel clamps what it
is told and keeps a floor under it, because a plugin that never reports one
must be a page rather than an empty strip.

1.0.1: Merge pull request #1 from AdminBolt/plugin-commands

Choose a tag to compare

@bobicloudvision bobicloudvision released this 10 Sep 22:28
9bff80c
Let a plugin run the commands it declared

1.0.0: Let plugins put their own pages in the panel

Choose a tag to compare

@bobicloudvision bobicloudvision released this 10 Sep 19:35
A plugin describes a page and the panel draws it with its own components. It
never emits HTML.

That is the whole reason to prefer this over an iframe. The page matches the
rest of the panel, follows the operator's theme, works at phone width, and is
escaped by the panel, so a plugin cannot inject markup into the panel even by
accident: there is no markup to inject. The panel already iframes CloudLinux's
UI, and the coupling that took (knowing its bundles, its config file, its
feature gates) is the thing this design exists to avoid.

Page, Section, Stat, Table, Form, Alert and Text cover what a plugin page
actually needs. Colours are the panel's semantic names rather than hex values,
so a plugin cannot fight the theme or produce something unreadable in dark
mode, and column types tell the panel how to format a value rather than the
plugin formatting it in the wrong timezone.

Iframes stay available for a terminal or an existing single-page app, declared
as render: iframe and proxied by the panel so the plugin still needs no public
port.

Security-relevant decisions:

The viewer identity travels in the same signed envelope as a hook delivery,
because it is what a plugin scopes its data by. An unsigned one would be a way
to read another account's data through a plugin. clientFor() now takes a page
request as well as a hook, so scoping to the account being viewed stays the
path of least resistance.

Only registered pages and actions are reachable. A request naming anything
else is refused before plugin code runs, so an action is not callable because
someone guessed it.

A secret field drops its value on serialisation whatever the plugin set, so a
stored credential is never served back to a browser. Show that one is set with
the placeholder instead.

A handler that throws returns a flat message; the detail goes to the plugin
log rather than to the person looking at the page.

Poll intervals floor at five seconds, since every poll is a request to the
plugin.