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

//                                      Copyright 2025 WebPros International, LLC
//                                                           All rights reserved.
// copyright@cpanel.net                                         http://cpanel.net
// This code is subject to the cPanel license. Unauthorized copying is prohibited.

/** @namespace cpanel.domains.services.domains */

define(
    [
        "angular",
        "lodash",
        "cjt/util/locale",
        "cjt/io/uapi-request",
        "cjt/io/api2-request",
        "cjt/io/uapi",
        "cjt/io/api2",
        "cjt/modules",
        "cjt/services/APICatcher",
        "cjt/services/cpanel/nvDataService",
    ],
    function(angular, _, LOCALE, UAPIRequest, API2Request) {

        "use strict";

        var app = angular.module("cpanel.domains.domains.service", [
            "cjt2.services.apicatcher",
            "cjt2.services.cpanel.nvdata",
        ]);

        // NVData key under which a user's prioritised TLDs are persisted so
        // they survive across sessions (CPANEL-53293). Namespaced to this
        // feature. The value is a pipe-delimited list of normalized,
        // dot-prefixed TLDs (e.g. ".com|.net"); an empty string clears it.
        var PRIORITIZED_TLDS_NVDATA_KEY = "domain_recommendations_prioritized_tlds";
        app.value("PAGE", PAGE);

        app.value("DOMAIN_TYPE_CONSTANTS", {
            SUBDOMAIN: "subdomain",
            ADDON: "addon",
            ALIAS: "alias",
            MAIN: "main_domain",
        });

        var CAN_EDIT_DOCROOT = {
            documentRoot: true,
        };

        // Client-side timeout for the domain recommendations call. The
        // upstream store (WHMCS) can be slow or hang; if the request never
        // settles the banner is stuck in its loading skeleton forever. After
        // this many milliseconds we reject so the banner hides instead.
        var RECOMMENDATIONS_TIMEOUT_MS = 10000;

        app.factory("domains", ["$q", "$timeout", "APICatcher", "DOMAIN_TYPE_CONSTANTS", "PAGE", "nvDataService", function($q, $timeout, APICatcher, DOMAIN_TYPE_CONSTANTS, PAGE, nvDataService) {

            /**
             * service wrapper for domain related functions
             *
             * @module domains
             *
             * @param  {Object} $q angular $q object
             * @param  {Object} $timeout angular $timeout object
             * @param  {Object} APICatcher cjt2 APICatcher service
             * @param  {Object} DOMIAIN_TYPE_CONSTANTS constants objects for use on domain types
             * @param  {Object} PAGE window.PAGE object
             *
             * @example
             * $domainsService.get()
             *
             */

            var _flattenedDomains;
            var _mainDomain;
            var _domainLookupMap = {};
            var _parkDomains;
            var _addOnDomains;
            var _subDomains;
            var _usageStats;

            // One-shot handoff for the "purchase a domain → Add Domain" flow
            // when it crosses a route change (Manage → List). The buyer's
            // domain is stashed here instead of a query param so consuming it
            // does not mutate the URL and trigger an ngRoute reload that would
            // tear down the freshly-opened chooser.
            var _pendingAddDomain = null;

            function _trackFrontendMixpanel(eventName) {
                if (window.mixpanel && typeof window.mixpanel.track === "function") {
                    window.mixpanel.track(eventName, {});
                }
            }

            var Domain = function Domain(domainObject) {

                var self = this;

                Object.keys(domainObject).forEach(function(key) {
                    self[key] = domainObject[key];
                });

                self.protocol = self.isHttpsRedirecting ? "https" : "http";
                self.isWildcard = self["domain"] && self["domain"].substr(0, 1) === "*";
                self.canBeSuggested = !self.isWildcard;

            };

            var _domainTypes = [
                {
                    label: LOCALE.maketext("Subdomain"),
                    value: DOMAIN_TYPE_CONSTANTS.SUBDOMAIN,
                    requiresCustomDocumentRoot: true,
                    stat: "subdomains",
                },
                {
                    label: LOCALE.maketext("Addon"),
                    value: DOMAIN_TYPE_CONSTANTS.ADDON,
                    requiresCustomDocumentRoot: true,
                    dependantStat: "subdomains",
                    stat: "addon_domains",
                },
                { label: LOCALE.maketext("Alias"), value: DOMAIN_TYPE_CONSTANTS.ALIAS, stat: "aliases" },
            ];

            var Domains = function() {};

            Domains.prototype = APICatcher;

            // -------- UTILS -------------

            Domains.prototype._cacheDomain = function _cacheDomain(domainObject) {

                if (!_flattenedDomains) {
                    _flattenedDomains = [];
                }

                var domain = new Domain(domainObject);

                if (_domainLookupMap[domain.domain]) {

                    // Domain already cached: replace it in-place so callers
                    // that mutate the existing entry see fresh data, and the
                    // shared array reference remains stable for any view
                    // bound to it.
                    var existing = _domainLookupMap[domain.domain];
                    angular.extend(existing, domain);

                    // The lookup map and flattenedDomains both point at the
                    // same object — extending in place updates both.
                } else {
                    _domainLookupMap[domain.domain] = domain;
                    _flattenedDomains.push(domain);
                }


                return domainObject;
            };

            Domains.prototype._uncacheDomain = function _uncacheDomain(domain) {
                var self = this;

                if (!_flattenedDomains) {
                    return false;
                }

                var domainObject = self._getDomainObject(domain);

                for (var i = _flattenedDomains.length - 1; i >= 0; i--) {
                    if (_flattenedDomains[i].domain === domainObject.domain) {
                        _flattenedDomains.splice(i, 1);
                    }
                }

                // Remove from the lookup map so stale entries with mutated flags
                // (e.g. canEdit: false set during removal) cannot be returned by
                // findDomainByName after the domain is gone.
                delete _domainLookupMap[domainObject.domain];
            };

            Domains.prototype.getCurrentDomains = function getCurrentDomains() {
                return _flattenedDomains;
            };

            Domains.prototype.invalidate = function invalidate() {
                _flattenedDomains = null;
                _domainLookupMap = {};
            };

            function _findStatById(_stats, id) {
                for (var statI in _stats) {
                    if (_stats.hasOwnProperty(statI) && _stats[statI].id === id) {
                        return _stats[statI];
                    }
                }
                return;
            }

            function _findDomainTypeByValue(value) {
                for (var domainTypeI in _domainTypes) {
                    if (_domainTypes.hasOwnProperty(domainTypeI) && _domainTypes[domainTypeI].value === value) {
                        return _domainTypes[domainTypeI];
                    }
                }
                return;
            }

            function _canCustomizeDocumentRoots() {
                return PAGE.hasWebServerRole === "1";
            }

            function _checkStatOverLimit(stat) {

                if (!stat) {
                    return;
                }

                var max = stat.maximum === null ? undefined : Number(stat.maximum);
                var usage = Number(stat.usage);

                if (!isNaN(max)) {

                    var per = usage / max;
                    if (max === 0 || per >= 1) {
                        return true;
                    }

                }

                return false;

            }

            Domains.prototype._getDomainObject = function _getDomainObject(domain) {
                var self = this;
                if (typeof domain === "string") {
                    return self.findDomainByName(domain);
                }

                return domain;
            };

            Domains.prototype._getSubDomainObject = function _getDomainObject(subdomain) {
                var self = this;
                if (typeof subdomain === "string") {
                    return self.findDomainByName(subdomain + "." + self.getMainDomain().domain);
                }

                return subdomain;
            };

            Domains.prototype._associateAddonDomains = function _associateAddonDomains() {
                var self = this;

                angular.forEach(_addOnDomains, function(addonDomain) {
                    var subdomainObject = self._getSubDomainObject(addonDomain.subdomain);
                    if (subdomainObject) {
                        subdomainObject.associatedAddonDomain = addonDomain.domain;
                    }
                });

            };

            // -------- \ UTILS -------------

            // -------- CREATE -------------

            /**
             * API Wrapper for adding a subdomain
             *
             * @method addSubdomain
             *
             * @param  {Object} domainObject object representing all the aspects of the domains
             *
             * @return {Promise<Object>} returns the api promise and then the newly added domain
             *
             */

            Domains.prototype.addSubdomain = function addSubdomain(domainObject) {
                var self = this;
                var apiCall = new API2Request.Class();
                apiCall.initialize("SubDomain", "addsubdomain");
                var subdomain = domainObject.subdomain.substr(0, domainObject.subdomain.length - (domainObject.domain.length + 1));
                apiCall.addArgument("domain", subdomain);
                apiCall.addArgument("rootdomain", domainObject.domain);
                apiCall.addArgument("canoff", "1");
                apiCall.addArgument("disallowdot", "0");
                apiCall.addArgument("dir", domainObject.fullDocumentRoot);

                return self.promise(apiCall).then(function(result) {
                    var domain = domainObject.newDomainName;
                    return self.fetchSingleDomainData(domain).then(function(updatedDomain) {
                        updatedDomain = angular.extend(updatedDomain, {
                            subdomain: updatedDomain.subdomain,
                            rootDomain: domainObject.domain,
                            type: DOMAIN_TYPE_CONSTANTS.SUBDOMAIN,
                            canEdit: PAGE.hasWebServerRole && CAN_EDIT_DOCROOT,
                            canRemove: true,
                        });
                        return self._cacheDomain(updatedDomain);
                    });
                });
            };

            /**
             * API Wrapper for adding an addon domain
             *
             * @method addAddonDomain
             *
             * @param  {Object} domainObject object representing all the aspects of the domains
             *
             * @return {Promise<Object>} returns the api promise and then the newly added domain
             *
             */

            Domains.prototype.addAddonDomain = function addAddonDomain(domainObject) {

                var self = this;

                var apiCall = new API2Request.Class();
                apiCall.initialize("AddonDomain", "addaddondomain");
                apiCall.addArgument("subdomain", domainObject.subdomain);
                apiCall.addArgument("newdomain", domainObject.newDomainName);
                apiCall.addArgument("ftp_is_optional", "1");
                apiCall.addArgument("dir", domainObject.documentRoot);
                apiCall.addArgument("create_temporary_domain", domainObject.createTemporaryDomain);

                return self.promise(apiCall).then(function(result) {
                    const newTemporaryDomainName = result.data ? result.data[0].domain : null;
                    return self.fetchSingleDomainData(domainObject.newDomainName || newTemporaryDomainName).then(function(updatedDomain) {
                        var addonDomain = angular.extend(angular.copy(updatedDomain), {
                            type: DOMAIN_TYPE_CONSTANTS.ADDON,
                            subdomain: updatedDomain.subdomain,
                            canEdit: PAGE.hasWebServerRole && CAN_EDIT_DOCROOT,
                            canRemove: true,
                            canRename: true,

                            // This accounts for a scenario where is_temporary attribute is not yet set
                            // on the first visit to manage domain after creating a temporary domain
                            // until the page gets reloaded
                            is_temporary: String(updatedDomain.is_temporary) === "1" || String(domainObject.createTemporaryDomain) === "1",
                        });
                        self._cacheDomain(angular.extend(angular.copy(updatedDomain), {
                            domain: updatedDomain.rootDomain,
                            subdomain: updatedDomain.subdomain,
                            type: DOMAIN_TYPE_CONSTANTS.SUBDOMAIN,
                            associatedAddonDomain: addonDomain,
                            canEdit: PAGE.hasWebServerRole && CAN_EDIT_DOCROOT,
                            canRemove: true,
                            is_temporary: String(updatedDomain.is_temporary) === "1" || String(domainObject.createTemporaryDomain) === "1",
                        }));
                        if (String(domainObject.createTemporaryDomain) === "1") {
                            _trackFrontendMixpanel("CP_USR:DOMAINS-TEMP_DOMAIN-CREATED");
                        }
                        return self._cacheDomain(addonDomain);
                    });

                });
            };

            /**
             * API Wrapper for adding an alias domain
             *
             * @method addAliasDomain
             *
             * @param  {Object} domainObject object representing all the aspects of the domains
             *
             * @return {Promise<Object>} returns the api promise and then the newly added domain
             *
             */

            Domains.prototype.addAliasDomain = function addAliasDomain(domainObject) {

                var self = this;

                var apiCall = new API2Request.Class();
                apiCall.initialize("Park", "park");
                apiCall.addArgument("domain", domainObject.newDomainName);

                return self.promise(apiCall).then(function() {
                    var parkedDomain = angular.copy(self.getMainDomain());
                    parkedDomain.domain = domainObject.newDomainName;
                    parkedDomain.type = DOMAIN_TYPE_CONSTANTS.ALIAS;
                    parkedDomain.canRemove = true;
                    parkedDomain.canRename = true;
                    return self._cacheDomain(parkedDomain);
                });
            };

            /**
             * Add a domain, automatically selecting APIs based on which domainType is set
             *
             * @method add
             *
             * @param  {Object} domainObject object representing all the aspects of the domains
             *
             * @return {Promise<Object>} returns the api promise and then the newly added domain
             *
             */

            Domains.prototype.add = function _addNewDomain(domainObject) {

                var self = this;

                var addNewPromise;

                if (domainObject.domainType === DOMAIN_TYPE_CONSTANTS.SUBDOMAIN) {
                    addNewPromise = self.addSubdomain(domainObject);
                } else if (domainObject.domainType === DOMAIN_TYPE_CONSTANTS.ADDON ) {
                    addNewPromise = self.addAddonDomain(domainObject);
                } else if (domainObject.domainType === DOMAIN_TYPE_CONSTANTS.ALIAS ) {
                    addNewPromise = self.addAliasDomain(domainObject);
                }

                addNewPromise.then(function(result) {

                    var domainType = _findDomainTypeByValue(domainObject.domainType);
                    var stat = _findStatById(self.getUsageStats(), domainType.stat);
                    if (stat) {
                        stat.usage++;
                    }
                    self.updateDomainTypeLimits();
                    return result;

                });

                return addNewPromise;

            };

            // -------- \ CREATE -------------

            /**
             * Convert a relative document root to a full document root based on the homedir and the PAGE.requirePublicHTMLSubs
             *
             * @method generateFullDocumentRoot
             *
             * @param  {String} relativeDocumentRoot document root relative to the homedir
             *
             * @return {String} returns the parsed document root
             *
             */

            Domains.prototype.generateFullDocumentRoot = function generateFullDocumentRoot(relativeDocumentRoot) {
                var self = this;

                var requirePublicHTMLSubs = PAGE.requirePublicHTMLSubs.toString() === "1";
                var fullDocumentRoot = self.getMainDomain().homedir + "/";
                if (requirePublicHTMLSubs) {
                    fullDocumentRoot += "public_html/";
                }
                fullDocumentRoot += relativeDocumentRoot ? relativeDocumentRoot.replace(/^\//, "") : "";
                return fullDocumentRoot;
            };

            // -------- READ -------------

            /**
             * Get the currently stored main domain
             *
             * @method getMainDomain
             *
             * @return {Object} returns the current main domain object
             *
             */

            Domains.prototype.getMainDomain = function _getMainDomain() {
                return _mainDomain;
            };

            /**
             * Find a domain object by the domain name
             *
             * @method findDomainByName
             *
             * @param  {String} domainName domain name (bob.com)
             *
             * @return {Object} returns the domain object if found
             *
             */
            Domains.prototype.findDomainByName = function _findDomainByName(domainName) {
                return _domainLookupMap[domainName];
            };

            /**
             * API Wrapper to fetch the main domain based on PAGE.mainDomain
             *
             * @method fetchSingleDomainData
             *
             * @return {Promise<Object>} returns a promise, then the single domain object
             *
             */

            Domains.prototype.fetchSingleDomainData = function fetchSingleDomainData(domain) {

                var self = this;
                var apiCall = new UAPIRequest.Class();
                apiCall.initialize("DomainInfo", "single_domain_data");
                apiCall.addArgument("domain", domain);
                apiCall.addArgument("return_https_redirect_status", 1);

                return self.promise(apiCall).then(function(result) {
                    var typeTranslated = 0;
                    if (result.data.type === "addon_domain") {
                        typeTranslated = DOMAIN_TYPE_CONSTANTS.ADDON;
                    } else if ( result.data.type === "sub_domain" ) {
                        typeTranslated = DOMAIN_TYPE_CONSTANTS.SUBDOMAIN;
                    }
                    return self.formatSingleDomain(result.data, typeTranslated);
                });
            };


            /**
             * API Wrapper to fetch the domains and cache them
             *
             * @method fetchDomains
             *
             * @return {Promise<Object>} returns a promise, then the main domain object
             *
             */

            Domains.prototype.fetchDomains = function fetchDomains() {

                var self = this;

                var apiCall = new UAPIRequest.Class();
                apiCall.initialize("DomainInfo", "domains_data");
                apiCall.addArgument("return_https_redirect_status", 1);

                return self.promise(apiCall).then(function(result) {
                    var mainDomain = self.formatSingleDomain(result.data.main_domain);
                    mainDomain.type =  DOMAIN_TYPE_CONSTANTS.MAIN;
                    mainDomain.canRemove = false;
                    mainDomain.canRename = !!(PAGE.features && PAGE.features.primarydomain_rename);
                    _mainDomain = mainDomain;
                    self._cacheDomain(_mainDomain);

                    // Cache (most of) the rest of the domains to speed this up
                    _subDomains = [];
                    var domains = result.data.sub_domains || [];
                    domains.forEach(function(rawDomain) {
                        var parsedDomain = self.formatSingleDomain(rawDomain, DOMAIN_TYPE_CONSTANTS.SUBDOMAIN);
                        this.push(parsedDomain);
                        self._cacheDomain(parsedDomain);
                    }, _subDomains);

                    _addOnDomains = [];
                    domains = result.data.addon_domains || [];
                    domains.forEach(function(rawDomain) {
                        var parsedDomain = self.formatSingleDomain(rawDomain, DOMAIN_TYPE_CONSTANTS.ADDON);
                        parsedDomain.canRename = true;
                        this.push(parsedDomain);
                        self._cacheDomain(parsedDomain);

                        // Also add in this thing's backing subdomain
                        var subdomainObj = _.assign( {}, parsedDomain );
                        subdomainObj.domain = subdomainObj.rootDomain;
                        subdomainObj.type   = DOMAIN_TYPE_CONSTANTS.SUBDOMAIN;
                        _subDomains.push(subdomainObj);
                        self._cacheDomain(subdomainObj);
                    }, _addOnDomains);

                    _parkDomains = [];
                    result.data.parked_domains.forEach(function(rawDomain) {
                        var parsedDomain    = self.formatSingleDomain(result.data.main_domain, DOMAIN_TYPE_CONSTANTS.ALIAS);
                        parsedDomain.domain = rawDomain;
                        parsedDomain.canRename = true;
                        this.push(parsedDomain);
                        self._cacheDomain(parsedDomain);
                    }, _parkDomains);

                    return _mainDomain;
                });
            };

            Domains.prototype.formatSingleDomain = function formatSingleDomain(rawDomain, typeOverride) {
                var self = this;
                var singleDomain = {
                    domain: rawDomain.domain,
                    homedir: rawDomain.homedir,
                    documentRoot: rawDomain.documentroot || rawDomain.dir,
                    rootDomain: rawDomain.servername,
                    isHttpsRedirecting: parseInt(rawDomain.is_https_redirecting),
                    hasValidHTTPSAliases: parseInt(rawDomain.all_aliases_valid),
                    nonHTTPS: !parseInt(rawDomain.can_https_redirect),
                    redirectsTo: rawDomain.status === "not redirected" ? null : rawDomain.status,
                    type: rawDomain.type,
                    realRootDomain: PAGE.mainDomain,
                    is_temporary: String(rawDomain.is_temporary) === "1" ? true : false,
                };
                if (typeOverride) {
                    singleDomain.type         = typeOverride;
                    singleDomain.homedir      = self.getMainDomain().homedir;
                    var altRootDomain = self.getMainDomain().domain;

                    // Root domain is going to differ when the domain in this context is a subdomain of an addon | parked domain.
                    if (typeOverride === DOMAIN_TYPE_CONSTANTS.SUBDOMAIN) {

                        // The subdomain could be of an addon|parked domain. So it is prudent to consider the last part
                        // of the domain as the root domain instead of hard coding it to the primary domain.
                        var lastPartOfDomain = rawDomain.domain.match(/.*\.(.+\..+)$/);
                        altRootDomain = (lastPartOfDomain) ? lastPartOfDomain[1] : altRootDomain;
                    }
                    var altSubDomain = singleDomain.rootDomain ? singleDomain.rootDomain.substr(0, singleDomain.rootDomain.lastIndexOf("." + altRootDomain)) : null;

                    singleDomain.canRemove    = true;
                    if (typeOverride !== DOMAIN_TYPE_CONSTANTS.ALIAS) {
                        singleDomain.canEdit      = PAGE.hasWebServerRole && CAN_EDIT_DOCROOT;
                        singleDomain.subdomain    = altSubDomain;
                    }

                    if (typeOverride === DOMAIN_TYPE_CONSTANTS.SUBDOMAIN) {
                        singleDomain.rootDomain = altRootDomain;
                    } else if ( typeOverride === DOMAIN_TYPE_CONSTANTS.ALIAS ) {
                        singleDomain.rootDomain = PAGE.mainDomain;
                    }
                }
                return singleDomain;
            };


            /**
             * API Wrapper to fetch the all domains (main, addon, subdomain, alias) and cache them
             *
             * @method get
             *
             * @return {Promise<Object>} returns a promise, then the array of all domain objects
             *
             */

            var domainsLoadingQ;

            Domains.prototype.get = function getDomains() {

                var self = this;

                if (domainsLoadingQ) {
                    return domainsLoadingQ;
                }

                if (_flattenedDomains) {
                    return $q.resolve(_flattenedDomains);
                }

                _flattenedDomains = [];

                return domainsLoadingQ = self.fetchDomains().then(function() {
                    self._associateAddonDomains();
                    return _flattenedDomains;
                }).finally(function() {
                    domainsLoadingQ = null;
                });

            };


            /**
             * API Wrapper to get the resource usage statistics
             *
             * @method getResourceUsageStats
             *
             * @return {Promise<Array>} returns a promise and then the array of usages statistics
             *
             */

            Domains.prototype.getResourceUsageStats = function _getResourceUsageStats() {

                var self = this;
                var apiCall = new UAPIRequest.Class();
                apiCall.initialize("ResourceUsage", "get_usages");

                return self.promise(apiCall).then(function(result) {
                    return result.data;
                });

            };

            /**
             * Get the currently stored domain types
             *
             * @method getDomainTypes
             *
             * @return {Array} array of domain type objects
             *
             */
            Domains.prototype.getDomainTypes = function _getBaseDomainTypes() {
                return _domainTypes;
            };

            /**
             * Get the currently stored usage statistics
             *
             * @method getUsageStats
             *
             * @return {Array} returns an array of usage stat objects
             *
             */
            Domains.prototype.getUsageStats = function _getUsageStats() {
                return _usageStats;
            };

            /**
             * Uses the current getUsageStats() and updates the overLimit on the domainTypes
             *
             * @method updateDomainTypeLimits
             *
             * @return {Array} returns the updated array of domain type objects
             *
             */
            Domains.prototype.updateDomainTypeLimits = function _updateDomainTypeLimits() {
                var self = this;

                var stats = self.getUsageStats();

                self.getDomainTypes().forEach(function(domainType) {
                    var domainTypeStat = _findStatById(stats, domainType.stat);
                    domainType.overLimit = _checkStatOverLimit(domainTypeStat);
                    if (!_canCustomizeDocumentRoots() && domainType.requiresCustomDocumentRoot ) {
                        domainType.overLimit = true;
                    } else if (!domainType.overLimit && domainType.dependantStat) {
                        domainType.overLimit = domainType.overLimit || _checkStatOverLimit(_findStatById(stats, domainType.dependantStat));
                    }
                });

                return self.getDomainTypes();

            };


            /**
             * Get the domain types and update their overlimit by quering the usage stats APIs
             *
             * @method getTypes
             *
             * param jsdocparam maybe?
             *
             * @return {Promise<Array>} returns a promise and then an array of domain types with the updated overLimit values
             *
             */
            Domains.prototype.getTypes = function _getDomainTypes() {
                var self = this;

                if (self.getUsageStats()) {
                    return $q.resolve(self.getDomainTypes());
                }


                return self.getResourceUsageStats().then(function(stats) {

                    _usageStats = stats;

                    self.updateDomainTypeLimits();

                    return self.getDomainTypes();

                });
            };

            // -------- \ READ -------------

            // -------- UPDATE -------------


            /**
             * Update the document root for a subdomain
             *
             * @method updateDocumentRoot
             *
             * @param  {String|Object} domain domain name or domain object
             *
             * @return {Promise<Object>} returns promise and then the updated domainObject
             *
             */
            Domains.prototype.updateDocumentRoot = function updateDocumentRoot(domain, documentRoot) {
                var self = this;
                var domainObject = self._getDomainObject(domain);

                ["subdomain", "rootDomain"].forEach(function(key) {
                    if (!domainObject[key]) {
                        throw new Error(key + " is required but undefined on " + domainObject.domain);
                    }
                });

                var rdomain = domainObject.rootDomain;
                if (domainObject.type === DOMAIN_TYPE_CONSTANTS.ADDON) {
                    rdomain = domainObject.realRootDomain;
                }

                var apiCall = new API2Request.Class();
                apiCall.initialize("SubDomain", "changedocroot");
                apiCall.addArgument("subdomain", domainObject.subdomain);
                apiCall.addArgument("rootdomain", rdomain);
                apiCall.addArgument("dir", documentRoot);

                return self.promise(apiCall).then(function(result) {
                    domainObject.documentRoot = documentRoot;

                    return self.fetchSingleDomainData(domainObject.domain).then(function(updatedDomain) {
                        var updatedDocumentRoot = updatedDomain.documentRoot;

                        // find and update existing domain
                        if (domainObject.type === DOMAIN_TYPE_CONSTANTS.ADDON) {

                            // This is an addon domain. So there is a subdomain that just had it's document root updated too
                            var subdomainObject = self._getSubDomainObject(domainObject.subdomain );
                            if (subdomainObject) {
                                subdomainObject.documentRoot = updatedDocumentRoot;
                            }
                        } else if (domainObject.associatedAddonDomain) {

                            // This is an addon domain. Check for an associated addon domain
                            var addonDomainObject = self._getDomainObject(domainObject.associatedAddonDomain);
                            addonDomainObject.documentRoot = updatedDocumentRoot;
                        }

                        domainObject.documentRoot = updatedDocumentRoot;

                        return domainObject;
                    });

                });
            };

            // -------- \ UPDATE -------------

            // -------- DELETE -------------

            /**
             * API Wrapper call to remove a subdomain
             *
             * @method removeSubdomain
             *
             * @param  {String|Object} domain domain name or domain object
             *
             * @return {Promise} returns the promise that removes the subdomain
             *
             */
            Domains.prototype.removeSubdomain = function removeSubdomain(domain) {
                var self = this;
                var domainObject = self._getDomainObject(domain);

                // If the domain does not contain its own rootDomain, it
                // is an indicator that the subdomain is parked on addon domain
                // which means the servername could not be used to extract the
                // subdomain. In this case we will use full domain as the removal point
                // for the api call (CPANEL-32624)
                var rootDomainRE = new RegExp("." + domainObject.rootDomain + "$");
                var domainStr;
                if (domainObject.domain.match(rootDomainRE)) {
                    domainStr = domainObject.subdomain + "_" + domainObject.rootDomain;
                } else {
                    domainStr = domainObject.domain;
                }

                var apiCall = new API2Request.Class();
                apiCall.initialize("SubDomain", "delsubdomain");
                apiCall.addArgument("domain", domainStr);

                return self.promise(apiCall);
            };

            /**
             * API Wrapper call to remove an addon domain
             *
             * @method removeAddonDomain
             *
             * @param  {String|Object} domain domain name or domain object
             *
             * @return {Promise} returns the promise that removes the addon domain
             *
             */
            Domains.prototype.removeAddonDomain = function removeAddonDomain(domain) {
                var self = this;
                var domainObject = self._getDomainObject(domain);

                var apiCall = new API2Request.Class();
                apiCall.initialize("AddonDomain", "deladdondomain");
                apiCall.addArgument("domain", domainObject.domain);

                // The addon domain's subdomain, an underscore (_), and the addon domain's main domain.
                apiCall.addArgument("subdomain", domainObject.subdomain + "_" + self.getMainDomain().domain);

                return self.promise(apiCall);
            };

            /**
             * API Wrapper call to remove an alias domain
             *
             * @method removeAliasDomain
             *
             * @param  {String|Object} domain domain name or domain object
             *
             * @return {Promise} returns the promise that removes the alias domain
             *
             */
            Domains.prototype.removeAliasDomain = function removeAliasDomain(domain) {
                var self = this;
                var domainObject = self._getDomainObject(domain);

                var apiCall = new API2Request.Class();
                apiCall.initialize("Park", "unpark");
                apiCall.addArgument("domain", domainObject.domain);

                return self.promise(apiCall);
            };

            /**
             * API Wrapper call to remove a redirect for a domain
             *
             * @method removeRedirect
             *
             * @param  {String|Object} domain domain name or domain object
             *
             * @return {Promise} returns the promise that removes the redirect from the domain
             *
             */
            Domains.prototype.removeRedirect = function removeRedirect(domain) {
                var self = this;
                var domainObject = self._getDomainObject(domain);

                var apiCall = new UAPIRequest.Class();
                apiCall.initialize("Mime", "delete_redirect");
                apiCall.addArgument("domain", domainObject.domain);
                apiCall.addArgument("src", domainObject.redirectTo);
                apiCall.addArgument("redirect", domainObject.documentRoot);

                return self.promise(apiCall).then(function() {
                    domainObject.redirectTo = "";
                });
            };


            Domains.prototype._removeDomainAdjustStats = function _removeDomainAdjustStats(domain) {
                var self = this;
                var domainObject = self._getDomainObject(domain);

                if (!domainObject) {
                    return;
                }

                // Remove the subdomain too, it's automatically deleted by the API
                if (domainObject.type === DOMAIN_TYPE_CONSTANTS.ADDON) {
                    self._removeDomainAdjustStats(domainObject.subdomain + "." + self.getMainDomain().domain);
                }

                self._uncacheDomain(domainObject);

                // Update Stat for Domain Type
                var domainTypeObject = _findDomainTypeByValue(domainObject.type);
                var stat = _findStatById(self.getUsageStats(), domainTypeObject.stat);
                if (stat) {
                    stat.usage--;
                }
                self.updateDomainTypeLimits();
            };

            /**
             * Remove a domain, redirects, and adjust statistics for the domain type
             *
             * @method remove
             *
             * @param  {String|Object} domain domain name or domain object
             *
             * @return {Promise} returns the promise that will remove the domain and redirects for a domain
             *
             */
            Domains.prototype.remove = function removeDomain(domain) {
                var self = this;
                var domainObject = self._getDomainObject(domain);

                var errorsEncounterd = false;

                var promises = [];

                if ( domainObject.type === DOMAIN_TYPE_CONSTANTS.SUBDOMAIN ) {
                    promises.push(self.removeSubdomain(domainObject));
                } else if ( domainObject.type === DOMAIN_TYPE_CONSTANTS.ADDON ) {
                    promises.push(self.removeAddonDomain(domainObject));
                } else if ( domainObject.type === DOMAIN_TYPE_CONSTANTS.ALIAS ) {
                    if (domainObject.redirectTo) {
                        promises.push(self.removeRedirect(domainObject));
                    }
                    promises.push(self.removeAliasDomain(domainObject));
                }

                return $q.all(promises).then(function(result) {
                    self._removeDomainAdjustStats(domainObject);
                }, function(result) {

                    if ( _.isArray(result) ) {
                        result.forEach(function(resultItem) {
                            if (resultItem && resultItem.error) {
                                errorsEncounterd = true;
                                throw resultItem.error;
                            }
                        });
                    } else if (result && result.error) {
                        errorsEncounterd = true;
                        throw result.error;
                    }
                }).finally(function() {
                    if (!errorsEncounterd) {
                        self._uncacheDomain(domainObject);
                    }
                });
            };

            Domains.prototype.getDocumentRootPattern = function() {
                var regExp = new RegExp("^[^" + _.escapeRegExp('%?* :|"<>\\') + "]+$");
                return regExp;
            };

            // -------- \ DELETE -------------

            Domains.prototype.canRedirectHTTPS = function() {
                return PAGE.canRedirectHTTPS === "1";
            };

            /**
             * API Wrapper call to toggle secure redirect for domains
             *
             * @method toggleHTTPSRedirect
             *
             * @param  {Boolean} state turning the redirect On of Off as a boolean value
             *
             * @param  {String} singleDomain a single domain name to be toggled, optional
             *
             * @return {Promise} returns the promise that toggles secure redirect for the domains
             *
             */
            Domains.prototype.toggleHTTPSRedirect = function toggleHTTPSRedirect(state, singleDomain) {
                var self = this;

                var apiCall = new UAPIRequest.Class();
                apiCall.initialize("SSL", "toggle_ssl_redirect_for_domains");
                apiCall.addArgument("state", state ? 1 : 0);


                if (singleDomain) {
                    var domArg = singleDomain;
                    apiCall.addArgument("domains", domArg);
                } else {
                    var domains2Toggle = [];
                    for (var i = 0; i < _flattenedDomains.length; i++) {
                        if (_flattenedDomains[i].selected && !_flattenedDomains[i].associatedAddonDomain) {

                            // Add in non-assoc subs only to prevent passing dupes
                            if (typeof ( _flattenedDomains[i].associatedAddonDomain) === "undefined") {
                                domains2Toggle.push( _flattenedDomains[i].domain );
                            }
                        }
                    }

                    var domainList = domains2Toggle.join(",");

                    apiCall.addArgument("domains", domainList );
                }

                return self.promise(apiCall);
            };

            Domains.prototype.replaceTemporaryDomain = function replaceTemporaryDomain(domainObject) {
                var self = this;
                var apiCall = new UAPIRequest.Class();

                apiCall.initialize("Domain", "convert_temporary_to_registered");
                apiCall.addArgument("domain", domainObject.domain);
                apiCall.addArgument("registered", domainObject.newDomainName);
                apiCall.addArgument("docroot", domainObject.documentRoot);

                return self.promise(apiCall).then(function() {
                    return self.fetchSingleDomainData(domainObject.newDomainName).then(function(updatedDomain) {

                        // The new domain is an addon (or alias if parked) — give it
                        // canRename so the page picks the correct UI state without
                        // forcing a full reload.
                        updatedDomain.canRename = true;

                        self._cacheDomain(updatedDomain);
                        self._cacheDomain(angular.extend(angular.copy(updatedDomain), {
                            domain: updatedDomain.rootDomain,
                            type: DOMAIN_TYPE_CONSTANTS.SUBDOMAIN,
                            associatedAddonDomain: updatedDomain,
                        }));
                        self._uncacheDomain(domainObject);

                        var domainType = _findDomainTypeByValue(updatedDomain.type);
                        var stat = _findStatById(self.getUsageStats(), domainType.stat);
                        if (stat) {
                            stat.usage++;
                        }
                        self.updateDomainTypeLimits();
                        _trackFrontendMixpanel("CP_USR:DOMAINS-TEMP_DOMAIN-CONVERTED");
                    });
                });
            };

            /**
             * Fetch domain suggestions from the DomainRecommendations UAPI
             *
             * @method fetchDomainRecommendations
             *
             * @param  {String} domain   The domain to get recommendations for
             * @param  {Number} pageSize Maximum number of suggestions to return
             * @param  {Array}  [tlds]   Optional list of TLDs (with or without
             *                           leading dot) to restrict suggestions to.
             *                           Sent to the backend as repeated `tld`
             *                           UAPI arguments (`tld`, `tld-1`, `tld-2`,
             *                           ...) per the cPanel multi-value convention.
             *
             * @return {Promise<Object>} returns a promise with the suggestions data
             *
             */
            Domains.prototype.fetchDomainRecommendations = function fetchDomainRecommendations(domain, pageSize, tlds) {
                var self = this;
                var apiCall = new UAPIRequest.Class();
                apiCall.initialize("DomainRecommendations", "domain_suggestions");
                apiCall.addArgument("search_field", domain);
                if (pageSize) {
                    apiCall.addArgument("page_size", pageSize);
                }
                if (tlds && tlds.length) {

                    // Strip the optional leading dot so the upstream store
                    // receives bare TLD names (e.g. "com" instead of ".com").
                    var normalized = tlds.map(function(t) {
                        return (t || "").replace(/^\./, "");
                    }).filter(function(t) { return t.length; });

                    if (normalized.length) {
                        apiCall.addArgument("tld", normalized[0]);
                        for (var i = 1; i < normalized.length; i++) {
                            apiCall.addArgument("tld-" + i, normalized[i]);
                        }
                    }
                }

                return self.promise(apiCall).then(function(result) {
                    return result.data;
                });
            };

            /**
             * Fetch domain purchase url from the DomainRecommendations UAPI
             *
             * @method fetchDomainPurchaseUrl
             *
             * @param  {String} domain   The domain to get the purchase URL for
             *
             * @return {Promise<Object>} returns a promise with the purchase URL data
             *
             */
            Domains.prototype.fetchDomainPurchaseUrl = function fetchDomainPurchaseUrl(domain, period) {
                var self = this;
                var apiCall = new UAPIRequest.Class();
                apiCall.initialize("DomainRecommendations", "purchase_domain");
                apiCall.addArgument("domain", domain);
                apiCall.addArgument("period", period);
                apiCall.addArgument("skip_backend_track", 1);

                return self.promise(apiCall).then(function(result) {
                    return result.data;
                });
            };

            /**
             * Fetch domain recommendations for both the promotional
             * banner and the explore modal in a single API call.
             *
             * Returns banner data (at most 2 suggestions with the
             * lowest 1-year price) and explore data (featured top 2
             * and remaining suggestions with pricing).
             *
             * When filterTlds is provided, it is forwarded to the UAPI
             * call as repeated `tld` arguments so the upstream store
             * constrains results to those TLDs.
             *
             * @method fetchRecommendations
             *
             * @param  {String} domain The domain to get recommendations for
             * @param  {Array}  [filterTlds] TLDs to constrain suggestions to (e.g. [".com", ".net"])
             *
             * @return {Promise<Object>} { banner: { recommendations, lowestPrice }, explore: { featured, available } }
             */
            Domains.prototype.fetchRecommendations = function fetchRecommendations(domain, filterTlds) {
                var BANNER_LIMIT = 2;
                var FEATURED_LIMIT = 2;
                var RECOMMENDATIONS_PAGE_SIZE = 10;

                // Race the call against a timeout timer. AngularJS 1.4 has no
                // $q.race, so we settle a single deferred from whichever side
                // wins. The underlying APICatcher promise does not expose a
                // true HTTP abort, so on timeout we abandon the in-flight
                // result rather than cancelling it at the network layer; the
                // UI is freed immediately either way. The caller's
                // catch/finally then hides the banner and logs the failure.
                var deferred = $q.defer();
                var timedOut = false;

                var timer = $timeout(function() {
                    timedOut = true;
                    deferred.reject(new Error("Domain recommendations request timed out after " + RECOMMENDATIONS_TIMEOUT_MS + "ms"));
                }, RECOMMENDATIONS_TIMEOUT_MS);

                this.fetchDomainRecommendations(domain, RECOMMENDATIONS_PAGE_SIZE, filterTlds)
                    .then(function(data) {

                        // The timeout already won the race and rejected the
                        // deferred; skip the now-pointless result processing.
                        if (timedOut) {
                            return;
                        }

                        var suggestions = data && data.suggestions ? data.suggestions : {};
                        var allDomains = Object.keys(suggestions);

                        // Banner: first 2 domains + lowest annual price
                        var bannerSelected = allDomains.slice(0, BANNER_LIMIT);
                        var bannerRecommendations = [];
                        var lowestPrice = null;

                        bannerSelected.forEach(function(domainName) {
                            bannerRecommendations.push({ domain: domainName });

                            var price = _extractAnnualPrice(suggestions[domainName]["pricing"]);
                            if (price && (lowestPrice === null || price.value < lowestPrice.value)) {
                                lowestPrice = price;
                            }
                        });

                        var exploreResults = allDomains.map(function(domainName) {
                            var price = _extractAnnualPrice(suggestions[domainName]["pricing"]);
                            return {
                                domain: domainName,
                                price: price ? price.display : "",
                                priceValue: price ? price.value : 0,
                                isPremium: !!suggestions[domainName]["is_premium"],
                            };
                        });

                        deferred.resolve({
                            banner: {
                                recommendations: bannerRecommendations,
                                lowestPrice: lowestPrice ? lowestPrice.display : "",
                            },
                            explore: {
                                featured: exploreResults.slice(0, FEATURED_LIMIT),
                                available: exploreResults.slice(FEATURED_LIMIT),
                            },
                        });
                    })
                    .catch(function(error) {
                        deferred.reject(error);
                    })
                    .finally(function() {

                        // Whichever side won the race, stop the timer. Settling
                        // an already-settled deferred is a no-op, so a late
                        // response after a timeout is harmlessly ignored.
                        $timeout.cancel(timer);
                    });

                return deferred.promise;
            };

            /**
             * Extract 1-year registration price from a pricing array.
             *
             * @param  {Array} pricing The pricing array from the API
             * @return {Object|null} { display: String, value: Number } or null
             */
            function _extractAnnualPrice(pricing) {
                if (!Array.isArray(pricing)) {
                    return null;
                }
                for (var i = 0; i < pricing.length; i++) {
                    if (pricing[i].period === 1 && pricing[i].register) {
                        return {
                            display: pricing[i].register.display_value,
                            value: parseFloat(pricing[i].register.value),
                        };
                    }
                }
                return null;
            }

            /**
             * Fetch the store configuration (without secrets) from
             * the DomainRecommendations UAPI. Used to build purchase
             * URLs for the explore modal.
             *
             * @method fetchStoreConfig
             *
             * @return {Promise<Object>} The sanitized store config
             */
            Domains.prototype.fetchStoreConfig = function fetchStoreConfig() {
                var self = this;
                var apiCall = new UAPIRequest.Class();
                apiCall.initialize("DomainRecommendations", "get_store_config");

                return self.promise(apiCall).then(function(result) {
                    return result.data;
                });
            };

            /**
             * Fetch the list of TLDs supported by the store from
             * the DomainRecommendations UAPI. Used to validate the
             * preferred TLD input in the explore modal.
             *
             * @method fetchSupportedTlds
             *
             * @return {Promise<Array<String>>} Array of dot-prefixed TLD strings (e.g. [".com", ".net"])
             */
            Domains.prototype.fetchSupportedTlds = function fetchSupportedTlds() {
                var self = this;
                var apiCall = new UAPIRequest.Class();
                apiCall.initialize("DomainRecommendations", "supported_tlds");

                return self.promise(apiCall).then(function(result) {
                    return result.data && Array.isArray(result.data.tlds) ? result.data.tlds : [];
                });
            };

            // Normalize a raw TLD to the canonical "leading dot, lowercase"
            // form used everywhere the prioritised TLDs are stored or
            // compared. Returns "" for empty/whitespace input.
            function _normalizeTld(raw) {
                var s = (raw || "").trim().toLowerCase();
                if (!s) {
                    return "";
                }
                return s.charAt(0) === "." ? s : "." + s;
            }

            /**
             * Load the user's persisted prioritised TLDs from NVData
             * (CPANEL-53293). Used to pre-populate the "Prioritize TLDs"
             * control across sessions.
             *
             * @method getPrioritizedTlds
             *
             * @return {Promise<Array<String>>} Resolves to an array of
             *   normalized, dot-prefixed TLDs (e.g. [".com", ".net"]).
             *   Resolves to [] when nothing is stored or the read fails, so
             *   callers never have to special-case the empty/error state.
             */
            Domains.prototype.getPrioritizedTlds = function getPrioritizedTlds() {
                return nvDataService.get(PRIORITIZED_TLDS_NVDATA_KEY)
                    .then(function(pairs) {
                        var pair = Array.isArray(pairs) ? pairs[0] : null;
                        var raw = pair && pair.value ? String(pair.value) : "";
                        return raw.split("|")
                            .map(_normalizeTld)
                            .filter(function(tld) {
                                return tld.length;
                            });
                    })
                    .catch(function(error) {

                        // A failed read should not break the flow — fall back
                        // to "no saved preference" so the control still works.
                        console.error("Failed to load prioritised TLDs:", error); // eslint-disable-line no-console
                        return [];
                    });
            };

            /**
             * Persist the user's prioritised TLDs as NVData (CPANEL-53293) so
             * they survive across sessions. Overwrites any previously-stored
             * value; passing an empty array clears the saved preference.
             *
             * @method setPrioritizedTlds
             *
             * @param  {Array<String>} [tlds] TLDs to persist (with or without
             *                          a leading dot). Falsy/empty clears the value.
             *
             * @return {Promise} Resolves once the write settles. A failed
             *   write is logged and swallowed so a persistence hiccup never
             *   blocks the search the user just triggered.
             */
            Domains.prototype.setPrioritizedTlds = function setPrioritizedTlds(tlds) {
                var serialized = (angular.isArray(tlds) ? tlds : [])
                    .map(_normalizeTld)
                    .filter(function(tld) {
                        return tld.length;
                    })
                    .join("|");

                return nvDataService.set(PRIORITIZED_TLDS_NVDATA_KEY, serialized, { nocache: true })
                    .then(function(result) {

                        // A per-key NVData write failure resolves with an
                        // `error` property rather than rejecting, so it has to
                        // be surfaced here in addition to the rejection path
                        // below — otherwise a failed save is silent.
                        if (result && result.error) {
                            console.error("Failed to save prioritised TLDs:", result.error); // eslint-disable-line no-console
                        }
                    })
                    .catch(function(error) {
                        console.error("Failed to save prioritised TLDs:", error); // eslint-disable-line no-console
                    });
            };

            Domains.prototype.renameDomain = function renameDomain(domainObject) {
                var self = this;
                var apiCall = new UAPIRequest.Class();

                apiCall.initialize("Domain", "rename_domain");
                apiCall.addArgument("domain", domainObject.domain);
                apiCall.addArgument("new_domain", domainObject.newDomainName);

                return self.promise(apiCall).then(function() {
                    return self.fetchSingleDomainData(domainObject.newDomainName).then(function(updatedDomain) {

                        // Preserve canRename across the rename so the page can
                        // immediately offer another rename without a full reload.
                        updatedDomain.canRename = true;

                        self._cacheDomain(updatedDomain);
                        self._uncacheDomain(domainObject);
                    });
                });
            };

            /**
             * Stash a domain to be added once the List Domains view loads.
             * Used by the Manage view's "Explore Domains → Purchase → Add
             * Domain" flow to hand the purchased domain to the chooser that
             * lives on the List view, across the route change.
             *
             * @method setPendingAddDomain
             * @param {String} domain The just-purchased domain name.
             */
            Domains.prototype.setPendingAddDomain = function setPendingAddDomain(domain) {
                _pendingAddDomain = domain || null;
            };

            /**
             * Read and clear the pending add-domain handoff. Returns null
             * when nothing is queued, so callers can branch directly on the
             * result. One-shot: a second call returns null.
             *
             * @method consumePendingAddDomain
             * @return {String|null} The queued domain name, or null.
             */
            Domains.prototype.consumePendingAddDomain = function consumePendingAddDomain() {
                var domain = _pendingAddDomain;
                _pendingAddDomain = null;
                return domain;
            };

            return new Domains();
        }]);
    }
);
Back to Directory