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