Skip to content

2.5.0 — SilverStripe 6 line

Choose a tag to compare

@jsirish jsirish released this 18 Aug 19:47
· 79 commits to 2 since this release
b03d4f6

Fixed

  • (#174, closing the known gap 1.9.0 left open) pages/$ID/apply-template with
    {"publish": "recursive"} now publishes each element's own duplicated descendants, not just the
    elemental area and its top-level elements.

    1.9.0 flagged this as unverified. The mechanism is now confirmed:
    TemplateElementDuplicator::duplicateElements() duplicates each template element with a bare
    $element->duplicate(), so DataObject::duplicate() decides what children get created. An
    element's children are only reachable by the publish walk if that element declares $owns
    itself — BaseElement doesn't — so anything duplicate() created that $owns didn't name
    stayed on draft after a "recursive" apply, unpublished and unauthorized, with no error and no
    signal.

    Scope, stated precisely: no stock Dynamic element actually hits this today. The elements
    with versioned children list them in $owns as well (ElementPhotoGallery => Images,
    ElementCard => ElementLink), so the existing walk already reached them; the one genuine
    cascade-only case, ElementStatCounters => Stats, has an unversioned child with nothing to
    publish. The gap is structural rather than currently-triggered — nothing stops an element
    declaring $cascade_duplicates without a matching $owns for a versioned child, and that
    combination silently loses content.

    PageHandler::applyTemplate() now walks each element via the new
    OwnedTreeWalker::walkDuplicates() and passes the result to publishOwnedTree() alongside the
    area and elements. That walk reproduces what duplicate() creates rather than reading
    $cascade_duplicates directly, which takes two corrections in opposite directions:

    • An empty $cascade_duplicates on a Versioned record makes the framework substitute
      $owns ∩ (many_many + belongs_to + has_many) (RecursivePublishable::onBeforeDuplicate()).
      Without this, grandchildren went missing at depth 2+, where the page-level $owns walk can no
      longer cover for it.
    • A many_many entry creates nothing — duplicateManyManyRelation() link-copies, attaching
      the same pre-existing records to the clone. Publishing those would push shared, possibly
      deliberately-draft records live and would newly 403 on classes no project allowlists.

    OwnedTreeWalker's cycle guard, depth cap, diamond handling, and unversioned-record handling are
    unchanged and now serve both walks. walk()'s signature is unchanged.

    Behavior change, worth reading before upgrading: a class reached only through the new walk
    that grants create/update but withholds action now gets 403 FORBIDDEN_CLASS on a
    "recursive" apply-template, and the whole call rolls back — the same behavior 1.9.0 introduced
    for the composition path, now reaching these records too.

    Two things widen that further than it first looks, both deliberate:

    • It applies to children of every element on the page, not only the ones the template just
      added. The endpoint has always published every element on the page; that breadth now extends
      to their children.
    • The $owns fallback reaches versioned records sitting below an unversioned intermediate,
      which nothing previously walked to at all.

    So an apply-template call that succeeded before can now hard-fail on a class the project never
    had to allowlist. That is the correct direction — those records really were created by the
    duplicate and really do need publishing, and failing loudly beats leaving them silently on draft
    — but it is a live endpoint changing behavior. Fix: grant action on the affected classes, or
    pass "publish": "none" if the cascade was never meant to publish them.

    This is also the first test coverage of any kind for apply-template's publish behavior.
    dynamic/silverstripe-elemental-templates remains suggested rather than required; the tests
    drive stubs via two new PageHandler config values, template_class and
    template_applicator_class, which default to the real package's class names.