Custom widgets let an administrator put their own small web pages, or a third
party's embed code, into the widget column beside the built-in widgets, and
choose which widgets new accounts start with. They are in Control Panel >
Community > Widgets (/admin/widgets), for administrators only: moderators
cannot see the page or call its endpoints.
Everything on this page was checked in Chromium 151 and Firefox 157. Safari (WebKit) was not tested, and where the two browsers tested behaved differently, Safari may differ again.
What a widget can and cannot do
A widget can show text, images and styles, and, if you allow scripts, run scripts of its own. Its links open as links in the app.
Each widget runs in its own sealed frame. Whatever its code, a widget:
- cannot see who is looking at it: it is never given a user name, user id, token or the address of the page around it;
- cannot read or change the page around it, this server's cookies or storage, or another widget;
- cannot call this server's API: its requests to this server are refused by the frame's policy, apart from images, which carry no cookie or credentials;
- cannot draw outside its own box, and grows only up to its maximum height;
- cannot open windows or dialogs, submit forms, start downloads or navigate the page around it.
What it is given: the 22 theme colours, the font and font size (as CSS variables, listed under CSS variables a widget can use on the page), the language, and light or dark. They follow the theme when it changes.
Links. A click on a link in a widget is passed to the app. A link to a page of this server opens in the app. Any other link opens in a new tab with no connection back to the app, after the external-link warning if the user has turned on Show warning when opening external URLs. A widget's script can only open a link within a few seconds of a real click or key press on the page, and at most three in five seconds.
The two kinds
- Custom HTML: your HTML, with CSS and, if allowed, JavaScript. You can paste a third party's embed code here; the hosts it loads from must be listed (see below).
- External frame: one page on another server, shown as it is, at a fixed
height. Use this when an embed code is a single
<iframe>: such embeds do not work as custom HTML in most browsers, and the editor offers to convert them. Only the address is stored:https, no port or user name, on a hostname other than this server's. The page runs in its own site's origin, so it keeps its own cookies and storage, and it cannot reach the app around it. Windows it opens stay sandboxed (no forms in them).
The scripts switch
Off by default. With it off, the widget's own scripts, inline handlers such
as onclick, javascript: links and script files, including those from the
sites you allow, do not run. Only the frame's own fixed script runs (height,
theme and links).
With scripts off, the server also refuses HTML that would contact another site on its own, before any viewer has agreed. Browsers do this for two things the frame's policy does not stop:
- any
<meta http-equiv>, since<meta http-equiv="refresh">sends the frame to another site as soon as it loads; - an
<iframe>or<frame>whose address is nothttpson one of the widget's allowed sites, or that writes its content inline (srcdoc). Chromium contacts a frame's site even when it then blocks the frame.
To be sure of the second rule, the server counts every <iframe and <frame
in the text, including ones in comments, scripts and SVG, and refuses the
widget when they do not all turn out to be frames it could check. Reword such
mentions, or remove them.
With scripts on, none of this can be checked: a script can do it at run time. Turn scripts on only for code you trust.
Hosts, and what users see
A widget of kind Custom HTML may load from at most 20 other sites ("hosts").
Each is a bare hostname over https, such as cdn.example.com: no path,
port, wildcard, IP address or this server's own hostname. A subdomain is a
separate host. The editor lists the hosts it finds in your code, and the
preview names each one the browser blocked, with an Allow button. Nothing
is allowed until you choose it.
The widget's frame may then load styles, images, fonts, media and frames from those hosts and connect to them, and with scripts on, load scripts from them. A widget with scripts on can use all of its hosts in every way they are allowed, as often as it likes, for as long as it is on screen. Every viewer's IP address and browser details reach those sites. A script there can also learn the viewer's languages, time zone, screen size and the widget's own address (which names this server and the widget), and send them on. It cannot learn who the viewer is from this server.
Users meet a widget with hosts behind a placeholder that names its sites, with Load (for this tab) and Always load. Nothing is requested from those sites, nor the widget itself, until they choose. While it is loaded, the title bar names the sites; such a widget always keeps its title bar, even with Show title bar off. That placeholder and the title bar are the only places a viewer sees which sites a widget contacts. Each user can set Server widgets that load content from other sites to Ask, Always load or Never load, in Settings > Privacy > Content from other sites. That setting is per device, and an administrator cannot change it.
A widget without hosts loads at once, with no placeholder. It contacts this server only, with one exception: with scripts on, a widget can send its own frame to another site. That request reaches the site, and the widget is then removed from the page with a notice, once per page load. A widget with scripts on cannot open a WebRTC connection: the frame removes it before your code runs.
An External frame always counts as contacting its site, and the placeholder
names it. Only its first address is checked: the third-party page can send its
frame elsewhere, this server included. Checked in Chromium and Firefox: the
app's own pages refuse to be framed there; a widget frame loads but stays
sealed; an uploaded file is either an image or a download the frame refuses,
and its policy stops any script in it. An /embed/ page would load with this
server's own origin, so it can tell when another site's page has put it inside
the app, and then stops before any of its code runs. Embeds on other sites,
and the preview in the app's embed code dialog, are not affected. Any route
added later that serves pages on this hostname must refuse to be framed, stay
sealed, or stop itself in the same way.
Default layouts
The Default layout tab sets the widgets new accounts start with: built-in widgets and custom ones, each with its own settings. Until a layout is saved, new accounts get the built-in three (calendar, notifications, trending), and nothing changes when you look at the tab. The sidebar shows one list: the left column's widgets first, then the right column's.
- New accounts (created after the first layout was saved) start with exactly the layout, in its order.
- Existing accounts keep the widgets they have. An entry reaches them only if Also add for existing users is switched on for it: then it is added once, after the widgets they already have, the next time they open the app on each device. An entry without the switch reaches only accounts created after the entry was added.
- Once means once. A widget a user removes on a device does not come back there. Removing an entry from the layout changes nobody's sidebar.
Layouts are per device. A user's widgets are stored in their browser and are not synced between devices, so a user who signs in on a second device gets the layout there too, whatever they removed on the first. The deck is not affected: deck users add server widgets from a widgets column's picker.
A custom widget that is switched off keeps its entry in the layout, but nobody gets it while it is off. Deleting a custom widget removes its entries from the layout.
Turning a widget off or deleting it
An inactive or deleted widget is no longer offered, and leaves the widgets of every open tab within a few seconds, without a reload: after each change the server tells the tabs connected to it, and the widget's frame is taken off the page. A tab whose connection to the server was down at that moment asks for the list again within about ten seconds of reconnecting. A page loaded after the change never shows it. Saving new code for a widget loads its frame again in open tabs, with the new code. To stop a widget doing harm, switch it off or delete it.
A widget's frame is sent with Cache-Control: no-cache and an ETag, so the
browser, and any cache in front of the server, asks the server each time a frame
loads. An unchanged widget gets a short "not modified" answer; a widget that was
switched off gets nothing, even in a tab that has not heard of the change yet.
Saving the default layout reaches open tabs too, but changes nobody's widgets there: the layout is applied when a page loads (see above).
Every create, update and delete is in the moderation log. An update records the host list before and after.
Safe mode
If a widget makes a tab unusable, for instance a script that never stops,
open the app with ?safe_mode=no_widgets at the end of its address:
https://example.social/?safe_mode=no_widgetsfor a user, who can then remove the widget: Edit widgets shows each server widget as "Server widgets are off in this tab (safe mode)", with its remove button;https://example.social/admin/widgets?safe_mode=no_widgetsfor you, to switch the widget off or fix it. The preview is off in such a tab too, since the code you came to fix may be what hangs it.
That tab starts without any server widget and requests no widget frame. It stays so while you move around the app; reloading the page without the parameter ends it. The home timeline rewrites its own address a few seconds after it loads, which drops the parameter, so a reload there ends safe mode as well. The default layout is not applied in a safe-mode tab; the next page load outside it applies it as usual.
Safe mode is not linked from anywhere in the app: pass the address on to a user who needs it.
Limits
| What | Limit |
|---|---|
| Custom widgets per server | 30 |
| HTML per widget | 64 KB (UTF-8) |
| Hosts per widget | 20 |
| Height | 1 to 800 px (the maximum for Custom HTML, the fixed height for External frame) |
| Name | 128 characters |
| External frame address | 2048 characters |
| Default layout | 50 entries, each with at most 4 KB of settings |
| Preview | 30 a minute per administrator, each kept for ten minutes |
Remote accounts (signed in to another server through this one) and visitors never see custom widgets.
For API users
The endpoints are admin/custom-widgets/{create,update,delete,list,preview},
for administrators only. An access token needs the permission
Manage custom widgets (write:admin:custom-widgets) or View custom
widgets (read:admin:custom-widgets). The default layout is saved through
admin/update-meta (defaultWidgets).
After each write, every streaming connection, signed in or not, receives the
broadcast event customWidgetsUpdated with the fields of the detailed meta that
changed: customWidgets (the active widgets, without their HTML),
defaultWidgets (without entries for inactive widgets) and
defaultWidgetsSince. A client can put these in place of its copy of meta
instead of asking for meta again.
Database
One additive migration (its file name ends in custom-widget): the
custom_widget table, and meta.defaultWidgets and
meta.defaultWidgetsSince. Nothing is dropped or renamed (see
Downgrading a Catodon instance to Sharkey).
What was not verified
- Safari, as above.
- DNS lookups from
<link rel="dns-prefetch">could not be observed on the test setup. Connections from it and frompreconnectwere looked for, and there were none.