/*
* site-monitor/services/sites.js Copyright 2021 cPanel, L.L.C.
* All rights reserved.
* copyright@cpanel.net http://cpanel.net
* This code is subject to the cPanel license. Unauthorized copying is prohibited
*/
/* global define, PAGE */
/** @namespace cpanel.siteMonitor.services.sites */
define(
[
"angular",
"lodash",
"cjt/util/locale",
"cjt/io/uapi-request",
"app/models/domain-type.enum",
"app/models/site",
"app/models/monitor",
"cjt/util/parse",
"app/services/monitoring",
"cjt/io/uapi",
"cjt/io/api2",
"cjt/modules",
"cjt/services/APICatcher",
"app/services/sitesCache"
],
function(angular, _, LOCALE, UAPIRequest, DomainType, Site, Monitor, PARSE) {
"use strict";
var app = angular.module("cpanel.siteMonitor.sitesService", [
"cjt2.services.apicatcher",
"cpanel.siteMonitor.sitesCacheService",
"cpanel.siteMonitor.monitoringService"
]);
app.value("PAGE", PAGE);
app.factory("sites", [
"$q",
"APICatcher",
"PAGE",
"sitesCache",
"monitoring",
function($q, APICatcher, PAGE, SitesCache, MonitoringAPI) {
/**
* service wrapper for domain related functions
*
* @module sites
*
* @param {Object} $q angular $q 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
* let sites = siteService.get();
* if(!sites) return;
* for(site in sites) {
* ...
* }
*/
var _mainDomain;
var _parkDomains;
var _addOnDomains;
var _subDomains;
var Sites = function() {};
Sites.prototype = APICatcher;
/**
* Lookup the sub-domain information by the subdomain name. This automatically
* added the main domain suffix so all you pass is the subdomain segment.
*
* @private
* @method _getSubDomainObject
* @param {string} subdomainName
* @returns {DomainInfo|undefined}
*/
Sites.prototype._getSubDomainObject = function _getSubDomainObject(subdomainName) {
var self = this;
if (typeof subdomainName === "string") {
return SitesCache.findByDomainName(subdomainName + "." + self.getMainDomain().domain);
}
return;
};
/**
* Updates the subdomains that host addon domains so the associations are identified.
*
* @private
* @method _associateAddonDomains
*/
Sites.prototype._associateAddonDomains = function _associateAddonDomains() {
var self = this;
angular.forEach(_addOnDomains, function(addonDomain) {
var subdomainObject = self._getSubDomainObject(addonDomain.subdomain);
if (subdomainObject) {
subdomainObject.associatedAddonDomain = addonDomain.domain;
}
});
};
/**
* Get the currently stored main domain
*
* @method getMainDomain
* @public
* @return {DomainInfo} returns the current main domain object
*/
Sites.prototype.getMainDomain = function _getMainDomain() {
return _mainDomain;
};
/**
* Find a domain object by the domain name
*
* @method findByDomainName
* @public
* @param {String} domainName domain name (bob.com)
* @return {Object} returns the domain object if found
*/
Sites.prototype.findByDomainName = function _findByDomainName(domainName) {
return SitesCache.findByDomainName(domainName);
};
/**
* Fetch the sites and caches them. This also returns the main domain information.
*
* @public
* @async
* @method fetchSites
* @return {Promise<Site[]>} returns a promise, then the sites for the account.
*/
Sites.prototype.fetchSites = function fetchSites() {
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.parseDomain(result.data.main_domain);
mainDomain.type = DomainType.MAIN;
_mainDomain = mainDomain;
SitesCache.add(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.parseDomain(rawDomain, DomainType.SUBDOMAIN);
_subDomains.push(parsedDomain);
SitesCache.add(parsedDomain);
});
_addOnDomains = [];
domains = result.data.addon_domains || [];
domains.forEach(function(rawDomain) {
var parsedDomain = self.parseDomain(rawDomain, DomainType.ADDON);
_addOnDomains.push(parsedDomain);
SitesCache.add(parsedDomain);
// Also add in this thing's backing subdomain
var subdomain = self.parseDomain(rawDomain, DomainType.ADDON);
subdomain.domain = subdomain.rootDomain;
subdomain.type = DomainType.SUBDOMAIN;
_subDomains.push(subdomain);
SitesCache.add(subdomain);
});
_parkDomains = [];
result.data.parked_domains.forEach(function(rawDomain) {
var parsedDomain = self.parseDomain(result.data.main_domain, DomainType.ALIAS);
parsedDomain.domain = rawDomain;
_parkDomains.push(parsedDomain);
SitesCache.add(parsedDomain);
});
return mainDomain;
});
};
/**
* Helper to check if any required keys are missing
*
* @param {Object} object - object to check.
* @returns {Function} - reducer build the list of missing keys.
*/
function makeMissingKeysReducer(object) {
return function(missing, key) {
if(!object[key]) {
missing.push(key);
}
return missing;
}
}
/**
* Parse the domain information returned by the api to build
* a Site model.
*
* @method parseDomain
* @param {DomainInfo} rawDomain
* @param {DomainType} actualType
* @returns {Site}
*/
Sites.prototype.parseDomain = function parseDomain(rawDomain, actualType) {
if (!rawDomain) {
throw new Error("Invalid domain information object");
}
var keys = Object.keys(rawDomain);
if (!keys || !keys.length) {
throw new Error("Invalid domain information object");
}
var missingRequired = [
"domain",
"homedir",
].reduce(makeMissingKeysReducer(rawDomain), []);
if (missingRequired.length) {
throw new Error("The domain information object is missing the keys: " + missingRequired.join(","));
}
if (!rawDomain.documentroot && !rawDomain.dir) {
throw new Error("The domain information object must have one of the keys: " + [ "documentroot" , "dir" ].join(","));
}
var self = this;
var domain = {
domain: rawDomain.domain,
homedir: rawDomain.homedir,
documentRoot: rawDomain.documentroot || rawDomain.dir,
rootDomain: rawDomain.servername,
isHttpsRedirecting: PARSE.parseBoolean(rawDomain.is_https_redirecting),
hasValidHTTPSAliases: PARSE.parseBoolean(rawDomain.all_aliases_valid),
nonHTTPS: !PARSE.parseBoolean(rawDomain.can_https_redirect),
redirectsTo: rawDomain.status === "not redirected" ? null : rawDomain.status,
type: rawDomain.type,
realRootDomain: PAGE.mainDomain,
};
if (actualType) {
domain.type = actualType;
domain.homedir = self.getMainDomain().homedir;
var altRootDomain = self.getMainDomain().domain;
var altSubDomain = domain.rootDomain ? domain.rootDomain.substr(0, domain.rootDomain.lastIndexOf("." + altRootDomain)) : null;
if (actualType !== DomainType.ALIAS) {
domain.subdomain = altSubDomain;
}
if (actualType === DomainType.SUBDOMAIN) {
domain.rootDomain = altRootDomain;
} else if ( actualType === DomainType.ALIAS ) {
domain.rootDomain = PAGE.mainDomain;
}
}
return new Site(domain);
};
var sitesLoadingPromise = null;
/**
* Helper to update the state of the monitors when the fetch failed.
*/
Sites.prototype._monitorFetchFailed = function failed() {
var self = this;
var sites = SitesCache.get();
if (sites && sites.length > 0) {
sites.forEach(function(site) {
site.fetchFailed = true;
});
}
};
/**
* Fetch the sites and their monitors all domains (main, addon, subdomain, alias).
* on a server. If there is a cached list already, the returned promise resolves to
* the cached versions saving api requests. The Cache is only updated if the cache is
* empty. You can use SitesCache.clear() to flush the cache before calling this method
* to for a new load from the server.
*
* @public
* @async
* @method get
* @param {Object} args
* @param {boolean} args.refresh - bypass and clear the cache before reloading
* @return {Promise<Site[]>} returns a promise, then the array of all domain objects
*/
Sites.prototype.get = function getSites(args) {
var self = this;
if (!args) { args = {}; }
var refresh = args.refresh || false;
// Prevent additional fetches when an existing fetch
// is already in progress. Just return the same promise.
if (sitesLoadingPromise) {
return sitesLoadingPromise;
}
if (!refresh) {
var sites = SitesCache.get();
if (sites.length > 0) {
return $q.resolve(sites);
}
}
// Clear the cache
SitesCache.clear();
self.error = null;
// Load a new dataset.
sitesLoadingPromise = $q
.all( [
self.fetchSites().then(function(response) {
if (self.error) {
// Call here and in catch for all so we handle the race
// between the two api calls.
self._monitorFetchFailed();
}
return response;
}),
MonitoringAPI.list()
] )
.then(function(results) {
self._associateAddonDomains();
var monitors = (typeof results[1] !== 'undefined') ? results[1] : undefined;
if (monitors) {
monitors.forEach(function(item) {
var monitor = Monitor.parseListItem(item);
var forDomain = monitor.domain.replace(/^www./, "");
var site = SitesCache.findByDomainName(forDomain);
if (site) {
site.addMonitor(monitor);
} else {
var newSite = new Site({ domain: forDomain, type: DomainType.EXTERNAL });
newSite.addMonitor(monitor);
SitesCache.add(newSite);
}
});
}
sitesLoadingPromise = null;
return SitesCache.get();
})
.catch(function(error) {
self.error = error;
// Call here and in then for domain so we handle the race
// between the two api calls.
self._monitorFetchFailed();
throw error;
});
return sitesLoadingPromise;
};
return new Sites();
}]);
}
);