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