Commit Graph

20 Commits

Author SHA1 Message Date
2a3afb5107 feat(16.2-07): purge the host site cache on every admin post and category write
- posts: FormAfterCreate/Update purge after the content_html translations,
  new FormAfterDelete; categories: FormAfterCreate and FormAfterDelete
- NewSiteCachePurger refuses half configuration, non-http(s) URLs, user
  info, fragments, http to a non-loopback host and tokens under 32 chars;
  Boot logs the endpoint host and path once
- TestPostsAdminPurgesSiteCache smoke test
- README: Configuration section (keys, contract, timing, scope), API rows
2026-10-10 18:27:12 +02:00
f3b223f131 feat(16.2-07): purge the host site cache after a category admin update
- classes.SiteCachePurger posts {kind, slug} with a bearer token to
  golem15.journal.site_cache.purge_url, 2 s timeout, logged, never fails a save
- PurgeSiteCacheAfterCommit runs it through lagoon.AfterCommit
- Boot builds and publishes the purger from the two config keys
- categories FormAfterUpdate registers the purge; Postgres test proves
  one authorized request after the commit
2026-10-10 18:19:58 +02:00
2c961b8408 fix(16.2): WR-03 never let concurrent media uploads overwrite each other
uniqueMediaKey checked Exists and the write followed without a
precondition, so two uploads of the same name (clipboard images are all
image.png) could both pick the free key and the second replaced the
first. StoreMedia now reserves the chosen key in-process while it
writes, and writes with IfNotExist; a key another process stored after
the check moves the upload on to the next free name.
2026-10-10 16:35:40 +02:00
5af746f31f fix(16.2): WR-04 serve content_html for markdown that starts with a tag
LooksLikeHTML decided by the first character alone, so a markdown post
opening with a hero <img>, a <div> or a comment was served as raw
markdown and its first edit stored ## and ** markers as HTML in both
columns. Load now serves such a value as HTML only when content_html is
empty or missing, or holds that same HTML (the value or SanitizeHTML of
it, white space at the ends ignored); otherwise it serves content_html.
2026-10-10 16:34:16 +02:00
159323197d fix(16.2): CR-01 serve content_html when a locale's content is blank
A translation that carries only content_html (an API write through
FormatTranslation) opened as an empty editor, and the first edit in
that locale silently replaced the translated body. Load now serves the
htmlColumn attribute whenever the stored content is blank and that
attribute is not empty; an untouched save echoes it and is skipped, an
edit writes the edited HTML to both columns.
2026-10-10 16:33:08 +02:00
da7a0732c3 test(16.2-06): full unit coverage of the Journal Blog Editor field types
- Decode: clean values byte for byte, nil, non-string refusal, the 35
  unsafe payloads stripped with notices exactly when changed
- notices order, ten-name cut, plural forms in lang/en and lang/pl
- LooksLikeHTML, options, ListValue, Upload sniffing and safe names
- StoreMedia layout, suffixes, no overwrite, size and folder rules
- editor corpus and policy cross-check extended in place: more negatives,
  notices only when stripped, stable second pass
- TestPostsEditorHooks: create and update, default and non-default
  writes, no writes, no writer
- TestCategoriesAdminBlogEditor (Docker host): untouched saves keep
  imported markup and NULL, clean edit, strip and report, blank to NULL,
  editor upload
2026-10-10 15:55:00 +02:00
d5a5413525 test(16.2-06): pin the imported-post resave contract
- element.test.ts: an untouched value survives Source tab, tab buttons,
  preview and reattach byte for byte with no summer-change
- TestBlogEditorLoad: HTML verbatim, markdown as content_html, FormatHTML
  fallback, nil and empty values
- TestPostsAdminResaveKeepsHTML (Docker host): GET then PUT the served
  values for an HTML and a markdown post, exact bytes, no notices, then a
  clean edited save in both locales
2026-10-10 15:42:31 +02:00
2384a61e38 feat(classes): allow the Blog Editor's own markup in the HTML policy
- figure data-embed ^youtube$ and data-id ^[A-Za-z0-9_-]{11}$ (the embed
  placeholder), img alt with any text, ol start ^[0-9]+$; nothing else
- TestEditorOutputPassesPolicy checks every editor-corpus.json entry byte
  for byte with an empty report, plus the negative cases;
  TestSanitizeHTMLReport pins the removal report
- CheckHTML documented as a predicate no write path refuses with; README
  lists the four additions and the report API
2026-10-10 12:11:29 +02:00
cb1c688213 feat(admin): category content on the Blog Editor and editor image uploads
- category content is the blogeditor type (uploadFolder categories): it is
  stripped and reported like post content; the 422 CheckHTML gate is
  retired and blank content still becomes NULL
- blogeditor and mlblogeditor Upload: magic-byte check for JPEG, PNG, GIF
  and WebP matching the extension, safe lowercase names, storage through
  classes.StoreMedia in the field's uploadFolder
- TestBlogEditorUploadSmoke, TestCategoriesAdminContent asserts the strip,
  README describes the field types, admin saves and editor uploads
2026-10-10 12:08:51 +02:00
15fcad7a93 refactor(api): share media storage with the editor upload
- classes.StoreMedia holds the media upload route's storage: extension
  allowlist, 10240 KiB cap, /journal/<folder>/<name> keys with the unique
  suffix, bucket write and public URL
- UploadMedia keeps authentication, permissions, the folder and form checks
  and answers with the same 201 body and messages
- uniqueMediaKey fails instead of overwriting when every numbered name is
  taken; the media path tests move to classes
2026-10-10 12:08:42 +02:00
fbd1c85168 feat(admin): post content on the Blog Editor field type
- blogeditor and mlblogeditor field types (classes.BlogEditorTypes) on the
  plugin; post content is mlblogeditor with htmlColumn content_html
