mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
271 lines
14 KiB
PHP
271 lines
14 KiB
PHP
<?php
|
|
/*
|
|
* Certificate form fields - shared by certificate_add.php and
|
|
* certificate_edit.php, which set $certificate_form_mode ('add' | 'edit'),
|
|
* $client_id (0 = pick the client here), $certificate_id and the
|
|
* $certificate_* values (escaped; empty for a new one) before including
|
|
* this. Posts what certificateFormSet() (functions/certificate.php) reads.
|
|
*
|
|
* Tabs: Certificate (the host it is served on, with Check to see what it
|
|
* serves now; the name, the star, what it is for and the client's domain it
|
|
* belongs to), Details (paste the certificate, or type the issuer and expiry
|
|
* when there is nothing to paste) and Notes. What a check finds - the
|
|
* issuer, dates, names and fingerprint - is saved, never typed.
|
|
*
|
|
* Should not be accessed directly.
|
|
*/
|
|
|
|
isset($mysqli) || die("Direct file access is not allowed");
|
|
|
|
$client_options = $domain_options = [];
|
|
if ($client_id) {
|
|
$sql = mysqli_query($mysqli, "SELECT domain_id, domain_name FROM domains
|
|
WHERE domain_client_id = $client_id AND (domain_archived_at IS NULL OR domain_id = " . intval($certificate_domain_id) . ")
|
|
ORDER BY domain_name ASC");
|
|
while ($row = mysqli_fetch_assoc($sql)) {
|
|
$domain_options[] = $row;
|
|
}
|
|
} else {
|
|
$sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('clients.client_id') . " ORDER BY client_name ASC");
|
|
while ($row = mysqli_fetch_assoc($sql)) {
|
|
$client_options[] = $row;
|
|
}
|
|
}
|
|
|
|
$form_id = 'certificateForm' . ($certificate_form_mode === 'edit' ? $certificate_id : 'New');
|
|
$has_pem = trim(html_entity_decode($certificate_public_key, ENT_QUOTES, 'UTF-8')) !== '';
|
|
|
|
?>
|
|
|
|
<ul class="nav nav-pills nav-fill flex-nowrap overflow-auto mb-3 itflow-asset-form-tabs" role="tablist">
|
|
<li class="nav-item">
|
|
<a class="nav-link active" data-bs-toggle="pill" href="#<?= $form_id ?>Certificate"><i class="fas fa-fw fa-lock me-2 d-none d-lg-inline"></i>Certificate</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="pill" href="#<?= $form_id ?>Details"><i class="fas fa-fw fa-file-code me-2 d-none d-lg-inline"></i>Paste or by hand</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="pill" href="#<?= $form_id ?>Notes"><i class="fas fa-fw fa-sticky-note me-2 d-none d-lg-inline"></i>Notes</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content" id="<?= $form_id ?>">
|
|
|
|
<!-- Where it is served, and what it is -->
|
|
<div class="tab-pane fade show active" id="<?= $form_id ?>Certificate">
|
|
<div class="row g-3">
|
|
<?php if (!$client_id) { ?>
|
|
<div class="col-12">
|
|
<label class="form-label">Client <strong class="text-danger">*</strong></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
|
|
<select class="form-select select2" name="client_id" required data-certificate-client>
|
|
<option value="">- Select Client -</option>
|
|
<?php foreach ($client_options as $client_option) { ?>
|
|
<option value="<?= intval($client_option['client_id']) ?>"<?= selectOptionClient($client_option['client_id'], $client_option['client_name'], $client_option['client_abbreviation'] ?? '') ?>><?= escapeHtml($client_option['client_name']) ?></option>
|
|
<?php } ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<?php } ?>
|
|
<div class="col-12">
|
|
<label class="form-label">Host</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fas fa-fw fa-server"></i></span>
|
|
<input type="text" class="form-control" name="domain" placeholder="www.example.com, portal.example.com:8443, fw01.example.local" maxlength="200" value="<?= $certificate_domain ?>"
|
|
autocomplete="off" spellcheck="false" data-certificate-host <?= $certificate_form_mode === 'edit' ? '' : 'autofocus' ?>>
|
|
<button type="button" class="btn btn-outline-secondary" data-certificate-check-button><i class="fas fa-fw fa-search me-1"></i>Check</button>
|
|
</div>
|
|
<div class="form-text" data-certificate-check>Where it is served - a web address is fine, and a port when it isn't 443. Check shows what it serves now; saving a new one checks it.</div>
|
|
<div class="itflow-certificate-check-result mt-2 d-none" data-certificate-result></div>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<label class="form-label">Name</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
|
|
<input type="text" class="form-control" name="name" placeholder="<?= $certificate_domain !== '' ? $certificate_domain : 'The host, if left empty' ?>" maxlength="200" value="<?= $certificate_name ?>" data-certificate-name>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 d-flex align-items-end">
|
|
<div class="form-check form-switch mb-2">
|
|
<input type="checkbox" class="form-check-input" name="favorite" value="1" id="<?= $form_id ?>Favorite" <?= $certificate_favorite ? 'checked' : '' ?>>
|
|
<label class="form-check-label" for="<?= $form_id ?>Favorite"><i class="fas fa-star text-warning me-1"></i>Starred</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">Description</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
|
|
<input type="text" class="form-control" name="description" placeholder="Client portal, VPN, firewall admin, Exchange" maxlength="500" value="<?= $certificate_description ?>">
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">Domain</label>
|
|
<?php if ($client_id) { ?>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-globe"></i></span>
|
|
<select class="form-select select2" name="domain_id" data-certificate-domain>
|
|
<option value="0">- Matched from the host -</option>
|
|
<?php foreach ($domain_options as $domain_option) {
|
|
$domain_option_id = intval($domain_option['domain_id']); ?>
|
|
<option value="<?= $domain_option_id ?>" <?= intval($certificate_domain_id) === $domain_option_id ? 'selected' : '' ?> data-icon="fas fa-globe"><?= escapeHtml($domain_option['domain_name']) ?></option>
|
|
<?php } ?>
|
|
</select>
|
|
</div>
|
|
<div class="form-text">The client's domain it is for. Left blank, it is the domain the host is under.</div>
|
|
<?php } else { ?>
|
|
<div class="form-text mt-0">Matched to the client's domain the host is under when it's saved.</div>
|
|
<?php } ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- The certificate itself, or the issuer and expiry by hand -->
|
|
<div class="tab-pane fade" id="<?= $form_id ?>Details">
|
|
<label class="form-label">Certificate (PEM)</label>
|
|
<textarea class="form-control font-monospace small" rows="9" name="public_key" spellcheck="false" placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----" data-certificate-pem><?= $certificate_public_key ?></textarea>
|
|
<div class="form-text mb-3">For a host ITFlow can't reach. The issuer, dates, names it covers and fingerprint are read from it - a whole chain is fine, only the first certificate is kept. Never paste the private key.</div>
|
|
|
|
<fieldset data-certificate-by-hand>
|
|
<div class="small text-body-secondary fw-semibold text-uppercase mb-2">Nothing to paste? By hand</div>
|
|
<div class="row g-3">
|
|
<div class="col-md-7">
|
|
<label class="form-label">Issuer</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
|
|
<input type="text" class="form-control" name="issued_by" maxlength="200" placeholder="Let's Encrypt, DigiCert, Sectigo" value="<?= $certificate_issued_by ?>">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label class="form-label">Expires</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fa fa-fw fa-calendar-times"></i></span>
|
|
<input type="date" class="form-control" name="expire" max="2999-12-31" value="<?= $certificate_expire ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-text" data-certificate-by-hand-note<?= $has_pem ? '' : ' hidden' ?>>Read from the certificate above - clear it to type these.</div>
|
|
</fieldset>
|
|
<?php if ($certificate_form_mode === 'edit') { ?>
|
|
<p class="text-body-secondary small text-end mt-3 mb-0">Certificate ID <span class="font-monospace"><?= intval($certificate_id) ?></span></p>
|
|
<?php } ?>
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
<div class="tab-pane fade" id="<?= $form_id ?>Notes">
|
|
<label class="form-label">Notes</label>
|
|
<textarea class="form-control" rows="10" name="notes" placeholder="Where it is installed, who renews it and how, the CSR and where the private key lives, anything to know before replacing it"><?= $certificate_notes ?></textarea>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
itflowReady(function () {
|
|
var form = document.getElementById(<?= json_encode($form_id) ?>).closest('form');
|
|
if (form.dataset.certificateFormWired) {
|
|
return;
|
|
}
|
|
form.dataset.certificateFormWired = '1';
|
|
|
|
// A required field on a tab that isn't showing: show that tab and point at it
|
|
var reported = false;
|
|
form.addEventListener('invalid', function (e) {
|
|
var pane = e.target.closest('.tab-pane');
|
|
var tab = pane && form.querySelector('[data-bs-toggle="pill"][href="#' + pane.id + '"]');
|
|
if (!tab || pane.classList.contains('active') || reported) {
|
|
return;
|
|
}
|
|
reported = true;
|
|
tab.addEventListener('shown.bs.tab', function () {
|
|
reported = false;
|
|
e.target.reportValidity();
|
|
}, { once: true });
|
|
bootstrap.Tab.getOrCreateInstance(tab).show();
|
|
}, true);
|
|
|
|
// A pasted certificate says the issuer and expiry - typing them is for when there is none
|
|
var pem = form.querySelector('[data-certificate-pem]');
|
|
var byHand = form.querySelector('[data-certificate-by-hand]');
|
|
var byHandNote = form.querySelector('[data-certificate-by-hand-note]');
|
|
function syncByHand() {
|
|
var pasted = pem.value.trim() !== '';
|
|
byHand.disabled = pasted;
|
|
byHandNote.hidden = !pasted;
|
|
}
|
|
pem.addEventListener('input', syncByHand);
|
|
syncByHand();
|
|
|
|
// The host: the name it is saved under, and Check - what it serves now
|
|
var host = form.querySelector('[data-certificate-host]');
|
|
var name = form.querySelector('[data-certificate-name]');
|
|
var button = form.querySelector('[data-certificate-check-button]');
|
|
var message = form.querySelector('[data-certificate-check]');
|
|
var result = form.querySelector('[data-certificate-result]');
|
|
var domainSelect = form.querySelector('[data-certificate-domain]');
|
|
var clientSelect = form.querySelector('[data-certificate-client]');
|
|
var clientInput = form.querySelector('input[name="client_id"]');
|
|
var messageDefault = message.textContent;
|
|
var buttonDefault = button.innerHTML;
|
|
var last = '';
|
|
|
|
host.addEventListener('input', function () {
|
|
name.placeholder = host.value.trim().replace(/^[a-z][a-z0-9+.\-]*:\/\//i, '').split(/[\/?#\s]/)[0].toLowerCase() || 'The host, if left empty';
|
|
});
|
|
|
|
function runCheck(force) {
|
|
var value = host.value.trim();
|
|
var client = clientSelect ? clientSelect.value : (clientInput ? clientInput.value : '');
|
|
var key = value + '|' + client;
|
|
if (!force && key === last) {
|
|
return;
|
|
}
|
|
last = key;
|
|
message.classList.remove('text-danger', 'text-warning', 'text-success');
|
|
result.classList.add('d-none');
|
|
result.innerHTML = '';
|
|
if (!value) {
|
|
message.textContent = messageDefault;
|
|
return;
|
|
}
|
|
button.disabled = true;
|
|
button.innerHTML = '<span class="spinner-border spinner-border-sm me-1" aria-hidden="true"></span>Checking';
|
|
message.textContent = 'Connecting to ' + value + '...';
|
|
itflowGet('ajax.php', { certificate_check: 'true', host: value, client_id: client, certificate_id: <?= intval($certificate_id) ?> }, function (data) {
|
|
button.disabled = false;
|
|
button.innerHTML = buttonDefault;
|
|
var response = {};
|
|
try { response = JSON.parse(data); } catch (e) {}
|
|
if (!response.message) {
|
|
message.textContent = messageDefault;
|
|
return;
|
|
}
|
|
message.classList.add({ ok: 'text-success', warn: 'text-warning', bad: 'text-danger' }[response.state] || '');
|
|
message.textContent = response.message;
|
|
if (response.html) {
|
|
result.innerHTML = response.html;
|
|
result.classList.remove('d-none');
|
|
}
|
|
// The client's domain it is under, unless one is picked already
|
|
if (domainSelect && response.domain_id && (domainSelect.value === '0' || domainSelect.value === '')) {
|
|
if (domainSelect.itflowSelect) {
|
|
domainSelect.itflowSelect.setValue(String(response.domain_id));
|
|
} else {
|
|
domainSelect.value = String(response.domain_id);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
button.addEventListener('click', function () { runCheck(true); });
|
|
// Enter in the host checks it, rather than saving half a form
|
|
host.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
runCheck(true);
|
|
}
|
|
});
|
|
if (clientSelect) {
|
|
clientSelect.addEventListener('change', function () { if (last) { runCheck(true); } });
|
|
}
|
|
});
|
|
</script>
|