/*
* user_manager/directives/issueList.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/issueList',[
"angular",
"cjt/util/locale"
],
function(angular, LOCALE) {
/**
* This directive renders a list of issues using a common template.
* Use the "issues" attribute to bind to an array of issue objects.
*
* Example:
* <cp-issue-list issues="user.issues"></cp-issue-list>
*
* Example with an id prefix:
* <li ng-repeat="user in users">
* <span>user.name</span>
* <cp-issue-list issues="user.issues" id-prefix="{{ $index }}"></cp-issue-list>
* </li>
*/
angular.module("App").directive("cpIssueList", [
function() {
var counter = 0;
return {
templateUrl: "directives/issueList.phtml",
scope: {
issues: "=", // The model. An array of issue objects.
idPrefix: "@" // Optional prefix for the generated IDs.
},
link: function(scope, elem, attrs) {
if (angular.isDefined(scope.issues) && !angular.isArray(scope.issues)) {
throw new TypeError("The issues attribute should evaluate to an array of issue objects.");
}
// Provide an automatically generated prefix if one is not provided.
scope.$watch("idPrefix", function(newVal) {
if (!newVal && newVal !== 0) {
scope.idPrefix = counter++;
}
});
/**
* Gets the best title for an issue.
* @param {Object} issue The issue object.
* @return {String} The full title string for the issue.
*/
scope.getIssueTitle = function(issue) {
if (issue.title) {
return issue.title;
}
if (issue.area === "quota") {
switch (issue.service) {
case "email":
return (issue.type === "error") ? LOCALE.maketext("Mail Quota Reached:") : LOCALE.maketext("Mail Quota Warning:");
case "ftp":
return (issue.type === "error") ? LOCALE.maketext("[asis,FTP] Quota Reached:") : LOCALE.maketext("[asis,FTP] Quota Warning:");
}
} else {
return (issue.type === "error") ? LOCALE.maketext("Error:") : LOCALE.maketext("Warning:");
}
};
}
};
}
]);
}
);
/*
* user_manager/directives/modelToLowerCase.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/modelToLowerCase',[
"angular",
],
function(angular) {
/**
* This directive simply adds a parser to transform input into lowercase before saving it to the model.
*
* Example: <input ng-model="myModel" model-to-lower-case>
*/
angular.module("App").directive("modelToLowerCase", [
function() {
return {
restrict: "A",
require: "ngModel",
link: function(scope, elem, attrs, ngModel) {
ngModel.$parsers.unshift(function(viewVal) {
return viewVal.toLocaleLowerCase();
});
}
};
}
]);
}
);
/* global define: false */
define(
'app/services/userService',[
// Libraries
"angular",
"lodash",
"jquery",
// CJT
"cjt/util/locale",
"cjt/io/api",
"cjt/io/uapi-request",
"cjt/io/uapi", // IMPORTANT: Load the driver so its ready
"cjt/util/parse",
"cjt/util/flatObject",
// Angular components
"cjt/services/APIService"
],
function(angular, _, $, LOCALE, API, APIREQUEST, APIDRIVER, PARSER, FLAT) {
// Fetch the current application
var app = angular.module("App");
var lastRequest_jqXHR;
/**
* Setup the domainlist models API service
*/
app.factory("userService", [
"$q",
"APIService",
"emailDaemonInfo",
"ftpDaemonInfo",
"webdiskDaemonInfo",
"features",
"defaultInfo",
function(
$q,
APIService,
emailDaemonInfo,
ftpDaemonInfo,
webdiskDaemonInfo,
features,
defaultInfo
) {
/**
* Metadata, including a quick lookup of what actions are supported on specific services.
*/
var modifiers = {
email: {
supports: {
serviceRunning: emailDaemonInfo.enabled,
allowed: features.email,
createable: features.email && emailDaemonInfo.enabled,
editable: features.email,
deletable: features.email,
viewable: true
},
name: "email"
},
ftp: {
supports: {
serviceRunning: ftpDaemonInfo.enabled,
allowed: features.ftp,
createable: features.ftp && ftpDaemonInfo.enabled,
editable: features.ftp,
deletable: features.ftp,
viewable: true
},
name: "ftp"
},
webdisk: {
supports: {
serviceRunning: webdiskDaemonInfo.enabled,
allowed: features.webdisk,
createable: features.webdisk && webdiskDaemonInfo.enabled,
editable: features.webdisk,
deletable: features.webdisk,
viewable: true
},
name: "webdisk"
}
};
/**
* Helper method to make adjustment to the user for the application
* @param {Object} user User or Candidate User
*/
function decorateUser(user) {
user.ui = {};
// Set the typeLabel
user.typeLabel = typeLabels[user.type];
}
/**
* Documentation-approved terminology for the different account types.
*/
var typeLabels = {
service: LOCALE.maketext("Service Account"),
hypothetical: LOCALE.maketext("Hypothetical Subaccount"),
sub: LOCALE.maketext("Subaccount"),
cpanel: LOCALE.maketext("cPanel Account")
};
/**
* Proper names for the various services.
*/
var serviceLabels = {
ftp: LOCALE.maketext("FTP"),
email: LOCALE.maketext("Email"),
webdisk: LOCALE.maketext("Web Disk")
};
/**
* Build the search keys field from the nested services field.
* @param {Object} user
* @return {String}
*/
function buildServiceSearchField(user) {
var search = [];
if (user.services.email.enabled) {
search.push("email");
}
if (user.services.ftp.enabled) {
search.push("ftp");
}
if (user.services.webdisk.enabled) {
search.push("webdisk webdav");
}
return search.join(" ");
}
/**
* Some account types don't have GUIDs, so we need to make a unique identifier for them.
* @param {Object} user A user object.
* @return {String} The unique string to be used as the GUID.
*/
function _generateGuid(user) {
if (user.service) {
// Service accounts and merge candidates have this set
return (user.full_username + ":" + user.service);
} else {
return (user.full_username + ":" + user.type);
}
}
/**
* Clean up the service object
* @param {Object} service Raw server service object.
* @param {Object} modifiers Additional metadata to be added for this service type.
* @return {Object} Cleanded up and decorated service ready for use in the application.
*/
function adjustService(service, modifiers) {
service.enabled = PARSER.parsePerlBoolean(service.enabled);
service.isNew = !service.enabled;
_.extend(service, _.cloneDeep(modifiers));
if (angular.isString(service.quota)) {
service.quota = PARSER.parseInteger(service.quota);
}
if (!angular.isUndefined(service.enabledigest)) {
service.enabledigest = PARSER.parsePerlBoolean(service.enabledigest);
}
}
/**
* Clean up the user
* @param {Object} user Raw server user object
* @return {Object} Cleaned up user object ready for use in the application.
*/
function adjustUser(user) {
// Normalize the booleans
var services = _.keys(user.services);
_.each(services, function(serviceName) {
adjustService(user.services[serviceName], modifiers[serviceName]);
});
user.can_delete = PARSER.parsePerlBoolean(user.can_delete);
user.can_set_quota = PARSER.parsePerlBoolean(user.can_set_quota);
user.can_set_password = PARSER.parsePerlBoolean(user.can_set_password);
user.special = PARSER.parsePerlBoolean(user.special);
user.synced_password = PARSER.parsePerlBoolean(user.synced_password);
user.sub_account_exists = PARSER.parsePerlBoolean(user.sub_account_exists);
user.has_siblings = PARSER.parsePerlBoolean(user.has_siblings);
user.dismissed = PARSER.parsePerlBoolean(user.dismissed);
user.has_invite = PARSER.parsePerlBoolean(user.has_invite);
user.has_expired_invite = PARSER.parsePerlBoolean(user.has_expired_invite);
// Set the formatted type label
user.typeLabel = typeLabels[user.type];
// Clean up the candidates
if (user.type === "hypothetical" || user.type === "sub") {
user.candidate_issues_count = 0;
user.serviceSearch = [];
if (user.dismissed_merge_candidates) {
user.dismissed_merge_candidates.forEach(function(candidate) {
angular.forEach(candidate.services, function(service, serviceName) {
adjustService(service, modifiers[serviceName]);
if (service.enabled) {
candidate.service = serviceName;
}
});
});
}
for (var j = 0, jl = user.merge_candidates.length; j < jl; j++) {
var candidate = user.merge_candidates[j];
// Normalize the booleans
candidate.can_delete = PARSER.parsePerlBoolean(candidate.can_delete);
candidate.can_set_quota = PARSER.parsePerlBoolean(candidate.can_set_quota);
candidate.can_set_password = PARSER.parsePerlBoolean(candidate.can_set_password);
candidate.sub_account_exists = PARSER.parsePerlBoolean(candidate.sub_account_exists);
candidate.has_siblings = PARSER.parsePerlBoolean(candidate.has_siblings);
candidate.dismissed = PARSER.parsePerlBoolean(candidate.dismissed);
for (var serviceName in candidate.services) {
if (candidate.services.hasOwnProperty(serviceName)) {
adjustService(candidate.services[serviceName], modifiers[serviceName]);
// Annotate the hypothetical/sub services to match the collected child
// services state to make search easier at the top level.
if (candidate.services[serviceName].enabled) {
user.services[serviceName].enabledInCandidate = true;
candidate.service = serviceName;
}
}
}
if (candidate.issues.length > 0) {
user.candidate_issues_count++;
}
// Set the formatted labels
candidate.typeLabel = typeLabels[candidate.type];
candidate.serviceLabel = serviceLabels[candidate.service];
// Create a synthetic field to make search
// by service string work.
candidate.serviceSearch = buildServiceSearchField(candidate);
user.serviceSearch.push(candidate.serviceSearch);
// Candidates are independent service accounts, so they won't have GUIDs
candidate.guid = _generateGuid(candidate);
}
} else if (user.type === "service") {
// Provide an easy lookup for the type of service like we do for merge candidates
services.some(function(service) {
if (user.services[service].enabled) {
user.service = service;
return true;
}
});
}
if (user.guid === null) {
user.guid = _generateGuid(user);
}
// Create a synthetic top level field to make search
// by service string work.
if (user.serviceSearch) {
user.serviceSearch.push( buildServiceSearchField(user) );
user.serviceSearch = user.serviceSearch.join(" ");
} else {
user.serviceSearch = buildServiceSearchField(user);
}
decorateUser(user);
if (user.merge_candidates) {
user.merge_candidates.forEach(decorateUser);
}
return user;
}
/**
* Extends a consolidated service object and adds additional services from a services
* object. These usually come from a user.services property.
*
* @method _extendConsolidatedServices
* @private
* @param {Object} destination The consolidated object to extend.
* @param {Object} source The services object to incorporate into the destination object.
* @param {Boolean} isDismissed Set to true if the services object comes from a dismissed merge candidate.
* @return {Object} The destination object.
*/
function _extendConsolidatedServices(destination, source, isDismissed) {
var services = Object.keys(source);
services.some(function(service) {
if (source[service].enabled) {
destination[service] = source[service];
destination[service].isCandidate = true;
if (isDismissed) {
destination[service].isDismissed = true;
}
return true;
}
});
return destination;
}
/**
* Takes the merge_candidates and dismissed_merge_candidates from a user and returns
* a consolidated service object with all of the relevant candidate services.
*
* @method consolidateCandidateServices
* @param {Object} user The user object to process.
* @param {Boolean} includeDismissed If true, dismissed_merge_candidates will also be included.
* @return {Object} The consolidated services object. The object only contains keys for
* services that are enabled on the merge candidates, so if there are
* no candidates (dismissed or otherwise) with FTP enabled the "ftp"
* will not exist at all.
*/
function consolidateCandidateServices(user, includeDismissed) {
var consolidatedCandidateServices = {};
user.merge_candidates.forEach(function(candidate) {
_extendConsolidatedServices(consolidatedCandidateServices, candidate.services);
});
if (includeDismissed) {
user.dismissed_merge_candidates.forEach(function(candidate) {
_extendConsolidatedServices(consolidatedCandidateServices, candidate.services, true);
});
}
return consolidatedCandidateServices;
}
/**
* Converts the response to our application data structure
* @param {Object} response
* @return {Object} Sanitized data structure.
*/
function convertResponseToList(response) {
var items = [];
if (response.data) {
var data = response.data;
for (var i = 0, length = data.length; i < length; i++) {
var user = adjustUser(data[i]);
items.push(user);
}
var totalItems = response.meta && response.meta.paginate && response.meta.paginate.is_paged ? response.meta.paginate.total_records : data.length;
return {
items: items,
totalItems: totalItems,
};
} else {
return {
items: [],
totalItems: 0,
};
}
}
// Fields to remove from the user
// before posting for edit call.
var NOT_FOR_POST_USER = [
"can_delete",
"can_set_quota",
"can_set_password",
"candidate_issues_count",
"issues",
"serviceSearch",
"merge_candidates",
"special",
"synced_password",
"sub_account_exists",
"has_siblings",
"parent_type",
"dismissed",
"dismissed_merge_candidates",
"has_invite",
"has_expired_invite",
"name",
"isNew"
];
/**
* Clean up the user so it can be posted back to the server.
* @param {Object} user
* @return {Object} Cleaned up user.
*/
function cleanUserForPost(user) {
var tmp = JSON.parse(JSON.stringify(user));
NOT_FOR_POST_USER.forEach(function(name) {
delete tmp[name];
});
var services = _.keys(tmp.services);
_.each(services, function(service) {
if (tmp.services[service].isCandidate) {
delete tmp.services[service];
} else {
tmp.services[service].enabled = tmp.services[service].enabled ? 1 : 0;
if (!angular.isUndefined(tmp.services[service].enabledigest)) {
tmp.services[service].enabledigest = tmp.services[service].enabledigest ? 1 : 0;
}
delete tmp.services[service].supports;
}
});
return FLAT.flatten(tmp);
}
/**
* Generates an empty user data structure.
* @return {Object}
*/
function _emptyUser() {
return {
username: "",
domain: "",
real_name: "",
alternate_email: "",
phone_number: "",
avatar_url: "",
services: {
email: {
name: modifiers.name,
enabled: false,
isNew: true,
quota: defaultInfo.email.default_value,
quotaUnit: "MB",
supports: modifiers.email.supports
},
ftp: {
name: modifiers.name,
enabled: false,
isNew: true,
quota: defaultInfo.ftp.default_value,
quotaUnit: "MB",
homedir: "public_html/",
supports: modifiers.ftp.supports
},
webdisk: {
name: modifiers.name,
enabled: false,
isNew: true,
homedir: "public_html/",
perms: "rw",
supports: modifiers.webdisk.supports,
enabledigest: false
}
}
};
}
/**
* Back fill the missing components for the user.
*
* @param {Object} user User as it exists on the backend.
* @return {Object} User with missing fields added and updated as needed.
*/
function _backfillUser(user) {
var u = _emptyUser();
$.extend(true, u, user);
if (!u.services.ftp.enabled) {
u.services.ftp.homedir += u.domain + "/" + u.username;
}
if (!u.services.webdisk.enabled) {
u.services.webdisk.homedir += u.domain + "/" + u.username;
}
return u;
}
// Set up the service's constructor and parent
var UserListService = function() {};
UserListService.prototype = new APIService();
// Extend the prototype with any class-specific functionality
angular.extend(UserListService.prototype, {
/**
* Generates an empty user data structure.
* @return {Object}
* {string} username
* {string} domain
* {string} real_name
* {string} alternate_email
* {string} phone_number
* {string} avatar_url
* {Object} services
* {Object} email
* {Boolean} enabled - true if the user has an email account associated, false otherwise
* {Number} quota - 0 for unlimited, otherwise in megabytes
* {Object} ftp
* {Boolean} enabled - true if the user has an ftp account associated, false otherwise
* {Number} quota - 0 for unlimited, otherwise in megabytes
* {String} homedir - directory where ftp user files are stored.
* {Object} webdisk
* {Boolean} enabled - true if the user has an webdisk account associated, false otherwise
* {String} homedir - directory where webdisk user files are stored.
* {???} perms - ??? RO, RW ???
*/
emptyUser: _emptyUser,
/**
* Back fill the missing components for the user.
*
* @param {Object} user User as it exists on the backend.
* @return {Object} User with missing fields added and updated as needed.
*/
backfillUser: _backfillUser,
/**
* Get a list domains that match the selection criteria passed in meta parameter
*
* @param {boolean} flat if true will flatten hypothetical users, if false will render hypothetical users.
* @param {object} meta Optional meta data to control sorting, filtering and paging
* @param {string} meta.sortBy Name of the field to sort by
* @param {string} meta.sortDirection asc or desc
* @param {string} meta.sortType Optional name of the sort rule to apply to the sorting
* @param {string} meta.filterBy Name of the filed to filter by
* @param {string} meta.filterCompare Optional comparator to use when comparing for filter.
* If not provided, will default to ???.
* May be one of:
* TODO: Need a list of valid filter types.
* @param {string} meta.filterValue Expression/argument to pass to the compare method.
* @param {string} meta.pageNumber Page number to fetch.
* @param {string} meta.pageSize Size of a page, will default to 10 if not provided.
* @return {Promise} Promise that will fulfill the request.
*/
fetchList: function(flat, meta) {
meta = meta || {};
var apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "list_users");
apiCall.addArgument("flat", flat ? 1 : 0);
if (meta.sortBy) {
meta.sortDirection = meta.sortDirection || "asc";
apiCall.addSorting(meta.sortBy, meta.sortDirection, meta.sortType);
}
var deferred = this.deferred(apiCall, {
transformAPISuccess: convertResponseToList
});
// pass the promise back to the controller
return deferred.promise;
},
/**
* Fetch a single user by its guid.
*
* @param {String} guid Unique identifier for the user
* @return {Promise} Promise that will fulfill the request for this user.
*/
fetchUser: function(guid) {
var apiCall = new APIREQUEST.Class();
// TODO: Replace with a more efficient single lookup call.
apiCall.initialize("UserManager", "lookup_user");
apiCall.addArgument("guid", guid);
var deferred = this.deferred(apiCall, {
transformAPISuccess: function(response) {
// The lookup_user api returns the matching user in the response, so we can
// clean it up and send it back to the promise handlers.
response.data = _backfillUser(adjustUser(response.data));
response.data.candidate_services = consolidateCandidateServices(response.data, true);
return response.data;
}
});
// pass the promise back to the controller
return deferred.promise;
},
/**
* Fetch a single service account by its type and full username (user@domain)
* @param {String} type email, ftp, webdisk
* @param {String} full_username user@domain
* @return {Promise} Promise that will fulfill the request for the service account.
*/
fetchService: function(type, full_username) {
var apiCall = new APIREQUEST.Class();
// TODO: Replace with a more efficient single lookup call.
apiCall.initialize("UserManager", "lookup_service_account");
apiCall.addArgument("type", type);
apiCall.addArgument("full_username", full_username);
var deferred = this.deferred(apiCall, {
transformAPISuccess: function(response) {
// The lookup_user api returns the matching user in the response, so we can
// clean it up and send it back to the promise handlers.
return _backfillUser(adjustUser(response.data));
}
});
// pass the promise back to the controller
return deferred.promise;
},
/**
* Delete a user/service account from the system.
* @param {Object} user As returned by the fetchList() api.
* @return {Promise} Promise that will fulfill the request.
*/
delete: function(user) {
var apiCall;
var promise;
if ("sub" === user.type) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "delete_user");
apiCall.addArgument("username", user.username);
apiCall.addArgument("domain", user.domain);
var deferred = this.deferred(apiCall, {
transformAPISuccess: function(response) {
if (response.data) {
response.data = adjustUser(response.data);
}
return response;
}
} );
promise = deferred.promise;
} else if ("service" === user.type) {
if (user.services.email.enabled) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("Email", "delete_pop");
apiCall.addArgument("email", user.full_username);
promise = this.deferred(apiCall).promise;
} else if (user.services.ftp.enabled) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("Ftp", "delete_ftp");
apiCall.addArgument("user", user.full_username);
apiCall.addArgument("destroy", 0);
promise = this.deferred(apiCall).promise;
} else if (user.services.webdisk.enabled) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("WebDisk", "delete_user");
apiCall.addArgument("user", user.full_username);
apiCall.addArgument("destroy", 0);
promise = this.deferred(apiCall).promise;
} else {
promise = $q(function(resolve, reject) {
reject(LOCALE.maketext("The system could not determine the service type for the “[_1]” service account.", user.full_username));
});
}
} else {
promise = $q(function(resolve, reject) {
reject(LOCALE.maketext("The system could not delete the “[_1]” account. You cannot delete the “[_2]” account type.", user.full_username, user.type));
});
}
// pass the promise back to the controller
return promise;
},
/**
* Performs the link and dismiss operations on any merge candidate services
* that have been flagged with willLink or willDismiss.
*
* @method linkAndDismiss
* @param {Object} user The user whose candidate services will be processed.
* @param {Object} [services] Optional. Alternative services object to use instead of
* user.services for the case where you want to link and
* dismiss based on the services object from a view model
* instead of an actual user model returned from the server.
* @return {Promise} Resolves with the user as returned from the server.
* Rejects with an object instead of just the error message to
* provide context as to which call (link or dismiss) failed.
*/
linkAndDismiss: function(user, services) {
// Gather lists of all services to be linked/dismissed
var dismissedServices = [];
var linkedServices = [];
angular.forEach((services || user.services), function(service, serviceName) {
if (!service.isCandidate) {
return;
} else if (service.willLink && service.willDismiss) {
throw "Developer Error: You cannot link and dismiss the same service account.";
} else if (service.willLink) {
linkedServices.push(serviceName);
} else if (service.willDismiss) {
dismissedServices.push(serviceName);
}
});
// Multiple links can be combined, as can multiple dismissals, so we'll have a max of 2 discrete API calls.
var apiCall, promise;
var promises = [];
// Dispatch the link API call
if (linkedServices.length) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "merge_service_account");
apiCall.addArgument("username", user.username);
apiCall.addArgument("domain", user.domain);
linkedServices.forEach(function(serviceName) {
apiCall.addArgument("services." + serviceName + ".merge", 1);
});
promise = this.deferred(apiCall, {
transformAPISuccess: function(response) {
return adjustUser(response.data);
},
transformAPIFailure: function(response) {
return {
error: response.error,
call: "link"
};
}
}).promise;
promises.push(promise);
}
// Dispatch the dismiss API call
if (dismissedServices.length) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "dismiss_merge");
apiCall.addArgument("username", user.username);
apiCall.addArgument("domain", user.domain);
dismissedServices.forEach(function(serviceName) {
apiCall.addArgument("services." + serviceName + ".dismiss", 1);
});
promise = this.deferred(apiCall, {
transformAPISuccess: function(response) {
return response.data;
},
transformAPIFailure: function(response) {
return {
error: response.error,
call: "link"
};
}
}).promise;
promises.push(promise);
}
var self = this;
return $q.all(promises).then(function(results) {
if (!results.length) {
// Nothing was done, so just return the original user.
return user;
} else {
// We can't be sure which user is the most up to date, so we'll just fetch it again.
return self.fetchUser(user.guid).then(function(fetchedUser) {
fetchedUser.dismissed_services = dismissedServices;
fetchedUser.linked_services = linkedServices;
return fetchedUser;
});
}
}).catch(function(error) {
return $q(function(resolve, reject) {
self.fetchUser(user.guid).then(function(fetchedUser) {
error.user = fetchedUser;
reject(error);
});
});
});
},
/**
* Create a user on the backend.
* @param {Object} user Definition of the user to be created.
* @return {Promise} When fulfilled, will have created the user or returned an error.
*/
create: function(user) {
var apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "create_user");
apiCall.addArgument("username", user.username);
apiCall.addArgument("domain", user.domain);
apiCall.addArgument("real_name", user.fullName);
apiCall.addArgument("alternate_email", user.recoveryEmail);
// If we're using the invite system, there's no need for a password. And vice-versa.
if (user.sendInvite) {
apiCall.addArgument("send_invite", 1);
} else {
apiCall.addArgument("password", user.password);
}
if (features.email && !user.services.email.isCandidate) {
apiCall.addArgument("services.email.enabled", user.services.email.enabled ? 1 : 0);
apiCall.addArgument("services.email.quota", user.services.email.quota);
}
if (features.ftp && !user.services.ftp.isCandidate) {
apiCall.addArgument("services.ftp.enabled", user.services.ftp.enabled ? 1 : 0);
if (ftpDaemonInfo.supports.quota) {
apiCall.addArgument("services.ftp.quota", user.services.ftp.quota);
}
apiCall.addArgument("services.ftp.homedir", user.services.ftp.homedir);
}
if (features.webdisk && !user.services.webdisk.isCandidate) {
apiCall.addArgument("services.webdisk.enabled", user.services.webdisk.enabled ? 1 : 0);
apiCall.addArgument("services.webdisk.homedir", user.services.webdisk.homedir);
apiCall.addArgument("services.webdisk.perms", user.services.webdisk.perms);
apiCall.addArgument("services.webdisk.enabledigest", user.services.webdisk.enabledigest ? 1 : 0);
}
var self = this;
return this.deferred(apiCall, {
transformAPISuccess: function(response) {
// The create api returns the new user in the response, so we can
// clean it up and send it back to the promise handlers.
return adjustUser(response.data);
}
}).promise.then(function(createResponse) {
return self.linkAndDismiss(createResponse, user.services);
});
},
/**
* Edit an existing user.
* @param {Object} user Definition of the user to be modified.
* @return {Promise} When fulfilled, will have modified the user or returned an error.
*/
edit: function(user) {
var apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "edit_user");
var cleanUser = cleanUserForPost(user);
for (var attribute in cleanUser) {
if (cleanUser.hasOwnProperty(attribute)) {
apiCall.addArgument(attribute, cleanUser[attribute]);
}
}
var deferred = this.deferred(apiCall, {
transformAPISuccess: function(response) {
// The edit api returns the new user in the response, so we can
// clean it up and send it back to the promise handlers.
return adjustUser(response.data);
}
});
return deferred.promise;
},
/**
* Edit the settings for an independent service account.
* @param {Object} user The desired end state of the account.
* @param {Object} originalService The original service configuration.
* @return {Promise} Resolves when the edit is succuessful. Rejects otherwise.
*/
editService: function(user, originalService) {
var apiCall,
promise,
promises = [];
// Email
if (user.services.email.enabled) {
if (user.services.email.quota !== originalService.quota) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("Email", "edit_pop_quota");
apiCall.addArgument("email", user.username);
apiCall.addArgument("domain", user.domain);
apiCall.addArgument("quota", user.services.email.quota);
promise = this.deferred(apiCall).promise;
promises.push(promise);
}
if (user.password) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("Email", "passwd_pop");
apiCall.addArgument("email", user.username);
apiCall.addArgument("domain", user.domain);
apiCall.addArgument("password", user.password);
promise = this.deferred(apiCall).promise;
promises.push(promise);
}
} else if (user.services.ftp.enabled) { // Ftp
if (user.services.ftp.quota !== originalService.quota) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("Ftp", "set_quota");
apiCall.addArgument("user", user.username);
apiCall.addArgument("domain", user.domain);
apiCall.addArgument("quota", user.services.ftp.quota);
promise = this.deferred(apiCall).promise;
promises.push(promise);
}
if (user.services.ftp.homedir !== originalService.homedir) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("Ftp", "set_homedir");
apiCall.addArgument("user", user.username);
apiCall.addArgument("domain", user.domain);
apiCall.addArgument("homedir", user.services.ftp.homedir);
promise = this.deferred(apiCall).promise;
promises.push(promise);
}
if (user.password) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("Ftp", "passwd");
apiCall.addArgument("user", user.username);
apiCall.addArgument("domain", user.domain);
apiCall.addArgument("pass", user.password);
promise = this.deferred(apiCall).promise;
promises.push(promise);
}
} else if (user.services.webdisk.enabled) { // Web Disk
if (user.services.webdisk.homedir !== originalService.homedir) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("WebDisk", "set_homedir");
apiCall.addArgument("user", user.full_username);
apiCall.addArgument("homedir", user.services.webdisk.homedir);
promise = this.deferred(apiCall).promise;
promises.push(promise);
}
if (user.services.webdisk.perms !== originalService.perms) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("WebDisk", "set_permissions");
apiCall.addArgument("user", user.full_username);
apiCall.addArgument("perms", user.services.webdisk.perms);
promise = this.deferred(apiCall).promise;
promises.push(promise);
}
if (user.password) {
apiCall = new APIREQUEST.Class();
apiCall.initialize("WebDisk", "set_password");
apiCall.addArgument("user", user.full_username);
apiCall.addArgument("password", user.password);
apiCall.addArgument("enabledigest", user.services.webdisk.enabledigest ? 1 : 0);
promise = this.deferred(apiCall).promise;
promises.push(promise);
}
if (!user.password && (user.services.webdisk.enabledigest !== originalService.enabledigest)) {
// TODO: We don't have a way to do this at this time without the password.
apiCall = new APIREQUEST.Class();
// promise = this.deferred(apiCall).promise;
// promises.push(promise);
}
} else { // Fallback
promise = $q(function(resolve, reject) {
reject(LOCALE.maketext("The system detected an unknown service for the “[_1]” service account.", user.full_username));
});
promises.push(promise);
}
return $q.all(promises);
},
/**
* Helper method that calls convertResponseToList to prepare the data structure
* @param {Object} response
* @return {Object} Sanitized data structure.
*/
prepareList: function(response) {
if (response.status) {
return convertResponseToList(response);
} else {
throw response.errors;
}
},
/**
* Link a service account to a sub-account of the same name.
* @param {Object} user Definition of the service account to be linked.
* @param {String} [type] Name of the one service we want. If missing will link all enabled services.
* @param {Boolean} [forceLink] Forces a link, even if the service is not enabled on the user object. You
* must provide a type to use this.
* @return {Promise} When fulfilled, will have linked the service account or returned an error.
*/
link: function(user, type, forceLink) {
var apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "merge_service_account");
apiCall.addArgument("username", user.username);
apiCall.addArgument("domain", user.domain);
if (type) {
if (user.services[type].enabled || forceLink) {
apiCall.addArgument("services." + type + ".merge", 1);
}
} else {
for (var serviceName in user.services) {
if ( user.services.hasOwnProperty(serviceName) &&
user.services[serviceName].enabled
) {
apiCall.addArgument("services." + serviceName + ".merge", 1);
}
}
}
var deferred = this.deferred(apiCall, {
transformAPISuccess: function(response) {
return adjustUser(response.data);
}
});
return deferred.promise;
},
/**
* Unlink a service account from a sub-account.
*
* @method unlink
* @param {Object} user Definition of the subaccount from which to unlink a service
* @param {String} serviceType The name of the service to unlink
* @return {Promise} When fulfilled, will have linked the service account or returned an error.
*/
unlink: function(user, serviceType) {
var apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "unlink_service_account");
apiCall.addArgument("username", user.username);
apiCall.addArgument("domain", user.domain);
apiCall.addArgument("service", serviceType);
apiCall.addArgument("dismiss", true);
// NOTE: This api needs to return a list including both the modified user and
// the now independent service as if it were dismissed.
var deferred = this.deferred(apiCall, {
transformAPISuccess: convertResponseToList
});
return deferred.promise;
},
/**
* Link all merge candidate service accounts of a sub-account (real or hypothetical).
* @param {Object} subAccount Definition of the sub-account whose merge candidates should be linked.
* @return {Promise} When fulfilled, will have linked the service account(s) or returned an error.
*/
linkAll: function(subAccount) {
var apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "merge_service_account");
apiCall.addArgument("username", subAccount.username);
apiCall.addArgument("domain", subAccount.domain);
for (var i = 0, l = subAccount.merge_candidates.length; i < l; i++) {
var serviceAccount = subAccount.merge_candidates[i];
for (var serviceName in serviceAccount.services) {
if ( serviceAccount.services.hasOwnProperty(serviceName) &&
serviceAccount.services[serviceName].enabled
) {
var arg = "services." + serviceName + ".merge";
apiCall.addArgument(arg, true);
}
}
}
var deferred = this.deferred(apiCall, {
transformAPISuccess: function(response) {
return adjustUser(response.data);
}
});
return deferred.promise;
},
/**
* Dismiss a link operation for an individual service account.
* @param {Object} user Definition of the service account to be linked.
* @return {Promise} When fulfilled, will have dismissed the service account from the merge candidates list.
*/
dismissLink: function(user) {
var apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "dismiss_merge");
apiCall.addArgument("username", user.username);
apiCall.addArgument("domain", user.domain);
for (var serviceName in user.services) {
if ( user.services.hasOwnProperty(serviceName) &&
user.services[serviceName].enabled
) {
var arg = "services." + serviceName + ".dismiss";
apiCall.addArgument(arg, true);
}
}
var deferred = this.deferred(apiCall, {
transformAPISuccess: function(response) {
return response.data;
}
});
return deferred.promise;
},
/**
* Dismiss all merge candidate service accounts of a sub-account (real or hypothetical).
* @param {Object} subAccount Definition of the sub-account whose merge candidates should be dismissed.
* @return {Promise} When fulfilled, will have dismissed the service account(s) or returned an error.
*/
dismissAll: function(subAccount) {
var apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "dismiss_merge");
apiCall.addArgument("username", subAccount.username);
apiCall.addArgument("domain", subAccount.domain);
for (var i = 0, l = subAccount.merge_candidates.length; i < l; i++) {
var serviceAccount = subAccount.merge_candidates[i];
for (var serviceName in serviceAccount.services) {
if ( serviceAccount.services.hasOwnProperty(serviceName) &&
serviceAccount.services[serviceName].enabled
) {
var arg = "services." + serviceName + ".dismiss";
apiCall.addArgument(arg, true);
}
}
}
var deferred = this.deferred(apiCall, {
transformAPISuccess: function(response) {
return response.data;
}
});
return deferred.promise;
},
/**
* Check for the presence of any existing accounts with the same name.
* The data returned when the promise is fulfilled matches the structure
* returned by UAPI UserManager::check_account_conflicts (see API documentation).
*
* @param {String} fullUsername The full user@domain name to check for.
* @return {Promise} When fulfilled, will have a response about whether a conflicting user exists.
*/
checkAccountConflicts: function(fullUsername) {
/* If the user continues typing in the box before an existing query has finished,
* abort it before starting a new one. */
if (lastRequest_jqXHR) {
lastRequest_jqXHR.abort();
}
var apiCall = new APIREQUEST.Class();
apiCall.initialize("UserManager", "check_account_conflicts");
apiCall.addArgument("full_username", fullUsername);
var deferred = this.deferred(apiCall, {
transformAPISuccess: function(response) {
if (response.data.accounts) {
response.data.accounts = adjustUser(response.data.accounts);
response.data.accounts.candidate_services = consolidateCandidateServices(response.data.accounts, true);
}
return response.data;
}
});
return $q(function(resolve, reject) {
deferred.promise.then(
function(data) {
if (data.conflict) { // convert the API true/false response into a promise compatible with async validation
reject( LOCALE.maketext("The username is not available.") );
} else {
resolve(data);
}
},
function(error) {
reject( LOCALE.maketext("The system failed to determine whether the username is available: [_1]", error ) );
}
);
});
},
/**
* Integrates the candidate_services values from one user into another user's actual services key.
*
* @method integrateCandidateServices
* @param {Object} dest The destination user object whose services property will be populated with
* the candidate services from the source user.
* @param {Object} src The source user object whose candidate_services property value will be
* assimilated into the appropriate service objects of the destination user.
* @return {Object} The processed destination user.
*/
integrateCandidateServices: function(dest, src) {
var candidateServices = (src && src.candidate_services) || {};
var services = dest.services;
var self = this;
angular.forEach(services, function(service, serviceName) {
if (candidateServices[serviceName]) {
services[serviceName] = candidateServices[serviceName];
} else if (services[serviceName].isCandidate) {
// If the previous service model was from a merge candidate, then
// it would be nice to start with a fresh set of defaults.
services[serviceName] = self.emptyUser().services[serviceName];
}
});
return dest;
},
/**
* Takes a subaccount user object and returns an array representing all of the user items for
* that particular full_username that would be included in the entire nested list of users.
*
* @method expandDismissed
* @param {Object} user The subaccount user object to process.
* @param {Boolean} onlyDismissed If true, only the dismissed accounts will be included in
* the returned array.
* @return {Array} An array of all dismissed service account user objects and,
* optionally, the subaccount user.
*/
expandDismissed: function(user, onlyDismissed) {
var ret = onlyDismissed ? [] : [user];
if (angular.isArray(user.dismissed_merge_candidates)) {
return ret.concat( user.dismissed_merge_candidates.map(adjustUser) );
} else {
throw new TypeError("Developer Error: dismissed_merge_candidates must be an array.");
}
},
/* override sendRequest from APIService to also save our last jqXHR object */
sendRequest: function(apiCall, handlers, deferred) {
apiCall = new APIService.AngularAPICall(apiCall, handlers, deferred);
lastRequest_jqXHR = apiCall.jqXHR;
return apiCall.deferred;
},
addInvitationIssues: function(user) {
if (user.has_invite) {
if (user.has_expired_invite) {
user.issues.unshift({
type: "error",
title: LOCALE.maketext("Invite Expired") + ":",
message: LOCALE.maketext("This user did not respond to the invitation before it expired. Please delete and re-create the user to send another invitation or set the user’s password yourself.")
});
} else {
user.issues.unshift({
type: "info",
title: LOCALE.maketext("Invite Pending") + ":",
message: LOCALE.maketext("This user has not used the invitation to set a password.")
});
}
}
}
});
return new UserListService();
}
]);
}
);
/*
# security/mod_security/views/domainlistController.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, PAGE: true */
/* jshint -W100 */
define(
'app/views/listController',[
"angular",
"lodash",
"cjt/util/locale",
"uiBootstrap",
"cjt/directives/alertList",
"cjt/services/alertService",
"cjt/directives/disableAnimations",
"cjt/directives/toggleSortDirective",
"cjt/directives/validationItemDirective",
"cjt/directives/spinnerDirective",
"cjt/directives/autoFocus",
"cjt/directives/lastItem",
"cjt/filters/wrapFilter",
"cjt/filters/breakFilter",
"cjt/services/dataCacheService",
"app/directives/issueList",
"app/directives/modelToLowerCase",
"app/services/userService"
],
function(angular, _, LOCALE) {
// Retrieve the current application
var app = angular.module("App");
// Setup the controller
var controller = app.controller(
"listController", [
"$scope",
"$routeParams",
"$q",
"$location",
"$filter",
"$timeout",
"userService",
"spinnerAPI",
"alertService",
"wrapFilter",
"dataCache",
"features",
"quotaInfo",
function(
$scope,
$routeParams,
$q,
$location,
$filter,
$timeout,
userService,
spinnerAPI,
alertService,
wrapFilter,
dataCache,
features,
quotaInfo
) {
/**
* Initialize the scope variables
*
* @private
* @method _initializeScope
*/
var _initializeScope = function() {
$scope.showAdvancedSettings = false;
$scope.alerts = alertService.getAlerts();
$scope.isOverQuota = !quotaInfo.under_quota_overall;
$scope.openConfirmation = null;
$scope.advancedFilters = {
services: "all",
issues: "both",
showLinkable: true // Linkable service accounts shown in hypothetical users.
};
// Setup the installed bit...
$scope.hasFeature = PAGE.hasFeature;
if (!$scope.hasFeature) {
return;
}
// setup data structures for the view
$scope.userList = [];
$scope.filteredUserList = [];
$scope.totalItems = 0;
$scope.meta = {
sortDirection: $routeParams.sortDirection || "asc",
sortBy: $routeParams.sortBy || "full_username",
sortType: $routeParams.sortType,
// NOTE: We don't want to use server side paging so, don't
// use these in the to the service layers list calls...
pageSize: $routeParams.pageSize || 50,
pageNumber: $routeParams.pageNumber || 1,
pageSizes: [10, 50, 100, 200],
};
$scope.features = features;
$scope.filteredTotalItems = 0;
$scope.filteredUsers = [];
};
/**
* Initialize the view
*
* @private
* @method _initializeView
*/
var _initializeView = function() {
var results;
if ($scope.isOverQuota) {
alertService.clear();
alertService.add({
message: LOCALE.maketext("Your [asis,cPanel] account exceeds its disk quota. You cannot add or edit users."),
type: "danger",
id: "over-quota-warning",
replace: false,
counter: false
});
}
// check for page data in the template if this is a first load
if (app.firstLoad.userList && PAGE.userList) {
app.firstLoad.userList = false;
try {
// Repackage the prefetch data
results = userService.prepareList(PAGE.userList);
// Allow the original list to garbage collect since
// we have already got what we need from it.
PAGE.userList = null;
// Stash a reference to the full list for later
dataCache.set("userList", results.items);
// Save it in scope
$scope.userList = dataCache.get("userList");
$scope.totalItems = $scope.userList.length;
} catch (e) {
alertService.clear();
var errors = e;
if (!angular.isArray(errors)) {
errors = [errors];
}
errors.forEach(function(error) {
alertService.add({
type: "danger",
message: error.toString(),
id: "fetchError"
});
});
}
} else {
// Check to see if the other view asked to suppress the fetch (and if the cache is actually available).
if ($location.search().loadFromCache && ( $scope.userList = dataCache.get("userList") ) ) {
$scope.totalItems = $scope.userList.length;
$scope.filteredTotalItems = $scope.userList.length; // since no filter yet
} else {
// Otherwise, retrieve it via ajax
$scope.fetch(!$scope.advancedFilters.showLinkable);
}
}
$scope.filteredData = false;
// Run anything chained in a separate cycle so it does
// not hold up page drawing.
return $timeout(function() {
updateUI(true);
}, 5);
};
/**
* Generate the viewable list of users by processing all the filtering
* and sorting in an unobserved set of arrays.
*
* @private
* @method updateUI
* @param {Boolean} shouldRunFilters If true, the user's filters will be processed,
* otherwise it's just pagination processing.
*/
function updateUI(shouldRunFilters) {
if (!$scope.userList) {
return;
}
spinnerAPI.start("loadingSpinner");
// Run this in a separate cycle so the UI can actually start
// the spinner.
$timeout(function() {
$scope.totalItems = $scope.userList.length;
// First filter the records down to the ones needed for this view.
var filteredUsers;
if (!shouldRunFilters) {
if ($scope.filteredData) {
filteredUsers = $scope.filteredUsers;
} else {
filteredUsers = $scope.userList;
}
} else {
var filterFilter = $filter("filter");
filteredUsers = filterFilter($scope.userList, $scope.filterText);
filteredUsers = filterFilter(filteredUsers, $scope.filterAdvanced);
$scope.filteredData = true;
}
// Now calculate the pagination
var startIndex = $scope.meta.pageSize * ($scope.meta.pageNumber - 1);
var endIndex = ($scope.meta.pageSize * $scope.meta.pageNumber);
var lastPage = false;
if (endIndex > filteredUsers.length) {
lastPage = true;
}
// Now attach to the view
$scope.filteredTotalItems = filteredUsers.length;
$scope.filteredUsers = filteredUsers;
if (filteredUsers.length < $scope.meta.pageSize) {
$scope.pagedFilteredUser = filteredUsers;
} else {
if (!lastPage) {
// Just the page we are looking for
$scope.pagedFilteredUser = filteredUsers.slice(startIndex, endIndex);
} else {
// Everything else
$scope.pagedFilteredUser = filteredUsers.slice(startIndex);
}
}
var lastPageTotalItems = $scope.pageTotalItems;
$scope.pageTotalItems = filteredUsers.length;
if ($scope.pageTotalItems === 0 || // No records
lastPageTotalItems === filteredUsers.length) { // No change in count
spinnerAPI.stop("loadingSpinner");
}
// Hide the initial loading panel if its still showing
$scope.hideViewLoadingPanel();
}, 5);
}
/**
* Called when the last row is inserted to stop the loading spinner
*
* @scope
* @method doneRendering
* @param {Object} user Just for debugging
*/
$scope.doneRendering = function(user) {
spinnerAPI.stop("loadingSpinner");
};
/**
* Navigate to the edit screen for the specified user or service
*
* @scope
* @method edit
* @param {Object} user
*/
$scope.edit = function(user) {
if ($scope.isOverQuota) {
return false;
}
if (user.type === "sub") {
$scope.loadView("edit/subaccount/" + user.guid, {}, { clearAlerts: true });
} else if (user.type === "service") {
var serviceType;
if (user.services.email && user.services.email.enabled) {
serviceType = "email";
} else if (user.services.ftp && user.services.ftp.enabled) {
serviceType = "ftp";
} else if (user.services.webdisk && user.services.webdisk.enabled) {
serviceType = "webdisk";
} else {
alertService.clear();
alertService.add({
type: "danger",
message: LOCALE.maketext("The service account is invalid."),
id: "errorServiceAccountNotValid"
});
return;
}
$scope.loadView("edit/service/" + serviceType + "/" + user.full_username, {}, { clearAlerts: true });
} else {
alertService.clear();
alertService.add({
type: "danger",
message: LOCALE.maketext("You cannot edit the account."),
id: "errorAccountNotValid"
});
return;
}
};
/**
* Filter method to test if the user should be filtered by a string value.
*
* @scope
* @method filterText
* @param {Object} user
* @return {Boolean} true if the user should be shown, false otherwise.
*/
$scope.filterText = function(user) {
if (!$scope.meta.filterValue) {
return true;
}
return [
"full_username",
"real_name",
"alternate_email",
"type",
"typeLabel",
"serviceSearch"
].some(function(key) {
var propVal = user[key];
if (propVal && propVal.toLocaleLowerCase().indexOf($scope.meta.filterValue) !== -1) {
return true;
}
});
};
/**
* Test if there is an active advanced search.
*
* @scope
* @method hasAdvancedSearch
* @return {Boolean} true if there is an advanced search option
* selected, false otherwise.
*/
$scope.hasAdvancedSearch = function() {
if ($scope.advancedFilters.services !== "all" ||
$scope.advancedFilters.issues !== "both") {
return true;
} else {
return false;
}
};
/**
* Filter method to test if the user should be filtered based on the various
* advanced search options.
*
* @scope
* @method filterAdvanced
* @param {Object} user
* @return {Boolean} true if the user should be shown, false otherwise.
*/
$scope.filterAdvanced = function(user) {
/**
* Filter the merge candidates the same way we filter them in the UI.
*
* @private
* @method areMergeCandidatesVisible
* @param {Object} user [description]
* @return {Boolean} true if there are merge candidates visible, false otherwise.
*/
var areMergeCandidatesVisible = function(user) {
var list = user.merge_candidates;
if ($scope.meta.filterValue) {
list = $filter("filter")(list, $scope.filterText);
}
list = $filter("filter")(list, $scope.filterAdvanced);
return !!list.length;
};
if ($scope.advancedFilters.issues === "noissues") {
switch (user.type) {
case "hypothetical":
if (!areMergeCandidatesVisible(user)) {
return false;
} else if (user.candidate_issues_count === user.merge_candidates.length) {
// Only hide this if the number of services and number of
// single service merge candidates are the same.
return false;
}
break;
case "sub":
if (user.issues.length > 0 ||
user.has_expired_invite ||
(areMergeCandidatesVisible(user) && user.candidate_issues_count)) {
return false;
}
break;
default:
if (user.issues.length > 0) {
return false;
}
}
}
if ($scope.advancedFilters.issues === "issues") {
switch (user.type) {
case "hypothetical":
if (!areMergeCandidatesVisible(user)) {
return false;
} else if (!user.candidate_issues_count) {
return false;
}
break;
case "sub":
if (user.issues.length === 0 &&
!user.has_expired_invite &&
(!areMergeCandidatesVisible(user) || !user.candidate_issues_count)) {
return false;
}
break;
default:
if (user.issues.length === 0) {
return false;
}
}
}
if ($scope.advancedFilters.services === "all") {
return true;
}
if ($scope.advancedFilters.services === "email" &&
(user.services.email.enabled || user.services.email.enabledInCandidate)) {
return true;
}
if ($scope.advancedFilters.services === "ftp" &&
(user.services.ftp.enabled || user.services.ftp.enabledInCandidate)) {
return true;
}
if ($scope.advancedFilters.services === "webdisk" &&
(user.services.webdisk.enabled || user.services.webdisk.enabledInCandidate)) {
return true;
}
return false;
};
/**
* Sort the list of sub-accounts and service accounts
*
* @scope
* @method sortList
* @param {Object} meta An object with metadata properties of sortBy, sortDirection, and sortType.
* @param {Boolean} [defaultSort] If true, this sort was not initiated by the user.
*/
$scope.sortList = function(meta, defaultSort) {
// clear the selected row
$scope.selectedRow = -1;
if (!defaultSort) {
var flat = !$scope.advancedFilters.showLinkable;
$scope.fetch(flat);
}
};
/**
* Clears the search term when the Esc key
* is pressed.
*
* @scope
* @method triggerClearSearch
* @param {Event} event - The event object
*/
$scope.triggerClearSearch = function(event) {
if (event.keyCode === 27) {
$scope.clearSearch();
}
};
/**
* Clears the search term
*
* @scope
* @method clearSearch
*/
$scope.clearSearch = function() {
$scope.meta.filterValue = "";
};
/**
* Fetch the list of sub-accounts and service accounts from the server.
*
* @scope
* @method fetch
* @return {Promise} Promise that when fulfilled will result in the list being loaded with the new criteria.
*/
$scope.fetch = function() {
// Setup the view for a full reload
$scope.filteredUsers = [];
$scope.filteredData = false;
$scope.showViewLoadingPanel();
// Start the load
var flat = !$scope.advancedFilters.showLinkable;
spinnerAPI.start("loadingSpinner");
return userService
.fetchList(flat, $scope.meta)
.then(function(results) {
dataCache.set("userList", results.items);
$scope.userList = dataCache.get("userList");
$scope.totalItems = $scope.userList.length;
$scope.pageNumber = 1;
updateUI(true);
}, function(error) {
// failure
alertService.add({
type: "danger",
message: error,
id: "fetchError"
});
})
.finally(function() {
spinnerAPI.stop("loadingSpinner");
});
};
/**
* Show the delete confirm dialog for a user.
*
* @scope
* @method showDeleteConfirm
* @param {Object} user
*/
$scope.showDeleteConfirm = function(user) {
user.ui.showDeleteConfirm = true;
};
/**
* Hide the delete confirm dialog for a user.
*
* @scope
* @method hideDeleteConfirm
* @param {Object} user
*/
$scope.hideDeleteConfirm = function(user) {
user.ui.showDeleteConfirm = false;
};
/**
* Check if we should show the delete confirm dialog for a specific user
* @scope
* @method canShowDeleteConfirm
* @param {Object} user
* @return {Boolean} true if it should show, false otherwise.
*/
$scope.canShowDeleteConfirm = function(user) {
return user.ui.showDeleteConfirm;
};
/**
* Check if a delete operation is underway for the passed user.
*
* @scope
* @method isDeleting
* @param {Object} user
* @return {Boolean} true if a delete operation is running, false otherwise.
*/
$scope.isDeleting = function(user) {
return user.ui.deleting;
};
/**
* Delete a user
* @param {Object} user The user to delete.
* @param {Object} [parent] The parent user, if there is one.
* @return {Promise} When resolved, the user has been deleted.
*/
$scope.deleteUser = function(user, parent) {
spinnerAPI.start("loadingSpinner");
user.ui.deleting = true;
return userService
.delete(user)
.then(function(results) {
var collection = parent ? parent.merge_candidates : $scope.userList;
var pos = collection.indexOf(user);
if (pos !== -1) {
if (results.data) { // delete_user returns a replacement back when appropriate
collection.splice(pos, 1, results.data);
} else {
collection.splice(pos, 1); // service deletes don't return anything
/* If all we have left is a hypothetical account with one merge candidate,
* get rid of the hypothetical account and replace it with that remaining
* service account. This is the same behavior we have with dismisses. */
if (parent && parent.type === "hypothetical" && parent.merge_candidates.length === 1) {
var parentPos = $scope.userList.indexOf(parent);
if (parentPos !== -1) {
$scope.userList.splice(parentPos, 1, parent.merge_candidates.pop());
}
}
}
// update the caches
dataCache.set("userList", $scope.userList);
updateUI(true);
}
}, function(error) {
// failure
alertService.add({
type: "danger",
message: error,
id: "deleteError"
});
})
.finally(function() {
user.ui.deleting = false;
spinnerAPI.stop("loadingSpinner");
});
};
/**
* Helper method to add the rendering text around the full username
* for the delete query.
*
* @note This may have been easier if we published maketext as a method on the ## no extract maketext
* controller and then you could do something like:
* <span>{{maketext("Do you wish to remove the “[_1]” user from your system?", user.full_username | wrap:[@.]:10)}}
*
* @scope
* @method wrappedDeleteText
* @param {Object} user
* @return {String}
*/
$scope.wrappedDeleteText = function(user) {
var wbrText = wrapFilter(user.full_username, "[@.]", 5);
return LOCALE.maketext("Do you wish to remove the “[_1]” user from your system?", wbrText);
};
/**
* Given a merge candidate, links it to a sub-account of the same name.
*
* @scope
* @method linkUser
* @param {Object} user The service account to link.
* @param {Object} parent The sub-account (real or hypothetical) to which the service account is being linked.
* @return {Promise}
*/
$scope.linkUser = function(user, parent) {
spinnerAPI.start("loadingSpinner");
user.ui.linking = true;
_buildLinkingCaches(user, parent);
return userService
.link(user)
.then(function(results) {
var collection = $scope.userList;
var pos = collection.indexOf(parent);
if (pos !== -1) {
/* The link operation gives us back the entire parent account record, including any
* remaining merge candidates. We just need to splice it back into the list at
* the appropriate spot. */
collection.splice(pos, 1, results);
// Update the cache
dataCache.set("userList", collection);
// Update the UI
updateUI(true);
alertService.add({
type: "success",
message: results.synced_password ?
LOCALE.maketext("The system successfully linked the service account to the “[_1]” user’s [asis,subaccount]. The service account passwords have not changed.", results.full_username) :
LOCALE.maketext("The system successfully linked the service account to the “[_1]” user’s [asis,subaccount]. The service account passwords did not change. You must provide a new password if you wish to enable any additional [asis,subaccount] services.", results.full_username),
id: "link-user-success",
replace: false
});
}
}, function(error) {
alertService.add({
type: "danger",
message: error,
id: "linkError"
});
})
.finally(function() {
spinnerAPI.stop("loadingSpinner");
user.ui.linking = false;
_buildLinkingCaches(user, parent);
});
};
/**
* Given a merge candidate, dismisses it from the merge candidate list.
*
* @scope
* @method dismissLink
* @param {Object} user The service account to dismiss.
* @param {Object} parent The sub-account (real or hypothetical) to which the service account would have been linked.
* @return {Promise}
*/
$scope.dismissLink = function(user, parent) {
spinnerAPI.start("loadingSpinner");
user.ui.linking = true;
_buildLinkingCaches(user, parent);
return userService
.dismissLink(user)
.then(function(results) {
var collection = $scope.userList;
var pos = collection.indexOf(parent);
var mergeCandidatePosition = collection[pos].merge_candidates.indexOf(user);
if (mergeCandidatePosition !== -1) {
/* Pull the service account out of the merge candidates section and move it up to the top level of the user list. */
var formerMergeCandidate = collection[pos].merge_candidates[mergeCandidatePosition];
collection[pos].merge_candidates.splice(mergeCandidatePosition, 1);
_insert(collection, formerMergeCandidate);
/* If, after the last dismiss, there is only one merge candidate left, and it is being shown as a
* merge candidate for a hypothetical sub-account, move it out to the top level too. This is a
* special case for hypothetical sub-accounts because we wouldn't normally show a single service
* account as a merge candidate unless the corresponding sub-account already existed. */
if ("hypothetical" === collection[pos].type && collection[pos].merge_candidates.length === 1) {
var finalMergeCandidate = collection[pos].merge_candidates.pop();
_insert(collection, finalMergeCandidate);
collection.splice(pos, 1); // remove the hypothetical sub-account too
}
// Update the cache
dataCache.set("userList", collection);
// Update the UI
updateUI(true);
}
}, function(error) {
alertService.add({
type: "danger",
message: error,
id: "dismissError"
});
})
.finally(function() {
spinnerAPI.stop("loadingSpinner");
user.ui.linking = false;
_buildLinkingCaches(user, parent);
});
};
/**
* Insert the user in the correct position in the collection.
*
* @private
* @method _insert
* @param {Array} collection
* @param {Object} newUser
*/
var _insert = function(collection, newUser) {
for (var i = 0, l = collection.length; i < l; i++) {
var user = collection[i];
if (user.full_username > newUser.full_username) {
collection.splice(i, 0, newUser);
return;
}
}
// It needs to go at the end of the list
collection.push(newUser);
};
/**
* Given a sub-account (real or hypothetical), link all available merge candidates.
*
* @scope
* @method linkAll
* @param {Object} parent The sub-account.
* @return {Promise}
*/
$scope.linkAll = function(parent) {
spinnerAPI.start("loadingSpinner");
parent.ui.linkingAny = parent.ui.linkingAll = true;
return userService
.linkAll(parent)
.then(function(results) {
var collection = $scope.userList;
var pos = collection.indexOf(parent);
if (pos !== -1) {
collection.splice(pos, 1, results);
// Update the cache
dataCache.set("userList", collection);
// Update the UI
updateUI(true);
}
alertService.add({
type: "success",
message: results.synced_password ?
LOCALE.maketext("The system successfully linked all of the service accounts for the “[_1]” user to the [asis,subaccount]. The service account passwords did not change.", results.full_username) :
LOCALE.maketext("The system successfully linked all of the service accounts for the “[_1]” user to the [asis,subaccount]. The service account passwords did not change. You must provide a new password if you wish to enable any additional [asis,subaccount] services.", results.full_username),
id: "link-all-success",
replace: false
});
}, function(error) {
alertService.add({
type: "danger",
message: error,
id: "dismissError"
});
})
.finally(function() {
spinnerAPI.stop("loadingSpinner");
parent.ui.linkingAny = parent.ui.linkingAll = false;
});
};
/**
* Given a sub-account (real or hypothetical), dismiss all available merge candidates.
*
* @scope
* @method dismissAll
* @param {Object} parent The sub-account.
* @return {Promise}
*/
$scope.dismissAll = function(parent) {
spinnerAPI.start("loadingSpinner");
parent.ui.linkingAny = parent.ui.linkingAll = true;
return userService
.dismissAll(parent)
.then(function(results) {
var collection = $scope.userList;
var pos = collection.indexOf(parent);
if (pos !== -1) {
/* Pull everything out of the merge candidates section and put it at the top level of the user list. */
var serviceAccount = collection[pos].merge_candidates.shift();
while ( serviceAccount ) {
_insert(collection, serviceAccount);
serviceAccount = collection[pos].merge_candidates.shift();
}
/* If the sub-account didn't already exist, stop displaying the placeholder now that the merge candidates are gone. */
if ("hypothetical" === parent.type) {
collection.splice(pos, 1);
}
// Update the cache
dataCache.set("userList", collection);
// Update the UI
updateUI(true);
}
}, function(error) {
alertService.add({
type: "danger",
message: error,
id: "linkError"
});
})
.finally(function() {
spinnerAPI.stop("loadingSpinner");
parent.ui.linkingAny = parent.ui.linkingAll = false;
});
};
/**
* Build the helpers state for linking and dismissing
*
* @private
* @method _buildLinkingCaches
* @param {Object} user
* @param {Object} parent
*/
var _buildLinkingCaches = function(user, parent) {
parent.ui.linkingAll = true;
parent.ui.linkingAny = false;
for (var i = 0, l = parent.merge_candidates.length; i < l; i++) {
if (parent.merge_candidates[i].ui.linking) {
parent.ui.linkingAny = true;
} else {
parent.ui.linkingAll = false;
}
}
};
// Get the page bootstrapped. Moved before the watchers to try to get the page to load faster
_initializeScope();
_initializeView().finally(function() {
/**
* Set up the watchers that facilitate caching for the filteredUserList
*/
$scope.$watchGroup([
"meta.filterValue",
"advancedFilters.services",
"advancedFilters.issues"
], function(newVals, oldVals) {
updateUI(true);
});
$scope.$watchGroup([
"meta.pageSize",
"meta.pageNumber"
], function(newVals, oldVals) {
updateUI();
});
});
}
]
);
return controller;
}
);
/*
# base/frontend/jupiter/user_manager/directives/validateUsernameWithDomain.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/directives/validateUsernameWithDomain',[
"angular",
"cjt/util/locale",
"cjt/validator/validator-utils",
"cjt/validator/validateDirectiveFactory",
"app/services/userService",
],
function(angular, LOCALE, validatorUtils, validatorFactory, userService) {
"use strict";
var module = angular.module("App");
/**
* This set of directives is intended to help with the problem of length
* validation for username@domain entry across two fields. In the product we
* often have one field for username and another for the domain selection. As
* of 11.54, we are imposing character limitations for the combined result of
* these two fields, including the @ character. This directive automates that
* validation.
*
* @example
*
* <form username-with-domain-wrapper>
* <input name="username" ng-model="username" username-with-domain="username">
* <input name="domain" ng-model="domain" username-with-domain="domain">
* <ul validation-container field-name="username"></ul>
* </form>
*
* Note: Both the wrapper and child directives are restricted to attributes.
*/
/**
* The wrapper directive just serves as a communication point between the two
* child directives.
*/
module.directive("usernameWithDomainWrapper", [function() {
var ParentController = function($attrs) {
this.username = this.domain = "";
this.$attrs = $attrs;
};
angular.extend(ParentController.prototype, {
setDomain: function(domain) {
if (typeof domain !== "undefined") {
this.domain = domain;
}
return this.getTotalLength();
},
setUsername: function(username) {
if (typeof username !== "undefined") {
this.username = username;
}
return this.getTotalLength();
},
getUsernameAndDomain: function() {
return this.username + "@" + this.domain;
},
getTotalLength: function() {
return this.getUsernameAndDomain().length;
},
});
return {
restrict: "A",
scope: false,
controller: ["$attrs", ParentController],
};
}]);
/**
* This directive will need two instances to function as intended, and they
* should both be descendants of the wrapper directive. One should have the
* attribute value of "username" and the other value should be "domain".
*/
module.directive("usernameWithDomain", ["userService", "$q", function(userService, $q) {
return {
restrict: "A",
scope: false,
require: ["^^usernameWithDomainWrapper", "ngModel"],
link: function( scope, elem, attrs, ctrls ) {
var parentCtrl = ctrls[0]; // The controller from the wrapper directive
var ngModel = ctrls[1]; // The ngModel controller from the current element
// Grab the type
var type = attrs.usernameWithDomain;
if (type === "username") {
// Save a reference to the $validate function on the wrapper so that the partner "domain"
// version of this directive can trigger validation for this "username" instance.
parentCtrl.validateUsername = ngModel.$validate;
// Set up the extended validation object the same way the validateDirectiveFactory does.
var formCtrl = elem.controller("form");
validatorUtils.initializeExtendedReporting(ngModel, formCtrl);
// This is the main validation function that checks the total length of the username@domain.
var validateUsernameWithDoamin = function(totalLength) {
var TOTAL_MAX_LENGTH = 254;
var result = validatorUtils.initializeValidationResult();
if (totalLength > TOTAL_MAX_LENGTH) {
result.addError("maxLength", LOCALE.maketext("The combined length of the username, [asis,@] character, and domain cannot exceed [numf,_1] characters.", TOTAL_MAX_LENGTH));
}
return result;
};
// Add the validator to the list. The validator goes through the validateDirectiveFactory
// "run" method to hopefully help compatibility going forward.
ngModel.$validators.usernameWithDomain = function(newUsername) {
var totalLength = parentCtrl.setUsername(newUsername);
return validatorFactory.run("usernameWithDomain", ngModel, formCtrl, validateUsernameWithDoamin, totalLength);
};
var validateUsernameIsAvailableAsync = function(value) {
return userService.checkAccountConflicts(value).then(function(responseData) {
scope.$eval(parentCtrl.$attrs.lookupCallback, { responseData: responseData });
return responseData;
}).then(
function() {
return validatorUtils.initializeValidationResult();
},
function(error) {
var result = validatorUtils.initializeValidationResult(true);
result.addError("usernameIsAvailable", error);
return result;
});
};
ngModel.$asyncValidators.usernameIsAvailable = function(modelValue, viewValue) {
var value = parentCtrl.getUsernameAndDomain();
return validatorFactory.runAsync($q, "usernameIsAvailable", ngModel, formCtrl, validateUsernameIsAvailableAsync, value);
};
} else if (type === "domain") {
// Unfortunately the viewChangeListeners array doesn't get triggered when you first set
// the model value (for whatever reason), so we'll need to set the domain to cover the
// case when the user doesn't change the default. $formatters don't get called for select
// controls when their value changes so this only fires on the initial render.
ngModel.$formatters.push(function(val) {
parentCtrl.setDomain( ngModel.$modelValue );
return val;
});
// When the domain model changes, we need to run the length check again, but the username
// is where people have the most flexibility to make changes, so we'll run the validation
// there to create the validation error messages near that field.
ngModel.$viewChangeListeners.push(function() {
parentCtrl.setDomain( ngModel.$modelValue );
parentCtrl.validateUsername();
});
} else {
throw new Error("The value for the username-with-domain directive needs to be set to 'username' or 'domain'.");
}
},
};
}]);
}
);
/*
# user_manager/directives/selectOnFocus.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/selectOnFocus',[
"angular",
],
function(angular) {
var module = angular.module("App");
module.directive("selectOnFocus", [
"$timeout",
function($timeout) {
return {
restrict: "A",
link: function(scope, element, attrs) {
var focusedElement = null;
var bindTo;
if ( element[0].tagName === "input" ) {
bindTo = element;
} else {
bindTo = element.find("input");
}
if ( bindTo.length === 1 ) {
bindTo.on("focus", function() {
var self = this;
if (focusedElement !== self) {
focusedElement = self;
$timeout(function() {
if ( self.select ) {
self.select();
}
}, 10);
}
});
bindTo.on("blur", function() {
focusedElement = null;
});
}
}
};
}
]);
}
);
/*
# user_manager/directives/limit.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/limit',[
"angular",
"lodash",
"cjt/core",
"cjt/util/locale",
"cjt/directives/bytesInput",
"app/directives/selectOnFocus"
],
function(angular, _, CJT, LOCALE) {
var module = angular.module("App");
module.directive("appLimit", [
"$timeout",
"$templateCache",
"$document",
function($timeout, $templateCache, $document) {
var _counter = 1;
var TEMPLATE_PATH = "directives/limit.phtml";
var RELATIVE_PATH = "user_manager/" + TEMPLATE_PATH;
var SCOPE_DECLARATION = {
id: "@?id",
unitsLabel: "@?unitsLabel",
unlimitedLabel: "@?unlimitedLabel",
unlimitedValue: "=unlimitedValue",
minimumValue: "=minimumValue",
maximumValue: "=maximumValue",
isDisabled: "=ngDisabled",
defaultValue: "=defaultValue",
maximumLength: "=maximumLength",
selectedUnit: "="
};
var UNLIMITED_DEFAULT_LABEL = "Unlimited";
var UNLIMITED_DEFAULT_VALUE = 0;
var _focusElement = function(el, wait) {
if (!el) {
return;
}
// https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
var elFocus = $document.activeElement ? $document.activeElement : null;
if (elFocus !== el) {
if (angular.isUndefined(wait)) {
el.focus();
} else {
$timeout(function() {
el.focus();
}, wait);
}
}
};
return {
restrict: "E",
templateUrl: CJT.config.debug ? CJT.buildFullPath(RELATIVE_PATH) : TEMPLATE_PATH,
replace: true,
require: "ngModel",
scope: SCOPE_DECLARATION,
compile: function(element, attrs) {
return {
pre: function(scope, element, attrs) {
if (angular.isUndefined(attrs.unlimitedLabel)) {
attrs.unlimitedLabel = UNLIMITED_DEFAULT_LABEL;
}
if (!attrs.id) {
attrs.id = "ctrlLimit_" + _counter++;
}
},
post: function(scope, element, attrs, ngModel) {
if (angular.isUndefined(scope.unlimitedValue)) {
scope.unlimitedValue = UNLIMITED_DEFAULT_VALUE;
}
if (angular.isUndefined(scope.minimumValue)) {
scope.minimumValue = 1;
}
scope.maximumLength = _parseIntOrDefault(scope.maximumLength, null);
scope.unlimitedValue = _parseIntOrDefault(scope.unlimitedValue, 0);
scope.minimumValue = _parseIntOrDefault(scope.minimumValue, 1);
scope.maximumValue = _parseIntOrDefault(scope.maximumValue, null);
scope.defaultValue = _parseIntOrDefault(scope.defaultValue, null);
scope.selectedUnit = scope.selectedUnit || "MB";
var elNumber = element.find(".textbox");
// Define how to transform the model into the parts needed for the view
ngModel.$formatters.push(function(modelValue) {
var unlimitedChecked = modelValue === scope.unlimitedValue;
return {
unlimitedChecked: unlimitedChecked,
value: unlimitedChecked ? "" : modelValue
};
});
// Define how to draw the output when the model changes
ngModel.$render = function() {
scope.unlimitedChecked = ngModel.$viewValue.unlimitedChecked;
scope.value = ngModel.$viewValue.value;
};
// Define how to transform the view into the model
ngModel.$parsers.push(function(viewValue) {
if (viewValue.unlimitedChecked) {
return scope.unlimitedValue;
} else {
return viewValue.value;
}
});
// Define how to set the view value when the view changes
scope.$watch("unlimitedChecked + value", function(newValue, oldValue) {
if (newValue === oldValue) {
return;
}
ngModel.$setViewValue({
unlimitedChecked: scope.unlimitedChecked,
value: scope.unlimitedChecked ? "" : scope.value
});
});
// input[type=number] do not natively respect the maxlength attribute
// the way a input[type=text] does. This even handler adds the missing
// behavior.
if (scope.maximumLength && scope.maximumLength > 0) {
elNumber.on("input", function(e) {
if (this.value.length > scope.maximumLength) {
this.value = this.value.slice(0, scope.maximumLength);
}
});
}
/**
* Handler for when the unlimited/unrestricted radio button is clicked or selected
*/
scope.makeUnlimited = function() {
if (scope.value !== "") {
scope.lastValue = scope.value;
} else if (scope.defaultValue) {
scope.lastValue = scope.defaultValue;
} else {
scope.lastValue = scope.minimumValue;
}
scope.unlimitedChecked = true;
scope.value = "";
};
/**
* Handler for when the limited/restricted radio button or the click shield for the
* input field is clicked or selected.
*/
scope.enableLimit = function() {
if (!scope.isDisabled) {
if (scope.unlimitedChecked) {
if (scope.value === "") {
// changing from unlimited to limits
if (scope.lastValue !== "") {
scope.value = scope.lastValue;
} else if (scope.defaultValue) {
scope.value = scope.defaultValue;
} else {
scope.value = scope.minimumValue;
}
}
scope.unlimitedChecked = false;
}
if ( elNumber.length === 0 ) {
elNumber = element.find(".textbox");
}
_focusElement(elNumber, 0);
}
};
// Setup the defaults for things not part of the ngModel handlers above.
if (scope.defaultValue) {
scope.lastValue = scope.defaultValue;
} else {
scope.lastValue = scope.minimumValue;
}
}
};
}
};
}
]);
function _parseIntOrDefault(value, defaultValue) {
if (angular.isString(value)) {
value = parseInt(value, 10); // parseInt returns NaN with undefined, null, or empty strings
}
return isNaN(value) ? defaultValue : value;
}
}
);
/*
# user_manager/directives/serviceConfigController.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/serviceConfigController',[
"angular",
"cjt/util/test"
], function(angular, TEST) {
var app = angular.module("App");
app.controller("serviceConfigController", [
"$scope",
"$attrs",
function($scope, $attrs) {
/**
* Does the service need conflict resolution?
*
* @method needsConflictResolution
* @return {Boolean}
*/
$scope.needsConflictResolution = function() {
return $scope.hasConflict() && !$scope.isResolved();
};
/**
* Would adding this service create a conflict?
*
* @method hasConflict
* @return {Boolean}
*/
$scope.hasConflict = function() {
return $scope.service && $scope.service.isCandidate;
};
/**
* Has the client resolved a conflict? Note that this method does not
* test to see if there is a conflict in the first place.
*
* @method isResolved
* @return {Boolean}
*/
$scope.isResolved = function() {
return $scope.service.willLink || $scope.service.willDismiss;
};
/**
* Is there a link action attribute present?
*
* @method hasLinkAction
* @return {Boolean}
*/
$scope.hasLinkAction = function() {
return !!$attrs.linkAction;
};
/**
* Stages a merge candidate for dismissal.
*
* @method setDismiss
*/
$scope.setDismiss = function() {
$scope.service.willDismiss = true;
$scope.service.enabled = false;
$scope.validateConflictResolution();
};
/**
* Stages a merge candidate for linking.
*
* @method setLink
*/
$scope.setLink = function() {
$scope.service.willLink = true;
$scope.service.enabled = true;
$scope.validateConflictResolution();
};
/**
* Clears any existing conflict resolution markers. Used for the undo action.
*
* @method clearConflictResolution
*/
$scope.clearConflictResolution = function() {
$scope.service.willLink = $scope.service.willDismiss = false;
$scope.validateConflictResolution();
};
/**
* Stages the service for linking and runs the supplied linkAction method against the parent scope.
*
* @method runLinkAction
* @return {Any} Returns whatever is returned from the linkAction method.
*/
$scope.runLinkAction = function() {
$scope.isLinking = true;
$scope.setLink();
if (!$scope.hasLinkAction()) {
$scope.isLinking = false;
return;
}
var ret = $scope.linkAction({ service: $scope.service });
if (TEST.isQPromise(ret)) {
ret.finally(function() {
$scope.isLinking = false;
});
} else {
$scope.isLinking = false;
}
return ret;
};
/**
* Sets validity for the control if conflict resolution is required.
*
* @method validateConflictResolution
*/
$scope.validateConflictResolution = function() {
if ($scope.conflictResolutionRequired) {
$scope.ngModel.$setValidity("conflictCleared", !$scope.needsConflictResolution());
}
};
/**
* Toggles the expanded/collapsed view of the service conflict summary.
*
* @method toggleConflictSummary
*/
$scope.toggleConflictSummary = function() {
$scope.isSummaryCollapsed = !$scope.isSummaryCollapsed;
};
$scope.isSummaryCollapsed = true;
}
]);
});
/*
# user_manager/directives/emailServiceConfig.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/emailServiceConfig',[
"angular",
"lodash",
"cjt/core",
"cjt/util/locale",
"cjt/directives/toggleSwitchDirective",
"cjt/filters/wrapFilter",
"app/directives/limit",
"app/directives/serviceConfigController"
],
function(angular, _, CJT, LOCALE) {
var module = angular.module("App");
module.directive("emailConfig", [
"defaultInfo",
function(defaultInfo) {
var TEMPLATE_PATH = "directives/emailServiceConfig.ptt";
var RELATIVE_PATH = "user_manager/" + TEMPLATE_PATH;
return {
restrict: "AE",
templateUrl: CJT.config.debug ? CJT.buildFullPath(RELATIVE_PATH) : TEMPLATE_PATH,
replace: true,
require: "ngModel",
scope: {
toggleService: "&toggleService",
isDisabled: "=ngDisabled",
showToggle: "=showToggle",
showUnlink: "=showUnlink",
unlinkService: "&unlinkService",
isInProgress: "&isInProgress",
showInfo: "=showInfo",
infoMessage: "@infoMessage",
showWarning: "=showWarning",
warningMessage: "@warningMessage",
showConflictDismiss: "=?",
conflictResolutionRequired: "=?",
linkAction: "&?"
},
controller: "serviceConfigController",
link: function(scope, element, attrs, ngModel) {
scope.ngModel = ngModel;
if (angular.isUndefined(scope.showWarning) ||
angular.isUndefined(scope.warningMessage) ||
scope.warningMessage === "") {
scope.showWarning = false;
}
if (angular.isUndefined(scope.showInfo) ||
angular.isUndefined(scope.infoMessage) ||
scope.infoMessage === "") {
scope.showInfo = false;
}
if (angular.isUndefined(scope.showToggle)) {
scope.showToggle = true;
}
if (angular.isUndefined(scope.showUnlink)) {
scope.showUnlink = false;
}
// Define how to draw the output when the model changes
ngModel.$render = function() {
scope.service = ngModel.$modelValue;
scope.validateConflictResolution();
};
scope.defaults = defaultInfo;
scope.maxMessage = LOCALE.maketext("Quotas cannot exceed [format_bytes,_1].", defaultInfo.email.max_quota * 1048576);
}
};
}
]);
}
);
/*
* cpanel - base/frontend/jupiter/_assets/services/directoryLookupService.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/services/directoryLookupService',[
"angular",
"lodash",
"cjt/core",
"cjt/util/locale",
"cjt/io/api",
"cjt/io/uapi-request",
"cjt/io/uapi",
"cjt/util/parse",
],
function(angular, _, CJT, LOCALE, API, APIREQUEST, APIDRIVER, PARSER) {
"use strict";
var app = angular.module("cpanel.services.directoryLookup", []);
var lastRequestJQXHR = null;
app.factory("directoryLookupService", [
"$q",
"APIService",
function($q, APIService) {
var DirectoryLookupService = function() {};
DirectoryLookupService.prototype = new APIService();
angular.extend(DirectoryLookupService.prototype, {
/**
* Query the directory completion API. Given a path prefix, which may
* include a partial directory name, returns an array of matching
* directories.
* @param {String} match The prefix to match.
* @return {Promise} When fulfilled, will have either provided the list of matching directories or failed.
*/
complete: function(match) {
/* Only allow one promise at a time for this service, and cancel any existing request, since
* the latest request will always supersede the existing one when typing into a text box. */
if (lastRequestJQXHR) {
lastRequestJQXHR.abort();
}
var apiCall = new APIREQUEST.Class();
apiCall.initialize("Fileman", "autocompletedir");
apiCall.addArgument("path", match);
apiCall.addArgument("dirsonly", true);
apiCall.addArgument("skipreserved", true);
apiCall.addArgument("html", 0);
/* If the last character of the path to match is a slash, then the user is probably hoping to see
* a list of all files underneath that directory. The API doesn't understand this unless you
* specify list_all mode, so we need to add that argument. */
if ( "/" === match.charAt(match.length - 1) ) {
apiCall.addArgument("list_all", true);
}
var deferred = this.deferred(apiCall, {
transformAPISuccess: function(response) {
var flattenedResponse = [];
for (var i = 0, l = response.data.length; i < l; i++) {
flattenedResponse.push(response.data[i].file);
}
return flattenedResponse;
},
});
return deferred.promise;
},
/* override sendRequest from APIService to also save our last jqXHR object */
sendRequest: function(apiCall, handlers, deferred) {
apiCall = new APIService.AngularAPICall(apiCall, handlers, deferred);
lastRequestJQXHR = apiCall.jqXHR;
return apiCall.deferred;
},
});
return new DirectoryLookupService();
},
]);
}
);
/*
# user_manager/directives/ftpServiceConfig.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/ftpServiceConfig',[
"angular",
"lodash",
"cjt/core",
"cjt/util/locale",
"cjt/directives/toggleSwitchDirective",
"cjt/filters/wrapFilter",
"cjt/filters/htmlFilter",
"app/services/directoryLookupService",
"app/directives/limit",
"app/directives/serviceConfigController"
],
function(angular, _, CJT, LOCALE) {
var module = angular.module("App");
module.directive("ftpConfig", [
"defaultInfo",
"ftpDaemonInfo",
"directoryLookupService",
function(defaultInfo, ftpDaemonInfo, directoryLookupService) {
var TEMPLATE_PATH = "directives/ftpServiceConfig.ptt";
var RELATIVE_PATH = "user_manager/" + TEMPLATE_PATH;
return {
restrict: "AE",
templateUrl: CJT.config.debug ? CJT.buildFullPath(RELATIVE_PATH) : TEMPLATE_PATH,
replace: true,
require: "ngModel",
scope: {
toggleService: "&toggleService",
isDisabled: "=ngDisabled",
showToggle: "=showToggle",
showUnlink: "=showUnlink",
unlinkService: "&unlinkService",
isInProgress: "&isInProgress",
showInfo: "=showInfo",
infoMessage: "@infoMessage",
showWarning: "=showWarning",
warningMessage: "@warningMessage",
showConflictDismiss: "=?",
conflictResolutionRequired: "=?",
linkAction: "&?"
},
controller: "serviceConfigController",
link: function(scope, element, attrs, ngModel) {
scope.ngModel = ngModel;
if (angular.isUndefined(scope.showWarning) ||
angular.isUndefined(scope.warningMessage) ||
scope.warningMessage === "") {
scope.showWarning = false;
}
if (angular.isUndefined(scope.showInfo) ||
angular.isUndefined(scope.infoMessage) ||
scope.infoMessage === "") {
scope.showInfo = false;
}
if (angular.isUndefined(scope.showToggle)) {
scope.showToggle = true;
}
if (angular.isUndefined(scope.showUnlink)) {
scope.showUnlink = false;
}
// Define how to draw the output when the model changes
ngModel.$render = function() {
scope.service = ngModel.$modelValue;
scope.validateConflictResolution();
};
scope.daemon = ftpDaemonInfo;
scope.defaults = defaultInfo;
// Helper to call the directory lookup service
scope.completeDirectory = function(prefix) {
return directoryLookupService.complete(prefix);
};
}
};
}
]);
}
);
/*
# user_manager/directives/webdiskServiceConfig.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/webdiskServiceConfig',[
"angular",
"lodash",
"cjt/core",
"cjt/util/locale",
"cjt/directives/toggleSwitchDirective",
"cjt/filters/wrapFilter",
"cjt/filters/htmlFilter",
"app/services/directoryLookupService",
"app/directives/limit",
"app/directives/serviceConfigController"
],
function(angular, _, CJT, LOCALE) {
var module = angular.module("App");
module.directive("webdiskConfig", [
"defaultInfo",
"sslInfo",
"directoryLookupService",
function(defaultInfo, sslInfo, directoryLookupService) {
var TEMPLATE_PATH = "directives/webdiskServiceConfig.ptt";
var RELATIVE_PATH = "user_manager/" + TEMPLATE_PATH;
return {
restrict: "AE",
templateUrl: CJT.config.debug ? CJT.buildFullPath(RELATIVE_PATH) : TEMPLATE_PATH,
replace: true,
require: "ngModel",
scope: {
toggleService: "&toggleService",
isDisabled: "=ngDisabled",
showToggle: "=showToggle",
showUnlink: "=showUnlink",
unlinkService: "&unlinkService",
isInProgress: "&isInProgress",
enableDigestControls: "=enableDigestControls",
showDigestWarning: "=showDigestWarning",
showInfo: "=showInfo",
infoMessage: "@infoMessage",
showWarning: "=showWarning",
warningMessage: "@warningMessage",
showConflictDismiss: "=?",
conflictResolutionRequired: "=?",
linkAction: "&?"
},
controller: "serviceConfigController",
link: function(scope, element, attrs, ngModel) {
scope.ngModel = ngModel;
if (angular.isUndefined(scope.showDigestWarning)) {
scope.showDigestWarning = false;
}
if (angular.isUndefined(scope.showWarning) ||
angular.isUndefined(scope.warningMessage) ||
scope.warningMessage === "") {
scope.showWarning = false;
}
if (angular.isUndefined(scope.showInfo) ||
angular.isUndefined(scope.infoMessage) ||
scope.infoMessage === "") {
scope.showInfo = false;
}
if (angular.isUndefined(scope.showToggle)) {
scope.showToggle = true;
}
if (angular.isUndefined(scope.showUnlink)) {
scope.showUnlink = false;
}
if (angular.isUndefined(scope.enableDigestControls)) {
scope.enableDigestControls = true;
}
// Define how to draw the output when the model changes
ngModel.$render = function() {
scope.service = ngModel.$modelValue;
scope.validateConflictResolution();
};
scope.defaults = defaultInfo;
scope.allowDigestAuth = sslInfo.is_self_signed;
// Helper to call the directory lookup service
scope.completeDirectory = function(prefix) {
return directoryLookupService.complete(prefix);
};
}
};
}
]);
}
);
/*
# user_manager/views/addEditController.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, PAGE: true */
define(
'app/views/addEditController',[
"angular",
"lodash",
"cjt/util/locale",
"cjt/validator/email-validator",
"cjt/directives/validationItemDirective",
"cjt/directives/validationContainerDirective",
"cjt/directives/validateEqualsDirective",
"cjt/directives/passwordFieldDirective",
"cjt/directives/actionButtonDirective",
"app/directives/validateUsernameWithDomain",
"app/directives/emailServiceConfig",
"app/directives/ftpServiceConfig",
"app/directives/webdiskServiceConfig",
"uiBootstrap"
],
function(angular, _, LOCALE) {
var DEFAULT_PASSWORD_STRENGTH = 10; // Out of 100
// Retrieve the current application
var app = angular.module("App");
// This will be returned by RequireJS for use in other controllers
var factory = function($scope, userService, emailDaemonInfo, ftpDaemonInfo, webdiskDaemonInfo, features, defaultInfo, quotaInfo, alertService) {
// Setup the base controller
var controller = {
/**
* Initialize the common scope variables
*
* @protected
* @method initializeScope
*/
initializeScope: function() {
$scope.ui = {
docrootByDomain: PAGE.docrootByDomain,
domainList: Object.keys(PAGE.docrootByDomain),
user: userService.emptyUser()
};
$scope.isOverQuota = !quotaInfo.under_quota_overall;
$scope.ui.user.domain = PAGE.primaryDomain; // TODO: Add nvdata here for last selected domain, fallback to primaryDomain
$scope.ui.user.services.ftp.homedir = PAGE.docrootByDomain[PAGE.primaryDomain] + "/";
$scope.ui.user.services.webdisk.homedir = PAGE.docrootByDomain[PAGE.primaryDomain] + "/";
$scope.inProgress = false;
$scope.minimumPasswordStrength = angular.isDefined(PAGE.minimumPasswordStrength) ? parseInt(PAGE.minimumPasswordStrength, 10) : DEFAULT_PASSWORD_STRENGTH;
$scope.emailDaemon = emailDaemonInfo;
$scope.ftpDaemon = ftpDaemonInfo;
$scope.webdiskDaemon = webdiskDaemonInfo;
$scope.features = features;
$scope.defaults = defaultInfo;
$scope.quotaInfo = quotaInfo;
$scope.useCandidateServices = this.useCandidateServices;
$scope.insertSubAndRemoveDupes = this.insertSubAndRemoveDupes;
},
/**
* Initialize the common view stuff
*
* @protected
* @method initializeView
*/
initializeView: function() {
alertService.clear();
this.showCpanelOverQuotaWarning();
},
/**
* Call this when this view is loaded first and a new record is
* created that does not appear in the prefetch data.
*
* @protected
* @method clearPrefetch
*/
clearPrefetch: function() {
app.firstLoad.userList = false;
},
/**
* Update the view model's service object with the candidate service information from a user
* object (either another or itself).
*
* @method useCandidateServices
* @param {Object} destUser The user model to update.
* @param {Object} srcUser The source user model that contains the candidate_services
* that will be integrated into the destUser.
*/
useCandidateServices: function(destUser, srcUser) {
userService.integrateCandidateServices(destUser, srcUser);
},
/**
* Inserts a subaccount and any of its dismissed service accounts into a user list and removes
* any duplicates it might find. This works off of the premise that you can only ever have one
* instance of a service account per username/domain.
*
* @method insertSubAndRemoveDupes
* @param {Object} newUser The user to insert. It can be a duplicate of one in the userList
* because it will ultimately just replace the old one.
* @param {Array} userList The list of user objects into which newUser will be inserted.
*/
insertSubAndRemoveDupes: function(newUser, userList) {
var startingIndex = _.sortedIndexBy(userList, newUser, "full_username");
// Get a list of all services that are enabled on the latest version of the subaccount.
var enabledServices = [];
angular.forEach(newUser.services, function(service, serviceName) {
if (service.enabled) {
enabledServices.push(serviceName);
}
});
// Also include any services that are enabled in dismissed service accounts since we'll
// be inserting them as well.
if (newUser.dismissed_merge_candidates) {
newUser.dismissed_merge_candidates.forEach(function(serviceAccount) {
enabledServices.push(serviceAccount.service);
});
}
// Loop over all users in the userList with the same full_username and remove if they
// have the same services enabled or if they aren't a service account (ex. hypotheticals
// or a previous version of the subaccount).
var index = startingIndex;
var splice, user, serviceName;
while (userList[index] && userList[index].full_username === newUser.full_username) {
user = userList[index];
if (user.type !== "service") {
splice = true;
} else {
// Loop over the service names that are enabled in newUser. If any of those services
// are enabled on the current user in the list, mark it for splicing. Also mark it if
// it's not a service account, because service accounts are the only type of account
// that can co-exist with newUser in the userList.
for (var esi = 0, esl = enabledServices.length; esi < esl; esi++) {
serviceName = enabledServices[esi];
if (user.services[serviceName].enabled) {
splice = true;
break;
}
}
}
if (splice) {
userList.splice(index, 1);
} else {
index++;
}
}
// Finally, splice in newUser and the dismissed users.
var usersToInsert = userService.expandDismissed(newUser);
userList.splice.apply(userList, [startingIndex, 0].concat(usersToInsert));
},
/**
* Shows a dire warning if the cPanel account is over quota.
*
* @method showCpanelOverQuotaWarning
*/
showCpanelOverQuotaWarning: function() {
if ($scope.isOverQuota) {
alertService.add({
message: LOCALE.maketext("Your [asis,cPanel] account exceeds its disk quota. You cannot add or edit users."),
type: "danger",
id: "over-quota-warning",
replace: false,
counter: false
});
}
}
};
return controller;
};
return factory;
}
);
/*
# user_manager/views/addController.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/addController',[
"angular",
"lodash",
"cjt/util/locale",
"app/views/addEditController",
"cjt/directives/alertList",
"cjt/directives/bytesInput",
"cjt/directives/toggleLabelInfoDirective",
"cjt/directives/toggleSwitchDirective",
"cjt/directives/labelSuffixDirective",
"cjt/services/alertService",
"app/services/userService",
"cjt/services/dataCacheService"
],
function(angular, _, LOCALE, baseCtrlFactory) {
// Retrieve the current application
var app = angular.module("App");
// Setup the controller
var controller = app.controller(
"addController", [
"$scope",
"$routeParams",
"$timeout",
"$location",
"$anchorScroll",
"userService",
"alertService",
"directoryLookupService",
"dataCache",
"defaultInfo",
"quotaInfo",
"emailDaemonInfo",
"ftpDaemonInfo",
"webdiskDaemonInfo",
"features",
"spinnerAPI",
function(
$scope,
$routeParams,
$timeout,
$location,
$anchorScroll,
userService,
alertService,
directoryLookupService,
dataCache,
defaultInfo,
quotaInfo,
emailDaemonInfo,
ftpDaemonInfo,
webdiskDaemonInfo,
features,
spinnerAPI
) {
var baseCtrl = baseCtrlFactory($scope, userService, emailDaemonInfo, ftpDaemonInfo, webdiskDaemonInfo, features, defaultInfo, quotaInfo, alertService);
/**
* Setup the scope for this controller.
*
* @method initializeScope
*/
var initializeScope = function() {
baseCtrl.initializeScope();
$scope.ui.user.sendInvite = $scope.ui.isInviteSubEnabled = !!window.PAGE.isInviteSubEnabled;
};
/**
* Setup the view for this controller.
*
* @method initializeView
*/
var initializeView = function() {
baseCtrl.initializeView();
};
initializeScope();
initializeView();
/**
* Toggle the service enabled state.
* @param {Object} service Specific service state from the user.services collection containing:
* @param {Boolean} service.enabled True if enabled, false otherwise
*/
$scope.toggleService = function(service) {
service.enabled = !service.enabled;
};
/**
* Create new user and then handle subsequent updating of the shared userList. If navigation is requested, it will
* move back to the list view. If navigation is suppressed, it will reset the form to a well known state and focus
* the first element so the user can start entering data again.
* @param {Object} user
* @param {Boolean} leave if true will navigate back to the list. if false, will clear the form and set focus on the full name.
*/
$scope.create = function(user, leave) {
$scope.inProgress = true;
alertService.clear();
// scroll to the button on submit
$anchorScroll("btn-create");
return userService
.create(user)
.then(function(user) {
var query;
var cachedUserList = dataCache.get("userList");
if (cachedUserList) {
$scope.insertSubAndRemoveDupes(user, cachedUserList);
dataCache.set("userList", cachedUserList);
query = { loadFromCache: true };
} else {
query = { loadFromCache: false };
}
baseCtrl.clearPrefetch();
alertService.add({
type: "success",
message: LOCALE.maketext("You successfully created the following user: [_1]", (user.real_name || user.full_username)),
id: "createSuccess",
autoClose: 10000
});
if (leave) {
$scope.loadView("list/rows", query);
} else {
var lastDomain = $scope.ui.user.domain;
initializeScope();
// Preserve the last domain so we can create
// a number of accounts on the same domain
$scope.ui.user.domain = lastDomain;
$scope.form.$setPristine();
// Scroll to the top of the form to restart
$anchorScroll("top");
$timeout(function() {
// Set the focus on the first field
var el = angular.element("#full-name");
if (el) {
el.focus();
}
}, 10);
}
}, function(error) {
var name = user.real_name || (user.username + "@" + user.domain);
error = error.error || error;
alertService.add({
type: "danger",
message: LOCALE.maketext("The system failed to create the “[_1]” user with the following error: [_2]", name, error),
id: "createError"
});
$anchorScroll("top");
var cachedUserList = dataCache.get("userList");
if (error.user && cachedUserList) {
$scope.insertSubAndRemoveDupes(error.user, cachedUserList);
dataCache.set("userList", cachedUserList);
}
baseCtrl.clearPrefetch();
})
.finally(function() {
$scope.inProgress = false;
});
};
// Handle the case where the user clears the homedir box
// and needs to know what the home directory folders are?
// Normally, the typeahead wont send this.
$scope.$watch("ui.user.services.ftp.homedir", function() {
if (!$scope.ui.user.services.ftp.homedir &&
$scope.form.txtFtpHomeDirectory &&
!$scope.form.txtFtpHomeDirectory.$pristine) {
$scope.form.txtFtpHomeDirectory.$setViewValue("/");
}
});
$scope.$watch("ui.user.services.webdisk.homedir", function() {
if (!$scope.ui.user.services.webdisk.homedir &&
$scope.form.txtWebDiskHomeDirectory &&
!$scope.form.txtWebDiskHomeDirectory.$pristine) {
$scope.form.txtWebDiskHomeDirectory.$setViewValue("/");
}
});
// Update the home directories as the user types
$scope.$watch("ui.user.username + '@' + ui.user.domain", function(newValue, oldValue) {
var parts = newValue.split("@");
// Update the ftp homedir
if (!$scope.ui.user.services.ftp.isCandidate && $scope.form.txtFtpHomeDirectory && $scope.form.txtFtpHomeDirectory.$pristine) {
$scope.ui.user.services.ftp.homedir = $scope.ui.docrootByDomain[parts[1]] + "/" + parts[0];
}
// Update the webdisk homedir
if (!$scope.ui.user.services.webdisk.isCandidate && $scope.form.txtWebDiskHomeDirectory && $scope.form.txtWebDiskHomeDirectory.$pristine) {
$scope.ui.user.services.webdisk.homedir = $scope.ui.docrootByDomain[parts[1]] + "/" + parts[0];
}
});
}
]
);
return controller;
}
);
/*
# user_manager/views/addController.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/editController',[
"angular",
"lodash",
"cjt/util/locale",
"app/views/addEditController",
"cjt/directives/alertList",
"cjt/directives/toggleLabelInfoDirective",
"cjt/directives/toggleSwitchDirective",
"cjt/services/alertService",
"cjt/directives/spinnerDirective",
"app/directives/issueList",
"app/services/userService",
"cjt/services/dataCacheService",
],
function(angular, _, LOCALE, baseCtrlFactory) {
// Retrieve the current application
var app = angular.module("App");
// Setup the controller
var controller = app.controller(
"editController", [
"$scope",
"$route",
"$routeParams",
"$timeout",
"$location",
"$anchorScroll",
"userService",
"alertService",
"spinnerAPI",
"dataCache",
"defaultInfo",
"quotaInfo",
"emailDaemonInfo",
"ftpDaemonInfo",
"webdiskDaemonInfo",
"features",
function(
$scope,
$route,
$routeParams,
$timeout,
$location,
$anchorScroll,
userService,
alertService,
spinnerAPI,
dataCache,
defaultInfo,
quotaInfo,
emailDaemonInfo,
ftpDaemonInfo,
webdiskDaemonInfo,
features
) {
var baseCtrl = baseCtrlFactory($scope, userService, emailDaemonInfo, ftpDaemonInfo, webdiskDaemonInfo, features, defaultInfo, quotaInfo, alertService);
/**
* Setup the scope for this controller.
*
* @method initializeScope
* @private
*/
var initializeScope = function() {
baseCtrl.initializeScope();
};
/**
* Setup the view for this controller.
*
* @method initializeView
* @private
*/
var initializeView = function() {
baseCtrl.initializeView();
};
initializeScope();
initializeView();
/**
* Toggle the service enabled state.
*
* @method toggleService
* @scope
* @param {Object} service Specific service state from the user.services collection containing:
* @param {Boolean} service.enabled True if enabled, false otherwise
*/
$scope.toggleService = function(service) {
service.enabled = !service.enabled;
};
/**
* Update a user
*
* @method updateUser
* @private
* @param {Object} user
* @return {Promise}
*/
function updateUser(user) {
spinnerAPI.start("loadingSpinner");
$scope.ui.isSaving = true;
return userService.edit(user).then(function(user) {
// Update the item in the list
var cachedUserList = dataCache.get("userList");
var loadFromCache = false;
if (cachedUserList) {
$scope.insertSubAndRemoveDupes(user, cachedUserList);
dataCache.set("userList", cachedUserList);
loadFromCache = true;
}
spinnerAPI.stop("loadingSpinner");
$scope.ui.isSaving = false;
$scope.loadView("list/rows", { loadFromCache: loadFromCache });
alertService.add({
type: "success",
message: LOCALE.maketext("The system successfully updated the following user: [_1]", user.full_username),
id: "updateUserSuccess",
autoClose: 10000
});
}, function(error) {
error = error.error || error;
alertService.clear();
alertService.add({
type: "danger",
message: LOCALE.maketext("The system failed to update the “[_1]” user with the following error: [_2]", user.full_username, error),
id: "updateFailedErrorServer"
});
spinnerAPI.stop("loadingSpinner");
$scope.ui.isSaving = false;
$anchorScroll("top");
});
}
/**
* Promote a service into a user and update with the other changes
*
* @method updateService
* @private
* @param {Object} user
* @return {Promise}
*/
function updateService(user) {
spinnerAPI.start("loadingSpinner");
$scope.ui.isSaving = true;
if (!$scope.canPromote(user)) {
// Use the old APIs since it can't be promoted
return userService.editService(user, $scope.ui.originalService).then(function() {
// We don't get back the data from the old apis, so for now,
// just reload the whole lister from an ajax call.
$scope.loadView("list/rows", { loadFromCache: false });
alertService.add({
type: "success",
message: LOCALE.maketext("The system successfully modified the service account: [_1]", user.full_username),
id: "updateServiceSuccess",
autoClose: 10000
});
}).catch(function(error) {
alertService.add({
type: "danger",
message: LOCALE.maketext("The system failed to modify the service account for “[_1]”: [_2]", user.full_username, error),
id: "updateServiceFailed",
});
$anchorScroll("top");
}).finally(function() {
spinnerAPI.stop("loadingSpinner");
$scope.ui.isSaving = false;
});
} else {
// Promote to a subaccount with a forced link and then perform the update.
return userService.link(user, $scope.ui.originalServiceType, true).then(function(sub) {
// It should be a subaccount now. Save the updated user back to the userList.
var cachedUserList = dataCache.get("userList");
$scope.insertSubAndRemoveDupes(user, cachedUserList);
dataCache.set("userList", cachedUserList);
// Now stage the edits
user.type = "sub";
user.guid = sub.guid;
return updateUser(user);
}, function(error) {
alertService.clear();
alertService.add({
type: "danger",
message: LOCALE.maketext("The system failed to upgrade the “[_1]” service account to a [asis,subaccount] with the following error: [_2]", user.full_username, error),
id: "updateFailedErrorServer"
});
$anchorScroll("top");
}).finally(function() {
spinnerAPI.stop("loadingSpinner");
$scope.ui.isSaving = false;
});
}
}
/**
* Update the user with the properties that have changed.
*
* @method update
* @scope
* @param {Object} user
* @return {Promise}
*/
$scope.update = function(user) {
$anchorScroll("btn-save");
switch ($scope.mode) {
case "subaccount":
return updateUser(user);
case "service":
return updateService(user);
default:
alertService.clear();
alertService.add({
type: "danger",
message: LOCALE.maketext("The system did not recognize the update mode: [_1]", $scope.mode),
id: "updateUnrecognizedMode"
});
return;
}
};
/**
* Test if there is an async server-side request running.
*
* @method isInProgress
* @return {Boolean} true if a request is being processed on the server. false otherwise.
*/
$scope.isInProgress = function() {
return $scope.ui.isSaving || $scope.ui.isLoading;
};
/**
* Unlink the specific service from the user.
*
* @method unlinkService
* @param {Object} user Definition of the subaccount from which to unlink a service
* @param {String} serviceType The name of the service to unlink
* @return {Promise}
*/
$scope.unlinkService = function(user, serviceType) {
spinnerAPI.start("loadingSpinner");
$scope.ui.isSaving = true;
return userService.unlink(user, serviceType).then(function() {
// Invalidate the cache
dataCache.remove("userList");
// Load the subuser
return loadSubuser(user.guid).then(function() {
spinnerAPI.stop("loadingSpinner");
$scope.ui.isSaving = false;
alertService.add({
type: "success",
message: LOCALE.maketext("The system successfully unlinked the “[_1]” service.", serviceType),
id: "unlinkServiceSuccess",
autoClose: 10000
});
});
}, function(error) {
alertService.clear();
alertService.add({
type: "danger",
message: LOCALE.maketext("The system failed to unlink the “[_1]” service with the following error: [_2]", serviceType, error),
id: "unlinkServiceFailed"
});
spinnerAPI.stop("loadingSpinner");
$scope.ui.isSaving = false;
$anchorScroll("top");
});
};
/**
* Check if this user is allowed to edit the service. It should be allowed if:
* 1) The user can be promoted to a subaccount
* 2) The user cannot be promoted, but the service is already enabled
* Otherwise, it should not allow.
*
* @method isAllowed
* @scope
* @param {Object} user
* @param {Object} service
* @return {Boolean} true if the service can be edited, false otherwise.
*/
$scope.isAllowed = function(user, service) {
// If you can promote, then all services are on the table.
// Otherwise, only the one currently enabled is allowed.
return $scope.canPromote(user) || service.enabled;
};
/**
* Check if we need to set the password to modify either enabled digest
* or enable webdisk service with digest.
*
* @method _needsPassword
* @private
* @param {Object} user Current user
* @param {Object} originalService Original webdisk configuration at load time in the editor.
* @return {Boolean} true if we need to also set the password, false otherwise.
*
*/
function _needsPassword(user, originalService) {
if ((user.type === "service" &&
((originalService.enabled === false) ||
(originalService.enabledigest === false))) ||
(user.type === "sub" &&
((originalService.enabled === false) ||
(originalService.enabledigest === false)))) {
// ------------------------------------------------------------------
// TODO: The above condition makes you change your password more then
// should be required. Actually we need to see if the digest auth
// hash is stored for the sub-account, but we don't have that ability
// right now, so forcing all changes to require a password. Fix this
// in case LC-3185. It should be something like:
//
// if ((user.type === "service" &&
// originalService.enabledigest === false) ||
// (user.type === "sub" && !user.has_digest_auth_hash &&
// ( (originalService.enabled === false) ||
// (originalService.enabledigest === false)))) {
//
// ------------------------------------------------------------------
return true;
} else {
return false;
}
}
/**
* Check if we can enable the digest controls.
*
* @method canEnabledDigest
* @scope
* @param {Object} user
* @return {Boolean} true if we can enable the digest auth checkbox, false otherwise.
*/
$scope.canEnableDigest = function(user) {
if (_needsPassword(user, $scope.ui.originalServices["webdisk"])) {
// Password must be defined to enable the digest controls
// when using the older style api calls since we don't have
// a call for enabling/disabling digest without the password
// in these older style apis or if a service has been merged,
// but does not share the password with sub-account.
//
return user.password ? true : false;
} else {
return true;
}
};
/**
* Check if we should show the warning about requiring the password
* to enabled/disable digest auth.
*
* @method showDigestRequiresPasswordWarning
* @scope
* @param {Object} user
* @return {Boolean} true if we should show the warning, false otherwise.
*/
$scope.showDigestRequiresPasswordWarning = function(user) {
return _needsPassword(user, $scope.ui.originalServices["webdisk"]) &&
user.services["webdisk"].enabled;
};
/**
* Check to see if we should show the Unlink button for the service.
* @param {Object} user The subaccount for which the unlink would occur if permitted.
* @param {Object} serviceType The service type being checked.
* @return {Boolean} If true, show the Unlink button.
*/
$scope.showUnlink = function(user, serviceType) {
return !user.synced_password &&
!user.services[serviceType].isNew &&
!user.services[serviceType].isCandidate;
};
/**
* Check if this user is allowed to turn on/off service. It should be allowed if:
* 1) The user is of type sub
* 2) The user is of type service and has no siblings and has not been dismissed
* Otherwise, it should not allow.
*
* @method canPromote
* @scope
* @param {Object} user
* @return {Boolean} true if the service can toggled, false otherwise.
*/
$scope.canPromote = function(user) {
if (user.type === "sub") {
return true;
} else if (user.type === "service") {
if (user.has_siblings || // If it has siblings the the user has not elected what to do with this service account yet, so it needs to be linked or dismissed first
user.sub_account_exists ) { // If it has an existing subaccount, then the account should remain independent now so you can not enable/disable the service as part of the service account, but must delete it instead to do this.
return false;
} else {
return true;
}
}
};
// Handle the case where the user clears the homedir box
// and needs to know what the home directory folders are?
// Normally, the typeahead wont send this.
$scope.$watch("ui.user.services.ftp.homedir", function() {
if (!$scope.ui.user.services.ftp.homedir &&
$scope.form.txtFtpHomeDirectory &&
!$scope.form.txtFtpHomeDirectory.$pristine) {
$scope.form.txtFtpHomeDirectory.$setViewValue("/");
}
});
$scope.$watch("ui.user.services.webdisk.homedir", function() {
if (!$scope.ui.user.services.webdisk.homedir &&
$scope.form.txtWebDiskHomeDirectory &&
!$scope.form.txtWebDiskHomeDirectory.$pristine) {
$scope.form.txtWebDiskHomeDirectory.$setViewValue("/");
}
});
// Make sure that only orignal services are enabled if the
// passwords are not synced, since we can not add services
// unless they provide a password in this case.
$scope.$watch("ui.user.password", function(value) {
if (value === "" && !$scope.canAddServices($scope.ui.user)) {
// Restore services to their original enabled state
// since you must provide a password to enable them.
["email", "ftp", "webdisk"].forEach(function(name) {
$scope.ui.user.services[name].enabled = $scope.ui.originalServices[name].enabled;
});
}
});
/**
* Test if we can add services.
*
* @method canAddServices
* @scope
* @param {Object} user
* @return {Boolean} true if the user can add services, false otherwise
*/
$scope.canAddServices = function(user) {
if (user.synced_password) {
return true;
} else {
// We must have a password to add services, and it will sync all them.
return !!user.password;
}
};
/**
* Load a sub user into the view
*
* @method loadSubuser
* @private
* @param {String} guid Unique identifier
*/
function loadSubuser(guid) {
if (!guid) {
alertService.clear();
alertService.add({
type: "warn",
message: LOCALE.maketext("You did not select a [asis,subaccount]."),
id: "missingUserWarning"
});
$scope.loadView("list/rows", { loadFromCache: true });
} else {
$scope.ui.isLoading = true;
$scope.ui.user = null;
spinnerAPI.start("loadingSpinner");
$scope.ui.user = userService.emptyUser();
return userService.fetchUser($routeParams.guid).then(
function(user) {
$scope.ui.user = user;
$scope.ui.originalServices = _.cloneDeep(user.services);
// Set the service values to those from the candidates
$scope.useCandidateServices(user, user);
// The API doesn't consider the invitation status to be an issue, but we will
// add it to the issue list for display purposes here on the edit screen.
userService.addInvitationIssues(user);
spinnerAPI.stop("loadingSpinner");
$scope.ui.isLoading = false;
},
function(error) {
alertService.clear();
alertService.add({
type: "warn",
message: LOCALE.maketext("The system could not load the [asis,subaccount] with the following error: [_1]", error),
id: "missingUserWarning"
});
$scope.loadView("list/rows", { loadFromCache: true });
});
}
}
/**
* Load the service by type and username
*
* @method loadService
* @private
* @param {String} type email|ftp|webdisk
* @param {String} fullUsername <username>@<domain>
*/
function loadService(type, fullUsername) {
if (!type || !fullUsername) {
alertService.clear();
alertService.add({
type: "warn",
message: LOCALE.maketext("You did not select a valid service account."),
id: "missingUserWarning"
});
$scope.loadView("list/rows", { loadFromCache: true });
} else {
$scope.ui.isLoading = true;
$scope.ui.user = null;
spinnerAPI.start("loadingSpinner");
$scope.ui.user = userService.emptyUser();
return userService.fetchService(type, fullUsername).then(
function(user) {
$scope.ui.user = user;
if ( type === "email" && user.services.email.quota === 0 && $scope.defaults.email.unlimitedValue !== 0 ) {
user.services.email.quota = $scope.defaults.email.unlimitedValue;
}
$scope.ui.originalService = _.cloneDeep(user.services[type]);
$scope.ui.originalServiceType = type;
$scope.ui.originalServices = _.cloneDeep(user.services);
$scope.ui.user.synced_password = true;
spinnerAPI.stop("loadingSpinner");
$scope.ui.isLoading = false;
},
function(error) {
alertService.clear();
alertService.add({
type: "warn",
message: LOCALE.maketext("The system could not load the service account with the following error: [_1]", error),
id: "missingServiceWarning"
});
$scope.loadView("list/rows", { loadFromCache: true });
}).finally(function() {
if ($scope.ui.user && !$scope.canPromote($scope.ui.user)) {
alertService.add({
type: "warn",
message: LOCALE.maketext("The system cannot upgrade this service account to a [asis,subaccount]. To access all the features within this interface, you must delete any accounts that share the same username or link this service account to a [asis,subaccount]."),
id: "cannotPromoteWarning"
});
}
});
}
}
/**
* Show the unsynced password warning if appropriate.
*
* @private
* @method showUnsyncedPasswordWarning
*/
function showUnsyncedPasswordWarning() {
if (!$scope.ui.user.synced_password) {
alertService.add({
type: "warn",
message: LOCALE.maketext("You cannot enable additional services for this [asis,subaccount] until you set its password. When you set the password, all of your services will utilize the same password."),
id: "unsyncedPasswordWarning",
replace: false,
counter: false
});
}
}
/**
* Performs the link and dismiss operations on any merge candidate services
* that have been flagged with willLink or willDismiss.
*
* @method linkServices
* @param {Object} user The user whose candidate services will be processed.
*/
$scope.linkServices = function(user) {
spinnerAPI.start("loadingSpinner");
$scope.ui.isSaving = true;
return userService.linkAndDismiss(user).then(function(result) {
var cachedUserList = dataCache.get("userList");
if (cachedUserList) {
$scope.insertSubAndRemoveDupes(result, cachedUserList);
dataCache.set("userList", cachedUserList);
}
$scope.ui.user.synced_password = result.synced_password;
result.linked_services.forEach(function(serviceName) {
$scope.ui.user.services[serviceName] = result.services[serviceName];
$scope.ui.originalServices[serviceName] = _.cloneDeep(result.services[serviceName]);
});
alertService.add({
type: "success",
message: result.synced_password ?
LOCALE.maketext("The system successfully linked the service account to the “[_1]” user’s [asis,subaccount]. The service account passwords have not changed.", result.full_username) :
LOCALE.maketext("The system successfully linked the service account to the “[_1]” user’s [asis,subaccount]. The service account passwords have not changed. You must provide a new password if you enable any additional [asis,subaccount] services.", result.full_username),
id: "link-user-success",
replace: false
});
}).catch(function(error) {
alertService.add({
type: "danger",
message: error.error ? error.error : error,
id: (error.call === "link") ? "link-error" : "link-and-dismiss-error"
});
$anchorScroll("top");
}).finally(function() {
$scope.ui.isSaving = false;
spinnerAPI.stop("loadingSpinner");
});
};
if (/^\/edit\/subaccount/.test($route.current.originalPath)) {
$scope.mode = "subaccount";
loadSubuser($routeParams.guid).finally(showUnsyncedPasswordWarning);
} else if (/^\/edit\/service/.test($route.current.originalPath)) {
$scope.mode = "service";
loadService($routeParams.type, $routeParams.user).finally(showUnsyncedPasswordWarning);
}
}
]
);
return controller;
}
);
/*
# user_manager/services/serverInfoService 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/services/serverInfoService',[
// Libraries
"angular",
"lodash",
// CJT
"cjt/util/parse",
],
function(angular, _, PARSER) {
// Fetch the current application
var app = angular.module("App");
/**
* Setup the domainlist models API service
*/
app.factory("serverInfoService", [ function() {
var self = {
/**
* Helper method that remodels the ssl server information for use in javascript
* @param {Object} sslInfo - SSL information object retrieved from the server.
* @return {Object} Sanitized data structure.
* Containing the following:
* @param {String} cert_match_method
* @param {Date String} cert_valid_not_after
* @param {Boolean} is_self_signed
* @param {Boolean} is_wild_card
* @param {Boolean} is_valid
* @param {String} ssldomain
* @param {Boolean} ssldomain_matches_cert
*/
prepareSslInfo: function(sslInfo) {
// Normalize the date
sslInfo.cert_valid_not_after = new Date(sslInfo.cert_valid_not_after * 1000);
sslInfo.cert_valid = new Date() < sslInfo.cert_valid_not_after;
// Normalize the booleans
sslInfo.is_self_signed = PARSER.parsePerlBoolean(sslInfo.is_self_signed);
sslInfo.is_wild_card = PARSER.parsePerlBoolean(sslInfo.is_wild_card);
sslInfo.ssldomain_matches_cert = PARSER.parsePerlBoolean(sslInfo.ssldomain_matches_cert);
return sslInfo;
},
/**
* Helper method that remodels the ftp daemon info for use in javascript
* @param {Object} daemon - Damon object passed from the backend.
* @return {Object} Sanitized data structure.
*/
prepareFtpDaemonInfo: function(daemon) {
// Normalize the booleans
daemon.enabled = PARSER.parsePerlBoolean(daemon.enabled);
daemon.supports.quota = PARSER.parsePerlBoolean(daemon.supports.quota);
daemon.supports.login_without_domain = PARSER.parsePerlBoolean(daemon.supports.login_without_domain);
return daemon;
},
/**
* Helper method to remodel the default data passed from the backend
* @param {Object} defaults - Defaults object passed from the backend with a property for each service
* The service includes the following structure:
*
* @param {Number} default_quota - When the user chooses to limit the quota, this is the default value filled it the textbox.
* @param {Number} default_value - The true default for the control (0 unlimited, otherwise, limit to the value)
* @param {Boolean} select_unlimited - Select unlimited by default.
* @param {Number} max_quota - Maximum quota allowed.
* @return {[type]} [description]
*/
prepareDefaultInfo: function(defaults) {
_.each(["email", "ftp", "webdisk"], function(serviceName) {
var service = defaults[serviceName];
_.each(["default_quota", "default_value", "max_quota", "unlimitedValue"], function(fieldName) {
service[fieldName] = parseInt(service[fieldName], 10);
if (isNaN(service[fieldName])) {
service[fieldName] = 0;
}
});
service.select_unlimited = PARSER.parsePerlBoolean(service.select_unlimited);
});
return defaults;
},
/**
* Helper method that remodels the cpanel account's quota info passed from the backend.
*
* @method prepareQuotaInfo
* @param {Object} quotaInfo The quota information from the backend.
* @return {Object} Remodeled data structure.
*/
prepareQuotaInfo: function(quotaInfo) {
return self.parseObj(quotaInfo, {
under_megabyte_limit: PARSER.parsePerlBoolean,
under_inode_limit: PARSER.parsePerlBoolean,
under_quota_overall: PARSER.parsePerlBoolean,
inodes_used: PARSER.parseInteger,
inode_limit: PARSER.parseInteger,
inodes_remain: PARSER.parseInteger,
megabytes_used: PARSER.parseNumber,
megabyte_limit: PARSER.parseNumber,
megabytes_remain: PARSER.parseNumber
});
},
/**
* Parses properties on an object according to a map.
*
* @method parseObj
* @param {Object} obj The object to process.
* @param {Object} parseMap A map of property names to transformation methods. The method value
* for a particular key will be used to process the property value on
* the target object.
* @return {Object} The original object, which has now been processed.
*/
parseObj: function(obj, parseMap) {
angular.forEach(parseMap, function(parseFn, key) {
obj[key] = parseFn( obj[key] );
});
return obj;
}
};
return self;
}]);
}
);
/*
# user_manager/index.js Copyright(c) 2020 cPanel, L.L.C.
# All rights reserved.
# copyright@cpanel.net http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
*/
/* global require: false, define: false, PAGE: false */
define(
'app/index',[
"angular",
"jquery",
"cjt/core",
"cjt/modules",
"ngRoute",
"uiBootstrap"
],
function(angular, $, CJT) {
return function() {
// First create the application
angular.module("App", [
"ngRoute",
"ui.bootstrap",
"cjt2.cpanel",
"cpanel.services.directoryLookup"
]);
// Then load the application dependencies
var app = require(
[
// Application Modules
"cjt/bootstrap",
"cjt/views/applicationController",
"cjt/services/autoTopService",
"app/views/listController",
"app/views/addController",
"app/views/editController",
"app/services/serverInfoService"
], function(BOOTSTRAP) {
var app = angular.module("App");
app.firstLoad = {
userList: true,
};
// setup the email server service data for the application
app.factory("emailDaemonInfo", function() {
return {
enabled: PAGE.isEmailRunning,
name: "exim",
supports: {
quota: true
}
};
});
// setup the ftp server service data for the application
app.factory("ftpDaemonInfo", [
"serverInfoService",
function(serverInfoService) {
return serverInfoService.prepareFtpDaemonInfo(PAGE.ftpDaemonInfo);
}
]);
// setup the webdisk server service data for the application
app.factory("webdiskDaemonInfo", function() {
return {
enabled: PAGE.isWebdavRunning,
name: "cpdavd",
supports: {
quota: false
}
};
});
// setup the ssl data for the server
app.factory("sslInfo", [
"serverInfoService",
function(serverInfoService) {
return serverInfoService.prepareSslInfo(PAGE.sslInfo);
}
]);
// Provide the quota info for the cPanel account
app.factory("quotaInfo", [
"serverInfoService",
function(serverInfoService) {
return serverInfoService.prepareQuotaInfo(PAGE.quotaInfo);
}
]);
// setup the defaults for the various services.
app.factory("defaultInfo", [
"serverInfoService",
function(serverInfoService) {
return serverInfoService.prepareDefaultInfo(PAGE.serviceDefaults);
}
]);
// services this account is allowed to work with
// based on cpanel account feature control.
app.value("features", PAGE.features);
// routing
app.config([
"$routeProvider",
function(
$routeProvider
) {
$routeProvider.when("/list/cards", {
controller: "listController",
templateUrl: CJT.buildFullPath("user_manager/views/listCardsView.phtml")
});
$routeProvider.when("/list/rows", {
controller: "listController",
templateUrl: "user_manager/views/listRowsView.ptt"
});
$routeProvider.when("/add", {
controller: "addController",
templateUrl: "user_manager/views/addEditView.ptt"
});
$routeProvider.when("/edit/subaccount/:guid", {
controller: "editController",
templateUrl: "user_manager/views/editView.ptt"
});
$routeProvider.when("/edit/service/:type/:user", {
controller: "editController",
templateUrl: "user_manager/views/editView.ptt"
});
$routeProvider.otherwise({
"redirectTo": "/list/rows"
});
}
]);
app.run(["autoTopService", function(autoTopService) {
autoTopService.initialize();
}]);
BOOTSTRAP("#content", "App");
});
return app;
};
}
);