Repository navigation
1.11.0 — SilverStripe 5.2 line
Added
-
(#119) New
ownsunpublish mode —records/$ClassRef/$ID/unpublish({"mode": "owns"}):
unpublishes the record, then every descendant reachable through its$ownsconfig
(OwnedTreeWalker::walkOwnedExcluding(), a new sibling of the walker's existingwalk()),
authorization-checked the same wayownspublish mode already is (classactionverb +
canEdit()), before writing anything. This is the unpublish half #119 left open when its
publish half shipped (1.9.0/2.4.0) — the issue itself flagged it as the harder, riskier half,
not a symmetric mirror:$ownsroutinely namesFile/Image, and a single asset is routinely
owned by more than one live record at once (one file ID simultaneously serving a hero slide, a
CTA card, and a page's own product image is a realistic shape). Unpublishing a
shared asset out from under a page that never asked to be touched would 403 that page via
AssetControlExtension, with no way to see why.ownsmode never unpublishes aFile/Imagereached through the walk —is_a()-excluded at
the walker itself (covers a project's ownFilesubclass too), not filtered from the result
afterwards, so nothing reachable only through an excluded asset leaks into the target set
either. Every excluded node is still reported, in a newskippedlist
({id, className, reason: "SHARED_ASSET_CLASS"}), not silently dropped. This is a deliberate
type exclusion, not a reference count: a count would need to be exhaustive across
has_one/has_many/many_many to be safe, and getting that wrong reproduces the exact hazard the
exclusion exists to prevent — an owned asset is simply never touched by this cascade.Two more deliberate asymmetries with
ownspublish mode: the root unpublishes first, then
descendants (the opposite order from publish, which writes leaves before the root) — so there
is never a window where a page is live but points at elements that have already been pulled;
and there is no$additionalparameter — publish's internal callers use it to reach an
element's own has_many children (sinceBaseElementdeclares no$owns), and unpublish has no
call site with the same problem.Composes with, rather than replaces, the existing
Hierarchystranded-descendants guard (#71)
and itsforcebypass — checked on the root AND every walked target, not just the root: nothing
prevents an owned relation from itself being aSiteTreewith its own live tree children, and
the guard is a per-record risk, not a per-call-site one (/review-prcaught the root-only cut
of this before merge).RecordActionsHandler'sowns-mode write now runs inside aDbTransaction
(matching howPageHandler::applyTemplate()wraps its own owned-tree cascade), so a
doUnpublish()hook throwing partway through the descendant loop rolls the whole cascade back
instead of leaving a live orphan (root unpublished, only some descendants following it) with no
way to tell from the response alone. TakesdryRun(previously400 PAYLOAD_INVALIDon every
unpublish call — narrowed to permitdryRunwithmode: "owns"specifically; a plain
single-mode unpublish andarchivestill reject it outright). Real (non-dryRun) response
addsmeta.unpublished(root first)
andmeta.skippedalongside the existing serialized-record shape;dryRunresponds with
{"data": {"wouldUnpublish": [...], "skipped": [...]}, "meta": {"operation": "unpublishDryRun", "mode": "owns"}}. Spec bumped tov1.15.