- Decode stores SanitizeHTMLReport output per locale with notices naming
  what was removed; clean HTML is stored byte for byte, nothing is refused
  because of markup
- Load serves HTML as stored and markdown posts as their content_html
- the admin hooks copy written content to content_html per locale without
  goldmark; untouched content leaves both columns as they are
- editor lang keys (en, pl plural notices), tracer smoke test, updated
  Postgres admin tests
2026-10-10 12:02:55 +02:00
cbf4891703 feat(classes): report what the HTML policy removes
- SanitizeHTMLReport returns the policy output plus HTMLRemovals: removed
  elements, attributes (repeats and changed values included) and comments,
  at most ten distinct names per list with exact counts
- SanitizeHTML is the string half of it; FormatHTML, FormatTranslation and
  CheckHTML keep their behaviour
2026-10-10 11:59:45 +02:00
5f47083450 fix(classes): treat duplicate attributes as a change
golang.org/x/net/html's tokenizer keeps only the first of repeated
attribute names, so FormatHTML's byte-preserving comparison saw
<a href="https://x" href="javascript:...">, <img src=ok
src="javascript:..."> and a repeated style as unchanged and stored the
raw bytes, and CheckHTML accepted them.

htmlTokens now scans each start tag's raw bytes for its attribute names,
following the tokenizer's own readTag rules, and reports the document as
changed when that list differs from the attributes the tokenizer kept.
Such input is stored as bluemonday's output, which keeps the first value
as browsers do, and CheckHTML refuses it. Input without repeated
attributes is still stored byte for byte. A differential run of 2M
random tags found no disagreement between the scan and the tokenizer.
2026-10-10 00:13:17 +02:00
0d00ec8e0d fix(api): sanitize translated content_html on every write
A translated content_html sent through the Journal API (syncTranslations
formatted only when content_html was empty) or through
journal:import-posts (no check) was stored verbatim and served by the
anonymous GET /_journal/api/v1/posts and /posts/{slug}. A translation
keyed on the default locale also wrote the host content_html column
past FormatHTML.

classes.FormatTranslation now sets content_html for every translated
locale before it is stored: rendered from a non-blank content with
FormatHTML (host-column parity; a supplied content_html is replaced),
otherwise a supplied content_html is sanitized with the new
classes.SanitizeHTML, which FormatHTML reuses. Both write paths call it.

PHP parity note: Winter's PostApiController::syncTranslations called
Post::formatHtml only when content_html was empty and otherwise stored
the supplied content_html verbatim; ImportPosts::syncTranslations stored
it verbatim; the MLJournalMarkdown admin widget always derived it from
content via formatHtml. The Go port now follows the admin widget on
every path. Response shapes and status codes for valid input are
unchanged; only the stored content_html of unsafe or content-mismatched
translations differs.
2026-10-10 00:09:47 +02:00
a3551f2bdf fix(classes): sanitize content_html with bluemonday instead of regex checks
- FormatHTML sanitizes the goldmark output with a bluemonday UGC policy
  extended only with what goldmark and the imported posts and categories
  use; unsafe markup is stripped, as Winter's Html::clean strips it
- HTML the policy keeps whole is stored byte for byte (token comparison)
- CheckHTML keeps its signature and 422 contract, refusing HTML the
  policy would change
- the three regexes are gone: the CR-01 bypasses are neutralized and
  "Data:" or "metadata:" text is no longer refused (WR-01)
2026-10-09 23:35:23 +02:00
03907ee387 test: FormatHTML keep and refuse coverage, admin re-save keeps HTML
- TestFormatHTMLKeepsRawHTML: imported-style bodies (paragraphs, attributes, https links and images, entities) round-trip; markdown keeps a trailing raw footer and style
- TestFormatHTMLRejectsUnsafeHTML: every unsafe case must return an error now that the gate is the only defence; adds svg onload, mixed-case schemes and tags, object, embed, script in an HTML block and raw data URLs
- TestPostsAdminResaveKeepsHTML: an admin update of HTML content keeps content_html
2026-10-09 22:48:59 +02:00
528d2d8451 fix(classes): keep raw HTML in content_html behind the safety gate
- FormatHTML renders with goldmark html.WithUnsafe(), as Winter's
  Markdown::parse passes raw HTML through, so an admin save, an import or
  an API write no longer turns imported HTML into raw HTML omitted
- rejectUnsafe still runs after the render and refuses script, iframe,
  object and embed tags, event handlers and javascript, vbscript and data
  URLs; API response shapes are unchanged
- TestFormatHTMLKeepsRawHTML covers HTML paragraphs and a raw footer
2026-10-09 20:58:49 +02:00
f22d08902e feat(admin): edit category content in the admin form behind the HTML gate
- add a content textarea (size huge) after description, with en and pl phrases
- add content to Category.Fillable
- export classes.CheckHTML and run it from FormBeforeCreate and FormBeforeUpdate:
  unsafe category content answers 422 on content, blank content is stored as NULL
2026-10-09 16:26:12 +02:00
Jakub Zych
d09edde080 test(15-04): complete PHPUnit map, migrations, FormatHTML, and search-gate tests
Named JOURNAL-001/002 fillable, FormatHTML unsafe tags, rollback/remigrate, RSS/categories/tags/media, and TestSearchGateOff with zero Typesense HTTP.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 19:18:53 +02:00
Jakub Zych
dea0934950 feat(15-02): add Posts admin with mlmarkdown and FormatHTML
YAML-driven Posts screen stores translatable markdown and regenerates
content_html in the plugin. Publish writes without access_publish are
refused; list and form queries stay on the principal without
access_other_posts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 18:41:10 +02:00