761 lines
20 KiB
PHP
761 lines
20 KiB
PHP
<?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_4_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_4_or_higher = $this->detectVersion2_4OrHigher();
|
|
}
|
|
|
|
private function detectVersion2_4OrHigher(): 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 >= 4) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get internet mode setting key based on server version.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getInternetModeSettingKey(): string
|
|
{
|
|
return $this->is_version_2_4_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 ? 'true' : 'false',
|
|
];
|
|
|
|
$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;
|
|
}
|
|
} |