Viewing File: /usr/local/cpanel/base/frontend/jupiter/domains/views/manageDomain.js
// Copyright 2026 WebPros International, LLC
// All rights reserved.
// copyright@cpanel.net http://cpanel.net
// This code is subject to the cPanel license. Unauthorized copying is prohibited.
/* global define */
/** @namespace cpanel.domains.views.manageDomain */
define(
[
"angular",
"lodash",
"cjt/util/locale",
"cjt/modules",
"cjt/directives/callout",
"cjt/directives/actionButtonDirective",
"app/services/domains",
"app/directives/domainSearchModalDirective",
"cjt/services/cpanel/nvDataService",
],
function(angular, _, LOCALE) {
"use strict";
var MODULE_NAMESPACE = "cpanel.domains.views.manageDomain";
var app = angular.module(MODULE_NAMESPACE, [
"cpanel.domains.domains.service",
"ngRoute",
"cjt2.services.alert",
]);
/**
* Create Controller for Domains
*
* @module manageDomain
*
* @param {Object} $scope angular scope
*
*/
var controller = app.controller(
"manageDomain",
["$scope", "$location", "$routeParams", "$q", "$timeout", "$sce", "domains", "alertService", "DOMAIN_TYPE_CONSTANTS", "PAGE", "nvDataService", "domainSearchModalService",
function($scope, $location, $routeParams, $q, $timeout, $sce, $domainsService, $alertService, DOMAIN_TYPE_CONSTANTS, PAGE, nvDataService, domainSearchModalService) {
function init() {
if (!$scope.currentDomain) {
$alertService.add({
"message": LOCALE.maketext("You did not specify a domain to manage."),
"type": "danger",
});
$location.path("/").search("");
}
$scope.showAdvancedDetailFields = false;
$scope.showSubdomainInDetails = $scope.currentDomain.type === DOMAIN_TYPE_CONSTANTS.ADDON;
if (DOMAIN_TYPE_CONSTANTS.SUBDOMAIN === $scope.currentDomain.type) {
// Current type is a subdomain, if it's associated with an addon domain
// we'd have to delete that domain instead.
$domainsService.get().then(function(domains) {
domains.forEach(function(domain) {
if (domain.subdomain === $scope.currentDomain.subdomain && domain.type === DOMAIN_TYPE_CONSTANTS.ADDON) {
$scope.associatedDomains.push(domain);
}
});
});
}
$scope.canUpdateDocumentRoot = $scope.currentDomain.type !== DOMAIN_TYPE_CONSTANTS.ALIAS;
$scope.workingDomain = _generateWorkingDomain();
}
function toggleReplaceDomainSection() {
$scope.replaceDomainLinkClicked = !$scope.replaceDomainLinkClicked;
}
function _generateWorkingDomain() {
var workingDomain = angular.copy($scope.currentDomain);
workingDomain.fullDocumentRoot = workingDomain.documentRoot;
workingDomain.renameDomainConfirmed = false;
// ensure documentRoot is consistent requirePublicHTMLSubs
var documentRoot = workingDomain.documentRoot;
var leadTrim = workingDomain.homedir;
if (documentRoot) {
// snip off the homedir
var regexp = new RegExp("^" + _.escapeRegExp(leadTrim) + "(/)?");
documentRoot = documentRoot.replace(regexp, "");
if ($scope.requirePublicHTMLSubs) {
// snip off public_html/
documentRoot = documentRoot
.replace(/^public_html/, "")
.replace(/^\//, "");
}
workingDomain.documentRoot = documentRoot;
}
return workingDomain;
}
function getFormFieldClasses(form) {
return form && !form.$pristine && form.$invalid ? "col-xs-12 col-md-6" : "col-xs-12";
}
function update(form, domainObject) {
var alertID = "updating_" + domainObject.domain;
var fullDocumentRoot = $domainsService.generateFullDocumentRoot(domainObject.documentRoot);
if (!domainObject.rootDomain) {
// This domain is missing its definition file, we should immediately error out
$alertService.add({
type: "danger",
id: alertID,
replace: true,
message: LOCALE.maketext("The domain userdata file for the “[_1]” domain appears to be missing.", domainObject.domain),
});
return;
}
function _updateSuccess() {
$alertService.add({
type: "success",
id: alertID,
replace: true,
message: LOCALE.maketext("You have successfully updated the document root to “[_1]” for the “[_2]” domain.", _.escape(fullDocumentRoot), domainObject.domain),
});
$scope.workingDomain = _generateWorkingDomain();
}
function _updateFailure() {
$alertService.removeById(alertID);
}
var promises = [];
if (!form.newDocumentRoot.$pristine) {
promises.push($domainsService.updateDocumentRoot(domainObject.domain, fullDocumentRoot).then(
_updateSuccess,
_updateFailure
));
}
if (promises.length) {
return $q.all(promises);
}
}
function replace(domainObject) {
$scope.temporaryDomainReplacementPending = true;
const fullDocumentRoot = $domainsService.generateFullDocumentRoot(domainObject.documentRoot);
const submittingDomainObject = {
documentRoot: domainObject.documentRoot,
domain: domainObject.domain,
newDomainName: domainObject.newDomainName,
};
return $domainsService.replaceTemporaryDomain(submittingDomainObject)
.then(function() {
$alertService.add({
type: "success",
message: LOCALE.maketext("The domain “[_1]” has been converted to “[_2]”. Its document root is now “[_3]”.", submittingDomainObject.domain, submittingDomainObject.newDomainName, _.escape(fullDocumentRoot)),
replace: false,
});
// Refresh currentDomain/workingDomain from the
// service cache so flags like canRename and
// is_temporary reflect the post-replace state.
// Without this, the user would have to fully
// reload the page before the Rename button
// appears for the now-registered domain.
var refreshed = $domainsService.findDomainByName(submittingDomainObject.newDomainName);
if (refreshed) {
$scope.currentDomain = refreshed;
$scope.workingDomain = _generateWorkingDomain();
}
$scope.replaceDomainLinkClicked = false;
$location.search("domain", submittingDomainObject.newDomainName);
$scope.temporaryDomainReplacementPending = false;
});
}
function _showDeletionDelayedMessage() {
$scope.deletionDelayed = true;
}
function removeDomain() {
if (!$scope.currentDomain.rootDomain) {
// This domain is missing it's definition file, we should immediately error out
$alertService.add({
type: "danger",
replace: true,
message: LOCALE.maketext("The domain userdata file for the “[_1]” domain appears to be missing.", $scope.currentDomain.domain),
});
return;
}
var $timer = $timeout(_showDeletionDelayedMessage, 1000);
$scope.domainRemovePending = true;
return $domainsService.remove($scope.currentDomain.domain).then(function() {
$alertService.add({
message: LOCALE.maketext("The system removed the “[_1]” domain and any associated redirections.", $scope.currentDomain.domain),
type: "success",
});
// Invalidate the domain cache so the next Manage
// page always loads fresh flags (canRemove, canRename)
// rather than stale data left over from mutations.
$domainsService.invalidate();
$location.path("/").search("");
}).finally(function() {
$scope.deletionDelayed = false;
$scope.domainRemovePending = false;
$timeout.cancel($timer);
});
}
function startRemovalConfirmation() {
$scope.confirmingRemoval = true;
}
function cancelRemoval() {
$scope.confirmingRemoval = false;
}
function toggleAdvancedDetailFields() {
$scope.showAdvancedDetailFields = !$scope.showAdvancedDetailFields;
}
function deleteDependentDomainsError(domain, associatedDomains) {
return LOCALE.maketext("The system created the “[_1]” subdomain when it created the “[_2]” domain. To delete this subdomain, you must first delete the following [numerate,_3,domain,domains]:", domain, associatedDomains[0].domain, associatedDomains.length);
}
function onUpdateNewDomainName() {
if (!$scope.workingDomain.newDomainName) {
return;
}
$scope.workingDomain.documentRoot = $scope.workingDomain.newDomainName;
$scope.workingDomain.fullDocumentRoot = $domainsService.generateFullDocumentRoot($scope.workingDomain.newDomainName);
$scope.workingDomain.subdomain = $scope.workingDomain.newDomainName;
}
function currentDomainIsTemporaryMainDomain() {
const currentDomainIsMainDomain = $scope.workingDomain.type === DOMAIN_TYPE_CONSTANTS.MAIN;
const mainDomainIsTemporary = $scope.mainDomain.is_temporary === true || String($scope.mainDomain.is_temporary) === "1";
return mainDomainIsTemporary && currentDomainIsMainDomain;
}
function toggleRenameDomainSection() {
$scope.renameDomainLinkClicked = !$scope.renameDomainLinkClicked;
if (!$scope.renameDomainLinkClicked) {
// Reset rename form state when collapsing the section
$scope.workingDomain.newDomainName = "";
$scope.workingDomain.renameDomainConfirmed = false;
// Restore focus to the trigger button so keyboard
// users do not lose their place in the page.
$timeout(function() {
var trigger = document.getElementById("rename_domain_link");
if (trigger) {
trigger.focus();
}
});
} else {
// Move focus into the new-domain input for keyboard
// users; rely on $timeout to wait for ng-if to
// render the input.
$timeout(function() {
var input = document.getElementById("renameDomainName");
if (input) {
input.focus();
}
});
}
}
function renameDomain(domainObject) {
var newName = domainObject.newDomainName;
if (!newName) {
return;
}
if (newName === domainObject.domain) {
$alertService.add({
type: "danger",
message: LOCALE.maketext("The new domain must differ from the current domain."),
replace: true,
});
return;
}
$scope.domainRenamePending = true;
var submittingDomainObject = {
domain: domainObject.domain,
newDomainName: newName,
};
return $domainsService.renameDomain(submittingDomainObject)
.then(function() {
var message;
if (domainObject.is_temporary) {
message = LOCALE.maketext("The temporary domain “[_1]” has been replaced with “[_2]”.", submittingDomainObject.domain, submittingDomainObject.newDomainName);
} else {
message = LOCALE.maketext("The domain “[_1]” has been renamed to “[_2]”.", submittingDomainObject.domain, submittingDomainObject.newDomainName);
}
$alertService.add({
type: "success",
message: message,
replace: false,
});
// Update in-place — reflect new name in both working and current domain objects
$scope.workingDomain.domain = submittingDomainObject.newDomainName;
$scope.workingDomain.is_temporary = false;
$scope.currentDomain.domain = submittingDomainObject.newDomainName;
$scope.currentDomain.is_temporary = false;
$scope.workingDomain.newDomainName = "";
$scope.workingDomain.renameDomainConfirmed = false;
$scope.renameDomainLinkClicked = false;
$scope.domainRenamePending = false;
// Re-bind currentDomain to the refreshed cache
// entry so derived flags (canRename, canEdit,
// documentRoot, homedir) reflect the post-rename
// state without forcing a full page reload.
var refreshed = $domainsService.findDomainByName(submittingDomainObject.newDomainName);
if (refreshed) {
$scope.currentDomain = refreshed;
$scope.workingDomain = _generateWorkingDomain();
$scope.workingDomain.newDomainName = "";
}
$location.search("domain", submittingDomainObject.newDomainName);
})
.catch(function(error) {
$scope.domainRenamePending = false;
$scope.workingDomain.renameDomainConfirmed = false;
throw error;
});
}
angular.extend($scope, {
associatedDomains: [],
deletionDelayed: false,
temporaryDomainReplacementPending: false,
domainRenamePending: false,
domainRemovePending: false,
renameDomainLinkClicked: false,
currentDomain: $domainsService.findDomainByName($routeParams["domain"]),
requirePublicHTMLSubs: PAGE.requirePublicHTMLSubs.toString() === "1",
webserverRoleAvailable: PAGE.hasWebServerRole,
mainDomain: $domainsService.getMainDomain(),
getFormFieldClasses: getFormFieldClasses,
documentRootPattern: $domainsService.getDocumentRootPattern(),
toggleAdvancedDetailFields: toggleAdvancedDetailFields,
update: update,
replace: replace,
renameDomain: renameDomain,
confirmingRemoval: false,
removeDomain: removeDomain,
startRemovalConfirmation: startRemovalConfirmation,
cancelRemoval: cancelRemoval,
deleteDependentDomainsError: deleteDependentDomainsError,
replaceDomainLinkClicked: false,
toggleReplaceDomainSection: toggleReplaceDomainSection,
toggleRenameDomainSection: toggleRenameDomainSection,
onUpdateNewDomainName: onUpdateNewDomainName,
currentDomainIsTemporaryMainDomain: currentDomainIsTemporaryMainDomain,
});
$scope.domainRecommendationsEnabled = !!PAGE.features.domain_recommendation;
$scope.domainRecommendations = [];
$scope.domainRecommendationPrice = "";
$scope.domainRecsLoading = false;
var cachedExploreData = null;
var recommendationsPromise = null;
$scope.domainRecsBannerDismissed = !!PAGE.domainRecsBannerDismissed;
if ($scope.domainRecommendationsEnabled && !$scope.domainRecsBannerDismissed && !$scope.currentDomain.is_temporary) {
var domainName = $domainsService.findDomainByName($routeParams["domain"]);
if (domainName && domainName.domain) {
$scope.domainRecsLoading = true;
recommendationsPromise = $domainsService.fetchRecommendations(domainName.domain)
.then(function(result) {
$scope.domainRecommendations = result.banner.recommendations;
if (result.banner.lowestPrice) {
$scope.domainRecommendationPrice = result.banner.lowestPrice + " / " + LOCALE.maketext("year");
}
cachedExploreData = result.explore;
return cachedExploreData;
})
.catch(function(error) {
// Log so a genuine fetch/processing failure
// (including a client-side timeout) stays
// diagnosable rather than looking identical
// to an empty result. The banner hides
// itself when no recommendations load;
// resolve to null so a modal opened
// mid-flight stops loading too.
console.error("Failed to load domain recommendations:", error);
return null;
})
.finally(function() {
$scope.domainRecsLoading = false;
});
}
}
$scope.dismissDomainRecommendations = function() {
$scope.domainRecsBannerDismissed = true;
nvDataService.set("domain_recs_banner_dismissed", "1", { nocache: true })
.catch(function(error) {
console.error("Failed to save banner dismissal:", error);
});
};
$scope.openExploreDomains = function() {
domainSearchModalService.open({
cachedExploreData: cachedExploreData,
source: "manage-domain-upsell-banner",
exploreDataPromise: recommendationsPromise,
// After a store purchase, send the user to the
// domain list and open its create-domain chooser
// on the "Registered Domain" step with the
// purchased name prefilled — mirroring the list
// page's own purchase → Add Domain flow. The
// chooser lives on the list
// view, so the domain is handed over through the
// domains service (not a query param, which would
// reload the route and discard the chooser), then
// we navigate to the list with no search params.
onAddDomain: function(domain) {
$domainsService.setPendingAddDomain(domain);
$location.path("/").search("");
},
});
};
init();
},
]
);
return {
namespace: MODULE_NAMESPACE,
};
}
);
Back to Directory