Repository navigation
2.7.0 — SilverStripe 6 line
Fixed
-
(#201) A composition's
page.match.idresolving to the wrong page used to silently
reparent every element in the payload whoseexternalIdalready existed anywhere on the
site —ExternalIdResolver::tryFind()is a global lookup by design, and composition force-
writes the resolved area's id onto whatever record it matches, with no check that the record
already belonged to a different page.RecordWriter::write()now rejects (new
CROSS_PAGE_REPARENTerror code, 409) when a server-derivedParentIDassignment (composition
only — an explicit clientfields.ParentIDwrite, e.g. viacontent_batch, is unaffected and
still governed solely by the existing element-type-allowed-on-page check) would move an
already-parented element onto a different page's area. Confirmed live (Rockline Industrial):
recovery needed raw SQL, since the API forbidsdeleteby design. The owner comparison checks
both id and base class —getOwnerPage()resolves against anyElementalAreasExtension
owner, not justSiteTree, so two owners from different base tables can share a numeric id.
Documented indocs/en/12_error-codes.md,docs/en/08_page-compositions.md, and the MCP tool
schema (schema/endpoints.jsonv1.17). Landed on branch1first (1.13.0), merged up here. -
(#202) A
content_batchhas_manyadd,remove, orset(theremoveAll()half) that
touched an already-published, clean related record left it desynced from LIVE with nothing to
notice or fix it —HasManyListunconditionally repoints/clears the related record's foreign
key via an ordinary draft write regardless of its Versioned state, leaving an attach stranded
modifiedOnDraftor a detach's LIVE row still pointing at the old parent.RecordWriter::write()
now republishes any already-published, NOT-already-modifiedOnDraftrecord a has_many relation
write touches this way, when the operation's ownpublish/defaultPublishisn'tnone—
authorization-checked against the related record's own classactionverb first, the same way a
subtree/ownspublish cascade checks every non-root record it touches. A record that was never
published, or that already had unrelated in-progress draft edits before this write touched it, is
left exactly as it was — this never forces an editor's own draft to LIVE. A many_manyaddwas
already unaffected for the related item (it only writes it when it isn't already in the
database). Landed on branch1first (1.13.0), merged up here. -
(#203)
dryRun: true(and a genuine atomic-failure rollback) already correctly roll back
an ordinary DB-onlyonBeforeWrite()/ValueTransformerside effect — verified with new
regression coverage rather than assumed. What no DB transaction can ever undo is a side effect
OUTSIDE the database (an HTTP call, a queued job, an external cache write) — confirmed live,
ElementOembed's oEmbed lookup left orphanEmbedObjectrows behind both a rolled-back
composition and adryRunprobe. AddedDynamic\ContentApi\Write\DryRunContext::isActive(),
a static flag any project write hook orValueTransformercan check to skip a non-DB side
effect during a dry run — this module can't intercept a project class's side effect on its
behalf, only make the dry-run state visible to it. Landed on branch1first (1.13.0), merged
up here. -
(#204) Investigated whether non-image (PDF) asset uploads have a working route — they do;
AssetServicehas never hardcodedImage, andcontent_asset_uploadcorrectly resolves a
.pdf(or any other extension) to plainFilevia the framework's own
File::get_class_for_file_extension(). The confirmed field incident's second half was real
though:AssetService::ingest()letSilverStripe\Core\Validation\ValidationException(a
disallowed extension,File.allowed_extensions) escape unmapped, surfacing as500 SERVER_ERRORwith a raw exception message instead of the422 VALIDATION_FAILEDevery other
write path in this module gives for the same failure class. Now mapped the same way. It never
silently reported success on a failed write — the controller's own top-levelThrowablecatch
already turns an uncaught exception into a proper (if less specific) error envelope. Landed on
branch1first (1.13.0, asSilverStripe\ORM\ValidationExceptionthere — branch1is SS5)
— merged up here with the exception import swapped to this branch's SS6 namespace. -
(#191, #195, #192, #205) Four write-path gaps that used to accept a request and silently do
something other than what was asked, found via a field audit of real usage on two production
consumer projects (~1,500 recorded MCP calls mined from session transcripts). All four now
reject rather than silently drop or coerce:WriteApplicator::applyFields()(and, on the colymba/apiwrite surface,
WriteGuardExtension::onBeforeWrite()) now validates a DBEnum-backed field's value against
its own declared list before writing —DBEnum::setValue()never validates, and MySQL
itself silently coerces an out-of-list value to''rather than rejecting it. New
INVALID_VALUEerror code.DBMultiEnum(aset-backed multi-select Enum subclass) is
handled correctly — each comma-separated value is checked independently.- (#191) A has_one relation named under a
relationspayload (has_many/many_many only) is
now rejected — with an actionable message — before the record is written, not after: a
relations: {"Parent": 74}on a create used to silently leaveParentIDuntouched, then
fail on unrelated-looking root-levelSiteTreevalidation instead of ever surfacing that
Parentwas the actual problem. - (#195)
LinkTransformer::transform()now rejects an unrecognized key in a structured
link payload, and a key that's only valid for a different link type (e.g.fileIdwith
"type": "ExternalLink") — both used to be silently dropped (DataObject::setCastedField()
falls back to a bare, never-persisted dynamic property when the resolved link class has no
matching column), leaving an empty or partially-populated link record with no signal
anything was wrong. - (#192)
CompositionService::compose()now also acceptsareaRelationat the request's
top level, as a defensive fallback for a caller reaching the endpoint directly over HTTP
(page.areaRelationremains the one documented shape); a mismatch between the two locations
is reported as a warning rather than resolved silently.
Compatibility note: three of these are newly-rejecting a write that previously returned
200 — a consumer currently sending a payload that happened to hit one of these gaps will now
get an error instead of a silent no-op:422 INVALID_VALUEfor an out-of-list enum value,
400 PAYLOAD_INVALIDfor a has_one underrelations, or422 UNKNOWN_FIELDfor an
unrecognized/wrong-type link key. Landed on branch1first (1.12.0), merged up here. -
(#198)
GenerateContentApiExposureTask/ExposureScaffoldergeneratedapi_access: 'GET,POST,PUT'for every class — no HTTP method maps to theaction(publish/unpublish/
archive) verb, so a project that pasted the generator's own output in as-is had a class that
looked fully CRUD-configured but could never actually publish a write through this API. Now
generates'GET,POST,PUT,action'. Landed on branch1first (1.13.0), merged up here — this
branch's ownExposureScaffolder(shared with branch1) gets the same fix. -
(#186)
ContentApiTestCasenow pinsSilverStripe\Assets\File/Imageto
api_access: falseinsetUp(). TwoAssetsTestcases failed on this branch's testbed
specifically: its own exposure config grantsImageits ownapi_access(added while closing
#174), which short-circuitsAssetHandler::governingAssetClass()'s ancestry walk before either
test'sFile-onlyConfig::modify()override was ever consulted — a host-config leak into the
module's own suite, not asilverstripe/assetsversion regression as originally suspected. The
same pin also restores the strength of the #119 unpublish-owns shared-asset regression test,
which previously depended on an absenceContentApiTestCasenever actually enforced. Landed on
branch1first, merged up here.
Changed
- (#115, #118) Merge-up of branch
1's backport of this branch's ownGenerateContentApiExposure
task (shipped here in 2.2.0) — no new capability on this branch, but the shared
ExposureScaffolder::AUTO_GENERATED_BANNERno longer hardcodes this branch'ssake tasks:
invocation syntax directly into the generated YAML's own header comment (it needed to stop
doing that so branch1'skey=valueadapter could reuse the same class); the emitted banner
text is worded generically now instead. - The shared
ExposureScaffolder's generated output now also documents (as a comment) the
separateColymba\RESTfulAPI\QueryHandlers\DefaultQueryHandler.modelsentry — the base both
this module's own/content-api/v1endpoints and colymba's generic/apisurface merge from,
perClassRegistry's own docblock, withClassRegistry.modelsas the narrower,
content-api-only overlay. A class registered only under the overlay resolves for
content_schema_classwhile every actual/api/$Modelrequest 404s or is silently invisible.
Landed on branch1first (1.13.0), merged up here.