Repository navigation
Migrations: Format migration expression values with the invariant culture (closes #24594) - #24597
Conversation
|
Hi there @andreaslborg, thank you for this contribution! 👍 While we wait for someone to have a look at your work, we wanted to let you know about that we have a checklist for some of the things we will consider during review:
Don't worry if you got something wrong. We like to think of a pull request as the start of a conversation, we're happy to provide guidance on improving your contribution. If you realize that you might want to make some changes then you can do that by adding new commits to the branch you created for this work and pushing new commits. They should then automatically show up as updates to this pull request. Thanks, from your friendly Umbraco GitHub bot 🤖 🙂 |
AndyButland
left a comment
There was a problem hiding this comment.
Thanks for this @andreaslborg. The numeric fix is looks good and I've confirmed that the tests prove it: with the change reverted, all six non-US cultures fail.
While reviewing I found a few related issues in the same code path, so I've pushed a follow-up commit:
- Date and time values.
DateTimeOffset,DateOnlyandTimeOnlyfall through to thedefaultbranch, as they don't have their ownTypeCode. With the invariant culture they came out asMM/dd/yyyy, which SQL Server reads according to the login's language, so on a day-first login it would fail or swap day and month. These types are now formatted by the syntax provider via newISqlSyntaxProvidermethods:FormatDateTimeOffset,FormatDateOnlyandFormatTimeOnly.FormatDateTimealready existed and was in use. - Single quotes.
SqlSyntaxProviderBase.GetQuotedValue(string)didn't escape embedded quotes, so a value likeO'Brienproduced invalid SQL. It now doubles them.
…igration-quoted-value-invariant-culture # Conflicts: # tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Persistence.Sqlite/SqliteSyntaxProviderTests.cs
|
@claude re-review this |
Re-review summaryThanks @andreaslborg and @AndyButland. This looks good overall. The numeric fix is correct, and the follow-up commit closes the related gaps in the same code path in a sensible way. What looks good
Suggestions (inline)
Minor, no action needed: On SQL Server, a No breaking changes found. |
…nd clarify SQLite remarks - Handle DateTime alongside Guid, DateTimeOffset, DateOnly and TimeOnly in MigrationExpressionBase.GetQuotedValue, so every type the syntax provider formats is in one place, and cover it in the migration date test. - Name the types in the SQLite date and time format remarks, as FormatDateTime is not overridden there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Load balancing: Admit one request per process into the content write path and remove the thread-pool hops under the ContentTree lock (#24292)
* fix(core): check and publish repository cache versions synchronously on the save path
Every repository read checks the cache version, and on a load-balanced backoffice that check
read the version from the database through the async repository and blocked on it with
GetAwaiter().GetResult(). The first read inside ContentService.Save does this while the global
ContentTree write lock is held, so the continuation needed a second thread-pool thread while the
first was blocked; on a starved pool that stretched the time the lock was held from
milliseconds to seconds and everything queued behind it timed out.
Add synchronous members beside the async ones on the version repository, accessor and service
(bridging defaults keep external implementations compiling), implement them natively with the
sync NPoco calls, and make the cache policy, the inline sync and the request-end flush use
them. The sync InsertOrUpdate overload was itself sync-over-async and is now genuinely
synchronous. The async members stay; the hot paths flip back to them once locks and
notification publishing are async (TODO V20).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(hybridcache): write the published-content cache rows synchronously inside the content transaction
DocumentRepository publishes ContentRefreshNotification from inside the content transaction,
under the ContentTree write lock, and EventAggregator.Publish runs async handlers through
Task.WaitAll on the saving thread. The HybridCache handler awaited two to four database and
cache calls there, each needing a free thread-pool thread while the saving thread was blocked.
Register a synchronous handler for the content and media refresh notifications that writes
the rows through new synchronous repository members, and defer the memory-cache removals on
trash and unpublish until after the commit, where the scope-exit refresher repeats them anyway.
The post-commit memory refresh reads its rows synchronously as well, so the ContentTree read
lock it takes is not held across a thread-pool hop. The async members and handlers stay for
the async-all-the-way work (TODO V20).
The integration tests count synchronous versus asynchronous repository calls during a publish,
unpublish, trash and draft save; they fail when the async handler is registered again.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(core): refresh publish status and the memory cache synchronously after the commit
The content cache refresher runs on the saving thread at scope exit and blocked on the async
publish-status read and memory-cache refresh with GetAwaiter().GetResult(). Give the publish
status repository and management service synchronous per-document members and use them, and
call the synchronous memory refresh, so the post-commit refresh needs no second thread-pool
thread. The full reload and the in-memory removal keep their async calls.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(hybridcache): serve the synchronous published-content lookups without the async pipeline
Stack samples of a node under load show the thread holding the ContentTree write lock during a
publish blocked in DocumentCache.GetById: the redirect tracker looks the document up from the
publishing notification, inside the transaction, and the synchronous members wrapped the async
lookup with GetAwaiter().GetResult(). On a starved thread pool the HybridCache probe and the
read-through fetch wait seconds for a continuation thread, and every save queued behind the lock
times out.
Add synchronous GetByKey/GetById to IDocumentCacheService (bridging defaults) and implement them
in DocumentCacheService: the converted-content tier, then the in-process HybridCache tier through
GetOrCreateAsync with the underlying data and all cache writes disabled (an L1 hit completes
synchronously; a lookup that would reach L2 is abandoned), then a synchronous database read
through a new IDatabaseCacheRepository.GetContentSource. The result populates the converted
tier as the async path does. DocumentCache's synchronous GetById members use the new lookups.
Four of the new integration tests fail without the change: a first publish with the redirect
tracker registered, a republish of content evicted from memory, a lookup on a memory miss, and
an unpublished lookup all reached the database through the async member before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(hybridcache): wait for the distributed cache write outside the content tree read lock
The post-commit memory refresh takes the ContentTree read lock, reads the row and writes the
cache entries. With a distributed cache configured, the HybridCache write completes only after
the distributed store has acknowledged it, and that continuation needs a thread-pool thread. On
a starved pool the refresh held the read lock for seconds, every save queued behind it, and the
stack samples of a node under load showed up to six refreshes blocked there at once.
HybridCache writes its in-process tier and issues the distributed write before its first await,
so the writes are still started while the lock is held and the lock still orders them against a
later commit's refresh; only the wait for their completion moves after the scope is disposed.
The unit test fails without the change: the scope is not disposed until the distributed write
completes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(core): register the synchronous request-end handler for the cache instruction flush
The first commit added a synchronous Handle to DatabaseServerMessengerNotificationHandler for the
request-end flush, but the registration still named the async handler, so HandleAsync kept running
and the flush still awaited the version write on a second thread-pool thread while holding the
CacheVersion lock. Register the synchronous handler instead; PublishAsync runs synchronous
handlers too, and the ordering of instructions before versions is unchanged.
The new test asserts the registration itself, since the handler tests exercise the members
directly and did not catch this.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* revert(hybridcache): keep the synchronous published-content lookups on the async pipeline
This reverts commit c1c9668c07a. The synchronous lookup served only the in-process tiers and read
the database on a miss, so every synchronous IPublishedContentCache.GetById caller stopped using
the distributed cache tier and stopped warming it. That is not acceptable for a load-balanced
setup, where a node must be able to serve from the distributed tier after a restart.
The remaining sync-over-async hop under the content tree write lock (the redirect tracker's
lookups on publish) is left as it was; whether the other commits reduce the pool starvation
enough without this change is measured on the load rig.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(core): skip the redirect tracker's published-cache lookup for content without a published route
The redirect tracker runs on the publishing notification, inside the content transaction and under
the ContentTree write lock, and starts by looking the entity up in the published content cache. For
content that has never been published that lookup can only miss: the synchronous cache member wraps
the asynchronous HybridCache pipeline, the distributed-tier miss is a round trip whose continuation
needs a thread-pool thread, and on a starved pool the write lock is held for seconds while every
other save queues behind it. Stack samples of a node under load showed the same thread blocked there
in consecutive samples.
The tracker already treats a missing published URL segment in the document URL service as "no
published route" for every item it traverses. Applying that test to the entity first, from the
in-memory document URL service, keeps content without a published route in any culture out of the
published cache entirely; content with a published route keeps the full lookup, so the distributed
tier is still read and warmed as before.
Both new tests fail without the change: the mocked tracker consults the cache once, and a first
publish with the redirect tracking handler registered reads through to the database once.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* chore(core): forward cancellation tokens in the publish status repository and tidy the new members
The asynchronous publish status queries accepted a cancellation token and never passed it to the
database calls; NPoco's FetchAsync and ExecuteScalarAsync take one. The omission predates this
branch but the lines were rewritten here, so the static analysis charged it to the pull request.
Also removes the unused scope parameter of DocumentCacheService.AfterCommit, which reads the
ambient scope context instead, and documents the parameters and return values of the synchronous
repository members added on this branch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* revert(hybridcache): await the distributed cache write under the content tree read lock again
Reverts 7710c18fd5d. Issuing the cache writes under the read lock and waiting for them after the scope
is disposed relied on HybridCache's internal ordering to keep the lock meaningful; that is not a trade
the code base should carry. The post-commit refresh awaits each write where it issues it, as before.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(hybridcache): await the in-transaction cache removals where the asynchronous handler did
The synchronous refresh deferred the memory-cache removals for trashed and unpublished content to after the
commit through the scope context, so that no distributed-cache round trip was awaited under the content tree
lock. Running cache work from an enlisted action changes the failure semantics of the save and is not a
pattern the code base should carry. The removals are awaited inside the transaction again, as the
asynchronous handler always did; they only run for trash and unpublish.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(core): admit one request per process into the content write path
Every request that waits for the content tree write lock blocks a thread
for the whole wait. Under a burst of saves those waiters exhaust the
thread pool, which slows every await the lock holder makes and so
stretches the time the lock is held; the requests queued behind it then
hit the lock timeout.
ContentEditingService and ContentPublishingService now await a
per-process admission before calling into the synchronous content
service, so at most one request per process holds a thread on the lock
wait or inside the critical section while the others wait without a
thread. The content tree lock still orders the writes across processes.
Admission is re-entrant, so work started inside an admitted operation,
such as a notification handler that saves related content, runs at once.
Temporary for v17, to be removed once the content tree lock can be
awaited.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(hybridcache): wait for the distributed cache write outside the content tree read lock
RefreshMemoryCache, the post-commit refresh on the saving node and the refresh the instruction-sync job runs
for other nodes' changes, blocked on the HybridCache distributed write while holding the ContentTree read lock.
That write completes on a thread-pool thread; on a saturated pool its completion queues behind the requests
blocked on the same lock, so the lock stays held until the distributed cache client times out, 10 to 30 s at
a time.
The refresh still reads the row and issues its cache writes under the lock (HybridCache updates the in-process
cache and sends the distributed write before its first await) and waits for them once the scope is disposed.
Refreshes of one document are serialised per node by a per-document lock, so a write a distributed cache sends
late cannot land on top of a newer one; refreshes of different documents stay independent, and no database
lock is held while waiting for a document lock.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(hybridcache): order removals after pending refresh writes
Addresses the Copilot review on #24292:
- RemoveFromMemoryCacheAsync takes the per-document lock, so the post-commit
removal of a document waits for a pending refresh write of it instead of
being overtaken by it. The removals inside the transaction stay outside the
lock: they run under the content tree write lock, which a refresh holding
the document lock may be waiting for.
- RefreshMemoryCache waits for both cache writes with Task.WhenAll, so a
failed write no longer releases the lock while the other is in flight.
- The comment states the cross-node bound instead of claiming full ordering.
Also consumes each write's ValueTask once (S5034), fixes the field order,
comment spacing and KeyedLock docs flagged by SonarQube.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(infrastructure): redirect guard checks invariant content per language
The document URL service stores per-language segments for invariant content
whose segment provider varies by culture, so a lookup for the invariant
culture finds nothing and the guard skipped redirect tracking for it. A
language lookup also finds a segment stored once without a language.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* style: address SonarQube findings in the changed files
Member order (public before private, static before instance), Handle
overloads adjacent, a pragma around the obsolete refresh notification
registrations, ThrowIfNull and no null-forgiving operators in the
synchronous InsertOrUpdate, a debug-log guard, comment spacing and a
missing returns doc. The TODO (V20) markers are deliberate and stay.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(core): skip the content write gate for callers already inside a scope
A caller that groups writes in one scope holds the content tree write lock
from its first write until the scope completes. Its next write waited for
the gate while a request admitted ahead of it waited for that lock, so
neither could finish until the request's lock wait failed.
Callers inside a scope now bypass the gate: they may hold the lock, and the
gate only exists to keep top-level waiters off the thread pool.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* HybridCache: Read cache sources without a transaction when a cache miss happens outside a scope (#24522)
* Read cache sources without a transaction when a cache miss happens outside a scope
* Add an explicit ScopelessSqlContext for potentially scopeless reads.
* Add missing tests for no-scope reads
* Removed now duplicate and slightly innaccurate comments following FetchContentSources* extraction.
* Additional documentation on interface.
* Additional test for GetMediaSourcesAsync to align with document equivalent.
---------
Co-authored-by: Andy Butland <abutland73@gmail.com>
* Tags: Fix tag group losing non-Latin characters on SQL Server (#24357)
* Tags: Write the tag group as a Unicode literal on SQL Server
TagRepository.GetTagSet builds an inline set of tags that is joined to
cmsTags and inserted into it. The tag text was emitted as N'...' but the
group as '...', so on SQL Server any character outside the database code
page was lost or replaced with a best-fit character before the insert
(e.g. "grupp-αł" was stored as "grupp-al" on a Latin1 database).
Emit both literals with the N prefix on SQL Server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: retrigger CI
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* SQL Server: Recognise SQL Server 2022 and 2025 versions (#24365)
* SQL Server: Recognise SQL Server 2022 and 2025 versions
SqlServerSyntaxProvider mapped product versions only up to 15 (2019), so
2022 (16) and 2025 (17) were reported as "Other". Add V2022 and V2025 to
VersionName and map them, so they are detected and can be configured via
DatabaseFactoryServerVersion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* SQL Server: Map product versions through a lookup table
Replaces the growing switch in MapProductVersion with a dictionary lookup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Extend version detection for LocalDb.
* Clarify test for out of range SQL version.
* Fix line numbers in CLAUDE.md.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
* Image Cropper: Store an empty value as null rather than the JSON text "null" (#24336)
* Image Cropper: Store an empty value as null rather than the JSON text "null" (closes #24335)
When the editor value is null and there is no current file, FromEditor
serialized the null ImageCropperValue and returned the string "null".
Inside a block that string is stored as the nested value, and publish
validation then fails to parse it as an ImageCropperValue (500).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read a stored "null" Image Cropper value as empty.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
* Trees: Map the parent entity type of folder tree items from the server instead of guessing it (#24366)
* Fix parent entity type in file-system tree mappers
* Fix media recycle bin tree root parent entity type
* Mocks: Resolve parent isFolder in tree responses
* Tree: Expose whether parent is a folder
* Fix parent entity type in type tree data sources
* Refactor to expose parentIsFolder as a flat field on folder tree items.
* Record whether the parent is a folder where it is already resolved.
* Tree: reload destination after single move/duplicate action (#24381)
* Mocks: Add Umb-Generated-Resource header to copy
The MSW copy handlers for data type, document type, document, media type and member type now return the Umb-Generated-Resource header with the new item's id, alongside Location. This matches the real Management API, so client code that reads the generated resource id from copy responses works against the mocks.
* return duplicated entity key
Update duplicate-to data source and repository contracts to return a typed response containing the created entity's unique key when available, alongside errors.
* Return duplicate unique from type duplicate actions
* Tree: Reload destination after move/duplicate to
* Tree: Fix children loading with target window
When a custom target window is set (setTargetTakeSize) and there is no base target, the children manager now clears and loads nothing instead of requesting children without a target.
On reload, a custom target window is always sent with the request, since it does not depend on the loaded items. takeBefore/takeAfter only use the counts of currently loaded items when the base target is among them, otherwise they fall back to the configured window.
Adds tests covering the target window behavior and documents setTargetTakeSize.
---------
Co-authored-by: Andy Butland <abutland73@gmail.com>
* Documents & Media: Hide type structure guidance in Production mode when no types are allowed (closes #24394) (#24484)
* fix(backoffice): hide Document Type structure guidance when it can't be edited
When no Document Types are allowed, the content create dialog now shows the
plain message (no Settings instructions, no "Edit permissions" button) if the
user lacks Settings access or the server runs in Production mode. This applies
to both the child and the root case, completing the port of #15853.
Fixes #24394
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(backoffice): apply the same no-allowed-types rules to the media create dialog
Media Type structure guidance and the "Edit permissions" button now only show
with Settings access outside Production mode. Creating at root also gets its
own "Allow at root" guidance instead of the child-node wording.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Use provider boolean literals when ordering by published with a culture (#24530)
* Use provider boolean literals when ordering by published with a culture
ApplySystemOrdering mixed the published column with the integer literals 0 and 1 in one CASE. SQL Server and SQLite accept that; a provider with a real boolean column (PostgreSQL) rejects it with 42804. Use SqlSyntax.ConvertIntegerToBoolean, as UserRepository does, so SQL Server and SQLite are unchanged.
* Test ordering by published with a culture on its own content, covering both CASE branches
Review feedback: the test now creates its own parent with an invariant and a culture-variant child
type (one published child of each), so the variant branch of the CASE decides part of the result,
and removes them afterwards so the fixture's other tests are unaffected. Comment made general.
* Validation: Align client min/max item messages with the server's (closes #24184) (#24540)
* fix(client): align min/max item validation messages with the server's
Pickers and other item inputs now default their min/max validation messages
to the validation_entriesShort / validation_entriesExceed terms, which state
the limit and how many items to add or remove. These are the messages the
server returns for the same violation, so the wording no longer changes
depending on which side validated the value.
The content picker and entity data picker property editors no longer pass a
static message of their own.
Fixes #24184
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(client): cover item count messages on a picker-context input
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Workspace entity action menu: hide when no actions are available (#24237)
* Workspace entity action menu: hide when no actions are available
Fixes #24236
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Workspace entity action menu tests: clean up reliably and fail fast
Use fixture() and an afterEach so the host element and test manifest are
removed even when a test fails. Assert on booleans instead of the element,
so a regression fails with a clear message instead of hanging the runner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Split Panel: Keep the tree at its set width in right-to-left languages (#24370)
* fix(backoffice): measure the split panel divider from the inline start
The split panel measured the divider, pointer and arrow keys from the
physical left edge, while its grid columns follow the writing direction.
In a right-to-left layout the start panel therefore took the complement
of its intended size (300px became 70% of the width).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(backoffice): read the split panel direction once per drag
The direction was read with getComputedStyle on every pointer move,
and the panel's rect was read twice per move. The direction can't
change during a drag, so it is now read when the drag starts, and each
move reads the rect once. A drag now does no more style or layout reads
per move than before the right-to-left fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Backoffice: Keep inline code left-to-right in right-to-left languages (#24371)
* fix(backoffice): keep inline code left-to-right in right-to-left text
Inline <code> inherited the UI direction, so in a right-to-left language
the bidi algorithm moved leading and trailing punctuation to the other
end (@RenderBody() read as ()RenderBody@).
UmbTextStyles and umb-localize now lay out code left to right and
isolate it from the surrounding text, the CSS equivalent of dir="ltr".
Elements that render inline code adopt UmbTextStyles, and the Tiptap
element path renders its separators between the items so they stay
between them in both directions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(backoffice): apply shared text styles to sortable list items
umb-sortable-list renders the consumer's items into its own shadow root,
so styles adopted by the consumer never reach them. Adopt UmbTextStyles
there instead, which keeps the aliases in the collection column and
layout configuration left to right.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(backoffice): carry only the inline code rule in sortable lists
umb-sortable-list is a generic component that renders consumers' items
into its own shadow root. Adopting all of UmbTextStyles there restyled
the text in every package's list items, which is wider than keeping
inline code left-to-right needs. It now carries just the code rule, as
umb-localize does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(backoffice): let a dir attribute decide the direction of inline code
The shared inline code rule overrode a dir attribute, because the
browser applies dir through its own styles. The rule now only applies
to code without one, in UmbTextStyles, umb-localize and
umb-sortable-list. The missing property editor view marks the data
type's name dir="auto", as a name can be in any language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(tiptap): hide element path separators from assistive technology
The separators between element path items are decoration, so screen
readers no longer read them out as "greater than".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Validation: Align client min/max item messages with the server's (closes #24184) (#24540)
* fix(client): align min/max item validation messages with the server's
Pickers and other item inputs now default their min/max validation messages
to the validation_entriesShort / validation_entriesExceed terms, which state
the limit and how many items to add or remove. These are the messages the
server returns for the same violation, so the wording no longer changes
depending on which side validated the value.
The content picker and entity data picker property editors no longer pass a
static message of their own.
Fixes #24184
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(client): cover item count messages on a picker-context input
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Block Editor: Open editor when exposing block from a custom view (closes #21717) (#24554)
* fix(client): open the block editor when creating a block from a custom view's expose button
Clicking the create overlay on a block with a custom view only exposed the block
for the current variant, leaving a (possibly empty) block without opening the editor.
When the block has content properties to edit, open the content editor instead;
submitting it exposes the block. Applies to Block Grid, List, Single and RTE (closes #21717)
* test(client): only expose multi-brand mock blocks in the default culture
Lets the other cultures show the create placeholder for custom block views.
* test(client): limit the English-only block exposure in mock data to the home page blocks
* Workspace: Let extensions make the whole entity of an entity detail workspace read-only (#24353)
* Workspace: let features make the whole workspace entity read-only
Entity detail workspace contexts get a readOnlyGuard, and expose readOnly and
getReadOnly() for whether the whole entity is read-only. A rule with
permitted: true and no variant makes the whole entity read-only. Like the
content workspace's guard, its rules are cleared whenever the workspace loads or
creates an entity, so they only apply to that entity. Content workspaces keep
their per-variant guard, where such a rule already applies to every variant.
While the entity is read-only, the Submit workspace action is disabled,
requesting a submit (including the save shortcut) does nothing, named
workspaces block renaming through their nameWriteGuard, the workspace name
header shows a Read-only tag, and the invariant workspace property dataset is
read-only, so its properties are too. The
Umb.Condition.WorkspaceEntityIsReadOnly condition lets extensions, such as
workspace actions, depend on whether the entity is read-only.
The entity detail workspace editor hides the editor while the workspace is
forbidden, including for a new entity, so only the access denied message shows.
* Sorter: stay disabled when disabled right after connecting
The sorter's deferred setup checks that the sorter is still enabled, so a
sorter disabled before its first animation frame keeps its items from
being dragged.
* Components: let the webhook, media type, language, template and collection inputs be read-only
umb-input-webhook-events, umb-input-webhook-headers, umb-input-media-type,
umb-input-template, umb-template-card and
umb-input-content-type-collection-configuration get a readonly property, and
umb-input-language hides its add button while read-only. While read-only they
hide adding, removing and choosing, but their items can still be opened.
umb-input-with-alias gets an append slot for content shown at the end of the
input, after the alias.
* Workspace: make schema editors follow the read-only workspace entity
The editors of document, media and member types, data types, dictionary items,
languages, member groups, partial views, scripts, stylesheets, templates and
webhooks become read-only while their workspace entity is read-only: fields and
code editors can't be changed, the content type design view can't add, remove
or sort tabs, groups and properties, and the name shows a Read-only tag. Their
Save workspace actions are hidden through Umb.Condition.WorkspaceEntityIsReadOnly.
The template and partial view workspaces make their entity read-only in
production runtime mode, and until the runtime mode is known, so their editors
and Save actions no longer check the runtime mode themselves.
* Workspace: read whether the entity is read-only from its read-only guard
Entity detail workspaces now use UmbReadOnlyVariantGuardManager, the guard content
workspaces already had, so content no longer overrides it. The readOnly and
getReadOnly() members are removed: readOnlyGuard.permitted and getPermitted() tell
whether the entity is read-only, and everything that followed readOnly now
observes the guard instead.
* Workspace: observe the read-only guard through the workspace context already consumed
Views and editors now read the read-only guard from the workspace context they
already consume, instead of consuming the entity detail workspace context a
second time. Components that only get a generic workspace context, and the
content type header, check it with the entity detail workspace context token's
discriminator.
* Content Type Designer: pass the read-only state down from the design editor to its tabs, groups and properties
* Templating: apply the production mode read-only rule when the workspace state is reset, and only while the server context is available
* Inputs: render the read-only dependent actions in their own methods
* Respect read-only state for culture selection
Prevent culture changes when the language workspace is read-only.
* Workspace: disable Save while the entity is read-only instead of hiding it
Remove the Umb.Condition.WorkspaceEntityIsReadOnly condition and let the
Submit workspace action disable itself through the read-only guard. Templates
and partial views no longer hide Save in production mode; it is disabled
through the same guard.
* Templating: show the production mode action before Save
Save stays visible but disabled in production mode, so the production mode
action now sits to its left, next to the disabled primary action.
* Workspace: let any submittable workspace declare a read-only guard
UmbSubmittableWorkspaceContext gets an optional readOnlyGuard, so the Submit
workspace action, the invariant property dataset and the content type designer
read it from the context they already consume. Workspaces that already have a
guard but aren't entity detail workspaces, such as the block workspace, now
disable Submit while read-only too.
* Workspace: make the header name read-only from the workspace read-only guard
* Inputs: show a disabled add button when a read-only input is empty
* honor read-only guard for names
Allow namable workspace contexts to expose a read-only guard and use it to control name editing, without requiring an entity detail context.
* scope read-only guards to entities
Use a general read-only guard for entity workspaces and retain variant-specific guards for content workspaces.
---------
Co-authored-by: Mads Rasmussen <madsr@hey.com>
* Acceptance Tests : Make the backoffice logout test resilient to startup routing closing the user modal (#24561)
* test(acceptance): retry logout when a navigation closes the user modal
The backoffice's initial routing fires navigationsuccess, which closes
non-routable modals such as the current-user modal. When the logout test
opened the modal during that window, the Logout button was removed and
the click waited until the test timeout (seen on nightly).
Re-open the modal and retry the Logout click until the token revoke is
seen, with a bounded click timeout so a detached button cannot hang.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(acceptance): wait for the revoke on each logout attempt
A Logout click can land as a navigation closes the modal without starting
the logout, which left the retry loop satisfied while the revoke never came.
Each attempt now waits for the revoke before counting as done, and re-opening
the modal reuses clickCurrentUserAvatarButton().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Data Access: Text searches treat LIKE wildcard characters as wildcards (#24549)
* test(infrastructure): cover text searches with LIKE wildcard characters
Searching content, members, dictionary items, redirect URLs or key-value
keys for text that holds % or _ also found text that merely matched the
wildcards, and on SQL Server [ opened a character set. The SQL expected
from string comparisons now carries an ESCAPE clause.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hNjhX7Wopdd6Hn36fo2W3
* fix(infrastructure): match text searches literally on every database
StartsWith, EndsWith and Contains comparisons, and the member, dictionary,
redirect URL and key-value searches, put the searched text into a LIKE
pattern unescaped. Wildcard characters in it then broadened the match on
every database; on SQL Server '[' opened a character set, and on PostgreSQL
a trailing backslash failed the statement.
ISqlSyntaxProvider gains EscapeLikeValue and GetLikeEscapeClause, with
default implementations. SqlWildcard keeps accepting patterns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hNjhX7Wopdd6Hn36fo2W3
* Remove the now unnecessary GetSafeLikePrefix helper.
* Add shared helper for default interface and base implementation of LIKE escaping.
* Added clarification remarks.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
* Dependencies: Bumps @umbraco-ui/uui from 2.1.1 to 2.2.0 (#24574)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Environment Permissions: Let code deny schema changes through the Management API (#24372)
* Schema Lockdown: let code make schema entity types read-only in the Management API
Implementations of ISchemaLockdownConfigurator, registered through the
SchemaLockdownConfigurators collection, can block creating, updating or deleting
schema entity types: data types, dictionary items, document, media and member
types, languages, member groups, partial views, scripts, stylesheets, templates
and webhooks. Nothing is locked unless a configurator says so.
Management API endpoints that change a locked entity type respond with 403
Forbidden and an Umb-Schema-Lockdown header naming the entity type and operation.
Reading stays allowed. GET /server/schema-lockdown reports the restricted entity
types and which operations are still allowed, so the backoffice can follow suit.
* Schema Lockdown: lock templates and partial views in production runtime mode
Core ships a schema lockdown configurator that, in production runtime mode,
blocks creating, updating and deleting templates and partial views. The template
controllers no longer check the runtime mode themselves, so in production mode
these requests now get 403 Forbidden with the Umb-Schema-Lockdown header instead
of 400 Bad Request, and updating a template's name or alias is blocked as well.
The constructors that took the runtime settings and the production mode template
operation statuses are obsolete.
The partial view repository also leaves the file in place when a partial view is
deleted in production mode, matching how it already skips writing files there.
* Schema Lockdown: allow deleting templates in production runtime mode
Deleting a template removes only its database record and leaves the file to the
deployment, so it is how a template removed from the code gets cleaned up on the
environments it was deployed to. Partial views have no record, only the file, so
deleting them stays denied.
* Schema Lockdown: complete the docs and tidy up the restrictions
* Schema Lockdown: give the real reason for locking templates in production runtime mode and drop an unused using
* Schema Lockdown: document the denial header in every OpenAPI document with governed operations
* Schema Lockdown: simplify the docs and comments
* Schema Lockdown: document the forbidden response of operations schema lockdown can deny, whatever the filter order
* Update src/Umbraco.Cms.Api.Management/SchemaLockdown/SchemaEntityTypeAttribute.cs
Co-authored-by: Andy Butland <abutland73@gmail.com>
* Schema Lockdown: deny a dictionary import unless both create and update are permitted
An action can declare several operations for an entity type, and every one of them has to be permitted. They're ordered by value, so the denial reported doesn't depend on attribute order. Importing dictionary items declares create and update, as it can add translations to existing items.
* Schema Lockdown: describe restrictions as permission verbs
Schema lockdown now denies permission verbs, named Umb.{EntityType}.{Operation} like the document permissions (for example Umb.DocumentType.Create), so clients don't need to know about entity types. Configurators deny verbs with Deny, using the constants in Constants.Permissions, and only changes are governed.
Endpoints infer the verbs from the HTTP method and the entity types their controller declares; [SchemaOperation] overrides them, and [SchemaOperation([])] marks an action that changes nothing. The 403 header and the schema lockdown endpoint report the denied verbs.
* Schema Lockdown: remove the schema lockdown endpoint until the backoffice uses it
The backoffice support for schema lockdown ships in a later minor, so the endpoint is left out until then. The denied permissions stay available through ISchemaRestrictions, and a denied request still names its permission in the Umb-Schema-Lockdown header.
* Environment Permissions: rename schema lockdown
* Environment Permissions: enforce declared permissions on their own and limit RequiresPermissionsFor to one entity type
* Environment Permissions: declare a verb prefix instead of an entity type
* Environment Permissions: fix a broken doc reference and tidy formatting
* Environment Permissions: build permission verbs from shared operation constants
* Environment Permissions: order usings and add trailing commas
* Environment Permissions: deny type imports unless both create and update are permitted
* Environment Permissions: call the first part of a permission verb its resource
* Added new DeniedPermission to backoffice CORS policy.
* Use an auto property for the denied permissions.
* Environment Permissions: schedule the new obsoletions for removal in Umbraco 20
* Environment Permissions: make the new tests internal
---------
Co-authored-by: Andy Butland <abutland73@gmail.com>
* Environment Permissions: Require both create and update to import a new document, media or member type (#24579)
Importing a type updates an existing type with the same alias, so importing one with a new key can change an existing type as well as create one.
* HybridCache: Cache content types in the MessagePack serializer factory (#24573)
* Cache content types in the serializer factory
* Add cache clearing for all servers
* Review: Clear the serializer type snapshot before a rebuild evicts entries
* Review: Add cache clearing for data type changes
* Collections: Cancel a queued collection load when the context is destroyed (closes #24462) (#24510)
Collections: Cancel a queued collection load when the context is destroyed
`loadCollection` is debounced, but `destroy()` did not cancel it, so a
load queued just before the context was destroyed still ran afterwards
and threw "Missing repository" as an unhandled rejection.
Fixes #24462
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Acceptance Tests: Read the Playwright retry report URL from a pipeline variable (#24586)
* chore(tests): read the Playwright retry report URL from a pipeline variable
The acceptance test report helper posted retry data to a hardcoded
endpoint. It now reads the endpoint from the PlaywrightReportUrl
environment variable, which the E2E test step maps from the pipeline
variable of the same name, and skips reporting when it is not set.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
* fix(tests): skip Playwright retry reporting when the report URL is not a URL
Builds without access to the PlaywrightReportUrl pipeline variable, such
as fork builds when the variable is secret, receive the unexpanded macro
text. Reporting is skipped in that case instead of throwing from fetch.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
* Acceptance Tests: Update Playwright to 1.61 (#24599)
* Update playwright version
* test(acceptance): restore the original workspace URL wait in goToContentWithName
Waiting for the variant route stepped around an empty document workspace
that the backoffice renders for /edit/{id}/view/... paths. Keep the helper
as it is on v17/dev so the tests surface that behaviour.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
* docs(tests): use the @umbraco-cms scope for the acceptance test helpers package
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
* Acceptance test: Enable reporting & Include the Umbraco version in the Playwright retry report (#24587)
* chore(tests): read the Playwright retry report URL from a pipeline variable
The acceptance test report helper posted retry data to a hardcoded
endpoint. It now reads the endpoint from the PlaywrightReportUrl
environment variable, which the E2E test step maps from the pipeline
variable of the same name, and skips reporting when it is not set.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
* fix(tests): skip Playwright retry reporting when the report URL is not a URL
Builds without access to the PlaywrightReportUrl pipeline variable, such
as fork builds when the variable is secret, receive the unexpanded macro
text. Reporting is skipped in that case instead of throwing from fetch.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
* chore(tests): include the Umbraco version in the Playwright retry report
The E2E stages expose the built package version as a stage variable,
the test step maps it into the UmbracoVersion environment variable and
the report helper sends it with each retry report.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
* fix(tests): omit the Umbraco version from the retry report when it is not resolved
A pipeline that does not define the umbracoVersion variable passes the
unexpanded macro text. The field is left out of the payload in that case.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
* Enable reporting again
* make one test fail for test purposes
* Revert "make one test fail for test purposes"
This reverts commit 8fe19e9ffb35d76ad8da6e3a9b01127715a8edc9.
---------
Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
* HybridCache: Rebuilding the database cache deadlocks with publishing on SQL Server (#24551)
* test(hybridcache): cover rebuilding the database cache while documents are published
The full rebuild deletes and rewrites every cmsContentNu row in one
transaction. On SQL Server a document published during the rebuild
deadlocked with it (in 3 runs out of 3 here), the publish was chosen as
the victim, and the document was left without a published cache row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hNjhX7Wopdd6Hn36fo2W3
* fix(hybridcache): lock the content trees while rebuilding the database cache
The full rebuild deletes and rewrites every cmsContentNu row in one
transaction, and the repository assumes the content tree is locked, but
the rebuilder took no lock. On SQL Server a document published during the
rebuild deadlocked with it, and the publish was chosen as the victim. The
rebuild now takes read locks on the content and media trees, so
saves wait for it as they did with the previous NuCache rebuild.
The deferred rebuild by content type runs in separate steps and is
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hNjhX7Wopdd6Hn36fo2W3
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Data Access: Integer and decimal properties do not order numerically on SQL Server and SQLite (#24552)
* test(persistence): cover ordering content by integer and decimal properties
Ordering content by a custom integer or decimal property sorts a string
built from the value, so the string must sort like the number. It did not
on SQL Server or SQLite: negative and large values came out of order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hNjhX7Wopdd6Hn36fo2W3
* fix(persistence): order integer and decimal properties numerically on SQL Server and SQLite
Ordering content by a custom integer or decimal property sorts a string
built from the value, so the string must sort like the number. It did
not on either database:
- SQL Server: STR({0}, 8) and STR({0}, 20, 9) overflow to asterisks
beyond their width, and zero-padding a negative value puts the digits
after the padding ("000-5"), so negatives misorder.
- SQLite: '{0}' put the quoted column name inside a string literal, so
every row got the same constant and the order was undefined.
Each conversion now covers the full range of its own column:
- Integers (all): the value is offset by 2^31 into the non-negative range
and zero-padded to 10 digits.
- Decimals, base syntax provider (DECIMAL(20,9)): offset by 10^11 and
formatted with 12 integer and 9 fractional digits.
- Decimals, SQL Server (DECIMAL(38,6)): an offset would need 39 digits,
so values are prefixed by sign, and negatives are complemented against
the largest value so they sort in reverse.
- Decimals, SQLite (text, lossless): the integer and fractional parts are
padded as text to the width of System.Decimal, and the digits of
negatives are complemented. No arithmetic, so no precision loss or
overflow.
Before this change both ordering tests failed on SQL Server (LocalDB)
and SQLite; they pass on both after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hNjhX7Wopdd6Hn36fo2W3
* Additional comments and tests.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
* Acceptance Tests: Report test tags, build number and pipeline URL for retried tests (#24604)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(api): align the environment permissions OpenAPI port with release/18.4.0
release/18.4.0 already ports DeniedPermissionHeaderFilter to DeniedPermissionHeaderTransformer.
Matching its files keeps the release/18.4.0 merge into main free of conflicts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Potential fix for pull request finding 'Add a timeout to best-effort retry reporting'
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* test(hybridcache): run the element rebuild deadlock test on SQL Server only
On SQLite a read lock doesn't exclude a writer, so a rebuild overlapping an element
publish waits on SQLite's single writer and the test times out. On SQL Server it
reproduces the rebuild/publish deadlock for elements without the ElementTree lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(tests): suppress API compatibility errors for template production mode tests
#24372 replaced them with environment permissions tests. Same suppressions as
release/18.4.0 (9eb8cb4b7a0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Migrations: Format migration expression values with the invariant culture (closes #24594) (#24597)
* Format migration expression values with the invariant culture
* Format migration date/time values via the syntax provider and escape quoted strings.
* Re-organise switch statements.
* refactor(infrastructure): group provider-formatted migration values and clarify SQLite remarks
- Handle DateTime alongside Guid, DateTimeOffset, DateOnly and TimeOnly in
MigrationExpressionBase.GetQuotedValue, so every type the syntax provider
formats is in one place, and cover it in the migration date test.
- Name the types in the SQLite date and time format remarks, as
FormatDateTime is not overridden there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test(acceptance): only open the current user modal when it is closed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: nltgpeterskoglund <154503808+nltgpeterskoglund@users.noreply.github.com>
Co-authored-by: talhamalik4025 <105341627+talhamalik4025@users.noreply.github.com>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
Co-authored-by: Matt Brailsford <me@mattbrailsford.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: Lee Kelleher <leekelleher@users.noreply.github.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com>
Co-authored-by: Stephan <sgay@pilotine.com>
Co-authored-by: Muhammad Abdullah Butt <168034895+1AbdullahButt@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andreas Lykke Borg <72602768+andreaslborg@users.noreply.github.com>
Description
Fixes #24594
MigrationExpressionBase.GetQuotedValueturned numeric values into SQL text withval.ToString(), which uses the current culture. On machines whose culture formats numbers differently from invariant, the generated SQL was invalid and the migration failed. This affectedInsert.IntoTable(...).Row(...),Update.Table(...).Set(...)andDelete.FromTable(...).Row(...), which all go throughGetQuotedValue.−), e.g.fo-FO,sv-SE,nb-NO,fi-FI:-73001became−73001.da-DK,de-DE:1.5mbecame1,5, which is read as two values.Fix: In
MigrationExpressionBase.cs,GetQuotedValuenow usesConvert.ToString(val, CultureInfo.InvariantCulture)for numeric values and for the default (string-quoted) case, so the generated SQL no longer depends on the machine's culture.Tests: Added
MigrationExpressionBaseTests, which runsInsertDataExpressionunderfo-FO,sv-SE,nb-NO,fi-FI,da-DK,de-DEanden-USand asserts that negative integers, decimals, doubles and longs are rendered invariantly. Without the fix, the six non-US cultures fail.Testing
Insert.IntoTable("umbracoLock").Row(new { id = -73001, value = 1, name = "CultureRepro" }).Do();.CultureInfo.DefaultThreadCurrentCulture = CultureInfo.GetCultureInfo("fo-FO");at the top ofProgram.cs.id = -73001.Automated:
dotnet test tests\Umbraco.Tests.UnitTests --filter "FullyQualifiedName~MigrationExpressionBaseTests"passed (7/7). The rest of the unit-test suite was not run.