Files
itflow/agent/domains.php
johnnyq 75df17698b Domains makeover: rebuilt domains list with expiry pills (red expired, amber close, blue close but auto-renewing), registrar and hosts shown once per vendor with their roles, SPF/DMARC/DKIM email pills, filters for Starred, Expiring in 45 days, Expired, No auto-renew and Email gaps with counts, registrar-or-host and tag filters, search across notes, hosts and tag names, safe column sorting, open straight to a domain with ?open=ID; new domain quick look with expiry, auto-renew, registration date and status, who registers it and hosts DNS, website and email, A/NS/MX records to copy, plain-language email checks (SPF strictness and duplicates, DMARC policy, DKIM at common selectors), certificates, services, the registrar and host logins, notes, TXT records, raw WHOIS and history; tabbed add/edit form that tidies pasted addresses to the domain, checks the name as it is typed (saved as, subdomain, already on the client), auto-renew switch, star, tags, client and our own vendors as registrar and hosts, Registrar does it all; add several domains at once from a pasted list; bulk look up now, set registrar or host, tags, star and unstar; domain tags (tag type 7) with a starter pack; refresher looks up a batch of up to 50 a run, never-checked, stale and soon-to-expire first; expiry alerts skip archived domains and clients, mention auto-renew and open the domain; vendor Domains tab opens the quick look and adds a domain with that vendor as registrar, our vendors get a Client domains tab; service quick look, overview renewals and search open the domain; client portal shows days to expiry and auto-renew; more export columns and filters; demo data with auto-renew, varied DMARC and DKIM, WHOIS lines and campaign domains near expiry. Fixes lookups, edits, the refresher and API updates wiping expiry dates and records when a lookup came back empty, edits logged without a client and dropping a past expiry date, every edit running slow lookups, expiry alerts for archived domains, deleted domains left on services and certificates, bulk archive needing delete rights and list actions limited to admins, registrar and host pickers accepting another client's vendors and dropping our own on save, services linking other clients' domains, a new client's website domain not added when its expiry could not be found, the WHOIS summary cut at 254 characters, and API names, vendors and duplicates not checked. Adds functions/domain.php helpers, domain quick look and shared form, domain_check lookup; removes domain_model.php. DB 2.8.3
2026-10-15 00:09:09 -04:00

500 lines
31 KiB
PHP

