/*
# cpanel - whostmgr/docroot/templates/easyapache4/services/ea4Util.js
# 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
*/
/* global define: false */
define(
'app/services/ea4Util',[
"angular",
"cjt/util/locale",
"lodash",
"cjt/io/api",
"cjt/io/whm-v1-request",
"cjt/io/whm-v1",
"cjt/services/APIService",
],
function(angular, LOCALE, _) {
"use strict";
var app = angular.module("whm.easyapache4.ea4Util", []);
app.factory("ea4Util", ["wizardState", function(wizardState) {
var util = {
eaRegex: /^ea-/i,
phpVerRegex: /^ea-php(\d{2})$/i,
phpExtRegex: /^ea-php(\d{2}-)/i,
apacheVerRegex: /^ea-apache(\d{2})$/i,
rubyVerRegex: /^ea-ruby(\d{2})/i,
apacheModulesRegex: /^ea-apache(\d{2}-)/i,
additionalPkgList: [],
defaultMeta: {
// Search/Filter settings
filterList: {},
filterValue: "",
isEmptyList: false,
// Pager settings
showPager: true,
maxPages: 0,
totalItems: 0,
currentPage: 1,
pageSize: 10,
pageSizes: [10, 20, 50, 100],
start: 0,
limit: 10,
},
pkgActions: {
removeList: [],
addList: [],
actionNeeded: false,
},
vhostWarning: {
exist: false,
text: "",
},
multiRequirements: {
exist: false,
orList: [],
chosenPackage: "",
},
autoSelectExt: {
list: [],
text: "",
errorList: [],
show: false,
showError: false,
showCommonExtensions: false,
},
checkUpdateInfo: {
isLoading: true,
pkgNumber: 0,
btnText: LOCALE.maketext("Checking for updates …"),
btnTitle: LOCALE.maketext("Checking for updates …"),
btnCss: "btn-ea4-looking-updates disabled",
},
};
util.cachedPrefixes = null;
util.cachePrefixes = function(prefixes) {
util.cachedPrefixes = prefixes;
};
util.getCachedPrefixes = function() {
return util.cachedPrefixes || [];
};
util.gatherAllRequirementsOfPkgs = function(pkgsToConsider, packageListDetails) {
var allRequires = [];
_.each(pkgsToConsider, function(pkg) {
if (!_.includes(allRequires, pkg)) {
var pkgInfo = packageListDetails[pkg];
if (typeof pkgInfo !== "undefined") {
var result = util.recurseForRequires(pkgInfo, pkg, allRequires, packageListDetails);
if (result !== undefined) {
allRequires = result;
}
}
allRequires.push(pkg);
}
});
return allRequires;
};
util.recurseForRequires = function(pkgInfo, origPkgName, allPkgList, packageListDetails) {
var pkgName = pkgInfo.package;
var additionalPrefixes = util.getCachedPrefixes();
var allPrefixes = ["ea"].concat(additionalPrefixes);
var matchesAnyPrefix = _.some(allPrefixes, function(prefix) {
return pkgName.startsWith(prefix + "-");
});
if (!matchesAnyPrefix) {
return;
}
if (_.includes(allPkgList, pkgName)) {
return;
}
if (typeof origPkgName !== "undefined" && pkgName !== origPkgName) {
allPkgList.push(pkgName);
}
var recurseArray = [];
if (pkgInfo.pkg_dep.requires.length > 0) {
recurseArray = _.clone(pkgInfo.pkg_dep.requires);
recurseArray = _.filter(recurseArray, function(pkg) {
if (typeof pkg !== "string" || _.isArray(pkg)) {
return false;
}
var reqMatchesPrefix = _.some(allPrefixes, function(prefix) {
return pkg.startsWith(prefix + "-");
});
return reqMatchesPrefix;
});
recurseArray = _.difference(recurseArray, allPkgList);
}
_.forEach(recurseArray, function(reqPkg) {
var reqPkgInfo = packageListDetails[reqPkg];
if (typeof reqPkgInfo !== "undefined") {
util.recurseForRequires(reqPkgInfo, origPkgName, allPkgList, packageListDetails);
}
});
return allPkgList;
};
/**
* @method getFormattedPackageList
* Takes an Array of packages names and returns a new sorted list
* with formatted names. By default takes out the "ea-" part of
* the package name. Also takes a second optional regex param to change the default behavior.
* @example getFormattedPackageList("ea-openssl"); // returns "openssl"
* @param {Array} pkgList - Array contains a list of packages names.
* @param {RegExp} regex - Optional
* @return {Array} - New formatted and sorted list.
*/
util.getFormattedPackageList = function(pkgList, regex) {
var formattedList = [];
var additionalPrefixes = util.getCachedPrefixes() || [];
var allPrefixes = ["ea"].concat(additionalPrefixes);
_.each(pkgList, function(pkg) {
var formattedName;
// For review list, preserve non-EA prefixes
var pkgPrefix = "ea";
var match = _.find(allPrefixes, function(prefix) {
return pkg.startsWith(prefix + "-");
});
if (match) {
pkgPrefix = match;
}
if (pkgPrefix === "ea") {
// For EA packages, strip the prefix as usual
formattedName = util._getReadableName(pkg, regex);
} else {
// For non-EA packages, keep the full name to preserve context
formattedName = pkg;
}
formattedList.push(formattedName);
});
return formattedList;
};
util.getFormattedPackageName = function(pkg, regex) {
return util._getReadableName(pkg, regex);
};
util._getReadableName = function(pkg, replaceRegex) {
var strippedName = pkg;
if (typeof replaceRegex !== "undefined" && replaceRegex !== "") {
strippedName = pkg.replace(replaceRegex, "");
} else {
if (util.apacheModulesRegex.test(pkg)) {
strippedName = pkg.replace(util.apacheModulesRegex, "");
} else {
// Strip ALL prefixes for consistent naming and sorting
var additionalPrefixes = util.getCachedPrefixes() || [];
var allPrefixes = ["ea"].concat(additionalPrefixes);
var prefixPattern = "^(" + allPrefixes.join("|") + ")-";
var prefixRegex = new RegExp(prefixPattern);
if (prefixRegex.test(pkg)) {
strippedName = pkg.replace(prefixRegex, "");
} else {
strippedName = pkg;
}
}
}
if (strippedName === "php") {
strippedName = "php (DSO)";
}
return strippedName;
};
/**
* @method getProfilePackagesByCategories
* Takes an Array of packages names and groups them together in different categories.
* The method will return an Object where the keys are the new categories.
* @param {Array} pkgList - list of packages names.
* @return {Object} - each keys equals a new category.
*/
util.getProfilePackagesByCategories = function(pkgList) {
var pkgCategories = util._pkgByCategory(pkgList);
return pkgCategories;
};
util._pkgByCategory = function(pkgList) {
var categories = {}, apacheList = [], phpExtList = [], others = [], phpVersions = [], apacheVersion = "";
var apachePrefix = "ea";
var additionalPrefixes = util.getCachedPrefixes();
var allPrefixes = ["ea"].concat(additionalPrefixes);
_.each(pkgList, function(pkg) {
if (util.apacheVerRegex.test(pkg)) {
apacheVersion = pkg;
var match = pkg.match(new RegExp("^(" + allPrefixes.join("|") + ")-apache"));
if (match) {
apachePrefix = match[1];
}
} else if (util.apacheModulesRegex.test(pkg)) {
apacheList = _.concat(apacheList, pkg);
} else if (util.phpVerRegex.test(pkg)) {
phpVersions = _.concat(phpVersions, pkg);
} else if (util.phpExtRegex.test(pkg)) {
phpExtList = _.concat(phpExtList, pkg);
} else {
others = _.concat(others, pkg);
}
});
if (apacheList.length > 0) {
apacheVersion = util._getReadableName(apacheVersion, util.eaRegex);
var categoryKey = apachePrefix + "_" + apacheVersion;
categories[categoryKey] = {
"name": util.versionToString(apacheVersion),
"prefix": apachePrefix !== "ea" ? apachePrefix : null,
"packages": util.getFormattedPackageListForProfile(apacheList, util.apacheModulesRegex),
};
}
if (phpVersions.length > 0) {
_.each(phpVersions.sort(), function(version) {
var verRegex = new RegExp("^" + _.escapeRegExp(version) + "-");
var extensions = _.filter(phpExtList, function(pkg) {
return verRegex.test(pkg);
});
phpExtList = _.difference(phpExtList, extensions);
var phpversion = util._getReadableName(version, util.eaRegex);
var phpPrefix = "ea";
var match = version.match(new RegExp("^(" + allPrefixes.join("|") + ")-php"));
if (match) {
phpPrefix = match[1];
}
var categoryKey = phpPrefix + "_" + phpversion;
categories[categoryKey] = {
"name": util.versionToString(phpversion).replace("Php", "PHP"),
"prefix": phpPrefix !== "ea" ? phpPrefix : null,
"packages": util.getFormattedPackageListForProfile(extensions, util.phpExtRegex),
};
});
}
// Handle Others.
if (others.length > 0) {
categories["others"] = {
"name": LOCALE.maketext("Additional Packages"),
"prefix": null,
"packages": util.getFormattedPackageListForProfile(others, /^ea-/),
};
}
return categories;
};
/**
* @method getFormattedPackageListForProfile
* Like getFormattedPackageList but always strips prefixes for profile view
* since it has labels to show namespace context
* @param {Array} pkgList - Array contains a list of packages names.
* @param {RegExp} regex - Optional
* @return {Array} - New formatted and sorted list with ALL prefixes stripped.
*/
util.getFormattedPackageListForProfile = function(pkgList, regex) {
var formattedList = [];
_.each(pkgList, function(pkg) {
// For profile view, always strip prefixes since we have labels
var formattedName = util._getReadableName(pkg, regex);
formattedList.push(formattedName);
});
return formattedList;
};
/**
* @method versionToString
* Returns a version in a user friendly format.
* @example versionToString("apache24") // returns "Apache 2.4"
* @param {String}
* @return {String}
*/
util.versionToString = function(version) {
return version.replace(/^([a-z]+)(\d)(\d)$/, function(match, p1, p2, p3) {
return _.capitalize(p1) + " " + p2 + "." + p3;
});
};
/**
* @method getPackageLabel
* Returns appropriate package label for a given package state.
* @example getPackageLabel(true, "updatable"); // returns "Update"
* @param {Boolean} selected
* @param {String} state
* @return {String}
*/
util.getPackageLabel = function(selected, state) {
var str = "";
if (selected) {
if (state === "updatable") {
str = "Update";
} else if (state !== "installed" && state !== "updatable") {
str = "Install";
} else if (state === "installed") {
str = "Unaffected";
}
} else if (state === "installed" || state === "updatable") {
str = "Uninstall";
}
return str;
};
/**
* @method getPackageClass
* Returns the proper css callout class for a given package state.
* @param {Boolean} selected
* @param {String} state
* @return {String}
*/
util.getPackageClass = function(selected, state) {
var classString;
if (selected) {
classString = "callout";
if (state === "updatable") {
classString += " callout-info";
} else {
classString += " callout-success";
}
} else {
if (state === "installed" || state === "updatable") {
classString = "callout callout-warning";
} else {
classString = "no-callout";
}
}
return classString;
};
/**
* @method getDefaultMetaData
* Returns a deep copy of the Default Meta Data needed
* to initialize the Wizard component
* @return {Object}
*/
util.getDefaultMetaData = function() {
return _.clone(util.defaultMeta);
};
/**
* @method getDefaultPageSizes
* Returns an Array with the Default page size values
* @return {Array}
*/
util.getDefaultPageSizes = function() {
return util.defaultMeta.pageSizes;
};
/**
* @method getUpdatedMetaData
* Returns updated meta data. While updating the metadata,
* the package list may be filtered depending on the search criteria.
* @param {Object} list - List of current packages
* @param {Object} meta - Current meta data
* @return {Object} updated meta data
*/
util.getUpdatedMetaData = function(list, meta) {
if (typeof list !== "undefined" && _.keys(list).length <= 0) {
meta.isEmptyList = true;
meta.totalItems = 0;
return meta;
}
// Filter settings.
var searchStr = meta.filterValue;
if (searchStr) {
var searchString = new RegExp(".*" + searchStr + ".*", "i");
list = _.pickBy(list, function(value, key) {
return (searchString.test(value.package) || searchString.test(value.short_description));
});
}
// Pager settings.
var pkgKeys = _.keys(list);
var pgSizes = util.getDefaultPageSizes();
pgSizes = _.filter(pgSizes, function(size) {
return (size <= pkgKeys.length);
});
meta.pageSizes = pgSizes;
var totalItems = pkgKeys.length;
// filter list based on page size and pagination
if (totalItems > _.min(pgSizes)) {
var startIdx = (meta.currentPage - 1) * meta.pageSize;
var endIdx = startIdx + meta.pageSize;
endIdx = endIdx > totalItems ? totalItems : endIdx;
list = _.pick(list, _.slice(pkgKeys, startIdx, endIdx));
// list statistics
meta.start = startIdx + 1;
meta.limit = endIdx;
meta.showPager = true;
} else {
meta.showPager = false;
if (pkgKeys.length === 0) {
meta.start = 0;
} else {
// list statistics
meta.start = 1;
}
meta.limit = pkgKeys.length;
}
meta.totalItems = totalItems;
meta.filterList = list;
meta.isEmptyList = _.keys(meta.filterList).length <= 0;
return meta;
};
/**
* @method getPageShowingText
* Returns a formated string message
* @param {Object} meta - Current meta data
* @return {String}
*/
util.getPageShowingText = function(meta) {
var newString = "";
if (meta && typeof meta.start !== "undefined" && typeof meta.limit !== "undefined" && typeof meta.totalItems !== "undefined" ) {
newString = LOCALE.maketext("[output,strong,Showing] [_1] - [_2] of [_3] items", meta.start, meta.limit, meta.totalItems);
}
return newString;
};
/**
* @method getExtensionsForPHPVersion
* Filters the package list and returns a new list
* with only the packages associated to the PHP version provided
* @param {String} version - PHP version (eg. ea-php71).
* @param {Array} pkgList - Array of package names string
* @return {Array}
*/
util.getExtensionsForPHPVersion = function(version, pkgList) {
var testString = new RegExp(version + ".*", "i");
var list = _.filter(pkgList, function(name) {
return testString.test(name);
});
return list;
};
/**
* @method decideShowHideRecommendations
* Based on the current state of package selection (i.e. on select or unselect),
* this method filters the recommendations and decides if the recommendation is to be shown or hidden.
* @param {Array} recommendations - Array of recommendation Objects
* @param {Array} pkgListToCheck - Package list to check if they match the recommendation criteria.
* @param {Boolean} onSelect
* @return {Array} - Array of filtered recommendation Objects
*/
util.decideShowHideRecommendations = function(recommendations, pkgListToCheck, onSelect, pkg) {
// Currently this recommendation system works strictly for DSO recommendation only. It can be modified to suit other
// recommendations as they come.
_.each(recommendations, function(reco) {
if (_.isUndefined(reco)) {
return;
}
if (typeof onSelect !== "undefined") {
// The display of a recommendation depends on when it should be
// shown (i.e. when installing a package or uninstalling a package)
if (onSelect) {
reco.show = (reco.on === "add");
} else {
reco.show = (reco.on === "remove");
}
} else {
reco.show = false;
}
if (reco.show) {
var pkgDisplayName = pkg;
// Show readable PHP version name if the pkg is PHP version.
if (util.phpVerRegex.test(pkg)) {
pkgDisplayName = util._getReadableName(pkg, util.eaRegex);
pkgDisplayName = util.versionToString(pkgDisplayName).replace("Php", "PHP");
}
var localizedName = LOCALE.makevar(reco.name); // The value of this variable is pulled in for translation via an ea4_recommendations TPDS
reco.displayName = LOCALE.maketext("Recommendations for “[_1]”: [_2]", pkgDisplayName, localizedName);
reco.desc = LOCALE.makevar(reco.desc); // The value of this variable is pulled in for translation via an ea4_recommendations TPDS
reco.showFootnote = false;
// 'showReco' flag is strictly used for the DSO recommendation.
var showReco = true;
_.each(reco.options, function(option) {
if (!_.isNil(option.recommended)) {
option.title = (option.recommended) ? LOCALE.maketext("Recommended") : LOCALE.maketext("Not Recommended");
}
option.text = LOCALE.makevar(option.text); // The value of this variable is pulled in for translation via an ea4_recommendations TPDS
option.show = true;
if (!_.isEmpty(option.items)) {
showReco = option.show = _.isEmpty(_.intersection(option.items, pkgListToCheck));
// If none of the options that include package recos need not be shown
// then we do not need to show the footnote.
if (!reco.showFootnote) {
reco.showFootnote = option.show;
}
} else {
// In DSO recommendation, we are hiding the second option
// as well when the first option
option.show = showReco;
}
});
reco.show = !_.every(reco.options, ["show", false]);
}
});
return recommendations;
};
/**
* @method getExtensionsOfSelectedPHPVersions
* @param {Object} pkgInfoList
* @param {Object} currPkgInfoList
* @param {Array} selectedPkgs - Array of packages names
* @return {Object}
*/
util.getExtensionsOfSelectedPHPVersions = function(pkgInfoList, currPkgInfoList, selectedPkgs) {
var extToConsider = [];
var noPHPSelected = false;
var allPhpVersions = _.filter(_.keys(pkgInfoList), function(pkg) {
return util.phpVerRegex.test(pkg);
}).sort();
var versionsToConsider = _.filter(selectedPkgs, function(pkg) {
return (util.phpVerRegex.test(pkg));
}).sort();
// Determine if all versions are selected or not. If selected,
// we do not need to extract a subset, we can just show all extensions.
if (!_.isEqual(allPhpVersions, versionsToConsider)) {
if (versionsToConsider.length > 0) {
// Extract only the extension packages of the versions to consider.
var workingExt = _.keys(currPkgInfoList);
_.each(versionsToConsider, function(ver) {
var testString = new RegExp(ver + ".*", "i");
var verExt = _.remove(workingExt, function(ext) {
return testString.test(ext);
});
extToConsider = _.concat(extToConsider, verExt);
});
} else {
noPHPSelected = true;
}
} else {
versionsToConsider = allPhpVersions;
extToConsider = _.keys(currPkgInfoList);
}
return { versions: versionsToConsider, extensions: extToConsider, noPHPSelected: noPHPSelected };
};
/**
* Following validations are done for filename:
* - It is invalid if the input is just . or ..
* - It is invalid if the filename contains / or NUL byte.
* - It is valid for all other cases.
*
* @method validateFilename
*/
util.validateFilename = function(filename) {
var valData = { valid: true, valMsg: "" };
if (/^\.{1,2}$/.test(filename)) {
valData.valid = false;
valData.valMsg = LOCALE.maketext("Filename [output,strong,cannot] be “[output,strong,_1]”.", filename);
} else if (/\/|\0/.test(filename)) {
valData.valid = false;
valData.valMsg = LOCALE.maketext("Filename [output,strong,cannot] include the following characters: [list_and,_1]", ["/", "NUL"]);
}
return valData;
};
util.setupVhostWarning = function(pkgInfo, vhostsCount) {
// Show them as a warning.
pkgInfo.vhostWarning.exist = true;
var localizedText = LOCALE.maketext(
"[quant,_1,virtual host currently uses,virtual hosts currently use] this version of PHP. If you remove this PHP version, your virtual hosts may not work properly.",
vhostsCount);
// This hack is to apply the right class to the number. This
// is to overcome the limitation of locale system to take multiple output types for a string.
localizedText = localizedText.replace(/^(\d+)/, "<span class='vhost-emphasis'>$1</span>");
pkgInfo.vhostWarning.text = localizedText;
return pkgInfo;
};
util.resetVhostWarning = function(pkgInfo) {
pkgInfo.vhostWarning = angular.copy(util.vhostWarning);
};
/**
* Creates tags based on the packages list provided.
*
* NOTE: [ Needs Improvement ]
* Until there is a better way to identify the important packages in a
* "Currently Installed Packages", we are going to use regex to extract
* Apache, and all PHP versions installed and add them as tags.
*
* @method createTagsForActiveProfile
* @param {Array} packages A list of packages.
*/
util.createTagsForActiveProfile = function(packages) {
var tags = [];
var skipRuby = false;
var foundPhpVersions = new Set();
// Create static regexes for tag creation that won't be affected by prefix updates
var additionalPrefixes = util.getCachedPrefixes() || [];
var allPrefixes = ["ea"].concat(additionalPrefixes);
var phpTagRegex = new RegExp("^(" + allPrefixes.join("|") + ")-php(\\d{2})$", "i");
var apacheTagRegex = new RegExp("^(" + allPrefixes.join("|") + ")-apache(\\d{2})$", "i");
var rubyTagRegex = new RegExp("^(" + allPrefixes.join("|") + ")-ruby(\\d{2})", "i");
_.each(packages, function(pkg) {
var newTag;
var apacheMatch = pkg.match(apacheTagRegex);
if (apacheMatch) {
var apacheVersion = apacheMatch[2];
newTag = "Apache " + apacheVersion.replace(/(\d)(\d)$/, "$1.$2");
tags = _.concat(tags, newTag);
} else {
var phpMatch = pkg.match(phpTagRegex);
if (phpMatch) {
var phpVersion = phpMatch[2];
var formattedVersion = "PHP " + phpVersion.replace(/(\d)(\d)$/, "$1.$2");
if (!foundPhpVersions.has(formattedVersion)) {
foundPhpVersions.add(formattedVersion);
tags = _.concat(tags, formattedVersion);
}
}
var rubyMatch = pkg.match(rubyTagRegex);
if (rubyMatch && !skipRuby) {
var rubyVersion = rubyMatch[2];
newTag = "Ruby " + rubyVersion.replace(/(\d)(\d)$/, "$1.$2");
tags = _.concat(tags, newTag);
skipRuby = true;
}
}
});
return tags;
};
/**
* @method checkMPMRequirement
* Checks if the user has any MPM packages (at least one needs to be installed).
* Updates MPM callout feedback.
* @param {Object} pkgData
* @param {Object} resolvedData
* @param {Array} selectedPkgs
* @param {Boolean} whenSelected
* @return {Object}
*/
util.checkMPMRequirement = function(pkgData, resolvedData, selectedPkgs, whenSelected) {
var mpmRegex = /mod[-_]mpm/;
var mpmInRemoveList = _.filter(_.uniq(resolvedData.removeList), function(pkg) {
return mpmRegex.test(pkg);
});
if (!whenSelected && mpmRegex.test(pkgData.package)) {
mpmInRemoveList = _.union(mpmInRemoveList, [pkgData.package]);
}
var subList = _.difference(selectedPkgs, mpmInRemoveList);
var mpmIndexInRemove = _.findIndex(subList, function(pkg) {
return mpmRegex.test(pkg);
});
var mpmIndexInAdd = _.findIndex(resolvedData.addList, function(pkg) {
return mpmRegex.test(pkg);
});
if (mpmIndexInRemove === -1 && mpmIndexInAdd === -1) {
// Show MPM callout;
pkgData.mpmMissing = true;
pkgData.mpmMissingMsg = LOCALE.maketext("Your selection removed [list_and,_1].", mpmInRemoveList) + " " + LOCALE.maketext("An [asis,MPM] package must exist on your system. Click “Continue” to select a new [asis,MPM] package.") + " " + LOCALE.maketext("Click “Cancel” to cancel this operation.");
pkgData.actions.actionNeeded = false;
}
return pkgData;
};
util.getCommonlyInstalledExtensions = function(allExtensions, installedPhpVersions) {
// Filter only installed extensions.
var installedExt = _.map(_.filter(allExtensions, ["selectedPackage", true]), "package");
var extGroupByVersions = [];
_.each(installedPhpVersions, function(version, index) {
// Group the extensions by PHP versions and normalize them to
// a common name to make it easy to extract the
// common extensions across all installed versions.
// Example:
// Trying to process the following array:
// [
// "ea-php54-libc-client", "ea-php54-build", "ea-php54-pear",
// "ea-php54-php-bcmath", "ea-php55-libc-client", "ea-php55-build", "ea-php55-pear",
// "ea-php70-libc-client", "ea-php70-build", "ea-php70-pear",
// "ea-php70-php-calendar", "ea-php70-php-curl"
// ]
// To:
// [
// [ "libc-client", "build", "pear", "php-bcmath" ],
// [ "libc-client", "build", "pear" ],
// [ "libc-client", "build", "pear", "php-calendar", "php-curl" ]
// ]
extGroupByVersions[index] = _.chain(installedExt)
.filter(function(ext) {
return _.startsWith(ext, version);
})
.map(function(ext) {
return _.replace(ext, util.phpExtRegex, "");
})
.value();
});
// Extract the common extensions.
// Note: EA packages use "php-bcmath" format while ALT packages use "bcmath" format.
// We need to normalize for comparison but preserve the original names.
var commonExtensions = [];
if (extGroupByVersions.length === 1) {
commonExtensions = extGroupByVersions[0];
} else {
// Helper function to normalize extension name for comparison
// Strips "php-" prefix if present to allow matching between EA and ALT packages
var normalizeForComparison = function(ext) {
return ext.indexOf("php-") === 0 ? ext.substring(4) : ext;
};
// Build a map of normalized names to original names for each group
var normalizedGroups = _.map(extGroupByVersions, function(group) {
var map = {};
_.each(group, function(ext) {
var normalizedName = normalizeForComparison(ext);
// Prefer the original name (with php- prefix if it exists)
if (!map[normalizedName] || ext.indexOf("php-") === 0) {
map[normalizedName] = ext;
}
});
return map;
});
// Find common normalized extension names
var commonNormalized = _.keys(normalizedGroups[0]);
for (var i = 1; i < normalizedGroups.length; i++) {
commonNormalized = _.intersection(commonNormalized, _.keys(normalizedGroups[i]));
}
// Map back to original names, preferring the php- prefixed version for EA compatibility
commonExtensions = _.map(commonNormalized, function(normalizedName) {
// Look through all groups and prefer the php- prefixed version
for (var j = 0; j < normalizedGroups.length; j++) {
var original = normalizedGroups[j][normalizedName];
if (original && original.indexOf("php-") === 0) {
return original;
}
}
// If no php- prefixed version found, return any version
return normalizedGroups[0][normalizedName];
});
}
// Only single PHP version can have DSO package installed at any instance. So
// if a DSO package come up in the commonExtensions, pull it out to eliminate
// having conflict issues while auto selecting common extensions.
// Note: Generally a DSO package is 'ea-php##-php'. Since 'ea-php##-' is stripped above, we are comparing it with just php.
var excludeList = ["php", "scldevel" ];
_.pullAll(commonExtensions, excludeList);
return commonExtensions;
};
/**
* Looks for ea-ruby##-** packages and return true if they do exist.
* @param {Object} pkgList Package information list,
* @return {Boolean} True if at least one ruby package exists. False otherwise.
*/
util.doRubyPkgsExist = function(pkgList) {
return _.some(pkgList, function(pkg) {
return util.rubyVerRegex.test(pkg.package);
});
};
util.updateRegexForPrefixes = function(additionalPrefixes) {
var allPrefixes = ["ea"].concat(additionalPrefixes || []);
var prefixPattern = "(" + allPrefixes.join("|") + ")";
util.eaRegex = new RegExp("^(" + allPrefixes.join("|") + ")-", "i");
util.phpVerRegex = new RegExp("^" + prefixPattern + "-php(\\d{2})$", "i");
util.phpExtRegex = new RegExp("^" + prefixPattern + "-php(\\d{2}-)", "i");
util.apacheVerRegex = new RegExp("^" + prefixPattern + "-apache(\\d{2})$", "i");
util.apacheModulesRegex = new RegExp("^" + prefixPattern + "-apache(\\d{2}-)", "i");
util.rubyVerRegex = new RegExp("^" + prefixPattern + "-ruby(\\d{2})", "i");
};
/**
* Looks for additional packages and return true if they do exist.
* @param {Array} additionalPkgsList Array of additional package objects.
* @param {Object} pkgList Package information list,
* @return {Boolean} True if at least one additional package exists. False otherwise.
*/
util.doAdditionalPkgsExist = function(additionalPkgsList, pkgList) {
return _.some(additionalPkgsList, function(addlPkg) {
return _.includes(_.keys(pkgList), addlPkg);
});
};
/**
* Show Footer container of the wizard.
*/
util.showFooter = function() {
wizardState.showFooter = true;
};
/**
* Hide Footer container of the wizard.
*/
util.hideFooter = function() {
wizardState.showFooter = false;
};
return util;
}]);
}
);
var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n<r.length;n++)e[r][r.charAt(n)]=n}return e[r][o]}var i={compressToBase64:function(r){if(null==r)return"";var n=i._compress(r,6,function(r){return o.charAt(r)});switch(n.length%4){default:case 0:return n;case 1:return n+"===";case 2:return n+"==";case 3:return n+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(n){return t(o,r.charAt(n))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(r){return null==r?"":""==r?null:i._decompress(r.length,16384,function(o){return r.charCodeAt(o)-32})},compressToUint8Array:function(r){for(var o=i.compress(r),n=new Uint8Array(2*o.length),e=0,t=o.length;e<t;e++){var s=o.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e<t;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(r){return null==r?"":i._compress(r,6,function(r){return n.charAt(r)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(o){return t(n,r.charAt(o))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(r,o,n){if(null==r)return"";var e,t,i,s={},u={},a="",p="",c="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<r.length;i+=1)if(a=r.charAt(i),Object.prototype.hasOwnProperty.call(s,a)||(s[a]=f++,u[a]=!0),p=c+a,Object.prototype.hasOwnProperty.call(s,p))c=p;else{if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define('lz-string',[],function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString});
/*
# cpanel - whostmgr/docroot/templates/easyapache4/services/ea4Data.js
# 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
*/
/* global define: false */
define(
'app/services/ea4Data',[
"angular",
"lodash",
// CJT
"cjt/io/api",
"cjt/io/whm-v1-request",
"cjt/io/whm-v1",
// Angular components
"cjt/services/APIService",
// App components
"app/services/ea4Util",
"lz-string",
],
function(angular, _, API, APIREQUEST, WHMv1, APIService, ea4Util, LZString) {
"use strict";
var app = angular.module("whm.easyapache4.ea4Data", []);
app.factory("ea4Data", ["$q", "$location", "ea4Util", "APIService", function($q, $location, ea4Util, APIService) {
var oData = {
isReadyForProvision: false,
};
oData.updateRegexesForPrefixes = function(prefixes) {
var allPrefixes = ["ea"].concat(prefixes || []);
var prefixPattern = "(" + allPrefixes.join("|") + ")";
oData.mpmRegex = new RegExp(prefixPattern + "-apache\\d{2}-mod[_-]mpm.*", "i");
oData.modulesRegex = new RegExp(prefixPattern + "-apache\\d{2}-mod.*", "i");
oData.phpRegex = new RegExp("^" + prefixPattern + "-php\\d{2}$", "i");
oData.phpExtRegex = new RegExp(prefixPattern + "-php\\d{2}-.*", "i");
oData.rubyRegex = new RegExp(prefixPattern + "-ruby\\d{2}-.*", "i");
};
// Initialize with default 'ea' only regexes
oData.updateRegexesForPrefixes([]);
/**
* Sets or gets the provision value
*
* @method provisionReady
* @param {Boolean} value
* @return {Boolean}
*/
oData.provisionReady = function(value) {
if (typeof value === "boolean") {
oData.isReadyForProvision = value;
} else {
return oData.isReadyForProvision;
}
};
/**
* @method getProfiles
* @return {Promise}
*/
oData.getProfiles = function() {
var deferred = $q.defer();
var apiCall = new APIREQUEST.Class();
apiCall.initialize("", "ea4_list_profiles");
API.promise(apiCall.getRunArguments())
.done(function(response) {
response = response.parsedResponse;
if (response.status) {
// Keep the promise
// CJT2 whm-v1.js has parsedResponse wrapper
// which is returning wrong data when only 1 key exists
// in this response's data.
// i.e. when only 'cpanel' key exists in the response,
// whm-v1.js -> _reduce_list_data is removing it and returning
// and returning data differently.
// That is reason 'response.raw.data' is being used here
// to have a consistent return always.
// the above whm-v1.js method needs to be fixed.
deferred.resolve(response.raw.data);
} else {
// Pass the error along
deferred.reject(response.error);
}
});
return deferred.promise;
};
/**
* @method getVhostsByPhpVersion
* @param {String} - php version
* @return {Promise}
*/
oData.getVhostsByPhpVersion = function(version) {
var deferred = $q.defer();
var apiCall = new APIREQUEST.Class();
apiCall.initialize("", "php_get_vhosts_by_version");
apiCall.addArgument("version", version);
API.promise(apiCall.getRunArguments())
.done(function(response) {
response = response.parsedResponse;
if (response.status) {
// Keep the promise
deferred.resolve(response.data);
} else {
// Pass the error along
deferred.reject(response.error);
}
});
return deferred.promise;
};
/**
* TODO: this method needs to handle API failure
*
* @method ea4GetCurrentPkgList
* @return {Promise}
*/
oData.ea4GetCurrentPkgList = function() {
var apiCall = new APIREQUEST.Class();
var apiService = new APIService();
apiCall.initialize("", "ea4_get_currently_installed_packages");
var deferred = apiService.deferred(apiCall);
return deferred.promise;
};
/**
* Returns ea4 meta info from the ea4_metainfo.json
* It currently contains information about additional pacakges,
* default PHP handler & default PHP package used.
*
* @method getEA4MetaInfo
* @return {Promise}
*/
oData.getEA4MetaInfo = function() {
var apiCall = new APIREQUEST.Class();
var apiService = new APIService();
apiCall.initialize("", "ea4_metainfo");
var deferred = apiService.deferred(apiCall);
return deferred.promise;
};
/**
* Returns pkgInfoList for additional pacakges.
*
* @method getPkgInfoForAdditionalPackages
* @argument {Object} pkgInfoList
* @return {Object}
*/
oData.getPkgInfoForAdditionalPackages = function(pkgInfoList) {
var list = {};
_.each(ea4Util.additionalPkgList, function(pkg) {
var pkgInfo = pkgInfoList[pkg];
if (typeof pkgInfo !== "undefined") {
list[pkg] = pkgInfo;
}
});
return list;
};
/**
* Returns pkgInfoList filtered by type.
*
* @method getPkgInfoSubset
* @argument {String} type
* @argument {Object} pkgInfoList
* @return {Object}
*/
oData.getPkgInfoSubset = function(type, pkgInfoList) {
var regex;
if (type === "additional") {
return oData.getPkgInfoForAdditionalPackages(pkgInfoList);
}
switch (type) {
case "mpm":
regex = oData.mpmRegex;
break;
case "modules":
regex = oData.modulesRegex;
break;
case "php":
regex = oData.phpRegex;
break;
case "extensions":
regex = oData.phpExtRegex;
break;
case "ruby":
regex = oData.rubyRegex;
break;
}
return oData.filterByRegex(regex, pkgInfoList);
};
/**
* @method filterByRegex
* @argument {Regex} regex
* @argument {Object} pkgInfoList
* @return {Promise}
*/
oData.filterByRegex = function(regex, pkgInfoList) {
var list = {};
if (typeof regex === "undefined") {
return pkgInfoList;
}
var filterPkgs =
_.filter(_.keys(pkgInfoList), function(pkg) {
return regex.test(pkg);
});
filterPkgs.sort();
_.each(filterPkgs, function(pkg) {
list[pkg] = pkgInfoList[pkg];
});
return list;
};
/**
* @method resolvePackages
* @argument {Array} packages - Array of string names
* @return {Promise}
*/
oData.resolvePackages = function(packages) {
var deferred = $q.defer();
oData.getAdditionalPkgPrefixes().then(function(additionalPrefixes) {
ea4Util.cachePrefixes(additionalPrefixes);
ea4Util.updateRegexForPrefixes(additionalPrefixes);
var allNamespaces = ["ea"].concat(additionalPrefixes);
if (!packages || packages.length === 0) {
deferred.resolve({
install: [],
uninstall: [],
upgrade: [],
unaffected: [],
});
return;
}
var apiCall = new APIREQUEST.Class();
apiCall.initialize("", "package_manager_resolve_actions");
var nsIndex = 0;
var pkgIndex = 0;
_.each(allNamespaces, function(ns) {
if (nsIndex === 0) {
apiCall.addArgument("ns", ns);
} else {
apiCall.addArgument("ns-" + nsIndex, ns);
}
nsIndex++;
});
_.each(packages, function(pkg) {
if (pkgIndex === 0) {
apiCall.addArgument("package", pkg);
} else {
apiCall.addArgument("package-" + pkgIndex, pkg);
}
pkgIndex++;
});
API.promise(apiCall.getRunArguments())
.done(function(response) {
response = response.parsedResponse;
if (response && response.status && response.raw && response.raw.data) {
deferred.resolve(response.raw.data);
} else {
deferred.resolve({
install: [],
uninstall: [],
upgrade: [],
unaffected: [],
});
}
})
.fail(function(error) {
deferred.reject(error);
});
}, function(error) {
// Fallback to EA only when additional prefixes API fails
ea4Util.cachePrefixes([]);
ea4Util.updateRegexForPrefixes([]);
var eaPackages = _.filter(packages, function(pkg) {
return pkg.startsWith("ea-");
});
if (eaPackages.length === 0) {
deferred.resolve({
install: [],
uninstall: [],
upgrade: [],
unaffected: [],
});
return;
}
var apiCall = new APIREQUEST.Class();
apiCall.initialize("", "package_manager_resolve_actions");
apiCall.addArgument("ns", "ea");
_.each(eaPackages, function(pkg, index) {
if (index === 0) {
apiCall.addArgument("package", pkg);
} else {
apiCall.addArgument("package-" + index, pkg);
}
});
API.promise(apiCall.getRunArguments())
.done(function(response) {
response = response.parsedResponse;
if (response && response.status && response.raw && response.raw.data) {
deferred.resolve(response.raw.data);
} else {
deferred.resolve({
install: [],
uninstall: [],
upgrade: [],
unaffected: [],
});
}
})
.fail(function(error) {
deferred.reject(error);
});
});
return deferred.promise;
};
/**
* @method doProvision
* @argument {Array} installPackages - Array of string names
* @argument {Array} uninstallPackages - Array of string names
* @argument {Array} upgradePackages - Array of string names
* @argument {String} profileID
* @return {Promise}
*/
oData.doProvision = function(installPackages, uninstallPackages, upgradePackages, profileID) {
var deferred = $q.defer();
var apiCall = new APIREQUEST.Class();
apiCall.initialize("", "package_manager_submit_actions");
// apiCall.addArgument("profileID", profileID);
// Prepare the package list
_.each(installPackages, function(pkg, index) {
apiCall.addArgument("install-" + index, pkg );
});
_.each(uninstallPackages, function(pkg, index) {
apiCall.addArgument("uninstall-" + index, pkg );
});
_.each(upgradePackages, function(pkg, index) {
apiCall.addArgument("upgrade-" + index, pkg );
});
API.promise(apiCall.getRunArguments())
.done(function(response) {
response = response.parsedResponse;
deferred.resolve(response.raw.data);
});
return deferred.promise;
};
/**
* @method tailingLog
* @argument {Number} buildID
* @argument {Number} offset
* @return {Promise}
*/
oData.tailingLog = function(buildID, offset) {
var deferred = $q.defer();
var apiCall = new APIREQUEST.Class();
apiCall.initialize("", "package_manager_get_build_log");
// Send the pid
apiCall.addArgument("build", buildID);
apiCall.addArgument("offset", offset);
API.promise(apiCall.getRunArguments())
.done(function(response) {
response = response.parsedResponse;
deferred.resolve(response.raw.data);
});
return deferred.promise;
};
/**
* @method getPkgInfoList
* @return {Promise}
*/
oData.getPkgInfoList = function() {
var deferred = $q.defer();
oData.getAdditionalPkgPrefixes().then(function(additionalPrefixes) {
ea4Util.cachePrefixes(additionalPrefixes);
ea4Util.updateRegexForPrefixes(additionalPrefixes);
var namespaces = ["ea"].concat(additionalPrefixes);
var promises = [];
_.each(namespaces, function(ns) {
var apiCall = new APIREQUEST.Class();
apiCall.initialize("", "package_manager_get_package_info");
apiCall.addArgument("ns", ns);
promises.push(API.promise(apiCall.getRunArguments()));
});
$q.all(promises).then(function(responses) {
var combinedData = {};
_.each(responses, function(response) {
response = response.parsedResponse;
if (response.status && response.raw.data && response.raw.data.packages) {
_.each(response.raw.data.packages, function(pkg) {
combinedData[pkg.package] = pkg;
});
}
});
deferred.resolve(combinedData);
}).catch(function(error) {
deferred.reject(error);
});
}, function(error) {
ea4Util.cachePrefixes([]);
ea4Util.updateRegexForPrefixes([]);
var apiCall = new APIREQUEST.Class();
apiCall.initialize("", "package_manager_get_package_info");
apiCall.addArgument("ns", "ea");
API.promise(apiCall.getRunArguments())
.done(function(response) {
response = response.parsedResponse;
var pkgData = {};
_.each(response.raw.data.packages, function(pkg) {
pkgData[pkg.package] = pkg;
});
deferred.resolve(pkgData);
})
.fail(function(error) {
deferred.reject(error);
});
});
return deferred.promise;
};
/**
* @method cancelOperation
*/
oData.cancelOperation = function() {
oData.clearEA4LocalStorageItems();
$location.path("profile");
};
/**
* @method clearEA4LocalStorageItems
*/
oData.clearEA4LocalStorageItems = function() {
localStorage.removeItem("pkgInfoList");
localStorage.removeItem("selectedProfile");
localStorage.removeItem("selectedPkgs");
localStorage.removeItem("provisionActions");
localStorage.removeItem("customize");
localStorage.removeItem("ea4Recommendations");
localStorage.removeItem("ea4RawPkgList");
localStorage.removeItem("ea4Update");
ea4Util.hideFooter();
};
/**
* Saves data in Local Storage
*
* @method setData
* @argument {Object} dataItems
*/
oData.setData = function(dataItems) {
_.each(_.keys(dataItems), function(item) {
try {
localStorage.setItem(
item,
LZString.compress(
JSON.stringify(dataItems[item])
)
);
} catch (e) {
console.error("Error storing localStorage item " + item + ": ", e);
}
});
};
/**
* Gets data from Local Storage
* Handles both compressed and uncompressed data for backward compatibility
*
* @method getData
* @argument {String} item
*/
oData.getData = function(item) {
try {
var rawData = localStorage.getItem(item);
if (!rawData) {
return null;
}
var decompressedData = LZString.decompress(rawData);
if (decompressedData) {
return JSON.parse(decompressedData);
} else {
// Decompression failed, assume it's uncompressed data (legacy format)
return JSON.parse(rawData);
}
} catch (e) {
return null;
}
};
/**
* Adds new properties to the packages object
*
* @method initPkgUIProps
* @argument {Object} pkgData
* @return {Object}
*/
oData.initPkgUIProps = function(pkgData) {
pkgData["actions"] = ea4Util.pkgActions;
pkgData["recommendations"] = [];
pkgData["multiRequirements"] = ea4Util.multiRequirements;
pkgData["vhostWarning"] = ea4Util.vhostWarning;
pkgData["mpmMissing"] = false;
pkgData["mpmMissingMsg"] = "";
pkgData["autoSelectExt"] = [];
// TODO:: Add more as you come across. It's easy to know and track all the properties
// if they are initialized at one place.
return pkgData;
};
// move to ea4Util
var setPkgSelections = function(selPkgs, pkgInfoList) {
var allSelectedPkgs = ea4Util.gatherAllRequirementsOfPkgs(selPkgs, pkgInfoList);
// Select all packages that are installed.
_.each(allSelectedPkgs, function(pkg) {
if (typeof pkgInfoList[pkg] !== "undefined") {
pkgInfoList[pkg].selectedPackage = true;
}
});
return pkgInfoList;
};
/**
* @method buildPkgInfoList
* @argument {Array} selPkgs - Array of Strings
* @argument {Array} packagesInfo - Array of Objects
* @argument {Object} ea4Recommendations - The key is the pkg name, the value an Array of recommendations
* @return {Object}
*/
oData.buildPkgInfoList = function(selPkgs, packagesInfo, ea4Recommendations) {
// Set all packages info.
var pkgInfoList = {};
var additionalPrefixes = ea4Util.getCachedPrefixes() || [];
var allPrefixes = ["ea"].concat(additionalPrefixes);
// Ignore any debug packages
packagesInfo = _.filter(packagesInfo, function(pkg) {
return (!/.*\-debuginfo/.test(pkg.package));
});
_.each(packagesInfo, function(pkg) {
var pkgName = pkg.package;
// Start adding UI specific attributes to each package data.
pkg = oData.initPkgUIProps(pkg);
if (!_.isEmpty(ea4Recommendations[pkgName])) {
pkg.recommendations = ea4Recommendations[pkgName];
}
// Add prefix information
var pkgPrefix = "ea";
var match = _.find(allPrefixes, function(prefix) {
return pkgName.startsWith(prefix + "-");
});
if (match) {
pkgPrefix = match;
}
pkg.prefix = pkgPrefix !== "ea" ? pkgPrefix : null;
pkg.selectedPackage = false;
// displayName: Clean name for display (used in customize wizard with labels)
pkg.displayName = ea4Util.getFormattedPackageName(pkgName);
// filterName: Full name for filtering (keeps prefixes for non-EA packages)
if (pkgPrefix === "ea") {
pkg.filterName = ea4Util.getFormattedPackageName(pkgName);
} else {
pkg.filterName = pkgName;
}
// reviewDisplayName: For review page, keep full names for non-EA packages
if (pkgPrefix === "ea") {
// For EA packages, use the stripped name (ea-php81 -> php81)
pkg.reviewDisplayName = ea4Util.getFormattedPackageName(pkgName);
} else {
// For non-EA packages, use the full package name (alt-php56 stays alt-php56)
pkg.reviewDisplayName = pkgName;
}
pkgInfoList[pkgName] = pkg;
});
pkgInfoList = setPkgSelections(selPkgs, pkgInfoList);
return pkgInfoList;
};
/**
* @method fixYumCache
* @return {Promise}
*/
oData.fixYumCache = function() {
var apiCall = new APIREQUEST.Class();
var apiService = new APIService();
apiCall.addArgument("ns", "ea");
apiCall.initialize("", "package_manager_fixcache");
var deferred = apiService.deferred(apiCall);
return deferred.promise;
};
/**
* @method saveAsNewProfile
* @argument {Object} content
* @argument {String} filename
* @argument {Number} overwrite
* @return {Promise}
*/
oData.saveAsNewProfile = function(content, filename, overwrite) {
var deferred = $q.defer();
var apiCall = new APIREQUEST.Class();
apiCall.initialize("", "ea4_save_profile");
apiCall.addArgument("filename", filename);
apiCall.addArgument("name", content.name);
apiCall.addArgument("overwrite", overwrite);
apiCall.addArgument("desc", content.desc || "");
apiCall.addArgument("version", content.version || "");
_.each(content.pkgs, function(pkg, index) {
apiCall.addArgument("pkg-" + index, pkg );
});
_.each(content.tags, function(tag, index) {
apiCall.addArgument("tag-" + index, tag );
});
API.promise(apiCall.getRunArguments())
.done(function(response) {
response = response.parsedResponse;
if (response.status) {
deferred.resolve(response.data);
} else {
deferred.reject(response);
}
});
return deferred.promise;
};
/**
* @method getEA4Recommendations
* @return {Promise}
*/
oData.getEA4Recommendations = function() {
var apiCall = new APIREQUEST.Class();
var apiService = new APIService();
apiCall.initialize("", "ea4_recommendations");
var deferred = apiService.deferred(apiCall);
return deferred.promise;
};
/**
* @method getUploadContentFromUrl
* @argument {Object} uploadUrl
* @return {Promise}
*/
oData.getUploadContentFromUrl = function(uploadUrl) {
var deferred = $q.defer();
var apiCall = new APIREQUEST.Class();
apiCall.initialize("", "cors_proxy_get");
apiCall.addArgument("url", uploadUrl);
API.promise(apiCall.getRunArguments())
.done(function(response) {
response = response.parsedResponse;
if (response.status) {
deferred.resolve(response.data);
} else {
deferred.reject(response.error);
}
});
return deferred.promise;
};
/**
* @method dataIsAvailable
* @return {Boolean}
*/
oData.dataIsAvailable = function() {
if (oData.getData("pkgInfoList")) {
return true;
} else {
return false;
}
};
/**
* @method getPkgInfoByType
* @argument {String} type
* @argument {Object} pkgInfoList
* @return {Object}
*/
oData.getPkgInfoByType = function(type, pkgInfoList) {
var subsetPkgInfoList = [];
if (type) {
subsetPkgInfoList = oData.getPkgInfoSubset(type, pkgInfoList);
}
return subsetPkgInfoList;
};
/**
* Get additional EA4 package prefixes
* @method getAdditionalPkgPrefixes
* @return {Promise}
*/
oData.getAdditionalPkgPrefixes = function() {
var deferred = $q.defer();
var apiCall = new APIREQUEST.Class();
apiCall.initialize("", "ea4_get_additional_pkg_prefixes");
API.promise(apiCall.getRunArguments())
.done(function(response) {
response = response.parsedResponse;
if (response.status) {
deferred.resolve(response.raw.data.additional_pkg_prefixes);
} else {
deferred.reject(response.raw.metadata.reason);
}
});
return deferred.promise;
};
// Expose only required methods.
return {
provisionReady: oData.provisionReady,
getProfiles: oData.getProfiles,
ea4GetCurrentPkgList: oData.ea4GetCurrentPkgList,
cancelOperation: oData.cancelOperation,
getPkgInfoForAdditionalPackages: oData.getPkgInfoForAdditionalPackages,
getAdditionalPkgPrefixes: oData.getAdditionalPkgPrefixes,
getPkgInfoSubset: oData.getPkgInfoSubset,
updateRegexesForPrefixes: oData.updateRegexesForPrefixes,
filterByRegex: oData.filterByRegex,
clearEA4LocalStorageItems: oData.clearEA4LocalStorageItems,
resolvePackages: oData.resolvePackages,
doProvision: oData.doProvision,
tailingLog: oData.tailingLog,
getPkgInfoList: oData.getPkgInfoList,
setData: oData.setData,
getData: oData.getData,
initPkgUIProps: oData.initPkgUIProps,
buildPkgInfoList: oData.buildPkgInfoList,
getVhostsByPhpVersion: oData.getVhostsByPhpVersion,
fixYumCache: oData.fixYumCache,
saveAsNewProfile: oData.saveAsNewProfile,
getEA4Recommendations: oData.getEA4Recommendations,
getUploadContentFromUrl: oData.getUploadContentFromUrl,
dataIsAvailable: oData.dataIsAvailable,
getPkgInfoByType: oData.getPkgInfoByType,
getEA4MetaInfo: oData.getEA4MetaInfo,
};
}]);
}
);
/*
# cpanel - whostmgr/docroot/templates/easyapache4/directives/eaWizard.js
# 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/directives/eaWizard',[
"angular",
"cjt/core",
"lodash",
"cjt/util/locale",
"cjt/filters/qaSafeIDFilter",
"app/services/ea4Data",
"app/services/ea4Util",
],
function(angular, CJT, _, LOCALE) {
"use strict";
// Retrieve the current application
var app = angular.module("App");
app.directive("eaWizard",
[ "$timeout", "ea4Data", "ea4Util", "wizardApi", "pkgResolution",
function($timeout, ea4Data, ea4Util, wizardApi, pkgResolution) {
var TEMPLATE_PATH = "directives/eaWizard.ptt";
var RELATIVE_PATH = "templates/easyapache4/" + TEMPLATE_PATH;
var ddo = {
replace: true,
restrict: "E",
templateUrl: CJT.config.debug ? CJT.buildFullPath(RELATIVE_PATH) : TEMPLATE_PATH,
scope: {
idPrefix: "@",
totalPackageInfoList: "=",
selectedPackages: "=",
metaData: "=",
stepName: "@",
stepIndex: "@",
stepTitle: "@",
stepPath: "@",
stepNext: "@",
onToggleFn: "&",
showSearch: "@",
showPagination: "@",
},
link: function postLink(scope, element, attrs) {
var continueResolvingDeps = function(thisPackage) {
thisPackage.multiRequirements.exist = false;
var chosenPkgName = thisPackage.multiRequirements.chosenPackage;
var data = pkgResolution.continueResolvingDependencies(thisPackage, scope.totalPackageInfoList[chosenPkgName], chosenPkgName, scope.totalPackageInfoList, scope.selectedPackages);
// Check if orListStructure exists. If yes - Do setup multireq view.
if (data.orListExist) {
thisPackage.multiRequirements = pkgResolution.setupMultiRequirementForUserInput(scope.totalPackageInfoList);
// return false since resolving dependencies is not yet complete.
return false;
} else if (data.actionNeeded) {
// If not orlist, check if action is needed. If yes - setup conflict/resolution alert view.
thisPackage = pkgResolution.setupConDepCallout(thisPackage, scope.totalPackageInfoList);
// return false since resolving dependencies is not yet complete.
return false;
} else { // If not orlist OR action needed, call apply dependency.
// Since no action is needed it is simply assumed that there are no conflicts
// and all dependencies can be added without any harm.
return scope.applyDependency(thisPackage);
}
};
/**
* Get required PHP extensions for a currently selected PHP version.
*
* @method getAllRequiredPhpExtensions
* @param {string} pkgName - PHP version package name
* @return {Array} reqPkgs - A list of required extensions.
*/
var getAllRequiredPhpExtensions = function(pkgName) {
var reqPkgs = [];
var allDeps = pkgResolution.getAllDepsRecursively(true, scope.totalPackageInfoList[pkgName], pkgName, scope.totalPackageInfoList, scope.selectedPackages);
if (allDeps) {
reqPkgs = ea4Util.getExtensionsForPHPVersion(pkgName, allDeps.requiredPkgs);
}
return reqPkgs;
};
/**
* Get common PHP extensions that can be auto selected for a currently selected PHP version.
*
* @method getAutoSelectPhpExtensions
* @param {string} package - PHP version package name
* @return {Array} autoSelectExtList - A list of auto select extensions.
*/
var getAutoSelectPhpExtensions = function(thisPackage) {
var autoSelectExtList = [];
var extList = scope.php.commonExtensions;
// Determine if this is a non-EA PHP package (uses different naming convention)
// EA packages: ea-php81-php-bcmath (with "php-" prefix on extension)
// Non-EA packages (alt, etc.): alt-php85-bcmath (no "php-" prefix on extension)
var isNonEaPackage = !(/^ea-/i.test(thisPackage));
extList = _.map(extList, function(ext) {
var candidateName = thisPackage + "-" + ext;
// If the candidate exists in the package list, use it
if (scope.totalPackageInfoList[candidateName]) {
return candidateName;
}
// For non-EA packages, try without the "php-" prefix
// (e.g., "php-bcmath" -> "bcmath")
if (isNonEaPackage && ext.indexOf("php-") === 0) {
var nonEaCandidateName = thisPackage + "-" + ext.substring(4);
if (scope.totalPackageInfoList[nonEaCandidateName]) {
return nonEaCandidateName;
}
}
// For EA packages, try with the "php-" prefix if not present
// (in case commonExtensions came from non-EA packages)
if (!isNonEaPackage && ext.indexOf("php-") !== 0) {
var eaCandidateName = thisPackage + "-php-" + ext;
if (scope.totalPackageInfoList[eaCandidateName]) {
return eaCandidateName;
}
}
// Return original candidate (will be filtered out later if not found)
return candidateName;
});
var requiredExtensions = getAllRequiredPhpExtensions(thisPackage);
// Check if the common extensions are actually available for the newly selected PHP version.
// only returns the extensions that are NOT required.
extList = _.filter(extList, function(ext) {
var extension = scope.totalPackageInfoList[ext];
var notRequired = false;
if (typeof extension !== "undefined" && !_.includes(requiredExtensions, ext)) {
notRequired = true;
}
return notRequired;
});
// Load the common extensions into the view model.
autoSelectExtList = _.map(extList, function(ext) {
var displayName = ea4Util.getFormattedPackageName(ext);
return {
package: ext,
displayName: displayName,
isSelected: true,
};
});
return autoSelectExtList;
};
var formatPhpVersionForDisplay = function(ver) {
var phpMatch = ver.match(ea4Util.phpVerRegex);
var formatted = "";
if (phpMatch) {
var phpVersion = phpMatch[1];
formatted = "PHP " + phpVersion.replace(/(\d)(\d)$/, "$1.$2");
}
return formatted;
};
/**
* Updates PHP package accordingly when selected/unselected. Details below:
* For PHP just calling eaWizard.checkDependency isn't enough when
* a PHP version package is unselected. There are some PHP specific stuff
* that are needed to be done.
* 1. Find if there are any vhosts that are using the PHP version being uninstalled
* and warn the user about that.
* 2. If all dependencies are resolved and everything is ok, then just remove all
* extensions of the PHP version being uninstalled as well.
* This method takes care of that.
*
* @method updatePHP
* @param {object} thisPackage The package info object of PHP version.
*/
var updatePHP = function(thisPackage) {
// The state of the selectedPackage is not toggled by default
// by the toggle-switch directive. That is for now explicitly
// done by common function eaWizard.checkDependency called by all EA4 templates.
// Since in this template we need to do some vhost checks even before calling that
// function the state is toggled temporarily. This is a nasty hack but will be there
// until toggle-switch directive is fixed to toggle the ng-model's state correctly
// before firing on-toggle event.
var selectedState = !thisPackage.selectedPackage;
var version = thisPackage.package;
if (!selectedState) {
// Find vhosts if any that are using this PHP version.
var vhostsCount = 0;
ea4Data.getVhostsByPhpVersion(version).then(function(data) {
if (typeof data !== "undefined") {
vhostsCount = data.length;
}
if (vhostsCount > 0) {
thisPackage = ea4Util.setupVhostWarning(thisPackage, vhostsCount);
return;
}
var isComplete = scope.checkDependency(thisPackage);
if (isComplete) {
scope.php.updatePHPExtensionList(thisPackage);
}
}, function(error) {
// Do nothing.
});
thisPackage.autoSelectExt = angular.copy(ea4Util.autoSelectExt);
} else {
scope.checkDependency(thisPackage);
if (thisPackage.selectedPackage) {
thisPackage.autoSelectExt = angular.copy(ea4Util.autoSelectExt);
thisPackage.autoSelectExt.list = getAutoSelectPhpExtensions(thisPackage.package);
var extCount = thisPackage.autoSelectExt.list.length;
thisPackage.autoSelectExt.text = LOCALE.maketext("In addition to the dependencies that this version of [asis,PHP] requires, the system detected [quant,_1,extension,extensions] of all other installed PHP versions that it will install for this version.", extCount);
var displayVersion = formatPhpVersionForDisplay(version);
thisPackage.autoSelectExt.okText = LOCALE.maketext("[_1][comment,package name] and Extensions[comment,action text]", displayVersion);
thisPackage.autoSelectExt.cancelText = LOCALE.maketext("[_1][comment,package name] Only[comment,action text]", displayVersion);
thisPackage.autoSelectExt.show = (extCount > 0 && !thisPackage.actions.actionNeeded) ? true : false;
}
}
};
/**
* Final part of updating a PHP version select/unselect action.
*
* @method finishUpdatePHP
* @param {object} thisPackage The package info object of PHP version.
*/
var finishUpdatePHP = function(thisPackage) {
// When uninstall make sure to remove all corresponding extensions.
if (!thisPackage.selectedPackage) {
var isComplete = scope.applyDependency(thisPackage);
if (isComplete) {
scope.php.updatePHPExtensionList(thisPackage);
}
} else {
scope.applyDependency(thisPackage);
scope.php.autoSelectExtList = getAutoSelectPhpExtensions(thisPackage.package);
thisPackage.autoSelectExt.show = (thisPackage.autoSelectExt.list.length > 0 && !thisPackage.actions.actionNeeded) ? true : false;
}
};
/**
* Continues to resolve dependencies after the user decides to continue.
*
* @method continueResolvingDeps
* @param {object} thisPackage The package info object of PHP version.
*/
var continueResolvingDepsForPhp = function(thisPackage) {
// When uninstall make sure to remove all corresponding extensions.
if (!thisPackage.selectedPackage) {
var isComplete = continueResolvingDeps(thisPackage);
if (isComplete) {
scope.php.updatePHPExtensionList(thisPackage);
}
} else {
continueResolvingDeps(thisPackage);
}
};
/** ***************
* SCOPE STUFF
*****************/
scope.php = {
commonExtensions: [],
/**
* Updates PHP Extension list by removing all extensions of the PHP version
* that is being uninstalled.
*
* @method updatePHPExtensionList
* @param {object} thisPackage The package object of PHP version being uninstalled.
*/
updatePHPExtensionList: function(thisPackage) {
if (!thisPackage.selectedPackage) {
var list = ea4Util.getExtensionsForPHPVersion(thisPackage.package, scope.selectedPackages);
_.each(list, function(pkg) {
if (typeof scope.totalPackageInfoList[pkg] !== "undefined") {
scope.totalPackageInfoList[pkg].selectedPackage = false;
}
_.pull(scope.selectedPackages, pkg);
});
}
},
/**
* Continues to check for dependencies after the user acknowledges the vhost warning and
* continues to uninstall a PHP version.
*
* @method continueCheckDependency
* @param {object} thisPackage The package info object of PHP version.
*/
continueCheckDependency: function(thisPackage) {
// If you come to this point, the vhost warning is already acknowledged
// by now and it is safe to assume that necessary steps are taken.
ea4Util.resetVhostWarning(thisPackage);
var isComplete = scope.checkDependency(thisPackage);
if (!thisPackage.selectedPackage && isComplete) {
scope.php.updatePHPExtensionList(thisPackage);
}
},
/**
* Reset the vhost warning object when select/unselect of a PHP version is canceled
*
* @method resetVhostWarning
* @param {object} thisPackage The package info object of PHP version.
*/
resetVhostWarning: function(thisPackage) {
return ea4Util.resetVhostWarning(thisPackage);
},
};
scope.extensions = {
_extToConsider: [],
_extensionList: {},
noPHPSelected: false,
filterPHPExtensions: function() {
var phpVersions = scope.phpVersions;
var showList = [];
_.each(phpVersions, function(ver) {
if (ver.selected) {
var testString = new RegExp(ver.version + ".*", "i");
var list = _.filter(scope.extensions._extToConsider, function(name) {
return testString.test(name);
});
showList = _.union(showList, list);
}
});
scope.currPkgInfoList = _.pick(scope.extensions._extensionList, showList);
scope.applyMetaData();
},
showPhpFilterTags: function() {
return (scope.stepName === "extensions" && !_.isEmpty(scope.extensions._extensionList));
},
};
scope.toggleLabel = function(thisPackage) {
return ea4Util.getPackageLabel(thisPackage.selectedPackage, thisPackage.state);
};
scope.packageClass = function(thisPackage) {
return ea4Util.getPackageClass(thisPackage.selectedPackage, thisPackage.state);
};
scope.applyDependency = function(thisPackage) {
var pkgName = thisPackage.package;
var selectedPackages = scope.selectedPackages;
var resData = pkgResolution.getResolvedData();
// At least 1 MPM package should be in the selected list
// Check for it everytime an MPM package is removed/uninstalled.
// Show a callout if there is no MPM in selected pkgs.
thisPackage.mpmMissing = false;
thisPackage.mpmMissingMsg = "";
if (thisPackage.actions.actionNeeded) {
thisPackage = ea4Util.checkMPMRequirement(thisPackage, resData, selectedPackages, thisPackage.selectedPackage);
if (thisPackage.mpmMissing) {
return false;
}
}
// Apply the actions
// 1. removeList must be pulled out of selected package list.
_.each(resData.removeList, function(conflict) {
_.pull(selectedPackages, conflict);
if ( typeof scope.totalPackageInfoList[conflict] !== "undefined" ) {
scope.totalPackageInfoList[conflict].selectedPackage = false;
}
});
// 2. addList must be added into selected package list.
_.each(resData.addList, function(dep) {
selectedPackages = _.concat(selectedPackages, dep);
if ( typeof scope.totalPackageInfoList[dep] !== "undefined" ) {
scope.totalPackageInfoList[dep].selectedPackage = true;
}
});
if (!thisPackage.selectedPackage) {
_.pull(selectedPackages, pkgName);
if ( typeof scope.totalPackageInfoList[pkgName] !== "undefined" ) {
scope.totalPackageInfoList[pkgName].selectedPackage = false;
}
}
// Finally set the current profile's pkgs.
scope.selectedPackages = selectedPackages;
thisPackage = pkgResolution.resetPkgResolveActions(thisPackage);
ea4Data.setData( { "pkgInfoList": scope.totalPackageInfoList } );
// Return true if everything is complete.
return true;
};
scope.checkDependency = function(thisPackage) {
var selected = false;
var pkgName = thisPackage.package;
var selectedPackages = scope.selectedPackages;
// REFACTOR: This needs to be re arranged (please see the comment in ea4Data.resetcommonvariables method)
thisPackage.actions = { removeList: [], addList: [], actionNeeded: false };
// Toggle the status of the package selection.
thisPackage.selectedPackage = scope.totalPackageInfoList[pkgName].selectedPackage = selected = !thisPackage.selectedPackage;
thisPackage.recommendations = ea4Util.decideShowHideRecommendations(thisPackage.recommendations, scope.selectedPackages, selected, pkgName);
thisPackage.showRecommendations = !_.every(thisPackage.recommendations, [ "show", false ]);
if (selected) {
var data = pkgResolution.resolveDependenciesWhenSelected(thisPackage, selectedPackages, scope.totalPackageInfoList);
// Check if orListStructure exists. If yes - Do setup multireq view.
if (data.orListExist) {
thisPackage.multiRequirements = pkgResolution.setupMultiRequirementForUserInput(scope.totalPackageInfoList);
// return false since resolving dependencies is not yet complete.
return false;
} else if (data.actionNeeded) {
// If not orlist, check if action is needed. If yes - setup conflict/resolution alert view.
thisPackage = pkgResolution.setupConDepCallout(thisPackage, scope.totalPackageInfoList);
// return false since resolving dependencies is not yet complete.
return false;
} else {
// If not orlist OR action needed, call apply dependency.
if (thisPackage.showRecommendations) {
thisPackage.actions.actionNeeded = true;
return false;
} else {
// Since no action is needed it is simply assumed that there are no conflicts
// and all dependencies can be added without any harm.
return scope.applyDependency(thisPackage);
}
}
} else {
pkgResolution.resolveDependenciesWhenUnSelected(thisPackage, selectedPackages, scope.totalPackageInfoList);
if (thisPackage.showRecommendations) {
thisPackage.actions.actionNeeded = true;
return false;
} else {
thisPackage.mpmMissing = false;
thisPackage.mpmMissingMsg = "";
var resData = pkgResolution.getResolvedData();
thisPackage = ea4Util.checkMPMRequirement(thisPackage, resData, selectedPackages, selected);
if (thisPackage.mpmMissing) {
return false;
} else {
return scope.applyDependency(thisPackage);
}
}
}
};
scope.applyMetaData = function() {
scope.metaData = ea4Util.getUpdatedMetaData(scope.currPkgInfoList, scope.metaData);
};
scope.getShowingText = function() {
return ea4Util.getPageShowingText(scope.metaData);
};
scope.showSearchPageSection = function() {
if (scope.stepName === "extensions") {
return (scope.stepName === "extensions" && !scope.extensions.noPHPSelected);
} else {
return true;
}
};
scope.showEmptyMessage = function() {
var show = false;
if (scope.stepName === "extensions") {
show = (!scope.extensions.noPHPSelected && scope.metaData.isEmptyList);
} else {
show = scope.metaData.isEmptyList;
}
return show;
};
scope.showToggleSwitch = function(pkgData) {
if (scope.stepName === "php") {
return (!pkgData.actions.actionNeeded && !pkgData.multiRequirements.exist && !pkgData.mpmMissing && !pkgData.vhostWarning.exist);
} else {
return (!pkgData.actions.actionNeeded && !pkgData.multiRequirements.exist && !pkgData.mpmMissing);
}
};
scope.initializeSelection = function(pkgData) {
if (scope.stepName === "php") {
updatePHP(pkgData);
} else {
scope.checkDependency(pkgData);
}
};
scope.php.resetAutoSelectExtensions = function(pkgData) {
pkgData.autoSelectExt = angular.copy(ea4Util.autoSelectExt);
};
scope.php.performAutoSelect = function(thisPackage) {
var extList = _.filter(thisPackage.autoSelectExt.list, ["isSelected", true]);
_.each(extList, function(oExt) {
var pkgName = oExt.package;
var selected = true;
var selectedPackages = scope.selectedPackages;
var pkgData = scope.totalPackageInfoList[pkgName];
pkgResolution.resetCommonVariables();
// Toggle the status of the package selection.
pkgData.selectedPackage = selected;
var data = pkgResolution.resolveDependenciesWhenSelected(pkgData, selectedPackages, scope.totalPackageInfoList);
// Check if orListStructure exists. If yes - Do setup multireq view.
if (data.orListExist || data.actionNeeded) {
pkgData.selectedPackage = !selected;
// Record conflicted packages
thisPackage.autoSelectExt.errorList.push(pkgName);
} else {
// Since no action is needed it is simply assumed that there are no conflicts
// and all dependencies can be added without any harm.
return scope.applyDependency(pkgData);
}
});
if (thisPackage.autoSelectExt.errorList.length > 0) {
thisPackage.autoSelectExt.showError = true;
} else {
scope.php.resetAutoSelectExtensions(thisPackage);
}
};
scope.continueProcess = function(pkgData) {
if (scope.stepName === "php") {
return continueResolvingDepsForPhp(pkgData);
} else {
return continueResolvingDeps(pkgData);
}
};
scope.finalizeSelection = function(pkgData) {
if (scope.stepName === "php") {
finishUpdatePHP(pkgData);
} else {
scope.applyDependency(pkgData);
}
};
// When user is shown with a compiled list of add/remove packages,
// if the user chose not to proceed with the change, this method ensures nothing
// is changed.
scope.resetSelection = function(thisPackage) {
thisPackage.selectedPackage = !thisPackage.selectedPackage;
thisPackage = pkgResolution.resetPkgResolveActions(thisPackage);
};
scope.proceed = function(step) {
ea4Data.setData(
{
"pkgInfoList": scope.totalPackageInfoList,
"selectedPkgs": scope.selectedPackages,
}
);
wizardApi.next(step);
};
var initWizard = function() {
scope.showSearch = false;
scope.showPagination = false;
var thisStep = wizardApi.getStepByName(scope.stepName);
if (typeof thisStep !== "undefined") {
scope.stepIndex = thisStep.stepIndex || -1;
scope.stepPath = thisStep.path || "";
scope.stepTitle = thisStep.title || "";
scope.idPrefix = thisStep.name || "eaWizard";
scope.stepNext = thisStep.nextStep || "";
}
scope.currPkgInfoList = ea4Data.getPkgInfoByType(scope.stepName, scope.totalPackageInfoList);
scope.metaData = ea4Util.getDefaultMetaData();
scope.applyMetaData(scope.currPkgInfoList, scope.metaData);
/* Set initial focus to the wizard step title. Also reset
focus to the new step title every time user clicks "Next".
This improves usability when navigating by keyboard. */
$timeout(function() {
element.find("#wizard-step-title").focus();
});
if (scope.stepName === "extensions") {
var currentPkgInfoList = scope.currPkgInfoList;
var phpVersionsAndExtensions = ea4Util.getExtensionsOfSelectedPHPVersions(scope.totalPackageInfoList, currentPkgInfoList, scope.selectedPackages);
scope.extensions.noPHPSelected = phpVersionsAndExtensions.noPHPSelected;
scope.phpVersions = _.map(phpVersionsAndExtensions.versions, function(ver) {
if (typeof scope.totalPackageInfoList[ver] !== "undefined") {
return {
version: scope.totalPackageInfoList[ver].package,
name: scope.totalPackageInfoList[ver].filterName || scope.totalPackageInfoList[ver].displayName,
selected: true,
};
}
});
scope.extensions._extToConsider = phpVersionsAndExtensions.extensions;
scope.extensions._extensionList = _.pick(currentPkgInfoList, scope.extensions._extToConsider);
scope.currPkgInfoList = scope.extensions._extensionList;
scope.applyMetaData();
} else if (scope.stepName === "php") {
// Find Common extensions installed on all existing PHP versions.
var allPhpExtensions = ea4Data.getPkgInfoByType("extensions", scope.totalPackageInfoList);
var installedPhpVersions = _.map( _.filter( scope.currPkgInfoList, function(pkgInfo) {
return (pkgInfo.state !== "not_installed");
} ), "package" );
scope.php.commonExtensions = ea4Util.getCommonlyInstalledExtensions(allPhpExtensions, installedPhpVersions);
}
};
initWizard();
},
};
return ddo;
},
]
);
}
);
/*
# templates/easyapache4/directives/saveAsProfile.js Copyright 2022 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: false */
define(
'app/directives/saveAsProfile',[
"angular",
"cjt/util/locale",
"cjt/core",
"lodash",
"cjt/decorators/growlDecorator",
"app/services/ea4Data",
"app/services/ea4Util",
"cjt/directives/validationContainerDirective",
"cjt/directives/validationItemDirective"
],
function(angular, LOCALE, CJT, _) {
// Retrieve the current application
var app = angular.module("App");
app.directive("saveAsProfile",
[ "ea4Data", "ea4Util", "growl", "growlMessages",
function(ea4Data, ea4Util, growl, growlMessages) {
var initContent = {
name: "",
filename: { name: "", valMsg: "" },
tags: [],
description: "",
version: "",
overwrite: false
};
var TEMPLATE_PATH = "directives/saveAsProfile.ptt";
var RELATIVE_PATH = "templates/easyapache4/" + TEMPLATE_PATH;
var ddo = {
replace: true,
restrict: "E",
templateUrl: CJT.config.debug ? CJT.buildFullPath(RELATIVE_PATH) : TEMPLATE_PATH,
scope: {
idPrefix: "@",
packages: "=",
actionHandler: "=",
position: "@",
onCancel: "&",
onSaveSuccess: "&",
onSaveError: "&",
show: "@",
saveButtonText: "@"
},
link: function postLink(scope, element, attrs) {
scope.saveAsData = _.cloneDeep(initContent);
scope.highlightOverwrite = false;
scope.actionHandler = scope.actionHandler || {};
scope.idPrefix = scope.idPrefix || "save";
scope.position = scope.position || "top";
scope.saveButtonText = scope.saveButtonText || LOCALE.maketext("Save");
/**
* Clears the save as profile form
*
* @method clearSaveProfileForm
*/
var clearSaveProfileForm = function() {
// reseting model values
scope.saveAsData = _.cloneDeep(initContent);
if (scope.form && scope.form.$dirty) {
scope.form.txtFilename.$setValidity("invalidFilename", true);
// mark the form pristine
scope.form.$setPristine();
}
if (!_.isUndefined(scope.onCancel)) {
scope.onCancel({ position: scope.position });
}
};
/**
* Save as new profile.
*
* @method saveForm
*/
scope.actionHandler.saveForm = function() {
// Destroy all growls before attempting to submit something.
growlMessages.destroyAllMessages();
// Throw console error when packages are not provided.
if (_.isUndefined(scope.packages)) {
throw "Packages for the profile are not provided. Wherever this directive is used, make sure to fill the packages attribute correctly.";
}
if (scope.form.$valid) {
// upload profile
var overwrite = scope.saveAsData.overwrite ? 1 : 0;
var inputTags = _.split(scope.saveAsData.tagsAsString, /\s*,\s*/);
var filenameWithExt = scope.saveAsData.filename.name + ".json";
var contentJson = {
"name": scope.saveAsData.name,
"desc": scope.saveAsData.desc,
"pkgs": scope.packages,
"tags": _.compact(inputTags)
};
return ea4Data.saveAsNewProfile(contentJson, filenameWithExt, overwrite)
.then(function(data) {
if (typeof data !== "undefined" && !_.isEmpty(data.path)) {
// TODO: Make the profile name to be a link to profiles page in the message.
growl.success(LOCALE.maketext("The system successfully saved the current packages to the “[_1]” profile. It is available in the EasyApache 4 profiles page.", _.escape(scope.saveAsData.name)));
clearSaveProfileForm();
if (!_.isUndefined(scope.onSaveSuccess)) {
scope.onSaveSuccess();
}
}
}, function(response) {
if (typeof response.data !== "undefined" && response.data.already_exists) {
scope.highlightOverwrite = true;
}
if (!_.isUndefined(scope.onSaveSuccess)) {
scope.onSaveError();
}
growl.error(_.escape(response.error));
});
}
};
/**
* Cancel save action.
*
* @method cancel
*/
scope.actionHandler.cancel = function() {
clearSaveProfileForm();
};
/**
* Run filename validation and set the validation
* inputs with the results accordingly.
*
* @method validateFilenameInput
*/
scope.validateFilenameInput = function() {
var valData = ea4Util.validateFilename(scope.saveAsData.filename.name);
scope.saveAsData.filename.valMsg = valData.valMsg;
scope.form.txtFilename.$setValidity("invalidFilename", valData.valid);
};
}
};
return ddo;
}
]
);
}
);
/*
# cpanel - whostmgr/docroot/templates/easyapache4/services/pkgResolution.js
# Copyright 2022 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
*/
define(
'app/services/pkgResolution',[
"angular",
"lodash",
"cjt/io/api",
"cjt/io/whm-v1-request",
"cjt/io/whm-v1",
"cjt/services/APIService",
],
function(angular, _) {
"use strict";
var app = angular.module("whm.easyapache4.pkgResolution", []);
app.factory("pkgResolution", function() {
var oData = {
// Common variables used.
eaRegex: /^ea-/,
allConflicts: [],
allRequires: [],
resolvedRemoveList: [],
requireStructure: {
"safe": [], // This will have array of safe required packages.
"unsafe": {}, // This will have { 'unsafe_req_pkg': [ con_pkgs_for_this_unsafe_req_pkg, … ], … }
},
conflictStructure: {
"safe": [], // This will have array of safe conflict packages.
"unsafe": {}, // This will have { 'unsafe_conflict_pkg': [ req_pkgs_for_this_unsafe_conflict_pkg, … ], … }
},
orListStructure: {
exist: false,
orLists: [],
},
resolvedPackages: {
addList: [],
removeList: [],
actionNeeded: false,
},
};
/**
* Resets all the common variables that are used during the conflict/requirement resolution.
* @method resetCommonVariables
*/
oData.resetCommonVariables = function() {
// Reset the common variables to keep them ready to re-use for next select/unselect actions.
oData.allConflicts = [];
oData.allRequires = [];
oData.resolvedRemoveList = [];
oData.requireStructure = {
"safe": [],
"unsafe": {},
};
oData.conflictStructure = {
"safe": [],
"unsafe": {},
};
oData.orListStructure = {
exist: false,
orLists: [],
};
oData.resolvedPackages = {
addList: [],
removeList: [],
actionNeeded: false,
};
};
/**
* Returns multirequirement data.
*
* @method getOrListStructure
* @returns {object}
*/
oData.getOrListStructure = function() {
return oData.orListStructure;
};
/**
* Updates multirequirement related data.
*
* @method setOrListStructure
* @param {object} data - New orListStructure data.
*/
oData.setOrListStructure = function(data) {
oData.orListStructure = data;
};
/**
* Returns the resolved pacakge data.
*
* @method getResolvedData
* @returns {object}
*/
oData.getResolvedData = function() {
return oData.resolvedPackages;
};
/**
* Resolves the requirements and conflicts by going through all
* nested levels of the current package dependencies.
*
* @method resolveDependenciesWhenSelected
* @param {object} thisPackage - The selected package data.
* @param {array} selectedPackages - Current list of selected packages.
* @param {array} pkgInfoList - A detailed list of all packages data.
* @returns {object} - Returns an object with information on more action needed and multirequirement status.
*/
oData.resolveDependenciesWhenSelected = function(thisPackage, selectedPackages, pkgInfoList) {
var retData = {
orListExist: false,
actionNeeded: false,
};
if (thisPackage) {
// Recurse through all requirements for this package
// and collect the requirements into 'allRequires'
// and the corresponding conflicts into 'allConflicts'
oData.recurseWhenSelected(thisPackage, thisPackage.package, pkgInfoList);
// Add this package too as it isn't added during the recursion.
oData.allRequires.push(thisPackage.package);
oData.orListStructure = oData.updateMultiRequirements(oData.orListStructure, selectedPackages, oData.allRequires, oData.allConflicts, pkgInfoList);
if (oData.orListStructure.exist) {
retData.orListExist = oData.orListStructure.exist;
} else {
var actionNeeded = oData.proceedToResolveRequirementsAndConflicts(thisPackage, selectedPackages, pkgInfoList);
retData.actionNeeded = actionNeeded;
}
}
return retData;
};
/**
* Updates the multi requirement object.
*
* @method updateMultiRequirements
* @param {object} multiReq - The multiRequirment object to be updated.
* @param {array} selectedPackages - Current list of selected packages.
* @param {array} allRequires - A list of all required packages in the current context.
* @param {array} allConflicts - A list of all conflicts in the current context.
* @param {object} pkgInfoList - A list containing package information of all the available packages in EA4 repo.
* @returns {object} - Updated multiRequirement object.
*/
oData.updateMultiRequirements = function(multiReq, selectedPackages, allRequires, allConflicts, pkgInfoList) {
if (multiReq) {
// In multiRequirement data, check to see if the packages in orLists
// are already in selectedPackages or in allRequires. If yes then resolve them.
var orLists = _.clone(multiReq.orLists);
_.each(orLists, function(eachList) {
var removeThisList = false;
var filterList = _.filter(eachList, function(pkg) {
// Consider only EasyApache packages if they are present on the system.
return (oData.eaRegex.test(pkg) && (typeof pkgInfoList[pkg] !== "undefined"));
});
removeThisList = _.isEmpty(filterList);
var alreadyInRequires = [];
if (!removeThisList) {
// See if any package in this OR list instance is among
// selected packages.
alreadyInRequires = _.intersection(filterList, selectedPackages);
// If not, see if any package in this OR list instance is already
// a required package.
if (_.isEmpty(alreadyInRequires)) {
alreadyInRequires = _.intersection(filterList, allRequires);
}
if (!_.isEmpty(alreadyInRequires)) {
if (filterList.length === 1) {
oData.allRequires = _.union(allRequires, filterList);
}
removeThisList = true;
} else {
var findConflicts = _.intersection(filterList, allConflicts);
filterList = _.difference(filterList, findConflicts);
if (filterList.length === 1) {
oData.allRequires = _.union(allRequires, filterList);
removeThisList = true;
}
}
}
// Look if this list can be removed now.
if (removeThisList) {
_.pull(multiReq.orLists, eachList);
} else if (!_.isEqual(eachList, filterList)) {
// In this case replace old list with new one.
_.pull(multiReq.orLists, eachList);
if (!_.isEmpty(filterList)) {
multiReq.orLists = _.concat(multiReq.orLists, [filterList]);
}
}
});
multiReq.exist = multiReq.orLists.length > 0;
}
return multiReq;
};
/**
* Continues to resolve conflict/requirements from where it previously
* left off. (i.e. If a multirequirement existence is detected, then it stops the resolution
* operation and prompts the user to choose one from the multiple requirements. Then after that it continues
* from this function.)
*
* @method continueResolvingDependencies
* @param {object} thisPackage - The selected package data.
* @param {object} chosenPkg - The chosen package data.
* @param {string} chosenPkgName
* @param {array} pkgInfoList - A detailed list of all packages data.
* @param {array} selectedPackages - Current list of selected packages.
* @returns {object} - Returns an object with information on more action needed and multirequirement status.
*/
oData.continueResolvingDependencies = function(thisPackage, chosenPkg, chosenPkgName, pkgInfoList, selectedPackages) {
var retData = {
orListExist: false,
actionNeeded: false,
};
// Recurse through all requirements for this package
// and collect the requirements into 'allRequires'
// and the corresponding conflicts into 'allConflicts'
oData.recurseWhenSelected(chosenPkg, chosenPkgName, pkgInfoList);
// Add this package too as it isn't added during the recursion.
oData.allRequires.push(chosenPkgName);
oData.orListStructure = oData.updateMultiRequirements(oData.orListStructure, selectedPackages, oData.allRequires, oData.allConflicts, pkgInfoList);
if (oData.orListStructure.exist) {
retData.orListExist = oData.orListStructure.exist;
} else {
var actionNeeded = oData.proceedToResolveRequirementsAndConflicts(thisPackage, selectedPackages, pkgInfoList);
retData.actionNeeded = actionNeeded;
}
return retData;
};
/**
* Second part of the conflict/dependency process after all nested levels
* of conflicts and requirements are collected through recursive process.
*
* @method proceedToResolveRequirementsAndConflicts
* @param {object} thisPackage - The selected package data.
* @param {array} selectedPackages - Current list of selected packages.
* @param {array} pkgInfoList - A detailed list of all packages data.
* @returns {Boolean} - Returns a flag if action needed or not.
*/
oData.proceedToResolveRequirementsAndConflicts = function(thisPackage, selectedPackages, pkgInfoList) {
// REFACTOR: thisPackage can be removed since it's not used in this method anywhere.
// var selectedPackages = $scope.selectedProfile.pkgs;
// var pkgInfoList = pkgInfoList;
// Get the required packages that are not selected.
var reqToConsider = _.difference(oData.allRequires, selectedPackages);
if (reqToConsider.length > 0) {
// Constructs requireStructure by
// adding safe and unsafe requirements appropriately.
oData.requireStructure = oData.buildRequireStructure(reqToConsider, selectedPackages, pkgInfoList);
}
var consToConsider = _.intersection(oData.allConflicts, selectedPackages);
if (consToConsider.length > 0) {
// Constructs conflictStructure by
// adding safe and unsafe conflicts appropriately.
oData.conflictStructure = oData.buildConflictStructure(consToConsider, selectedPackages, pkgInfoList);
}
// This step ensures all unsafe require are resolved
// and moved to requireStructure.safe array.
oData.requireStructure = oData.resolveUnsafeRequires(selectedPackages, oData.requireStructure, pkgInfoList);
// This step ensures all unsafe conflicts are resolved
// and moved to conflictStructure.safe array.
oData.conflictStructure = oData.resolveUnsafeConflicts(selectedPackages, oData.conflictStructure, pkgInfoList);
// At this point we have the following data:
// * resolvedRemoveList - list of all conflicts resolved and ready to remove
// * conflictStructure.safe - list of all safe conflicts to remove
// * requireStructure.safe - list of all safe requires to add
oData.resolvedPackages.removeList = _.union(oData.resolvedRemoveList, oData.conflictStructure.safe);
oData.resolvedPackages.addList = oData.requireStructure.safe;
oData.resolvedPackages.actionNeeded = oData.resolvedPackages.removeList.length > 0;
return oData.resolvedPackages.actionNeeded;
};
/**
* Builds a requirement structure which separates out the safe and unsafe requirements.
* Safe requirement: If a particular required package is not conflicting with any installed packages.
* Unsafe requirement: If it conflicts with any installed package.
*
* @method buildRequireStructure
* @param {array} reqToConsider
* @param {array} selectedPackages - Current list of selected packages.
* @param {array} pkgInfoList - A detailed list of all packages data.
* @returns {object} - Returns an object with safe and unsafe requirements.
*/
oData.buildRequireStructure = function(reqToConsider, selectedPackages, pkgInfoList) {
var reqStruct = oData.requireStructure;
if (pkgInfoList) {
_.each(reqToConsider, function(req) {
var reqIsSafe = true;
_.each(selectedPackages, function(pkg) {
var pkgDetails = pkgInfoList[pkg];
if (typeof pkgDetails !== "undefined" &&
_.includes(pkgDetails.pkg_dep.conflicts, req)) {
// At this point this 'req' is unsafe since it conflicts with an existing package.
// initialize an array (if it's not already) to store this unsafe req's conflicts.
if (typeof reqStruct.unsafe[req] === "undefined") {
reqStruct.unsafe[req] = [];
}
reqStruct.unsafe[req].push(pkg);
reqIsSafe = false;
}
});
if (reqIsSafe) {
reqStruct.safe.push(req);
}
});
}
return reqStruct;
};
/**
* Builds a conflict structure which separates out the safe and unsafe conflicts.
* Safe conflict: If a particular conflicting package is a not a requirement for any installed packages.
* Unsafe conflict: If it is a requirement for any installed packages.
*
* @method buildConflictStructure
* @param {array} consToConsider
* @param {array} selectedPackages - Current list of selected packages.
* @param {array} pkgInfoList - A detailed list of all packages data.
* @returns {object} - Returns an object with safe and unsafe conflicts.
*/
oData.buildConflictStructure = function(consToConsider, selectedPackages, pkgInfoList) {
var conStruct = oData.conflictStructure;
_.each(consToConsider, function(con) {
var conIsSafe = true;
_.each(selectedPackages, function(pkg) {
var pkgDetails = pkgInfoList[pkg];
if (typeof pkgDetails !== "undefined" &&
_.includes(pkgDetails.pkg_dep.requires, con)) {
// At this point this 'con' is unsafe since it is required by an existing package.
// initialize an array (if it's not already) to store this unsafe con's requirements.
if (typeof conStruct.unsafe[con] === "undefined") {
conStruct.unsafe[con] = [];
}
conStruct.unsafe[con].push(pkg);
conIsSafe = false;
}
});
if (conIsSafe) {
conStruct.safe.push(con);
}
});
return conStruct;
};
/**
* Unsafe requirements are examined and resolved by adding them to the
* remove list accordingly.
*
* @method resolveUnsafeRequires
* @param {array} selectedPackages - Current list of selected packages.
* @param {object} reqStruct
* @param {array} pkgInfoList - A detailed list of all packages data.
* @returns {object} - Updated reqStruct with resolved unsafe requirements.
*/
oData.resolveUnsafeRequires = function(selectedPackages, reqStruct, pkgInfoList) {
if (reqStruct) {
var oUnsafeRequires = reqStruct.unsafe;
var keys = _.keys(oUnsafeRequires);
_.each(keys, function(req) {
var conflictsToResolve = oUnsafeRequires[req];
conflictsToResolve = _.difference(conflictsToResolve, oData.resolvedRemoveList);
_.each(conflictsToResolve, function(con) {
// TODO: The includes if condition can be removed since resolvedRemoveList items
// are removed from conflictsToResolve list in line above using _.difference operation.
if (!_.includes(oData.resolvedRemoveList, con)) {
var selPkgsToConsider = _.difference(selectedPackages, oData.resolvedRemoveList);
oData.recurseConflictRequirements(pkgInfoList[con], con, selPkgsToConsider, pkgInfoList);
oData.resolvedRemoveList = _.union(oData.resolvedRemoveList, [con]);
}
});
// Move this 'req' from unsafe to safe.
reqStruct.safe.push(req);
// TODO: This line may not be needed since the unsafe object is reset at the end of this function.
_.unset(reqStruct.unsafe, req);
});
// At this point all unsafe requires are resolved. So remove unsafe from reqStruct.
reqStruct.unsafe = {};
}
return reqStruct;
};
/**
* Unsafe conflict are examined and resolved by adding them to the
* remove list accordingly.
*
* @method resolveUnsafeConflicts
* @param {array} selectedPackages - Current list of selected packages.
* @param {object} conStruct
* @param {array} pkgInfoList - A detailed list of all packages data.
* @returns {object} - Updated conStruct with resolved unsafe conflicts.
*/
oData.resolveUnsafeConflicts = function(selectedPackages, conStruct, pkgInfoList) {
if (conStruct) {
// Remove all conflict objects from conflictStructure.unsafe that are present in resolvedRemoveList.
// since they are already resolved.
var oUnsafeConflicts = _.omit(conStruct.unsafe, oData.resolvedRemoveList);
var keys = _.keys(oUnsafeConflicts);
_.each(keys, function(con) {
var reqsToResolve = _.difference(oUnsafeConflicts[con], oData.resolvedRemoveList);
_.each(reqsToResolve, function(req) {
// TODO: The includes if condition can be removed since resolvedRemoveList items
// are removed from conflictsToResolve list in line above using _.difference operation.
if (!_.includes(oData.resolvedRemoveList, req)) {
var selPkgsToConsider = _.difference(selectedPackages, oData.resolvedRemoveList);
oData.recurseConflictRequirements(pkgInfoList[req], req, selPkgsToConsider, pkgInfoList);
oData.resolvedRemoveList = _.union(oData.resolvedRemoveList, [req]);
}
});
// Move this 'con' from unsafe to safe.
conStruct.safe.push(con);
// TODO: This line may not be needed since the unsafe object is reset at the end of this function.
_.unset(conStruct.unsafe, con);
});
// At this point all unsafe conflicts are resolved. So remove unsafe from conStruct.
conStruct.unsafe = {};
}
return conStruct;
};
/**
* Recursive method that recurses through all requirements of a package and
* collect their corresponding conflicts.
*
* @method recurseConflictRequirements
* @param {object} conPackage - conflict package data.
* @param {string} origPkgName - The original package name of the package selected in the current context.
* @param {array} selPkgsToConsider - Current list selected packages.
* @param {array} pkgInfoList - A detailed list of all packages data.
*/
oData.recurseConflictRequirements = function(conPackage, origPkgName, selPkgsToConsider, pkgInfoList) {
var pkgName = conPackage.package;
if (origPkgName !== pkgName) {
oData.resolvedRemoveList.push(pkgName);
}
var recurseArray = _.pull(selPkgsToConsider, pkgName);
// Find all selected packages that require pkgName.
recurseArray = _.filter(recurseArray, function(pkg) {
var pkgDetails = pkgInfoList[pkg];
return (typeof pkgDetails !== "undefined" && _.includes(pkgDetails.pkg_dep.requires, pkgName));
});
_.each(recurseArray, function(recurPkg) {
oData.recurseConflictRequirements(pkgInfoList[recurPkg], origPkgName, selPkgsToConsider, pkgInfoList);
});
};
// TODO: Try to return data instead of changing global variable from within
// this method.
/**
* This method takes the selected package (origPkgName)
* and finds recursively requirements and conflicts down to all levels and store them in
* 'allRequires' & 'allConflicts' variables respectively. It also identifies packages
* which have multi requirement options and feed them into 'orListStructure' variable. They are handled
* through other methods.
*
* @method recurseWhenSelected
* @param {object} pkgInfo - The selected package data.
* @param {string} origPkgName - The original package name of the package selected in the current context.
* @param {array} pkgInfoList - A detailed list of all packages data.
*/
oData.recurseWhenSelected = function(pkgInfo, origPkgName, pkgInfoList) {
if (pkgInfo) {
var pkgName = pkgInfo.package;
// Proceed only if it is an EA package and not previously went through this recursion.
if (!oData.eaRegex.test(pkgName)) {
return;
}
if (_.includes(oData.allRequires, pkgName)) {
return;
}
// Add the package to oData.allRequires list unless it is the package selected in UI.
if (typeof origPkgName !== "undefined" && pkgName !== origPkgName) {
oData.allRequires.push(pkgName);
}
// Collect all 'ea' packages that are in conflict with current pkg.
oData.allConflicts = _.union(oData.allConflicts, _.filter(pkgInfo.pkg_dep.conflicts, function(pkg) {
return (oData.eaRegex.test(pkg));
}));
var recurseArray = [];
if (pkgInfo.pkg_dep.requires.length > 0) {
recurseArray = _.clone(pkgInfo.pkg_dep.requires);
// IDENTIFY multi-requirement arrays.
// A multi-requirement array means for example cgid requires either
// mpm-event OR mpm-worker
// so cgid will have a multi-requirement array:
// [ mpm-event, mpm-worker ]
// We identify such multi-requirement arrays, find if any one of them is selected,
// and use that. If not we collect such arrays in oData.orListStructure.orLists
// and show it to the user and let them choose.
var orLists = _.remove(recurseArray, function(pkg) {
return _.isArray(pkg);
});
if (orLists.length > 0) {
oData.orListStructure.orLists = _.unionWith(oData.orListStructure.orLists, orLists, _.isEqual);
}
recurseArray = _.filter(recurseArray, function(pkg) {
return (oData.eaRegex.test(pkg));
});
recurseArray = _.difference(recurseArray, oData.allRequires);
}
_.forEach(recurseArray, function(reqPkg) {
var reqPkgInfo = pkgInfoList[reqPkg];
if (typeof reqPkgInfo !== "undefined") {
oData.recurseWhenSelected(reqPkgInfo, origPkgName, pkgInfoList);
}
});
}
};
/**
* Get all dependencies(requires/conflicts) recursively for a given package when selected.
*
* @method getAllDepsRecursively
* @param {Boolean} selected - given package selected state.
* @param {Object} pkgInfo - Package information object.
* @param {String} origPkgName - Package name.
* @param {String} pkgInfoList - List of all package info objects.
* @param {array} selectedPackages - Current list of selected packages.
* @return {Object} -
* On package select action: { 'requiredPkgs': [...], conflictPkgs: [...] }
* On package unselect action: { 'removedList': [...] }
*/
oData.getAllDepsRecursively = function(selected, pkgInfo, origPkgName, pkgInfoList, selectedPackages) {
var deps = {};
if (selected) {
oData.recurseWhenSelected(pkgInfo, origPkgName, pkgInfoList);
deps = {
requiredPkgs: oData.allRequires,
conflictPkgs: oData.allConflicts,
};
} else {
oData.recurseWhenUnselected(pkgInfo, origPkgName, pkgInfoList, selectedPackages);
deps = {
removedList: oData.resolvedRemoveList,
};
}
return deps;
};
/**
* Resolves the requirements and conflicts of an unselected package, by going through all
* nested levels of the current package dependencies.
* Updates the resolvePackages.removeList.
*
* @method resolveDependenciesWhenUnSelected
* @param {object} thisPackage - The selected package data.
* @param {array} selectedPackages - Current list of selected packages.
* @param {array} pkgInfoList - A detailed list of all packages data.
*/
oData.resolveDependenciesWhenUnSelected = function(thisPackage, selectedPackages, pkgInfoList) {
if (thisPackage) {
var pkgName = thisPackage.package;
oData.resolvedRemoveList.push(pkgName);
oData.recurseWhenUnselected(thisPackage, pkgName, pkgInfoList, selectedPackages); // Updates oData.resolvedRemoveList.
oData.resolvedPackages.removeList = _.uniq(oData.resolvedRemoveList);
}
};
/**
* This recursive method takes the unselected package (origPkgName)
* and finds packages that depend on it and add it to the removedList.
* The recursion gets applied to the removedList all the way up and find package dependencies
* that need to be removed.
*
* @method recurseWhenUnselected
* @param {object} pkgInfo - The selected package data.
* @param {string} origPkgName - The selected package name.
* @param {array} pkgInfoList - A detailed list of all packages data.
* @param {array} selectedPkgsToUnselectFrom - Selected package list that is used as a reference to check what needs to be removed.
*/
oData.recurseWhenUnselected = function(pkgInfo, origPkgName, pkgInfoList, selectedPkgsToUnselectFrom) {
var pkgName = pkgInfo.package;
// Proceed only if it is an EA package and not previously went through this recursion.
if (!oData.eaRegex.test(pkgName)) {
return;
}
if (_.includes(oData.allRequires, pkgName)) {
return;
}
// Add the package to allRequires list unless it is the package selected in UI.
if (typeof origPkgName !== "undefined" && pkgName !== origPkgName) {
oData.allRequires.push(pkgName);
}
/* NOTES:
* Pick the packages from the `selectedPackages` list which depend (require) on the package being unselected.
* Recurse back all the way until we find all packages that depend on the one's being unselected.
**/
var recurseArray = [];
if (selectedPkgsToUnselectFrom.length > 0) {
_.each(selectedPkgsToUnselectFrom, function(pkg) {
if (_.includes(pkgInfoList && pkgInfoList[pkg] && pkgInfoList[pkg].pkg_dep.requires, pkgName)) {
// At this point, the package 'pkg' depends directly/indirectly on the original package. So,
// add this to the remove list.
oData.resolvedRemoveList.push(pkg);
recurseArray.push(pkg);
}
});
selectedPkgsToUnselectFrom = _.pullAll(selectedPkgsToUnselectFrom, recurseArray);
// IDENTIFY multi-requirement arrays.
// A multi-requirement array means for example cgid requires either
// mpm-event OR mpm-worker
// so cgid will have a multi-requirement array:
// [ mpm-event, mpm-worker ]
// We identify such multi-requirement arrays, find if any one of them is selected,
// and use that. If not we collect such arrays in orListStructure.orLists
// and show it to the user and let them choose.
var orLists = _.remove(recurseArray, function(pkg) {
return _.isArray(pkg);
});
// From each orList, add the one's that are currently selected.
_.each(orLists, function(orList) {
_.concat(recurseArray, _.filter(orList, function(pkg) {
return (typeof pkgInfoList[pkg] !== "undefined" && pkgInfoList[pkg].selectedPackage);
}));
});
recurseArray = _.filter(recurseArray, function(pkg) {
return (oData.eaRegex.test(pkg));
});
recurseArray = _.difference(recurseArray, oData.allRequires);
}
_.forEach(recurseArray, function(reqPkg) {
var reqPkgInfo = pkgInfoList[reqPkg];
if (typeof reqPkgInfo !== "undefined") {
oData.recurseWhenUnselected(reqPkgInfo, origPkgName, pkgInfoList, selectedPkgsToUnselectFrom);
}
});
};
/**
* Constructs multiRequirement object for a package's dependencies.
*
* @method setupMultiRequirementForUserInput
* @param {object} pkgInfoList An package info object with key (package name) value (package object) pairs.
*/
oData.setupMultiRequirementForUserInput = function(pkgInfoList) {
var multiRequirements = {};
// Check if orLists is empty. If empty,
// Pull the first orList from multiReq.
var orListStruct = oData.getOrListStructure();
if (typeof orListStruct !== "undefined") {
var orList = orListStruct.orLists.shift();
if (typeof orList !== "undefined" && orList.length > 0) {
var orListWithPkgNames = _.map(orList, function(orPkg) {
if (typeof pkgInfoList[orPkg] !== "undefined") {
return { "package": orPkg, "displayName": pkgInfoList[orPkg].displayName };
}
});
multiRequirements = {
exist: orListStruct.exist,
orList: orListWithPkgNames,
chosenPackage: "",
};
}
}
return multiRequirements;
};
/**
* Resets all common and package object variables that deal with
* conflict/requirement resolving process.
*
* @method resetPkgResolveActions
* @param {object} pkgInfo - Package object.
* @returns {object} - package object.
*/
oData.resetPkgResolveActions = function(pkgInfo) {
oData.resetCommonVariables();
// Reset the actions of this package to empty
if (pkgInfo) {
// TODO: merge pkgInfo.actions & multirequirements into
// pkgInfo.resolveData = {
// removeList: [], addList: [], actionNeeded: false,
// multiRequirements: {}
// };
pkgInfo.actions = { removeList: [], addList: [], actionNeeded: false };
pkgInfo.mpmMissing = false;
pkgInfo.mpmMissingMsg = "";
pkgInfo.multiRequirements = {};
}
return pkgInfo;
};
/**
* Prepares the callout to display the conflict requirements that are identified
* during the resolving process.
*
* @method setupConDepCallout
* @param {object} pkgInfo - The selected package data.
* @param {array} pkgInfoList - A detailed list of all packages data.
* @returns {object} - Selected package data with updated dependency resolution info for callout .
*/
oData.setupConDepCallout = function(pkgInfo, pkgInfoList) {
// get resolved package data.
var resData = oData.getResolvedData();
if (resData.actionNeeded) {
pkgInfo.actions.actionNeeded = resData.actionNeeded;
pkgInfo.actions.removeList = _.map(resData.removeList, function(pkg) {
return pkgInfoList[pkg].displayName;
});
pkgInfo.actions.addList = _.map(resData.addList, function(pkg) {
return pkgInfoList[pkg].displayName;
});
}
return pkgInfo;
};
return oData;
});
}
);
/*
# cpanel - whostmgr/docroot/templates/easyapache4/services/wizardApi.js
# Copyright 2022 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: false */
define(
'app/services/wizardApi',[
"angular",
"lodash",
"cjt/util/locale",
// CJT
"cjt/io/api",
"cjt/io/whm-v1-request",
"cjt/io/whm-v1",
// Angular components
"cjt/services/APIService",
// App components
],
function(angular, _, LOCALE) {
"use strict";
var app = angular.module("whm.easyapache4.wizardApi", []);
app.factory("wizardApi", ["$location", "wizardState", "ea4Data", "ea4Util", function($location, wizardState, ea4Data, ea4Util) {
var oData = {
defaultWizardState: {
showWizard: false,
showSearchAndPage: false,
currentStepIndex: 0,
showFooter: false,
currentStep: "",
lastStepName: "",
steps: {
"mpm": { name: "mpm", title: LOCALE.maketext("Apache [output,acronym,MPM,Multi-Processing Modules]"), path: "mpm", stepIndex: 1, nextStep: "modules" },
"modules": { name: "modules", title: LOCALE.maketext("Apache Modules"), path: "modules", stepIndex: 2, nextStep: "php" },
"php": { name: "php", title: LOCALE.maketext("[output,acronym,PHP,PHP Hypertext Preprocessor] Versions"), path: "php", stepIndex: 3, nextStep: "extensions" },
"extensions": { name: "extensions", title: LOCALE.maketext("[output,acronym,PHP,PHP Hypertext Preprocessor] Extensions"), path: "extensions", stepIndex: 4, nextStep: "ruby" },
"ruby": { name: "ruby", title: LOCALE.maketext("[asis,Ruby] via [asis,Passenger]"), path: "ruby", stepIndex: 5, nextStep: "additional" },
"additional": { name: "additional", title: LOCALE.maketext("Additional Packages"), path: "additional", stepIndex: 6, nextStep: "review" },
"review": { name: "review", title: LOCALE.maketext("Review"), path: "review", stepIndex: 7, nextStep: "" },
},
},
};
oData.getDefaultWizardState = function() {
return oData.defaultWizardState;
};
/**
* Checks the existence of certain packages and keeps or removes certain steps. After
* the evaluation it rebuilds the wizard steps accordingly.
* @param {Object} wizardSteps Object containing wizard steps.
* @param {Object} rebuildArgs
* @return {Object} Returns the new rebuilt wizardSteps Object.
*/
oData.rebuildWizardSteps = function(wizardSteps, rebuildArgs) {
wizardSteps = wizardSteps || {};
if (!rebuildArgs.rubyPkgsExist) {
delete wizardSteps["ruby"];
}
if (!rebuildArgs.additionalPkgsExist) {
delete wizardSteps["additional"];
}
// Sort the steps. orderby isn't working directly in the ng-repeat (shrug).
var sortedSteps = _.orderBy(_.values(wizardSteps), ["stepIndex"], ["asc"]);
wizardSteps = _.keyBy(sortedSteps, function(step) {
return step.name;
});
return wizardSteps;
};
oData.init = function() {
wizardState.steps = oData.defaultWizardState.steps;
var pkgList = ea4Data.getData("pkgInfoList");
if (pkgList) {
var rebuildArgs = {
rubyPkgsExist: ea4Util.doRubyPkgsExist(pkgList),
additionalPkgsExist: ea4Data.getData("additionalPkgsExist"),
};
wizardState.steps = oData.rebuildWizardSteps(oData.defaultWizardState.steps, rebuildArgs);
}
wizardState.showWizard = false;
wizardState.showSearchAndPage = false;
wizardState.showFooter = false;
wizardState.currentStepIndex = 1;
wizardState.lastStepName = "review";
};
oData.updateWizard = function(config) {
_.each(_.keys(config), function(key) {
wizardState[key] = config[key];
});
};
oData.getStepByName = function(stepName) {
return wizardState.steps[stepName];
};
oData.getStepNameByIndex = function(index) {
var stepObj = _.find(wizardState.steps, ["stepIndex", index]);
if (typeof stepObj !== "undefined") {
return stepObj.name;
}
};
/**
* Reset the wizard to it initial state. It will forward any
* arguments passed into the call to the registered
* function.
*
* @name reset
*/
oData.reset = function() {
wizardState = oData.getDefaultWizardState();
};
/**
* This function auto updates wizardState to the next step index and go to that step
* if no arguments are passed.
* If stepName argument is passed, then it updates the wizardState to the given step,
* and goes to the given step.
*
* @name next
* @arg stepName [optional] If passed, this method will send to the given step's view.
*/
oData.next = function(stepName) {
if (stepName) {
wizardState.currentStepIndex = oData.getStepByName(stepName).stepIndex;
wizardState.currentStep = stepName;
} else {
wizardState.currentStepIndex++;
stepName = oData.getNextStepNameByIndex(wizardState.currentStepIndex);
wizardState.currentStep = stepName;
}
$location.path(stepName);
};
oData.getNextStepNameByIndex = function(index) {
var lastStepIndex = oData.getLastStep().stepIndex;
var stepObj = _.find(wizardState.steps, ["stepIndex", index]);
if (typeof stepObj === "undefined") {
// Find the next available step.
for (var i = index + 1; i <= lastStepIndex; i++) {
stepObj = _.find(wizardState.steps, ["stepIndex", i]);
if (stepObj === "undefined") {
continue;
}
}
}
return (stepObj) ? stepObj.name : "";
};
/**
* Get the wizard's last step object.
* @return {Object} Returns wizard step object.
*/
oData.getLastStep = function() {
return wizardState.steps[wizardState.lastStepName];
};
return {
init: oData.init,
getStepByName: oData.getStepByName,
updateWizard: oData.updateWizard,
next: oData.next,
getDefaultWizardState: oData.getDefaultWizardState,
reset: oData.reset,
rebuildWizardSteps: oData.rebuildWizardSteps,
getLastStep: oData.getLastStep,
getNextStepNameByIndex: oData.getNextStepNameByIndex,
};
}]);
}
);
/*
# whostmgr/docroot/templates/easyapache4/directives/fileModel.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: false */
define('app/directives/fileModel',[
"angular"
], function(angular) {
// This directive updates the $scope when an <input type="file"> changes.
// AngularJS ng-model does not keep the state of <input type="file"> linked with $scope.
angular.module("App")
.directive("fileModel", ["$parse", function($parse) {
return {
restrict: "A",
require: "ngModel",
link: function link($scope, $element, $attrs, $ngModelCtrl) {
var model = $parse($attrs.fileModel);
$element.bind("change", function() {
var file = this.files[0];
if (file) {
$scope.$apply(function() {
model.assign($scope, file);
// Mark as dirty
$ngModelCtrl.$setViewValue($ngModelCtrl.$modelValue);
$ngModelCtrl.$setDirty();
});
}
});
}
};
}]);
});
/*
# whostmgr/docroot/templates/cpanel_customization/directive/fileType.js Copyright 2022 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: false */
define('app/directives/fileType',[
"angular"
], function(angular) {
// This directive validates an <input type="file"> based on the "type" property of a selected file.
// The file-type attribute should contain an expression defining an array of valid types.
angular.module("App")
.directive("fileType", [function() {
function checkType(file, types) {
var valid = false;
var fileType = file.type;
// IE doesn't return file.type for some MIME types.
// For example it doesn't for 'json' type.
// FIX: For 'json' in IE browsers.
// If (file.type is empty){
// match file extension with requested type.
// }
// NOTE: This is not a fix for all but will cover at least JSON.
if (fileType === "") {
var matchArr = file.name.match(/\.((?:.(?!\.))+)$/);
fileType = (matchArr.length > 0) ? matchArr[1] : "";
// Hack for json type
if (fileType === "json") {
fileType = "application/" + fileType;
}
}
valid = types.some(function(type) {
return type === fileType;
});
return valid;
}
return {
restrict: "A",
require: "ngModel",
link: function link($scope, $element, $attrs, $ngModelCtrl) {
$element.bind("change", function() {
var file = this.files[0];
if (file && !checkType(file, $scope.$eval($attrs.fileType))) {
$ngModelCtrl.$setValidity("filetype", false);
} else {
$ngModelCtrl.$setValidity("filetype", true);
}
});
}
};
}]);
});
/*
# whostmgr/docroot/templates/cpanel_customization/directive/fileSize.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 */
define('app/directives/fileSize',[
"angular"
], function(angular) {
// This directive validates an <input type="file"> based on the "type" property of a selected file.
// The file-type attribute should contain an expression defining an array of valid types.
angular.module("App")
.directive("fileSize", [function() {
return {
restrict: "A",
require: "ngModel",
link: function link($scope, $element, $attrs, $ngModelCtrl) {
$element.bind("change", function() {
var file = this.files[0];
if (file) {
// Check for empty files being uploaded
if (file.size === 0) {
$ngModelCtrl.$setValidity("fileSize", false);
} else {
$ngModelCtrl.$setValidity("fileSize", true);
}
}
});
}
};
}]);
});
/*
# templates/easyapache4/views/profile.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 */
/* eslint-disable no-console, no-use-before-define, camelcase, no-useless-escape */
define(
'app/views/profile',[
"angular",
"cjt/util/locale",
"lodash",
"cjt/services/alertService",
"cjt/directives/alertList",
"cjt/decorators/growlDecorator",
"app/directives/fileModel",
"app/directives/fileType",
"app/directives/fileSize",
"app/services/ea4Data",
"app/services/ea4Util",
"app/services/pkgResolution",
],
function(angular, LOCALE, _) {
"use strict";
// Retrieve the current application
var app = angular.module("App");
app.controller("profile",
[ "$scope", "$timeout", "$location", "$uibModal", "ea4Data", "ea4Util", "alertService", "growl", "growlMessages",
function($scope, $timeout, $location, $uibModal, ea4Data, ea4Util, alertService, growl, growlMessages) {
$scope.profileList = [];
$scope.activeProfile = {};
$scope.loadingProfiles = false;
$scope.errorOccurred = false;
$scope.noProfiles = false;
$scope.upload = {
show: false,
profile: {},
content: {},
disableLocalSec: true,
localSecIsOpen: true,
disableUrlSec: false,
urlSecIsOpen: false,
url: {
value: "",
filename: "",
filenameValMsg: "",
showFilenameInput: false,
},
overwrite: false,
highlightOverwrite: false,
};
$scope.convertProfile = { show: false };
var _ea4Recommendations = {};
$scope.isLoading = function() {
return ( $scope.loadingProfiles || $scope.loadingProfileData );
};
var resetEA4UI = function() {
// Reset wizard attributes.
// TODO: This will stay here until ui.router is implemented
// in the next template refactor.
$scope.customize.wizard.currentStep = "";
$scope.customize.wizard.showWizard = false;
alertService.clear();
// This cancels any previously customized packages.
ea4Data.clearEA4LocalStorageItems();
};
var customizeProfile = function(thisProfile) {
// This cancels any previously customized packages.
ea4Data.clearEA4LocalStorageItems();
ea4Data.setData(
{
"selectedPkgs": thisProfile.pkgs,
"customize": true,
"ea4Recommendations": _ea4Recommendations,
});
$location.path("loadPackages");
};
var goProvision = function(thisProfile) {
ea4Data.setData( { "selectedProfile": thisProfile } );
$location.path("review");
};
$scope.$on("$viewContentLoaded", function() {
resetEA4UI();
$scope.loadProfiles();
});
$scope.checkForEA4Updates = function() {
$scope.customize.checkUpdateInfo = angular.copy(ea4Util.checkUpdateInfo);
var promise = ea4Data.getPkgInfoList();
promise.then(function(data) {
if (typeof data !== "undefined") {
var rawPkgList = data;
ea4Data.setData({ "ea4RawPkgList": rawPkgList });
var updatePkgs = _.map(_.filter(rawPkgList, ["state", "updatable"]), function(pkg) {
return pkg.package;
});
// Count the number of packages in updatable state.
$scope.customize.checkUpdateInfo.pkgNumber = updatePkgs.length;
$scope.customize.toggleUpdateButton();
}
$scope.customize.checkUpdateInfo.isLoading = false;
}, function(error) {
alertService.add({
type: "danger",
message: error,
id: "alertMessages",
closeable: false,
});
});
};
$scope.loadProfiles = function() {
$scope.profileList = [];
$scope.loadingProfiles = true;
ea4Data.getProfiles().then(function(profData) {
if (typeof profData !== "undefined") {
$scope.noProfiles = false;
$scope.checkForEA4Updates();
$scope.loadingProfileData = true;
ea4Data.getEA4Recommendations().
then(function(result) {
_ea4Recommendations = result.data;
setProfileData(profData, _ea4Recommendations); // eslint-disable-line no-use-before-define
}, function(error) {
showProfileErrors(error);
}).finally(function() {
$scope.loadingProfileData = false;
});
} else {
$scope.noProfiles = true;
}
}, function(error) {
if (error) {
$scope.errorOccurred = true;
ea4Data.setData( { "ea4ThrewError": true } );
$location.path("yumUpdate");
}
}).finally(function() {
$scope.loadingProfiles = false;
});
};
$scope.viewProfile = function(thisProfile) {
var viewingProfile = angular.copy(thisProfile);
$uibModal.open({
templateUrl: "profileModalContent.tmpl",
controller: "ModalInstanceCtrl",
resolve: {
data: function() {
return viewingProfile;
},
},
});
};
$scope.customizeCurrentProfile = function(thisProfile) {
customizeProfile(thisProfile);
};
$scope.proceedNext = function(thisProfile, customize) {
// Track if customize button clicked or provision button clicked.
$scope.clickedCustomize = customize;
// Show a warning if there are packages in profile not on server.
if (!thisProfile.isValid) {
thisProfile.showValidationWarning = true;
return;
}
thisProfile.showValidationWarning = false;
$scope.continueAction(thisProfile);
};
$scope.continueAction = function(thisProfile) {
var customize = $scope.clickedCustomize;
// Reset the clicked variable for next use.
$scope.clickedCustomize = false;
// Insert Apache 2.4 into the profile. This ensures people
// get apache in whatever state their profile is.
if (thisProfile.pkgs.indexOf("ea-apache24") === -1) {
thisProfile.pkgs.push("ea-apache24");
}
if (customize) {
customizeProfile(thisProfile);
} else {
goProvision(thisProfile);
}
};
/**
* Resets the clicked variable for next use.
*
* @method reset
*/
$scope.reset = function(thisProfile) {
$scope.clickedCustomize = false;
thisProfile.showValidationWarning = false;
};
$scope.hideRecommendations = function(activeProfile) {
activeProfile.showRecommendations = false;
// Upon closing recommendation panel, return focus to recommendation link for screenreader/keyboard users
$timeout(function() {
angular.element("#toggleRecommendations").focus();
});
};
$scope.showRecommendations = function(activeProfile) {
activeProfile.showRecommendations = true;
// Apply focus to recommendation container for screenreader/keyboard users
$timeout(function() {
angular.element("#recommendations_container").focus();
});
};
var recommendationsOfActiveProfile = function(activeProfile, recommendations) {
var currPkgList = activeProfile.pkgs;
var filterPkgsWithRecos = _.intersection(currPkgList, _.keys(recommendations));
var filteredRecos = {};
_.each(filterPkgsWithRecos, function(pkg) {
var reco = recommendations[pkg];
var recosList = ea4Util.decideShowHideRecommendations(reco, currPkgList, true, pkg); // passing 'true' as args to get recommendations of installed packages.
// On the profiles page show only recommendations that have level: danger.
recosList = _.filter(recosList, ["level", "danger"]);
if (!_.isEmpty(recosList)) {
filteredRecos[pkg] = {};
filteredRecos[pkg].recosList = recosList;
filteredRecos[pkg].show = !_.every(recosList, [ "show", false ]);
// Set the footnote.
filteredRecos[pkg].footNote = LOCALE.maketext("These recommendations appear because you have “[_1]” installed on your system.", pkg);
}
});
return filteredRecos;
};
/* Upload Popover section */
var resetValidators = function(formInput) {
var valErrors = formInput.$error;
if (typeof valErrors !== "undefined") {
_.each(_.keys(valErrors), function(valKey) {
$scope.formUpload.profile_file.$setValidity(valKey, true);
});
}
};
/**
* Clears everything in the upload popover.
*
* @method clearUploadPopover
*/
var clearUploadPopover = function() {
// Destroy all growls before attempting to submit something.
growlMessages.destroyAllMessages();
// reseting model values
var uploadData = $scope.upload;
uploadData.content = {};
uploadData.overwrite = false;
uploadData.highlightOverwrite = false;
uploadData.disableLocalSec = true;
uploadData.localSecIsOpen = true;
uploadData.disableUrlSec = false;
uploadData.urlSecIsOpen = false;
clearUploadLocalForm($scope.upload);
clearUploadUrlForm($scope.upload.url);
};
/**
* Clears the upload local section in Upload accordion.
*
* @method clearUploadLocalForm
*/
var clearUploadLocalForm = function(uploadData) {
// reseting upload local section.
uploadData.profile = {};
if ($scope.formUpload && $scope.formUpload.$dirty) {
resetValidators($scope.formUpload.profile_file);
// mark the form pristine
$scope.formUpload.$setPristine();
try {
angular.element("#profile_file").val(null); // for IE11, latest browsers
} catch (error) {
// For IE10 and others
angular.element("#form_upload_profile").reset();
}
}
};
/**
* Clears the upload url section in Upload accordion.
*
* @method clearUploadUrlForm
*/
var clearUploadUrlForm = function(uploadUrlData) {
// var uploadData = $scope.upload;
uploadUrlData.filename = "";
uploadUrlData.filenameValMsg = "";
uploadUrlData.value = "";
uploadUrlData.showFilenameInput = false;
if ($scope.formUpload && $scope.formUpload.$dirty) {
var valErrors = $scope.formUpload.profile_file_url.$error;
if (typeof valErrors !== "undefined") {
_.each(_.keys(valErrors), function(valKey) {
$scope.formUpload.profile_file.$setValidity(valKey, true);
});
}
resetValidators($scope.formUpload.profile_file_url);
resetValidators($scope.formUpload.txtUploadUrlFilename);
// mark the form pristine
$scope.formUpload.$setPristine();
}
};
/**
* Validates the profile content to
* check if it contains name & at least
* one package.
*
* @method validateProfile
*/
var validateProfile = function(fileContent) {
var valid = true;
if (_.isEmpty(fileContent.name) ||
_.isEmpty(fileContent.pkgs)) {
valid = false;
}
return valid;
};
/**
* Validate the uploaded filename to see if it contains
* restricted characters.
*
* @method validateFilename
*/
var validateFilename = function(filename) {
var valid = true;
if (/(?:\.\.|\\|\/)/.test(filename)) {
valid = false;
}
return valid;
};
/**
* Reads the uploaded file to validate it.
*
* @method getAndValidateUploadData
*/
var getAndValidateUploadData = function() {
// Destroy all growls before attempting to submit something.
growlMessages.destroyAllMessages();
var fileData = $scope.upload.profile;
if (!validateFilename(fileData.name)) {
$scope.$apply($scope.formUpload.profile_file.$setValidity("invalidfilename", false));
return;
} else {
$scope.$apply($scope.formUpload.profile_file.$setValidity("invalidfilename", true));
}
var reader = new FileReader();
reader.readAsText(fileData);
reader.onloadend = function() {
if (reader.readyState && !reader.error) {
// Check if the file has the required data.
var upContent = validateUploadContent(reader.result);
_.each(_.keys(upContent.val_results), function(val_key) {
$scope.$apply($scope.formUpload.profile_file.$setValidity(val_key, upContent.val_results[val_key]));
});
if ($scope.formUpload.profile_file_url.$valid) {
$scope.upload.content = upContent.content;
}
}
};
};
/**
* This validates the given content
* and updates the validators accordingly.
*
* @method validateUploadContent
*/
var validateUploadContent = function(uploadContent) {
// Check if the file has the required data.
var content = "";
var valResults = {};
try {
content = JSON.parse(uploadContent);
valResults["invalidformat"] = true;
if (!validateProfile(content)) {
valResults["content"] = false;
} else {
valResults["content"] = true;
}
} catch (e) {
valResults["invalidformat"] = false;
console.log(e);
}
return { "content": content, "val_results": valResults };
};
/**
* Cancels the upload action for local section.
*
* @method cancelUpload
*/
$scope.cancelUpload = function() {
$scope.upload.show = false;
clearUploadPopover();
};
/**
* Cancels the upload action for url section.
*
* @method resetUploadUrl
*/
$scope.resetUploadUrl = function() {
clearUploadUrlForm($scope.upload.url);
};
/**
* Gets the content from the provided url and performs
* validation checks to make sure it is a valid JSON
* content with valid profile data.
*
* @method getAndValidateUploadDataFromURL
*/
$scope.getAndValidateUploadDataFromURL = function() {
// Destroy all growls before attempting to submit something.
growlMessages.destroyAllMessages();
return ea4Data.getUploadContentFromUrl($scope.upload.url.value)
.then(function(data) {
if (typeof data !== "undefined" && data.status === "200") {
var contentType = data.headers["content-type"];
var validType = /^(application|text)\/json/.test(contentType);
if (!validType) {
$scope.formUpload.profile_file_url.$setValidity("filetype", false);
return;
} else {
$scope.formUpload.profile_file_url.$setValidity("filetype", true);
}
// Check if the file has the required data.
var upContent = validateUploadContent(data.content);
_.each(_.keys(upContent.val_results), function(val_key) {
$scope.formUpload.profile_file_url.$setValidity(val_key, upContent.val_results[val_key]);
});
if ($scope.formUpload.profile_file_url.$valid) {
$scope.upload.content = upContent.content;
$scope.upload.url.showFilenameInput = true;
} else {
$scope.upload.url.showFilenameInput = false;
}
} else {
var errorMsg = LOCALE.maketext("Status: “[output,strong,_1]”. Reason: “[output,em,_2]”.", _.escape(data.status), _.escape(data.reason));
growl.error(errorMsg);
}
}, function(error) {
growl.error(_.escape(error));
});
};
/**
* Scope method that calls ea4Util service's validateFilename method and
* sets the validation message accordingly.
*
* @method validateFilenameInput
*/
$scope.validateFilenameInput = function() {
var valData = ea4Util.validateFilename($scope.upload.url.filename);
$scope.upload.url.filenameValMsg = valData.valMsg;
$scope.formUpload.txtUploadUrlFilename.$setValidity("valFilename", valData.valid);
};
/**
* Uploads Profiles.
*
* @method uploadProfile
*/
$scope.uploadProfile = function() {
// Destroy all growls before attempting to submit something.
growlMessages.destroyAllMessages();
if ($scope.formUpload.$valid) {
// upload profile
var overwrite = $scope.upload.overwrite ? 1 : 0;
var filenameWithExt = (typeof $scope.upload.profile.name !== "undefined") ? $scope.upload.profile.name : $scope.upload.url.filename + ".json";
return ea4Data.saveAsNewProfile($scope.upload.content, filenameWithExt, overwrite)
.then(function(data) {
if (typeof data !== "undefined" && !_.isEmpty(data.path)) {
$scope.loadProfiles();
growl.success(LOCALE.maketext("The system successfully uploaded your profile."));
$scope.cancelUpload();
}
}, function(response) {
if (!_.isEmpty(response.data) && response.data.already_exists) {
$scope.upload.highlightOverwrite = true;
}
growl.error(_.escape(response.error));
});
}
};
/**
* This toggle function handles enabling/disabling sections
* so that at least one upload section is always open.
*
* @method handleAccordionToggle
*/
$scope.handleAccordionToggle = function() {
clearUploadLocalForm($scope.upload);
clearUploadUrlForm($scope.upload.url);
if ($scope.upload.urlSecIsOpen) {
$scope.upload.disableLocalSec = false;
$scope.upload.disableUrlSec = true;
} else {
$scope.upload.disableUrlSec = false;
$scope.upload.disableLocalSec = true;
}
};
/**
* Shows the given popover with their initialization logic.
*
* @method showPopover
*/
$scope.showPopover = function(popoverName) {
$scope.convertProfile.cancel();
switch (popoverName) {
case "upload":
$scope.upload.show = true;
document.querySelector("#profile_file").onchange = getAndValidateUploadData;
var accordionLinkEls = document.querySelectorAll(".panel-heading a");
_.each(accordionLinkEls, function(el) {
el.onclick = $scope.handleAccordionToggle;
});
break;
case "convert":
$scope.convertProfile.show = true;
break;
}
};
/**
* This method sets all the profile data including recommendations.
*
* @method setProfileData
*/
var setProfileData = function(data, recommendations) {
var profileTypes = _.sortBy(_.keys(data));
_.each(profileTypes, function(type) {
if (typeof data[type] !== "undefined") {
_.each(data[type], function(profile) {
profile.profileType = type;
profile.tagsAsString = LOCALE.list_and(profile.tags);
// Initialize with a valid flag.
profile.isValid = true;
profile.showValidationWarning = false;
if (!profile.active) { // Active profile is shown separately.
profile.isValid = _.isEmpty(profile.validation_data.not_on_server);
if (!profile.isValid) {
profile.validation_data.not_on_server_without_prefix = ea4Util.getFormattedPackageList(profile.validation_data.not_on_server);
}
profile.id = type + "_" + profile.path.replace(/\.json/, "");
// If the type is other than cPanel Or Custom, it should be a vendor in which case the
// path changes a bit.
var pathByType = ( type !== "cpanel" && type !== "custom" ) ? "vendor/" + type : type;
profile.downloadUrl = "ea4_profile_download/" + pathByType + "?filename=" + profile.path;
$scope.profileList.push(profile);
} else {
$scope.activeProfile = profile;
// need active profile packages in customize scope so can run packages updates
$scope.customize.activeProfilePkgs = profile.pkgs;
var recos = recommendationsOfActiveProfile($scope.activeProfile, recommendations);
if (!_.isEmpty(_.keys(recos))) {
$scope.activeProfile.showRecommendations = false;
var actual_recos = _.pickBy(recos, function(value, key) {
return recos[key].show;
});
var recoCnt = 0;
_.each(_.keys(actual_recos), function(key) {
recoCnt += _.filter(actual_recos[key].recosList, ["show", true]).length;
} );
$scope.activeProfile.recommendations = actual_recos;
$scope.activeProfile.recommendationLabel = LOCALE.maketext("[quant,_1,Recommendation,Recommendations]", recoCnt);
$scope.activeProfile.recommendationsExist = recoCnt ? true : false;
} else {
$scope.activeProfile.recommendations = {};
}
}
});
}
});
// Check if there are any profiles.
$scope.noProfiles = ($scope.profileList.length <= 0);
// Active Profile. At present active profile will always be 'Currently Installed Packages'
// This may change in future.
// TODO: Add this method to ea4Util
var tags = ea4Util.createTagsForActiveProfile($scope.activeProfile.pkgs);
$scope.activeProfile.tags = tags;
$scope.activeProfile.tagsAsString = LOCALE.list_and(tags);
};
/**
* Error handling method for profile load failures.
*
* @method showProfileErrors
*/
var showProfileErrors = function(error) {
$scope.errorOccurred = true;
alertService.add({
type: "danger",
message: error,
id: "alertMessages",
closeable: false,
});
};
},
]
);
app.controller("ModalInstanceCtrl",
["$scope", "$uibModalInstance", "data", "ea4Util",
function($scope, $uibModalInstance, data, ea4Util) {
$scope.modalData = {};
var profileInfo = data;
profileInfo.pkgs = ea4Util.getProfilePackagesByCategories(profileInfo.pkgs);
$scope.modalData = profileInfo;
$scope.closeModal = function() {
$uibModalInstance.close();
};
},
]
);
}
);
/*
# templates/easyapache4/views/yumUpdate.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 */
define(
'app/views/yumUpdate',[
"angular",
"cjt/util/locale",
"lodash",
"cjt/services/alertService",
"cjt/directives/alertList",
"app/services/ea4Data",
"app/services/ea4Util",
"app/services/pkgResolution"
],
function(angular, LOCALE, _) {
// Retrieve the current application
var app = angular.module("App");
app.controller("yumUpdate",
[ "$scope", "$location", "ea4Data", "ea4Util", "alertService", "growl", "growlMessages",
function($scope, $location, ea4Data, ea4Util, alertService, growl, growlMessages) {
$scope.fixFailed = false;
var fixYumCache = function() {
$scope.fixingYum = true;
ea4Data.fixYumCache().then(function(result) {
if (result.status && result.data.cache_seems_ok_now) {
app.firstLoad = false;
ea4Data.setData( { "ea4ThrewError": false } );
$location.path("profile");
} else {
$scope.fixFailed = true;
}
}, function(error) {
$scope.fixFailed = true;
}).finally(function() {
$scope.fixingYum = false;
});
};
$scope.$on("$viewContentLoaded", function() {
// Destroy all old growls when view is loaded.
growlMessages.destroyAllMessages();
var error = ea4Data.getData("ea4ThrewError");
if (error) {
fixYumCache();
}
});
}]);
}
);
/*
# cpanel - whostmgr/docroot/templates/easyapache4/views/customize.js
# 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
*/
/* global define */
define(
'app/views/customize',[
"angular",
"lodash",
"cjt/util/locale",
"app/services/ea4Data",
"app/services/pkgResolution",
],
function(angular, _, LOCALE) {
"use strict";
// Retrieve the current application
var app = angular.module("App");
app.controller("customize",
[ "$scope", "ea4Data", "pkgResolution", "wizardState", "wizardApi", "$location", "ea4Util",
function($scope, ea4Data, pkgResolution, wizardState, wizardApi, $location, ea4Util) {
$scope.customize = {
pkgInfoList: {},
selectedPkgs: [],
// This contains only the current step's package info.
currentPkgInfoList: {},
saveProfilePopup: {
position: "top",
showTop: false,
showBottom: false,
},
// this contains the packages to run the update
activeProfilePkgs: [],
};
$scope.customize.wizard = wizardState;
$scope.customize.wizardApi = wizardApi;
/* ------- Save As Profile ------- */
$scope.customize.showsaveProfilePopup = function(position) {
$scope.customize.saveProfilePopup.position = position;
if (position !== "top") {
$scope.customize.saveProfilePopup.showTop = false;
$scope.customize.saveProfilePopup.showBottom = true;
} else {
$scope.customize.saveProfilePopup.showTop = true;
$scope.customize.saveProfilePopup.showBottom = false;
}
};
$scope.customize.clearSaveProfilePopup = function(position) {
if (position !== "top") {
$scope.customize.saveProfilePopup.showTop = false;
$scope.customize.saveProfilePopup.showBottom = false;
$scope.customize.saveProfilePopup.position = "top";
} else {
$scope.customize.saveProfilePopup.showTop = false;
$scope.customize.saveProfilePopup.showBottom = false;
}
};
$scope.customize.loadData = function(type) {
pkgResolution.resetCommonVariables();
$scope.customize.pkgInfoList = ea4Data.getData("pkgInfoList");
var customizeMode = ea4Data.getData("customize");
if (_.keys($scope.customize.pkgInfoList).length <= 0) {
ea4Data.cancelOperation();
} else {
$scope.customize.selectedPkgs = ea4Data.getData("selectedPkgs");
// set showWizard flag
wizardApi.updateWizard(
{
"showWizard": customizeMode,
"currentStep": type,
}
);
if (type === "review") {
ea4Util.hideFooter();
} else {
ea4Util.showFooter();
}
}
};
$scope.customize.processPkgInfoList = function(data) {
if (typeof data !== "undefined") {
var additionalPrefixes = ea4Util.getCachedPrefixes() || [];
ea4Data.updateRegexesForPrefixes(additionalPrefixes);
var recos = ea4Data.getData("ea4Recommendations");
$scope.customize.pkgInfoList = ea4Data.buildPkgInfoList($scope.customize.selectedPkgs, data, recos);
ea4Data.setData({ "pkgInfoList": $scope.customize.pkgInfoList });
}
};
$scope.customize.loadPkgInfoList = function() {
pkgResolution.resetCommonVariables();
$scope.customize.selectedPkgs = ea4Data.getData("selectedPkgs");
var promise = ea4Data.getPkgInfoList();
promise.then(function(data) {
$scope.customize.processPkgInfoList(data);
});
return promise;
};
$scope.customize.proceed = function(step) {
ea4Data.setData(
{
"pkgInfoList": $scope.customize.pkgInfoList,
"selectedPkgs": $scope.customize.selectedPkgs,
}
);
wizardApi.next(step);
};
$scope.customize.getStepClass = function(step) {
if (step === $scope.customize.wizard.currentStep) {
return "active";
}
};
$scope.customize.getViewWidthCss = function(isWizard) {
return (isWizard ? "col-xs-9" : "col-xs-12");
};
$scope.customize.provisionEA4Updates = function() {
// This cancels any previously customized packages.
ea4Data.clearEA4LocalStorageItems();
ea4Data.setData(
{
"selectedPkgs": $scope.customize.activeProfilePkgs,
"ea4Update": true,
});
$location.path("review");
};
$scope.customize.toggleUpdateButton = function() {
var updateCount = $scope.customize.checkUpdateInfo.pkgNumber;
if (updateCount > 0) {
$scope.customize.checkUpdateInfo.btnText = LOCALE.maketext("Update [asis,EasyApache 4]");
$scope.customize.checkUpdateInfo.btnTitle = LOCALE.maketext("Update [asis,EasyApache 4]");
$scope.customize.checkUpdateInfo.btnCss = "btn-primary";
} else {
$scope.customize.checkUpdateInfo.btnText = LOCALE.maketext("[asis,EasyApache 4] is up to date[comment,no punctuation due to usage]");
$scope.customize.checkUpdateInfo.btnTitle = LOCALE.maketext("[asis,EasyApache 4] is up to date[comment,no punctuation due to usage]");
$scope.customize.checkUpdateInfo.btnCss = "btn-primary disabled";
}
};
},
]
);
}
);
/*
# cpanel - whostmgr/docroot/templates/easyapache4/views/loadPackages.js
# Copyright 2022 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: false */
define(
'app/views/loadPackages',[
"angular",
"lodash",
"cjt/services/alertService",
],
function(angular, _) {
"use strict";
// Retrieve the current application
var app = angular.module("App");
app.controller("loadPackages",
["$scope", "alertService", "wizardApi", "wizardState", "ea4Data", "ea4Util", "pkgResolution",
function($scope, alertService, wizardApi, wizardState, ea4Data, ea4Util, pkgResolution) {
var loadPkgInfoData = function() {
var rawPkgList = ea4Data.getData("ea4RawPkgList");
if (rawPkgList === null) {
var promise = $scope.customize.loadPkgInfoList();
// REFACTOR: ERROR returns should be handled correctly.
promise.then(function() {
ea4Data.getEA4MetaInfo().then(function(response) {
if (response.data) {
ea4Util.additionalPkgList = response.data.additional_packages;
// Find if additional packages don't exist in the system.
var additionalPkgsExist = ea4Util.doAdditionalPkgsExist(ea4Util.additionalPkgList, $scope.customize.pkgInfoList);
ea4Data.setData({ "additionalPkgsExist": additionalPkgsExist });
var rebuildArgs = {
rubyPkgsExist: ea4Util.doRubyPkgsExist($scope.customize.pkgInfoList),
additionalPkgsExist: additionalPkgsExist,
};
wizardState.steps = wizardApi.rebuildWizardSteps(wizardState.steps, rebuildArgs);
$scope.customize.proceed("mpm");
}
}, function(error) {
alertService.add({
type: "danger",
message: error,
id: "alertMessages",
closeable: false,
});
});
}, function(error) {
alertService.add({
type: "danger",
message: error,
id: "alertMessages",
closeable: false,
});
});
} else {
pkgResolution.resetCommonVariables();
$scope.customize.selectedPkgs = ea4Data.getData("selectedPkgs");
$scope.customize.processPkgInfoList(rawPkgList);
wizardApi.init();
$scope.customize.proceed("mpm");
}
};
$scope.$on("$viewContentLoaded", function() {
loadPkgInfoData();
});
},
]
);
}
);
/*
# templates/easyapache4/views/mpm.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: false */
define(
'app/views/mpm',[
"angular",
],
function(angular) {
// Retrieve the current application
var app = angular.module("App");
app.controller("mpm",
["$scope",
function($scope) {
$scope.$on("$viewContentLoaded", function() {
$scope.customize.loadData("mpm");
});
}
]
);
}
);
/*
# templates/easyapache4/views/modules.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: false */
define(
'app/views/modules',[
"angular",
],
function(angular) {
// Retrieve the current application
var app = angular.module("App");
app.controller("modules",
["$scope",
function($scope) {
$scope.$on("$viewContentLoaded", function() {
$scope.customize.loadData("modules");
});
}
]
);
}
);
/*
# templates/easyapache4/views/php.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
*/
define(
'app/views/php',[
"angular",
"cjt/util/locale",
],
function(angular, LOCALE) {
// Retrieve the current application
var app = angular.module("App");
app.controller("php",
["$scope", "PAGE", "$sce",
function($scope, PAGE, $sce) {
/**
* Builds the CloudLinux promotion banner and shows it off.
*
* @method setClBanner
*/
var setClBanner = function() {
var productData;
var phpelsProductData;
var productIsLicensed;
$scope.showCLBanner = false;
$scope.promotePhpEls = PAGE.promote_phpels === true;
$scope.promoteCL = PAGE.cl_data.cl_is_supported && !PAGE.cl_data.cl_is_installed;
phpelsProductData = PAGE.phpels_data;
$scope.phpelsHasCustomUrl = phpelsProductData.purchase_phpels_data.is_url;
$scope.phpelsIcon = PAGE.phpels_icon;
productData = PAGE.cl_data;
productIsLicensed = PAGE.cl_licensed === true;
$scope.hasCustomUrl = productData.purchase_cl_data.is_url;
$scope.clIcon = PAGE.cl_icon;
$scope.cl_is_supported = productData.cl_is_supported;
$scope.linkTarget = "_blank";
$scope.purchaseLink = "";
$scope.clActionText = "";
$scope.clBannerText = "";
$scope.clBannerHtml = $sce.trustAsHtml("");
var purchaseCLData = productData.purchase_cl_data;
if (productData.cl_is_supported && !productData.cl_is_installed && !purchaseCLData.disable_upgrade) {
if ( purchaseCLData.server_timeout || (purchaseCLData.error_msg && purchaseCLData.error_msg !== "")) {
$scope.hideUpgradeOption = true;
} else {
$scope.hideUpgradeOption = false;
if (productIsLicensed) {
$scope.purchaseLink = "scripts13/install_cloudlinux_EA4";
$scope.clActionText = LOCALE.maketext("Install [_1]", "CloudLinux");
$scope.linkTarget = "_self"; // No need for popup if staying in WHM
} else {
$scope.purchaseLink = productData.purchase_cl_data.url;
$scope.clActionText = LOCALE.maketext("Upgrade to [_1]", "CloudLinux");
}
}
$scope.purchaseClData = purchaseCLData;
}
var purchaseData = phpelsProductData.purchase_phpels_data;
$scope.phpelsBannerTitle = LOCALE.maketext("[output,strong,Need to keep older PHP versions secure?]");
$scope.phpelsBannerBody = LOCALE.maketext("cPanel provides the latest stable versions of PHP. If you require legacy versions like PHP 5.6 or 7.x, you can keep outdated websites secure with [output,strong,TuxCare Extended Lifecycle Support (ELS)] from CloudLinux. This service provides hardened PHP versions continuously patched against known vulnerabilities, making it the easiest way to maintain security for older PHP versions. [output,url,_1,Learn more.,target,_2]",
"https://go.cpanel.net/about-elsphp",
"blank"
);
$scope.phpelsBannerText = $scope.phpelsBannerTitle + "<br>" + $scope.phpelsBannerBody;
$scope.phpelsBannerHtml = $sce.trustAsHtml($scope.phpelsBannerText);
$scope.clBannerTitle = LOCALE.maketext("[output,strong,Upgrade your server security and performance.]");
$scope.clBannerBody = LOCALE.maketext("[output,strong,CloudLinux OS] is a feature-rich platform solution that addresses multiple server challenges in a single package, including hardened PHP for legacy version, while also delivering enhanced security, performance, and resource isolation across your hosting environment. [output,url,_1,Learn more.,target,_2]",
"https://go.cpanel.net/about-cloudlinux",
"blank"
);
$scope.clBannerText = $scope.clBannerTitle + "<br>" + $scope.clBannerBody;
$scope.clBannerHtml = $sce.trustAsHtml($scope.clBannerText);
$scope.phpelsPurchaseLink = phpelsProductData.purchase_phpels_data.url || "scripts13/purchase_phpels_init_MULTIPHP";
$scope.phpelsActionText = LOCALE.maketext("Buy [_1]", "TuxCare ELS PHP");
$scope.purchasePhpelsData = purchaseData;
if ( $scope.promotePhpEls || $scope.promoteCL ) {
$scope.showCLBanner = true;
}
};
$scope.sendMixPanelEvent = function(eventName) {
var mixpanel = window && window.mixpanel;
if (mixpanel && typeof mixpanel.track === "function") {
mixpanel.track(eventName, {});
}
};
$scope.$on("$viewContentLoaded", function() {
$scope.customize.loadData("php");
setClBanner();
});
},
]
);
}
);
/*
# templates/easyapache4/views/extensions.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: false */
define(
'app/views/extensions',[
"angular"
],
function(angular) {
// Retrieve the current application
var app = angular.module("App");
app.controller("extensions",
["$scope",
function($scope) {
$scope.$on("$viewContentLoaded", function() {
$scope.customize.loadData("extensions");
});
}
]
);
}
);
/*
# templates/easyapache4/views/additionalPackages.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: false */
define(
'app/views/additionalPackages',[
"angular",
],
function(angular) {
"use strict";
// Retrieve the current application
var app = angular.module("App");
app.controller("additionalPackages",
["$scope",
function($scope) {
$scope.$on("$viewContentLoaded", function() {
$scope.customize.loadData("additional");
});
}
]
);
}
);
/*
# templates/easyapache4/views/review.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: false */
define(
'app/views/review',[
"angular",
"lodash",
"app/services/ea4Data",
"app/services/ea4Util"
],
function(angular, _) {
// Retrieve the current application
var app = angular.module("App");
app.controller("review",
[ "$scope", "$location", "ea4Data", "ea4Util", "growlMessages",
function($scope, $location, ea4Data, ea4Util, growlMessages) {
$scope.readyToProvision = false;
$scope.gettingResults = true;
/**
* We can send packages from external apps directly to this view.
* The data should be sent as querystring params.
* 'queryStr' variable will track those params.
*
* querystring param: 'install': to install a package (can be multiple).
* querystring param: 'uninstall': to uninstall a package (can be multiple).
* Example:
* <url>/<cpsesskey>/scripts7/EasyApache4/review?install=mpm-prefork&install=cgi&uninstall=mpm-worker&uninstall=cgid
*/
var queryStr = {};
/**
* Prepares the selected packages to be installed and does an
* API call to resolve those packages.
*
* @method prepareForReview
* @param {Array} A list of packages to be installed.
* @param {String} [Optional] profile id(usually path of a profile).
*/
var prepareForReview = function(packageListForReview, profileId) {
// Get status of each package in the package list
ea4Data.resolvePackages(packageListForReview).then(function(data) {
// Get the packages display format
$scope.installList = ea4Util.getFormattedPackageList(data.install);
$scope.uninstallList = ea4Util.getFormattedPackageList(data.uninstall);
$scope.upgradeList = ea4Util.getFormattedPackageList(data.upgrade);
$scope.existingList = ea4Util.getFormattedPackageList(data.unaffected);
if (!$scope.installList.length && !$scope.upgradeList.length && !$scope.uninstallList.length) {
$scope.noActionRequired = true;
return;
}
// Put all lists into Web Storage
ea4Data.setData(
{
"provisionActions":
{
profileId: profileId,
install: data.install,
uninstall: data.uninstall,
upgrade: data.upgrade
}
});
// Enable the Provision button
$scope.readyToProvision = true;
// Allow provision to run
ea4Data.provisionReady(true);
}, function(error) {
$scope.apiError = true;
$scope.yumErrorMessage = error;
}).finally(function() {
$scope.gettingResults = false;
});
};
/**
* update selectedPackages with new install and/or uninstall
* packages sent through querystring from directly called from
* an external application.
* This helps in by-passing customize steps when we need to install
* few packages required in other applications.
*
* @method updateSelPackagesAndReview
* @param {Object} angular query string object
*/
var updateSelPackagesAndReview = function(qs) {
// 1. Get the current package list.
// 2. Add packages that need to be installed to 'selPkgs'
// 3. Remove packages that need to be uninstalled from 'selPkgs'.
var selPkgs = [];
ea4Data.ea4GetCurrentPkgList().then(function(result) {
if (result.status) {
selPkgs = result.data;
// qs["install"] may have a single string or an array of strings.
var installList = (_.isArray(qs["install"])) ? qs["install"] : [ qs["install"] ];
selPkgs = _.union(selPkgs, installList);
// qs["uninstall"] may have a single string or an array of strings.
var uninstallList = (_.isArray(qs["uninstall"])) ? qs["uninstall"] : [ qs["uninstall"] ];
selPkgs = _.difference(selPkgs, uninstallList);
prepareForReview(selPkgs);
}
});
};
$scope.$on("$viewContentLoaded", function() {
// A list of install/uninstall package set sent through querystring from an external location.
queryStr = $location.search();
if (!_.isEmpty(queryStr) &&
(!_.isEmpty(queryStr["install"]) || !_.isEmpty(queryStr["uninstall"]))) {
updateSelPackagesAndReview(queryStr);
} else {
var customize = ea4Data.getData("customize");
var ea4Update = ea4Data.getData("ea4Update");
if (customize) {
$scope.customize.loadData("review");
prepareForReview(ea4Data.getData("selectedPkgs"));
} else if (ea4Update) {
prepareForReview(ea4Data.getData("selectedPkgs"));
} else {
var selectedProfile = ea4Data.getData("selectedProfile");
if (!selectedProfile) {
ea4Data.cancelOperation();
}
prepareForReview(selectedProfile.pkgs, selectedProfile.fullPath);
}
}
});
$scope.cancel = function() {
ea4Data.cancelOperation();
};
}]);
}
);
/*
# templates/easyapache4/views/provision.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: false */
define(
'app/views/provision',[
"angular",
"cjt/util/locale",
"app/services/ea4Data",
"app/services/ea4Util"
],
function(angular, LOCALE) {
"use strict";
// Retrieve the current application
var app = angular.module("App");
app.controller("provision",
[ "$scope", "$location", "ea4Data", "spinnerAPI",
function($scope, $location, ea4Data, spinnerAPI) {
var realTimeLog = "";
// REFACTOR: One-way binding eligible.
$scope.realTimeLogDisplay = "";
var errorDetected = false;
var startTailing = function() {
ea4Data.tailingLog($scope.buildID, $scope.currentTailingPosition)
.then(function(data) {
// $scope.inErrorMode = false;
var inErrorMode = false;
for (var i = 0, content = data.content; i < content.length; i++) {
// Ignore the beginning and ending lines of log, replace them with more meaningful words
if (content[i] === "-- " + $scope.buildID + " --") {
var startText = LOCALE.maketext("Provision process started.");
realTimeLog += startText + "\r\n";
$scope.realTimeLogDisplay += "<span class='text-success'><strong>" + startText + "</strong></span>\r\n";
continue;
}
if (content[i] === "-- /" + $scope.buildID + " --") {
var endText = LOCALE.maketext("Provision process finished.");
realTimeLog += endText + "\r\n";
$scope.realTimeLogDisplay += "<span class='text-success'><strong>" + endText + "</strong></span>\r\n";
continue;
}
realTimeLog += content[i] + "\r\n";
content[i] = content[i].replace(/&/gm, "&").replace(/</gm, "<").replace(/>/gm, ">").replace(/'/gm, "'").replace(/"/gm, """);
// Detect error messages
if (content[i] === "-- error(" + $scope.buildID + ") --") {
inErrorMode = true;
errorDetected = true;
continue;
}
if (content[i] === "-- /error(" + $scope.buildID + ") --") {
inErrorMode = false;
continue;
}
if (inErrorMode) {
content[i] = "<span class='text-danger'>" + content[i] + "</span>";
}
if (/Error:.*/gm.test(content[i])) {
content[i] = "<span class='text-danger'>" + content[i] + "</span>";
errorDetected = true;
}
$scope.realTimeLogDisplay += content[i] + "\r\n";
}
// Because of the $scope digest, putting 100 ms delay on auto scrolling
// the output window
// TODO: Split this out into a directive to avoid touching the DOM directly
window.setTimeout(function() {
var log = document.getElementById("log");
if (log) {
log.scrollTop = log.scrollHeight;
}
}, 100);
$scope.currentTailingPosition = data.offset;
if (data.still_running) {
window.setTimeout(startTailing(), 100);
} else {
spinnerAPI.stop("provisionSpinner");
$scope.finished = true;
ea4Data.provisionReady(false);
}
});
};
var startProvision = function(provisionActions) {
spinnerAPI.start("provisionSpinner");
$scope.provisionStarted = true;
errorDetected = false;
ea4Data.doProvision(provisionActions.install,
provisionActions.uninstall,
provisionActions.upgrade,
provisionActions.profileId)
.then(function(data) {
// TODO: see if this shud be in scope
$scope.buildID = data.build;
// TODO: see if this shud be in scope
$scope.currentTailingPosition = 0;
startTailing();
}).finally(function() {
// every time we provision we are getting updates
// so we reset the update button state
$scope.customize.checkUpdateInfo.pkgNumber = 0;
$scope.customize.toggleUpdateButton();
ea4Data.clearEA4LocalStorageItems();
app.firstLoad = false;
ea4Data.php_set_session_save_path();
});
};
$scope.$on("$viewContentLoaded", function() {
// Reset wizard attributes.
$scope.customize.wizard.currentStep = "";
$scope.customize.wizard.showWizard = false;
var provisionActions = ea4Data.getData("provisionActions");
if (!ea4Data.provisionReady() ||
(typeof provisionActions === "undefined")) {
ea4Data.cancelOperation();
}
// REFACTOR: THIS part needs to be re-visited when working
// on using latest tail log method.
var hash = $location.hash();
if (hash === "bottom") {
startProvision(provisionActions);
} else {
$location.hash("bottom");
}
});
$scope.cancel = function() {
ea4Data.cancelOperation();
};
$scope.resultReady = function() {
var result = null;
if (!errorDetected) {
result = "alert-success";
$scope.resultSummary = LOCALE.maketext("The provision process is complete.");
} else {
result = "alert-danger";
$scope.resultSummary = LOCALE.maketext("The provision process exited with errors. Please check the log for details.");
}
return result;
};
var destroyClickedElement = function(event) {
// remove the link from the DOM
document.body.removeChild(event.target);
};
// REFACTOR: Need to be re-written.
$scope.saveLog = function() {
// grab the content of the form field and place it into a variable
var textToWrite = realTimeLog;
var textFileAsBlob = new Blob([textToWrite], { type: "text/plain" });
var fileNameToSaveAs = "log.txt";
var downloadLink = document.createElement("a");
downloadLink.download = fileNameToSaveAs;
downloadLink.innerHTML = "My Hidden Link";
window.URL = window.URL || window.webkitURL;
downloadLink.href = window.URL.createObjectURL(textFileAsBlob);
downloadLink.onclick = destroyClickedElement;
downloadLink.style.display = "none";
document.body.appendChild(downloadLink);
downloadLink.click();
};
}
]
);
}
);
/*
# templates/easyapache4/views/ruby.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: false */
define(
'app/views/ruby',[
"angular"
],
function(angular) {
// Retrieve the current application
var app = angular.module("App");
app.controller("ruby",
["$scope",
function($scope) {
$scope.$on("$viewContentLoaded", function() {
$scope.customize.loadData("ruby");
});
}
]
);
}
);
/*
# cpanel - whostmgr/docroot/templates/easyapache4/index.js
# Copyright 2022 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/index',[
"angular",
"cjt/core",
"lodash",
"cjt/modules",
],
function(angular, CJT, _) {
"use strict";
return function() {
// First create the application
angular.module("App", [
"cjt2.config.whm.configProvider", // This needs to load first
"ngRoute",
"ui.bootstrap",
"cjt2.whm",
"angular-growl",
"whm.easyapache4.ea4Util",
"whm.easyapache4.ea4Data",
"whm.easyapache4.pkgResolution",
"whm.easyapache4.wizardApi",
]);
// Then load the application dependencies
var appModule = require(
[
"cjt/bootstrap",
"cjt/util/locale",
// Application Modules
"cjt/directives/toggleSwitchDirective",
"cjt/directives/searchDirective",
"app/directives/eaWizard",
"app/directives/saveAsProfile",
"app/services/ea4Data",
"app/services/ea4Util",
"app/services/pkgResolution",
"app/services/wizardApi",
"cjt/views/applicationController",
"app/views/profile",
"app/views/yumUpdate",
"app/views/customize",
"app/views/loadPackages",
"app/views/mpm",
"app/views/modules",
"app/views/php",
"app/views/extensions",
"app/views/additionalPackages",
"app/views/review",
"app/views/provision",
"app/views/ruby",
], function(BOOTSTRAP, LOCALE) {
var app = angular.module("App");
app.value("PAGE", PAGE);
// REFACTOR: This can be sent into ea4Data service.
app.firstLoad = true;
var wizardState = {};
app.value("wizardState", wizardState);
app.config(["$routeProvider", "$compileProvider",
function($routeProvider, $compileProvider) {
// Setup the routes
$routeProvider
.when("/profile", {
controller: "profile",
templateUrl: CJT.buildFullPath("easyapache4/views/profile.ptt"),
})
.when("/loadPackages", {
controller: "loadPackages",
templateUrl: CJT.buildFullPath("easyapache4/views/loadPackages.ptt"),
})
.when("/mpm", {
controller: "mpm",
templateUrl: CJT.buildFullPath("templates/easyapache4/views/mpm.phtml"),
})
.when("/modules", {
controller: "modules",
templateUrl: CJT.buildFullPath("templates/easyapache4/views/modules.phtml"),
})
.when("/php", {
controller: "php",
templateUrl: CJT.buildFullPath("easyapache4/views/php.ptt"),
})
.when("/extensions", {
controller: "extensions",
templateUrl: CJT.buildFullPath("templates/easyapache4/views/extensions.phtml"),
})
.when("/additional", {
controller: "additionalPackages",
templateUrl: CJT.buildFullPath("templates/easyapache4/views/additionalPackages.phtml"),
})
.when("/review", {
controller: "review",
templateUrl: CJT.buildFullPath("easyapache4/views/review.ptt"),
})
.when("/ruby", {
controller: "ruby",
templateUrl: CJT.buildFullPath("templates/easyapache4/views/ruby.phtml"),
})
.when("/provision", {
controller: "provision",
templateUrl: CJT.buildFullPath("easyapache4/views/provision.ptt"),
})
.when("/yumUpdate", {
controller: "yumUpdate",
templateUrl: CJT.buildFullPath("easyapache4/views/yumUpdate.ptt"),
})
.otherwise({
"redirectTo": "/profile",
});
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/);
},
]);
app.run(["$rootScope", "$location", "ea4Util", "wizardApi", "wizardState", function($rootScope, $location, ea4Util, wizardApi, wizardState) {
if (_.isEmpty(wizardState)) {
wizardApi.init();
ea4Util.hideFooter();
}
// register listener to watch route changes
$rootScope.$on("$routeChangeStart", function() {
$rootScope.currentRoute = $location.path();
});
}]);
BOOTSTRAP(document);
}
);
return appModule;
};
}
);