Viewing File: /usr/local/cpanel/base/frontend/jupiter/user_manager/services/userService.js

/* global define: false */

define(
    [

        // 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();
            }
        ]);
    }
);
Back to Directory File Manager