Skip to content

[DEFECT] Custom Fields Double Rendering and Malformed HTML Injection in Iframe Mode #34207

@nicobytes

Description

@nicobytes

Problem Statement

When requesting a Custom Field via the API with newRenderMode set to iframe, the content is rendering incorrectly, leading to UI artifacts and functionality breakage.

Based on DOM inspection, the backend appears to be rendering the content twice:

Once as correct HTML.

Once as a raw, escaped JSON string (e.g., <div id=""displayURLTitle""...) injected directly into the document body.

This malformed injection causes an Uncaught SyntaxError: Invalid or unexpected token in the browser console, preventing the custom field scripts from executing correctly.

This issue is specifically observed when Edit Mode is enabled, with newRenderMode: iframe

Image Image Image

Steps to Reproduce

  1. Go to Pages Content type
  2. Enable the new edit mode
  3. Go to template custom field
  4. Choose deprecated API aka iframes
  5. Put this code.
<script type="application/javascript">
// Global Constants
const hostId = "$request.getSession().getAttribute('CMS_SELECTED_HOST_ID')";
const defaultTemplateName = '$config.getStringProperty("DEFAULT_TEMPLATE_NAME", "System Template")'; // try to preload the default template.

dojo.require("dotcms.dojo.data.TemplateReadStore");

// UTILS

/**
* Normalizes the key of a template
*
*/
const normalizeKey = function (template) {
    return template.fullTitle.replace(new RegExp("\\("+template.hostName+"\\)"), '').replace(/\s+/g,'').toLowerCase();
}

/**
* Returns a map of templates by name and by id
*
*/
const getTemplatesMaps = (templates) => {
    const mapByName = templates.reduce(function(map, template) {
        const key = normalizeKey(template);
        if (!map[key]) {
            map[key] = template;
        }

        return map;
    }, {});

    const mapById = templates.reduce(function(map, template) {
        map[template.identifier] = template;
        return map;
    }, {});


    return {
        mapByName,
        mapById
    }
}

// UTILS END
/**
* Fetches the template image from the server
*
*/
function fetchTemplateImage(templateId) {
    fetch("/api/v1/templates/image", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({ templateId})
    }).then(async (response) => {
        // The ok value represents the result of the response status 200 codes
        if (response.ok) {
            const result = await response.json();

            getTemplateCallBack(result); // here we pass the result of the json response to the callback function
        } else {
            throw new Error("Error fetching template image");
        }
    })
    .catch((error) => {
        const imageEl = dojo.byId("templateThumbnailHolder");
        imageEl.src = "/html/images/shim.gif";
        imageEl.style.border = "0px";
    });
};

/**
* Callback function to handle the template fetch
*
*/
const onTemplateFetchComplete = function(templates, currentRequest) {
    const templateId = dojo.byId("template").value;
    const isTemplateValid  = templateId && templateId != "0";

    if(!templates || templates.length === 0){
        return;
    }

    const { mapById, mapByName } = getTemplatesMaps(templates);
    const normalizedName = defaultTemplateName.replace(/\s+/g,'').toLowerCase();
    const template = isTemplateValid ? mapById[templateId]:mapByName[normalizedName];

    if(!template){
        return;
    }

    const { identifier, fullTitle } = template;

    // We set the values directly into the components because setting it directly into`templateSelect` fires another load operation.
    dojo.byId("currentTemplateId").value = identifier;
    dojo.byId("template").value = identifier;
    dijit.byId('templateSel').set("displayedValue", fullTitle);
    fetchTemplateImage(identifier);
};

function resetTemplateSelection() {
    const templateSel = dijit.byId("templateSel");
    templateSel.set("value","");
    templateSel.filter();
    dojo.byId("template").value= '';
    dojo.byId("templateSel").value = "";
    dojo.byId("currentTemplateId").value = "";
    dojo.byId("templateThumbnailHolder").src = "/html/images/shim.gif";
    dojo.byId("templateThumbnailHolder").style.border = '0px';
}


/**
* Get the template callback
*
*/
function getTemplateCallBack(data) {
    const imageInode = data.identifier;
    const imageEl=dojo.byId("templateThumbnailHolder");

    if (isInodeSet(imageInode)) {
    	imageEl.src = "/dA/" + imageInode + "/250w";
        imageEl.style.border = '1px solid #B6CBEB';
        imageEl.style.marginTop = '1rem';
    } else {
        imageEl.src  = "/html/images/shim.gif";
        imageEl.style.border = '0px';
    }

}

dojo.ready(function(){
    const templateId = dojo.byId("template").value;
    const isTemplateValid  = templateId && templateId != "0";

    const currentTemplateIdElement = dojo.byId("currentTemplateId");
    currentTemplateIdElement.value = templateId;

    const templateStore = new dotcms.dojo.data.TemplateReadStore({
        hostId: '',
        templateSelected: templateId,
        allSiteLabel: true,
    });

    const templateSelect = new dijit.form.FilteringSelect({
        id:"templateSel",
        name:"templateSel",
        style:"width:350px;",
        onChange: templateChanged,
        store: templateStore,
        searchDelay: 300,
        pageSize: 15,
        autoComplete: false,
        ignoreCase: true,
        labelType:"html",
        searchType:"html",
        labelAttr: "htmlTitle",
        searchAttr: "fullTitle",
        value: templateId,
        invalidMessage: '$text.get("Invalid-option-selected")',
    },"templateHolder");

    if (isTemplateValid){
       fetchTemplateImage(templateId);

        const templateSel = dijit.byId("templateSel");
        templateSel.set("value", templateId);
    }

    const templateFetchParams = {
        query: {
            fullTitle: '*'
        },
        queryOptions: {},
        start: 0,
        count: 15,
        onComplete: onTemplateFetchComplete
    };

    function handleAllSiteClick() {
        templateStore.hostId = "*";
        templateStore.allSiteLabel=false;
        resetTemplateSelection();
    }

    /**
    * Handles the template change event
    *
    */
    function templateChanged() {
        const templateSel = dijit.byId("templateSel");
        const value = templateSel?.get('value');

        if(!value) {
            resetTemplateSelection();
            return;
        }

        if(value == "0") {
            handleAllSiteClick();
            return;
        }

        dojo.byId("template").value=value;
        fetchTemplateImage(value);
    }

    templateStore.fetch(templateFetchParams);
});
</script>

<div id="templateHolder"></div>
<input id="currentTemplateId" type="hidden" name="currentTemplateId" value=""/>
<div>
    <img id="templateThumbnailHolder" src="/html/images/shim.gif" alt="Template Thumbnail"/>
</div>

Acceptance Criteria

The Iframe should return valid, clean HTML only once. It should not contain escaped string representations of the DOM elements or double-rendered content.

dotCMS Version

latest

Severity

Critical - System unusable

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions