Jul 24 - Update Rich-text editor to support uploading images and inserting table

This commit is contained in:
2026-07-24 09:55:41 -04:00
parent 69b97d51fe
commit 10a8658b67
12 changed files with 227 additions and 968 deletions
+23 -10
View File
@@ -106,16 +106,28 @@ shows only `is_published` topics (sections with no published topics are hidden).
## Content editing (rich text / publish / reorder)
- **Rich text:** Quill 1.3.7 vendored at `static/vendor/` (no CDN — survives a
locked-down server or strict CSP). It's a *progressive enhancement over a real
`<textarea name="body_html" id="body_src">`*: `body.quill-on` is added only
after `new Quill()` succeeds (hides the textarea, shows the editor). If Quill
fails to load, the textarea stays usable and a save never wipes the body.
- **Rich text:** Quill 2.0.3 vendored at `static/vendor/quill.min.js` +
`quill.snow.css` (no CDN — survives a locked-down server or strict CSP). It's a
*progressive enhancement over a real `<textarea name="body_html" id="body_src">`*:
`body.quill-on` is added only after `new Quill()` succeeds (hides the textarea,
shows the editor). If Quill fails to load, the textarea stays usable and a save
never wipes the body. The submit handler keeps the body when it contains an
`img`/`table` even though `getText()` is empty for embed-only content.
- **Images & tables:** the custom `#editor-toolbar` has a `ql-image` button and a
`.ql-table-op` button group. Images upload via `POST /admin/upload`
(`login_required`, CSRF via `X-CSRFToken` header): extension allowlist +
magic-byte sniff (`_sniff_image`, SVG excluded), 8 MB cap, saved as a random
`uuid4().hex.<ext>` under `static/uploads/`, returns `{url}`; the handler
`insertEmbed`s it (no base64 → DB stays small). Tables use Quill 2's *built-in*
`table: true` module (`getModule('table')` → insertTable/insertRow…/deleteTable) —
no third-party plugin. Table buttons `preventDefault` on mousedown to keep the
cell selection; cell-dependent ops are wrapped in try/catch.
- **Sanitize on save:** `sanitize_html()` (bleach) runs on every `body_html`
write — allowlist `ALLOWED_TAGS`/`ALLOWED_ATTRS`, `strip=True`, and bleach
restricts link protocols to http/https/mailto (blocks `javascript:`). Empty /
`<p><br></p>` editor content is stored as NULL. The public page still renders
`body_html` via `Markup`, but the content is now sanitized at the source.
write — allowlist `ALLOWED_TAGS`/`ALLOWED_ATTRS` (now includes `img` + table
tags + `data-row`), `strip=True`; bleach restricts URL protocols on both `href`
and `img src` to http/https/mailto, blocking `javascript:` and `data:` (uploads
are relative `/static` paths). Empty / `<p><br></p>` editor content is stored as
NULL. The public page renders `body_html` via `Markup`, sanitized at the source.
- **Draft/publish:** `topic.is_published` (default 1, so existing rows stay
live). Public route filters to `Section.published_topics` and drops sections
with none. Admin: per-topic `/topic/<id>/toggle` (quick button) + a Published
@@ -139,7 +151,8 @@ sudo mysql < add_admin.sql # audit_log (existing DBs)
sudo mysql < add_publish.sql # topic.is_published (existing DBs)
# .env: SECRET_KEY, ADMIN_USERNAME, ADMIN_PASSWORD_HASH, SESSION_COOKIE_SECURE=1
sudo ./venv/bin/pip install -r requirements.txt # includes bleach
sudo systemctl restart jqc-features
sudo -u jqc mkdir -p static/uploads # in-body image uploads (writable)
sudo systemctl restart jqc-features # Quill 2 assets are static — no other step
# fail2ban:
sudo cp deploy/fail2ban/filter.d/jqc-admin.conf /etc/fail2ban/filter.d/
sudo cp deploy/fail2ban/jail.d/jqc-admin.local /etc/fail2ban/jail.d/