Problem
The Languagevariable content type — which the entire i18n subsystem depends on — is not marked as a system content type, so nothing prevents it from being deleted.
Task04210CreateDefaultLanguageVariable creates it with:
// Task04210CreateDefaultLanguageVariable.java:66-67
final boolean isSystem = Boolean.FALSE;
final boolean isFixed = Boolean.TRUE;
fixed = true protects the type's fields from being modified. It does not protect the type itself. The only guard against deletion is the system flag:
// ContentTypeFactoryImpl.dbDelete
if (type.defaultType()) {
throw new DotDataException("contenttype.delete.cannot.delete.default.type");
}
if (type.system()) {
throw new DotDataException("contenttype.delete.cannot.delete.system.type");
}
Since system = false, that guard never fires. Languagevariable is also absent from ContentTypeAPI.reservedStructureVars and reservedStructureNames (which cover host, folder, file, forms, htmlpage, menulink, container, template, user, calendarEvent).
Impact
Anyone with permission to delete content types can remove it, with no warning and no guard.
Once it is gone, LanguageVariableAPIImpl looks it up unconditionally and throws:
APILocator.getContentTypeAPI(user).find(LANGUAGEVARIABLE_VAR_NAME);
// -> com.dotcms.contenttype.exception.NotFoundInDbException:
// Content Type with id:'Languagevariable' not found
So the failure mode is site-wide i18n breakage, and it degrades quietly — language-variable resolution falls back to emitting the raw key rather than the translated value.
Note that ContentTypeAPIImpl deletes asynchronously by default (DELETE_CONTENT_TYPE_ASYNC), so the deletion can land some time after the request that triggered it, making the connection between cause and symptom harder to see.
How this surfaced
Observed in CI on #36943. Within a single integration-test shard the content type existed early in the run (a test at position 2 did contentTypeApi.find("Languagevariable") successfully) and was gone by position 21:
ContentletAjaxTest:216 NotFoundInDbException: Content Type with id:'Languagevariable' not found
FieldUtilTest:129 expected:<test[]> but was:<test[1786122699489]> <- i18n fell back to the raw key
FieldUtilTest:80 ConditionTimeout waiting for "test"
ContentTypeDataGen later logged Content type Languagevariable not found Creating language variable content type and recreated it — after the failures.
Had the type been marked system, whatever deleted it would have thrown instead of silently succeeding, and this class of failure would be impossible.
Proposed fix
- Add a startup task setting the flag on existing installations — changing the run-once task alone does not help anyone already upgraded:
UPDATE structure SET system = true WHERE velocity_var_name = 'Languagevariable';
- Create it as
system = true going forward.
- Consider adding
languagevariable to reservedStructureVars / reservedStructureNames, so a user cannot create or rename another type into that slot.
Known consequence to plan for
Task240306MigrateLegacyLanguageVariablesTest.removeLanguageVariableContentType() deliberately deletes this content type to verify that executeUpgrade() recreates it. Once the type is protected, that delete will throw and the test will fail.
That test scenario needs rework as part of this change — either dropping the type via direct SQL to set up the scenario, or reconsidering whether "recreate after deletion" is still a scenario worth covering once deletion is impossible through the API.
Not established
Which specific operation removed the type in the CI run above. The defect stands on its own regardless: the type is deletable when it should not be.
Problem
The
Languagevariablecontent type — which the entire i18n subsystem depends on — is not marked as a system content type, so nothing prevents it from being deleted.Task04210CreateDefaultLanguageVariablecreates it with:fixed = trueprotects the type's fields from being modified. It does not protect the type itself. The only guard against deletion is thesystemflag:Since
system = false, that guard never fires.Languagevariableis also absent fromContentTypeAPI.reservedStructureVarsandreservedStructureNames(which coverhost,folder,file,forms,htmlpage,menulink,container,template,user,calendarEvent).Impact
Anyone with permission to delete content types can remove it, with no warning and no guard.
Once it is gone,
LanguageVariableAPIImpllooks it up unconditionally and throws:So the failure mode is site-wide i18n breakage, and it degrades quietly — language-variable resolution falls back to emitting the raw key rather than the translated value.
Note that
ContentTypeAPIImpldeletes asynchronously by default (DELETE_CONTENT_TYPE_ASYNC), so the deletion can land some time after the request that triggered it, making the connection between cause and symptom harder to see.How this surfaced
Observed in CI on #36943. Within a single integration-test shard the content type existed early in the run (a test at position 2 did
contentTypeApi.find("Languagevariable")successfully) and was gone by position 21:ContentTypeDataGenlater loggedContent type Languagevariable not found Creating language variable content typeand recreated it — after the failures.Had the type been marked
system, whatever deleted it would have thrown instead of silently succeeding, and this class of failure would be impossible.Proposed fix
system = truegoing forward.languagevariabletoreservedStructureVars/reservedStructureNames, so a user cannot create or rename another type into that slot.Known consequence to plan for
Task240306MigrateLegacyLanguageVariablesTest.removeLanguageVariableContentType()deliberately deletes this content type to verify thatexecuteUpgrade()recreates it. Once the type is protected, that delete will throw and the test will fail.That test scenario needs rework as part of this change — either dropping the type via direct SQL to set up the scenario, or reconsidering whether "recreate after deletion" is still a scenario worth covering once deletion is impossible through the API.
Not established
Which specific operation removed the type in the CI run above. The defect stands on its own regardless: the type is deletable when it should not be.