|
| Description: |
h2. Problem
{{XWikiHibernateStore#loadXWikiDoc}} resolves each xobject's document reference from the *wiki in the context* instead of from the wiki of the document being loaded, and then silently discards every xobject whose reference does not match the document:
{code:java} BaseObject object = it.next(); ... // It seems to search before is case insensitive. And this would break the loading if we get an // object which doesn't really belong to this document if (!object.getDocumentReference().equals(doc.getDocumentReference())) { continue; } {code}
The mismatch comes from {{BaseObject#setName(String)}}, the setter Hibernate calls when hydrating a row from {{xwikiobjects}} (the {{name}} column is mapped by property access in {{xwiki.hbm.xml}}). On a freshly hydrated object the document reference is still null, so the stored local name is resolved with the _current mixed_ document reference resolver, that is against the wiki in the context:
{code:java} public void setName(String name) { DocumentReference reference = getDocumentReference();
if (reference != null) { // ... keep the existing wiki, replace space and page ... } else { reference = getCurrentMixedDocumentReferenceResolver().resolve(name); } setDocumentReference(reference); } {code}
So any call that reaches the store with a context wiki different from the document's wiki gets back a document that exists, has its content, and has *no xobjects at all*, with nothing logged. {{XWikiCacheStore}} then stores that stripped document in the document cache, whose lifetime is unbounded, so it keeps being served until that document is next saved.
h2. This is not a cache key problem
An earlier version of this issue blamed {{XWikiCacheStore#getKey(XWikiDocument, XWikiContext)}}, which normalises the key to the context wiki. That behaviour is intended, it mirrors the Hibernate store, and changing it would break the API. Every {{XWiki}} API entry point into the store switches the context wiki to the document's own wiki before touching it, so the normalisation is a no-op and no cross-wiki key collision is reachable through the API:
* {{XWiki#getDocument(XWikiDocument, XWikiContext)}} * {{XWiki#saveDocument(XWikiDocument, String, boolean, boolean, XWikiContext)}} * {{XWiki#deleteDocument(XWikiDocument, boolean, boolean, XWikiContext)}} * {{XWiki#exists(DocumentReference, XWikiContext)}}
The {{uid}} serializer used for the key does include the wiki, so there is no blanket collision either. A search of the code base for callers that reach an {{XWikiStoreInterface}} directly without aligning the context found none: the direct {{getStore().loadXWikiDoc}} / {{saveXWikiDoc}} / {{deleteXWikiDoc}} uses outside {{XWiki}} are in the attachment stores and in the legacy {{Package}}, all inside an already switched save path, and the only {{getNotCacheStore()}} user, {{LazyXWikiDocument}}, switches the wiki itself.
The point of this issue is therefore not that such a caller is known to exist, but that when one does the store answers with silent data loss instead of an error.
h2. User visible consequence
A page renders as if it had never been configured: its xobjects are invisible, so sheets and administration sections that read them show empty fields, or leak raw Velocity where a property of the missing XClass is dereferenced. Meanwhile REST, Solr backed live tables and direct queries keep returning the correct data, and nothing shows up in the logs. On a multi wiki instance this typically hits configuration pages shipped by an extension that is installed on more than one wiki, since those exist at the same path in several wikis.
h2. Suggested fix
{{loadXWikiDoc}} should not silently return a document stripped of its xobjects. Either align the context to the document's wiki for the duration of the load, or resolve each xobject's reference against the document being loaded rather than against the context. In addition, the {{continue}} that drops an object should at least log a warning, so that a caller which does not align the context produces a diagnosable message instead of an unexplained empty page.
h2. How it was found
While investigating the flickering {{org.xwiki.image.style.test.ui.AllIT$NestedImageStyleIT#imageStyleAdministration}} test (see XWIKI-24720 for a different, already mitigated flicker of the same test). Its failure rate on the CI went from about 0.2% (June and July 2026) to about 2.8% (since mid August 2026).
The failure is always {{NoSuchElementException: *[name='Image.Style.Code.ConfigurationClass_0_defaultStyle']}} in {{ImageStyleAdministrationPage#getDefaultStyle}}. The archived screenshots show the Image Styles administration section rendered with two literal {{$escapetool.xml($property.translatedPrettyName)}} labels and without the "Default Style" select and "Force Default Style" checkbox, while the rest of the section renders normally. Frames extracted from the failure video show the section is already broken on the _first_ visit to the administration, more than 20 seconds before the assertion fails, and that it stays broken across page loads, so it is not a UI or wait race and no page object change can work around it.
The same rendering was reproduced on a local instance by deleting the {{Image.Style.Code.ConfigurationClass}} xobject of {{Image.Style.Code.Configuration}} over REST: {{$configurationDoc.display('defaultStyle', 'edit')}} then returns an empty string because {{XWikiDocument#display}} bails out on {{pclass == null}}, and {{$configurationObj.xWikiClass.get('defaultStyle')}} returns null, which is what leaks the two raw Velocity labels. A document loaded with a mismatched context wiki is in exactly that state.
The test uses {{@WikisSource}}, which runs it once on the main wiki and once on a subwiki, so {{Image.Style.Code.Configuration}} exists at the same path in both wikis. The two parameterized runs fail independently of each other.
Note that the code path which reaches the store with a mismatched context in that scenario has *not* been identified, so this issue describes a robustness defect in the store rather than a proven root cause for that test.
h2. Update: the imageStyleAdministration test is no longer evidence for this issue
A diagnostic was added to that test's page object so that a failure dumps the markup the server rendered together with the document as REST serves it. It fired on {{stable-18.8.x}} build 3, on the {{wiki1}} run: [https://ci.xwiki.org/job/XWiki%20Environment%20Tests/job/xwiki-platform/job/stable-18.8.x/3/testReport/]
REST lists the {{Image.Style.Code.ConfigurationClass}} xobject of {{wiki1:Image.Style.Code.Configuration}} (page version 3.1) a few seconds after the section rendered without the fields. Both requests read that document through the same document cache entry, and nothing saves it in between, so the document was *not* returned without its xobjects: the {{continue}} described above did not fire.
The xobject cannot be keyed against the wrong wiki either, since its XClass reference is stored relative to the document and resolved against the document's own reference in {{BaseCollection#getXClassReference()}}, not against the context.
What is empty in that failure is therefore the XClass itself: {{Image.Style.Code.ConfigurationClass}} was read as if it had no properties, which is what leaks the raw {{$property.translatedPrettyName}} labels and makes {{XWikiDocument#display}} return an empty string. That is a different defect and is tracked on XWIKI-24720.
This issue still stands on its own — {{loadXWikiDoc}} dropping xobjects silently is a real robustness defect — but it no longer has a suspected occurrence behind it.
h2. Notes
* Same family as XWIKI-13632 and XWIKI-24673. * {{XWikiDocument#getId()}} deliberately excludes the wiki from the Hibernate id as well, so the same page path in two wikis is indistinguishable at the Hibernate identifier level too. |
|