Viewing File: /usr/local/cpanel/whostmgr/docroot/templates/packages/pkgform.js

//                                      Copyright 2026 WebPros International, LLC
//                                                           All rights reserved.
// copyright@cpanel.net                                         http://cpanel.net
// This code is subject to the cPanel license. Unauthorized copying is prohibited.

(function() {
    "use strict";
    var DOM = YAHOO.util.Dom,
        EVENT = YAHOO.util.Event,
        DRY_DOCK_ID = "extensionDryDock";

    // Package type visibility configuration (set from template)
    let packageTypesVisibility = {};

    // Package type to feature label mapping (set from template)
    let packageTypeFeatureLabels = {};

    // Default package type constant
    const DEFAULT_PACKAGE_TYPE = "standard";

    /**
     * Toggles the disabled state of all form inputs within an element.
     * Stores original disabled state to restore later.
     *
     * @method setInputsDisabled
     * @param {HTMLElement} container - The container element
     * @param {boolean} shouldDisable - Whether to disable the inputs
     */
    const setInputsDisabled = (container, shouldDisable) => {
        const inputs = container.querySelectorAll("input, select, textarea");
        for (const input of inputs) {
            if (shouldDisable) {
                if (!input.hasAttribute("data-original-disabled")) {
                    input.setAttribute("data-original-disabled", input.disabled);
                }
                input.disabled = true;
            } else {
                const originalDisabled = input.getAttribute("data-original-disabled");
                if (originalDisabled !== null) {
                    input.disabled = originalDisabled === "true";
                    input.removeAttribute("data-original-disabled");
                }
            }
        }
    };

    /**
     * Sets element visibility and toggles input disabled state.
     *
     * @method setElementVisibility
     * @param {HTMLElement} el - The element to update
     * @param {boolean} isVisible - Whether the element should be visible
     */
    const setElementVisibility = (el, isVisible) => {
        el.style.display = isVisible ? "" : "none";
        setInputsDisabled(el, !isVisible);
    };

    /**
     * Updates form visibility based on selected package type.
     * Handles fields, sections, extensions, and extension preselection.
     *
     * @method updateFormVisibility
     * @param {string} typeId - The selected package type ID
     */
    const updateFormVisibility = (typeId) => {
        const config = packageTypesVisibility[typeId];
        if (!config) {
            return;
        }

        // Update sections and fields (pre-computed boolean visibility)
        document.querySelectorAll("[data-section]").forEach(el => {
            setElementVisibility(el, config.sections[el.getAttribute("data-section")]);
        });

        document.querySelectorAll("[data-field]:not([data-field='options'])").forEach(el => {
            setElementVisibility(el, config.fields[el.getAttribute("data-field")]);
        });

        // Update extensions (allow/block logic: allow takes precedence, default visible)
        const ext = config.extensions || {};
        document.querySelectorAll("[data-extension]").forEach(el => {
            const id = el.getAttribute("data-extension");
            const isVisible = ext.allow ? ext.allow.includes(id) :
                ext.block ? !ext.block.includes(id) : true;
            setElementVisibility(el, isVisible);
        });

        // Handle extension checkboxes (visibility, preselect, hide)
        const checkboxes = DOM.getElementsByClassName("packageOptionSelector", "input", "packageExtensions");
        for (const checkbox of checkboxes) {
            const pkgName = DOM.getAttribute(checkbox, "data-packageOptions");
            const wrapper = checkbox.parentNode;
            const isVisible = ext.allow ? ext.allow.includes(pkgName) :
                ext.block ? !ext.block.includes(pkgName) : true;

            // Not visible per allow/block? Hide wrapper completely
            if (!isVisible) {
                wrapper.style.display = "none";
                continue;
            }

            // Visible but in hide list? Hide wrapper, auto-enable form
            if (ext.hide && ext.hide.includes(pkgName)) {
                wrapper.style.display = "none";
                if (!checkbox.checked) {
                    checkbox.checked = true;
                    const subform = DOM.get(pkgName);
                    if (subform) {
                        showExtensionForm(subform, checkbox);
                    }
                }
                continue;
            }

            // Normal case: show wrapper, handle preselect
            wrapper.style.display = "";
            if (ext.preselect && ext.preselect.includes(pkgName) && !checkbox.checked) {
                checkbox.checked = true;
                const subform = DOM.get(pkgName);
                if (subform) {
                    showExtensionForm(subform, checkbox);
                }
            }
        }

        // Update 'last' class on visible property editors
        document.querySelectorAll(".propertyGroup").forEach(group => {
            const current = group.querySelector(".propertyEditor.last");
            if (current) {
                DOM.removeClass(current, "last");
            }
            const lastVisible = Array.from(group.querySelectorAll(".propertyEditor"))
                .filter(ed => ed.style.display !== "none")
                .pop();
            if (lastVisible) {
                DOM.addClass(lastVisible, "last");
            }
        });
    };


    // Default feature list name - read from template, fallback to "default"
    const getDefaultFeatureList = () => {
        const featureListSelect = document.querySelector('select[name="featurelist"]');
        return (featureListSelect && featureListSelect.dataset.default) || "default";
    };

    /*
     * Enables or disables the Save button based on the feature list dropdown.
     *
     * Save is disabled while the dropdown has no valid value: either no feature
     * lists are available for the selected package type, or the package's
     * original list was deleted and the admin has not yet picked a replacement
     * (the empty placeholder is selected). Selecting a valid list re-enables
     * Save so the package can be repaired from this page.
     *
     * @method updateSubmitButtonState
     */
    const updateSubmitButtonState = () => {
        const featureListSelect = document.querySelector('select[name="featurelist"]');
        const submitBtn = document.getElementById("submit");
        if (!submitBtn) {
            return;
        }
        const hasValidFeatureList = !!(featureListSelect && featureListSelect.value);
        submitBtn.disabled = !hasValidFeatureList;

        // Keep the accessible relationship in sync with the disabled state.
        // A disabled Save button is otherwise announced only as "dimmed" with
        // no reason or recovery action, so point aria-describedby at whichever
        // warning message explains why (and how to fix it). Clear it when Save
        // is enabled so no stale description lingers.
        if (hasValidFeatureList) {
            submitBtn.removeAttribute("aria-describedby");
        } else {
            const messageId = getActiveFeatureListWarningMessageId();
            if (messageId) {
                submitBtn.setAttribute("aria-describedby", messageId);
            } else {
                submitBtn.removeAttribute("aria-describedby");
            }
        }
    };

    /*
     * Returns the id of the visible feature list warning message - the deleted
     * list mismatch warning or the no-available-lists warning - so the disabled
     * Save button can describe itself with the relevant explanation. Returns
     * null when neither warning is shown.
     *
     * @method getActiveFeatureListWarningMessageId
     */
    const getActiveFeatureListWarningMessageId = () => {
        const isVisible = (el) => !!el && el.style.display !== "none";
        if (isVisible(document.getElementById("featureListMismatchWarning"))) {
            return "featureListMismatchWarningMessage";
        }
        if (isVisible(document.getElementById("featureListWarning"))) {
            return "featureListWarningMessage";
        }
        return null;
    };

    /*
     * Points the "View" feature list link at the selected list, or hides it
     * when nothing valid is selected - an empty dropdown or the placeholder
     * shown in the no-list / deleted-list states - since there is nothing to
     * view in those cases.
     *
     * @method updateFeatureListViewLink
     */
    const updateFeatureListViewLink = () => {
        const featureListSelect = document.querySelector('select[name="featurelist"]');
        const editLink = document.getElementById("editFeatureListLink");
        if (!editLink) {
            return;
        }
        const selected = featureListSelect && featureListSelect.value;
        if (selected) {
            editLink.style.display = "";
            editLink.setAttribute("href", "../scripts2/featuremanager/editFeatureList?name=" + encodeURIComponent(selected));
        } else {
            editLink.style.display = "none";
        }
    };

    /*
     * Reconciles the Save button and the "View" link when the feature list
     * selection changes.
     *
     * @method handleFeatureListChange
     */
    const handleFeatureListChange = () => {
        updateSubmitButtonState();
        updateFeatureListViewLink();
    };

    /*
     * Normalizes the feature list dropdown to the repaired state after a
     * successful save.
     *
     * The deleted-list flow seeds an empty "Select a feature list …"
     * placeholder so the admin must deliberately pick a replacement. Once the
     * save succeeds the package references a real list, so that placeholder is
     * stale: re-selecting it - or resetting the form - would disable Save again
     * with no visible warning to explain why, reintroducing the very
     * disabled-with-no-reason state this page is meant to fix. Drop the
     * placeholder, keep the saved list selected, and promote it to the reset
     * default so the form stays usable for further edits.
     *
     * Exposed on window so the AJAX save handler in the template can call it
     * without the page reloading.
     *
     * @method normalizeFeatureListAfterSave
     */
    const normalizeFeatureListAfterSave = () => {
        const featureListSelect = document.querySelector('select[name="featurelist"]');
        if (!featureListSelect) {
            return;
        }

        // Drop the empty placeholder; the saved package references a real list.
        for (const option of Array.from(featureListSelect.options)) {
            if (option.value === "") {
                option.remove();
            }
        }

        // Anchor Reset to the saved selection rather than the removed
        // placeholder (or an arbitrary first option) by making it the default.
        for (const option of featureListSelect.options) {
            option.defaultSelected = option.selected;
        }

        // Reconcile Save and the View link now that only valid lists remain.
        handleFeatureListChange();
    };
    window.normalizeFeatureListAfterSave = normalizeFeatureListAfterSave;

    /*
     * Filters the feature list dropdown based on the selected package type.
     * Shows only the feature lists defined for the package type in PACKAGE_FEATURE_LISTS.
     * If a package type has no defined feature lists, show a warning and clears the dropdown.
     * Falls back to the default package type's feature lists if packageType is empty or falsy.
     *
     * @method filterFeatureListsByPackageType
     * @param {String} packageType The selected package type (e.g., 'standard', 'nova')
     */
    const filterFeatureListsByPackageType = (packageType) => {
        const featureListSelect = document.querySelector('select[name="featurelist"]');
        const warningDiv = document.getElementById("featureListWarning");
        const mismatchDiv = document.getElementById("featureListMismatchWarning");
        const submitBtn = document.getElementById("submit");

        if (!featureListSelect) {
            return;
        }

        // Get the mapping from global variable set in template
        const packageFeatureLists = window.PACKAGE_FEATURE_LISTS || {};

        // We assume packageFeatureLists to be always defined with at least the standard package type.
        // Fall back to default package type 'standard' if packageType is empty or falsy.
        const effectivePackageType = packageType || DEFAULT_PACKAGE_TYPE;
        const allowedFeatureLists = packageFeatureLists[effectivePackageType] || [];

        // Show/hide warning based on whether feature lists are available
        if (warningDiv) {
            if (allowedFeatureLists.length || mismatchDiv) {
                warningDiv.style.display = "none";
            } else {
                warningDiv.style.display = "block";

                // Update warning message with feature label
                const msgSpan = document.getElementById("featureListWarningMessage");
                if (msgSpan) {
                    const label = packageTypeFeatureLabels[effectivePackageType] || "";

                    // SECURITY NOTE: This is XSS-safe by design. Do NOT add HTML escaping/sanitization here.
                    // - textContent assignment prevents script injection (Web API protection)
                    // - the LOCALE [_1] placeholder performs simple String() conversion (no HTML interpretation)
                    // - Source: label from Cpanel::Features::load_addon_feature_descs() escaped by Template Toolkit .json().html()
                    // Additional sanitization would provide no security benefit and could harm display of feature names.
                    msgSpan.textContent = LOCALE.maketext("This package type requires a feature list with the “[_1]” feature enabled. Create one before you save this package.", label);
                    if (submitBtn) {
                        submitBtn.disabled = true;
                    }
                }
            }
        }

        // If no feature lists are available for this package type, replace the
        // dropdown contents with a placeholder directing the admin to create
        // one, and hide the "View" link since there is nothing to view. The
        // accompanying warning provides the create action.
        if (allowedFeatureLists.length === 0) {
            featureListSelect.innerHTML = "";
            const placeholder = document.createElement("option");
            placeholder.value = "";
            placeholder.textContent = LOCALE.maketext("Create a feature list …");
            placeholder.selected = true;
            featureListSelect.appendChild(placeholder);

            updateFeatureListViewLink();
            updateSubmitButtonState();
            return;
        }

        // When the package's original feature list was deleted, force the
        // admin to make a deliberate choice instead of silently auto-selecting
        // a replacement: select a non-value placeholder so the mismatch warning
        // stays meaningful and Save stays disabled until a valid list is picked.
        const mismatchActive = !!mismatchDiv && mismatchDiv.style.display !== "none";

        // Store the currently selected value to try to restore it after filtering.
        // This is useful if package types share feature lists (overlap), allowing
        // the user's selection to persist when switching between types.
        const currentValue = featureListSelect.value;

        // Check if the current value exists in the new allowed list
        const currentValueInNewList = allowedFeatureLists.includes(currentValue);

        // Determine which value to select:
        // 1. In the mismatch state, select nothing - the placeholder is selected.
        // 2. Keep current selection if it exists in the new list
        // 3. Otherwise, prefer the default feature list if it exists in the new list
        // 4. Otherwise, the first option will be auto-selected
        const defaultFeatureList = getDefaultFeatureList();
        const preferredValue = mismatchActive
            ? null
            : currentValueInNewList
                ? currentValue
                : (allowedFeatureLists.includes(defaultFeatureList) ? defaultFeatureList : null);

        // Clear existing options
        featureListSelect.innerHTML = "";

        if (mismatchActive) {
            const placeholder = document.createElement("option");
            placeholder.value = "";
            placeholder.textContent = LOCALE.maketext("Select a feature list …");
            placeholder.selected = true;
            featureListSelect.appendChild(placeholder);
        }

        // Add filtered options
        for (const featurelist of allowedFeatureLists) {
            const option = document.createElement("option");
            option.value = featurelist;
            option.textContent = featurelist;
            if (preferredValue && featurelist === preferredValue) {
                option.selected = true;
            }
            featureListSelect.appendChild(option);
        }

        // Update the "View" link to match the selection: shown and pointed at
        // the chosen list, or hidden while the placeholder is selected.
        updateFeatureListViewLink();

        // Reconcile the Save button now that the dropdown reflects the
        // selected package type's valid feature lists.
        updateSubmitButtonState();
    };

    /*
     * Handles package type radio button change.
     * Updates the visual selection state of package type cards
     * and updates form field/section visibility.
     *
     * @method handlePackageTypeChange
     * @param {Event} evt The change event
     */
    const handlePackageTypeChange = (evt) => {
        const radio = EVENT.getTarget(evt);
        if (!radio || !radio.parentNode) {
            return;
        }

        const cards = DOM.getElementsByClassName("pkgTypeCard", "label", "packageTypeSelector");
        if (!cards || !cards.length) {
            return;
        }

        for (const card of cards) {
            DOM.removeClass(card, "pkgTypeCardSelected");
        }

        const selectedCard = radio.parentNode;
        if (selectedCard && DOM.hasClass(selectedCard, "pkgTypeCard")) {
            DOM.addClass(selectedCard, "pkgTypeCardSelected");
        }

        // Update form visibility based on selected package type
        updateFormVisibility(radio.value);

        // Filter feature lists based on the newly selected package type
        filterFeatureListsByPackageType(radio.value);
    };

    /*
     * Adds change handlers to package type radio buttons.
     * Called from onDOMReady
     *
     * @method addPackageTypeHandlers
     */
    const addPackageTypeHandlers = () => {
        const radios = DOM.getElementsByClassName("pkgTypeRadio", "input", "packageTypeSelector");

        // React to manual feature list selection so the admin can repair a
        // package whose original feature list was deleted: (re-)enable Save once
        // a valid list is chosen. The mismatch warning intentionally persists
        // until the change is saved and the page reloads.
        const featureListSelect = document.querySelector('select[name="featurelist"]');
        if (featureListSelect) {
            EVENT.addListener(featureListSelect, "change", handleFeatureListChange);
        }

        // A native form reset restores the select's options without firing a
        // "change" event, so the Save button and "View" link would otherwise be
        // left in a stale state (e.g. Save enabled and View pointing at a list
        // the placeholder no longer reflects). Reconcile after the browser has
        // applied the reset, which happens asynchronously, hence setTimeout(0).
        const mainform = document.forms.mainform;
        if (mainform) {
            EVENT.addListener(mainform, "reset", () => {
                setTimeout(handleFeatureListChange, 0);
            });
        }

        // Add change listeners to radio buttons (if present)
        if (radios && radios.length) {
            for (const radio of radios) {
                EVENT.addListener(radio, "change", handlePackageTypeChange);
            }
        }

        // Apply initial filtering based on the currently selected package type
        const selectedRadio = document.querySelector('input[name="package_type"]:checked');
        if (selectedRadio) {
            filterFeatureListsByPackageType(selectedRadio.value);
        } else {

            // On edit page, no radio buttons exist - check for hidden input
            const hiddenInput = document.querySelector("input[name='package_type'][type='hidden']");
            if (hiddenInput) {
                filterFeatureListsByPackageType(hiddenInput.value);
            }
        }
    };

    /*
     * Moves a given package extension editor fieldset from
     * the form to the form "drydock."
     *
     * @method drydockExtensionForm
     * @param {HTMLElement} subform The form to move
     */
    var drydockExtensionForm = function(subform) {
        var dryDock = DOM.get(DRY_DOCK_ID);
        var removedSubform = subform.parentNode.removeChild(subform);

        if (removedSubform) {
            dryDock.appendChild(removedSubform);
            DOM.replaceClass(removedSubform, "visible", "hidden");
        }
    };

    /*
     * Moves a given package extension editor fieldset from
     * the form "drydock" to the form.
     *
     * @method showExtensionForm
     * @param {HTMLElement} subform The form to move
     * @param {HTMLElement} control The control (usually a checkbox) that shows/hides the package extension fields
     */
    function showExtensionForm(subform, control) {
        var dryDock = DOM.get(DRY_DOCK_ID);
        var subformToShow = dryDock.removeChild(subform);
        if (subformToShow) {
            var packageExtensionsContainer = DOM.get("packageExtensions");
            packageExtensionsContainer.insertBefore(subformToShow, control.parentNode.nextSibling);
            DOM.replaceClass(subformToShow, "hidden", "visible");
        }
    }

    /*
     * Toggles the visibility of a given package extension form.
     *
     * @method showHidePackageOptions
     * @param {MouseEvent} mouseEvt Mouse event data
     * @param {Object} controlData Click handler data structure
     */
    var showHidePackageOptions = function(mouseEvt, controlData) {
        var subform = DOM.get(controlData.packageName);
        var relatedControl = DOM.get(controlData.controlId);
        if (relatedControl.checked) {
            showExtensionForm(subform, relatedControl);
        } else {
            drydockExtensionForm(subform);
        }
    };

    /*
     * Adds click handlers to package extension toggle control (usually a checkbox).
     * Click handlers add remove related fieldset items from page form.
     * Called from onDOMReady
     *
     * @method addClickHandlers
     */
    var addClickHandlers = function() {
        var pkgOptionsControls = DOM.getElementsByClassName("packageOptionSelector", "input", "packageExtensions");
        var pkgOptionsControlCount = pkgOptionsControls.length;
        for (var i = 0; i < pkgOptionsControlCount; i++) {
            var control = pkgOptionsControls[i];
            control.checked = false; // turn off checkbox on reload
            EVENT.addListener(control, "click",
                showHidePackageOptions, {
                    packageName: DOM.getAttribute(control, "data-packageOptions"),
                    controlId: control.id,
                }
            );
        }
    };

    /*
     * Adds "last" class to last property editor within a property group.
     * Makes sure last property editor doesn't have a bottom border
     * (primarily for IE8 compatibility).
     * Called from onDOMReady
     *
     * @method addLastStyleToPropertyGroups
     */
    var addLastStyleToPropertyGroups = function() {
        var isLastPropertyEditor = function(el) {
            return DOM.hasClass(el, "propertyEditor");
        };

        var fixLastPropertyEditors = function(containerId) {
            var packageExtensions = DOM.getElementsByClassName("propertyGroup", "div", containerId);

            var propertyGroupCount = packageExtensions.length;
            for (var j = 0; j < propertyGroupCount; j++) {
                var lastInGroup = DOM.getLastChildBy(packageExtensions[j], isLastPropertyEditor);
                if (lastInGroup) {
                    DOM.addClass(lastInGroup, "last");
                }
            }
        };

        // check extension dry dock first
        fixLastPropertyEditors("extensionDryDock");

        // now do package extensions
        fixLastPropertyEditors("packageExtensions");
    };

    /**
     * Initializes package type visibility and feature labels from data attributes.
     * Called from onDOMReady.
     *
     * @method initPackageTypesFromDataAttr
     */
    const initPackageTypesFromDataAttr = () => {
        const selector = DOM.get("packageTypeSelector");
        if (selector) {
            const configData = DOM.getAttribute(selector, "data-visibility-config");
            if (configData) {
                try {
                    packageTypesVisibility = JSON.parse(configData);
                } catch (error) {

                    // Invalid JSON
                    console.error(error);
                }
            }
        }

        // Load feature labels mapping from warning div
        const warningDiv = DOM.get("featureListWarning");
        if (warningDiv) {
            const labelsData = DOM.getAttribute(warningDiv, "data-feature-labels");
            if (labelsData) {
                try {
                    packageTypeFeatureLabels = JSON.parse(labelsData);
                } catch (error) {

                    // NOTE: Parse failures can be safely ignored. If packageTypeFeatureLabels remains empty,
                    // the feature list warning message will simply display without a feature label (empty string).
                    // This is acceptable degraded UX and does not break functionality.
                    console.error(error);
                }
            }
        }

        // Apply initial visibility for currently selected package type
        applyInitialVisibility();
    };

    /**
     * Applies visibility for the initially selected package type.
     * Called after loading the visibility config.
     *
     * @method applyInitialVisibility
     */
    function applyInitialVisibility() {
        const checkedRadio = document.querySelector(".pkgTypeRadio:checked");
        if (checkedRadio) {
            updateFormVisibility(checkedRadio.value);
        } else {

            // Default to 'standard' if no radio is checked (e.g., hidden input)
            const hiddenInput = document.querySelector("input[name='package_type'][type='hidden']");
            if (hiddenInput) {
                updateFormVisibility(hiddenInput.value);
            }
        }
    }

    EVENT.onDOMReady(initPackageTypesFromDataAttr);
    EVENT.onDOMReady(addClickHandlers);
    EVENT.onDOMReady(addLastStyleToPropertyGroups);
    EVENT.onDOMReady(addPackageTypeHandlers);
}());
Back to Directory