github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Hoist per-viewer trustmask lookup out of the tag loop (#3646) (#3647)

  • Hoist per-viewer trustmask lookup out of the tag loop (#3646)

Rendering a tag list called LJ::S2::TagDetail once per tag, and for a logged-in non-owner that recomputed the viewer's trust relationship (trusts_or_has_member + trustmask) for every tag. Both resolve to _trustmask($u, $remote), an unmemoized memcache round-trip, so a journal with thousands of tags fired thousands of redundant gets and took ~20s.

The relationship is identical for every tag, so compute it once via the new LJ::S2::tag_viewer_context and pass it into TagDetail. As defense-in-depth, memoize trustmask per request in a dedicated %LJ::REQCACHE_TRUSTMASK (cleared in start_request, invalidated on edge changes) rather than the bare %LJ::REQ_CACHE, which is never cleared between requests and would leak stale masks across viewers.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

  • Trim comments in the trustmask fix

Cut a redundant per-line comment, condense two headers, and drop duplicate issue references left over from the first pass.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

  • Drop the redundant %LJ::REQ_CACHE warning from the memoization comment

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Load LJ::Location where it's used so entry locations render again (#3645)

  • Load LJ::Location where it's used so entry locations render again

LJ::currents renders an entry's current location by calling LJ::Location->new inside an eval, but nothing on the render path ever loaded LJ::Location -- it was only ever called, never used/required. Once Apache/mod_perl (which broadly preloaded modules at startup) was retired in favor of Starman-only, the module stopped being resident, so the eval died silently, $loc came back undef, and the location was dropped from every entry. Mood and music don't go through LJ::Location, which is why they kept working.

Add use LJ::Location to the three files that call it: LJ::Entry (the user-visible display path), LJ::Protocol (current_coords validation), and LJ::Hooks::Setters (the icbm/location setter). Add t/currents.t, which exercises LJ::currents without loading LJ::Location itself and asserts the location renders -- it fails against the pre-fix tree and passes now.

CODE TOUR: If you set a "current location" on a post, it recently stopped showing up on the entry even though mood and music still did. The value was being saved correctly -- the site just wasn't displaying it. This restores the current location on entries.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

  • t/currents.t: use the Dreamwidth-only license header

It's a new file, not forked from LiveJournal; drop the fork boilerplate.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Don't memoize the trusted-anon verdict in %LJ::REQ_CACHE (#3644)

%LJ::REQ_CACHE is never cleared between requests, so the cached verdict leaked across requests -- and therefore across visitors -- on a persistent worker. Drop the caching entirely: validation is one HMAC plus an already-request-cached load_userid, and only runs where a captcha would otherwise be shown. Adds a regression test simulating back-to-back requests from different browsers on one worker.

Co-authored-by: Claude Fable 5 noreply@anthropic.com

Commit: ca89c2f6 Author: Mark Smith

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Skip captchas for logged-out browsers that recently held a good session (#3643)

  • Fix DW::Captcha->site_enabled when called as a class method on the base

3594 changed the abstract base's implementationenabled to return 0, which

made every class-method DW::Captcha->site_enabled call return false -- so require_captcha_test bailed early (disabling all comment captchas) and the per-journal comment captcha setting hid itself. Have the base report whether any implementation is enabled instead; subclass behavior is unchanged, and a fallback base instance still cleanly no-ops when nothing is configured.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

  • Skip captchas for logged-out browsers that recently held a good session

Adds an HMAC-signed "ljtrust" cookie (LJ::Session::update_trust_cookie, set alongside the master cookie at login/renewal and re-signed on uniq rotation; never cleared on logout). It's signed over the userid, the browser's ljuniq ident, and an issue time, using the rotating LJ::get_secret pool.

LJ::Session->trusted_anon_user validates it on logged-out requests -- sig, current-uniq binding, 60-day age, cookie generation -- then re-checks the account's standing live (visible, validated email, individual), so suspension revokes the bypass immediately.

A trusted anon then gets the logged-in treatment from both captcha gates: the interstitial view gate (DW::Captcha::should_captcha_view, checked lazily at each would-show point so trusted browsers never feed the fraud tempban counter) and the comment checks (LJ::Talk::Post::require_captcha_test: journal setting R and comment_html_anon are skipped; F/A, maxcomments, rate limits, and sysbans -- everything a logged-in user would still face -- are not). Each skip increments dw.captcha.bypassed with tags mirroring dw.captcha.shown.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

  • Address review: derive trust window from session_length, tag bypass metric with impl type

TRUST_COOKIE_MAX_AGE now delegates to LJ::Session->session_length('long') instead of duplicating the value, and dw.captcha.bypassed carries the same type: tag as dw.captcha.shown so the two slice identically.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Auto-deploy web-shop to follow web-stable (#3642)

Add a deploy-shop-follows-stable job to web22-deploy.yml that promotes the same image digest to the web-shop tier after a web-stable deploy succeeds. Guarded on inputs.service == 'web-stable' so shop/canary/unauth deploys don't fan out, and needs: deploy so a failed stable deploy leaves shop on its current version.

Co-authored-by: Claude Opus 4.8 (1M context) noreply@anthropic.com

Commit: d140c907 Author: Mark Smith

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Stop eagerly populating talk2row in get_talk_data (#3640)

  • Stop eagerly populating talk2row in get_talk_data

get_talk_data rebuilt the packed talk2 blob under its lock, and inside that loop it also wrote a per-comment talk2row cache entry for every comment via one LJ::MemCache::add each -- thousands of sequential memcache round-trips held the lock for 10s+ on large threads, which is what made comments intermittently vanish (readers timed out waiting, per the diagnostics in #3639).

Drop the eager per-row write. The talk2row cache is already populated lazily and in batch by get_talk2_row_multi (get_multi read, single IN() DB fallback, populate-on-miss), so the eager loop was redundant pre-warming -- and it warmed all N rows when a page shows ~25. Both paths write identical talk2row schema fields, so the cached value is unchanged; only the timing (lazy vs eager) moves.

Adds t/comment-talk2-rowcache.t verifying comments load correctly and consistently from a cold cache through both get_talk_data and get_talk2_row_multi (run with and without memcache via memcache_stress).

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

  • Test the comment caches across a full read/write lifecycle

Rework t/comment-talk2-rowcache.t into a cache-lifecycle test that asserts the database is read only on a cache miss and memcache serves warm reads, through reads, writes, and invalidations: cold read hits DB once and caches the blob (without eagerly caching per-comment rows); warm read serves from memcache; get_talk2_row_multi hits DB once then serves warm; posting a comment clears the blob but leaves unrelated rows; deleting a comment clears the blob and that comment's row; and reads after each write regenerate correctly.

Adds two test-only hooks ($LJ::TGET_TALK_DATA_DB, $LJ::TGET_TALK2_ROW_DB, guarded like the existing TGET_TALK_DATA_MEMCACHE hook) so the test can count DB reads on each comment-loading path.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

  • Test: guard loader return values before dereferencing

Assert get_talk_data returned data and get_talk2_row_multi returned both rows, and make the is_deeply key-derefs undef-safe, so an unexpected undef in the test env fails cleanly instead of dying in keys %$undef.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

get_talk_data: convert lock to DW::Locker + diagnostics for vanishing comments (#3639)

Commit: 8ebeaa2d Author: Mark Smith

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Add startup-cron.sh: run-once entrypoint for scheduled ECS tasks (#3638)

  • Add startup-cron.sh: run-once entrypoint for scheduled ECS tasks

startup-prod.sh loops forever to keep a resident queue worker alive, which is exactly wrong for a scheduled (cron) task -- it must run its command once and exit so EventBridge Scheduler launches a fresh task next time. This entrypoint validates config, then execs the command so its exit code becomes the task's (what a task-failure alarm keys on). Overlap between runs is guarded in the app layer (ljmaint via DW::Locker's global GET_LOCK), not here.

Prep for moving the ljmaint/stats crons to EventBridge Scheduler -> ECS RunTask (companion dw-terraform change).

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01JhHtJhPnKr7zaCHZsfAd1P

  • Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI 175728472+Copilot@users.noreply.github.com

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Suspend userpics via the suspend/unsuspend commands; fixes for listings, caching, and CDN purge (#3637)

  • Fix suspended userpics: hide from listings, don't block on CDN purge

Follow-up to #3636. Two bugs surfaced when suspending a pic in production:

  1. The suspended icon stayed on the icons page and in counts, because load_user_userpics only filtered 'X' (expunged). Exclude 'S' too, so suspended pics drop out of every listing/count/selection path (both icons pages, the API, RPC). The serving handler no longer relies on the pic being in that list -- it drops no_expunged so the get() DB fallback still finds suspended (and expunged) pics, serving the default for suspended and 404 for expunged/missing.

  2. Suspending "kept loading": the Bunny purge ran with async=false (Bunny holds the connection until edge propagation) inline in the request. Switch to async=true and a 5s timeout so the purge is fire-and-forget and a slow or unreachable CDN can't hang the console; the suspension takes effect regardless.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

  • Rework: keep suspended pics cached, annotate instead of hide

Addresses review feedback that the prior approach forced a DB lookup on every userpic cache miss. Instead of excluding suspended pics from load_user_userpics (which needed a per-request DB fallback in the serving handler), keep them in the memcached list:

  • load_user_userpics returns suspended pics again, and the handler uses no_expunged, so serving stays DB-free -- a suspended pic is found in the cached list and served as the default image; expunged/missing return 404.
  • The manage-icons page (edit/icons.tt) now shows suspended icons with a "[Suspended]" note and disabled controls (mirroring inactive) rather than hiding them, so the owner isn't left wondering where an icon went. They still can't be selected for posting (get_userpic_info filters them) or set as default (EditIcons update_userpics guards).
  • Add a 1-year Cache-Control to userpic responses so the CDN actually caches them; "immutable" is intentionally omitted so browsers still revalidate.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

  • Fold userpic suspend into the existing suspend/unsuspend commands

Per review, mirror the entry-suspension feature instead of inventing new commands: the existing suspend/unsuspend console commands now dispatch on a userpic URL (new LJ::Userpic->new_from_url, paralleling LJ::Entry->new_from_url) to suspend/unsuspend a single icon -- gated on the same suspend priv and logged to statushistory as suspend/unsuspend. Delete the standalone SuspendUserpic/UnsuspendUserpic command modules and their now-unused PrivList statushistory types.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

  • Exclude suspended pics from selection; split cache TTL; tidy comments

  • icon_keyword_menu and the REST icons API built usable-icon lists from load_user_userpics filtering only inactive/expunged, so suspended pics leaked in as selectable now that they stay in the list. Exclude suspended there too.

  • Cache-Control: keep the year-long CDN cache via s-maxage but use a 24h browser max-age, so a suspended/purged pic clears from a browser within a day.
  • Trim comments to describe the present code (no decision/absence narration).

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

  • Handle suspend edge cases: render/submit race and command feedback

  • edit/icons.tt now emits a hidden pic_suspended_$pid flag (mirroring pic_inactive), and update_userpics honors it, so an icon unsuspended between page render and form submit isn't treated as editable -- which would have cleared its disabled keyword/comment/description fields.

  • The suspend/unsuspend commands now check the return of suspend_userpic/unsuspend_userpic (error out on failure) and surface the hook output, restoring the CDN purge success/failure feedback to the operator.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Add suspend/unsuspend for individual userpics (DMCA) (#3636)

Commit: dc3135de Author: Mark Smith

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Finish the web->web22 migration in the tooling (#3635)

  • Finish the web->web22 migration in the tooling

The old "web" build/deploy was retired (12730963b) but the tooling still pointed some web services at it, so dwtool deploy web-stable-service resolved to the dead web-deploy.yml / ghcr.io/dreamwidth/web and failed.

  • dwtool: map web-canary/web-unauthenticated/web-stable to WorkflowWeb22 and ImageBaseWeb22 (config.go table + ecs.go classifyService switch), and drop the now-unused WorkflowWeb / ImageBaseWeb constants. web-shop was already correct. Updates the stale "ghcr.io/dreamwidth/web" example comments too.
  • task defs: point the web container image base at ghcr.io/dreamwidth/web22 in web-{canary,shop,stable,unauthenticated}-service.json. (The deploy workflow overrides the image at render time, so this was inert but misleading.)

Left as-is: the CloudWatch log-group names under /dreamwidth/web — that's a logging namespace, not the image repo or deploy pipeline.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01JhHtJhPnKr7zaCHZsfAd1P

  • Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI 175728472+Copilot@users.noreply.github.com

  • dwtool: drop the history-narrating comment on the web22 constant

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01JhHtJhPnKr7zaCHZsfAd1P

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Redesign /manage/icons for density and a mobile-friendly layout (#3634)

  • Redesign /manage/icons for density and a mobile-friendly layout

The page split the upload form and the icon list into two side-by-side columns (large-4 / large-8) that stacked below 1024px, burying the icon list under a tall full-width upload box. Rework it into a full-width upload row above a responsive icon grid (two-up on screens >= 640px, one-up on mobile), and tighten the upload form:

  • Source is now an "Upload file" button plus an "or enter URL" field instead of two radios with separate labels and inputs. Each upload row carries a hidden src_N field that the JS sets from the chosen source; the controller now gates every row by its own src_N (previously only the first row was gated, by a single src), so an unused file/URL input never trips an empty-input error.
  • One "Add another" button beside the primary action (now "Upload") replaces the separate add-from-file / add-from-URL buttons; editicons.js builds a combined-source extra-upload row.
  • Fields sit on one aligned grid (right-aligned labels, inputs, help text to the right); "About Icons" becomes a "?" toggle by the heading; the "acceptable formats" line is dropped (upload already errors on bad types).

Retire the strings this removes (fromfile/fromurl + accesskeys, btn.addfile/addurl, formats.desc) via deadphrases.dat.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01JhHtJhPnKr7zaCHZsfAd1P

  • icons: address review — Source label a11y + src_N re-render/autofill

  • "Source:" heads a file-or-URL group, not one control, so make it a non-label element (styled the same) instead of a

  • Default src_0 from the re-rendered formdata (url when urlpic_0 is populated) so a validation-error re-submit uses the URL field.
  • Initialize each row's src_N from any pre-filled file/URL value on JS bind, covering form re-render and browser autofill.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01JhHtJhPnKr7zaCHZsfAd1P

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Preserve empty form fields in DW::Request arg parsing (#3633)

  • Preserve empty form fields in DW::Request arg parsing

stringto_multivalue split an empty value on \0, and split(/\0/, "") returns (), so a submitted-but-empty field (foo=) produced no output pairs and was dropped from the Hash::MultiValue -- get_args/post_args returned undef for it instead of "". Keep a single "" when the split is empty.

This is a BML->Plack regression: the old Apache::BML populated %GET/%POST/ %FORM directly and kept empty fields, but BML pages now read form data via $r->get_args/$r->post_args, which route through stringto_multivalue. It broke the customize form, which relies on empty values being passed back.

Diagnosis by momijizukamori.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_0129Jpyx2K3DGDYZPTjXjnkz

  • Preserve trailing empty values in repeated params

split(/\0/, ...) without a negative limit drops trailing empty fields, so a repeated param ending empty (empty=x&empty=, joined to "x\0") still lost its final "". Use a -1 limit, and add GET/POST tests for the repeated-param case.

Addresses review feedback on #3633.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_0129Jpyx2K3DGDYZPTjXjnkz

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

CI: add request-handling tests to the fast run

t/request-*.t exercise the DW::Request abstraction (multi-value params, POST body parsing) -- foundational to every form/controller and cheap (~1s each). Placed above the Plack integration step.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

Commit: 4be012c3 Author: Mark Smith

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Test-suite health: fix use-strict.t, add core-flow CI steps + nightly full run (#3632)

  • use-strict.t: discover files by walking the tree, not git

The test enumerated Perl files via git ls-tree HEAD, which fails inside the devcontainer: $LJHOME/.git is a worktree pointer to a host path absent in the container, so it found zero files and died on an empty plan ("You said to run 0 tests"). CI doesn't run this test, so it was effectively dead coverage. Replace the git discovery with a File::Find walk (same skip filters, pruning vendored/ generated trees) so it runs -- and passes, 833 files -- in any checkout.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

  • CI: add core-flow test steps + nightly full-suite job

Per-PR CI (ci.yml) gains a curated set of core user-facing/security tests (auth, posting, commenting, access control, captcha, settings -- ~39s, all verified running with real assertions), grouped into steps for at-a-glance failure attribution. All test steps now carry if: ${{ !cancelled() }} so one failing group no longer hides the others in a single push.

nightly.yml runs the entire t/ suite once a day (and on demand) to catch rot in everything the fast PR run skips -- the slow tail, console/admin tests, migrations -- pinging Discord on failure. Mirrors ci.yml's container + setup.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

  • CI: skip redundant cpm install + tidy only changed files

Two speedups for the per-PR run (which was ~5 min, dominated by an ~85s full-tree tidy and a ~35s redundant dependency install):

  • A "Detect changed files" step diffs against the PR base (or push before).
  • CPAN deps are only reinstalled when doc/dependencies-* actually changed on the branch; the devcontainer image (rebuilt nightly + on dep changes) already has everything otherwise.
  • Formatting checks only the branch's changed .pl/.pm/.t via tidyall instead of re-tidying the whole tree; main is always tidy so that's sufficient. The full t/02-tidy.t still runs in the nightly. Both fall back to install-all/check-all if a base commit can't be determined.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

  • CI: use absolute tidyall path (no extlib symlink in CI checkout)

The changed-files tidy step called extlib/bin/tidyall, but that symlink only exists in the devcontainer, not CI's checkout. Point at the real binary (/opt/dreamwidth-extlib/bin/tidyall), which works in both.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

  • CI: normalize step names

Consistent scheme -- "Test - " for test steps, " ()" for the check steps -- instead of the misleading "Core" prefix (the plack/ cleaner/routing steps are just as core; they were named after what they cover).

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01J3Ak8rgLkb6FVSRKaMb8ea

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Trim CLAUDE.md to accurate, actionable guidance (#3631)

  • Trim CLAUDE.md to accurate, actionable guidance

Fix stale/wrong facts (tests use MySQL not SQLite; Starman runs 3 workers not one; drop rotting subtest/test-file counts; job queue is DW::TaskQueue, not TheSchwartz), correct two misleading operational lines (git runs on the host since a worktree's .git doesn't resolve in the container; CI runs only a subset of t/, so a green CI doesn't mean a change is tested), add a terse comment-style rule, and prune the Architecture section (~70 lines of reference material Claude can read from the code, and which had already rotted) down to a module map, the request pipeline, and the one real dev-container gotcha.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_0129Jpyx2K3DGDYZPTjXjnkz

  • Use tidyall -a for the auto-format command

Bare tidyall with no file args doesn't rewrite anything; -a formats the whole tree, matching the "Apply code formatting" intent and the rest of the file.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_0129Jpyx2K3DGDYZPTjXjnkz

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Add adversarial XSS corpus test for the HTML cleaner (initial) (#3627)

  • Add adversarial XSS corpus test for LJ::CleanHTML

An initial improvement to the cleaner test suite: a library of known XSS vectors (classic, HTML5, scheme-obfuscation, CSS, and mutation-XSS) run through the real clean_event / clean_comment / clean_userbio display paths, asserting no JavaScript-execution vector survives. The oracle checks whether an exec vector remains rather than matching exact output, so it stays meaningful as cleaner internals change.

Baselines what the current cleaner catches (the whole standard XSS canon -- 177 assertions pass) versus what it misses: clean_event's permissive allow-most mode lets SVG/MathML foreign content and its SMIL/XML-Events children through with only attribute-level filtering, whereas clean_comment's strict allowlist strips them. Those known gaps are marked TODO so the suite stays green while recording exactly what is missed.

More coverage to follow (other surfaces/formatting modes, non-XSS viewer harms, and a stronger test oracle).

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01JhHtJhPnKr7zaCHZsfAd1P

  • cleaner-xss: make the safety oracle browser-accurate; drop redundant clean

Address review feedback on the is_unsafe oracle:

  • Decode entities single-pass like a browser (decode_entities_once): raw / /: and numeric entities (hex any-case incl. A-F and uppercase X, plus decimal) are now decoded before scheme detection, so a cleaner regression that emitted an entity-obfuscated javascript: URL can no longer pass silently. An already-escaped 	 stays literal (inert) and is not re-decoded, so it does not false-positive. Whitespace and control chars are stripped the way browsers ignore them in URLs/schemes.
  • Add oracle self-checks asserting it sees obfuscated schemes/handlers and ignores inert escaped input and attribute-value text.
  • Clean each payload once and reuse it for both the assertion and the diag instead of cleaning twice.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01JhHtJhPnKr7zaCHZsfAd1P

  • cleaner-xss: cover all surfaces and formatting modes

Expand the corpus from clean_event/clean_comment/clean_userbio into a surface x mode matrix: entries and comments in each formatting mode (default html, preformatted, markdown, syndicated feed HTML), plus subjects (clean_subject / clean_subject_all / clean_and_trim_subject). Adds a markdown-only vector set for what Text::Markdown can generate (link/image destinations, reference links, autolinks, raw HTML passthrough, code spans) before the clean runs.

No new leaks: the current cleaner neutralizes the whole corpus across every mode and surface -- the modes share the security-critical allow/eat/remove lists, and subjects strip to their tiny allowlist. The one documented gap (SVG/MathML foreign content in clean_event) is unchanged and still TODO.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01JhHtJhPnKr7zaCHZsfAd1P

  • Add characterization tests for third-party resource loading in the cleaner

Document (rather than change) how the cleaner treats external resource loading in user content. Loading an external image or inline CSS url() leaks the viewer's IP/User-Agent to that host -- a privacy tradeoff DW intentionally accepts (hotlinking is a long-standing feature; DW::Proxy exists only to fix http->https mixed content, not to hide the viewer).

These tests pin that accepted behavior so any change is deliberate, and lock in the controls that stop the dangerous variants: blocks are removed (so there's no place for the attribute selectors that CSS-based exfiltration needs) and script-y CSS (url(javascript:), expression()) is stripped from inline styles. No behavior change.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01JhHtJhPnKr7zaCHZsfAd1P

  • cleaner-xss: address review + add clean_embed coverage

Review fixes: - Retarget the SVG/MathML TODO gaps to the dangerous constructs that actually survive (SMIL / driving handlers/navigation, XML Events , MathML foreign-HTML), instead of asserting "strip all svg/math" -- which a namespace-aware sanitizer wouldn't do anyway. - Add positive controls (benign inline formatting, a safe link, plain text are preserved) so a regression that strips everything can't pass as vacuously safe. - Scope the CSS checks (expression/@import/binding/url(js)) to style="" values and blocks so benign body text can't false-positive.

New coverage: - clean_embed (the RTE media-embed path) with a media-permitting oracle (allow_media => 1):

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

Remove the redundant, broken plack devcontainer (#3630)

.devcontainer/plack was the opt-in "test Plack" variant from when Apache was still the default. Plack/Starman is now the only server, so it is redundant -- and it was broken: its build.context was ".." (resolving to .devcontainer/), so the Dockerfile's COPY doc/... could not reach the repo-root doc/ and the build failed for any contributor who selected it in VS Code. The default config (prebuilt public image) is the one to use.

Claude-Session: https://claude.ai/code/session_0129Jpyx2K3DGDYZPTjXjnkz

Co-authored-by: Claude Opus 4.8 noreply@anthropic.com

Commit: a465c038 Author: Mark Smith

remove ICQ from other sites (#3613)

Jul. 1st, 2026 02:59 am
github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

remove ICQ from other sites (#3613)

Commit: 09eef1c0 Author: Carly Ho

allow rubytext in comments (#3624)

Jul. 1st, 2026 02:58 am
github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
[personal profile] github posting in [site community profile] changelog

allow rubytext in comments (#3624)

Commit: 87baba5b Author: Carly Ho

Profile

github: shadowy octopus with the head of a robot, emblazoned with the Dreamwidth swirl (Default)
github

July 2012

S M T W T F S
1234567
891011121314
15161718192021
22232425262728
2930 31    

Page Summary

Style Credit

Expand Cut Tags

No cut tags
Page generated Jul. 8th, 2026 03:52 am
Powered by Dreamwidth Studios