forked from github/cinny
🦉 allowing external images as setting
For the config.json (server-level default for new users):
```json
{
"defaultSettings": {
"externalImages": "homeserver"
}
}
```
User's own choice in the UI always overrides the config.json default.
This commit is contained in:
@@ -19,3 +19,8 @@ In this owl folder should go all documents we produce for this Endeavour.
|
||||
- Find out how we can set a theme by default for new users.
|
||||
- img tags in messages should be rendered, without requiring them to be on a local cache. We do not need to secure against identification attacks.
|
||||
- youtube links should create embedded players (if enabled).
|
||||
|
||||
|
||||
## Commit Messages
|
||||
|
||||
All Commits for OWL changes start with the Owl Emoji 🦉
|
||||
|
||||
176
owl/REPORT-002-external-images.md
Normal file
176
owl/REPORT-002-external-images.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# OWL Report #002 - External Images: Settings & Scoping
|
||||
|
||||
Date: 2026-04-15
|
||||
|
||||
## The Question
|
||||
|
||||
We've already removed the external image block (sanitize.ts + react-custom-html-parser.tsx). But should this be unconditional? Options:
|
||||
|
||||
1. Global toggle (user setting)
|
||||
2. Per-room setting
|
||||
3. Automatic based on federation status
|
||||
4. Homeserver-level default via config.json
|
||||
|
||||
## What Context Is Available at Render Time
|
||||
|
||||
The key rendering call chain is:
|
||||
|
||||
```
|
||||
RoomTimeline.tsx (has: room object, mx client, settings)
|
||||
→ getReactCustomHtmlParser(mx, roomId, params)
|
||||
→ HTMLReactParserOptions (handles img tags)
|
||||
→ RenderMessageContent (receives pre-built parser options)
|
||||
```
|
||||
|
||||
At the `RoomTimeline` level (line 528 of `src/app/features/room/RoomTimeline.tsx`), we have:
|
||||
- Full `room` object (Room from matrix-js-sdk)
|
||||
- `mx` (MatrixClient) — knows the homeserver domain
|
||||
- All user settings via Jotai atoms
|
||||
|
||||
### Federation Detection
|
||||
|
||||
Rooms carry an `m.federate` flag in their `m.room.create` state event:
|
||||
```typescript
|
||||
const createEvent = getStateEvent(room, StateEvent.RoomCreate);
|
||||
const isFederated = createEvent?.getContent()?.['m.federate'] !== false;
|
||||
```
|
||||
Default is `true` (federated). Local-only rooms explicitly set `m.federate: false`.
|
||||
|
||||
### Homeserver Detection
|
||||
|
||||
We can tell if a message sender is local:
|
||||
```typescript
|
||||
const senderDomain = event.getSender()?.split(':')[1];
|
||||
const isLocal = senderDomain === mx.getDomain();
|
||||
```
|
||||
|
||||
## Options Analysis
|
||||
|
||||
### Option A: Global User Setting
|
||||
|
||||
Add `allowExternalImages: boolean` to settings, default `false` (or `true` via config.json for owl).
|
||||
|
||||
**Pros:** Simple, one toggle, works everywhere.
|
||||
**Cons:** All-or-nothing. A privacy-conscious user can't allow it for trusted rooms only.
|
||||
|
||||
### Option B: Per-Room Setting (Room State Event)
|
||||
|
||||
Store a custom state event like `cx.owl.room.settings` with `{ allowExternalImages: true }` per room. Room admins control it.
|
||||
|
||||
**Pros:** Fine-grained, admin-controlled.
|
||||
**Cons:** Requires room admin action. UX overhead. Need UI for room settings. Only works in rooms we control.
|
||||
|
||||
### Option C: Automatic by Federation Status
|
||||
|
||||
If `m.federate === false` (local-only room) → allow external images. If federated → block.
|
||||
|
||||
**Pros:** Zero configuration, matches the threat model (federated = untrusted external servers can leak IPs via images).
|
||||
**Cons:** Most rooms default to `m.federate: true` even on a single homeserver. Users would need to explicitly create non-federated rooms. Doesn't match the real intent — on owl.cx, all rooms are effectively local even if technically federated.
|
||||
|
||||
### Option D: Homeserver-Level Default via config.json
|
||||
|
||||
Add to config.json:
|
||||
```json
|
||||
{
|
||||
"defaultSettings": {
|
||||
"allowExternalImages": "homeserver"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Where the value is one of: `"always"`, `"never"`, `"homeserver"` (only in rooms on this homeserver), `"local"` (only non-federated rooms).
|
||||
|
||||
**Pros:** Server operator controls the policy. Matches the owl use case perfectly — "we trust our own server."
|
||||
**Cons:** More logic to implement. Need to define what "homeserver room" means (all members local? room alias local? room ID local?).
|
||||
|
||||
### Option E: Layered Approach (Recommended)
|
||||
|
||||
Combine a global setting with smart defaults:
|
||||
|
||||
1. **Global setting** `externalImages`: `"always"` | `"never"` | `"homeserver"`
|
||||
- Default: `"never"` (safe for general Cinny users)
|
||||
- owl config.json sets: `"homeserver"` or `"always"`
|
||||
2. **"homeserver" mode**: allow external images when the room's server matches the user's homeserver (check room ID domain: `!roomid:owl.cx`)
|
||||
3. **Per-room override** (future): room admins can set `cx.owl.room.settings` state event to override
|
||||
|
||||
This gives us:
|
||||
- Safe default for upstream/general use
|
||||
- owl.cx sets `"homeserver"` via config.json — external images work in all owl rooms
|
||||
- Users can override to `"always"` or `"never"` in settings
|
||||
- Room-level control can be added later without changing the architecture
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Settings Layer
|
||||
|
||||
**`src/app/state/settings.ts`:**
|
||||
```typescript
|
||||
// Add to Settings interface:
|
||||
externalImages: 'always' | 'never' | 'homeserver';
|
||||
|
||||
// Add to defaultSettings:
|
||||
externalImages: 'never',
|
||||
```
|
||||
|
||||
**`config.json` (owl deployment):**
|
||||
```json
|
||||
{
|
||||
"defaultSettings": {
|
||||
"externalImages": "homeserver"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rendering Layer
|
||||
|
||||
**`src/app/plugins/react-custom-html-parser.tsx`:**
|
||||
Add `allowExternalImages?: boolean` to the params type. When false, revert to the original behavior (convert external img to `<a>` link).
|
||||
|
||||
**`src/app/features/room/RoomTimeline.tsx`:**
|
||||
Read the `externalImages` setting, check room context, compute the boolean `allowExternalImages`, pass it into `getReactCustomHtmlParser` params.
|
||||
|
||||
```typescript
|
||||
const [externalImages] = useSetting(settingsAtom, 'externalImages');
|
||||
const allowExternalImages = useMemo(() => {
|
||||
if (externalImages === 'always') return true;
|
||||
if (externalImages === 'never') return false;
|
||||
// 'homeserver': check if room belongs to our homeserver
|
||||
const roomDomain = room.roomId.split(':')[1];
|
||||
return roomDomain === mx.getDomain();
|
||||
}, [externalImages, room.roomId, mx]);
|
||||
```
|
||||
|
||||
**`src/app/utils/sanitize.ts`:**
|
||||
`sanitizeCustomHtml` needs the flag too, since it runs before the React parser. Either:
|
||||
- Pass it as a parameter: `sanitizeCustomHtml(html, { allowExternalImages })`
|
||||
- Or move all image logic to the React parser (simpler — sanitizer just passes img tags through, parser decides what to render)
|
||||
|
||||
The second approach is cleaner: the sanitizer allows img tags with any src (already done), and the React parser decides whether to render or convert to link based on the flag.
|
||||
|
||||
### Settings UI
|
||||
|
||||
Add a dropdown in `src/app/features/settings/general/General.tsx` under the "Media" or "Privacy" section:
|
||||
|
||||
```
|
||||
External Images: [Always / Homeserver Only / Never]
|
||||
```
|
||||
|
||||
### Files to Touch
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/app/state/settings.ts` | Add `externalImages` to Settings + defaults |
|
||||
| `src/app/plugins/react-custom-html-parser.tsx` | Accept + check `allowExternalImages` param |
|
||||
| `src/app/features/room/RoomTimeline.tsx` | Compute flag from setting + room context, pass to parser |
|
||||
| `src/app/features/settings/general/General.tsx` | Add UI dropdown |
|
||||
| `src/app/utils/sanitize.ts` | Already done — img tags pass through |
|
||||
|
||||
### What About the Sanitizer?
|
||||
|
||||
Current state: we already removed the img→link conversion in `sanitize.ts`. For the settings-based approach, we have two options:
|
||||
|
||||
**Option 1 (simpler):** Leave sanitize.ts as-is (allows all img src through). The React parser handles the allow/block decision. This means the sanitized HTML contains `<img src="https://...">` but the React parser may convert it to a link at render time.
|
||||
|
||||
**Option 2 (stricter):** Restore the original sanitize.ts behavior and only bypass it when `allowExternalImages` is true. This requires passing options to `sanitizeCustomHtml`, which changes its signature and every call site.
|
||||
|
||||
**Recommendation:** Option 1. The sanitizer's job is preventing XSS, not policy decisions. Let the React parser handle the rendering policy — it already has all the context it needs.
|
||||
Reference in New Issue
Block a user