Observed defect: a node's source_format column can disagree with the
actual bytes in data. A Collatz thread on foxhop.net has data filled
with RST (======, ---, :: blocks) yet source_format="markdown". The
earlier helper trusted source_format and returned the RST untouched,
so GET /api/v1/export/.../thread.md served raw RST to anyone who asked
for markdown.
data_html is the canonical rendered form — set at every write via
set_data() regardless of input syntax — so converting data_html → markdown
via pandoc sidesteps the label mismatch entirely. Falls back to raw
data if data_html is missing or pandoc fails.
Three fixes to thread/namespace export:
1. Nodes authored in non-markdown source_format (rst, mediawiki, latex, ...)
had their raw source dumped directly into the markdown renderer. A RST
thread exported as .md therefore yielded RST, not markdown. New
_node_data_as_markdown helper converts node.data through pandoc when
source_format != markdown, with a raw-source fallback on pandoc failure.
2. node_tree_to_markdown prepended '# {root_node.title}' on top of data
that already carries its own H1 (either native markdown or a converted
RST underline heading). Drop the prepend — the data owns the title.
3. convert() passed --metadata title=X which makes pandoc render a visible
title-block in HTML/PDF above the body. Combined with (2) and the data's
own H1 this showed the title three times in exported HTML/PDF. For
html-family outputs (and pdf via wkhtmltopdf) switch to -V pagetitle=X
so only the <title> tag gets populated; other formats still use the
proper --metadata title=X for real document metadata.
- New route /{node_id}/revisions rendered by revision-history.j2
- Show content/history tabs on wiki root nodes
- Export dropdown includes history (json + html) links on wiki namespaces
- Node action buttons now gate on can_wiki_edit (not can_alter_node)
so wiki-mode members can edit their own root nodes
- Suppress Topic link when URI matches current domain (self-reference)
- Tests covering revision timestamps, wiki_edit(), can_wiki_edit(),
/revisions endpoints, CSRF trusted origins, export menu gating,
pandoc rst→html rendering
moderators property returned self.enabled_users (every enabled member),
so every user who had ever posted got moderator powers: spam
approve/deny, edit/delete other users' comments, moderator Slack
notifications. Bug was universal across all namespaces — any site where
users post was affected.
Fix: moderators now returns only users with role='moderator'.
Owners now implicitly pass is_moderator() checks, since NamespaceUser
is single-role per user per namespace; this keeps owners functional
without needing a data migration.
Adds chaostheory theme to the shared env.tar.gz build so foxhop.net
(which sets app.theme = chaostheory and shares /opt/remarkbox/env with
my.remarkbox.com) has the theme's templates + static assets available
after deploy.
- views/__init__.py: add MAX_CONTENT_LENGTH = 500_000 (shared constant with comment
explaining the CWE-407 / bleach ReDoS rationale)
- reply_node.py: reject oversized content before set_data() / clean_raw_html()
- modify_node.py: same guard on edit path
- new_thread.py: same guard on new thread path
- requirements.py3.txt: tighten bleach>=2.1.4 -> bleach>=6.0.0 with CVE note
- test_render.py: unit tests for bleach version contract, API stability,
sanitization correctness, and ReDoS resistance timing
- test_views.py: functional tests for content length enforcement on all three
browser form paths (reply, new thread)
- list_nodes.py: strip empty tokens, cap keywords to 10 before passing to model
- node.py: add .limit(200) per keyword query — was unbounded .all()
- namespace.py: add .limit(500) on root.children in dict_dump — was unbounded iteration
- __init__.py: cap page_number to 1000 — large offsets force full table scans
Diff: GET /api/v1/revisions/{id}/diff/{other_id} — unified diff between two revisions.
UI: <details>-based export menus on thread and node views (works without JS).
UI: Revision history link for wiki-mode namespaces.
Maps which domains go through the edge proxy (142.93.73.64) vs direct to
origin (162.243.167.224). Prevents repeat of the 5-day meta/faq SSL outage
caused by missing proxy blocks.
Pyramid imports pkg_resources which was removed in setuptools 82.
Pin across requirements, Makefile, and CI scripts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Belt-and-suspenders: ensure setuptools is in the cached venv
before any Make target runs. Shell executor preserves env/
between builds but Python 3.12+ venvs lack setuptools.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The venv target is a no-op on cached CI runners where env/bin/activate
already exists. Move setuptools install into install-source-dev-and-test
and install-source-prod so it runs every time, regardless of venv cache.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The requirements.py3.txt change alone wasn't enough — pip treats
setuptools specially during editable installs and may not install
it as a runtime dependency. Installing it immediately after venv
creation ensures pkg_resources is available for Pyramid and theme
loading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pkg_resources (used for theme entry point loading) lives in
setuptools, which Python 3.12+ no longer bundles in venvs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lock/unlock, watch/unwatch, disable/enable, verify, approve/deny
now return JSON for XHR requests and swap client-side without a
page reload. Falls back to normal POST + redirect when JS is off.
The dynamic CSS sets margin-left: -48px on .nested-avatar to hang
avatars into .node padding. In the preview container (no matching
padding, overflow:hidden), this pushed the avatar off-screen.
Use plain .avatar class for preview avatars instead.
Browsers do not render images inside display:none containers and
re-assigning src does not reliably trigger re-render. Store the
avatar URL in a data attribute and create the img element via
document.createElement after the container is already visible.
Browsers skip rendering images inside display:none containers.
Re-assign the src attribute after making the div visible to force
the browser to render the SVG data URI.
Move the preview author header (avatar + name + timestamp) inside the
preview div as a persistent child element that JS shows/hides, rather
than serializing it via innerHTML which was failing to render the image.
<template>.innerHTML has DocumentFragment quirks. Use a plain
hidden div instead — its innerHTML is read directly from the
normal DOM to clone the avatar and author header for previews.