Serve an uploaded file as data, not as a document that can run
An SVG is an XML document that may carry a script, and it is an accepted image type because floor-plan maps and branding genuinely want vector. Loaded through an img tag that script never runs, so the tiles and maps were never the risk. Opening the file's own URL is - and the application image route is public, so that URL needs no session. Every route that serves an upload now goes through one helper that sends Content-Security-Policy: default-src 'none'; sandbox, and nosniff. Seven routes across core and five plugins, so a new one added later starts from the same place rather than repeating the reasoning. Banning the format instead would have cost the maps their only sensible one. The app also sent no security headers at all. It now sets nosniff, frame-ancestors self (as X-Frame-Options too, for the display bays' browsers) and a referrer policy. Deliberately NOT a page-wide CSP: this serves an SPA with inline styles, so a real script-src policy is a change worth making with the frontend in front of you, and a permissive header claiming one would be worse than having none. Contract 0.19.0. send_upload is on the shopdb.api surface, because a plugin serving user-supplied bytes should not have to remember these headers. The same bump records that get_dashboard_widgets has taken data and shape rather than a component name since the dashboard was rebuilt - that shipped without a bump, while BasePlugin and PLUGIN-HOOKS.md both still documented the shape nothing renders, which is how five plugins came to declare widgets pointing at components nobody had written.
This commit is contained in:
@@ -9,7 +9,7 @@ The contract is locked in [ADR-001](../docs/adr/ADR-001-asset-as-platform-contra
|
||||
The framework declares its contract version in `shopdb/__init__.py`:
|
||||
|
||||
```python
|
||||
__contract_version__ = '0.18.0'
|
||||
__contract_version__ = '0.19.0'
|
||||
```
|
||||
|
||||
Each plugin's `manifest.json` declares the range of contract versions it supports:
|
||||
@@ -154,20 +154,39 @@ class PrintersPlugin(BasePlugin):
|
||||
|
||||
### `get_dashboard_widgets() -> List[Dict]`
|
||||
|
||||
Returns dashboard widget definitions for the home page.
|
||||
Returns dashboard card definitions for the home page.
|
||||
|
||||
A card declares DATA AND SHAPE, never a component name. Core owns a small set of
|
||||
generic renderers and draws the card; the plugin says what to show, where it
|
||||
comes from, and how to link it.
|
||||
|
||||
**Changed in contract 0.19.0.** The previous shape named a Vue component per
|
||||
widget (`'component': 'NotificationsWidget'`). That cannot survive a lean build,
|
||||
because a plugin's component may never be staged into the frontend bundle
|
||||
(ADR-013), and in practice five plugins declared widgets pointing at components
|
||||
nobody had written - so they rendered as nothing. A card using the old shape is
|
||||
ignored. This is the same correction ADR-010 already made for asset panels.
|
||||
|
||||
```python
|
||||
class NotificationsPlugin(BasePlugin):
|
||||
class GeEnforcePlugin(BasePlugin):
|
||||
def get_dashboard_widgets(self):
|
||||
return [{
|
||||
'name': 'recent_notifications',
|
||||
'component': 'NotificationsWidget',
|
||||
'endpoint': '/api/notifications/recent',
|
||||
'size': 'medium',
|
||||
'position': 1,
|
||||
'id': 'geenforce-failures', # stable, unique across plugins
|
||||
'title': 'Enforcement failures',
|
||||
'endpoint': '/api/geenforce/dashboard/failures',
|
||||
'render': 'exceptions', # a core renderer, not a component
|
||||
'severity': 'critical', # orders cards on the page
|
||||
'permission': 'geenforce.manage', # hidden without it
|
||||
'empty': 'hide', # say nothing when there is nothing
|
||||
'position': 10,
|
||||
'viewall': '/geenforce', # optional link behind the heading
|
||||
'map': {'title': 'hostname', 'detail': 'entryname'},
|
||||
}]
|
||||
```
|
||||
|
||||
`empty: 'hide'` is not cosmetic. A card that reports "nothing wrong" every day
|
||||
teaches people to stop reading the page.
|
||||
|
||||
Consumed by `GET /api/dashboard/widgets`, which merges widgets from all enabled
|
||||
plugins sorted by `position` (disabled plugins are skipped; a broken plugin is
|
||||
isolated in prod, re-raised in dev/test).
|
||||
|
||||
Reference in New Issue
Block a user