20 Commits

Author SHA1 Message Date
mariano 5fac26040a change icon to ti-cloud-up 2026-05-21 10:25:21 +02:00
mariano 9ee28330ac modifica icone 2 2026-05-21 09:49:06 +02:00
mariano 8a52489b41 modifica icone 2026-05-21 09:37:58 +02:00
mariano 9979d62a2a sistemazione finale - server .php 2026-05-21 09:26:03 +02:00
mariano 01d678e855 group in list 2026-05-21 07:35:32 +02:00
mariano 0e80c1430f problema gruppo pc 2026-05-20 14:21:18 +02:00
mariano d69c465c92 modifica lista missing computer 2026-05-20 14:17:45 +02:00
mariano aba2ecf4aa modifica visualizzazione pagina server 2026-05-20 14:08:23 +02:00
mariano df28fc0018 sistemazione erore pag server.form.php 2026-05-20 13:48:33 +02:00
mariano 7a578db677 modifica pulsanti comandi 2026-05-20 13:44:44 +02:00
mariano 771409e396 tab in pagina computer.form 2026-05-20 13:39:31 +02:00
mariano 6441a8636d elimina pippo.txt 2026-05-20 13:16:07 +02:00
mariano 6a49489b73 merge all 2026-05-20 13:11:03 +02:00
mariano a5c20dc834 pippo.txt 2026-05-20 12:25:45 +02:00
mariano 6056a29865 remove debug raw data 2026-05-20 12:04:21 +02:00
mariano a20e429456 modifica redirect 2026-05-20 12:00:39 +02:00
mariano a1c3a5a07e pulsanti e create client 2026-05-20 11:46:28 +02:00
mariano f4bd50c1b1 pulsanti e sistemazione create client 2026-05-20 11:16:14 +02:00
mariano 7143d721fb fix true/false toggle switch 2026-05-20 10:26:50 +02:00
mariano fe9223a617 fix internet_enabled 2026-05-20 10:13:30 +02:00
10 changed files with 702 additions and 104 deletions
+50
View File
@@ -267,3 +267,53 @@ Il test del pulsante "Test API" continua a restituire 403 Forbidden quando acces
- `src/AssetTab.php`: Aggiunti metodi `startBackup()`, `saveInternetMode()`, `saveDefaultDirs()` - `src/AssetTab.php`: Aggiunti metodi `startBackup()`, `saveInternetMode()`, `saveDefaultDirs()`
- `src/UrbackupApiClient.php`: Aggiunti metodi `updateClientSettings()`, `saveInternetMode()` - `src/UrbackupApiClient.php`: Aggiunti metodi `updateClientSettings()`, `saveInternetMode()`
- `front/asset.form.php`: Gestione azioni per salvataggio impostazioni e backup - `front/asset.form.php`: Gestione azioni per salvataggio impostazioni e backup
### Refactoring server.form.php e nuove tab (v0.4.7)
**Data**: 2026-05-21
**Descrizione**: Restrutturazione completa della pagina server con tab laterali e nuove funzionalità:
**Refactoring Server::showForm()**:
- Estratto `showFormFields(?int $ID)` da `showForm()` per permettere layout custom
- `showForm()` ora chiama header + fields + buttons in sequenza
**Tab laterali (server.form.php)**:
- Side tabs a sinistra (col-2) con `nav-pills flex-column`
- 4 tab: Server, Linked clients, Unlinked clients, Missing clients
- Tab persistence via URL hash (JS su `shown.bs.tab`)
**Missing clients tab**:
- Mostra asset GLPI con root location matching, non ancora collegati a ServerAsset e non presenti su UrBackup
- Colonne: Name (link all'asset), Entity, Location (completename), Inventory number, IP, State, User, Group
- Ricerca testuale client-side
- Ordinamento colonne click-to-sort vanilla JS
- Navigazione server spostata in server.form.php (sopra i tab, indipendente dalle sezioni)
**Bug fix namespace risolto**:
- `getCachedName()` usava `new $classname()` con stringa → PHP cercava `GlpiPlugin\Urbackup\Group` invece di `\Group`
- Fix: `$fqcn = '\\' . ltrim($classname, '\\')` per forzare global namespace
**Gruppo asset corretto**:
- Gruppo non in `glpi_computers.groups_id` (colonna inesistente)
- Letto da `glpi_groups_items` WHERE `type = 1` (GROUP_TYPE_NORMAL)
- Usa `completename` per gerarchia (es. "Helpdesk > Livello 1")
**Indirizzo IP asset**:
- Query JOIN: `glpi_ipaddresses → glpi_networknames → glpi_networkports`
- Primo IP valido per l'item
**Breadcrumb**:
- Cambiato da `'Assets'` a `'admin'` in `Html::header()` di server.php e server.form.php
- Mostra: Pagina principale > Amministrazione > UrBackup servers
**Fix port default per nuovi server**:
- `prepareInputForAdd()` ora setta default 55414 per `port`, 'http' per `protocol`, 1 per `is_active`, ecc.
- `prepareInputForUpdate()` normalizza anche valori vuoti
**File modificati**:
- `src/Server.php`: `showFormFields()`, `showMissingClientsTab()`, `getCachedName()` fix, `getAssetGroupName()`, `getAssetIp()`, `getCachedLocationName()`, `prepareInputForAdd/Update` default fix, icona menu `ti-hard-drives`
- `front/server.form.php`: Tab laterali, tab hash JS, breadcrumb admin, navigazione server
- `front/server.php`: Breadcrumb admin, rimosso pulsante "Add" manuale (GLPI lo genera)
- `src/Profile.php`: Icona profilo `ti-hard-drives`
- `src/AssetTab.php`: Icona tab asset `ti-hard-drives`
+32
View File
@@ -152,6 +152,38 @@ if (isset($_POST['execute'])) {
} }
} }
break; break;
case 'create_client':
if (Profile::canCurrentUser(CREATE)) {
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, false);
if ($link !== null) {
$server = new \GlpiPlugin\Urbackup\Server();
$server->getFromDB((int) $link['plugin_urbackup_servers_id']);
$client_name = ServerAsset::getAssetName($itemtype, $items_id);
try {
$api = new \GlpiPlugin\Urbackup\UrbackupApiClient($server);
$api->addClient($client_name);
} catch (\Throwable $e) {
// ignore
}
}
}
break;
case 'delete_client':
if (Profile::canCurrentUser(PURGE)) {
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, false);
if ($link !== null) {
$server = new \GlpiPlugin\Urbackup\Server();
$server->getFromDB((int) $link['plugin_urbackup_servers_id']);
$client_name = ServerAsset::getAssetName($itemtype, $items_id);
try {
$api = new \GlpiPlugin\Urbackup\UrbackupApiClient($server);
$api->removeClient($client_name);
} catch (\Throwable $e) {
// ignore
}
}
}
break;
case 'set_internet_mode': case 'set_internet_mode':
if (Profile::canCurrentUser(UPDATE)) { if (Profile::canCurrentUser(UPDATE)) {
$enabled = (int) ($_POST['internet_mode'] ?? 0) === 1; $enabled = (int) ($_POST['internet_mode'] ?? 0) === 1;
+122 -20
View File
@@ -2,7 +2,7 @@
use GlpiPlugin\Urbackup\Profile; use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server; use GlpiPlugin\Urbackup\Server;
use Html; use GlpiPlugin\Urbackup\ServerAsset;
if (!defined('GLPI_ROOT')) { if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__, 4)); define('GLPI_ROOT', dirname(__DIR__, 4));
@@ -17,6 +17,18 @@ if (!Profile::canCurrentUser(UPDATE)) {
$server = new Server(); $server = new Server();
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['link_asset'])) {
$itemtype = (string) ($_POST['itemtype'] ?? '');
$items_id = (int) ($_POST['items_id'] ?? 0);
$server_id = (int) ($_POST['id'] ?? 0);
if ($itemtype !== '' && $items_id > 0 && $server_id > 0) {
ServerAsset::connectAssetToServer($itemtype, $items_id, $server_id);
}
Html::redirect(PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php?id=' . $server_id);
}
$id = $_POST['id'] ?? 0; $id = $_POST['id'] ?? 0;
if ($id > 0) { if ($id > 0) {
@@ -35,30 +47,120 @@ $ID = $_GET['id'] ?? null;
Html::header( Html::header(
$ID ? __('Edit UrBackup server', 'urbackup') : __('Add UrBackup server', 'urbackup'), $ID ? __('Edit UrBackup server', 'urbackup') : __('Add UrBackup server', 'urbackup'),
'', '',
'Assets', 'admin',
'GlpiPlugin\Urbackup\Server' 'GlpiPlugin\Urbackup\Server'
); );
$server->showForm($ID); if ($ID > 0) {
global $DB;
if ($ID > 0 && $server->getFromDB($ID)) { $server->getFromDB($ID);
echo '<div class="card mt-4">';
echo '<div class="card-header">';
echo '<h5 class="mb-0">' . htmlspecialchars(__('Linked clients', 'urbackup')) . '</h5>';
echo '</div>';
echo '<div class="card-body">';
Server::showLinkedClientsTab($server);
echo '</div>';
echo '</div>';
echo '<div class="card mt-4">'; $serverIterator = $DB->request([
echo '<div class="card-header">'; 'FROM' => Server::getTable(),
echo '<h5 class="mb-0">' . htmlspecialchars(__('Unlinked clients', 'urbackup')) . '</h5>'; 'ORDER' => 'name',
echo '</div>'; ]);
echo '<div class="card-body">'; $serverIds = [];
Server::showUnlinkedClientsTab($server); foreach ($serverIterator as $row) {
echo '</div>'; $serverIds[(int) $row['id']] = (string) $row['name'];
echo '</div>'; }
$serverIdKeys = array_keys($serverIds);
$currentIndex = array_search((int) $ID, $serverIdKeys, true);
$totalServers = count($serverIds);
$prevId = ($currentIndex !== false && $currentIndex > 0) ? $serverIdKeys[$currentIndex - 1] : null;
$nextId = ($currentIndex !== false && $currentIndex < $totalServers - 1) ? $serverIdKeys[$currentIndex + 1] : null;
$baseUrl = PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php';
?>
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-center">
<a href="<?php echo htmlspecialchars(PLUGIN_URBACKUP_WEB_DIR . '/front/server.php'); ?>" class="btn btn-sm btn-icon btn-ghost-secondary me-2"
data-bs-toggle="tooltip" data-bs-placement="bottom" title="<?php echo htmlspecialchars(__('List')); ?>">
<i class="ti ti-list-search fs-2"></i>
</a>
<?php if ($prevId !== null): ?>
<a href="<?php echo htmlspecialchars($baseUrl . '?id=' . $prevId); ?>"
class="btn btn-sm btn-icon btn-ghost-secondary me-2"
data-bs-toggle="tooltip" data-bs-placement="bottom" title="<?php echo htmlspecialchars(__('Previous')); ?>">
<i class="fs-2 ti ti-chevron-left"></i>
</a>
<?php else: ?>
<span class="btn btn-sm btn-icon btn-ghost-secondary me-2 bs-invisible">
<i class="fs-2 ti ti-chevron-left"></i>
</span>
<?php endif; ?>
<span class="fw-bold mx-2"><?php echo htmlspecialchars(($currentIndex !== false ? $currentIndex + 1 : 1) . ' / ' . $totalServers); ?></span>
<?php if ($nextId !== null): ?>
<a href="<?php echo htmlspecialchars($baseUrl . '?id=' . $nextId); ?>"
class="btn btn-sm btn-icon btn-ghost-secondary ms-2"
data-bs-toggle="tooltip" data-bs-placement="bottom" title="<?php echo htmlspecialchars(__('Next')); ?>">
<i class="fs-2 ti ti-chevron-right"></i>
</a>
<?php else: ?>
<span class="btn btn-sm btn-icon btn-ghost-secondary ms-2 bs-invisible">
<i class="fs-2 ti ti-chevron-right"></i>
</span>
<?php endif; ?>
</div>
</div>
<div class="row">
<div class="col-2">
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#tab-server">
<?php echo htmlspecialchars(__('Server')); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-linked">
<?php echo htmlspecialchars(__('Linked clients', 'urbackup')); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-unlinked">
<?php echo htmlspecialchars(__('Unlinked clients', 'urbackup')); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-missing">
<?php echo htmlspecialchars(__('Missing clients', 'urbackup')); ?>
</a>
</li>
</ul>
</div>
<div class="col-10">
<div class="tab-content">
<div class="tab-pane active" id="tab-server">
<?php $server->showForm($ID); ?>
</div>
<div class="tab-pane" id="tab-linked">
<?php Server::showLinkedClientsTab($server); ?>
</div>
<div class="tab-pane" id="tab-unlinked">
<?php Server::showUnlinkedClientsTab($server); ?>
</div>
<div class="tab-pane" id="tab-missing">
<?php Server::showMissingClientsTab($server); ?>
</div>
</div>
</div>
</div>
<?php
} else {
$server->showForm($ID);
} }
?>
<script>
$(function () {
var hash = window.location.hash;
if (hash) {
$('[data-bs-toggle="tab"][href="' + hash + '"]').tab('show');
}
$('[data-bs-toggle="tab"]').on('shown.bs.tab', function (e) {
window.location.hash = $(this).attr('href');
});
});
</script>
<?php
Html::footer(); Html::footer();
+2 -16
View File
@@ -15,27 +15,13 @@ if (!Profile::canCurrentUser(READ)) {
Html::displayRightError(); Html::displayRightError();
} }
$can_read = Profile::canCurrentUser(READ);
$can_create = Profile::canCurrentUser(CREATE);
Html::header( Html::header(
'UrBackup Servers', 'UrBackup Servers',
'', '',
'Assets', 'admin',
'' 'GlpiPlugin\Urbackup\Server'
); );
if ($can_create) {
echo "<div class='center'>";
echo Html::link(
__('Add UrBackup server', 'urbackup'),
'/plugins/urbackup/front/server.form.php',
['class' => 'btn btn-primary']
);
echo "</div>";
echo "<br>";
}
Search::show('GlpiPlugin\Urbackup\Server', [ Search::show('GlpiPlugin\Urbackup\Server', [
'is_deleted' => 0, 'is_deleted' => 0,
'massiveaction' => true, 'massiveaction' => true,
+35 -31
View File
@@ -63,7 +63,7 @@ class AssetTab extends CommonDBTM
return ''; return '';
} }
return self::createTabEntry(__('UrBackup', 'urbackup'), 0, null, 'ti ti-cloud-upload'); return self::createTabEntry(__('UrBackup', 'urbackup'), 0, null, 'ti ti-cloud-up');
} }
/** /**
@@ -116,8 +116,6 @@ class AssetTab extends CommonDBTM
*/ */
private static function showNoServerLinkedBlock(CommonDBTM $item): void private static function showNoServerLinkedBlock(CommonDBTM $item): void
{ {
$asset_location_id = LocationHelper::getAssetLocationId($item);
$root_location_id = LocationHelper::getRootLocationIdForAsset($item);
$is_sub_location = LocationHelper::assetIsInSubLocation($item); $is_sub_location = LocationHelper::assetIsInSubLocation($item);
$servers = LocationHelper::getAvailableServersForAsset($item); $servers = LocationHelper::getAvailableServersForAsset($item);
@@ -128,16 +126,6 @@ class AssetTab extends CommonDBTM
echo "<table class='tab_cadre_fixe'>"; echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='2'>" . htmlspecialchars(__('UrBackup server selection', 'urbackup')) . "</th></tr>"; echo "<tr><th colspan='2'>" . htmlspecialchars(__('UrBackup server selection', 'urbackup')) . "</th></tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Asset location ID', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars((string) $asset_location_id) . "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Root location ID', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars((string) $root_location_id) . "</td>";
echo "</tr>";
if ($is_sub_location) { if ($is_sub_location) {
echo "<tr class='tab_bg_1'>"; echo "<tr class='tab_bg_1'>";
echo "<td colspan='2'><em>"; echo "<td colspan='2'><em>";
@@ -336,15 +324,36 @@ class AssetTab extends CommonDBTM
): void { ): void {
echo "<div class='plugin-urbackup-inner-tabs'>"; echo "<div class='plugin-urbackup-inner-tabs'>";
echo "<h3>" . htmlspecialchars(__('State', 'urbackup')) . "</h3>"; echo '<ul class="nav nav-tabs" id="urbackupTabs">';
echo '<li class="nav-item">';
echo '<a class="nav-link active" id="state-tab" data-bs-toggle="tab" href="#state" role="tab">';
echo htmlspecialchars(__('State', 'urbackup'));
echo '</a></li>';
echo '<li class="nav-item">';
echo '<a class="nav-link" id="actions-tab" data-bs-toggle="tab" href="#actions" role="tab">';
echo htmlspecialchars(__('Actions', 'urbackup'));
echo '</a></li>';
echo '<li class="nav-item">';
echo '<a class="nav-link" id="logs-tab" data-bs-toggle="tab" href="#logs" role="tab">';
echo htmlspecialchars(__('Info / Log', 'urbackup'));
echo '</a></li>';
echo '</ul>';
echo '<div class="tab-content">';
echo '<div class="tab-pane fade show active" id="state" role="tabpanel">';
self::showStateSection($server, $link, $api_data); self::showStateSection($server, $link, $api_data);
echo '</div>';
echo "<h3>" . htmlspecialchars(__('Actions', 'urbackup')) . "</h3>"; echo '<div class="tab-pane fade" id="actions" role="tabpanel">';
self::showActionsSection($item, $server, $link, $api_data); self::showActionsSection($item, $server, $link, $api_data);
echo '</div>';
echo "<h3>" . htmlspecialchars(__('Info / Log', 'urbackup')) . "</h3>"; echo '<div class="tab-pane fade" id="logs" role="tabpanel">';
self::showInfoLogSection($api_data); self::showInfoLogSection($api_data);
echo '</div>';
echo '</div>';
echo "</div>"; echo "</div>";
} }
@@ -418,17 +427,6 @@ class AssetTab extends CommonDBTM
echo "</table>"; echo "</table>";
echo '<details class="mt-2" style="cursor:pointer;color:#666;font-size:0.85em">';
echo '<summary>' . htmlspecialchars(__('API raw data (debug)', 'urbackup')) . '</summary>';
echo '<pre style="max-height:300px;overflow:auto;background:#f5f5f5;padding:8px;border:1px solid #ddd;font-size:0.8em">';
echo "--- client_settings ---\n\n";
echo htmlspecialchars(json_encode($settings, JSON_PRETTY_PRINT));
echo "\n\n--- client_status ---\n\n";
echo htmlspecialchars(json_encode($status, JSON_PRETTY_PRINT));
echo "\n\n--- recent_backups ---\n\n";
echo htmlspecialchars(json_encode($api_data['recent_backups'] ?? [], JSON_PRETTY_PRINT));
echo '</pre>';
echo '</details>';
} }
/** /**
@@ -488,10 +486,20 @@ class AssetTab extends CommonDBTM
echo "<tr class='tab_bg_1'>"; echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Backup commands', 'urbackup')) . "</td>"; echo "<td>" . htmlspecialchars(__('Backup commands', 'urbackup')) . "</td>";
echo "<td>"; echo "<td>";
echo "<div style='display:flex;gap:24px'>";
echo "<div>";
echo "<strong>" . htmlspecialchars(__('File backup', 'urbackup')) . "</strong><br>";
self::showActionButton($item, 'incremental_file_backup', __('Incremental file backup', 'urbackup'), 'btn btn-secondary'); self::showActionButton($item, 'incremental_file_backup', __('Incremental file backup', 'urbackup'), 'btn btn-secondary');
echo "<br>";
self::showActionButton($item, 'full_file_backup', __('Full file backup', 'urbackup'), 'btn btn-secondary'); self::showActionButton($item, 'full_file_backup', __('Full file backup', 'urbackup'), 'btn btn-secondary');
echo "</div>";
echo "<div>";
echo "<strong>" . htmlspecialchars(__('Image backup', 'urbackup')) . "</strong><br>";
self::showActionButton($item, 'incremental_image_backup', __('Incremental image backup', 'urbackup'), 'btn btn-secondary'); self::showActionButton($item, 'incremental_image_backup', __('Incremental image backup', 'urbackup'), 'btn btn-secondary');
echo "<br>";
self::showActionButton($item, 'full_image_backup', __('Full image backup', 'urbackup'), 'btn btn-secondary'); self::showActionButton($item, 'full_image_backup', __('Full image backup', 'urbackup'), 'btn btn-secondary');
echo "</div>";
echo "</div>";
echo "</td>"; echo "</td>";
echo "</tr>"; echo "</tr>";
@@ -707,10 +715,6 @@ class AssetTab extends CommonDBTM
echo "<textarea name='default_dirs' rows='4' cols='80'>" . htmlspecialchars($display) . "</textarea><br>"; echo "<textarea name='default_dirs' rows='4' cols='80'>" . htmlspecialchars($display) . "</textarea><br>";
echo '<div style="color:#999;font-size:0.8em">';
echo 'raw: ' . htmlspecialchars(json_encode($raw));
echo '</div>';
echo Html::submit(__('Save'), [ echo Html::submit(__('Save'), [
'name' => 'execute', 'name' => 'execute',
'class' => 'btn btn-primary', 'class' => 'btn btn-primary',
+2 -4
View File
@@ -88,16 +88,14 @@ class AssetController
case 'set_internet_mode': case 'set_internet_mode':
if (Profile::canCurrentUser(UPDATE)) { if (Profile::canCurrentUser(UPDATE)) {
$internet_mode = (int) $request->request->get('internet_mode', 0); $enabled = (int) $request->request->get('internet_mode', 0) === 1;
$serverAsset = new ServerAsset();
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, true); $link = ServerAsset::getLinkForAsset($itemtype, $items_id, true);
if ($link) { if ($link) {
$server = new Server(); $server = new Server();
if ($server->getFromDB((int) $link['plugin_urbackup_servers_id'])) { if ($server->getFromDB((int) $link['plugin_urbackup_servers_id'])) {
$api = new UrbackupApiClient($server); $api = new UrbackupApiClient($server);
$client_name = (string) ($item->fields['name'] ?? ''); $client_name = (string) ($item->fields['name'] ?? '');
$setting_key = $api->getInternetModeSettingKey(); $api->saveInternetMode($client_name, $enabled);
$api->changeClientSetting($client_name, $setting_key, $internet_mode);
} }
} }
} }
+1 -1
View File
@@ -14,7 +14,7 @@ class Profile extends \Profile
public static function getIcon() public static function getIcon()
{ {
return 'ti ti-server'; return 'ti ti-cloud-up';
} }
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
+449 -10
View File
@@ -12,9 +12,12 @@ use CommonDBTM;
use CommonGLPI; use CommonGLPI;
use Dropdown; use Dropdown;
use Entity; use Entity;
use Group;
use Html; use Html;
use Location; use Location;
use Session; use Session;
use State;
use User;
class Server extends CommonDBTM class Server extends CommonDBTM
{ {
@@ -151,7 +154,7 @@ class Server extends CommonDBTM
if (self::canView()) { if (self::canView()) {
$menu['title'] = self::getMenuName(); $menu['title'] = self::getMenuName();
$menu['page'] = self::getSearchURL(false); $menu['page'] = self::getSearchURL(false);
$menu['icon'] = 'ti ti-server'; $menu['icon'] = 'ti ti-cloud-up';
$menu['links']['search'] = self::getSearchURL(false); $menu['links']['search'] = self::getSearchURL(false);
@@ -324,7 +327,14 @@ class Server extends CommonDBTM
$this->initForm($ID, $options); $this->initForm($ID, $options);
$this->showFormHeader($options); $this->showFormHeader($options);
$this->showFormFields($ID);
$this->showFormButtons($options);
return true;
}
public function showFormFields(?int $ID): void
{
echo "<tr class='tab_bg_1'>"; echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Name')) . "</td>"; echo "<td>" . htmlspecialchars(__('Name')) . "</td>";
echo "<td>"; echo "<td>";
@@ -464,7 +474,7 @@ class Server extends CommonDBTM
echo "</td>"; echo "</td>";
echo "</tr>"; echo "</tr>";
$apiStatus = (int) ($this->fields['last_api_status'] ?? 0); $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
echo "<tr class='tab_bg_1'>"; 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>" . htmlspecialchars(__('API connection status', 'urbackup')) . "<br><small class='text-muted'>" . htmlspecialchars(__('Click Save to test connection', 'urbackup')) . "</small></td>";
echo "<td colspan='3'>"; echo "<td colspan='3'>";
@@ -479,10 +489,6 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
echo "</td>"; echo "</td>";
echo "</tr>"; echo "</tr>";
} }
$this->showFormButtons($options);
return true;
} }
/** /**
@@ -542,6 +548,26 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
public function prepareInputForAdd(mixed $input): mixed public function prepareInputForAdd(mixed $input): mixed
{ {
if (!is_array($input)) {
return $input;
}
if (!isset($input['port']) || $input['port'] === '' || $input['port'] === null) {
$input['port'] = 55414;
}
if (!isset($input['protocol']) || $input['protocol'] === '') {
$input['protocol'] = 'http';
}
if (!isset($input['is_active']) || $input['is_active'] === '' || $input['is_active'] === null) {
$input['is_active'] = 1;
}
if (!isset($input['is_recursive']) || $input['is_recursive'] === '' || $input['is_recursive'] === null) {
$input['is_recursive'] = 0;
}
if (!isset($input['ignore_ssl']) || $input['ignore_ssl'] === '' || $input['ignore_ssl'] === null) {
$input['ignore_ssl'] = 0;
}
return $this->prepareInputForUpdate($input); return $this->prepareInputForUpdate($input);
} }
@@ -551,6 +577,16 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
return $input; return $input;
} }
if (isset($input['port']) && ($input['port'] === '' || $input['port'] === null)) {
$input['port'] = 55414;
}
if (isset($input['protocol']) && $input['protocol'] === '') {
$input['protocol'] = 'http';
}
if (isset($input['ignore_ssl']) && ($input['ignore_ssl'] === '' || $input['ignore_ssl'] === null)) {
$input['ignore_ssl'] = 0;
}
if (!empty($input['id']) && (int) $input['id'] > 0) { if (!empty($input['id']) && (int) $input['id'] > 0) {
$server = new self(); $server = new self();
if ($server->getFromDB((int) $input['id'])) { if ($server->getFromDB((int) $input['id'])) {
@@ -789,7 +825,6 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
echo '<thead>'; echo '<thead>';
echo '<tr>'; echo '<tr>';
echo '<th>' . htmlspecialchars(__('Asset', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Asset', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Name', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Client name', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Client name', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Version', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Version', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>';
@@ -840,8 +875,14 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
$itemUrl = $glpiItem ? $glpiItem->getLinkURL() : ''; $itemUrl = $glpiItem ? $glpiItem->getLinkURL() : '';
echo '<tr>'; echo '<tr>';
echo '<td>' . htmlspecialchars($itemTypeLabel . ' #' . $link['items_id']) . '</td>'; echo '<td>';
echo '<td>' . ($itemUrl ? '<a href="' . htmlspecialchars($itemUrl) . '">' . htmlspecialchars($clientName) . '</a>' : htmlspecialchars($clientName)) . '</td>'; if ($itemUrl) {
echo '<a href="' . htmlspecialchars($itemUrl) . '">' . htmlspecialchars($clientName) . '</a>';
} else {
echo htmlspecialchars($clientName);
}
echo ' <small class="text-muted">(' . htmlspecialchars($itemTypeLabel) . ')</small>';
echo '</td>';
echo '<td>' . htmlspecialchars($urbackupClientName) . '</td>'; echo '<td>' . htmlspecialchars($urbackupClientName) . '</td>';
echo '<td>' . htmlspecialchars($clientVersion) . '</td>'; echo '<td>' . htmlspecialchars($clientVersion) . '</td>';
echo '<td>' . $statusHtml . '</td>'; echo '<td>' . $statusHtml . '</td>';
@@ -904,6 +945,51 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
return; 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 '<table class="table table-striped table-hover">';
echo '<thead>'; echo '<thead>';
echo '<tr>'; echo '<tr>';
@@ -912,11 +998,15 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Status', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Last backup', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('Last backup', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('IP address', 'urbackup')) . '</th>'; echo '<th>' . htmlspecialchars(__('IP address', 'urbackup')) . '</th>';
echo '<th>' . htmlspecialchars(__('Actions', 'urbackup')) . '</th>';
echo '</tr>'; echo '</tr>';
echo '</thead>'; echo '</thead>';
echo '<tbody>'; echo '<tbody>';
foreach ($unlinkedClients as $uc) { foreach ($unlinkedClients as $uc) {
$clientName = (string) ($uc['name'] ?? 'Unknown');
$clientNameLower = strtolower($clientName);
$online = $uc['online'] ?? null; $online = $uc['online'] ?? null;
$apiStatusString = $uc['status'] ?? ''; $apiStatusString = $uc['status'] ?? '';
$statusHtml = self::renderOnlineBadge($online, $apiStatusString); $statusHtml = self::renderOnlineBadge($online, $apiStatusString);
@@ -928,15 +1018,364 @@ $apiStatus = (int) ($this->fields['last_api_status'] ?? 0);
$clientVersion = $uc['client_version_string'] ?? $uc['client_version'] ?? '-'; $clientVersion = $uc['client_version_string'] ?? $uc['client_version'] ?? '-';
echo '<tr>'; echo '<tr>';
echo '<td>' . htmlspecialchars((string) ($uc['name'] ?? 'Unknown')) . '</td>'; echo '<td>' . htmlspecialchars($clientName) . '</td>';
echo '<td>' . htmlspecialchars($clientVersion) . '</td>'; echo '<td>' . htmlspecialchars($clientVersion) . '</td>';
echo '<td>' . $statusHtml . '</td>'; echo '<td>' . $statusHtml . '</td>';
echo '<td>' . htmlspecialchars($lastBackup ?: '-') . '</td>'; echo '<td>' . htmlspecialchars($lastBackup ?: '-') . '</td>';
echo '<td>' . htmlspecialchars($clientIp ?: '-') . '</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 '</tr>';
} }
echo '</tbody>'; echo '</tbody>';
echo '</table>'; echo '</table>';
} }
public static function showMissingClientsTab(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;
}
$serverLocationId = (int) ($server->fields['locations_id'] ?? 0);
if ($serverLocationId <= 0) {
echo '<div class="alert alert-info">';
echo htmlspecialchars(__('No location configured for this server.', 'urbackup'));
echo '</div>';
return;
}
$rootLocationId = LocationHelper::getRootLocationId($serverLocationId);
$linkedIterator = $DB->request([
'FROM' => ServerAsset::getTable(),
]);
$linkedAssetKeys = [];
foreach ($linkedIterator as $row) {
$linkedAssetKeys[$row['itemtype'] . ':' . $row['items_id']] = true;
}
try {
$client = new UrbackupApiClient($server);
$urbackupClients = $client->getStatus();
} catch (\Throwable $e) {
echo '<div class="alert alert-danger">';
echo htmlspecialchars($e->getMessage());
echo '</div>';
return;
}
$urbackupClientNames = [];
foreach ($urbackupClients as $uc) {
$name = strtolower((string) ($uc['name'] ?? $uc['clientname'] ?? $uc['hostname'] ?? ''));
if ($name !== '') {
$urbackupClientNames[$name] = true;
}
}
$cacheEntity = [];
$cacheLocation = [];
$cacheState = [];
$cacheUser = [];
$cacheGroup = [];
$itemtypes = Config::getEnabledItemtypes();
$missingAssets = [];
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;
}
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => [
'is_deleted' => 0,
],
]);
foreach ($iterator as $assetRow) {
$name = (string) ($assetRow['name'] ?? '');
if ($name === '') {
continue;
}
$nameLower = strtolower($name);
$assetLocationId = (int) ($assetRow['locations_id'] ?? 0);
$assetRootLocationId = LocationHelper::getRootLocationId($assetLocationId);
if ($assetRootLocationId !== $rootLocationId) {
continue;
}
$key = $itemtype . ':' . $assetRow['id'];
$assetId = (int) $assetRow['id'];
if (isset($linkedAssetKeys[$key])) {
continue;
}
if (isset($urbackupClientNames[$nameLower])) {
continue;
}
$entityName = self::getCachedName('Entity', (int) ($assetRow['entities_id'] ?? 0), $cacheEntity);
$locationName = self::getCachedLocationName($assetLocationId, $cacheLocation);
$stateName = self::getCachedName('State', (int) ($assetRow['states_id'] ?? 0), $cacheState);
$userName = self::getCachedName('User', (int) ($assetRow['users_id'] ?? 0), $cacheUser);
$groupName = self::getAssetGroupName($itemtype, $assetId, $cacheGroup);
$ip = self::getAssetIp($itemtype, $assetId);
$missingAssets[] = [
'itemtype' => $itemtype,
'items_id' => $assetId,
'name' => $name,
'entity' => $entityName,
'location' => $locationName,
'otherserial' => (string) ($assetRow['otherserial'] ?? ''),
'ip' => $ip,
'state' => $stateName,
'user' => $userName,
'group' => $groupName,
];
}
}
if (count($missingAssets) === 0) {
echo '<div class="alert alert-success">';
echo htmlspecialchars(__('All assets in this location are linked or already on the UrBackup server.', 'urbackup'));
echo '</div>';
return;
}
echo '<div class="d-flex justify-content-end mb-2">';
echo '<input type="text" id="missing-search" class="form-control form-control-sm" placeholder="' . htmlspecialchars(__('Search...', 'urbackup')) . '" style="width:250px">';
echo '</div>';
$formAction = PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php';
echo '<table id="missing-table" class="table table-striped table-hover">';
echo '<thead><tr>';
echo '<th class="sortable" data-col="0">' . htmlspecialchars(__('Name')) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="1">' . htmlspecialchars(Entity::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="2">' . htmlspecialchars(Location::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="3">' . htmlspecialchars(__('Inventory number')) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="4">' . htmlspecialchars(__('IP address', 'urbackup')) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="5">' . htmlspecialchars(State::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="6">' . htmlspecialchars(User::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th class="sortable" data-col="7">' . htmlspecialchars(Group::getTypeName(1)) . ' <span class="sort-arrow"></span></th>';
echo '<th data-col="8">' . htmlspecialchars(__('Actions', 'urbackup')) . '</th>';
echo '</tr></thead>';
echo '<tbody>';
foreach ($missingAssets as $asset) {
$item = new $asset['itemtype']();
$itemLink = '';
if ($item instanceof CommonDBTM && $item->getFromDB($asset['items_id'])) {
$itemLink = $item->getLinkURL();
}
echo '<tr>';
echo '<td>';
if ($itemLink !== '') {
echo '<a href="' . htmlspecialchars($itemLink) . '">' . htmlspecialchars($asset['name']) . '</a>';
} else {
echo htmlspecialchars($asset['name']);
}
echo '</td>';
echo '<td>' . htmlspecialchars($asset['entity']) . '</td>';
echo '<td>' . htmlspecialchars($asset['location']) . '</td>';
echo '<td>' . htmlspecialchars($asset['otherserial']) . '</td>';
echo '<td>' . htmlspecialchars($asset['ip']) . '</td>';
echo '<td>' . htmlspecialchars($asset['state']) . '</td>';
echo '<td>' . htmlspecialchars($asset['user']) . '</td>';
echo '<td>' . htmlspecialchars($asset['group']) . '</td>';
echo '<td>';
echo '<form method="post" action="' . htmlspecialchars($formAction) . '" class="d-inline">';
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::hidden('itemtype', ['value' => $asset['itemtype']]);
echo Html::hidden('items_id', ['value' => $asset['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();
echo '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo <<<'JAVASCRIPT'
<script>
$(function () {
var sortDir = {};
$('#missing-table th.sortable').on('click', function () {
var col = parseInt($(this).data('col'));
var $table = $('#missing-table');
var $tbody = $table.find('tbody');
var rows = $tbody.find('tr').get();
var dir = sortDir[col] === 'asc' ? 'desc' : 'asc';
sortDir[col] = dir;
rows.sort(function (a, b) {
var aVal = $(a).children('td').eq(col).text().trim().toLowerCase();
var bVal = $(b).children('td').eq(col).text().trim().toLowerCase();
if (aVal < bVal) return dir === 'asc' ? -1 : 1;
if (aVal > bVal) return dir === 'asc' ? 1 : -1;
return 0;
});
$.each(rows, function (i, row) { $tbody.append(row); });
$table.find('th .sort-arrow').text('');
$(this).find('.sort-arrow').text(dir === 'asc' ? '\u25B2' : '\u25BC');
});
$('#missing-search').on('keyup', function () {
var val = $(this).val().toLowerCase();
$('#missing-table tbody tr').each(function () {
var match = false;
$(this).children('td').each(function () {
if ($(this).text().toLowerCase().indexOf(val) > -1) {
match = true;
return false;
}
});
$(this).toggle(match);
});
});
});
</script>
JAVASCRIPT;
}
private static function getCachedName(string $classname, int $id, array &$cache): string
{
if ($id <= 0) {
return '';
}
if (!isset($cache[$id])) {
$fqcn = '\\' . ltrim($classname, '\\');
$obj = new $fqcn();
if ($obj->getFromDB($id)) {
$cache[$id] = (string) ($obj->fields['completename'] ?? $obj->fields['name'] ?? '');
} else {
$cache[$id] = '';
}
}
return $cache[$id];
}
private static function getAssetGroupName(string $itemtype, int $items_id, array &$cache): string
{
global $DB;
$iterator = $DB->request([
'FROM' => 'glpi_groups_items',
'WHERE' => [
'itemtype' => $itemtype,
'items_id' => $items_id,
'type' => \Group_Item::GROUP_TYPE_NORMAL,
],
'LIMIT' => 1,
]);
foreach ($iterator as $row) {
$groupId = (int) ($row['groups_id'] ?? 0);
if ($groupId > 0) {
return self::getCachedName('Group', $groupId, $cache);
}
}
return '';
}
private static function getCachedLocationName(int $id, array &$cache): string
{
if ($id <= 0) {
return '';
}
if (!isset($cache[$id])) {
$obj = new Location();
if ($obj->getFromDB($id)) {
$cache[$id] = (string) ($obj->fields['completename'] ?? $obj->fields['name'] ?? '');
} else {
$cache[$id] = '';
}
}
return $cache[$id];
}
private static function getAssetIp(string $itemtype, int $items_id): string
{
global $DB;
$iterator = $DB->request([
'SELECT' => ['ipa.name'],
'FROM' => 'glpi_ipaddresses AS ipa',
'INNER JOIN' => [
'glpi_networknames AS nn' => [
'ON' => [
'nn' => 'items_id',
'ipa' => 'id',
['AND' => ['ipa.itemtype' => 'NetworkName']],
],
],
'glpi_networkports AS np' => [
'ON' => [
'np' => 'id',
'nn' => 'items_id',
['AND' => ['nn.itemtype' => 'NetworkPort']],
],
],
],
'WHERE' => [
'np.itemtype' => $itemtype,
'np.items_id' => $items_id,
],
'LIMIT' => 1,
]);
foreach ($iterator as $row) {
$ip = (string) ($row['name'] ?? '');
if ($ip !== '') {
return $ip;
}
}
return '';
}
} }
+6 -11
View File
@@ -37,7 +37,7 @@ class UrbackupApiClient
private string $server_version = ''; private string $server_version = '';
private bool $is_version_2_5_or_higher = false; private bool $is_version_2_4_or_higher = false;
/** /**
* Constructor. * Constructor.
@@ -52,15 +52,10 @@ class UrbackupApiClient
$this->password = (string) ($server->fields['api_password'] ?? ''); $this->password = (string) ($server->fields['api_password'] ?? '');
$this->ignore_ssl = ((int) ($server->fields['ignore_ssl'] ?? 0)) === 1; $this->ignore_ssl = ((int) ($server->fields['ignore_ssl'] ?? 0)) === 1;
$this->server_version = (string) ($server->fields['server_version'] ?? ''); $this->server_version = (string) ($server->fields['server_version'] ?? '');
$this->is_version_2_5_or_higher = $this->detectVersion2_5OrHigher(); $this->is_version_2_4_or_higher = $this->detectVersion2_4OrHigher();
} }
/** private function detectVersion2_4OrHigher(): bool
* Detect if server version is 2.5 or higher.
*
* @return bool
*/
private function detectVersion2_5OrHigher(): bool
{ {
if ($this->server_version === '') { if ($this->server_version === '') {
return false; return false;
@@ -77,7 +72,7 @@ class UrbackupApiClient
if ($major > 2) { if ($major > 2) {
return true; return true;
} }
if ($major === 2 && $minor >= 5) { if ($major === 2 && $minor >= 4) {
return true; return true;
} }
@@ -91,7 +86,7 @@ class UrbackupApiClient
*/ */
public function getInternetModeSettingKey(): string public function getInternetModeSettingKey(): string
{ {
return $this->is_version_2_5_or_higher ? 'internet_mode_enabled' : 'internet_mode'; return $this->is_version_2_4_or_higher ? 'internet_mode_enabled' : 'internet_mode';
} }
/** /**
@@ -391,7 +386,7 @@ class UrbackupApiClient
'sa' => 'clientsettings_save', 'sa' => 'clientsettings_save',
't_clientid' => $client_id, 't_clientid' => $client_id,
'overwrite' => 'true', 'overwrite' => 'true',
$key => $enabled ? '1' : '0', $key => $enabled ? 'true' : 'false',
]; ];
$data = $this->apiAction('settings', $params); $data = $this->apiAction('settings', $params);
-8
View File
@@ -7,14 +7,6 @@
<tr> <tr>
<th colspan="2">{{ __('UrBackup server selection', 'urbackup') }}</th> <th colspan="2">{{ __('UrBackup server selection', 'urbackup') }}</th>
</tr> </tr>
<tr class="tab_bg_1">
<td>{{ __('Asset location ID', 'urbackup') }}</td>
<td>{{ asset_location_id }}</td>
</tr>
<tr class="tab_bg_1">
<td>{{ __('Root location ID', 'urbackup') }}</td>
<td>{{ root_location_id }}</td>
</tr>
{% if is_sub_location %} {% if is_sub_location %}
<tr class="tab_bg_1"> <tr class="tab_bg_1">
<td colspan="2"><em> <td colspan="2"><em>