<?php
/*
* Domains - one client's, or every domain the agent can see from All Client
* Docs. Starred first. Each row has when it expires (and whether it renews
* by itself), who registers it and who hosts DNS, the website and email, and
* how its email is protected (SPF, DMARC, DKIM). The pills along the top
* count and filter the ones to act on: expiring soon, expired, email gaps.
*/
// Default Column Sortby Filter
$sort = "domain_name";
$order = "ASC";
// If client_id is in URI then show client Side Bar and client header
if (isset($_GET['client_id'])) {
require_once "includes/inc_all_client.php";
$client_query = "AND domain_client_id = $client_id";
$client_url = "client_id=$client_id&";
$archived = isset($_GET['archived']) && $_GET['archived'] == 1 ? 1 : 0;
$archive_query = $archived ? "domain_archived_at IS NOT NULL" : "domain_archived_at IS NULL";
} else {
require_once "includes/inc_client_overview_all.php";
$client_query = '';
$client_url = '';
$archived = isset($_GET['archived']) && $_GET['archived'] == 1 ? 1 : 0;
$archive_query = $archived ? "(client_archived_at IS NOT NULL OR domain_archived_at IS NOT NULL)" : "(client_archived_at IS NULL AND domain_archived_at IS NULL)";
}
// Perms
enforceUserPermission('module_support');
$can_edit = domainCan(2);
$can_delete = domainCan(3) && !empty($config_destructive_deletes_enable);
// Client Filter (all clients)
$client = '';
if (!$client_url && !empty($_GET['client'])) {
$client = intval($_GET['client']);
$client_query = "AND domain_client_id = $client";
}
// Vendor Filter - the registrar or any of the hosts
$vendor_filter = intval($_GET['vendor'] ?? 0);
$vendor_query = $vendor_filter ? "AND $vendor_filter IN (domain_registrar, domain_dnshost, domain_webhost, domain_mailhost)" : '';
// Tags Filter - a domain with any of them
if (isset($_GET['tags']) && is_array($_GET['tags']) && !empty($_GET['tags'])) {
$tag_filter = implode(",", array_map('intval', $_GET['tags']));
$tag_query = "AND domains.domain_id IN (SELECT domain_id FROM domain_tags WHERE tag_id IN ($tag_filter))";
} else {
$tag_filter = 0;
$tag_query = '';
}
// Which ones - the pills along the top. The old "Expiring in" filter
// (?expire_days=30 from the dashboard and the expiry notifications) still works.
$soon_days = DOMAIN_SOON_DAYS;
$shows = [
'' => ['label' => 'All', 'where' => ''],
'starred' => ['label' => 'Starred', 'icon' => 'fa-star', 'where' => 'domains.domain_favorite = 1'],
'soon' => ['label' => "Expiring in $soon_days days", 'icon' => 'fa-hourglass-half', 'where' => "domains.domain_expire BETWEEN CURDATE() AND CURDATE() + INTERVAL $soon_days DAY"],
'expired' => ['label' => 'Expired', 'icon' => 'fa-calendar-times', 'where' => 'domains.domain_expire < CURDATE()'],
'noauto' => ['label' => 'No auto-renew', 'icon' => 'fa-hand-paper', 'where' => 'domains.domain_auto_renew = 0'],
'mail' => ['label' => 'Email gaps', 'icon' => 'fa-envelope-open-text', 'where' => domainMailGapSql()],
];
$show = isset($shows[$_GET['show'] ?? '']) ? ($_GET['show'] ?? '') : '';
$show_query = $shows[$show]['where'] ? 'AND ' . $shows[$show]['where'] : '';
$expire_days = $_GET['expire_days'] ?? '';
$expire_query = '';
if ($expire_days === 'expired') {
$expire_query = 'AND domains.domain_expire < CURDATE()';
} elseif (intval($expire_days) > 0) {
$expire_query = 'AND domains.domain_expire BETWEEN CURDATE() AND CURDATE() + INTERVAL ' . intval($expire_days) . ' DAY';
}
// Only these sort - filter_header.php will hand through any lowercase word, and
// an unknown column takes the page down with a mysqli exception
$sortable_columns = ['domain_name' => 'domains.domain_name', 'domain_expire' => 'domains.domain_expire', 'registrar_name' => 'registrar.vendor_name',
'client_name' => 'client_name', 'domain_created_at' => 'domains.domain_created_at', 'domain_refreshed_at' => 'domains.domain_refreshed_at'];
if (!isset($sortable_columns[$sort])) {
$sort = 'domain_name';
}
// Domains without an expiry go last whichever way the expiry is sorted
$sort_sql = ($sort === 'domain_expire' ? 'domains.domain_expire IS NULL, ' : '') . $sortable_columns[$sort] . " $order";
[$domain_columns, $domain_joins] = domainSelectSql();
$search_query = "AND (domains.domain_name LIKE '%$q%' OR domains.domain_description LIKE '%$q%' OR domains.domain_notes LIKE '%$q%'
OR registrar.vendor_name LIKE '%$q%' OR dnshost.vendor_name LIKE '%$q%' OR mailhost.vendor_name LIKE '%$q%' OR webhost.vendor_name LIKE '%$q%' OR client_name LIKE '%$q%'
OR domains.domain_id IN (SELECT domain_id FROM domain_tags JOIN tags USING (tag_id) WHERE tag_name LIKE '%$q%'))";
$scope_query = clientScopeSql('domain_client_id');
$filters = "$archive_query $tag_query $vendor_query $search_query $expire_query $scope_query $client_query";
$sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS $domain_columns
FROM domains $domain_joins
WHERE $filters $show_query
ORDER BY domains.domain_favorite DESC, $sort_sql, domains.domain_name ASC LIMIT $record_from, $record_to");
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
$domains = [];
while ($row = mysqli_fetch_assoc($sql)) {
$domains[intval($row['domain_id'])] = $row;
}
$domains_tags = tagsForRecords('domain', array_keys($domains));
// How many of each, for the pills - everything in scope, before the pill itself
$count_sql = [];
foreach ($shows as $key => $s) {
$count_sql[] = $key === '' ? 'COUNT(*)' : "COALESCE(SUM({$s['where']}), 0)";
}
$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', $count_sql) . " FROM domains $domain_joins WHERE $filters"));
$counts = [];
foreach (array_keys($shows) as $i => $key) {
$counts[$key] = intval($row[$i]);
}
$filter_url = function ($changes) {
$params = array_merge($_GET, $changes);
unset($params['page']);
return '?' . http_build_query(array_filter($params, function ($v) { return $v !== '' && $v !== null; }));
};
// Vendors for the filter - the ones registering or hosting a domain in this scope
$sql_vendors_filter = mysqli_query($mysqli, "SELECT DISTINCT vendor_id, vendor_name, vendor_description, vendor_client_id FROM vendors
JOIN domains ON vendor_id IN (domain_registrar, domain_dnshost, domain_webhost, domain_mailhost)
LEFT JOIN clients ON client_id = domain_client_id
WHERE $archive_query $scope_query $client_query
ORDER BY vendor_name ASC");
// Vendors the bulk "set registrar or host" offers - the client's and ours, or only ours across clients
$bulk_vendor_options = $can_edit ? domainVendorChoices($client_url ? $client_id : 0) : [];
?>
<div class="card">
<div class="card-header bg-dark py-2">
<h3 class="card-title mt-2"><i class="fa fa-fw fa-globe me-2"></i>Domains</h3>
<div class="card-tools">
<?php if ($can_edit || $num_rows[0]) { ?>
<div class="btn-group">
<?php if ($can_edit) { ?>
<button type="button" class="btn btn-primary ajax-modal" data-modal-size="lg" data-modal-url="modals/domain/domain_add.php?<?= $client_url ?>">
<i class="fas fa-plus me-2"></i>New Domain
</button>
<?php } ?>
<?php if ($num_rows[0] > 0) { ?>
<button type="button" class="btn btn-primary dropdown-toggle <?= $can_edit ? 'dropdown-toggle-split' : '' ?>" data-bs-toggle="dropdown"><?= $can_edit ? '' : '<i class="fas fa-ellipsis-h"></i>' ?></button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item text-dark ajax-modal" href="#"
data-modal-url="<?= buildExportModalUrl('modals/domain/domain_export.php', ['client_id', 'client', 'expire_days', 'archived', 'q', 'tags', 'vendor']) ?>">
<i class="fa fa-fw fa-download me-2"></i>Export
</a>
</div>
<?php } ?>
</div>
<?php } ?>
</div>
</div>
<div class="card-header py-3">
<form autocomplete="off">
<?php if ($client_url) { ?>
<input type="hidden" name="client_id" value="<?= $client_id ?>">
<?php } ?>
<input type="hidden" name="archived" value="<?= $archived ?>">
<input type="hidden" name="show" value="<?= escapeHtml($show) ?>">
<div class="row g-2 align-items-center">
<div class="col-lg-3 col-md-6">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search domain, registrar, host, notes...">
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
</div>
</div>
<div class="col-lg-3 col-md-6">
<select onchange="this.form.submit()" class="form-select select2" name="tags[]" data-placeholder="- Tags -" multiple>
<?php
// Tags in use on domains in this scope, plus any already picked
$sql_tags_filter = mysqli_query($mysqli, "
SELECT tag_id, tag_name, tag_color, tag_icon FROM tags
WHERE tag_type = 7
AND (
EXISTS (SELECT 1 FROM domain_tags JOIN domains ON domains.domain_id = domain_tags.domain_id
WHERE domain_tags.tag_id = tags.tag_id " . clientScopeSql('domain_client_id') . " $client_query)
OR tag_id IN ($tag_filter)
)
ORDER BY tag_name ASC
");
while ($row = mysqli_fetch_assoc($sql_tags_filter)) {
$tag_id = intval($row['tag_id']); ?>
<option value="<?= $tag_id ?>" <?php if (isset($_GET['tags']) && is_array($_GET['tags']) && in_array($tag_id, $_GET['tags'])) { echo 'selected'; } ?><?= selectOptionColor($row['tag_color'] ?? '', $row['tag_icon'] ?? '') ?>><?= escapeHtml($row['tag_name']) ?></option>
<?php } ?>
</select>
</div>
<div class="<?= $client_url ? 'col-lg-4' : 'col-lg-2' ?> col-md-6">
<select class="form-select select2" name="vendor" onchange="this.form.submit()">
<option value="">- Any registrar or host -</option>
<?php while ($row = mysqli_fetch_assoc($sql_vendors_filter)) {
$vendor_id = intval($row['vendor_id']); ?>
<option <?= $vendor_filter === $vendor_id ? 'selected' : '' ?> value="<?= $vendor_id ?>"<?= selectOptionVendor($row['vendor_name'], intval($row['vendor_client_id']) === 0 ? 'Ours' : $row['vendor_description']) ?>><?= escapeHtml($row['vendor_name']) ?></option>
<?php } ?>
</select>
</div>
<?php if (!$client_url) { ?>
<div class="col-lg-2 col-md-6">
<select class="form-select select2" name="client" onchange="this.form.submit()">
<option value="" <?php if ($client == "") { echo "selected"; } ?>>- All Clients -</option>
<?php
$sql_clients_filter = mysqli_query($mysqli, "
SELECT DISTINCT client_id, client_name, client_abbreviation
FROM clients
JOIN domains ON domain_client_id = client_id
WHERE $archive_query
" . clientScopeSql('clients.client_id') . "
ORDER BY client_name ASC
");
while ($row = mysqli_fetch_assoc($sql_clients_filter)) {
$client_id_filter = intval($row['client_id']); ?>
<option <?php if ($client == $client_id_filter) { echo "selected"; } ?> value="<?= $client_id_filter ?>"<?= selectOptionClient($row['client_id'], $row['client_name'], $row['client_abbreviation'] ?? '') ?>><?= escapeHtml($row['client_name']) ?></option>
<?php } ?>
</select>
</div>
<?php } ?>
<div class="col-lg-2 col-md-6">
<div class="d-flex justify-content-end gap-2">
<a href="<?= escapeHtml($filter_url(['archived' => $archived ? '' : 1])) ?>"
class="btn <?= $archived ? 'btn-dark' : 'btn-outline-dark' ?>" title="<?= $archived ? 'Back to active domains' : 'Show archived domains' ?>">
<i class="fa fa-fw fa-archive"></i><span class="d-lg-none ms-2">Archived</span>
</a>
<?php if ($can_edit) { ?>
<div class="dropdown" id="bulkActionButton" hidden>
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount">0</span>)
</button>
<div class="dropdown-menu dropdown-menu-end">
<?php if (!$archived) { ?>
<button class="dropdown-item" type="submit" form="bulkActions" name="bulk_refresh_domains">
<i class="fas fa-fw fa-sync-alt me-2"></i>Look up now
</button>
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#domainBulkVendorModal">
<i class="fas fa-fw fa-building me-2"></i>Set registrar or host
</a>
<?php } ?>
<a class="dropdown-item" href="#" data-tag-picker data-tag-entity="domain" data-tag-bulk data-csrf="<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-tags me-2"></i>Tags
</a>
<button class="dropdown-item" type="submit" form="bulkActions" name="bulk_favorite_domains">
<i class="fas fa-fw fa-star text-warning me-2"></i>Star
</button>
<button class="dropdown-item" type="submit" form="bulkActions" name="bulk_unfavorite_domains">
<i class="far fa-fw fa-star me-2"></i>Unstar
</button>
<div class="dropdown-divider"></div>
<?php if ($archived) { ?>
<button class="dropdown-item text-info" type="submit" form="bulkActions" name="bulk_restore_domains">
<i class="fas fa-fw fa-redo me-2"></i>Restore
</button>
<?php if ($can_delete) { ?>
<button class="dropdown-item text-danger text-bold confirm-link" type="submit" form="bulkActions" name="bulk_delete_domains"
data-confirm-text="The domains go for good, with their history. Services and certificates stop pointing at them.">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</button>
<?php } ?>
<?php } else { ?>
<button class="dropdown-item text-danger confirm-link" type="submit" form="bulkActions" name="bulk_archive_domains">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</button>
<?php } ?>
</div>
</div>
<?php } ?>
</div>
</div>
</div>
</form>
<!-- Which ones - counted over what the filters above leave -->
<nav class="nav nav-pills itflow-asset-tabs mt-2 flex-nowrap overflow-auto">
<?php foreach ($shows as $key => $s) {
$alarm = $counts[$key] && in_array($key, ['soon', 'expired', 'mail'], true); ?>
<a class="nav-link text-nowrap <?= $show === $key ? 'active' : '' ?>" href="<?= escapeHtml($filter_url(['show' => $key, 'expire_days' => ''])) ?>">
<?php if (!empty($s['icon'])) { ?><i class="fas fa-fw <?= $s['icon'] ?> me-1"></i><?php } ?><?= $s['label'] ?>
<span class="badge rounded-pill ms-1 <?= $alarm ? ($key === 'expired' ? 'text-bg-danger' : 'text-bg-warning') : '' ?>"><?= $counts[$key] ?></span>
</a>
<?php } ?>
</nav>
<?php if ($expire_query) { ?>
<div class="small text-body-secondary mt-2">
<i class="fas fa-filter me-1"></i><?= $expire_days === 'expired' ? 'Expired' : 'Expiring in ' . intval($expire_days) . ' days' ?>
<a class="ms-2" href="<?= escapeHtml($filter_url(['expire_days' => ''])) ?>">Clear</a>
</div>
<?php } ?>
</div>
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 itflow-asset-table">
<thead class="<?php if (!$num_rows[0]) { echo "d-none"; } ?> text-nowrap table-light">
<tr>
<?php if ($can_edit) { ?>
<th class="checkbox-column border-end">
<div class="form-check">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)" title="Select all on this page">
</div>
</th>
<?php } ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=domain_name&order=<?= $disp ?>">
Domain <?php if ($sort == 'domain_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=domain_expire&order=<?= $disp ?>">
Expires <?php if ($sort == 'domain_expire') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=registrar_name&order=<?= $disp ?>">
Registrar and hosts <?php if ($sort == 'registrar_name') { echo $order_icon; } ?>
</a>
</th>
<th>Email</th>
<?php if (!$client_url) { ?>
<th>
<a class="text-reset text-decoration-none" href="?<?= $url_query_strings_sort ?>&sort=client_name&order=<?= $disp ?>">
Client <?php if ($sort == 'client_name') { echo $order_icon; } ?>
</a>
</th>
<?php } ?>
<th></th>
</tr>
</thead>
<tbody>
<?php
foreach ($domains as $domain_id => $row) {
$row_client_id = intval($row['domain_client_id']);
$domain_name = escapeHtml($row['domain_name']);
$domain_description = escapeHtml($row['domain_description']);
$domain_favorite = intval($row['domain_favorite']);
$domain_archived_at = escapeHtml($row['domain_archived_at']);
$domain_tags = $domains_tags[$domain_id] ?? [];
$expiry = domainExpiry($row['domain_expire'], $row['domain_auto_renew']);
$quick_url = "modals/domain/domain.php?id=$domain_id";
?>
<tr>
<?php if ($can_edit) { ?>
<td class="checkbox-column bg-light border-end">
<div class="form-check">
<input class="form-check-input bulk-select" type="checkbox" name="domain_ids[]" value="<?= $domain_id ?>" aria-label="Select <?= $domain_name ?>">
</div>
</td>
<?php } ?>
<td class="itflow-asset-name-cell">
<div class="d-flex align-items-center gap-3">
<?= domainTile() ?>
<div class="min-w-0">
<a class="fw-semibold text-body ajax-modal" href="#" data-modal-size="lg" data-modal-url="<?= $quick_url ?>"><?= $domain_name ?></a>
<?php if ($domain_favorite) { ?><i class="fas fa-star text-warning small ms-1" title="Starred"></i><?php } ?>
<?php if ($domain_archived_at) { ?><span class="badge rounded-pill text-bg-secondary fw-normal ms-1">Archived</span><?php } ?>
<?php if ($domain_description) { ?>
<div class="small text-body-secondary text-truncate itflow-asset-desc" title="<?= $domain_description ?>"><?= $domain_description ?></div>
<?php } ?>
<?php if ($domain_tags || $can_edit) { ?>
<div class="mt-1"><?= tagPills('domain', $domain_id, $domain_tags, "domains.php?$client_url", $can_edit) ?></div>
<?php } ?>
</div>
</div>
</td>
<td class="text-nowrap">
<?= domainExpiryPill($expiry) ?>
<?php if ($expiry) { ?>
<div class="small text-body-secondary mt-1"><?= escapeHtml($expiry['date']) ?></div>
<?php } ?>
</td>
<td class="small itflow-domain-who">
<?php $who = domainVendorsHtml($row); ?>
<?= $who ? '<div class="d-flex flex-column gap-1">' . $who . '</div>' : '<span class="text-body-secondary">-</span>' ?>
</td>
<td class="itflow-domain-mail"><?= domainMailPillsHtml(array_diff_key(domainMailChecks($row), ['mx' => 1])) ?></td>
<?php if (!$client_url) { ?>
<td><a href="domains.php?client_id=<?= $row_client_id ?>"><?= escapeHtml($row['client_name']) ?></a></td>
<?php } ?>
<td class="text-end">
<div class="dropdown dropstart">
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-fw fa-ellipsis-v"></i></button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="<?= $quick_url ?>">
<i class="fas fa-fw fa-eye text-secondary me-2"></i>Quick look
</a>
<a class="dropdown-item" href="https://<?= $domain_name ?>" target="_blank" rel="noopener noreferrer">
<i class="fas fa-fw fa-external-link-alt text-secondary me-2"></i>Open the website
</a>
<?php if ($can_edit && !$domain_archived_at) { ?>
<a class="dropdown-item ajax-modal" href="#" data-modal-size="lg" data-modal-url="modals/domain/domain_edit.php?id=<?= $domain_id ?>">
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
</a>
<a class="dropdown-item" href="post.php?refresh_domain=<?= $domain_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-sync-alt text-secondary me-2"></i>Look up now
</a>
<a class="dropdown-item" href="post.php?<?= $domain_favorite ? 'unfavorite_domain' : 'favorite_domain' ?>=<?= $domain_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="<?= $domain_favorite ? 'far' : 'fas' ?> fa-fw fa-star text-secondary me-2"></i><?= $domain_favorite ? 'Unstar' : 'Star' ?>
</a>
<?php } ?>
<?php if ($can_edit) { ?>
<div class="dropdown-divider"></div>
<?php if ($domain_archived_at) { ?>
<a class="dropdown-item" href="post.php?restore_domain=<?= $domain_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-redo text-secondary me-2"></i>Restore
</a>
<?php if ($can_delete) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_domain=<?= $domain_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>"
data-confirm-text="<?= $domain_name ?> goes for good, with its history. Services and certificates stop pointing at it.">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</a>
<?php } ?>
<?php } else { ?>
<a class="dropdown-item text-danger confirm-link" href="post.php?archive_domain=<?= $domain_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-archive me-2"></i>Archive
</a>
<?php } ?>
<?php } ?>
</div>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</form>
<?php if (!$num_rows[0]) { ?>
<div class="itflow-asset-empty">
<i class="fas fa-globe fa-2x mb-2 opacity-50"></i>
<div><?= $q || $tag_query || $show || $vendor_query || $expire_query ? 'No domains match.' : 'No domains yet.' ?></div>
</div>
<?php } ?>
<?php require_once "../includes/filter_footer.php"; ?>
</div>
<?php if ($can_edit && !$archived) { ?>
<!-- Bulk: who registers or hosts the domains picked -->
<div class="modal fade" id="domainBulkVendorModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-building me-2"></i>Set registrar or host</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<label class="form-label">What they do</label>
<div class="btn-group w-100 mb-3" role="group">
<?php foreach (DOMAIN_ROLES as $role => $info) { ?>
<input type="radio" class="btn-check" form="bulkActions" name="role" value="<?= $role ?>" id="domainBulkRole<?= $role ?>" <?= $role === 'registrar' ? 'checked' : '' ?>>
<label class="btn btn-outline-secondary" for="domainBulkRole<?= $role ?>"><i class="fas <?= $info['icon'] ?> me-1"></i><?= $info['label'] ?></label>
<?php } ?>
</div>
<label class="form-label">Vendor</label>
<select class="form-select select2" form="bulkActions" name="vendor_id">
<option value="0">- Nobody -</option>
<?php foreach ($bulk_vendor_options as $vendor) { ?>
<option value="<?= intval($vendor['vendor_id']) ?>"<?= selectOptionVendor($vendor['vendor_name'], intval($vendor['vendor_client_id']) === 0 ? 'Ours' : $vendor['vendor_description']) ?>><?= escapeHtml($vendor['vendor_name']) ?></option>
<?php } ?>
</select>
<div class="form-text"><?= $client_url ? "The client's vendors, and ours." : 'Across clients only our own vendors can be set - open a client to use theirs.' ?></div>
</div>
<div class="modal-footer">
<button type="submit" form="bulkActions" name="bulk_set_domain_vendor" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Set it</button>
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
</div>
</div>
</div>
</div>
<?php } ?>
<?php if (!empty($_GET['open'])) { ?>
<script>
// Opened from search or the overview - straight to that domain's quick look
document.addEventListener('DOMContentLoaded', function () {
var open = document.createElement('a');
open.className = 'ajax-modal';
open.href = '#';
open.dataset.modalSize = 'lg';
open.dataset.modalUrl = 'modals/domain/domain.php?id=<?= intval($_GET['open']) ?>';
document.body.appendChild(open);
open.click();
open.remove();
});
</script>
<?php } ?>
<script src="../js/bulk_actions.js"></script>
<?php require_once "../includes/footer.php";