File manager - Edit - /usr/local/cpanel/whostmgr/docroot/templates/feature/views/editFeatureListController.js
Back
// Copyright 2025 WebPros International, LLC // All rights reserved. // copyright@cpanel.net http://cpanel.net // This code is subject to the cPanel license. Unauthorized copying is prohibited. /* exported $sce */ define( [ "angular", "lodash", "jquery", "cjt/util/locale", "uiBootstrap", "cjt/directives/searchDirective", "cjt/directives/spinnerDirective", "cjt/services/alertService", "app/services/featureListService", ], function(angular, _, $, LOCALE) { "use strict"; // Retrieve the current application var app = angular.module("App"); var controller = app.controller( "editFeatureListController", ["$scope", "$location", "$anchorScroll", "$routeParams", "spinnerAPI", "alertService", "featureListService", "$sce", "PAGE", function($scope, $location, $anchorScroll, $routeParams, spinnerAPI, alertService, featureListService, $sce, PAGE) { $scope.featureListName = $routeParams.name; $scope.featureListHeading = LOCALE.maketext("Select all features for: [_1]", $scope.featureListName); $scope.isDisabledFeatureList = $scope.featureListName === "disabled"; /** * Validates onlyOneRules - "There can be only one!" */ $scope.validateOnlyOneRules = function(newValue) { if (!newValue) { return { valid: true }; } // Skip validation for "disabled" feature list if ($scope.isDisabledFeatureList) { return { valid: true }; } const checkedFeatureNames = $scope.featureList .filter(feature => feature.value) .map(feature => feature.name); const allRules = $scope.featureList .filter(feature => feature.onlyOneRules) .flatMap(feature => feature.onlyOneRules); for (const rule of allRules) { const violation = $scope.checkRuleViolation(rule, checkedFeatureNames); if (violation) { return violation; } } return { valid: true }; }; /** * Check if feature matches pattern (string or {pattern, flags}) */ $scope.matches = function(name, pattern) { try { const patternStr = pattern.pattern || pattern; // Skip empty patterns or Perl's empty regex pattern if (!patternStr || patternStr === "(?^u:)" || patternStr === "(?-u:)") { return false; } return new RegExp(patternStr, pattern.flags || "").test(name); } catch (e) { void e; return false; } }; /** * Get allowed feature names from allowList * Also includes dependencies so they remain selectable */ $scope.getAllowedNames = function(feature) { if (!feature.allowList || !feature.allowList.length) { return null; } const allowed = {}; allowed[feature.name] = true; // Include dependencies in allowed list so they can still be checked if (feature.dependencies && feature.dependencies.length) { feature.dependencies.forEach(dep => { allowed[dep] = true; }); } $scope.featureList.forEach(f => { if (feature.allowList.some(pattern => $scope.matches(f.name, pattern))) { allowed[f.name] = true; } }); return allowed; }; /** * Get blocked feature names from blockList */ $scope.getBlockedNames = function(feature) { if (!feature.blockList || !feature.blockList.length) { return null; } const blocked = {}; $scope.featureList.forEach(f => { if (feature.blockList.some(pattern => $scope.matches(f.name, pattern))) { blocked[f.name] = true; } }); return blocked; }; /** * Check if feature should be restricted */ $scope.shouldRestrict = function(feature, allowedNames, blockedNames) { // Check allowList if (allowedNames && !allowedNames[feature.name]) { return true; } // Check blockList if (blockedNames && blockedNames[feature.name]) { return true; } return false; }; /** * Apply restriction to a feature (deselect and disable) */ $scope.restrictFeature = function(targetFeature, sourceFeatureName) { const wasSelected = targetFeature.value; if (wasSelected) { targetFeature.value = false; } targetFeature.restrictedBy = targetFeature.restrictedBy || {}; targetFeature.restrictedBy[sourceFeatureName] = true; targetFeature.isRestricted = true; return wasSelected; }; /** * Apply allowList/blockList restrictions */ $scope.applyRestrictions = function(feature) { if ($scope.isDisabledFeatureList) { return { deselected: [] }; } const deselected = []; const allowedNames = $scope.getAllowedNames(feature); const blockedNames = $scope.getBlockedNames(feature); $scope.featureList.forEach(f => { if ($scope.shouldRestrict(f, allowedNames, blockedNames)) { if ($scope.restrictFeature(f, feature.name)) { deselected.push(f); } } }); return { deselected }; }; /** * Remove restrictions when feature is deselected */ $scope.removeRestrictions = function(feature) { $scope.featureList.forEach(f => { if (f.restrictedBy && f.restrictedBy[feature.name]) { delete f.restrictedBy[feature.name]; if (Object.keys(f.restrictedBy).length === 0) { delete f.restrictedBy; delete f.isRestricted; } } }); return { valid: true }; }; /** * Get restriction message for a feature */ $scope.getRestrictionMessage = function(feature) { if (!feature.isRestricted || !feature.restrictedBy) { return ""; } const restrictingFeatures = Object.keys(feature.restrictedBy); if (restrictingFeatures.length === 0) { return ""; } const restrictingFeatureObjects = restrictingFeatures.map(name => { const foundFeature = $scope.featureList.find(f => f.name === name); return foundFeature || { name: name, label: name }; }); const labels = $scope.formatFeatureLabelsWithBadges(restrictingFeatureObjects); return LOCALE.maketext("This feature is disabled because it is incompatible with: “[_1]”.", labels.join(", ")); }; /** * Check if a single rule is violated by the checked features */ $scope.checkRuleViolation = function(rule, checkedFeatureNames) { const pattern = rule.pattern || rule; const flags = rule.flags || ""; let regex; try { regex = new RegExp(pattern, flags); } catch (e) { console.warn("Invalid onlyOneRules pattern:", pattern, flags, e); return null; } const matchingFeatures = checkedFeatureNames.filter(name => regex.test(name)); if (matchingFeatures.length <= 1) { return null; } const matchingFeatureObjects = matchingFeatures.map(name => { const foundFeature = $scope.featureList.find(feature => feature.name === name); return foundFeature || { name: name, label: name }; }); const matchingLabels = $scope.formatFeatureLabelsWithBadges(matchingFeatureObjects); return { valid: false, error: LOCALE.maketext("Your feature list can only include one of the following features: “[_1]”.", matchingLabels.join(", ")), }; }; /** * Recursively update dependencies when enabling a feature * Dependencies that are disabled (from the disabled feature list) will not be auto-enabled. * The backend will validate and reject if disabled features are required. */ $scope.enableDependencies = function(targetFeature, changedFeatures = []) { // Skip dependency management for "disabled" feature list if ($scope.isDisabledFeatureList) { return changedFeatures; } if (!targetFeature.dependencies || !targetFeature.dependencies.length) { return changedFeatures; } const dependencyFeatures = $scope.featureList.filter(candidateFeature => targetFeature.dependencies.includes(candidateFeature.name) ); dependencyFeatures.forEach(dependencyFeature => { // Force disabled features to be unchecked and skip enabling them // The backend will validate and reject the save if (dependencyFeature.disabled) { dependencyFeature.value = false; return; } if (!dependencyFeature.value) { dependencyFeature.value = true; changedFeatures.push(dependencyFeature); $scope.enableDependencies(dependencyFeature, changedFeatures); } }); return changedFeatures; }; /** * Recursively disable features that depend on this feature and their dependencies */ $scope.disableDependentFeatures = function(targetFeature, changedFeatures = []) { // Skip dependency management for "disabled" feature list if ($scope.isDisabledFeatureList) { return changedFeatures; } // Find features that depend on the target feature const dependentFeatures = $scope.featureList.filter(candidateFeature => candidateFeature.dependencies && candidateFeature.dependencies.includes(targetFeature.name) && candidateFeature.value && !candidateFeature.disabled ); dependentFeatures.forEach(dependentFeature => { dependentFeature.value = false; changedFeatures.push(dependentFeature); // If this feature is being auto-disabled due to a dependency change, // it must also stop restricting other features. $scope.removeRestrictions(dependentFeature); // Clear any stale alerts that may have been created when the feature // was previously toggled on. alertService.removeById("warningOnlyOneRule_" + dependentFeature.name); alertService.removeById("warningRestrict_" + dependentFeature.name); alertService.removeById("errorDisabledDependency_" + dependentFeature.name); // Also disable features that depend on this dependent feature $scope.disableDependentFeatures(dependentFeature, changedFeatures); }); return changedFeatures; }; /** * Check if a feature has dependencies that are in the disabled feature list * @param {Object} feature The feature to check * @return {Object|null} Object with feature name and disabled dependencies, or null if no issues */ $scope.getDisabledDependencyError = function(feature) { if (!feature || !feature.disabledDependencies || !feature.disabledDependencies.length) { return null; } // Get the labels for the disabled dependencies const disabledDepLabels = feature.disabledDependencies.map(depName => { const depFeature = $scope.featureList.find(f => f.name === depName); return depFeature ? (depFeature.label || depFeature.name) : depName; }); return { feature: feature.name, featureLabel: feature.label || feature.name, disabledDependencies: feature.disabledDependencies, disabledDependencyLabels: disabledDepLabels, }; }; /** * Validate all features before saving to check for disabled dependencies * @return {Object} Validation result with valid flag and errors array */ $scope.validateDisabledDependencies = function() { // Skip validation for "disabled" feature list if ($scope.isDisabledFeatureList) { return { valid: true, errors: [] }; } const errors = []; $scope.featureList.filter(f => f.value && !f.disabled).forEach(feature => { const error = $scope.getDisabledDependencyError(feature); if (error) { errors.push(error); } }); return { valid: errors.length === 0, errors: errors, }; }; /** * Format disabled dependency errors into localized messages * @param {Array} errors Array of error objects from validateDisabledDependencies * @return {Array} Array of localized error message strings */ $scope.formatDisabledDependencyErrors = function(errors) { return errors.map(error => LOCALE.maketext( "The “[_1]” feature requires “[_2]” which [numerate,_3,is,are] globally disabled.", error.featureLabel, error.disabledDependencyLabels.join(", "), error.disabledDependencies.length ) ); }; /** * Show user notification about automatic feature changes */ $scope.notifyFeatureChanges = function(changedFeatures, action) { if (!changedFeatures.length) { return; } const featureLabels = $scope.formatFeatureLabelsWithBadges(changedFeatures); let message; if (action === "enabled") { message = LOCALE.maketext("The following features were automatically enabled due to dependencies: “[_1]”.", featureLabels.join(", ")); } else { message = LOCALE.maketext("The following features were automatically disabled because they depend on unselected features: “[_1]”.", featureLabels.join(", ")); } alertService.add({ type: "info", message: message, id: "infoDependencyChanges", replace: true, }); }; /** * Handler for checkbox changes that validates onlyOneRules */ $scope.handleFeatureChange = function(feature) { if (!feature) { return; } let changedFeatures = []; // If feature is being unchecked if (!feature.value) { // Disable features that depend on this one (unless we're in "disabled" feature list) if (!$scope.isDisabledFeatureList) { changedFeatures = $scope.disableDependentFeatures(feature); $scope.notifyFeatureChanges(changedFeatures, "disabled"); } $scope.removeRestrictions(feature); alertService.removeById("warningOnlyOneRule_" + feature.name); alertService.removeById("warningRestrict_" + feature.name); alertService.removeById("errorDisabledDependency_" + feature.name); return; } // Skip further processing when editing the disabled list // (no dependency validation needed for the disabled list itself) if ($scope.isDisabledFeatureList) { return true; } // Check for disabled dependencies before proceeding const disabledDependencyError = $scope.getDisabledDependencyError(feature); if (disabledDependencyError) { // Revert the change feature.value = false; // Show error message alertService.add({ type: "danger", message: LOCALE.maketext( "Cannot enable “[_1]” because it requires “[_2]” which [numerate,_3,is,are] globally disabled. To use this feature, first remove [numerate,_3,this feature,these features] from the “disabled” feature list.", disabledDependencyError.featureLabel, disabledDependencyError.disabledDependencyLabels.join(", "), disabledDependencyError.disabledDependencies.length ), id: "errorDisabledDependency_" + feature.name, replace: true, }); return false; } // If feature is being checked, enable its dependencies (unless we're in "disabled" feature list) changedFeatures = $scope.enableDependencies(feature); // Apply restrictions (auto-deselect conflicts) const restrictResult = $scope.applyRestrictions(feature); const validation = $scope.validateOnlyOneRules(feature.value); if (!validation.valid) { // Revert the change and all dependency changes feature.value = false; changedFeatures.forEach(changedFeature => { changedFeature.value = false; }); $scope.removeRestrictions(feature); // Show error message alertService.add({ type: "warning", message: validation.error, id: "warningOnlyOneRule_" + feature.name, replace: true, }); return false; } // Notify about dependency changes (only if dependency management is enabled) $scope.notifyFeatureChanges(changedFeatures, "enabled"); // Notify about auto-deselected features if (restrictResult.deselected && restrictResult.deselected.length) { const count = restrictResult.deselected.length; const featureName = feature.label || feature.name; let message = LOCALE.maketext("The system automatically deselected [quant,_1,feature,features] because [numerate,_1,it conflicts,they conflict] with “[_2]”.", count, featureName); alertService.add({ type: "info", message: message, id: "infoRestrict_" + feature.name, replace: false }); } // Clear any previous warnings for this feature alertService.removeById("warningOnlyOneRule_" + feature.name); alertService.removeById("warningRestrict_" + feature.name); return true; }; /** * Format feature labels with badge information for alerts * @param {Array|Object} features Array of feature objects or single feature object * @return {Array} Array of formatted label strings */ $scope.formatFeatureLabelsWithBadges = function(features) { const featureArray = Array.isArray(features) ? features : [features]; return featureArray.map(feature => { const label = feature.label || feature.name; return feature.badgeLabel ? `${label} (${feature.badgeLabel})` : label; }); }; /** * Get all feature names that are part of exclusive groups */ $scope.getExclusiveFeatures = function() { // Skip exclusive feature logic for "disabled" feature list if ($scope.isDisabledFeatureList) { return []; } const allRules = $scope.featureList .filter(feature => feature.onlyOneRules && feature.onlyOneRules.length) .flatMap(feature => feature.onlyOneRules); const exclusiveFeatures = {}; allRules.forEach(rule => { const pattern = rule.pattern || rule; const flags = rule.flags || ""; try { const regex = new RegExp(pattern, flags); $scope.featureList .filter(feature => regex.test(feature.name)) .forEach(feature => exclusiveFeatures[feature.name] = true); } catch (e) { console.warn("Invalid onlyOneRules pattern:", pattern, flags, e); } }); return Object.keys(exclusiveFeatures); }; /** * Toggles the checked states - smart selection for exclusive features */ $scope.toggleAllFeatures = function() { const isSelecting = !$scope.allFeaturesChecked(); if (!isSelecting) { $scope.featureList.forEach(f => { const wasSelected = f.value; f.value = false; if (wasSelected) { $scope.removeRestrictions(f); } }); alertService.removeById("infoExclusiveFeaturesSkipped"); alertService.removeById("warningOnlyOneRuleSelectAll"); return; } const exclusiveFeatureNames = {}; $scope.getExclusiveFeatures().forEach(name => exclusiveFeatureNames[name] = true); const skippedFeatures = $scope.featureList .filter(feature => !feature.disabled && !feature.isRestricted && isSelecting && exclusiveFeatureNames[feature.name] && !feature.value) .map(feature => { feature.value = false; // Force exclusive features to stay unchecked return feature; // Return entire feature object for badge formatting }); const skippedLabels = $scope.formatFeatureLabelsWithBadges(skippedFeatures); $scope.featureList .filter(feature => !feature.disabled && !feature.isRestricted && !exclusiveFeatureNames[feature.name]) .forEach(feature => { feature.value = isSelecting; }); const hasSkippedFeatures = skippedLabels.length && isSelecting; if (hasSkippedFeatures) { alertService.add({ type: "info", message: LOCALE.maketext("The “Select All” action skipped features that override the standard login process. You can manually select them if needed: “[_1]”.", skippedLabels.join(", ")), id: "infoExclusiveFeaturesSkipped", replace: true, }); } else { alertService.removeById("infoExclusiveFeaturesSkipped"); } alertService.removeById("warningOnlyOneRuleSelectAll"); }; /** * Helper function that returns true if all non-exclusive features are checked * Exclusive features are ignored since they cannot all be selected simultaneously * * @method allFeaturesChecked * @return {Boolean} */ $scope.allFeaturesChecked = function() { // bail out if the page is still loading or feature list is nonexistent if ($scope.loadingPageData || !$scope.featureList) { return false; } const exclusiveFeatures = $scope.getExclusiveFeatures(); for (let i = 0, length = $scope.featureList.length; i < length; i++) { const currentFeature = $scope.featureList[i]; // Skip disabled features and exclusive features if (currentFeature.disabled || currentFeature.isRestricted || exclusiveFeatures.includes(currentFeature.name)) { continue; } // If any non-exclusive, non-disabled feature is unchecked, return false if (currentFeature.value === false) { return false; } } // All non-exclusive features are checked return true; }; /** * Save the list of features and return to the feature list view * * @method save * @param {Array} list Array of feature objects. * @return {Promise} */ $scope.save = function(list) { // Validate for disabled dependencies before saving const validation = $scope.validateDisabledDependencies(); if (!validation.valid) { const errorMessages = $scope.formatDisabledDependencyErrors(validation.errors); alertService.add({ type: "danger", message: LOCALE.maketext("Cannot save feature list: “[_1]”. To fix this, remove the required features from the “disabled” feature list.", errorMessages.join(" ")), id: "errorCannotSaveDisabledDeps", replace: true, }); return; } return featureListService .save($scope.featureListName, list) .then(function success() { alertService.add({ type: "success", message: LOCALE.maketext("You have successfully updated the “[_1]” feature list.", _.escape($scope.featureListName)), id: "alertSaveSuccess", replace: true, }); $scope.loadView("featureList"); }, function failure(error) { alertService.add({ type: "danger", message: error, id: "errorSaveFeatureList", }); }); }; /** * Fetch the list of hits from the server * @method fetch * @return {Promise} Promise that when fulfilled will result in the list being loaded with the new criteria. */ $scope.fetch = function() { $scope.loadingPageData = true; spinnerAPI.start("featureListSpinner"); alertService.removeById("errorFetchFeatureList"); return featureListService .load($scope.featureListName, $scope.featureDescriptions) .then(function success(results) { $scope.featureList = results.items; if (!$scope.isDisabledFeatureList) { $scope.featureList.filter(f => f.value).forEach(f => $scope.applyRestrictions(f)); } // Check for disabled dependencies on load const validation = $scope.validateDisabledDependencies(); if (!validation.valid) { const errorMessages = $scope.formatDisabledDependencyErrors(validation.errors); alertService.add({ type: "warning", message: LOCALE.maketext("This feature list cannot be saved in its current state: “[_1]”. To fix this, remove the required features from the “disabled” feature list.", errorMessages.join(" ")), id: "warningDisabledDepsOnLoad", replace: false, autoClose: 0, }); } $scope.loadingPageData = false; }, function failure(error) { alertService.add({ type: "danger", message: error, id: "errorFetchFeatureList", }); // throw an error for chained promises throw error; }).finally(function() { $scope.loadingPageData = false; spinnerAPI.stop("featureListSpinner"); }); }; $scope.$on("$viewContentLoaded", function() { alertService.clear(); var featureDescriptions = featureListService.prepareList(PAGE.featureDescriptions); $scope.featureDescriptions = _.fromPairs(_.zip(_.map(featureDescriptions.items, "id"), featureDescriptions.items)); if ( !featureDescriptions.status ) { $scope.loadingPageData = "error"; alertService.add({ type: "danger", message: LOCALE.maketext("There was a problem loading the page. The system is reporting the following error: [_1].", PAGE.featureDescriptions.metadata.reason), id: "errorFetchFeatureDescriptions", }); } else { // load the feature list $scope.fetch(); } }); }, ]); return controller; } );
| ver. 1.4 |
Github
|
.
| PHP 8.1.34 | Generation time: 0.02 |
proxy
|
phpinfo
|
Settings