This commit is contained in:
mariano
2026-05-20 09:20:27 +02:00
commit 1dc84aa5eb
199 changed files with 8444 additions and 0 deletions
+766
View File
@@ -0,0 +1,766 @@
<?php
declare(strict_types=1);
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*
* Based on urbackup-server-python-web-api-wrapper by uroni
* Reference: https://github.com/uroni/urbackup-server-python-web-api-wrapper
*/
namespace GlpiPlugin\Urbackup;
use RuntimeException;
class UrbackupApiClient
{
private object $server;
private string $base_url;
private string $username;
private string $password;
private bool $ignore_ssl;
private int $timeout = 10;
private string $session = '';
private bool $logged_in = false;
private int $lastlogid = 0;
private string $server_version = '';
private bool $is_version_2_5_or_higher = false;
/**
* Constructor.
*
* @param Server|object $server UrBackup server object
*/
public function __construct(object $server)
{
$this->server = $server;
$this->base_url = rtrim($server->getWebInterfaceUrl(), '/') . '/x';
$this->username = (string) ($server->fields['api_username'] ?? '');
$this->password = (string) ($server->fields['api_password'] ?? '');
$this->ignore_ssl = ((int) ($server->fields['ignore_ssl'] ?? 0)) === 1;
$this->server_version = (string) ($server->fields['server_version'] ?? '');
$this->is_version_2_5_or_higher = $this->detectVersion2_5OrHigher();
}
/**
* Detect if server version is 2.5 or higher.
*
* @return bool
*/
private function detectVersion2_5OrHigher(): bool
{
if ($this->server_version === '') {
return false;
}
$parts = explode('.', $this->server_version);
if (count($parts) < 2) {
return false;
}
$major = (int) $parts[0];
$minor = (int) $parts[1];
if ($major > 2) {
return true;
}
if ($major === 2 && $minor >= 5) {
return true;
}
return false;
}
/**
* Get internet mode setting key based on server version.
*
* @return string
*/
public function getInternetModeSettingKey(): string
{
return $this->is_version_2_5_or_higher ? 'internet_mode_enabled' : 'internet_mode';
}
/**
* Extract setting value from 2.5+ structured format or simple value.
*
* @param mixed $setting Setting value
* @param mixed $default Default value
*
* @return mixed
*/
public function extractSettingValue(mixed $setting, mixed $default = null): mixed
{
if (is_array($setting) && array_key_exists('value', $setting)) {
return $setting['value'];
}
return $setting ?? $default;
}
/**
* Test API connection.
*
* @return array<string, mixed>
*/
public function testConnection(): array
{
try {
$this->login();
$identity = $this->getServerIdentity();
return [
'success' => true,
'message' => $identity !== ''
? sprintf(__('Connection successful. Server identity: %s', 'urbackup'), $identity)
: __('Connection successful.', 'urbackup'),
'identity' => $identity,
];
} catch (RuntimeException $e) {
return [
'success' => false,
'message' => $e->getMessage(),
'identity' => '',
];
}
}
/**
* Login to UrBackup web API.
*
* @return bool
*/
public function login(): bool
{
if ($this->logged_in) {
return true;
}
$login = $this->request('login', [], 'POST', false);
if (!$login || !isset($login['success']) || $login['success'] !== true) {
$salt = $this->request('salt', ['username' => $this->username], 'POST', false);
if (!$salt || !isset($salt['ses']) || $salt['ses'] === '') {
if (isset($salt['error']) && $salt['error'] === 1) {
throw new RuntimeException(__('Username does not exist on UrBackup server.', 'urbackup'));
}
throw new RuntimeException(__('Unable to get salt from UrBackup server.', 'urbackup'));
}
$this->session = (string) $salt['ses'];
if (isset($salt['salt'])) {
$password_md5 = $this->buildPasswordHash($this->password, $salt);
$login = $this->request('login', [
'username' => $this->username,
'password' => $password_md5,
'ses' => $this->session,
], 'POST', false);
if (!$login || !isset($login['success']) || $login['success'] !== true) {
throw new RuntimeException(__('Unable to authenticate against UrBackup server. Password may be wrong.', 'urbackup'));
}
$this->logged_in = true;
return true;
}
throw new RuntimeException(__('Salt response missing salt field.', 'urbackup'));
}
$this->session = (string) ($login['session'] ?? '');
$this->logged_in = true;
return true;
}
/**
* Build password hash for authentication.
*
* @param string $password Plain password
* @param array $salt Salt response from server
*
* @return string
*/
private function buildPasswordHash(string $password, array $salt): string
{
$salt_str = (string) ($salt['salt'] ?? '');
$rnd = (string) ($salt['rnd'] ?? '');
$pbkdf2_rounds = (int) ($salt['pbkdf2_rounds'] ?? 0);
$passwordMd5Bin = md5($salt_str . $password, true);
$passwordMd5 = bin2hex($passwordMd5Bin);
if ($pbkdf2_rounds > 0 && function_exists('hash_pbkdf2')) {
$passwordMd5 = hash_pbkdf2(
'sha256',
$passwordMd5Bin,
$salt_str,
$pbkdf2_rounds,
0,
false
);
}
return md5($rnd . $passwordMd5);
}
/**
* Get server identity.
*
* @return string
*/
public function getServerIdentity(): string
{
$data = $this->apiAction('server_identity');
if (isset($data['server_identity'])) {
return (string) $data['server_identity'];
}
if (isset($data['identity'])) {
return (string) $data['identity'];
}
if (isset($data['name'])) {
return (string) $data['name'];
}
return '';
}
/**
* Get all client statuses.
*
* @return array<int, array<string, mixed>>
*/
public function getStatus(): array
{
$data = $this->apiAction('status');
if (isset($data['status']) && is_array($data['status'])) {
return array_values($data['status']);
}
if (isset($data['clients']) && is_array($data['clients'])) {
return array_values($data['clients']);
}
if (array_is_list($data)) {
return $data;
}
return [];
}
/**
* Get client status by GLPI asset name.
*
* @param string $client_name Client name
*
* @return array<string, mixed>|null
*/
public function getClientStatusByName(string $client_name): ?array
{
foreach ($this->getStatus() as $client) {
$name = (string) ($client['name'] ?? $client['clientname'] ?? $client['hostname'] ?? '');
if (strcasecmp($name, $client_name) === 0) {
return $client;
}
}
return null;
}
/**
* Get client ID by name.
*
* @param string $client_name Client name
*
* @return int
*/
public function getClientIdByName(string $client_name): int
{
$client = $this->getClientStatusByName($client_name);
if ($client === null) {
return 0;
}
return (int) ($client['id'] ?? $client['clientid'] ?? $client['client_id'] ?? 0);
}
/**
* Get client settings.
*
* @param string $client_name Client name
*
* @return array<string, mixed>
*/
public function getClientSettings(string $client_name): array
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return [];
}
$data = $this->apiAction('settings', [
'sa' => 'clientsettings',
't_clientid' => $client_id,
]);
if (isset($data['settings']) && is_array($data['settings'])) {
return $data['settings'];
}
return $data;
}
/**
* Change a client setting.
*
* @param string $client_name Client name
* @param string $key Setting key
* @param mixed $value New value
*
* @return bool
*/
public function changeClientSetting(string $client_name, string $key, mixed $value): bool
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return false;
}
$data = $this->apiAction('settings', [
'sa' => 'clientsettings_save',
't_clientid' => $client_id,
'overwrite' => 'true',
$key => (string) $value,
]);
return $this->responseIsSuccess($data);
}
public function updateClientSettings(string $client_name, string $key, string $value): bool
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return false;
}
$data = $this->apiAction('settings', [
'sa' => 'clientsettings_save',
't_clientid' => $client_id,
'overwrite' => 'true',
$key => $value,
]);
return $this->responseIsSuccess($data);
}
public function saveInternetMode(string $client_name, bool $enabled): bool
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return false;
}
$key = $this->getInternetModeSettingKey();
$params = [
'sa' => 'clientsettings_save',
't_clientid' => $client_id,
'overwrite' => 'true',
$key => $enabled ? '1' : '0',
];
$data = $this->apiAction('settings', $params);
return $this->responseIsSuccess($data);
}
/**
* Get client internet authentication key.
*
* @param string $client_name Client name
*
* @return string
*/
public function getClientAuthKey(string $client_name): string
{
$settings = $this->getClientSettings($client_name);
$raw = $settings['internet_authkey'] ?? $settings['internetAuthkey'] ?? '';
if (is_array($raw) && isset($raw['value'])) {
return (string) $raw['value'];
}
if (is_array($raw)) {
return '';
}
return (string) $raw;
}
/**
* Add a client to UrBackup server.
*
* @param string $client_name Client name
*
* @return bool
*/
public function addClient(string $client_name): bool
{
$data = $this->apiAction('add_client', [
'clientname' => $client_name,
]);
return $this->responseIsSuccess($data);
}
/**
* Remove a client from UrBackup server.
*
* @param string $client_name Client name
*
* @return bool
*/
public function removeClient(string $client_name): bool
{
$client_id = $this->getClientIdByName($client_name);
$payload = [
'clientname' => $client_name,
];
if ($client_id > 0) {
$payload['clientid'] = $client_id;
}
$data = $this->apiAction('remove_client', $payload);
return $this->responseIsSuccess($data);
}
/**
* Start incremental file backup.
*
* @param string $client_name Client name
*
* @return bool
*/
public function startIncrementalFileBackup(string $client_name): bool
{
return $this->startBackup($client_name, 'incr_file');
}
/**
* Start full file backup.
*
* @param string $client_name Client name
*
* @return bool
*/
public function startFullFileBackup(string $client_name): bool
{
return $this->startBackup($client_name, 'full_file');
}
/**
* Start incremental image backup.
*
* @param string $client_name Client name
*
* @return bool
*/
public function startIncrementalImageBackup(string $client_name): bool
{
return $this->startBackup($client_name, 'incr_image');
}
/**
* Start full image backup.
*
* @param string $client_name Client name
*
* @return bool
*/
public function startFullImageBackup(string $client_name): bool
{
return $this->startBackup($client_name, 'full_image');
}
/**
* Get recent backups.
*
* @param string $client_name Client name
* @param int $limit Limit
*
* @return array<int, array<string, mixed>>
*/
public function getRecentBackups(string $client_name, int $limit = 40): array
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return [];
}
$data = $this->apiAction('backups', [
'sa' => 'backups',
'clientid' => $client_id,
]);
$rows = [];
foreach (($data['backups'] ?? []) as $backup) {
if (is_array($backup)) {
$backup['backup_type'] = __('File backup', 'urbackup');
$rows[] = $backup;
}
}
foreach (($data['backup_images'] ?? []) as $backup) {
if (is_array($backup)) {
$backup['backup_type'] = __('Image backup', 'urbackup');
$rows[] = $backup;
}
}
usort($rows, static function (array $a, array $b): int {
$timeA = (int) ($a['time'] ?? $a['backuptime'] ?? $a['backup_time'] ?? $a['created'] ?? 0);
$timeB = (int) ($b['time'] ?? $b['backuptime'] ?? $b['backup_time'] ?? $b['created'] ?? 0);
return $timeB <=> $timeA;
});
return array_slice($rows, 0, $limit);
}
/**
* Get client log rows.
*
* @param string $client_name Client name
* @param int $limit Limit
*
* @return array<int, array<string, mixed>>
*/
public function getClientLogs(string $client_name, int $limit = 50): array
{
$client_id = $this->getClientIdByName($client_name);
$data = $this->apiAction('livelog', [
'clientid' => $client_id,
'lastid' => $this->lastlogid,
]);
$logs = [];
foreach (($data['logdata'] ?? []) as $row) {
if (is_array($row)) {
$logs[] = $row;
}
}
if (count($logs) > 0) {
$last_entry = end($logs);
$this->lastlogid = (int) ($last_entry['id'] ?? 0);
}
return array_slice($logs, 0, $limit);
}
/**
* Start backup command.
*
* @param string $client_name Client name
* @param string $type Backup type
*
* @return bool
*/
private function startBackup(string $client_name, string $type): bool
{
$client_id = $this->getClientIdByName($client_name);
if ($client_id <= 0) {
return false;
}
$data = $this->apiAction('start_backup', [
'start_client' => $client_id,
'start_type' => $type,
]);
if (isset($data['result']) && is_array($data['result'])) {
foreach ($data['result'] as $result) {
if (isset($result['start_ok']) && $result['start_ok'] === true) {
return true;
}
}
}
return $this->responseIsSuccess($data);
}
/**
* Execute an authenticated API action.
*
* @param string $action Action name
* @param array<string, mixed> $params Parameters
*
* @return array<string, mixed>
*/
private function apiAction(string $action, array $params = []): array
{
if (!$this->logged_in) {
$this->login();
}
$params['ses'] = $this->session;
return $this->request($action, $params, 'POST', true);
}
/**
* Execute HTTP request.
*
* @param string $action API action
* @param array<string, mixed> $params Parameters
* @param string $method HTTP method (GET/POST)
* @param bool $require_success Require successful HTTP
*
* @return array<string, mixed>
*/
private function request(string $action, array $params, string $method = 'POST', bool $require_success = true): array
{
if (!function_exists('curl_init')) {
throw new RuntimeException(__('PHP cURL extension is required for UrBackup API.', 'urbackup'));
}
$ch = curl_init();
if ($ch === false) {
throw new RuntimeException(__('Unable to initialize cURL.', 'urbackup'));
}
$url = $this->base_url . '?a=' . urlencode($action);
if ($method === 'GET') {
$url .= '&' . http_build_query($params);
}
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_SSL_VERIFYPEER => !$this->ignore_ssl,
CURLOPT_SSL_VERIFYHOST => $this->ignore_ssl ? 0 : 2,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
],
];
if ($method === 'POST') {
$options[CURLOPT_POST] = true;
$options[CURLOPT_POSTFIELDS] = http_build_query($params);
}
$options[CURLOPT_URL] = $url;
curl_setopt_array($ch, $options);
$raw = curl_exec($ch);
if ($raw === false) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException(
sprintf(__('UrBackup API request failed: %s', 'urbackup'), $error)
);
}
$http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($require_success && ($http_code < 200 || $http_code >= 300)) {
throw new RuntimeException(
sprintf(__('UrBackup API returned HTTP status %d.', 'urbackup'), $http_code)
);
}
if ($raw === '' || $raw === 'null') {
return [];
}
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
if (str_starts_with(trim($raw), '<')) {
throw new RuntimeException(
sprintf(__('UrBackup API returned non-JSON response (HTML). Check server URL and authentication.', 'urbackup'))
);
}
return [
'raw' => $raw,
'success' => $http_code >= 200 && $http_code < 300,
];
}
return $decoded;
}
/**
* Evaluate generic API success response.
*
* @param array<string, mixed> $data Response
*
* @return bool
*/
private function responseIsSuccess(array $data): bool
{
if (isset($data['success']) && $data['success'] === true) {
return true;
}
if (isset($data['ok']) && $data['ok'] === true) {
return true;
}
if (isset($data['saved_ok']) && $data['saved_ok'] === true) {
return true;
}
if (isset($data['result']) && $data['result'] === 'ok') {
return true;
}
if (isset($data['start_ok']) && $data['start_ok'] === true) {
return true;
}
return false;
}
}