This commit is contained in:
mariano
2026-05-20 09:20:27 +02:00
commit 1dc84aa5eb
199 changed files with 8444 additions and 0 deletions
+913
View File
@@ -0,0 +1,913 @@
<?php
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup;
use CommonDBTM;
use CommonGLPI;
use Dropdown;
use Html;
use Session;
use Throwable;
class AssetTab extends CommonDBTM
{
public static $rightname = 'plugin_urbackup';
/**
* Extract actual value from a UrBackup setting struct.
*
* API returns settings as: {"use":N, "value":..., "value_client":..., "value_group":...}
*/
private static function extractSettingValue(mixed $setting, mixed $default = null): mixed
{
if (is_array($setting) && array_key_exists('value', $setting)) {
return $setting['value'];
}
return $setting ?? $default;
}
/**
* Get type name.
*
* @param int $nb Number
*
* @return string
*/
public static function getTypeName($nb = 0): string
{
return __('UrBackup', 'urbackup');
}
/**
* Get tab name for item.
*
* @param CommonGLPI $item Item
* @param int $withtemplate With template
*
* @return string
*/
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0): string
{
if (!Config::isItemtypeEnabled($item::class)) {
return '';
}
if (!Profile::canCurrentUser(READ)) {
return '';
}
return self::createTabEntry(__('UrBackup', 'urbackup'), 0, null, 'ti ti-cloud-upload');
}
/**
* Display tab content for item.
*
* @param CommonGLPI $item Item
* @param int $tabnum Tab number
* @param int $withtemplate With template
*
* @return bool
*/
public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0): bool
{
if (!Config::isItemtypeEnabled($item::class)) {
return false;
}
if (!Profile::canCurrentUser(READ)) {
echo "<div class='alert alert-warning'>";
echo htmlspecialchars(__('You do not have permission to view UrBackup information.', 'urbackup'));
echo "</div>";
return true;
}
$itemtype = $item::class;
$items_id = (int) ($item->fields['id'] ?? 0);
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, true);
echo "<div class='plugin-urbackup-asset-tab'>";
if ($link === null) {
self::showNoServerLinkedBlock($item);
} else {
self::showServerLinkedBlock($item, $link);
}
echo "</div>";
return true;
}
/**
* Show block when no server is linked.
*
* @param CommonDBTM $item Asset item
*
* @return 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);
$servers = LocationHelper::getAvailableServersForAsset($item);
echo "<div class='alert alert-info'>";
echo htmlspecialchars(__('No UrBackup server linked.', 'urbackup'));
echo "</div>";
echo "<table class='tab_cadre_fixe'>";
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) {
echo "<tr class='tab_bg_1'>";
echo "<td colspan='2'><em>";
echo htmlspecialchars(
__('The asset is in a sub-location. The plugin will use the server assigned to the root location.', 'urbackup')
);
echo "</em></td>";
echo "</tr>";
}
if (count($servers) === 0) {
echo "<tr class='tab_bg_1'>";
echo "<td colspan='2'>";
echo "<div class='alert alert-warning'>";
echo htmlspecialchars(__('No UrBackup server available for the root location of this asset.', 'urbackup'));
echo "</div>";
echo "</td>";
echo "</tr>";
echo "</table>";
return;
}
if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) {
echo "<tr class='tab_bg_1'>";
echo "<td colspan='2'>";
echo htmlspecialchars(__('A server is available, but you do not have permission to link this asset.', 'urbackup'));
echo "</td>";
echo "</tr>";
echo "</table>";
return;
}
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Available servers for root location', 'urbackup')) . "</td>";
echo "<td>";
echo "<form method='post' action='" . htmlspecialchars(PLUGIN_URBACKUP_WEB_DIR . "/front/asset.form.php") . "'>";
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::hidden('itemtype', ['value' => $item::class]);
echo Html::hidden('items_id', ['value' => (int) $item->fields['id']]);
Dropdown::showFromArray(
'plugin_urbackup_servers_id',
$servers,
[
'value' => 0,
]
);
echo Html::submit(__('Connect', 'urbackup'), [
'name' => 'connect',
'class' => 'btn btn-primary',
]);
Html::closeForm();
echo "</td>";
echo "</tr>";
echo "</table>";
}
/**
* Show block when server is linked.
*
* @param CommonDBTM $item Asset item
* @param array<string, mixed> $link Link row
*
* @return void
*/
private static function showServerLinkedBlock(CommonDBTM $item, array $link): void
{
$server = new Server();
$server_id = (int) ($link['plugin_urbackup_servers_id'] ?? 0);
if ($server_id <= 0 || !$server->getFromDB($server_id)) {
echo "<div class='alert alert-danger'>";
echo htmlspecialchars(__('The linked UrBackup server no longer exists.', 'urbackup'));
echo "</div>";
return;
}
$api_data = self::loadApiData($item, $server, $link);
echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='4'>" . htmlspecialchars(__('UrBackup status', 'urbackup')) . "</th></tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Linked server', 'urbackup')) . "</td>";
echo "<td>" . $server->getLink() . "</td>";
echo "<td>" . htmlspecialchars(__('IP address', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars((string) $server->fields['ip_address']) . "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('UrBackup server version', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars((string) ($server->fields['server_version'] ?? '')) . "</td>";
echo "<td>" . htmlspecialchars(__('Client name', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars(ServerAsset::getAssetName($item::class, (int) $item->fields['id'])) . "</td>";
echo "</tr>";
echo "</table>";
if ($api_data['error'] !== '') {
echo "<div class='alert alert-warning'>";
echo htmlspecialchars($api_data['error']);
echo "</div>";
}
if ($api_data['ip_warning'] !== '') {
echo "<div class='alert alert-warning'>";
echo htmlspecialchars($api_data['ip_warning']);
echo "</div>";
}
self::showInternalTabs($item, $server, $link, $api_data);
}
/**
* Load API data for asset tab.
*
* @param CommonDBTM $item Asset
* @param Server $server Server
* @param array<string, mixed> $link Link
*
* @return array<string, mixed>
*/
private static function loadApiData(CommonDBTM $item, Server $server, array $link): array
{
$client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']);
$asset_ip = ServerAsset::extractAssetIp($item);
$data = [
'client_found' => false,
'client_status' => [],
'client_settings' => [],
'authkey' => '',
'recent_backups' => [],
'logs' => [],
'error' => '',
'ip_warning' => '',
];
try {
$api = new UrbackupApiClient($server);
$client_status = $api->getClientStatusByName($client_name);
if ($client_status !== null) {
$data['client_found'] = true;
$data['client_status'] = $client_status;
$urbackup_ip = (string) (
$client_status['ip'] ??
$client_status['ip_address'] ??
$client_status['addr'] ??
''
);
if ($asset_ip !== '' && $urbackup_ip !== '' && $asset_ip !== $urbackup_ip) {
$data['ip_warning'] = sprintf(
__('Client name matches, but GLPI IP "%s" differs from UrBackup IP "%s".', 'urbackup'),
$asset_ip,
$urbackup_ip
);
}
$data['client_settings'] = $api->getClientSettings($client_name);
$data['authkey'] = $api->getClientAuthKey($client_name);
$data['recent_backups'] = $api->getRecentBackups($client_name, 10);
$data['logs'] = $api->getClientLogs($client_name, 50);
}
} catch (Throwable $e) {
$data['error'] = $e->getMessage();
}
return $data;
}
/**
* Show internal sections.
*
* @param CommonDBTM $item Asset
* @param Server $server Server
* @param array<string, mixed> $link Link
* @param array<string, mixed> $api_data API data
*
* @return void
*/
private static function showInternalTabs(
CommonDBTM $item,
Server $server,
array $link,
array $api_data
): void {
echo "<div class='plugin-urbackup-inner-tabs'>";
echo "<h3>" . htmlspecialchars(__('State', 'urbackup')) . "</h3>";
self::showStateSection($server, $link, $api_data);
echo "<h3>" . htmlspecialchars(__('Actions', 'urbackup')) . "</h3>";
self::showActionsSection($item, $server, $link, $api_data);
echo "<h3>" . htmlspecialchars(__('Info / Log', 'urbackup')) . "</h3>";
self::showInfoLogSection($api_data);
echo "</div>";
}
/**
* Show state section.
*
* @param Server $server Server
* @param array<string, mixed> $link Link
* @param array<string, mixed> $api_data API data
*
* @return void
*/
private static function showStateSection(Server $server, array $link, array $api_data): void
{
$status = $api_data['client_status'];
$settings = $api_data['client_settings'];
echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='2'>" . htmlspecialchars(__('Client state', 'urbackup')) . "</th></tr>";
if (!$api_data['client_found']) {
echo "<tr class='tab_bg_1'>";
echo "<td colspan='2'>";
echo htmlspecialchars(__('Client not found on UrBackup server.', 'urbackup'));
echo "</td>";
echo "</tr>";
echo "</table>";
return;
}
$internetMode = self::extractSettingValue($settings['internet_mode_enabled'] ?? $settings['internet_mode'] ?? null, 0);
$internetModeDisplay = ((int) $internetMode === 1)
? '<span class="badge bg-success">' . __('Yes', 'urbackup') . '</span>'
: '<span class="badge bg-secondary">' . __('No', 'urbackup') . '</span>';
$rows = [
__('Client version', 'urbackup') => $status['client_version_string'] ?? $status['client_version'] ?? $status['version'] ?? '-',
__('Online / Offline', 'urbackup') => self::formatOnlineStatus($status),
__('Last file backup', 'urbackup') => self::formatTimestamp($status['file_lastbackup'] ?? $status['lastbackup'] ?? $status['last_file_backup'] ?? ''),
__('Last image backup', 'urbackup') => self::formatTimestamp($status['image_lastbackup'] ?? $status['lastbackup_image'] ?? $status['last_image_backup'] ?? ''),
__('Last file backup result', 'urbackup') => self::formatBoolStatus($status['file_ok'] ?? null),
__('Last image backup result', 'urbackup') => self::formatBoolStatus($status['image_ok'] ?? null),
__('Current activities', 'urbackup') => $status['status'] ?? $status['activity'] ?? '-',
__('Internet mode', 'urbackup') => $internetModeDisplay,
];
foreach ($rows as $label => $value) {
$displayValue = is_array($value) ? json_encode($value) : (string) $value;
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars((string) $label) . "</td>";
echo "<td>" . $displayValue . "</td>";
echo "</tr>";
}
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Internet authentication key', 'urbackup')) . "</td>";
echo "<td>";
$authKey = (string) $api_data['authkey'];
if ($authKey === '') {
echo '<span class="text-muted">-</span>';
} else {
echo '<input type="password" class="form-control" value="' . htmlspecialchars($authKey) . '" readonly id="urbackup-authkey">';
echo '<div class="form-check mt-2">';
echo '<input type="checkbox" class="form-check-input" id="urbackup-show-key" onchange="document.getElementById(\'urbackup-authkey\').type = this.checked ? \'text\' : \'password\'">';
echo '<label class="form-check-label" for="urbackup-show-key">' . __('Show', 'urbackup') . '</label>';
echo '</div>';
}
echo "</td>";
echo "</tr>";
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>';
}
/**
* Show actions section.
*
* @param CommonDBTM $item Asset
* @param Server $server Server
* @param array<string, mixed> $link Link
* @param array<string, mixed> $api_data API data
*
* @return void
*/
private static function showActionsSection(
CommonDBTM $item,
Server $server,
array $link,
array $api_data
): void {
echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='2'>" . htmlspecialchars(__('Available actions', 'urbackup')) . "</th></tr>";
if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) {
echo "<tr class='tab_bg_1'>";
echo "<td colspan='2'>";
echo htmlspecialchars(__('You do not have permission for UrBackup actions.', 'urbackup'));
echo "</td>";
echo "</tr>";
echo "</table>";
return;
}
if (!$api_data['client_found'] && Profile::canCurrentUser(CREATE)) {
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Create client in UrBackup', 'urbackup')) . "</td>";
echo "<td>";
self::showActionButton($item, 'create_client', __('Create client in UrBackup', 'urbackup'), 'btn btn-primary');
echo "</td>";
echo "</tr>";
}
if (Profile::canCurrentUser(UPDATE)) {
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Internet mode', 'urbackup')) . "</td>";
echo "<td>";
self::showInternetModeForm($item, $api_data);
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Default directories', 'urbackup')) . "</td>";
echo "<td>";
self::showDefaultDirsForm($item, $api_data);
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Backup commands', 'urbackup')) . "</td>";
echo "<td>";
self::showActionButton($item, 'incremental_file_backup', __('Incremental file backup', 'urbackup'), 'btn btn-secondary');
self::showActionButton($item, 'full_file_backup', __('Full file backup', 'urbackup'), 'btn btn-secondary');
self::showActionButton($item, 'incremental_image_backup', __('Incremental image backup', 'urbackup'), 'btn btn-secondary');
self::showActionButton($item, 'full_image_backup', __('Full image backup', 'urbackup'), 'btn btn-secondary');
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Disconnect client', 'urbackup')) . "</td>";
echo "<td>";
self::showDisconnectButton($item);
echo "</td>";
echo "</tr>";
}
if (Profile::canCurrentUser(PURGE)) {
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('Delete client from UrBackup server', 'urbackup')) . "</td>";
echo "<td>";
echo "<div class='alert alert-warning'>";
echo htmlspecialchars(
__('The client deletion will be queued on the UrBackup server and may require up to 24 hours.', 'urbackup')
);
echo "</div>";
self::showActionButton($item, 'delete_client', __('Delete client from UrBackup server', 'urbackup'), 'btn btn-danger');
echo "</td>";
echo "</tr>";
}
echo "</table>";
}
/**
* Show info/log section.
*
* @param array<string, mixed> $api_data API data
*
* @return void
*/
private static function formatBytes(mixed $bytes): string
{
$bytes = (float) ($bytes ?? 0);
if ($bytes <= 0) {
return '-';
}
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = floor(log($bytes, 1024));
$i = min((int) $i, count($units) - 1);
return sprintf('%.1f %s', $bytes / pow(1024, $i), $units[$i]);
}
/**
* Show info/log section.
*
* @param array<string, mixed> $api_data API data
*
* @return void
*/
private static function showInfoLogSection(array $api_data): void
{
$recent_backups = $api_data['recent_backups'] ?? [];
usort($recent_backups, function ($a, $b) {
$tsA = (int) ($a['time'] ?? $a['backuptime'] ?? $a['backup_time'] ?? $a['created'] ?? $a['start_time'] ?? 0);
$tsB = (int) ($b['time'] ?? $b['backuptime'] ?? $b['backup_time'] ?? $b['created'] ?? $b['start_time'] ?? 0);
return $tsB - $tsA;
});
echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='6'>" . htmlspecialchars(__('Recent backups', 'urbackup')) . "</th></tr>";
echo "<tr>";
echo "<th>" . htmlspecialchars(__('Type')) . "</th>";
echo "<th>" . htmlspecialchars(__('Date')) . "</th>";
echo "<th>" . htmlspecialchars(__('Result')) . "</th>";
echo "<th>" . htmlspecialchars(__('Size')) . "</th>";
echo "<th>" . htmlspecialchars(__('Incremental', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Backup ID', 'urbackup')) . "</th>";
echo "</tr>";
foreach ($recent_backups as $backup) {
$incremental = (int) ($backup['incremental'] ?? 0);
$timestamp = $backup['time'] ?? $backup['backuptime'] ?? $backup['backup_time'] ?? $backup['created'] ?? $backup['start_time'] ?? 0;
$dateFormatted = self::formatTimestamp($timestamp);
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars((string) ($backup['backup_type'] ?? '')) . "</td>";
echo "<td>" . htmlspecialchars($dateFormatted) . "</td>";
echo "<td><span class='text-success'><i class='ti ti-circle-check'></i> " . htmlspecialchars(__('Success', 'urbackup')) . "</span></td>";
echo "<td>" . htmlspecialchars(self::formatBytes($backup['size'] ?? $backup['size_bytes'] ?? 0)) . "</td>";
echo "<td>" . htmlspecialchars($incremental === 1 ? __('Yes', 'urbackup') : __('No', 'urbackup')) . "</td>";
echo "<td><code>" . htmlspecialchars((string) ($backup['backupid'] ?? $backup['id'] ?? '-')) . "</code></td>";
echo "</tr>";
}
if (count($api_data['recent_backups']) === 0) {
echo "<tr class='tab_bg_1'><td colspan='6'>";
echo htmlspecialchars(__('No recent backup information available.', 'urbackup'));
echo "</td></tr>";
}
echo "</table>";
echo "<br>";
$logs = array_reverse($api_data['logs'] ?? []);
echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='3'>" . htmlspecialchars(__('Client logs', 'urbackup')) . "</th></tr>";
echo "<tr>";
echo "<th>" . htmlspecialchars(__('Date')) . "</th>";
echo "<th>" . htmlspecialchars(__('Level')) . "</th>";
echo "<th>" . htmlspecialchars(__('Message')) . "</th>";
echo "</tr>";
foreach ($logs as $log) {
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(self::formatTimestamp($log['time'] ?? $log['created'] ?? '')) . "</td>";
echo "<td>" . htmlspecialchars((string) ($log['level'] ?? $log['severity'] ?? '')) . "</td>";
echo "<td>" . htmlspecialchars((string) ($log['message'] ?? $log['msg'] ?? $log['text'] ?? '')) . "</td>";
echo "</tr>";
}
if (count($api_data['logs']) === 0) {
echo "<tr class='tab_bg_1'><td colspan='3'>";
echo htmlspecialchars(__('No client logs available.', 'urbackup'));
echo "</td></tr>";
}
echo "</table>";
}
/**
* Show generic UrBackup action button.
*
* @param CommonDBTM $item Asset
* @param string $action Action
* @param string $label Button label
* @param string $class CSS class
*
* @return void
*/
private static function showActionButton(CommonDBTM $item, string $action, string $label, string $class): void
{
echo "<form method='post' action='" . htmlspecialchars(PLUGIN_URBACKUP_WEB_DIR . "/front/asset.form.php") . "' class='plugin-urbackup-inline-form'>";
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::hidden('itemtype', ['value' => $item::class]);
echo Html::hidden('items_id', ['value' => (int) $item->fields['id']]);
echo Html::hidden('urbackup_action', ['value' => $action]);
echo Html::submit($label, [
'name' => 'execute',
'class' => $class,
]);
Html::closeForm();
echo " ";
}
/**
* Show internet mode form.
*
* @param CommonDBTM $item Asset
* @param array<string, mixed> $api_data API data
*
* @return void
*/
private static function showInternetModeForm(CommonDBTM $item, array $api_data): void
{
$settings = $api_data['client_settings'];
$current = (int) self::extractSettingValue(
$settings['internet_mode_enabled'] ?? $settings['internet_mode'] ?? null,
0
);
echo "<form method='post' action='" . htmlspecialchars(PLUGIN_URBACKUP_WEB_DIR . "/front/asset.form.php") . "' class='plugin-urbackup-inline-form'>";
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::hidden('itemtype', ['value' => $item::class]);
echo Html::hidden('items_id', ['value' => (int) $item->fields['id']]);
echo Html::hidden('urbackup_action', ['value' => 'set_internet_mode']);
echo Html::hidden('execute', ['value' => '1']);
echo Html::hidden('internet_mode', ['value' => '0']);
echo '<div class="form-check form-switch">';
echo '<input class="form-check-input" type="checkbox" name="internet_mode" value="1" id="internet-mode-toggle" role="switch"'
. ($current ? ' checked' : '')
. ' onchange="this.form.submit()">';
echo '<label class="form-check-label" for="internet-mode-toggle">'
. htmlspecialchars(__('Enable Internet mode', 'urbackup'))
. '</label>';
echo '</div>';
Html::closeForm();
}
/**
* Show default directories form.
*
* @param CommonDBTM $item Asset
* @param array<string, mixed> $api_data API data
*
* @return void
*/
private static function showDefaultDirsForm(CommonDBTM $item, array $api_data): void
{
$settings = $api_data['client_settings'];
$raw = $settings['default_dirs'] ?? '';
$display = '';
if (is_array($raw)) {
// API returns struct: {"use":N,"value":"paths","value_client":"","value_group":""}
$display = (string) ($raw['value'] ?? '');
} else {
$display = (string) $raw;
}
echo "<form method='post' action='" . htmlspecialchars(PLUGIN_URBACKUP_WEB_DIR . "/front/asset.form.php") . "'>";
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::hidden('itemtype', ['value' => $item::class]);
echo Html::hidden('items_id', ['value' => (int) $item->fields['id']]);
echo Html::hidden('urbackup_action', ['value' => 'set_default_dirs']);
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'), [
'name' => 'execute',
'class' => 'btn btn-primary',
]);
Html::closeForm();
}
/**
* Show disconnect button.
*
* @param CommonDBTM $item Asset
*
* @return void
*/
private static function showDisconnectButton(CommonDBTM $item): void
{
echo "<form method='post' action='" . htmlspecialchars(PLUGIN_URBACKUP_WEB_DIR . "/front/asset.form.php") . "'>";
echo Html::hidden('_glpi_csrf_token', ['value' => Session::getNewCSRFToken()]);
echo Html::hidden('itemtype', ['value' => $item::class]);
echo Html::hidden('items_id', ['value' => (int) $item->fields['id']]);
echo Html::submit(__('Disconnect', 'urbackup'), [
'name' => 'disconnect',
'class' => 'btn btn-warning',
]);
Html::closeForm();
}
/**
* Format online status.
*
* @param array<string, mixed> $status Status
*
* @return string
*/
private static function formatOnlineStatus(array $status): string
{
$online = $status['online'] ?? $status['is_online'] ?? null;
return match (true) {
$online === true || $online === 1 || $online === '1' || $online === 'true' => __('Online', 'urbackup'),
$online === false || $online === 0 || $online === '0' || $online === 'false' => __('Offline', 'urbackup'),
default => '',
};
}
/**
* Format bool status.
*
* @param mixed $value Value
*
* @return string
*/
private static function formatBoolStatus(mixed $value): string
{
return match (true) {
$value === true || $value === 1 || $value === '1' || $value === 'true' => __('OK', 'urbackup'),
$value === false || $value === 0 || $value === '0' || $value === 'false' => __('Failed', 'urbackup'),
default => '',
};
}
/**
* Format timestamp-like value.
*
* @param mixed $value Timestamp
*
* @return string
*/
private static function formatTimestamp(mixed $value): string
{
if ($value === null || $value === '' || $value === 0 || $value === '0') {
return '';
}
$ts = (int) $value;
if ($ts > 0 && $ts < 2000000000) {
// Unix timestamp
return Html::convDateTime(date('Y-m-d H:i:s', $ts));
}
$str = trim((string) $value);
if ($str === '') {
return '';
}
// ISO date string like "2024-01-15T10:30:00" or "2024-01-15 10:30:00"
$normalized = str_replace('T', ' ', $str);
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/', $normalized)) {
return Html::convDateTime($normalized);
}
// Plain date like "2024-01-15"
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $str)) {
return Html::convDate($str);
}
return $str;
}
public static function startBackup(CommonDBTM $item, string $type): bool
{
$link = ServerAsset::getLinkForAsset($item::class, (int) $item->fields['id'], false);
if ($link === null) {
return false;
}
$server_id = (int) ($link['plugin_urbackup_servers_id'] ?? 0);
if ($server_id <= 0) {
return false;
}
$server = new Server();
if (!$server->getFromDB($server_id)) {
return false;
}
$client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']);
if ($client_name === '') {
return false;
}
try {
$api = new UrbackupApiClient($server);
switch ($type) {
case 'file':
return $api->startIncrementalFileBackup($client_name);
case 'image':
return $api->startIncrementalImageBackup($client_name);
}
} catch (\Throwable $e) {
return false;
}
return false;
}
public static function saveInternetMode(CommonDBTM $item, bool $enabled): bool
{
$link = ServerAsset::getLinkForAsset($item::class, (int) $item->fields['id'], false);
if ($link === null) {
return false;
}
$server_id = (int) ($link['plugin_urbackup_servers_id'] ?? 0);
if ($server_id <= 0) {
return false;
}
$server = new Server();
if (!$server->getFromDB($server_id)) {
return false;
}
$client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']);
if ($client_name === '') {
return false;
}
try {
$api = new UrbackupApiClient($server);
return $api->saveInternetMode($client_name, $enabled);
} catch (\Throwable $e) {
return false;
}
}
public static function saveDefaultDirs(CommonDBTM $item, string $dirs): bool
{
$link = ServerAsset::getLinkForAsset($item::class, (int) $item->fields['id'], false);
if ($link === null) {
return false;
}
$server_id = (int) ($link['plugin_urbackup_servers_id'] ?? 0);
if ($server_id <= 0) {
return false;
}
$server = new Server();
if (!$server->getFromDB($server_id)) {
return false;
}
$client_name = ServerAsset::getAssetName($item::class, (int) $item->fields['id']);
if ($client_name === '') {
return false;
}
try {
$api = new UrbackupApiClient($server);
return $api->updateClientSettings($client_name, 'default_dirs', trim($dirs));
} catch (\Throwable $e) {
return false;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace GlpiPlugin\Urbackup\Command;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\UrbackupApiClient;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class TestApiCommand extends Command
{
protected function configure(): void
{
$this
->setName('urbackup:test-api')
->setDescription('Test UrBackup server API connection')
->addArgument('server_id', InputArgument::REQUIRED, 'Server ID');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$server_id = (int) $input->getArgument('server_id');
$server = new Server();
if (!$server->getFromDB($server_id)) {
$output->writeln("<error>Server not found: $server_id</error>");
return Command::FAILURE;
}
$output->writeln("Testing API for: " . $server->fields['name']);
$output->writeln("URL: " . $server->getWebInterfaceUrl());
$output->writeln("Username: " . $server->fields['api_username']);
$client = new UrbackupApiClient($server);
try {
$result = $client->testConnection();
if ($result['success']) {
$output->writeln("<info>SUCCESS: " . $result['message'] . "</info>");
$output->writeln("Identity: " . $result['identity']);
} else {
$output->writeln("<error>FAILED: " . $result['message'] . "</error>");
}
return $result['success'] ? Command::SUCCESS : Command::FAILURE;
} catch (\Throwable $e) {
$output->writeln("<error>Exception: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
}
+414
View File
@@ -0,0 +1,414 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup;
use CommonDBTM;
use Computer;
use Glpi\Asset\AssetDefinition;
use Glpi\Asset\AssetDefinitionManager;
use DBmysql;
use Html;
use Session;
class Config extends CommonDBTM
{
public static $rightname = 'config';
/**
* Get type name.
*
* @param int $nb Number
*
* @return string
*/
public static function getTypeName($nb = 0): string
{
return __('UrBackup configuration', 'urbackup');
}
/**
* Get table name.
*
* @param string|null $classname Class name
*
* @return string
*/
public static function getTable($classname = null): string
{
return 'glpi_plugin_urbackup_configs';
}
/**
* Ensure default configuration.
*
* @return void
*/
public static function ensureDefaultConfiguration(): void
{
self::ensureAssetType('Computer', true, true);
}
/**
* Ensure an asset type is registered.
*
* @param string $itemtype Itemtype
* @param bool $is_active Active
* @param bool $is_default Default
*
* @return void
*/
public static function ensureAssetType(
string $itemtype,
bool $is_active = true,
bool $is_default = false
): void {
global $DB;
$table = self::getAssetTypesTable();
if (!$DB->tableExists($table)) {
return;
}
$existing = $DB->request([
'FROM' => $table,
'WHERE' => [
'itemtype' => $itemtype,
],
'LIMIT' => 1,
]);
if (count($existing) > 0) {
$row = $existing->current();
$DB->update(
$table,
[
'is_active' => $is_active ? 1 : 0,
'is_default' => $is_default ? 1 : 0,
'date_mod' => $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'),
],
[
'id' => (int) $row['id'],
]
);
return;
}
$DB->insert(
$table,
[
'itemtype' => $itemtype,
'is_active' => $is_active ? 1 : 0,
'is_default' => $is_default ? 1 : 0,
'date_creation' => $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'),
'date_mod' => $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s'),
]
);
}
/**
* Get assettypes table name.
*
* @return string
*/
public static function getAssetTypesTable(): string
{
return 'glpi_plugin_urbackup_assettypes';
}
/**
* Check whether itemtype is enabled for UrBackup.
*
* @param string $itemtype Itemtype
*
* @return bool
*/
public static function isItemtypeEnabled(string $itemtype): bool
{
global $DB;
if ($itemtype === '') {
return false;
}
if ($itemtype === 'Computer') {
return true;
}
$table = self::getAssetTypesTable();
if (!$DB->tableExists($table)) {
return false;
}
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => [
'itemtype' => $itemtype,
'is_active' => 1,
],
'LIMIT' => 1,
]);
return count($iterator) > 0;
}
/**
* Get enabled itemtypes.
*
* @return array<int, string>
*/
public static function getEnabledItemtypes(): array
{
global $DB;
$types = ['Computer'];
$table = self::getAssetTypesTable();
if (!$DB->tableExists($table)) {
return $types;
}
$iterator = $DB->request([
'FROM' => $table,
'WHERE' => [
'is_active' => 1,
],
'ORDER' => 'itemtype',
]);
foreach ($iterator as $row) {
$itemtype = (string) $row['itemtype'];
if ($itemtype !== '' && !in_array($itemtype, $types, true)) {
$types[] = $itemtype;
}
}
return $types;
}
/**
* Register tabs on enabled asset types.
*
* @return void
*/
public static function registerAssetTabs(): void
{
foreach (self::getEnabledItemtypes() as $itemtype) {
if (!class_exists($itemtype)) {
continue;
}
if (!is_a($itemtype, CommonDBTM::class, true)) {
continue;
}
\Plugin::registerClass(AssetTab::class, [
'addtabon' => $itemtype,
]);
}
}
/**
* Show config form.
*
* @param int $ID ID
* @param array $options Options
*
* @return bool
*/
public function showForm($ID, array $options = []): bool
{
Session::checkRight('config', UPDATE);
echo "<form method='post' action='" . htmlspecialchars($options['target'] ?? '') . "'>";
echo "<div class='center'>";
echo "<table class='tab_cadre_fixe'>";
echo "<tr><th colspan='4'>" . htmlspecialchars(__('UrBackup configuration', 'urbackup')) . "</th></tr>";
echo "<tr>";
echo "<th>" . htmlspecialchars(__('Asset type', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Enabled', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Default', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Type', 'urbackup')) . "</th>";
echo "</tr>";
foreach (self::getConfigurableAssetTypes() as $itemtype => $label) {
$enabled = self::isItemtypeEnabled($itemtype);
$is_default = ($itemtype === 'Computer');
$is_asset_definition = self::isAssetDefinition($itemtype);
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars($label) . "</td>";
if ($itemtype === 'Computer') {
echo "<td>" . htmlspecialchars(__('Always', 'urbackup')) . "</td>";
echo "<td>" . htmlspecialchars(__('Yes', 'urbackup')) . "</td>";
} elseif ($is_asset_definition) {
// GLPI 11 Asset Definition
echo "<td>";
Html::showCheckbox([
'name' => 'assettypes[' . htmlspecialchars($itemtype) . '][is_active]',
'checked' => $enabled,
]);
echo "</td>";
echo "<td>";
Html::showCheckbox([
'name' => 'assettypes[' . htmlspecialchars($itemtype) . '][is_default]',
'checked' => $is_default,
]);
echo "</td>";
echo "<td><span class='badge bg-info'>Asset Definition</span></td>";
} else {
// Legacy type
echo "<td>";
Html::showCheckbox([
'name' => 'assettypes[' . htmlspecialchars($itemtype) . ']',
'checked' => $enabled,
]);
echo "</td>";
echo "<td>" . ($is_default ? htmlspecialchars(__('Yes', 'urbackup')) : '') . "</td>";
echo "<td><span class='badge bg-secondary'>Legacy</span></td>";
}
echo "</tr>";
}
echo "<tr>";
echo "<td colspan='4' class='center'>";
echo Html::submit(__('Save', 'urbackup'), [
'name' => 'update',
'class' => 'btn btn-primary',
]);
echo "</td>";
echo "</tr>";
echo "</table>";
echo "</div>";
Html::closeForm();
return true;
}
/**
* Save configuration.
*
* @param array<string, mixed> $input Input data
*
* @return void
*/
public static function saveConfiguration(array $input): void
{
Session::checkRight('config', UPDATE);
$selected = $input['assettypes'] ?? [];
foreach (self::getConfigurableAssetTypes() as $itemtype => $label) {
if ($itemtype === 'Computer') {
self::ensureAssetType('Computer', true, true);
continue;
}
// Check if it's an Asset Definition (new format with is_active/is_default)
if (self::isAssetDefinition($itemtype)) {
$is_active = isset($selected[$itemtype]['is_active']) && (bool) $selected[$itemtype]['is_active'];
$is_default = isset($selected[$itemtype]['is_default']) && (bool) $selected[$itemtype]['is_default'];
self::ensureAssetType($itemtype, $is_active, $is_default);
} else {
// Legacy format (simple checkbox)
$is_enabled = isset($selected[$itemtype]) && (bool) $selected[$itemtype];
self::ensureAssetType($itemtype, $is_enabled, false);
}
}
}
/**
* Get configurable asset types (legacy + GLPI 11 Asset Definition).
*
* @return array<string, string>
*/
public static function getConfigurableAssetTypes(): array
{
$types = [
'Computer' => Computer::getTypeName(Session::getPluralNumber()),
];
// Legacy types
$known_types = [
'Printer',
'Peripheral',
'NetworkEquipment',
'Phone',
'Monitor',
];
foreach ($known_types as $itemtype) {
if (!class_exists($itemtype)) {
continue;
}
if (!is_a($itemtype, CommonDBTM::class, true)) {
continue;
}
$types[$itemtype] = $itemtype::getTypeName(Session::getPluralNumber());
}
// GLPI 11 Asset Definition
if (class_exists(AssetDefinitionManager::class)) {
try {
$assetDefinitions = AssetDefinitionManager::getInstance()->getDefinitions(true);
foreach ($assetDefinitions as $definition) {
// Access fields directly like CommonDBTM
$system_name = $definition->fields['system_name'] ?? '';
$name = $definition->fields['name'] ?? '';
if (!empty($system_name)) {
$types[$system_name] = !empty($name) ? $name : $system_name;
}
}
} catch (\Throwable $e) {
// If AssetDefinitionManager fails, skip
}
}
return $types;
}
/**
* Check if itemtype is a GLPI 11 Asset Definition.
*
* @param string $itemtype Itemtype or system name
*
* @return bool
*/
public static function isAssetDefinition(string $itemtype): bool
{
if (!class_exists(AssetDefinitionManager::class)) {
return false;
}
try {
$assetDefinitions = AssetDefinitionManager::getInstance()->getDefinitions(true);
foreach ($assetDefinitions as $definition) {
if (($definition->fields['system_name'] ?? '') === $itemtype) {
return true;
}
}
} catch (\Throwable $e) {
// If fails, return false
}
return false;
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup\Controller;
use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\ServerAsset;
use GlpiPlugin\Urbackup\UrbackupApiClient;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use CommonDBTM;
use Html;
use Session;
class AssetController
{
#[Route('/plugin/urbackup/asset/action', name: 'urbackup_asset_action', methods: ['POST'])]
public function assetAction(Request $request): void
{
Session::checkLoginUser();
Session::checkCSRF($_POST);
$itemtype = (string) $request->request->get('itemtype', '');
$items_id = (int) $request->request->get('items_id', 0);
if ($itemtype === '' || $items_id <= 0 || !class_exists($itemtype)) {
Session::addMessageAfterRedirect(
__('Invalid asset reference.', 'urbackup'),
true,
ERROR
);
Html::back();
}
if (!Config::isItemtypeEnabled($itemtype)) {
Session::addMessageAfterRedirect(
__('UrBackup is not enabled for this asset type.', 'urbackup'),
true,
ERROR
);
Html::back();
}
$item = new $itemtype();
if (!$item instanceof CommonDBTM || !$item->getFromDB($items_id)) {
Session::addMessageAfterRedirect(
__('Asset not found.', 'urbackup'),
true,
ERROR
);
Html::back();
}
$action = (string) $request->request->get('urbackup_action', '');
switch ($action) {
case 'connect':
$server_id = (int) $request->request->get('plugin_urbackup_servers_id', 0);
if (ServerAsset::connectAssetToServer($itemtype, $items_id, $server_id)) {
Session::addMessageAfterRedirect(
__('Asset connected to UrBackup server.', 'urbackup'),
true,
INFO
);
}
break;
case 'disconnect':
if (ServerAsset::disconnectAsset($itemtype, $items_id)) {
Session::addMessageAfterRedirect(
__('Asset disconnected from UrBackup server.', 'urbackup'),
true,
INFO
);
}
break;
case 'set_internet_mode':
if (Profile::canCurrentUser(UPDATE)) {
$internet_mode = (int) $request->request->get('internet_mode', 0);
$serverAsset = new ServerAsset();
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, true);
if ($link) {
$server = new Server();
if ($server->getFromDB((int) $link['plugin_urbackup_servers_id'])) {
$api = new UrbackupApiClient($server);
$client_name = (string) ($item->fields['name'] ?? '');
$setting_key = $api->getInternetModeSettingKey();
$api->changeClientSetting($client_name, $setting_key, $internet_mode);
}
}
}
break;
case 'create_client':
if (Profile::canCurrentUser(CREATE)) {
$serverAsset = new ServerAsset();
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, true);
if ($link) {
$server = new Server();
if ($server->getFromDB((int) $link['plugin_urbackup_servers_id'])) {
$api = new UrbackupApiClient($server);
$client_name = (string) ($item->fields['name'] ?? '');
$api->addClient($client_name);
}
}
}
break;
case 'incremental_file_backup':
case 'full_file_backup':
case 'incremental_image_backup':
case 'full_image_backup':
if (Profile::canCurrentUser(UPDATE)) {
$serverAsset = new ServerAsset();
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, true);
if ($link) {
$server = new Server();
if ($server->getFromDB((int) $link['plugin_urbackup_servers_id'])) {
$api = new UrbackupApiClient($server);
$client_name = (string) ($item->fields['name'] ?? '');
switch ($action) {
case 'incremental_file_backup':
$api->startIncrementalFileBackup($client_name);
break;
case 'full_file_backup':
$api->startFullFileBackup($client_name);
break;
case 'incremental_image_backup':
$api->startIncrementalImageBackup($client_name);
break;
case 'full_image_backup':
$api->startFullImageBackup($client_name);
break;
}
}
}
}
break;
case 'delete_client':
if (Profile::canCurrentUser(PURGE)) {
$serverAsset = new ServerAsset();
$link = ServerAsset::getLinkForAsset($itemtype, $items_id, true);
if ($link) {
$server = new Server();
if ($server->getFromDB((int) $link['plugin_urbackup_servers_id'])) {
$api = new UrbackupApiClient($server);
$client_name = (string) ($item->fields['name'] ?? '');
$api->removeClient($client_name);
}
}
}
break;
}
Html::back();
}
#[Route('/plugin/urbackup/api/clients', name: 'urbackup_api_clients', methods: ['GET'])]
public function getClients(Request $request): JsonResponse
{
Session::checkLoginUser();
$server_id = (int) $request->query->get('server_id', 0);
if ($server_id <= 0) {
return new JsonResponse([]);
}
$server = new Server();
if (!$server->getFromDB($server_id)) {
return new JsonResponse([]);
}
$api = new UrbackupApiClient($server);
$clients = $api->getStatus();
return new JsonResponse($clients);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup\Controller;
use GlpiPlugin\Urbackup\Config;
use GlpiPlugin\Urbackup\Profile;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Html;
use Session;
class ConfigController extends AbstractController
{
#[Route('/plugin/urbackup/config', name: 'urbackup_config', methods: ['GET', 'POST'])]
public function configure(Request $request): Response
{
Session::checkRight('config', UPDATE);
if ($request->isMethod('POST') && $request->request->has('update')) {
Config::saveConfiguration($request->request->all());
Html::back();
}
$configurableTypes = Config::getConfigurableAssetTypes();
$enabledTypes = [];
foreach ($configurableTypes as $itemtype => $label) {
$enabledTypes[$itemtype] = Config::isItemtypeEnabled($itemtype);
}
return $this->render('config/config.html.twig', [
'configurable_types' => $configurableTypes,
'enabled_types' => $enabledTypes,
]);
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup\Controller;
use GlpiPlugin\Urbackup\Server;
use GlpiPlugin\Urbackup\Profile;
use GlpiPlugin\Urbackup\UrbackupApiClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Html;
use Session;
use Search;
class ServerController extends AbstractController
{
#[Route('/plugin/urbackup/servers', name: 'urbackup_server_list', methods: ['GET'])]
public function listServers(): void
{
if (!Server::canView()) {
Html::displayRightError();
}
Html::header(
Server::getTypeName(Session::getPluralNumber()),
$_SERVER['PHP_SELF'],
'admin',
Server::class
);
Search::show(Server::class);
Html::footer();
}
#[Route('/plugin/urbackup/server/{id}', name: 'urbackup_server_show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function showServer(int $id): void
{
$server = new Server();
if ($id > 0) {
$server->check($id, READ);
} else {
$server->check(-1, CREATE);
$server->getEmpty();
}
$server->display(['id' => $id]);
}
#[Route('/plugin/urbackup/server/test/{id}', name: 'urbackup_server_test', methods: ['POST', 'GET'])]
public function testConnection(int $id = 0): JsonResponse
{
if (!Profile::canCurrentUser(READ)) {
return new JsonResponse([
'success' => false,
'message' => 'No permission',
], 403);
}
if ($id <= 0) {
$id = (int) ($_POST['id'] ?? $_GET['id'] ?? 0);
}
if ($id <= 0) {
return new JsonResponse([
'success' => false,
'message' => 'Invalid server ID',
], 400);
}
$server = new Server();
if (!$server->getFromDB($id)) {
return new JsonResponse([
'success' => false,
'message' => 'Server not found',
], 404);
}
try {
$client = new UrbackupApiClient($server);
$result = $client->testConnection();
$server->update([
'id' => $id,
'last_api_status' => $result['success'] ? 1 : 0,
'last_api_message' => $result['message'],
'last_api_check' => date('Y-m-d H:i:s'),
]);
return new JsonResponse($result);
} catch (Throwable $e) {
return new JsonResponse([
'success' => false,
'message' => $e->getMessage(),
], 500);
}
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup;
use CommonDBTM;
use Location;
final class LocationHelper
{
/**
* Get asset location ID.
*
* @param CommonDBTM $item Asset item
*
* @return int
*/
public static function getAssetLocationId(CommonDBTM $item): int
{
return (int) ($item->fields['locations_id'] ?? 0);
}
/**
* Get root location ID from any sub-location.
*
* Required rule:
* if the computer/asset is placed in a sub-location,
* the UrBackup server reference is the one assigned to the root location.
*
* @param int $locations_id Location ID
*
* @return int
*/
public static function getRootLocationId(int $locations_id): int
{
if ($locations_id <= 0) {
return 0;
}
$location = new Location();
if (!$location->getFromDB($locations_id)) {
return 0;
}
$current_id = (int) $location->fields['id'];
$parent_id = (int) ($location->fields['locations_id'] ?? 0);
while ($parent_id > 0) {
$parent = new Location();
if (!$parent->getFromDB($parent_id)) {
break;
}
$current_id = (int) $parent->fields['id'];
$parent_id = (int) ($parent->fields['locations_id'] ?? 0);
}
return $current_id;
}
/**
* Get root location ID for an asset.
*
* @param CommonDBTM $item Asset item
*
* @return int
*/
public static function getRootLocationIdForAsset(CommonDBTM $item): int
{
return self::getRootLocationId(self::getAssetLocationId($item));
}
/**
* Get available UrBackup servers for an asset based on its root location.
*
* @param CommonDBTM $item Asset item
*
* @return array<int, string>
*/
public static function getAvailableServersForAsset(CommonDBTM $item): array
{
$root_location_id = self::getRootLocationIdForAsset($item);
if ($root_location_id <= 0) {
return [];
}
return Server::getActiveServersForRootLocation($root_location_id);
}
/**
* Check whether asset is in a sub-location.
*
* @param CommonDBTM $item Asset item
*
* @return bool
*/
public static function assetIsInSubLocation(CommonDBTM $item): bool
{
$location_id = self::getAssetLocationId($item);
if ($location_id <= 0) {
return false;
}
$root_location_id = self::getRootLocationId($location_id);
return $root_location_id > 0 && $root_location_id !== $location_id;
}
}
+264
View File
@@ -0,0 +1,264 @@
<?php
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup;
use CommonDBTM;
use Html;
use Session;
class MassiveAction extends CommonDBTM
{
public const ACTION_CONNECT_SERVER = 'connect_server';
public const ACTION_DISCONNECT_SERVER = 'disconnect_server';
public static $rightname = 'plugin_urbackup';
/**
* Get type name.
*
* @param int $nb Number
*
* @return string
*/
public static function getTypeName($nb = 0): string
{
return __('UrBackup massive action', 'urbackup');
}
/**
* Show sub-form for UrBackup massive actions.
*
* @param \MassiveAction $ma Massive action object
*
* @return bool
*/
public static function showMassiveActionsSubForm(\MassiveAction $ma): bool
{
switch ($ma->getAction()) {
case self::ACTION_CONNECT_SERVER:
if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) {
return false;
}
echo "<div class='center'>";
echo "<table class='tab_cadre_fixe'>";
echo "<tr>";
echo "<th colspan='2'>" . htmlspecialchars(__('Connect selected assets to an UrBackup server', 'urbackup')) . "</th>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td>" . htmlspecialchars(__('UrBackup server', 'urbackup')) . "</td>";
echo "<td>";
Server::dropdown([
'name' => 'plugin_urbackup_servers_id',
'entity' => $_SESSION['glpiactiveentities'] ?? [],
'entity_sons' => true,
'condition' => ['is_active' => 1],
]);
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td colspan='2' class='center'>";
echo Html::submit(__('Connect', 'urbackup'), [
'name' => 'massiveaction',
'class' => 'btn btn-primary',
]);
echo "</td>";
echo "</tr>";
echo "</table>";
echo "</div>";
return true;
case self::ACTION_DISCONNECT_SERVER:
if (!Profile::canCurrentUser(UPDATE)) {
return false;
}
echo "<div class='center'>";
echo "<table class='tab_cadre_fixe'>";
echo "<tr>";
echo "<th>" . htmlspecialchars(__('Disconnect selected assets from UrBackup server', 'urbackup')) . "</th>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
echo "<td class='center'>";
echo Html::submit(__('Disconnect', 'urbackup'), [
'name' => 'massiveaction',
'class' => 'btn btn-warning',
]);
echo "</td>";
echo "</tr>";
echo "</table>";
echo "</div>";
return true;
}
return parent::showMassiveActionsSubForm($ma);
}
/**
* Process UrBackup massive actions.
*
* @param \MassiveAction $ma Massive action object
* @param CommonDBTM $item Current item object
* @param array<int> $ids Selected IDs
*
* @return void
*/
public static function processMassiveActionsForOneItemtype(
\MassiveAction $ma,
CommonDBTM $item,
array $ids
): void {
$itemtype = $item->getType();
if (!Config::isItemtypeEnabled($itemtype)) {
foreach ($ids as $id) {
$ma->itemDone($itemtype, (int) $id, \MassiveAction::ACTION_KO);
}
$ma->addMessage(__('UrBackup is not enabled for this asset type.', 'urbackup'));
return;
}
switch ($ma->getAction()) {
case self::ACTION_CONNECT_SERVER:
self::processConnectServer($ma, $item, $ids);
return;
case self::ACTION_DISCONNECT_SERVER:
self::processDisconnectServer($ma, $item, $ids);
return;
}
parent::processMassiveActionsForOneItemtype($ma, $item, $ids);
}
/**
* Process connect-to-server massive action.
*
* @param \MassiveAction $ma Massive action object
* @param CommonDBTM $item Current item object
* @param array<int> $ids Selected IDs
*
* @return void
*/
private static function processConnectServer(
\MassiveAction $ma,
CommonDBTM $item,
array $ids
): void {
if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) {
foreach ($ids as $id) {
$ma->itemDone($item->getType(), (int) $id, \MassiveAction::ACTION_KO);
}
$ma->addMessage(__('You do not have permission to connect assets to UrBackup servers.', 'urbackup'));
return;
}
$input = $ma->getInput();
$server_id = (int) ($input['plugin_urbackup_servers_id'] ?? 0);
if ($server_id <= 0) {
foreach ($ids as $id) {
$ma->itemDone($item->getType(), (int) $id, \MassiveAction::ACTION_KO);
}
$ma->addMessage(__('No UrBackup server selected.', 'urbackup'));
return;
}
$server = new Server();
if (!$server->getFromDB($server_id)) {
foreach ($ids as $id) {
$ma->itemDone($item->getType(), (int) $id, \MassiveAction::ACTION_KO);
}
$ma->addMessage(__('UrBackup server not found.', 'urbackup'));
return;
}
foreach ($ids as $id) {
$id = (int) $id;
if (!$item->getFromDB($id)) {
$ma->itemDone($item->getType(), $id, \MassiveAction::ACTION_KO);
continue;
}
$result = ServerAsset::connectAssetToServer(
$item->getType(),
$id,
$server_id
);
$ma->itemDone(
$item->getType(),
$id,
$result ? \MassiveAction::ACTION_OK : \MassiveAction::ACTION_KO
);
}
}
/**
* Process disconnect-from-server massive action.
*
* @param \MassiveAction $ma Massive action object
* @param CommonDBTM $item Current item object
* @param array<int> $ids Selected IDs
*
* @return void
*/
private static function processDisconnectServer(
\MassiveAction $ma,
CommonDBTM $item,
array $ids
): void {
if (!Profile::canCurrentUser(UPDATE)) {
foreach ($ids as $id) {
$ma->itemDone($item->getType(), (int) $id, \MassiveAction::ACTION_KO);
}
$ma->addMessage(__('You do not have permission to disconnect assets from UrBackup servers.', 'urbackup'));
return;
}
foreach ($ids as $id) {
$id = (int) $id;
if (!$item->getFromDB($id)) {
$ma->itemDone($item->getType(), $id, \MassiveAction::ACTION_KO);
continue;
}
$result = ServerAsset::disconnectAsset(
$item->getType(),
$id
);
$ma->itemDone(
$item->getType(),
$id,
$result ? \MassiveAction::ACTION_OK : \MassiveAction::ACTION_KO
);
}
}
}
+246
View File
@@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
namespace GlpiPlugin\Urbackup;
use CommonGLPI;
use Glpi\Application\View\TemplateRenderer;
use Session;
class Profile extends \Profile
{
public static $rightname = 'plugin_urbackup';
public static function getIcon()
{
return 'ti ti-server';
}
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
{
if ($item instanceof \Profile && $item->getField('interface') === 'central') {
return self::createTabEntry(Server::getTypeName(2));
}
return '';
}
public static function displayTabContentForItem(
CommonGLPI $item,
$tabnum = 1,
$withtemplate = 0
) {
if (!$item instanceof \Profile) {
return false;
}
$profile = new \Profile();
$profile->getFromDB($item->getID());
$rights = self::getAllRights();
$twig = TemplateRenderer::getInstance();
$twig->display('@urbackup/profile.html.twig', [
'id' => $item->getID(),
'profile' => $profile,
'title' => self::getTypeName(Session::getPluralNumber()),
'rights' => $rights,
]);
return true;
}
public static function getTypeName($nb = 0): string
{
return _n('UrBackup', 'UrBackup', $nb, 'urbackup');
}
public static function getAllRights(): array
{
return [
[
'itemtype' => Server::class,
'label' => __('UrBackup Servers', 'urbackup'),
'field' => 'plugin_urbackup',
],
];
}
public static function installRights(): void
{
self::registerRights();
$profiles_id = (int) ($_SESSION['glpiactiveprofile']['id'] ?? 0);
if ($profiles_id > 0) {
self::setProfileRights($profiles_id, READ | UPDATE | CREATE | DELETE);
}
global $DB;
$all_profiles = $DB->request([
'SELECT' => 'id',
'FROM' => 'glpi_profiles',
]);
foreach ($all_profiles as $profile) {
if ($profile['id'] !== $profiles_id) {
$existing = $DB->request([
'FROM' => 'glpi_profilerights',
'WHERE' => [
'profiles_id' => $profile['id'],
'name' => self::$rightname,
],
'LIMIT' => 1,
]);
if (count($existing) === 0) {
$DB->insert('glpi_profilerights', [
'profiles_id' => $profile['id'],
'name' => self::$rightname,
'rights' => READ,
]);
}
}
}
}
public static function uninstallRights(): void
{
global $DB;
$DB->delete('glpi_profilerights', [
'name' => self::$rightname,
]);
\ProfileRight::deleteProfileRights([self::$rightname]);
}
public static function registerRights(): void
{
global $DB;
$iterator = $DB->request([
'FROM' => 'glpi_profilerights',
'WHERE' => ['name' => self::$rightname],
'LIMIT' => 1,
]);
if (count($iterator) > 0) {
return;
}
\ProfileRight::addProfileRights([self::$rightname]);
}
public static function setProfileRights(int $profiles_id, int $rights): void
{
global $DB;
if ($profiles_id <= 0) {
return;
}
$iterator = $DB->request([
'FROM' => 'glpi_profilerights',
'WHERE' => [
'profiles_id' => $profiles_id,
'name' => self::$rightname,
],
'LIMIT' => 1,
]);
if (count($iterator) > 0) {
$row = $iterator->current();
$DB->update(
'glpi_profilerights',
['rights' => $rights],
['id' => (int) $row['id']]
);
return;
}
$DB->insert('glpi_profilerights', [
'profiles_id' => $profiles_id,
'name' => self::$rightname,
'rights' => $rights,
]);
}
public static function getProfileRights(int $profiles_id): int
{
global $DB;
if ($profiles_id <= 0) {
return 0;
}
$iterator = $DB->request([
'FROM' => 'glpi_profilerights',
'WHERE' => [
'profiles_id' => $profiles_id,
'name' => self::$rightname,
],
'LIMIT' => 1,
]);
if (count($iterator) === 0) {
return 0;
}
$row = $iterator->current();
return (int) ($row['rights'] ?? 0);
}
public static function canCurrentUser(int $right): bool
{
$profiles_id = (int) ($_SESSION['glpiactiveprofile']['id'] ?? 0);
if ($profiles_id === 0) {
$user_id = (int) ($_SESSION['glpiID'] ?? 0);
if ($user_id > 0) {
global $DB;
$iterator = $DB->request([
'SELECT' => 'profiles_id',
'FROM' => 'glpi_profiles_users',
'WHERE' => [
'users_id' => $user_id,
'is_dynamic' => 0,
],
'LIMIT' => 1,
]);
if (count($iterator) > 0) {
$row = $iterator->current();
$profiles_id = (int) $row['profiles_id'];
}
}
}
if ($profiles_id > 0) {
$rights = self::getProfileRights($profiles_id);
if (($rights & $right) === $right) {
return true;
}
}
return (bool) Session::haveRight(self::$rightname, $right);
}
public static function initProfile($profile = null): void
{
$profile_id = 0;
if ($profile instanceof \Profile) {
$profile_id = $profile->getID();
} elseif (is_array($profile) && isset($profile['id'])) {
$profile_id = (int) $profile['id'];
}
if ($profile_id > 0) {
$current_rights = self::getProfileRights($profile_id);
if ($current_rights === 0) {
self::setProfileRights($profile_id, READ);
}
}
}
}
+942
View File
@@ -0,0 +1,942 @@
<?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;
}
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 '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ($unlinkedClients as $uc) {
$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((string) ($uc['name'] ?? 'Unknown')) . '</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>';
}
}
+291
View File
@@ -0,0 +1,291 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup;
use CommonDBTM;
use CommonGLPI;
use Html;
use Session;
class ServerAsset 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_serverassets';
}
/**
* Get type name.
*
* @param int $nb Number
*
* @return string
*/
public static function getTypeName($nb = 0): string
{
return _n('UrBackup linked asset', 'UrBackup linked assets', $nb, 'urbackup');
}
/**
* Connect asset to server.
*
* @param string $itemtype Itemtype
* @param int $items_id Item ID
* @param int $server_id Server ID
*
* @return bool
*/
public static function connectAssetToServer(string $itemtype, int $items_id, int $server_id): bool
{
global $DB;
if (!Profile::canCurrentUser(UPDATE) && !Profile::canCurrentUser(CREATE)) {
return false;
}
if (!Config::isItemtypeEnabled($itemtype)) {
return false;
}
if ($items_id <= 0 || $server_id <= 0) {
return false;
}
if (!class_exists($itemtype)) {
return false;
}
$item = new $itemtype();
if (!$item instanceof CommonDBTM || !$item->getFromDB($items_id)) {
return false;
}
$table = self::getTable();
$date = $_SESSION['glpi_currenttime'] ?? date('Y-m-d H:i:s');
$existing = self::getLinkForAsset($itemtype, $items_id, false);
if ($existing !== null) {
return $DB->update(
$table,
[
'plugin_urbackup_servers_id' => $server_id,
'client_name' => (string) ($item->fields['name'] ?? ''),
'client_ip' => self::extractAssetIp($item),
'is_active' => 1,
'date_mod' => $date,
],
[
'id' => (int) $existing['id'],
]
);
}
return $DB->insert(
$table,
[
'plugin_urbackup_servers_id' => $server_id,
'itemtype' => $itemtype,
'items_id' => $items_id,
]
);
}
/**
* Disconnect asset from server.
*
* @param string $itemtype Itemtype
* @param int $items_id Item ID
*
* @return bool
*/
public static function disconnectAsset(string $itemtype, int $items_id): bool
{
global $DB;
if (!Profile::canCurrentUser(UPDATE)) {
return false;
}
$link = self::getLinkForAsset($itemtype, $items_id, true);
if ($link === null) {
return true;
}
return $DB->delete(
self::getTable(),
[
'id' => (int) $link['id'],
]
);
}
/**
* Get active link for asset.
*
* @param string $itemtype Itemtype
* @param int $items_id Item ID
* @param bool $active_only Active only
*
* @return array<string, mixed>|null
*/
public static function createForAsset(
string $itemtype,
int $items_id,
int $servers_id
): bool {
global $DB;
$item = getItemForItemtype($itemtype);
if (!$item || !$item->getFromDB($items_id)) {
return false;
}
$result = $DB->insert(self::getTable(), [
'plugin_urbackup_servers_id' => $servers_id,
'itemtype' => $itemtype,
'items_id' => $items_id,
]);
return $result;
}
public static function getAssetName(string $itemtype, int $items_id): string
{
$item = getItemForItemtype($itemtype);
if (!$item || !$item->getFromDB($items_id)) {
return '';
}
return (string) ($item->fields['name'] ?? '');
}
public static function getLinkForAsset(
string $itemtype,
int $items_id,
bool $active_only = true
): ?array {
global $DB;
$iterator = $DB->request([
'FROM' => self::getTable(),
'WHERE' => [
'itemtype' => $itemtype,
'items_id' => $items_id,
],
'LIMIT' => 1,
]);
if (count($iterator) === 0) {
return null;
}
return $iterator->current();
}
/**
* Extract asset IP address.
*
* @param CommonDBTM $item Item
*
* @return string
*/
public static function extractAssetIp(CommonDBTM $item): string
{
if (!isset($item->fields['ip_address']) || $item->fields['ip_address'] === '') {
return '';
}
$ip = $item->fields['ip_address'];
if (is_array($ip)) {
return $ip[0] ?? '';
}
return (string) $ip;
}
/**
* Display tab content for item.
*
* @param CommonGLPI $item Server item
* @param int $tabnum Tab number
* @param int $withtemplate Template
*
* @return bool
*/
public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0): bool
{
global $DB;
if (!$item instanceof Server) {
return false;
}
echo "<div class='center'>";
echo "<table class='tab_cadre_fixehov'>";
echo "<tr><th colspan='5'>" . htmlspecialchars(__('Linked assets', 'urbackup')) . "</th></tr>";
echo "<tr>";
echo "<th>" . htmlspecialchars(__('Asset', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Type')) . "</th>";
echo "<th>" . htmlspecialchars(__('IP address', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Last file backup', 'urbackup')) . "</th>";
echo "<th>" . htmlspecialchars(__('Last image backup', 'urbackup')) . "</th>";
echo "</tr>";
$iterator = $DB->request([
'FROM' => self::getTable(),
'WHERE' => [
'plugin_urbackup_servers_id' => (int) $item->fields['id'],
'is_active' => 1,
],
'ORDER' => 'client_name',
]);
foreach ($iterator as $row) {
$asset_label = (string) $row['client_name'];
$itemtype = (string) $row['itemtype'];
$items_id = (int) $row['items_id'];
if (class_exists($itemtype)) {
$asset = new $itemtype();
if ($asset instanceof CommonDBTM && $asset->getFromDB($items_id)) {
$asset_label = $asset->getLink();
}
}
echo "<tr class='tab_bg_1'>";
echo "<td>" . $asset_label . "</td>";
echo "<td>" . htmlspecialchars($itemtype) . "</td>";
echo "<td>" . htmlspecialchars((string) $row['client_ip']) . "</td>";
echo "<td>" . htmlspecialchars((string) $row['last_file_backup']) . "</td>";
echo "<td>" . htmlspecialchars((string) $row['last_image_backup']) . "</td>";
echo "</tr>";
}
echo "</table>";
echo "</div>";
return true;
}
}
+766
View File
@@ -0,0 +1,766 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*
* Based on urbackup-server-python-web-api-wrapper by uroni
* Reference: https://github.com/uroni/urbackup-server-python-web-api-wrapper
*/
namespace GlpiPlugin\Urbackup;
use RuntimeException;
class UrbackupApiClient
{
private object $server;
private string $base_url;
private string $username;
private string $password;
private bool $ignore_ssl;
private int $timeout = 10;
private string $session = '';
private bool $logged_in = false;
private int $lastlogid = 0;
private string $server_version = '';
private bool $is_version_2_5_or_higher = false;
/**
* Constructor.
*
* @param Server|object $server UrBackup server object
*/
public function __construct(object $server)
{
$this->server = $server;
$this->base_url = rtrim($server->getWebInterfaceUrl(), '/') . '/x';
$this->username = (string) ($server->fields['api_username'] ?? '');
$this->password = (string) ($server->fields['api_password'] ?? '');
$this->ignore_ssl = ((int) ($server->fields['ignore_ssl'] ?? 0)) === 1;
$this->server_version = (string) ($server->fields['server_version'] ?? '');
$this->is_version_2_5_or_higher = $this->detectVersion2_5OrHigher();
}
/**
* Detect if server version is 2.5 or higher.
*
* @return bool
*/
private function detectVersion2_5OrHigher(): bool
{
if ($this->server_version === '') {
return false;
}
$parts = explode('.', $this->server_version);
if (count($parts) < 2) {
return false;
}
$major = (int) $parts[0];
$minor = (int) $parts[1];
if ($major > 2) {
return true;
}
if ($major === 2 && $minor >= 5) {
return true;
}
return false;
}
/**
* Get internet mode setting key based on server version.
*
* @return string
*/
public function getInternetModeSettingKey(): string
{
return $this->is_version_2_5_or_higher ? 'internet_mode_enabled' : 'internet_mode';
}
/**
* Extract setting value from 2.5+ structured format or simple value.
*
* @param mixed $setting Setting value
* @param mixed $default Default value
*
* @return mixed
*/
public function extractSettingValue(mixed $setting, mixed $default = null): mixed
{
if (is_array($setting) && array_key_exists('value', $setting)) {
return $setting['value'];
}
return $setting ?? $default;
}
/**
* Test API connection.
*
* @return array<string, mixed>
*/
public function testConnection(): array
{
try {
$this->login();
$identity = $this->getServerIdentity();
return [
'success' => true,
'message' => $identity !== ''
? sprintf(__('Connection successful. Server identity: %s', 'urbackup'), $identity)
: __('Connection successful.', 'urbackup'),
'identity' => $identity,
];
} catch (RuntimeException $e) {
return [
'success' => false,
'message' => $e->getMessage(),
'identity' => '',
];
}
}
/**
* Login to UrBackup web API.
*
* @return bool
*/
public function login(): bool
{
if ($this->logged_in) {
return true;
}
$login = $this->request('login', [], 'POST', false);
if (!$login || !isset($login['success']) || $login['success'] !== true) {
$salt = $this->request('salt', ['username' => $this->username], 'POST', false);
if (!$salt || !isset($salt['ses']) || $salt['ses'] === '') {
if (isset($salt['error']) && $salt['error'] === 1) {
throw new RuntimeException(__('Username does not exist on UrBackup server.', 'urbackup'));
}
throw new RuntimeException(__('Unable to get salt from UrBackup server.', 'urbackup'));
}
$this->session = (string) $salt['ses'];
if (isset($salt['salt'])) {
$password_md5 = $this->buildPasswordHash($this->password, $salt);
$login = $this->request('login', [
'username' => $this->username,
'password' => $password_md5,
'ses' => $this->session,
], 'POST', false);
if (!$login || !isset($login['success']) || $login['success'] !== true) {
throw new RuntimeException(__('Unable to authenticate against UrBackup server. Password may be wrong.', 'urbackup'));
}
$this->logged_in = true;
return true;
}
throw new RuntimeException(__('Salt response missing salt field.', 'urbackup'));
}
$this->session = (string) ($login['session'] ?? '');
$this->logged_in = true;
return true;
}
/**
* Build password hash for authentication.
*
* @param string $password Plain password
* @param array $salt Salt response from server
*
* @return string
*/
private function buildPasswordHash(string $password, array $salt): string
{
$salt_str = (string) ($salt['salt'] ?? '');
$rnd = (string) ($salt['rnd'] ?? '');
$pbkdf2_rounds = (int) ($salt['pbkdf2_rounds'] ?? 0);
$passwordMd5Bin = md5($salt_str . $password, true);
$passwordMd5 = bin2hex($passwordMd5Bin);
if ($pbkdf2_rounds > 0 && function_exists('hash_pbkdf2')) {
$passwordMd5 = hash_pbkdf2(
'sha256',
$passwordMd5Bin,
$salt_str,
$pbkdf2_rounds,
0,
false
);
}
return md5($rnd . $passwordMd5);
}
/**
* Get server identity.
*
* @return string
*/
public function getServerIdentity(): string
{
$data = $this->apiAction('server_identity');
if (isset($data['server_identity'])) {
return (string) $data['server_identity'];
}
if (isset($data['identity'])) {
return (string) $data['identity'];
}
if (isset($data['name'])) {
return (string) $data['name'];
}
return '';
}
/**
* Get all client statuses.
*
* @return array<int, array<string, mixed>>
*/
public function getStatus(): array
{
$data = $this->apiAction('status');
if (isset($data['status']) && is_array($data['status'])) {
return array_values($data['status']);
}
if (isset($data['clients']) && is_array($data['clients'])) {
return array_values($data['clients']);
}
if (array_is_list($data)) {
return $data;
}
return [];
}
/**
* Get client status by GLPI asset name.
*
* @param string $client_name Client name
*
* @return array<string, mixed>|null
*/
public function getClientStatusByName(string $client_name): ?array
{
foreach ($this->getStatus() as $client) {
$name = (string) ($client['name'] ?? $client['clientname'] ?? $client['hostname'] ?? '');
if (strcasecmp($name, $client_name) === 0) {
return $client;
}
}
return null;
}
/**
* Get client ID by name.
*
* @param string $client_name Client name
*
* @return int
*/
public function getClientIdByName(string $client_name): int
{
$client = $this->getClientStatusByName($client_name);
if ($client === null) {
return 0;
}
return (int) ($client['id'] ?? $client['clientid'] ?? $client['client_id'] ?? 0);
}
/**
* Get client settings.
*
* @param string $client_name Client name
*
* @return array<string, mixed>
*/
public function getClientSettings(string $client_name): array
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return [];
}
$data = $this->apiAction('settings', [
'sa' => 'clientsettings',
't_clientid' => $client_id,
]);
if (isset($data['settings']) && is_array($data['settings'])) {
return $data['settings'];
}
return $data;
}
/**
* Change a client setting.
*
* @param string $client_name Client name
* @param string $key Setting key
* @param mixed $value New value
*
* @return bool
*/
public function changeClientSetting(string $client_name, string $key, mixed $value): bool
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return false;
}
$data = $this->apiAction('settings', [
'sa' => 'clientsettings_save',
't_clientid' => $client_id,
'overwrite' => 'true',
$key => (string) $value,
]);
return $this->responseIsSuccess($data);
}
public function updateClientSettings(string $client_name, string $key, string $value): bool
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return false;
}
$data = $this->apiAction('settings', [
'sa' => 'clientsettings_save',
't_clientid' => $client_id,
'overwrite' => 'true',
$key => $value,
]);
return $this->responseIsSuccess($data);
}
public function saveInternetMode(string $client_name, bool $enabled): bool
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return false;
}
$key = $this->getInternetModeSettingKey();
$params = [
'sa' => 'clientsettings_save',
't_clientid' => $client_id,
'overwrite' => 'true',
$key => $enabled ? '1' : '0',
];
$data = $this->apiAction('settings', $params);
return $this->responseIsSuccess($data);
}
/**
* Get client internet authentication key.
*
* @param string $client_name Client name
*
* @return string
*/
public function getClientAuthKey(string $client_name): string
{
$settings = $this->getClientSettings($client_name);
$raw = $settings['internet_authkey'] ?? $settings['internetAuthkey'] ?? '';
if (is_array($raw) && isset($raw['value'])) {
return (string) $raw['value'];
}
if (is_array($raw)) {
return '';
}
return (string) $raw;
}
/**
* Add a client to UrBackup server.
*
* @param string $client_name Client name
*
* @return bool
*/
public function addClient(string $client_name): bool
{
$data = $this->apiAction('add_client', [
'clientname' => $client_name,
]);
return $this->responseIsSuccess($data);
}
/**
* Remove a client from UrBackup server.
*
* @param string $client_name Client name
*
* @return bool
*/
public function removeClient(string $client_name): bool
{
$client_id = $this->getClientIdByName($client_name);
$payload = [
'clientname' => $client_name,
];
if ($client_id > 0) {
$payload['clientid'] = $client_id;
}
$data = $this->apiAction('remove_client', $payload);
return $this->responseIsSuccess($data);
}
/**
* Start incremental file backup.
*
* @param string $client_name Client name
*
* @return bool
*/
public function startIncrementalFileBackup(string $client_name): bool
{
return $this->startBackup($client_name, 'incr_file');
}
/**
* Start full file backup.
*
* @param string $client_name Client name
*
* @return bool
*/
public function startFullFileBackup(string $client_name): bool
{
return $this->startBackup($client_name, 'full_file');
}
/**
* Start incremental image backup.
*
* @param string $client_name Client name
*
* @return bool
*/
public function startIncrementalImageBackup(string $client_name): bool
{
return $this->startBackup($client_name, 'incr_image');
}
/**
* Start full image backup.
*
* @param string $client_name Client name
*
* @return bool
*/
public function startFullImageBackup(string $client_name): bool
{
return $this->startBackup($client_name, 'full_image');
}
/**
* Get recent backups.
*
* @param string $client_name Client name
* @param int $limit Limit
*
* @return array<int, array<string, mixed>>
*/
public function getRecentBackups(string $client_name, int $limit = 40): array
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return [];
}
$data = $this->apiAction('backups', [
'sa' => 'backups',
'clientid' => $client_id,
]);
$rows = [];
foreach (($data['backups'] ?? []) as $backup) {
if (is_array($backup)) {
$backup['backup_type'] = __('File backup', 'urbackup');
$rows[] = $backup;
}
}
foreach (($data['backup_images'] ?? []) as $backup) {
if (is_array($backup)) {
$backup['backup_type'] = __('Image backup', 'urbackup');
$rows[] = $backup;
}
}
usort($rows, static function (array $a, array $b): int {
$timeA = (int) ($a['time'] ?? $a['backuptime'] ?? $a['backup_time'] ?? $a['created'] ?? 0);
$timeB = (int) ($b['time'] ?? $b['backuptime'] ?? $b['backup_time'] ?? $b['created'] ?? 0);
return $timeB <=> $timeA;
});
return array_slice($rows, 0, $limit);
}
/**
* Get client log rows.
*
* @param string $client_name Client name
* @param int $limit Limit
*
* @return array<int, array<string, mixed>>
*/
public function getClientLogs(string $client_name, int $limit = 50): array
{
$client_id = $this->getClientIdByName($client_name);
$data = $this->apiAction('livelog', [
'clientid' => $client_id,
'lastid' => $this->lastlogid,
]);
$logs = [];
foreach (($data['logdata'] ?? []) as $row) {
if (is_array($row)) {
$logs[] = $row;
}
}
if (count($logs) > 0) {
$last_entry = end($logs);
$this->lastlogid = (int) ($last_entry['id'] ?? 0);
}
return array_slice($logs, 0, $limit);
}
/**
* Start backup command.
*
* @param string $client_name Client name
* @param string $type Backup type
*
* @return bool
*/
private function startBackup(string $client_name, string $type): bool
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return false;
}
$data = $this->apiAction('start_backup', [
'start_client' => $client_id,
'start_type' => $type,
]);
if (isset($data['result']) && is_array($data['result'])) {
foreach ($data['result'] as $result) {
if (isset($result['start_ok']) && $result['start_ok'] === true) {
return true;
}
}
}
return $this->responseIsSuccess($data);
}
/**
* Execute an authenticated API action.
*
* @param string $action Action name
* @param array<string, mixed> $params Parameters
*
* @return array<string, mixed>
*/
private function apiAction(string $action, array $params = []): array
{
if (!$this->logged_in) {
$this->login();
}
$params['ses'] = $this->session;
return $this->request($action, $params, 'POST', true);
}
/**
* Execute HTTP request.
*
* @param string $action API action
* @param array<string, mixed> $params Parameters
* @param string $method HTTP method (GET/POST)
* @param bool $require_success Require successful HTTP
*
* @return array<string, mixed>
*/
private function request(string $action, array $params, string $method = 'POST', bool $require_success = true): array
{
if (!function_exists('curl_init')) {
throw new RuntimeException(__('PHP cURL extension is required for UrBackup API.', 'urbackup'));
}
$ch = curl_init();
if ($ch === false) {
throw new RuntimeException(__('Unable to initialize cURL.', 'urbackup'));
}
$url = $this->base_url . '?a=' . urlencode($action);
if ($method === 'GET') {
$url .= '&' . http_build_query($params);
}
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_SSL_VERIFYPEER => !$this->ignore_ssl,
CURLOPT_SSL_VERIFYHOST => $this->ignore_ssl ? 0 : 2,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
],
];
if ($method === 'POST') {
$options[CURLOPT_POST] = true;
$options[CURLOPT_POSTFIELDS] = http_build_query($params);
}
$options[CURLOPT_URL] = $url;
curl_setopt_array($ch, $options);
$raw = curl_exec($ch);
if ($raw === false) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException(
sprintf(__('UrBackup API request failed: %s', 'urbackup'), $error)
);
}
$http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($require_success && ($http_code < 200 || $http_code >= 300)) {
throw new RuntimeException(
sprintf(__('UrBackup API returned HTTP status %d.', 'urbackup'), $http_code)
);
}
if ($raw === '' || $raw === 'null') {
return [];
}
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
if (str_starts_with(trim($raw), '<')) {
throw new RuntimeException(
sprintf(__('UrBackup API returned non-JSON response (HTML). Check server URL and authentication.', 'urbackup'))
);
}
return [
'raw' => $raw,
'success' => $http_code >= 200 && $http_code < 300,
];
}
return $decoded;
}
/**
* Evaluate generic API success response.
*
* @param array<string, mixed> $data Response
*
* @return bool
*/
private function responseIsSuccess(array $data): bool
{
if (isset($data['success']) && $data['success'] === true) {
return true;
}
if (isset($data['ok']) && $data['ok'] === true) {
return true;
}
if (isset($data['saved_ok']) && $data['saved_ok'] === true) {
return true;
}
if (isset($data['result']) && $data['result'] === 'ok') {
return true;
}
if (isset($data['start_ok']) && $data['start_ok'] === true) {
return true;
}
return false;
}
}