Stable
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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>';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user