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 */ 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> */ 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|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 */ 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> */ 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> */ 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 $params Parameters * * @return array */ 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 $params Parameters * @param string $method HTTP method (GET/POST) * @param bool $require_success Require successful HTTP * * @return array */ 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 $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; } }