Files
urbackup/src/Server.php
T
2026-05-20 11:46:28 +02:00

1008 lines
34 KiB
PHP

<?php
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup;
use CommonDBTM;
use CommonGLPI;
use Dropdown;
use Entity;
use Html;
use Location;
use Session;
class Server extends CommonDBTM
{
public static $rightname = 'plugin_urbackup';
/**
* Get table name.
*
* @param string|null $classname Class name
*
* @return string
*/
public static function getTable($classname = null): string
{
return 'glpi_plugin_urbackup_servers';
}
/**
* Get type name.
*
* @param int $nb Number
*
* @return string
*/
public static function getTypeName($nb = 0): string
{
return _n('UrBackup server', 'UrBackup servers', $nb, 'urbackup');
}
/**
* GLPI standard profile rights definition.
*
* This makes UrBackup appear in the standard GLPI 11 profile rights UI.
*
* @param string $interface Interface
*
* @return array<int, array<string, string>>
*/
public function getRights($interface = 'central'): array
{
return [
READ => [
'short' => __('Read'),
'long' => __('Read'),
],
UPDATE => [
'short' => __('Update'),
'long' => __('Update'),
],
CREATE => [
'short' => __('Create'),
'long' => __('Create'),
],
DELETE => [
'short' => __('Delete'),
'long' => __('Delete'),
],
PURGE => [
'short' => __('Purge'),
'long' => __('Purge'),
],
];
}
/**
* Check view right.
*
* @return bool
*/
public static function canView(): bool
{
return Profile::canCurrentUser(READ);
}
/**
* Check create right.
*
* @return bool
*/
public static function canCreate(): bool
{
return Profile::canCurrentUser(CREATE);
}
/**
* Check update right.
*
* @return bool
*/
public static function canUpdate(): bool
{
return Profile::canCurrentUser(UPDATE);
}
/**
* Check delete right.
*
* @return bool
*/
public static function canDelete(): bool
{
return Profile::canCurrentUser(DELETE);
}
/**
* Check purge right.
*
* @return bool
*/
public static function canPurge(): bool
{
return Profile::canCurrentUser(PURGE);
}
/**
* Get menu name.
*
* @return string
*/
public static function getMenuName(): string
{
return self::getTypeName(Session::getPluralNumber());
}
/**
* Get menu content.
*
* @return array<string, mixed>
*/
public static function getMenuContent(): array
{
$menu = [];
if (self::canView()) {
$menu['title'] = self::getMenuName();
$menu['page'] = self::getSearchURL(false);
$menu['icon'] = 'ti ti-server';
$menu['links']['search'] = self::getSearchURL(false);
if (self::canCreate()) {
$menu['links']['add'] = self::getFormURL(false);
}
}
return $menu;
}
/**
* Define tabs.
*
* @param array<string, mixed> $options Options
*
* @return array<string, string>
*/
public function defineTabs($options = []): array
{
$ong = [];
$this->addDefaultFormTab($ong);
$this->addStandardTab(ServerAsset::class, $ong, $options);
$this->addStandardTab('Log', $ong, $options);
return $ong;
}
/**
* Search options.
*
* @return array<int, array<string, mixed>>
*/
public function rawSearchOptions(): array
{
$tab = [];
$tab[] = [
'id' => 'common',
'name' => __('Characteristics'),
];
$tab[] = [
'id' => 1,
'table' => self::getTable(),
'field' => 'name',
'name' => __('Name'),
'datatype' => 'itemlink',
'massiveaction' => false,
];
$tab[] = [
'id' => 2,
'table' => self::getTable(),
'field' => 'ip_address',
'name' => __('IP address', 'urbackup'),
'datatype' => 'string',
];
$tab[] = [
'id' => 3,
'table' => self::getTable(),
'field' => 'port',
'name' => __('Network port', 'urbackup'),
'datatype' => 'integer',
];
$tab[] = [
'id' => 4,
'table' => self::getTable(),
'field' => 'protocol',
'name' => __('Protocol', 'urbackup'),
'datatype' => 'string',
];
$tab[] = [
'id' => 5,
'table' => self::getTable(),
'field' => 'server_version',
'name' => __('UrBackup server version', 'urbackup'),
'datatype' => 'string',
];
$tab[] = [
'id' => 6,
'table' => Entity::getTable(),
'field' => 'completename',
'name' => Entity::getTypeName(1),
'datatype' => 'dropdown',
];
$tab[] = [
'id' => 7,
'table' => Location::getTable(),
'field' => 'completename',
'name' => Location::getTypeName(1),
'datatype' => 'dropdown',
];
$tab[] = [
'id' => 8,
'table' => self::getTable(),
'field' => 'is_active',
'name' => __('Active'),
'datatype' => 'bool',
];
$tab[] = [
'id' => 9,
'table' => self::getTable(),
'field' => 'last_api_status',
'name' => __('Last API status', 'urbackup'),
'datatype' => 'bool',
];
$tab[] = [
'id' => 10,
'table' => self::getTable(),
'field' => 'last_api_check',
'name' => __('Last API check', 'urbackup'),
'datatype' => 'datetime',
];
$tab[] = [
'id' => 11,
'table' => self::getTable(),
'field' => 'date_creation',
'name' => __('Creation date'),
'datatype' => 'datetime',
];
$tab[] = [
'id' => 12,
'table' => self::getTable(),
'field' => 'date_mod',
'name' => __('Last update'),
'datatype' => 'datetime',
];
$tab[] = [
'id' => 13,
'table' => self::getTable(),
'field' => 'id',
'name' => __('View', 'urbackup'),
'massiveaction' => false,
'datatype' => 'raw',
'searchtype' => 'view',
];
return $tab;
}
/**
* Show server form.
*
* @param int $ID ID
* @param array $options Options
*
* @return bool
*/
public function showForm($ID, array $options = []): bool
{
if ($ID > 0) {
$this->check($ID, READ);
} else {
$this->check(-1, CREATE);
$this->getEmpty();
}
$this->initForm($ID, $options);
$this->showFormHeader($options);
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Name')) . "</td>";
echo "<td>";
echo Html::input('name', [
'value' => $this->fields['name'] ?? '',
'size' => 40,
]);
echo "</td>";
echo "<td>" . htmlspecialchars(__('Active')) . "</td>";
echo "<td>";
Dropdown::showYesNo('is_active', (int) ($this->fields['is_active'] ?? 1));
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(Entity::getTypeName(1)) . "</td>";
echo "<td>";
Entity::dropdown([
'name' => 'entities_id',
'value' => (int) ($this->fields['entities_id'] ?? ($_SESSION['glpiactive_entity'] ?? 0)),
]);
echo "</td>";
echo "<td>" . htmlspecialchars(__('Recursive')) . "</td>";
echo "<td>";
Dropdown::showYesNo('is_recursive', (int) ($this->fields['is_recursive'] ?? 0));
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(Location::getTypeName(1)) . "</td>";
echo "<td>";
Location::dropdown([
'name' => 'locations_id',
'value' => (int) ($this->fields['locations_id'] ?? 0),
]);
echo "<br><small>";
echo htmlspecialchars(
__('Associate the server with the main/root location. Assets in sub-locations will use this root location server.', 'urbackup')
);
echo "</small>";
echo "</td>";
echo "<td>" . htmlspecialchars(__('Protocol', 'urbackup')) . "</td>";
echo "<td>";
Dropdown::showFromArray(
'protocol',
[
'http' => 'HTTP',
'https' => 'HTTPS',
],
[
'value' => $this->fields['protocol'] ?? 'http',
]
);
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('IP address', 'urbackup')) . "</td>";
echo "<td>";
echo Html::input('ip_address', [
'value' => $this->fields['ip_address'] ?? '',
'size' => 40,
]);
echo "</td>";
echo "<td>" . htmlspecialchars(__('Network port', 'urbackup')) . "</td>";
echo "<td>";
echo Html::input('port', [
'value' => $this->fields['port'] ?? 55414,
'type' => 'number',
'min' => 1,
'max' => 65535,
]);
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('UrBackup server version', 'urbackup')) . "</td>";
echo "<td>";
echo Html::input('server_version', [
'value' => $this->fields['server_version'] ?? '',
'size' => 30,
]);
echo "</td>";
echo "<td>" . htmlspecialchars(__('Ignore SSL verification', 'urbackup')) . "</td>";
echo "<td>";
Dropdown::showYesNo('ignore_ssl', (int) ($this->fields['ignore_ssl'] ?? 0));
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('API username', 'urbackup')) . "</td>";
echo "<td>";
echo Html::input('api_username', [
'value' => $this->fields['api_username'] ?? '',
'size' => 40,
'autocomplete' => 'off',
]);
echo "</td>";
echo "<td>" . htmlspecialchars(__('API password', 'urbackup')) . "</td>";
echo "<td>";
echo "<input type='password' name='api_password' value='" .
htmlspecialchars((string) ($this->fields['api_password'] ?? '')) .
"' autocomplete='new-password'>";
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Comments')) . "</td>";
echo "<td colspan='3'>";
echo "<textarea name='comment' rows='5' cols='100'>" .
htmlspecialchars((string) ($this->fields['comment'] ?? '')) .
"</textarea>";
echo "</td>";
echo "</tr>";
if ($ID > 0) {
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('UrBackup web interface', 'urbackup')) . "</td>";
echo "<td colspan='3'>";
$url = $this->getWebInterfaceUrl();
if ($url !== '#') {
echo "<a href='" . htmlspecialchars($url) . "' target='_blank' rel='noopener' class='btn btn-secondary'>";
echo htmlspecialchars(__('Open UrBackup interface', 'urbackup'));
echo "</a>";
} else {
echo htmlspecialchars(__('No URL available', 'urbackup'));
}
echo "</td>";
echo "</tr>";
$apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('API connection status', 'urbackup')) . "<br><small class='text-muted'>" . htmlspecialchars(__('Click Save to test connection', 'urbackup')) . "</small></td>";
echo "<td colspan='3'>";
if ($apiStatus === 1) {
echo '<span class="text-success fw-bold"><i class="ti ti-check"></i> ' . htmlspecialchars(__('API connection OK', 'urbackup')) . '</span>';
} else {
echo '<span class="text-danger fw-bold"><i class="ti ti-x"></i> ' . htmlspecialchars(__('API connection failed', 'urbackup')) . '</span>';
if (!empty($this->fields['last_api_message'])) {
echo '<br><small class="text-muted">' . htmlspecialchars((string) $this->fields['last_api_message']) . '</small>';
}
}
echo "</td>";
echo "</tr>";
}
$this->showFormButtons($options);
return true;
}
/**
* Get web interface URL.
*
* @return string
*/
public function getWebInterfaceUrl(): string
{
$protocol = (string) ($this->fields['protocol'] ?? 'http');
$ip = (string) ($this->fields['ip_address'] ?? '');
$port = (int) ($this->fields['port'] ?? 55414);
if ($ip === '') {
return '#';
}
return sprintf('%s://%s:%d', $protocol, $ip, $port);
}
/**
* Get active servers assigned to a root location.
*
* @param int $locations_id Root location ID
*
* @return array<int, string>
*/
public static function getActiveServersForRootLocation(int $locations_id): array
{
global $DB;
$servers = [];
if ($locations_id <= 0) {
return $servers;
}
if (!$DB->tableExists(self::getTable())) {
return $servers;
}
$iterator = $DB->request([
'FROM' => self::getTable(),
'WHERE' => [
'locations_id' => $locations_id,
'is_active' => 1,
],
'ORDER' => 'name',
]);
foreach ($iterator as $row) {
$servers[(int) $row['id']] = (string) $row['name'];
}
return $servers;
}
public function prepareInputForAdd(mixed $input): mixed
{
return $this->prepareInputForUpdate($input);
}
public function prepareInputForUpdate(mixed $input): mixed
{
if (!is_array($input)) {
return $input;
}
if (!empty($input['id']) && (int) $input['id'] > 0) {
$server = new self();
if ($server->getFromDB((int) $input['id'])) {
$serverFields = $server->fields;
$ip = $input['ip_address'] ?? $serverFields['ip_address'] ?? '';
$port = $input['port'] ?? $serverFields['port'] ?? 55414;
$protocol = $input['protocol'] ?? $serverFields['protocol'] ?? 'http';
$apiUsername = $input['api_username'] ?? $serverFields['api_username'] ?? '';
$apiPassword = $input['api_password'] ?? $serverFields['api_password'] ?? '';
$ignoreSsl = $input['ignore_ssl'] ?? $serverFields['ignore_ssl'] ?? 0;
if ($ip !== '') {
$tmpServer = new self();
$tmpServer->fields = [
'id' => (int) $input['id'],
'ip_address' => $ip,
'port' => $port,
'protocol' => $protocol,
'api_username' => $apiUsername,
'api_password' => $apiPassword,
'ignore_ssl' => $ignoreSsl,
];
try {
$client = new UrbackupApiClient($tmpServer);
$result = $client->testConnection();
$input['last_api_status'] = $result['success'] ? 1 : 0;
$input['last_api_message'] = $result['message'] ?? '';
$input['last_api_check'] = date('Y-m-d H:i:s');
} catch (\Throwable $e) {
$input['last_api_status'] = 0;
$input['last_api_message'] = $e->getMessage();
$input['last_api_check'] = date('Y-m-d H:i:s');
}
}
}
}
return $input;
}
public function testApiConnection(): array
{
$ip = (string) ($this->fields['ip_address'] ?? '');
if ($ip === '') {
return [
'status' => 'no_ip',
'html' => '<span class="text-muted">' . htmlspecialchars(__('No IP address configured', 'urbackup')) . '</span>',
];
}
try {
$client = new UrbackupApiClient($this);
$result = $client->testConnection();
$this->update([
'id' => (int) $this->fields['id'],
'last_api_status' => $result['success'] ? 1 : 0,
'last_api_message' => $result['message'] ?? '',
'last_api_check' => date('Y-m-d H:i:s'),
]);
if ($result['success']) {
return [
'status' => 'ok',
'html' => '<span class="text-success fw-bold"><i class="ti ti-check"></i> ' .
htmlspecialchars(__('API connection OK', 'urbackup')) . '</span>',
];
}
return [
'status' => 'failed',
'html' => '<span class="text-danger fw-bold"><i class="ti ti-x"></i> ' .
htmlspecialchars(__('API connection failed', 'urbackup')) . '</span><br>' .
'<small class="text-muted">' . htmlspecialchars($result['message'] ?? '') . '</small>',
];
} catch (\Throwable $e) {
$message = $e->getMessage();
$isUnreachable = $this->isNetworkError($message);
return [
'status' => $isUnreachable ? 'unreachable' : 'failed',
'html' => '<span class="' . ($isUnreachable ? 'text-warning' : 'text-danger') . ' fw-bold">' .
'<i class="ti ' . ($isUnreachable ? 'ti-wifi-off' : 'ti-x') . '"></i> ' .
htmlspecialchars($isUnreachable ? __('Server unreachable', 'urbackup') : __('API connection failed', 'urbackup')) .
'</span><br>' .
'<small class="text-muted">' . htmlspecialchars($message) . '</small>',
];
}
}
private function isNetworkError(string $message): bool
{
$networkKeywords = [
'timeout',
'could not resolve host',
'couldn\'t connect to host',
'connection refused',
'connection timed out',
'network is unreachable',
'no route to host',
'ssl',
'certificate',
'curl error',
'request failed',
'returned HTTP status',
'returned non-JSON response',
'problem with the ssl certificate',
'ssl certificate problem',
'ssl connect error',
'ssl wrong version',
];
$lowerMessage = strtolower($message);
foreach ($networkKeywords as $keyword) {
if (str_contains($lowerMessage, strtolower($keyword))) {
return true;
}
}
if (preg_match('/http status [45]\d{2}/', $lowerMessage)) {
return true;
}
return false;
}
private static function renderOnlineBadge(mixed $online, mixed $statusString): string
{
$online = match (true) {
$online === true || $online === 1 || $online === '1' || $online === 'true' => 1,
$online === false || $online === 0 || $online === '0' || $online === 'false' => 0,
default => null,
};
$parts = [];
if ($online === 1) {
$parts[] = '<span class="text-success fw-bold"><i class="ti ti-player-play"></i> '
. htmlspecialchars(__('Online', 'urbackup')) . '</span>';
} elseif ($online === 0) {
$parts[] = '<span class="text-muted"><i class="ti ti-player-pause"></i> '
. htmlspecialchars(__('Offline', 'urbackup')) . '</span>';
} else {
$parts[] = '<span class="text-muted">-</span>';
}
$statusString = is_string($statusString) ? trim((string) $statusString) : '';
if ($statusString !== '') {
$badgeClass = 'secondary';
if (strtolower($statusString) === 'ok') {
$badgeClass = 'success';
} elseif (in_array(strtolower($statusString), ['minor_problems', 'minor problems'], true)) {
$badgeClass = 'warning';
} elseif (in_array(strtolower($statusString), ['major_problems', 'major problems', 'error'], true)) {
$badgeClass = 'danger';
} elseif (strtolower($statusString) === 'paused') {
$badgeClass = 'secondary';
}
$parts[] = '<span class="badge bg-' . $badgeClass . ' ms-1">'
. htmlspecialchars($statusString) . '</span>';
}
return implode(' ', $parts);
}
private static function formatLastBackup(mixed $value): string
{
if ($value === null || $value === '' || $value === 0 || $value === '0') {
return '';
}
if (is_numeric($value)) {
$timestamp = (int) $value;
if ($timestamp > 0 && $timestamp < 2000000000) {
return date('Y-m-d H:i:s', $timestamp);
}
return (string) $value;
}
return (string) $value;
}
public static function showLinkedClientsTab(Server $server): void
{
global $DB;
$apiStatus = (int) ($server->fields['last_api_status'] ?? 0);
if ($apiStatus !== 1) {
echo '<div class="alert alert-warning">';
echo htmlspecialchars(__('API connection not working. Save server to test connection.', 'urbackup'));
echo '</div>';
return;
}
$iterator = $DB->request([
'FROM' => 'glpi_plugin_urbackup_serverassets',
'WHERE' => [
'plugin_urbackup_servers_id' => (int) $server->fields['id'],
],
]);
$linkedAssets = [];
foreach ($iterator as $row) {
$linkedAssets[] = $row;
}
if (count($linkedAssets) === 0) {
echo '<div class="alert alert-info">';
echo htmlspecialchars(__('No linked assets', 'urbackup'));
echo '</div>';
return;
}
try {
$client = new UrbackupApiClient($server);
$urbackupClients = $client->getStatus();
if (empty($urbackupClients)) {
echo '<div class="alert alert-warning">';
echo htmlspecialchars(__('No clients found on UrBackup server', 'urbackup'));
echo '</div>';
return;
}
} catch (\Throwable $e) {
echo '<div class="alert alert-danger">';
echo 'API Error: ' . htmlspecialchars($e->getMessage());
echo '</div>';
return;
}
echo '<table class="table table-striped table-hover">';
echo '<thead>';
echo '<tr>';
echo '<th>' . htmlspecialchars(__('Asset', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Name', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Client name', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Version', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Last backup', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('IP address', 'urbackup')) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ($linkedAssets as $link) {
$glpiItem = null;
if (!empty($link['itemtype']) && !empty($link['items_id'])) {
$itemClass = $link['itemtype'];
if (class_exists($itemClass)) {
$glpiItem = new $itemClass();
$glpiItem->getFromDB((int) $link['items_id']);
}
}
$clientName = $glpiItem ? ($glpiItem->fields['name'] ?? '') : '';
$urbackupClient = null;
foreach ($urbackupClients as $uc) {
$ucName = (string) ($uc['name'] ?? $uc['clientname'] ?? $uc['hostname'] ?? '');
if (strcasecmp($ucName, $clientName) === 0) {
$urbackupClient = $uc;
break;
}
}
$online = $urbackupClient ? ($urbackupClient['online'] ?? null) : null;
$apiStatusString = $urbackupClient ? ($urbackupClient['status'] ?? '') : '';
$statusHtml = self::renderOnlineBadge($online, $apiStatusString);
$lastBackupRaw = $urbackupClient
? ($urbackupClient['lastbackup'] ?? $urbackupClient['lastbackup_time'] ?? $urbackupClient['last_backup'] ?? $urbackupClient['last_backup_time'] ?? $urbackupClient['file_lastbackup'] ?? $urbackupClient['image_lastbackup'] ?? '')
: '';
$lastBackup = self::formatLastBackup($lastBackupRaw);
$clientIp = $urbackupClient
? ($urbackupClient['client_ip'] ?? $urbackupClient['ip'] ?? $urbackupClient['ip_address'] ?? $urbackupClient['clientaddress'] ?? '')
: '';
$clientVersion = $urbackupClient ? ($urbackupClient['client_version_string'] ?? $urbackupClient['client_version'] ?? '-') : '-';
$urbackupClientName = $urbackupClient ? ($urbackupClient['name'] ?? '-') : '-';
$itemTypeLabel = !empty($link['itemtype']) ? (class_exists($link['itemtype']) ? $link['itemtype']::getTypeName(1) : $link['itemtype']) : '';
$itemUrl = $glpiItem ? $glpiItem->getLinkURL() : '';
echo '<tr>';
echo '<td>' . htmlspecialchars($itemTypeLabel . ' #' . $link['items_id']) . '</td>';
echo '<td>' . ($itemUrl ? '<a href="' . htmlspecialchars($itemUrl) . '">' . htmlspecialchars($clientName) . '</a>' : htmlspecialchars($clientName)) . '</td>';
echo '<td>' . htmlspecialchars($urbackupClientName) . '</td>';
echo '<td>' . htmlspecialchars($clientVersion) . '</td>';
echo '<td>' . $statusHtml . '</td>';
echo '<td>' . htmlspecialchars($lastBackup ?: '-') . '</td>';
echo '<td>' . htmlspecialchars($clientIp ?: '-') . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
}
public static function showUnlinkedClientsTab(Server $server): void
{
$apiStatus = (int) ($server->fields['last_api_status'] ?? 0);
if ($apiStatus !== 1) {
echo '<div class="alert alert-warning">';
echo htmlspecialchars(__('API connection not working. Save server to test connection.', 'urbackup'));
echo '</div>';
return;
}
global $DB;
$iterator = $DB->request([
'FROM' => 'glpi_plugin_urbackup_serverassets',
]);
$linkedNames = [];
foreach ($iterator as $row) {
$assetName = ServerAsset::getAssetName($row['itemtype'], (int) $row['items_id']);
if ($assetName !== '') {
$linkedNames[] = strtolower($assetName);
}
}
try {
$client = new UrbackupApiClient($server);
$urbackupClients = $client->getStatus();
} catch (\Throwable $e) {
echo '<div class="alert alert-danger">';
echo htmlspecialchars($e->getMessage());
echo '</div>';
return;
}
$unlinkedClients = [];
foreach ($urbackupClients as $uc) {
$name = strtolower((string) ($uc['name'] ?? $uc['clientname'] ?? $uc['hostname'] ?? ''));
if (!in_array($name, $linkedNames, true)) {
$unlinkedClients[] = $uc;
}
}
if (count($unlinkedClients) === 0) {
echo '<div class="alert alert-info">';
echo htmlspecialchars(__('No unlinked clients found on UrBackup server', 'urbackup'));
echo '</div>';
return;
}
$serverLocationId = (int) ($server->fields['locations_id'] ?? 0);
$linkableAssets = [];
if ($serverLocationId > 0) {
$itemtypes = Config::getEnabledItemtypes();
foreach ($unlinkedClients as $uc) {
$clientName = (string) ($uc['name'] ?? '');
if ($clientName === '') {
continue;
}
$clientNameLower = strtolower($clientName);
foreach ($itemtypes as $itemtype) {
if (!class_exists($itemtype)) {
continue;
}
$assetItem = new $itemtype();
if (!$assetItem instanceof CommonDBTM) {
continue;
}
$table = $assetItem->getTable();
if (!$DB->tableExists($table)) {
continue;
}
$assetIterator = $DB->request([
'FROM' => $table,
'WHERE' => [
'name' => $clientName,
'is_deleted' => 0,
],
'LIMIT' => 1,
]);
foreach ($assetIterator as $assetRow) {
$assetLocationId = (int) ($assetRow['locations_id'] ?? 0);
$rootLocationId = LocationHelper::getRootLocationId($assetLocationId);
if ($rootLocationId > 0 && $rootLocationId === $serverLocationId) {
$linkableAssets[$clientNameLower] = [
'itemtype' => $itemtype,
'items_id' => (int) $assetRow['id'],
];
}
break;
}
}
}
}
echo '<table class="table table-striped table-hover">';
echo '<thead>';
echo '<tr>';
echo '<th>' . htmlspecialchars(__('Name', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Version', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Last backup', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('IP address', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Actions', 'urbackup')) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ($unlinkedClients as $uc) {
$clientName = (string) ($uc['name'] ?? 'Unknown');
$clientNameLower = strtolower($clientName);
$online = $uc['online'] ?? null;
$apiStatusString = $uc['status'] ?? '';
$statusHtml = self::renderOnlineBadge($online, $apiStatusString);
$lastBackupRaw = $uc['lastbackup'] ?? $uc['lastbackup_time'] ?? $uc['last_backup'] ?? $uc['last_backup_time'] ?? $uc['file_lastbackup'] ?? $uc['image_lastbackup'] ?? '';
$lastBackup = self::formatLastBackup($lastBackupRaw);
$clientIp = $uc['client_ip'] ?? $uc['ip'] ?? $uc['ip_address'] ?? $uc['clientaddress'] ?? '';
$clientVersion = $uc['client_version_string'] ?? $uc['client_version'] ?? '-';
echo '<tr>';
echo '<td>' . htmlspecialchars($clientName) . '</td>';
echo '<td>' . htmlspecialchars($clientVersion) . '</td>';
echo '<td>' . $statusHtml . '</td>';
echo '<td>' . htmlspecialchars($lastBackup ?: '-') . '</td>';
echo '<td>' . htmlspecialchars($clientIp ?: '-') . '</td>';
echo '<td>';
if (isset($linkableAssets[$clientNameLower])) {
$match = $linkableAssets[$clientNameLower];
$formAction = PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php';
echo '<form method="post" action="' . htmlspecialchars($formAction) . '" class="d-inline">';
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::hidden('itemtype', ['value' => $match['itemtype']]);
echo Html::hidden('items_id', ['value' => $match['items_id']]);
echo Html::hidden('id', ['value' => (int) $server->fields['id']]);
echo '<button type="submit" name="link_asset" value="1" class="btn btn-primary btn-sm">';
echo htmlspecialchars(__('Connect'));
echo '</button>';
Html::closeForm();
} else {
echo '<span class="text-muted">—</span>';
}
echo '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
}
}