// Copyright 2024 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 LOCALE:false */
(function(window) {
"use strict";
// -----------------------
// Shortcuts
// -----------------------
var YAHOO = window.YAHOO;
var CPANEL = window.CPANEL;
var DOM = YAHOO.util.Dom;
var EVENT = YAHOO.util.Event;
var LANG = YAHOO.lang;
var REGION = YAHOO.util.Region;
var document = window.document;
var Handlebars = window.Handlebars;
var PAGE = window.PAGE;
var nvdata = PAGE && PAGE.nvdata || {};
var certificates_table_sort = nvdata.certificates_table_sort || { key: "domains" };
// -----------------------
// Constants
// -----------------------
var IP_COMBOBOX_CONFIG = {
useShadow: false,
expander: "ipexpander",
applyLocalFilter: true,
queryMatchCase: false,
typeAhead: true,
autoHighlight: false,
};
var MAX_COLUMN_WIDTH = {
user: 90,
apache: 130,
};
var STAR_DOT_REGEXP = /^[*][.]/;
var SORRY_COMMA_REGEXP = /sorry,/i;
// -----------------------
// State Variables
// -----------------------
var FQDN_TO_CREATED_DOMAIN = {};
var errorNotice;
var validators = {};
var formProgressOverlay;
var sslResultsPanel;
var hiddenWhiteSpaceListTemplate;
var pageHasDomainSelector = false; // True in cPanel, false in WHM.
var lastCaughtFocus = null; // The variable that catchNextFocus() sets.
var pageProgressPanel;
var certificatesDataTable = null; // This is the certificate DataTable shared by changeUserComplete() and searchOk()
var current_user = null;
var current_browse_source = nvdata.browse_source || "user";
var dialogEventsSetup = false;
var installMessageMaker;
var ipSelectorItemTemplate;
var wildcard_subdomain_warning;
/**
* Request a list of the certificates.
* @public
* @method browsessl
* @param {object} clicked_el The element from which to expand the progress panel
* @static
*/
function browsessl(clicked_el) {
clearErrorNotice();
pageProgressPanel = new CPANEL.ajax.Progress_Panel( null, {
zIndex: 2000, // to be above CJT validation message overlays
effect: CPANEL.ajax.FADE_MODAL,
} );
if (clicked_el) {
pageProgressPanel.show_from_source(clicked_el);
} else {
pageProgressPanel.show();
}
pageProgressPanel.after_hideEvent.subscribe( pageProgressPanel.destroy, pageProgressPanel, true );
var api_call;
if (CPANEL.is_whm()) {
if ( current_browse_source === "user" ) {
// Select the last selected user again
// so we retain context.
api_call = {
func: "listcrts",
data: { user: current_user },
};
} else {
api_call = {
func: "fetch_ssl_vhosts",
};
}
} else {
api_call = {
version: 3,
module: "SSL",
func: "list_certs",
api_data: {
filter: [ [ "domain_is_configured", "eq", 1 ] ],
},
};
}
api_call.callback = {
success: searchOk,
failure: searchFailure,
};
CPANEL.api(api_call);
}
/**
* Transform a a domain list into a list of simple structures
* that inform the template whether to make the domain a link.
*/
var installSuccessDomainListXform = function(d) {
return { domain: d, makeLink: !/^[*]/.test(d) };
};
/**
* Sends the form data off to do the SSL install for Apache.
*
* @method sendApacheInstall
* @param {String|Object} form The form, or its ID.
*/
function sendApacheInstall(form) {
form = DOM.get(form);
var formdata = CPANEL.dom.get_data_from_form(form);
// No need to catch exceptions here since validation has already run.
var certParse = CPANEL.ssl.parseCertificateText(formdata.crt);
var progress = new CPANEL.ajax.Progress_Panel( null, {
show_status: true,
status_html: LOCALE.maketext("Installing …"),
} );
progress.show_from_source(form);
progress.after_hideEvent.subscribe( progress.destroy, progress, true );
/**
* What to do with an "OK" click when the page is to reload.
*/
var reload_button_handler = function(e) {
var this_button = this.getButtons()[0];
this_button.disabled = true;
if (document.activeElement === this_button) {
this_button.blur();
}
// Strip out a query from the URL, and reload.
window.location.href = window.location.pathname;
};
/**
* What to do with an "OK" click when the page doesn't reload.
*/
var non_reload_button_handler = function(e) {
this.cancel();
};
/**
* What to call when the install API call returns successfully.
*/
var onsuccess = function(o) {
var ip = o.cpanel_data.ip || o.argument && o.argument.ip;
var tData = {};
if ( o.cpanel_data.working_domains ) {
tData.workingDomains = o.cpanel_data.working_domains.map(installSuccessDomainListXform);
tData.workingDomainsMessage = LOCALE.maketext("The SSL website is now active and accessible via HTTPS on [numerate,_1,this domain,these domains]:", tData.workingDomains.length);
}
if ( o.cpanel_data.warning_domains && o.cpanel_data.warning_domains.length ) {
tData.warningDomains = o.cpanel_data.warning_domains.map(installSuccessDomainListXform);
tData.warningDomainsMessage = LOCALE.maketext("The SSL website is also accessible via [numerate,_1,this domain,these domains], but the certificate does not support [numerate,_1,it,them]. Web browsers will show a warning when accessing [numerate,_1,this domain,these domains] via HTTPS:", tData.warningDomains.length);
}
if ( o.cpanel_data.extra_certificate_domains && o.cpanel_data.extra_certificate_domains.length ) {
tData.extraCertificateDomains = o.cpanel_data.extra_certificate_domains.map( function(d) {
return { domain: d };
} );
tData.extraCertificateDomainsMessage = LOCALE.maketext("The SSL certificate also supports [numerate,_1,this domain,these domains], but [numerate,_1,this domain does,these domains do] not refer to the SSL website mentioned above:", tData.extraCertificateDomains.length);
}
var okHandler, messageTemplate;
switch ( o.cpanel_data.action ) {
case "install":
tData.statusMessageHTML = LOCALE.maketext("You have successfully configured SSL.");
okHandler = reload_button_handler;
tData.needReload = true;
break;
case "update":
tData.statusMessageHTML = LOCALE.maketext("You have successfully updated the SSL website’s certificate.");
okHandler = reload_button_handler;
tData.needReload = true;
break;
case "none":
tData.statusMessageHTML = LOCALE.maketext("This SSL certificate was already installed.");
okHandler = non_reload_button_handler;
}
if (!messageTemplate) {
messageTemplate = DOM.get("installSuccessTemplate").text;
}
if ( !installMessageMaker ) {
installMessageMaker = Handlebars.compile( messageTemplate );
}
var message = installMessageMaker( tData );
var dialog = new CPANEL.ajax.Common_Dialog( null, {
effect: CPANEL.ajax.FADE_MODAL,
buttons: [
{
text: LOCALE.maketext("OK"),
isDefault: true,
handler: okHandler,
},
],
} );
var header_text;
if (o.cpanel_data.action === "update") {
header_text = LOCALE.maketext("SSL Certificate Successfully Updated");
} else {
header_text = LOCALE.maketext("SSL Host Successfully Installed");
}
dialog.setHeader( CPANEL.widgets.Dialog.applyDialogHeader(header_text) );
dialog.beforeShowEvent.subscribe( function() {
dialog.form.innerHTML = message;
dialog.center();
} );
progress.fade_to(dialog);
};
var apicall;
if ( CPANEL.is_cpanel() ) {
apicall = {
version: 3,
module: "SSL",
func: "install_ssl",
data: {
domain: formdata.domain,
cert: formdata.crt,
key: formdata.key,
cabundle: formdata.cabundle,
},
};
} else {
apicall = {
func: "installssl",
data: {
domain: formdata.domain,
ip: formdata.ip,
crt: formdata.crt,
key: formdata.key,
cab: formdata.cabundle,
},
};
}
apicall.callback = CPANEL.ajax.build_page_callback( onsuccess, {
on_error: function() {
progress.hide();
},
} );
apicall.callback.argument = { ip: formdata.ip, domain: formdata.domain };
CPANEL.api(apicall);
}
/**
* Fetches the selected certificate from the dialog box.
* @method handleBeforeSubmit
* @param {String} type The CustomEvent type
* @param {Object[]} args The CustomEvent arguments
* @param {Object} obj The scope object
* @static
*/
function handleBeforeSubmit(type, args, obj) {
var selected = certificatesDataTable.getSelectedRows()[0];
var record = certificatesDataTable.getRecord(selected);
if ( current_browse_source === "user" ) {
fetchByCertId( record.getData("id"), current_user );
} else {
_populateFormByVhost( record.getData("servername") );
}
}
/**
* Fire off an async call that should eventually populate the form with
* the indicated SSL vhost's SSL components.
*
* @method _populateFormByVhost
* @private
* @param servername {String} The servername of the vhost whose information to load.
*/
function _populateFormByVhost( servername ) {
CPANEL.sharedjs.sslinstall.showFormProgressOverlay();
CPANEL.api( {
func: "fetch_vhost_ssl_components",
api_data: {
filter: [ ["servername", "eq", servername] ],
},
callback: CPANEL.ajax.build_page_callback(
populate_form_with_ssl_components,
{
on_error: CPANEL.sharedjs.sslinstall.hideFormProgressOverlay,
}
),
}
);
}
/**
* Schlep the results of an AJAX call into the form.
* This assumes that the passed-in object is for an API call that returns:
* [
* { certificate:"..", key:"..", cabundle:".." }
* ]
*
* @method populate_form_with_ssl_components
* @param o {Object} An API response object, as described.
*/
var populate_form_with_ssl_components = function(o) {
var payload = o.cpanel_data[0];
var mainform = DOM.get("mainform");
mainform.crt.value = payload.certificate;
mainform.key.value = payload.key;
mainform.cabundle.value = payload.cabundle || "";
try {
_populate_domain_from_parsed_cert(CPANEL.ssl.parseCertificateText( payload.certificate ));
} catch (e) {}
CPANEL.sharedjs.sslinstall.hideFormProgressOverlay();
CPANEL.sharedjs.sslinstall.runValidation();
CPANEL.sharedjs.sslinstall.updateUI();
};
/**
* Formatter that injects hidden whitespace after a period
* in a string
* @param {Object} elCell DOM object for the cell.
* @param {Object} oRecord Record for this row.
* @param {Object} oColumn Column definition for this cell.
* @param {Object} oData Data for the specific cell.
*/
var injectHiddenWhiteSpace = function(elCell, oRecord, oColumn, oData) {
if (LANG.isValue(oData)) {
elCell.innerHTML = oData.html_encode().replace(/\./g, ".<wbr><a class=\"wbr\"></a>");
elCell.title = oData;
}
};
/**
* Formatter that injects hidden whitespace after a period
* in a string
* @param {Object} elCell DOM object for the cell.
* @param {Object} oRecord Record for this row.
* @param {Object} oColumn Column definition for this cell.
* @param {Object} oData Data for the specific cell.
*/
var injectHiddenWhiteSpaceList = function(elCell, oRecord, oColumn, oData) {
if (oData && oData.length) {
var array = oData;
var data = [];
for (var i = 0, l = array.length; i < l; i++) {
var tmp = array[i].html_encode();
var item = {
title: tmp,
value: tmp.replace(/\./g, ".<wbr><a class=\"wbr\"></a>"),
};
data.push(item);
}
// Process the template
elCell.innerHTML = hiddenWhiteSpaceListTemplate( { data: data });
}
};
/**
* Formatter that correctly formats date to be
* international compliant in the users selected language.
* @param {Object} elCell DOM object for the cell.
* @param {Object} oRecord Record for this row.
* @param {Object} oColumn Column definition for this cell.
* @param {Object} oData Data for the specific cell.
*/
var formatLocaleDate = function(elCell, oRecord, oColumn, oData) {
if (LANG.isValue(oData)) {
elCell.innerHTML = LOCALE.datetime( oData, "date_format_short" );
elCell.title = LOCALE.datetime( oData, "datetime_format_long" );
}
};
/**
* Formatter that shows either the issuer or Self-Signed.
* @param {Object} elCell DOM object for the cell.
* @param {Object} oRecord Record for this row.
* @param {Object} oColumn Column definition for this cell.
* @param {Object} oData Data for the specific cell.
*/
var formatIssuer = function(elCell, oRecord, oColumn, oData) {
if (oRecord && oRecord.getData("is_self_signed")) {
elCell.innerHTML = LOCALE.maketext("Self-Signed");
} else {
injectHiddenWhiteSpace.apply(this, arguments);
}
};
/**
* Formats INPUT TYPE=RADIO elements.
*
* @method formatRadio
* @param el {HTMLElement} The element to format with markup.
* @param oRecord {YAHOO.widget.Record} Record instance.
* @param oColumn {YAHOO.widget.Column} Column instance.
* @param oData {Object} (Optional) Data value for the cell.
* @param oDataTable {YAHOO.widget.DataTable} DataTable instance.
* @static
*/
var formatRadio = function(el, oRecord, oColumn, oData) {
var value = oData + "";
el.innerHTML = "<input type=\"radio\"" +
" name=\"selected-cert\"" +
" class=\"" + YAHOO.widget.DataTable.CLASS_RADIO + "\"" +
" value=\"" + value.html_encode() + "\" />";
};
/**
* Converts data to type Date from a linux timestamp.
* @method parseLinuxTimeStamp
* @param oData {Date | String | Number} Data to convert.
* @return {Date} A Date instance.
*/
var parseLinuxTimeStamp = function(oData) {
var date = null;
// Convert to date
if (LANG.isValue(oData) && !(oData instanceof Date)) {
date = new Date(parseInt(oData * 1000, 10));
} else {
return oData;
}
// Validate
if (date instanceof Date) {
return date;
} else {
YAHOO.log("Could not convert data " + LANG.dump(oData) + " to type Date", "warn", this.toString());
return null;
}
};
/**
* Converts data to type Boolean from perl boolean.
* @method parsePerlBoolean
* @param oData {Date | String | Number} Data to convert.
* @return {Boolean} A Boolean instance.
*/
var parsePerlBoolean = function(oData) {
var value = null;
// Convert to boolean
if (LANG.isValue(oData) && (typeof oData !== "boolean")) {
if (oData && (parseInt(oData, 10) === 1)) {
value = true;
} else {
value = false;
}
} else {
return oData;
}
// Validate
if (typeof value === "boolean") {
return value;
} else {
YAHOO.log("Could not convert data " + LANG.dump(oData) + " to type Date", "warn", this.toString());
return null;
}
};
/**
* Utility method for sorting DataTable by the issuer.
* This sorts self-signed certs "greater".
* @method sortIssuer
* @param a {Record} The first Record to compare.
* @param b {Record} The second Record to compare.
* @param desc {Boolean} Whether the sort is a descending sort or not.
* @return {Number} -1 if a is first, 1 if a is second, 0 if the same
*/
var sortIssuer = function(a, b, desc) {
var a_is_self_signed = a.getData("is_self_signed");
var b_is_self_signed = b.getData("is_self_signed");
if (a_is_self_signed) {
if (b_is_self_signed) {
return 0;
}
return desc ? -1 : 1;
} else if (b_is_self_signed) {
return desc ? 1 : -1;
}
return YAHOO.util.Sort.compare( a.getData("issuer.organizationName"), b.getData("issuer.organizationName"), desc );
};
/**
* Utility method for sorting DataTable by the domains lists.
* @method sortDomains
* @param a {Record} The first Record to compare.
* @param b {Record} The second Record to compare.
* @param desc {Boolean} Whether the sort is a descending sort or not.
* @return {Number} -1 if a is first, 1 if a is second, 0 if the same
*/
var sortDomains = function(a, b, desc) {
return YAHOO.util.Sort.compare( a.getData("domains").join("\n"), b.getData("domains").join("\n"), desc );
};
// Setup the schema definitions for the data source
var certResponseSchema = {
fields: [
"id",
"domain",
"issuer.organizationName",
{ key: "not_after", parser: parseLinuxTimeStamp },
"friendly_name", // for browsing user
"servername", // for browsing apache
"domains",
{ key: "is_self_signed", parser: parsePerlBoolean },
],
};
var certColumnDefs = [
{
key: "id",
label: "",
formatter: formatRadio,
sortable: false,
abbr: LOCALE.maketext("Select a certificate below:"),
},
{
key: "domains",
maxAutoWidth: MAX_COLUMN_WIDTH.user,
label: LOCALE.maketext("Domains"),
formatter: injectHiddenWhiteSpaceList,
sortable: true,
sortOptions: { sortFunction: sortDomains },
abbr: LOCALE.maketext("Domain names on the certificate."),
},
{
key: "issuer.organizationName",
maxAutoWidth: MAX_COLUMN_WIDTH.user,
label: LOCALE.maketext("Issuer"),
formatter: formatIssuer,
sortable: true,
sortOptions: { sortFunction: sortIssuer },
abbr: LOCALE.maketext("Issuer organization name."),
},
{
key: "not_after",
label: LOCALE.maketext("Expiration"),
formatter: formatLocaleDate,
sortable: true,
abbr: LOCALE.maketext("The certificate’s expiration date"),
},
{
key: "friendly_name",
maxAutoWidth: MAX_COLUMN_WIDTH.user,
label: LOCALE.maketext("Description"),
formatter: injectHiddenWhiteSpace,
sortable: true,
abbr: LOCALE.maketext("A user-defined description for the certificate."),
},
];
/**
* Handle things that always happen when the change-user AJAX call returns,
* regardless of success or failure.
* @method _changeUserOnReturn
* @param o {Object} CPANEL.api response object
* @static
*/
function _changeUserOnReturn(o) {
var users = DOM.get("users");
if ( o.argument && (o.argument.oldActiveElement === users) && !lastCaughtFocus ) {
users.focus();
}
}
/**
* Handle the case where the user context change triggered
* listcrts request succeeds.
* @method changeUserComplete
* @param o {Object} CPANEL.api response object
* @static
*/
function changeUserComplete(o) {
_changeUserOnReturn(o);
_set_column_widths_for_browse_source("user");
certificatesDataTable.showColumn("friendly_name");
_update_cert_browser_table_with_new_certs(o.cpanel_data);
// Update the current user
current_user = o.argument.user;
}
function _set_column_widths_for_browse_source(browse_source) {
var colset = certificatesDataTable.getColumnSet();
for (var c = 0; c < colset.flat.length; c++) {
colset.flat[c].maxAutoWidth = MAX_COLUMN_WIDTH[browse_source];
}
}
// WHM "fetch_ssl_vhosts" returns data in a format that doesn't quite jive
// with what the cert browser here expects, so this function massages that
// data to play nicely with the cert browser table.
function _format_fetch_ssl_vhosts_for_cert_browser(vhosts) {
var with_cert = vhosts.filter(function(v) {
return v.crt;
});
// Grab "servername" from the "crt" hash.
var crts = with_cert.map( function(v) {
v.crt.servername = v.servername;
return v.crt;
} );
// Make sure we show each cert only once.
var certs_already_seen = {};
crts = crts.filter( function(c) {
if ( certs_already_seen[c.id] ) {
return false;
}
certs_already_seen[c.id] = true;
return true;
} );
return crts;
}
function loadApacheCertsComplete(o) {
certificatesDataTable.hideColumn("friendly_name");
_set_column_widths_for_browse_source("apache");
var crts = _format_fetch_ssl_vhosts_for_cert_browser( o.cpanel_data );
_update_cert_browser_table_with_new_certs(crts);
}
function _update_cert_browser_table_with_new_certs(crts) {
_enable_cert_browser_controls();
// Hide the table message
certificatesDataTable.hideTableMessage();
var certDataSource = new YAHOO.util.LocalDataSource(crts);
certDataSource.responseType = YAHOO.util.DataSource.TYPE_JSARRAY;
certDataSource.responseSchema = certResponseSchema;
certificatesDataTable.load( { datasource: certDataSource } );
var state = certificatesDataTable.getState();
if ( state.sortedBy ) {
certificatesDataTable.sortColumn(
certificatesDataTable.getColumn( state.sortedBy.key ),
state.sortedBy.dir
);
}
certificatesDataTable.selectRow(0);
}
/**
* Handle the case where loading new certs fails during the API call.
* This works for both user and Apache cert browsing.
*
* @method chageUserFailed
* @param {Object} o Error object.
* @static
*/
function loadNewCertsFailed(o) {
_changeUserOnReturn(o);
var postRenderOnFail = function() {
certificatesDataTable.unsubscribe("postRenderEvent", postRenderOnFail);
// Show an error
certificatesDataTable.showTableMessage(
LOCALE.maketext("The certificate list could not be retrieved because of an error: [_1]", o.cpanel_error.html_encode()),
YAHOO.widget.DataTable.CLASS_ERROR);
};
certificatesDataTable.subscribe("postRenderEvent", postRenderOnFail);
// Remove all the rows
var length = certificatesDataTable.getRecordSet().getLength();
if (length > 0) {
certificatesDataTable.deleteRows(0, length);
// postRenderOnFail is triggered after the delete completes.
} else {
// No records to delete, so call synchronously
postRenderOnFail();
}
}
/**
* Set a body listener that catches the next focus event.
* When it catches one, it sets the "lastCaughtFocus" variable.
* This is useful for re-focusing an element that you've disabled
* for an AJAX call, but only doing so if the user hasn’t focused
* something else in the meantime.
*
* TODO: Make this a class that tracks lastCaughtFocus internally.
*
* @method catchNextFocus
* @static
*/
function catchNextFocus() {
lastCaughtFocus = null;
var listener = function(e) {
EVENT.removeListener( this, "focusin", listener );
lastCaughtFocus = e;
};
EVENT.on( document.body, "focusin", listener );
}
/**
* Convenience wrapper around YAHOO.util.Event.preventDefault()
*/
var preventDefault = function(e) {
EVENT.preventDefault(e);
};
/**
* Handler for when the browse source changes.
*
* NOTE: Context object MUST be the clicked radio button.
*
* @method _onBrowseSourceChange
* @private
* @param e {Object} The event object from a YUI 2 DOM listener.
*/
var _onBrowseSourceChange = function(e) {
var clicked_el = this;
current_browse_source = clicked_el.value;
if ( current_browse_source === "user" ) {
// Read this manually because get_data_from_form will reject values from
// disabled form controls.
var select = DOM.get("users");
_update_cert_table_for_user( select[select.selectedIndex].value );
} else {
_update_cert_table_for_apache();
}
};
/**
* Handler for when the user in the cert browser popup changes.
*
* NOTE: Context object MUST be the user <select>.
*
* @method _onUserChange
* @private
* @param e {Object} The event object from a YUI 2 DOM listener.
*/
var _onUserChange = function(e) {
if (this.selectedIndex === -1) {
return;
}
var new_user = this.options[this.selectedIndex].value;
if ( new_user !== current_user ) {
_update_cert_table_for_user(new_user);
}
};
/**
* Fire off an AJAX call that eventually will update the cert browser table.
* This is for when we want to query Apache.
*
* @method _update_cert_table_for_apache
* @private
*/
var _update_cert_table_for_apache = function() {
var what_to_do = function() {
// Show the loading message
certificatesDataTable.showTableMessage(
LOCALE.maketext("Loading installed Apache certificates …"),
YAHOO.widget.DataTable.CLASS_LOADING);
CPANEL.api( {
func: "fetch_ssl_vhosts",
callback: {
success: loadApacheCertsComplete,
failure: loadNewCertsFailed,
},
} );
};
_update_cert_table(what_to_do);
};
/**
* Query the DOM to return the cert browser controls.
*
* @method _get_cert_browser_controls
* @private
*/
var _get_cert_browser_controls = function() {
return CPANEL.Y.all("input[name=browse_source], select#users");
};
/**
* Enable or disable the cert browser controls as needed.
* This is smart enough not to enable the user drop-down when we're
* browsing Apache.
*
* @method _get_cert_browser_controls
* @private
*/
var _set_cert_browser_controls_disabled = function(to_disable) {
_get_cert_browser_controls().forEach( function(el) {
// Ensure that we don't enable the user selector when we're
// browsing Apache's certificates.
if ( to_disable || (current_browse_source === "user") || (el !== DOM.get("users")) ) {
el.disabled = to_disable;
}
} );
};
/**
* Enable the cert browser controls.
*
* @method _get_cert_browser_controls
* @private
*/
var _enable_cert_browser_controls = function() {
_set_cert_browser_controls_disabled(false);
};
/**
* Disable the cert browser controls.
*
* @method _get_cert_browser_controls
* @private
*/
var _disable_cert_browser_controls = function() {
_set_cert_browser_controls_disabled(true);
};
/**
* Fire off an AJAX call that eventually will update the cert browser table.
* This is code common to both Apache and user-sslstorage browsing.
*
* @method _update_cert_table_for_apache
* @private
*/
var _update_cert_table = function(what_to_do_after_clearing_table) {
_disable_cert_browser_controls();
// For update_cert_table_for_user. Put it here in case some browser
// fires "focus" when you disable the activeElement.
catchNextFocus();
var postRender = function() {
certificatesDataTable.unsubscribe("postRenderEvent", postRender);
return what_to_do_after_clearing_table.apply(this, arguments);
};
// Remove all the rows
var length = certificatesDataTable.getRecordSet().getLength();
if (length > 0) {
certificatesDataTable.subscribe("postRenderEvent", postRender);
certificatesDataTable.deleteRows(0, length);
// postRender is triggered after the delete completes.
} else {
// No records to delete, so call synchronously
postRender();
}
};
/**
* Fire off an AJAX call that eventually will update the cert browser table.
* This is for when we want to query a user's datastore.
*
* @method _update_cert_table_for_user
* @private
*/
var _update_cert_table_for_user = function(new_user) {
// If the users drop-down is focused,
// then we want to restore that focused state when the
// AJAX call returns, unless the user has focused
// something else in the meantime.
var oldActiveElement = document.activeElement;
/**
* What to do immediately after the cert datatable is done rendering.
*/
var postRender = function() {
// Show the loading message
certificatesDataTable.showTableMessage(
LOCALE.maketext("Loading certificates for “[output,strong,_1]” …", new_user),
YAHOO.widget.DataTable.CLASS_LOADING);
CPANEL.api( {
func: "listcrts",
data: { user: new_user },
callback: {
success: changeUserComplete,
failure: loadNewCertsFailed,
argument: {
user: new_user,
oldActiveElement: oldActiveElement,
},
},
} );
};
_update_cert_table(postRender);
};
/**
* Handle the case where the listcrts request fails.
*
* @method searchFailure
* @param o {Object} CPANEL.api response object
* @static
*/
function searchFailure(o) {
showErrorNotice( LOCALE.maketext("The certificate list could not be retrieved because of an error: [_1]", o.cpanel_error.html_encode()));
}
/**
* Handle the case where the listcrts request succeeds.
* @method searchOk
* @param o {Object} CPANEL.api response object
* @static
*/
function searchOk(o) {
/**
* The handler for when the SSL cert browser is shown.
* This event needs to be setup every time the API returns since it
* changes the table's data.
*/
var beforeShowResultsPanel = function() {
sslResultsPanel.beforeShowEvent.unsubscribe(beforeShowResultsPanel);
// Add to prevent the parent window from scrolling when
// the popup is scrolling past its top or bottom.
EVENT.addListener(document.body, "scroll", preventDefault);
_enable_cert_browser_controls();
// Set the current user (in WHM)
if (DOM.get("users")) {
var users = DOM.get("users");
CPANEL.dom.set_form_el_value(users, current_user);
}
// For Apache certs, we process below
var crts = o.cpanel_data;
if ( CPANEL.is_cpanel() && PAGE && PAGE.data ) {
var c; // silly jshint
var installable_domains = PAGE.data.installable_domains;
// TODO: Implement this using YUI 3's array extras?
for (c = crts.length - 1; c >= 0; c--) {
var l = installable_domains.length;
var cert_matches_at_least_one_account_domain = false;
for ( var i = 0; i < l; i++ ) {
if ( CPANEL.ssl.doesDomainMatchOneOf( installable_domains[i], crts[c].domains ) ) {
cert_matches_at_least_one_account_domain = true;
break;
}
}
if (!cert_matches_at_least_one_account_domain) {
crts.splice(c, 1);
}
}
} else if ( current_browse_source === "apache" ) {
crts = _format_fetch_ssl_vhosts_for_cert_browser(crts);
// We set maxAutoWidth to MAX_COLUMN_WIDTH.user in the column
// definitions. If case the table's first view is with Apache
// certs instead, switch maxAutoWidth here to what it needs to be.
for (var cd = 0; cd < certColumnDefs.length; cd++) {
certColumnDefs[cd].maxAutoWidth = MAX_COLUMN_WIDTH.apache;
if (certColumnDefs[cd].key === "friendly_name") {
certColumnDefs[cd].hidden = true;
}
}
}
// Setup the data-source for the data table
var certDataSource = new YAHOO.util.LocalDataSource(crts);
certDataSource.responseType = YAHOO.util.DataSource.TYPE_JSARRAY;
certDataSource.responseSchema = certResponseSchema;
// Setup the scrolling data table.
var list = DOM.get("certlist");
if (list) {
certificatesDataTable = new YAHOO.widget.ScrollingDataTable(
"certlist",
certColumnDefs,
certDataSource,
{
height: "250px",
className: "sortable",
MSG_EMPTY: LOCALE.maketext("This account does not have any installable certificates."),
}
);
// Do the sort on the client since the API,
// as of early 2013, doesn't know how to sort arrays.
// NOTE: This doesn't work if you put it in the instantiation config
// since that just tells DataTable how the data is *already* sorted.
certificatesDataTable.sortColumn(
certificatesDataTable.getColumn(certificates_table_sort.key),
certificates_table_sort.dir || YAHOO.widget.DataTable.CLASS_ASC
);
certificatesDataTable.set("selectionMode", "single");
certificatesDataTable.subscribe("theadCellClickEvent", function(e) {
// This fixes an issue with selected and sorting.
if (selectedRecord) {
this.unselectAllRows();
actionSource = "sort";
this.selectRow(selectedRecord);
}
});
certificatesDataTable.subscribe( "columnSortEvent", function(oArgs) {
var the_sort = this.get("sortedBy");
certificates_table_sort = {
dir: the_sort.dir,
key: the_sort.key,
};
CPANEL.nvdata.save();
} );
// Capture the source for the select even, we are trying
// to screen out the ones that are from mouse based actions.
var actionSource = null;
var selectedRow = null;
var selectedRecord = null;
certificatesDataTable.subscribe("radioClickEvent", function(e) {
var radio = e.target;
if (radio.checked) {
this.unselectAllRows();
var row = this.getTrEl(radio);
actionSource = "radio";
this.selectRow(row);
}
});
certificatesDataTable.subscribe("rowSelectEvent", function(e) {
// Prevent text selection as recommended in datatable
this.clearTextSelection();
// We only get here for up and down arrow keys
var row = e.el;
var targetRecord = this.getRecord(row);
selectedRecord = targetRecord;
// Select the radio button.
CPANEL.Y(row).one("input.yui-dt-radio").checked = true;
if (actionSource !== null) {
selectedRow = row;
// Not a keyboard event so reset it and return.
actionSource = null;
return;
}
// Scroll as needed
var parentContainer = row.offsetParent.parentNode;
var regionParent = REGION.getRegion(parentContainer);
var regionRow = REGION.getRegion(row);
var regionIntersect = regionRow.intersect(regionParent);
if (regionIntersect === null ||
regionIntersect.getArea() === 0 ||
regionIntersect.height < regionRow.height) {
// NOTE: There is still a little issue here, but this is
// usable. If you scroll the selected item off the screen
// then try to use the keyboard to scroll selection, it doesn't
// correctly move the selected item into view.
// regionIntersect.getArea() === 0 means no overlap
// regionIntersect.height < regionRow.height mean partial overlap
// Calculate the fudge for partial overlaps
var deltaHeight = regionIntersect ? regionIntersect.height : 0;
// Determine if we need to scroll to top or scroll to bottom
// depending of in we are scrolling up or down.
var nextRow = this.getNextTrEl(row);
var scrollingUp = selectedRow === nextRow;
if (scrollingUp) {
parentContainer.scrollTop -= regionRow.height + deltaHeight;
} else {
parentContainer.scrollTop += regionRow.height + deltaHeight;
}
}
// Update the state variable used to track the previous row.
selectedRow = row;
});
certificatesDataTable.subscribe("rowDblclickEvent", function(e) {
// Prevent text selection as recommended in datatable
this.clearTextSelection();
EVENT.preventDefault(e);
var row = DOM.get(e.target);
this.getRecord(row);
sslResultsPanel.submit();
});
certificatesDataTable.subscribe("rowClickEvent", function(e) {
EVENT.preventDefault(e);
this.unselectAllRows();
var row = DOM.get(e.target);
actionSource = "click";
this.selectRow(row);
});
certificatesDataTable.subscribe("postRenderEvent", function(e) {
sslResultsPanel.center();
});
certificatesDataTable.selectRow(0);
}
};
if (!dialogEventsSetup) {
sslResultsPanel.beforeHideEvent.subscribe(function() {
// Restore normal parent window scrolling behavior.
EVENT.removeListener(document.body, "scroll", preventDefault);
// Release the table for garbage collection since
// we will just recreate it on each show.
certificatesDataTable.destroy();
certificatesDataTable = null;
});
dialogEventsSetup = true;
}
sslResultsPanel.beforeShowEvent.subscribe(beforeShowResultsPanel);
// Show the popup
pageProgressPanel.fade_to(sslResultsPanel);
}
/**
* Clears the notice area.
* @method clearErrorNotice
* @static
*/
function clearErrorNotice() {
if (errorNotice && errorNotice.cfg) {
errorNotice.hide();
errorNotice.destroy();
errorNotice = null;
}
}
/**
* Shows an error message
* @method showErrorNotice
* @param text {String} Error message text.
* @param extraText {String} Text to add to the collapsible section.
* @param extraOpenLabel {String} Open collapsable section label.
* @param extraCloseLabel {String} Close collapsable section label.
* @static
*/
function showErrorNotice(text, extraText, extraOpenLabel, extraCloseLabel) {
if (formProgressOverlay && formProgressOverlay.cfg) {
formProgressOverlay.hide();
}
if (pageProgressPanel && pageProgressPanel.cfg) {
pageProgressPanel.hide();
}
var BUTTON_PREFIX = "en_btn_";
var AREA_PREFIX = "en_area_";
var buttonId, areaId;
var elID = DOM.generateId();
if (extraText) {
extraOpenLabel = extraOpenLabel || LOCALE.maketext("Show");
extraCloseLabel = extraCloseLabel || LOCALE.maketext("Hide");
buttonId = BUTTON_PREFIX + elID;
areaId = AREA_PREFIX + elID;
text += YAHOO.lang.substitute(DOM.get("error-extra-block").text, {
"btnId": buttonId,
"areaId": areaId,
"label": extraOpenLabel,
"content": extraText.html_encode(),
});
}
errorNotice = new CPANEL.widgets.Dynamic_Page_Notice({
level: "error",
content: text,
visible: false,
});
if (extraText) {
// Setup the events just before we show the notice
errorNotice.beforeShowEvent.subscribe(function(e) {
var theButtonId = buttonId;
var btn = DOM.get(theButtonId);
// Setup the toggle event handler
EVENT.on(btn, "click", function(e) {
var el = DOM.get(areaId);
if (DOM.getStyle(el, "display") === "none") {
DOM.setStyle(el, "display", "");
this.innerHTML = extraCloseLabel;
} else {
DOM.setStyle(el, "display", "none");
this.innerHTML = extraOpenLabel;
}
errorNotice = null;
});
});
}
errorNotice.show();
errorNotice.hideEvent.subscribe( function() {
CPANEL.align_panels_event.fire();
} );
CPANEL.align_panels_event.fire();
}
/**
* Set a Page_Progress_Overlay as an AJAX status indicator.
*
* @method showFormProgressOverlay
* @static
*/
function showFormProgressOverlay(content_html) {
if ( !formProgressOverlay || !formProgressOverlay.cfg ) {
formProgressOverlay = new CPANEL.ajax.Page_Progress_Overlay( null, {
zIndex: 2000, // to be above CJT validation message overlays
covers: DOM.get("mainform"),
show_status: !!content_html,
status_html: content_html,
} );
} else {
formProgressOverlay.set_status_now(content_html);
}
formProgressOverlay.show();
}
/**
* Hide the Page_Progress_Overlay AJAX status indicator.
*
* @method hideFormProgressOverlay
* @static
*/
function hideFormProgressOverlay(content_html) {
formProgressOverlay.hide();
}
/**
* Validate the domain, allowing wildcards through.
*
* @method validateDomain
* @return {Boolean}
*/
var validateDomain = function(el) {
var val = ( typeof el === "object" ) ? el.value : el;
val = val.trim();
return !val || CPANEL.validate.host(val.replace(/^\*\./, ""));
};
/**
* Validate that there is at least one service selected.
*
* @method _validate_services_selected
* @private
* @return {Boolean}
*/
function _validate_services_selected() {
return !!CPANEL.dom.get_data_from_form("mainform").service;
}
/**
* Validate the IP, allowing wildcards through.
* NOTE: This only works for WHM. In cPanel, there is no need to check an IP.
*
* @method validateIP
* @return {Boolean}
*/
var validateIP = function(el) {
return !validateIP_fail_reason(el) ? true : false;
};
/**
* Give a reason, if any, why the IP is invalid.
* Returns the empty string otherwise.
*
* @method validateIP_for_domain_fail_reason
* @return {String}
*/
var validateIP_fail_reason = function(el) {
var val = ( typeof el === "object" ) ? el.value : el;
if (val.length) {
if ( !CPANEL.validate.ip(val) ) {
return LOCALE.maketext("Enter a valid IP address.");
}
var sslips = PAGE.properties && PAGE.properties.sslips;
if (sslips && !sslips[val]) {
return LOCALE.maketext("The IP address “[_1]” is not available, or you do not have permission to use it.", val);
}
}
return "";
};
/**
* Validate the certificate.
*
* @method validateCert
* @return {Boolean}
*/
var validateCert = function(el) {
var val = el.value.trim();
if (!val) {
return true; // Another validator handles empty-string.
}
try {
return !!CPANEL.ssl.parseCertificateText(val);
} catch (e) {
return false;
}
return true;
};
/**
* Validate the key.
*
* @method validateKey
* @return {Boolean}
*/
var validateKey = function(el) {
var val = el.value.trim();
if (!val) {
return true; // Another validator handles empty-string.
}
var parse;
try {
parse = CPANEL.ssl.parseKeyText(val);
} catch (e) {}
return !!parse;
};
/**
* Validate the CA bundle.
*
* @method validateCABundle
* @return {Boolean}
*/
var validateCABundle = function(el) {
var val = el.value.trim();
if (!val) {
return true;
}
try {
return !!CPANEL.ssl.parseCABundleText(val);
} catch (e) {
return false;
}
return true;
};
/**
* Verify that one of these is true:
* a) The certificate is invalid.
* b) The domain is invalid.
* c) The selected domain and certificate match.
*
* @method validateDomainCertificateMatch
* @param {string} el The DOM element whose value is the certificate text.
* @return {Boolean}
*/
var validateDomainCertificateMatch = function(el) {
var cert = el.value.trim();
if (!cert) {
return true;
}
var formData = CPANEL.dom.get_data_from_form("mainform");
var domain = formData.domain;
if ( !validateDomain(domain) ) {
return true;
}
var all_fqdns = [domain];
if (pageHasDomainSelector) {
var alias_subdomains = PAGE.data.domain_aliases[domain] || [];
all_fqdns = all_fqdns.concat( alias_subdomains.map(
function(as) {
return as + "." + domain;
}
) );
}
return all_fqdns.some( function(fqdn) {
try {
return CPANEL.ssl.validateCertificateForDomain( cert, fqdn );
} catch (e) {
return true;
}
} );
};
/**
* Verify that one of these is true:
* a) The key is invalid.
* b) The certificate is invalid.
* c) The key and certificate match.
* Return true for invalids to prevent validate.js from spooging the user
* with extra warnings.
*
* @method validateKeyCert
* @return {Boolean}
*/
var validateKeyCert = function(el) {
var val = el.value.trim();
if (!val) {
return true;
}
try {
var key_parse = CPANEL.ssl.parseKeyText(val);
var cert_text = el.form.crt.value.trim();
var cert_parse = CPANEL.ssl.parseCertificateText(cert_text);
return cert_parse.modulus === key_parse.modulus;
} catch (e) {
return true;
}
};
/**
* Verify that one of these is true:
* a) The CA bundle is invalid.
* b) The certificate is invalid.
* c) The CA bundle and the certificate match.
* Return true for invalids to prevent validate.js from spooging the user
* with extra warnings.
*
* @method validateCABundleCert
* @return {Boolean}
*/
var validateCABundleCert = function(el) {
var cab = el.value.trim();
if ( !cab ) {
return true;
}
var cert = el.form.crt.value.trim();
try {
var cert_parse = CPANEL.ssl.parseCertificateText(cert);
var cab_parse = CPANEL.ssl.parseCABundleText(cab);
var cabLeafSubject = JSON.stringify(cab_parse.shift().subjectList);
var certIssuer = JSON.stringify(cert_parse.issuerList);
return cabLeafSubject === certIssuer;
} catch (e) {
return true;
}
};
/**
* HTML-escape a string and wrap it in <code>.
*
* @method wrapCode
* @param {string} str The string to process.
* @return {string} The processed string.
*/
var wrapCode = function(str) {
return "<code>" + str.html_encode() + "</code>";
};
/**
* Same as updateUI, but builds in a very small delay for paste events.
*
* @param object A set of named parameters; can be:
* active_element: The element that "caused" the UI update.
* @method delayed_updateUI
*/
var delayed_updateUI = function(opts) {
window.setTimeout( function() {
updateUI(opts);
}, 1 );
};
/**
* Show parsed data and whatever warnings need be,
* and update which of the "Fetch" buttons shows.
* NOTE: This does NOT do validation; validate.js handles that separately.
*
* @param object A set of named parameters; can be:
* active_element: The element that "caused" the UI update. (Not for "services" ui_mode!)
* @method updateUI
*/
var updateUI = function(opts) {
var certParse;
var pageForm = DOM.get("mainform");
var formData = CPANEL.dom.get_data_from_form(pageForm);
var formDomain = formData.domain;
var formCrt = formData.crt;
try {
certParse = CPANEL.ssl.parseCertificateText(formCrt);
} catch (e) {}
var shown = CPANEL.widgets.ssl.showCertificateParse(
formCrt.trim(),
"cert_parse",
{}
);
DOM.setStyle( "cert_parse", "display", shown ? "" : "none" );
if ( DOM.get("fetch-cert") ) {
DOM.get("fetch-cert").disabled = !certParse;
}
if ( DOM.get("fetch-domain") ) {
DOM.get("fetch-domain").disabled = !validateDomain( formDomain );
}
if (opts && opts.active_element) {
var domainFieldHasText = (formDomain && formDomain.trim()) ? 1 : 0;
var crtFieldHasText = (formCrt && formCrt.trim()) ? 1 : 0;
if ( (opts.active_element === "domain" && domainFieldHasText) ||
( !crtFieldHasText && domainFieldHasText )
) {
if ( DOM.getStyle("fetch-cert", "display") !== "none" ) {
CPANEL.animate.fade_out("fetch-cert");
}
if ( DOM.getStyle("fetch-domain", "display") === "none" ) {
CPANEL.animate.fade_in("fetch-domain");
}
} else if (certParse) {
if ( (PAGE.ui_mode !== "services") && (DOM.getStyle("fetch-domain", "display") !== "none") ) {
CPANEL.animate.fade_out("fetch-domain");
}
if ( DOM.getStyle("fetch-cert", "display") === "none" ) {
CPANEL.animate.fade_in("fetch-cert");
}
}
}
CPANEL.align_panels_event.fire();
};
/**
* Handle a failed fetch_for_install_form() callback.
*
* @method sslInfoFailure
* @param {Object} o The CPANEL.api callback parameter.
*/
function sslInfoFailure(o) {
showErrorNotice( LOCALE.maketext("The lookup failed because of an error: [_1]", o.cpanel_error.html_encode()) );
}
/**
* Paste in data from a fetch_for_install_form() callback.
*
* @method sslInfoOk
* @param {Object} o The CPANEL.api callback parameter.
*/
function sslInfoOk(o) {
formProgressOverlay.hide();
var result = o.cpanel_data[0] || o.cpanel_data;
if (!result) {
return;
}
var need = o.argument && o.argument.need;
var exceptions = o.argument && o.argument.exceptions;
var formParse = CPANEL.dom.get_data_from_form("mainform");
var setCert = !o.argument || (o.argument.known !== "crt");
var setIp = !formParse.ip || !validateIP(formParse.ip);
// If we "need" the certificate, also paste in the key and CA bundle.
// We might as well use the "domain" from this API response since then
// we know that the IP and domain match.
// All things being equal, we could easily grab a domain from the
// certificate parse in JS, but that "tricks" the UI into
// fetching on the domain, which clobbers the cert.
["key", "cab", "crt", "domain", "ip"].forEach( function(i) {
var curResult = result[i];
var elId = "ssl" + i;
if ( !exceptions || exceptions.indexOf(elId) === -1 ) {
if (!curResult || SORRY_COMMA_REGEXP.test(curResult) || !DOM.get(elId) || (need && need !== i)) {
return;
} else if ( setIp && (i === "ip") && !result.user && PAGE.properties.sslips[curResult] ) { // always set ip if we have it
CPANEL.dom.set_form_el_value(elId, curResult);
} else if ( i === "domain" ) {
setDomain(curResult);
} else if ( ( ( i === "crt" ) && setCert ) || i === "key" || i === "cab" ) {
CPANEL.dom.set_form_el_value(elId, curResult);
}
}
} );
if ( !o.cpanel_data.crt && o.cpanel_messages[0] ) {
var notice = new CPANEL.widgets.Dynamic_Page_Notice( {
container: "autofill_message_container",
visible: false, // capture the slide animation
level: o.cpanel_messages[0].level,
content: o.cpanel_messages[0].content.html_encode(),
} );
var slide = notice.animated_show();
slide.onTween.subscribe( CPANEL.align_panels );
notice.hideEvent.subscribe( CPANEL.align_panels );
}
updateUI();
runValidation();
}
/**
* Sets the domain selector, allowing for the www subdomain.
* If there is nothing that matches, set the empty string.
*
* @param {String} domain The domain to attempt setting.
* @method setDomain
* @return {string} Which domain we set.
*/
function setDomain(domain) {
var domainsToSet = [
domain,
domain.replace(STAR_DOT_REGEXP, ""),
"",
];
var curDomain;
while ( domainsToSet[0] ) {
curDomain = domainsToSet.shift();
if ( CPANEL.dom.set_form_el_value( "ssldomain", curDomain ) ) {
return curDomain;
}
}
}
/**
* Clear CJT validation.
* If the form is emptied like we do when we click Update SSL
* We need to clear all the validation messages
*
* @method clearValidation
*/
function clearValidation() {
["ip", "domain", "cert", "key", "cab"].forEach( function(validation_type) {
if ( validators[validation_type] ) {
validators[validation_type].clear_messages();
}
} );
}
/**
* Run CJT validation.
* NOTE: This does not call the function under CPANEL.validate.form_checkers
* because that function is designed for when you submit the form, and it'll
* make validation errors appear if (for example) the entire form is empty.
*
* @method runValidation
*/
function runValidation() {
// === Do NOT do it this way .. see above.
// CPANEL.validate.form_checkers.btnInstall( null, 1 );
var form_values = CPANEL.dom.get_data_from_form("mainform");
var validation_types = ["cert", "key"];
if (PAGE.ui_mode !== "services") {
validation_types.push("domain");
}
validation_types.forEach( function(validation_type) {
validators[validation_type].verify();
} );
if (validators.ip && form_values.ip) {
validators.ip.verify();
}
// Validate the CAB separately because it alone can legitimately be empty.
validators.cab.verify();
}
/**
* Fire off an AJAX lookup by a given certificate ID.
*
* @method fetchByCertId
*
* @param certId {string} - The id of the certificate to fetch from the server.
* @param user {string} - The name of the user to retrieve the certificate from.
* @return {boolean}
* @static
*/
function fetchByCertId(certId, user) {
showFormProgressOverlay();
var api_call;
if (CPANEL.is_whm()) {
api_call = {
func: "fetchcrtinfo",
data: {
id: certId,
user: user,
},
};
} else {
api_call = {
version: 3,
module: "SSL",
func: "fetch_cert_info",
data: { id: certId },
};
}
// Ideally this would use build_page_callback; however, that function
// doesn't (yet) play nicely with WHM API v1 batch calls.
api_call.callback = {
success: function(o) {
formProgressOverlay.hide();
var certParse;
if ( o.cpanel_data.certificate ) {
DOM.get("sslcrt").value = o.cpanel_data.certificate;
// NOTE: For better UCC support, we should show users a drop-down
// or a combobox of domains and let them choose.
if ( PAGE.ui_mode !== "services" ) {
try {
certParse = CPANEL.ssl.parseCertificateText( o.cpanel_data.certificate );
} catch (e) {}
_populate_domain_from_parsed_cert(certParse);
}
}
if ( o.cpanel_data.key && !SORRY_COMMA_REGEXP.test(o.cpanel_data.key) ) {
DOM.get("sslkey").value = o.cpanel_data.key;
}
if ( certParse && certParse.isSelfSigned ) {
DOM.get("sslcab").value = "";
} else if ( o.cpanel_data.cabundle && !SORRY_COMMA_REGEXP.test(o.cpanel_data.cabundle) ) {
DOM.get("sslcab").value = o.cpanel_data.cabundle;
}
updateUI();
runValidation();
},
failure: function(o) {
showErrorNotice( LOCALE.maketext("The certificate information could not be retrieved because of an error: [_1]", (o.cpanel_error || o.statusText).html_encode() ) );
},
};
CPANEL.api( api_call );
}
/**
* Fetch SSL items that match given criteria.
*
* @method fetch_for_install_form
* @param {string} known What we are searching on: domain or sslcrt
* @param {Array} exceptions What *not* to touch on the form.
* @return {Boolean}
*/
function fetch_for_install_form(known, exceptions) {
clearErrorNotice();
var formData = CPANEL.dom.get_data_from_form("mainform");
var domain = formData.domain;
if (domain) {
domain = domain.trim();
}
var sslcrt = formData.crt;
if (sslcrt) {
sslcrt = sslcrt.trim();
}
var callData = {};
if (known === "crt" && sslcrt && !SORRY_COMMA_REGEXP.test(sslcrt) ) {
callData.crtdata = sslcrt; // WHM
callData.certificate = sslcrt; // cpanel
} else if (known === "domain" && domain) {
callData.domain = domain;
} else if (sslcrt && !SORRY_COMMA_REGEXP.test(sslcrt)) {
callData.crtdata = sslcrt; // WHM
callData.certificate = sslcrt; // cpanel
known = "crt";
} else if (domain) {
callData.domain = domain;
known = "domain";
} else {
return;
}
var api_req = {
data: callData,
callback: {
success: sslInfoOk,
failure: sslInfoFailure,
argument: { known: known, exceptions: exceptions },
},
};
if (known === "domain") {
// We only want one result. In the future
// it might be nice to show the user all of
// the results and have them pick, but for
// now we just do the one.
api_req.api_data = {
paginate: { start: 0, size: 1 },
};
var all_domains = [ callData.domain ];
if ( callData.domain.substr(0, 1) !== "*" ) {
var aliases;
if (CPANEL.is_whm()) {
aliases = PAGE.auto_domains;
} else {
aliases = PAGE.data.domain_aliases[ callData.domain ];
}
aliases = aliases.map( function(ad) {
return ad + "." + callData.domain;
} );
all_domains.push.apply( all_domains, aliases );
}
callData.domains = all_domains.join(",");
delete callData.domain;
}
if ( CPANEL.is_whm() ) {
api_req.func = (known === "domain") ? "fetch_ssl_certificates_for_fqdns" : "fetchsslinfo";
} else {
api_req.version = 3;
api_req.module = "SSL";
api_req.func = (known === "domain") ? "fetch_certificates_for_fqdns" : "fetch_key_and_cabundle_for_certificate";
}
CPANEL.api( api_req );
if ( formProgressOverlay && formProgressOverlay.cfg ) {
formProgressOverlay.destroy();
}
showFormProgressOverlay();
}
/**
* Initialize the IP drop-down in WHM.
*
* @method _initializeIpSelector
* @private
*/
var _initializeIpSelector = function() {
var ips = PAGE.properties.sslips;
var ip_options = PAGE.properties.ip_options;
var cfg = YAHOO.lang.augmentObject( { maxResultsDisplayed: ip_options.length }, IP_COMBOBOX_CONFIG );
var ipcombo = new CPANEL.widgets.Combobox( DOM.get("sslip"), null, ip_options, cfg );
ipcombo.itemSelectEvent.subscribe(function(type, ACo) {
var selected_ip = ACo[2][0];
if (!DOM.get("ssldomain").value && ips[selected_ip] && ips[selected_ip].sslhost) {
if (ips[selected_ip].hasssl) {
DOM.get("ssldomain").value = ips[selected_ip].sslhost;
} else if (ips[selected_ip].iptype === "dedicated") {
/* It might would be nice if we presented them with
a popup of all the domains that the user
owns and let them select which domain
they would like to use */
}
ssldomain_change_delayed();
}
runValidation();
});
ipcombo.formatResult = function(oResultItem, sQuery) {
var ip = oResultItem[0];
var ip_info;
if (ips[ip].primary_ssl_servername) {
if (ips[ip].primary_ssl_aliases && ips[ip].primary_ssl_aliases.length) {
ip_info = LOCALE.maketext("SSL is installed; “[_1]” ([numerate,_2,alias,aliases] [list_and,_3]) is primary.", ips[ip].primary_ssl_servername, ips[ip].primary_ssl_aliases.length, ips[ip].primary_ssl_aliases.sort());
} else {
ip_info = LOCALE.maketext("SSL is installed; “[_1]” is primary.", ips[ip].primary_ssl_servername);
}
}
// TODO: Benchmark this with a large number of IPs.
// It may need to be assembled without Handlebars for
// the sake of users in slower environments.
var sMarkup = ipSelectorItemTemplate( {
ip: ip,
hasssl: !!ips[ip].primary_ssl_servername,
isShared: !!parseInt(ips[ip].is_shared_ip, 10),
ipInfo_html: ip_info,
} );
return sMarkup;
};
};
/**
* Validate the cert and update the UI.
*
* @param {object} opts An object to pass to updateUI().
*/
var parse_and_do_cert_validate = function(opts) {
updateUI( opts );
validators.cert.verify();
};
/**
* Validate the cert and update the UI, but do this after a slight delay.
* This is good for events like "onpaste" that don't immediately change
* the input value (because they can still be canceled within the handler).
*
* @param {object} opts An object to pass to delayed_updateUI().
*/
var delayed_parse_and_do_cert_validate = function(opts) {
delayed_updateUI( opts );
window.setTimeout( validators.cert.verify.bind(validators.cert), 1 );
};
/**
* Thin wrapper around delayed_parse_and_do_cert_validate()
* that designates the active element as the domain input.
*/
var ssldomain_change_delayed = function() {
delayed_parse_and_do_cert_validate( { active_element: "domain" } );
};
/**
* Thin wrapper around parse_and_do_cert_validate()
* that designates the active element as the domain input.
*/
var ssldomain_change = function() {
parse_and_do_cert_validate( { active_element: "domain" } );
};
/**
* Thin wrapper around the IP validation to delay it slightly.
*/
var delayed_ip_domain_validate = function() {
window.setTimeout( function() {
validators.ip.verify();
}, 1 );
};
/**
* Thin wrapper around the key and CAB validation to delay it slightly.
*/
var delayed_key_cab_validate = function() {
window.setTimeout( function() {
validators.key.verify();
validators.cab.verify();
}, 1 );
};
/**
* Thin wrapper around updateUI() to set the certificate input
* as the active/changed element.
*/
var sslcrt_change = function() {
updateUI( {
active_element: "crt",
});
};
/**
* Thin wrapper around delayed_updateUI() to set the certificate input
* as the active/changed element.
*/
var sslcrt_change_delayed = function() {
delayed_updateUI( { active_element: "crt" } );
};
/**
* Hide the warning, and don't spew if the warning isn't there.
*/
var _hide_wildcard_subdomain_warning = function() {
if ( wildcard_subdomain_warning ) {
wildcard_subdomain_warning.hide();
}
};
/**
* Maybe show the warning, maybe not, y'know?
*/
var _process_wildcard_subdomain_warning = function() {
var formDomain = this.value.trim();
if (formDomain && /^\*\./.test(formDomain)) {
var base_domain = formDomain.substr(2) || "domain.com";
var wildcard_domain = formDomain.substr(0, 2) + base_domain;
var content = LOCALE.maketext("We recommend that users manage individual subdomains (e.g., “[_1]”, “[_2]”) instead of a single wildcard subdomain (e.g., “[_3]”).", "sample1." + base_domain, "sample2." + base_domain, wildcard_domain);
if (!wildcard_subdomain_warning) {
wildcard_subdomain_warning = new CPANEL.widgets.Page_Notice( {
container: "wildcard_subdomain_warning",
level: "warn",
visible: false, /* To avoid animating. */
content: content,
} );
} else {
wildcard_subdomain_warning.cfg.setProperty("content", content);
}
wildcard_subdomain_warning.show();
} else {
_hide_wildcard_subdomain_warning();
}
};
/**
* Initialize the page's "validators" hash
*
* @method _set_up_validators
* @private
*/
var _set_up_validators = function() {
if ( PAGE.ui_mode === "services" ) {
validators.services = new CPANEL.validate.validator( LOCALE.maketext("Service") );
validators.services.add_for_submit( "service_to_install", _validate_services_selected, LOCALE.maketext("Choose a service.") );
validators.services.attach();
EVENT.on( DOM.get("mainform").service, "click", validators.services.verify.bind(validators.services) );
} else {
validators.domain = new CPANEL.validate.validator( LOCALE.maketext("Domain") );
validators.domain.add_for_submit( "ssldomain", "min_length($input$,1)", LOCALE.maketext("Choose a domain.") );
validators.domain.add( "ssldomain", validateDomain, LOCALE.maketext("This is not a valid domain.") );
validators.domain.validateSuccess.subscribe(_process_wildcard_subdomain_warning, DOM.get("ssldomain"), true);
validators.domain.validateFailure.subscribe(_hide_wildcard_subdomain_warning);
validators.domain.attach();
if ( DOM.get("sslip") ) {
validators.ip = new CPANEL.validate.validator( LOCALE.maketext("IP") );
validators.ip.add_for_submit( "sslip", validateIP, validateIP_fail_reason );
validators.ip.attach();
}
}
validators.cert = new CPANEL.validate.validator( LOCALE.maketext("Certificate") );
validators.cert.add_for_submit( "sslcrt", "min_length($input$.trim(),1)", LOCALE.maketext("Provide or retrieve a certificate.") );
validators.cert.add( "sslcrt", validateCert, LOCALE.maketext("The certificate is not valid.") );
if ( PAGE.ui_mode !== "services" ) {
validators.cert.add( "sslcrt", validateDomainCertificateMatch, LOCALE.maketext("The certificate does not match your selected domain.") );
}
validators.cert.attach();
validators.key = new CPANEL.validate.validator( LOCALE.maketext("Key") );
validators.key.add_for_submit( "sslkey", "min_length($input$.trim(),1)", LOCALE.maketext("Provide or retrieve a key.") );
validators.key.add( "sslkey", validateKey, LOCALE.maketext("The key is invalid.") );
validators.key.add( "sslkey", validateKeyCert, LOCALE.maketext("The key does not match the certificate.") );
validators.key.attach();
validators.cab = new CPANEL.validate.validator( LOCALE.maketext("Certificate Authority Bundle") );
validators.cab.add( "sslcab", validateCABundle, LOCALE.maketext("The CA bundle is invalid.") );
validators.cab.add( "sslcab", validateCABundleCert, LOCALE.maketext("The CA bundle does not match the certificate.") );
validators.cab.attach();
var submit_button = CPANEL.Y.one("#mainform input[type=submit], #mainform button[type=submit]");
CPANEL.validate.attach_to_form( submit_button.id, validators, {
no_panel: true,
success_callback: _send_install,
} );
};
/**
* Kick-start the install AJAX.
*
* @method _send_install
* @private
*/
var _send_install = function() {
var install_function = ( PAGE.ui_mode === "services" ) ? PAGE.sendInstall : sendApacheInstall;
return install_function("mainform");
};
/**
* Initialize the page's event listeners, including extra validation triggers.
* This *MUST NOT* be called before _set_up_validators!!
*
* @method _set_up_listeners
* @private
*/
var _set_up_listeners = function() {
EVENT.on("fetch-cert", "click", function(e) {
fetch_for_install_form("crt");
});
if ( DOM.get("fetch-domain") ) {
EVENT.on("fetch-domain", "click", function(e) {
fetch_for_install_form("domain");
});
}
if ( PAGE.ui_mode !== "services" ) {
if ( pageHasDomainSelector ) {
EVENT.on("ssldomain", "change", ssldomain_change_delayed );
} else if (CPANEL.dom.has_oninput) {
EVENT.on( "ssldomain", "input", ssldomain_change );
} else {
EVENT.on( "ssldomain", "paste", ssldomain_change_delayed );
EVENT.on( "ssldomain", "keyup", ssldomain_change_delayed );
EVENT.on( "ssldomain", "change", ssldomain_change_delayed );
}
}
EVENT.on( "mainform", "reset", function() {
delayed_updateUI();
window.setTimeout( runValidation, 1 );
} );
// Make key and CAB validation fire also if the cert changes.
if (CPANEL.dom.has_oninput) {
EVENT.on( "sslcrt", "input", validators.key.verify.bind(validators.key) );
EVENT.on( "sslcrt", "input", validators.cab.verify.bind(validators.cab) );
} else {
EVENT.on( "sslcrt", "paste", delayed_key_cab_validate );
EVENT.on( "sslcrt", "keyup", delayed_key_cab_validate );
EVENT.on( "sslcrt", "change", delayed_key_cab_validate );
}
if ( CPANEL.dom.has_oninput ) {
EVENT.on( "sslcrt", "input", sslcrt_change );
} else {
EVENT.on( "sslcrt", "paste", sslcrt_change_delayed );
EVENT.on( "sslcrt", "keyup", sslcrt_change );
EVENT.on( "sslcrt", "change", sslcrt_change );
}
if ( DOM.get("sslip") ) {
if (CPANEL.dom.has_oninput) {
EVENT.on( "ssldomain", "input", validators.ip.verify.bind(validators.ip) );
} else {
EVENT.on( "ssldomain", "paste", delayed_ip_domain_validate );
EVENT.on( "ssldomain", "keyup", delayed_ip_domain_validate );
EVENT.on( "ssldomain", "change", delayed_ip_domain_validate );
}
}
};
var make_ssl_browser_panel = function() {
var new_panel = new YAHOO.widget.Dialog(DOM.generateId(), {
fixedcenter: true,
close: true,
draggable: true,
modal: true,
postMethod: "none",
buttons: [
{ text: LOCALE.maketext("Use Certificate"), handler: function() {
this.submit();
}, isDefault: true },
{ text: LOCALE.maketext("Cancel"), classes: ["cancel"], handler: function() {
this.cancel();
} },
],
visible: false,
});
new_panel.setBody("");
DOM.addClass( new_panel.element, "ssl-results-panel" );
// Render the control into the element
new_panel.render(document.body);
return new_panel;
};
/**
* Initialize the page's event listeners, including extra validation triggers.
*
* @method _set_up_listeners
* @private
*/
var _set_up_ssl_browser = function() {
sslResultsPanel = make_ssl_browser_panel();
sslResultsPanel.beforeSubmitEvent.subscribe( handleBeforeSubmit, sslResultsPanel );
// Setup the current user on loading the page.
if (PAGE && PAGE.properties) {
current_user = PAGE.properties.selectedUser;
}
// Bug in YUI 2 Dialog: setBody has to happen before render.
sslResultsPanel.setHeader(CPANEL.widgets.Dialog.applyDialogHeader(LOCALE.maketext("SSL Certificate List")));
var formTemplate = Handlebars.compile( DOM.get("browsessl_default_form").text );
var introBlurb;
if ( CPANEL.is_cpanel() ) {
introBlurb = LOCALE.maketext("Choose a certificate to install.");
var limitationBlurb = LOCALE.maketext("Certificates that do not have a domain associated with your account are not listed here.") + " ";
introBlurb += " " + limitationBlurb + " " + LOCALE.maketext("You can manage all of your saved certificates on the [output,url,_1,“Certificates” page].", "crts.html");
} else {
introBlurb = LOCALE.maketext("Choose the account or Apache domain that contains the desired certificate to install. Then, select the certificate.");
}
sslResultsPanel.form.innerHTML = formTemplate( {
introBlurb_html: introBlurb,
} );
// NOTE: We do NOT call normalize_select_arrows() on the user drop-down
// because of the disable/enable triggers on that element.
// Users likely expect their platform's behavior here.
// Fetch the template for the dialog if it exists
var template = DOM.get("hiddenWhiteSpaceListTemplate");
if (template) {
hiddenWhiteSpaceListTemplate = Handlebars.compile(template.text);
}
// This could also have gone into _set_up_listeners().
EVENT.on("sslbrowse", "click", function(e) {
browsessl(this);
});
EVENT.on( sslResultsPanel.form.users, "change", _onUserChange);
// A radio button collection
EVENT.on( sslResultsPanel.form.browse_source, "click", _onBrowseSourceChange );
};
function _populate_FQDN_TO_CREATED_DOMAIN() {
for (var created in PAGE.data.domain_aliases) {
FQDN_TO_CREATED_DOMAIN[created] = created;
var aliases = PAGE.data.domain_aliases[created];
for (var a = 0; a < aliases.length; a++) {
FQDN_TO_CREATED_DOMAIN[ aliases[a] + "." + created ] = created;
}
}
}
var _populate_domain_from_parsed_cert = function(certParse) {
if (certParse) {
var cert_domains = certParse.domains;
// In WHM, the SSL domain is just a text input field, not a drop-down selector.
if ( CPANEL.is_whm() ) {
// Re-populate the field with the common name when a cert is selected.
DOM.get("ssldomain").value = cert_domains[0];
} else {
var form_data = CPANEL.dom.get_data_from_form("mainform");
var selected_domain = form_data.domain.trim();
var sel_dom_matches_cert = CPANEL.ssl.doesDomainMatchOneOf(selected_domain, cert_domains);
if (!sel_dom_matches_cert) {
var set_ok;
// First, let’s see if we have any exact
// matches for the subject.commonName.
// This is good for wildcards but also
// just in general.
var created_domain = FQDN_TO_CREATED_DOMAIN[ cert_domains[0] ];
if (created_domain) {
set_ok = CPANEL.dom.set_form_el_value( "ssldomain", created_domain );
}
if (!set_ok) {
MATCH_SEEK:
for (var fqdn in FQDN_TO_CREATED_DOMAIN) {
if (CPANEL.ssl.doesDomainMatchOneOf(fqdn, cert_domains)) {
created_domain = FQDN_TO_CREATED_DOMAIN[fqdn];
if (CPANEL.dom.set_form_el_value( "ssldomain", created_domain )) {
break;
}
}
}
}
// "" //Reset domain <select> if this is a wildcard.
}
}
}
};
/**
* Initialize the page
*
* @method initialize
* @private
*/
var initialize = function() {
if ( PAGE.ui_mode !== "services" ) {
( new CPANEL.widgets.Page_Notice( {
visible: false,
level: "info", // can also be "warn", "error", "success"
content: DOM.get("ssl-install-require-template").text,
} ) ).show();
(new CPANEL.widgets.Page_Notice( null, {
visible: false,
level: "info",
content: LOCALE.maketext("To give website clients the best experience, ensure that each [asis,SSL] website’s certificate matches every domain on the website.") + "<br><br>" + LOCALE.maketext("When you install a valid certificate onto a website, the system also configures email, calendar, web disk, and [asis,cPanel]-related services to use that certificate for all of the website’s domains that match the certificate. Requests to these services from [asis,SNI]-enabled clients via the matching domains will receive the installed certificate.") + "<br><br>" + LOCALE.maketext("For more information, read our [output,url,_1,SSL Installation Workflow] documentation.", "https://go.cpanel.net/whmdocs66sslinstallworkflow"),
} ) ).show();
}
pageHasDomainSelector = !!CPANEL.Y.one("select#ssldomain");
if (pageHasDomainSelector) {
CPANEL.dom.normalize_select_arrows("ssldomain");
}
_set_up_validators();
_set_up_listeners();
_set_up_ssl_browser();
if ( DOM.get("sslip") ) {
ipSelectorItemTemplate = Handlebars.compile( DOM.get("ipSelectorItemTemplate").text.trim() );
_initializeIpSelector();
}
updateUI();
if ( DOM.get("sslcrt").value.trim() ) {
if ( PAGE.ui_mode !== "services" ) {
DOM.setStyle("fetch-cert", "display", "");
try {
var certParse = CPANEL.ssl.parseCertificateText( DOM.get("sslcrt").value.trim() );
var certDomains = certParse.domains.slice(0);
while ( certDomains[0] ) {
if ( setDomain( certDomains.shift() ) ) {
break;
}
}
} catch (e) {}
}
runValidation();
}
CPANEL.namespace( "CPANEL.sharedjs.sslinstall" );
YAHOO.lang.augmentObject( CPANEL.sharedjs.sslinstall, {
domain_change_delayed: ssldomain_change_delayed, /* export this to the window for whm ip selector */
updateUI: updateUI,
runValidation: runValidation,
clearValidation: clearValidation,
showFormProgressOverlay: showFormProgressOverlay,
hideFormProgressOverlay: hideFormProgressOverlay,
populate_form_with_ssl_components: populate_form_with_ssl_components,
make_ssl_browser_panel: make_ssl_browser_panel,
} );
if (pageHasDomainSelector) {
_populate_FQDN_TO_CREATED_DOMAIN();
}
};
CPANEL.nvdata.register( "certificates_table_sort", function() {
return certificates_table_sort;
} );
if ( CPANEL.is_whm() ) {
CPANEL.nvdata.register( "browse_source", function() {
return current_browse_source;
} );
}
window.fetch_for_install_form = fetch_for_install_form;
// Register startup events.
YAHOO.util.Event.onDOMReady(initialize);
})(window);