Affected Version
2026.2 (analysed on a 2026.1 installation running pimcore/pimcore: 2026.x-dev). The code is the
same on the other supported lines.
Affected capability
Other (Direct Edit / Portal Engine / Mercure)
Steps to reproduce
- Install Direct Edit and Portal Engine, with the Mercure hub as shipped in
pimcore/skeleton and
pimcore/demo-enterprise (MERCURE_EXTRA_DIRECTIVES: anonymous).
- Remove the
anonymous directive from the hub, so it only accepts subscriptions that present a
valid JWT.
- Open an asset in a Portal Engine data pool and start a Direct Edit session.
Actual Behavior
The Direct Edit status indicator in Portal Engine never updates: its Mercure subscription is
rejected with 401 and the failure is invisible, because eventSource.onerror is an empty
function while the browser retry-loops.
The reason is that the non-Studio Direct Edit channel has no authorization at all. Direct Edit
notifies the browser over two separate Mercure channels:
1. Studio channel. Service\Studio\DirectEditService publishes through StudioBackendBundle's
PublishServiceInterface to the Studio user topic. publish($topic, $payload) is called without a
third argument, so these are private updates, and the Studio frontend consumes them through
Studio's GlobalMessageBus. Authorized by Studio's mercureAuthorization cookie. Fine.
2. "Classic" channel. ClientCommunicationService::sendClientUploadEvent() and
sendStartLocalEditingEvent() publish through Direct Edit's own PublishService::sendUpdate():
$update = new Update($topic, json_encode($data), false); // private = false
to http://www.pimcore.com/direct-edit/client-upload/user/<applicationPrefix><userId>, and the
browser subscribes to the hub directly. Nothing hands that browser a subscriber token.
PublishService::getClientJwtCookie() still exists but has zero callers anywhere in vendor: the
Admin Classic controller that used to set the cookie was removed, and Direct Edit's
doc/10_Integrate_into_Custom_Application.md documents issuing a token as the integrator's job.
The channel works today only because the updates are published public and the hub accepts
anonymous subscribers.
Portal Engine is a first-party consumer of this channel.
Controller/DataPool/AssetController.php passes MercureUrlService::getClientSideUrl() and the
prefixed portal user id into the frontend config, and
assets/scripts/features/asset/components/detail/DirectEditStatus.js opens:
const url = new URL(getConfig("directEdit.mercureUrl"))
url.searchParams.append("topic", 'http://www.pimcore.com/direct-edit/client-upload/user/' + getConfig("directEdit.userId"))
const eventSource = new EventSource(url) // no credentials, no token
Portal users authenticate as PortalUser, a context entirely separate from Studio users, so they
never receive Studio's cookie and there is no other cookie to fall back on.
Two further defects in that component, independent of the authorization question:
- the
useEffect has no cleanup, so the EventSource is never closed on unmount and every mount
of the asset detail view leaves another open hub subscription behind;
eventSource.onerror = event => {} swallows every failure, which is what makes the 401 above
invisible.
This is the reason the hub hardening proposed in #346 had to be dropped
(pimcore/demo-enterprise#528 and pimcore/skeleton#287 were closed): anonymous is currently
load-bearing for a shipped bundle.
Not affected: the desktop client pimcore/direct-edit-client subscribes to no Mercure topic at all.
It uses pimcorefile:// plus HTTPS download/upload and a chokidar file watch; eventsource
appears only in its devDependencies and is required by no source file.
Expected Behavior
Every Mercure subscription Pimcore opens is authorized, so the hub can be configured to reject
unauthorized subscribers.
Suggested direction:
Direct Edit
Portal Engine
- Fetch that token before opening the
EventSource (or pass withCredentials: true if the
mechanism ends up cookie-based), and renew it for as long as the page is open.
- Close the
EventSource in a useEffect cleanup.
- Give
onerror a real handler, so a refused or dropped subscription is visible instead of silent.
Skeleton / demo
Verification
Against a throwaway dunglas/mercure, publishing a public update on the Direct Edit topic:
| Subscriber |
hub with anonymous |
hub without |
| no token (Portal Engine, custom app) |
200, delivered |
401 |
any valid subscriber JWT, topic not in its subscribe claim |
200, delivered |
200, delivered |
The subscribe claim only gates private delivery; any valid token is enough to connect and receive
public updates on any topic. A token per user is therefore already enough to unblock the hub
change, and publishing private is the follow-on step.
Related: #346 (the Studio side of the same authorization model), fixed by
pimcore/studio-ui-bundle#4009 and pimcore/studio-backend-bundle#2012. Direct Edit in Studio was a
second victim of that bug - its "upload complete" notification is a private update on the Studio
user topic, so it was being dropped along with everything else after an hour.
Affected Version
2026.2 (analysed on a 2026.1 installation running
pimcore/pimcore: 2026.x-dev). The code is thesame on the other supported lines.
Affected capability
Other (Direct Edit / Portal Engine / Mercure)
Steps to reproduce
pimcore/skeletonandpimcore/demo-enterprise(MERCURE_EXTRA_DIRECTIVES: anonymous).anonymousdirective from the hub, so it only accepts subscriptions that present avalid JWT.
Actual Behavior
The Direct Edit status indicator in Portal Engine never updates: its Mercure subscription is
rejected with
401and the failure is invisible, becauseeventSource.onerroris an emptyfunction while the browser retry-loops.
The reason is that the non-Studio Direct Edit channel has no authorization at all. Direct Edit
notifies the browser over two separate Mercure channels:
1. Studio channel.
Service\Studio\DirectEditServicepublishes through StudioBackendBundle'sPublishServiceInterfaceto the Studio user topic.publish($topic, $payload)is called without athird argument, so these are private updates, and the Studio frontend consumes them through
Studio's
GlobalMessageBus. Authorized by Studio'smercureAuthorizationcookie. Fine.2. "Classic" channel.
ClientCommunicationService::sendClientUploadEvent()andsendStartLocalEditingEvent()publish through Direct Edit's ownPublishService::sendUpdate():to
http://www.pimcore.com/direct-edit/client-upload/user/<applicationPrefix><userId>, and thebrowser subscribes to the hub directly. Nothing hands that browser a subscriber token.
PublishService::getClientJwtCookie()still exists but has zero callers anywhere in vendor: theAdmin Classic controller that used to set the cookie was removed, and Direct Edit's
doc/10_Integrate_into_Custom_Application.mddocuments issuing a token as the integrator's job.The channel works today only because the updates are published public and the hub accepts
anonymous subscribers.
Portal Engine is a first-party consumer of this channel.
Controller/DataPool/AssetController.phppassesMercureUrlService::getClientSideUrl()and theprefixed portal user id into the frontend config, and
assets/scripts/features/asset/components/detail/DirectEditStatus.jsopens:Portal users authenticate as
PortalUser, a context entirely separate from Studio users, so theynever receive Studio's cookie and there is no other cookie to fall back on.
Two further defects in that component, independent of the authorization question:
useEffecthas no cleanup, so theEventSourceis never closed on unmount and every mountof the asset detail view leaves another open hub subscription behind;
eventSource.onerror = event => {}swallows every failure, which is what makes the401aboveinvisible.
This is the reason the hub hardening proposed in #346 had to be dropped
(pimcore/demo-enterprise#528 and pimcore/skeleton#287 were closed):
anonymousis currentlyload-bearing for a shipped bundle.
Not affected: the desktop client
pimcore/direct-edit-clientsubscribes to no Mercure topic at all.It uses
pimcorefile://plus HTTPS download/upload and achokidarfile watch;eventsourceappears only in its devDependencies and is required by no source file.
Expected Behavior
Every Mercure subscription Pimcore opens is authorized, so the hub can be configured to reject
unauthorized subscribers.
Suggested direction:
Direct Edit
endpoint in the bundle: either revive
getClientJwtCookie()behind a route, or add a tokenendpoint that
FileEditControllerTraitexposes. Whatever the mechanism, it has to be renewable -a token that lives an hour while the page stays open all day reproduces [Studio UI] Mercure subscription silently falls back to anonymous 1 hour after page load, all live updates stop #346 exactly.
Portal Engine
EventSource(or passwithCredentials: trueif themechanism ends up cookie-based), and renew it for as long as the page is open.
EventSourcein auseEffectcleanup.onerrora real handler, so a refused or dropped subscription is visible instead of silent.Skeleton / demo
MERCURE_EXTRA_DIRECTIVES: anonymousfrom theshipped
docker-compose.yamlfiles, which is the defence in depth [Studio UI] Mercure subscription silently falls back to anonymous 1 hour after page load, all live updates stop #346 asked for. Direct Edit'sown
doc/01_Installation/04_Mercure_Setup.mdsample already omitsanonymous, so the documentedsetup and the shipped setup currently disagree.
Verification
Against a throwaway
dunglas/mercure, publishing a public update on the Direct Edit topic:anonymoussubscribeclaimThe
subscribeclaim only gates private delivery; any valid token is enough to connect and receivepublic updates on any topic. A token per user is therefore already enough to unblock the hub
change, and publishing private is the follow-on step.
Related: #346 (the Studio side of the same authorization model), fixed by
pimcore/studio-ui-bundle#4009 and pimcore/studio-backend-bundle#2012. Direct Edit in Studio was a
second victim of that bug - its "upload complete" notification is a private update on the Studio
user topic, so it was being dropped along with everything else after an hour.