2026-05-20 09:20:27 +02:00
< ? php
/**
* -------------------------------------------------------------------------
* UrBackup plugin for GLPI
* -------------------------------------------------------------------------
*/
namespace GlpiPlugin\Urbackup ;
use CommonDBTM ;
use CommonGLPI ;
use Dropdown ;
use Entity ;
2026-05-20 14:17:45 +02:00
use Group ;
2026-05-20 09:20:27 +02:00
use Html ;
use Location ;
use Session ;
2026-05-20 14:17:45 +02:00
use State ;
use User ;
2026-05-20 09:20:27 +02:00
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 );
2026-05-20 14:08:23 +02:00
$this -> showFormFields ( $ID );
$this -> showFormButtons ( $options );
return true ;
}
2026-05-20 09:20:27 +02:00
2026-05-20 14:08:23 +02:00
public function showFormFields ( int $ID ) : void
{
2026-05-20 09:20:27 +02:00
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> " ;
2026-05-20 14:08:23 +02:00
$apiStatus = ( int ) ( $this -> fields [ 'last_api_status' ] ? ? 0 );
2026-05-20 09:20:27 +02:00
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> " ;
}
}
/**
* 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 ( __ ( '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>' ;
2026-05-20 14:08:23 +02:00
echo '<td>' ;
if ( $itemUrl ) {
echo '<a href="' . htmlspecialchars ( $itemUrl ) . '">' . htmlspecialchars ( $clientName ) . '</a>' ;
} else {
echo htmlspecialchars ( $clientName );
}
echo ' <small class="text-muted">(' . htmlspecialchars ( $itemTypeLabel ) . ')</small>' ;
echo '</td>' ;
2026-05-20 09:20:27 +02:00
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 ;
}
2026-05-20 11:46:28 +02:00
$serverLocationId = ( int ) ( $server -> fields [ 'locations_id' ] ? ? 0 );
$linkableAssets = [];
if ( $serverLocationId > 0 ) {
$itemtypes = Config :: getEnabledItemtypes ();
foreach ( $unlinkedClients as $uc ) {
$clientName = ( string ) ( $uc [ 'name' ] ? ? '' );
if ( $clientName === '' ) {
continue ;
}
$clientNameLower = strtolower ( $clientName );
foreach ( $itemtypes as $itemtype ) {
if ( ! class_exists ( $itemtype )) {
continue ;
}
$assetItem = new $itemtype ();
if ( ! $assetItem instanceof CommonDBTM ) {
continue ;
}
$table = $assetItem -> getTable ();
if ( ! $DB -> tableExists ( $table )) {
continue ;
}
$assetIterator = $DB -> request ([
'FROM' => $table ,
'WHERE' => [
'name' => $clientName ,
'is_deleted' => 0 ,
],
'LIMIT' => 1 ,
]);
foreach ( $assetIterator as $assetRow ) {
$assetLocationId = ( int ) ( $assetRow [ 'locations_id' ] ? ? 0 );
$rootLocationId = LocationHelper :: getRootLocationId ( $assetLocationId );
if ( $rootLocationId > 0 && $rootLocationId === $serverLocationId ) {
$linkableAssets [ $clientNameLower ] = [
'itemtype' => $itemtype ,
'items_id' => ( int ) $assetRow [ 'id' ],
];
}
break ;
}
}
}
}
2026-05-20 09:20:27 +02:00
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>' ;
2026-05-20 11:46:28 +02:00
echo '<th>' . htmlspecialchars ( __ ( 'Actions' , 'urbackup' )) . '</th>' ;
2026-05-20 09:20:27 +02:00
echo '</tr>' ;
echo '</thead>' ;
echo '<tbody>' ;
foreach ( $unlinkedClients as $uc ) {
2026-05-20 11:46:28 +02:00
$clientName = ( string ) ( $uc [ 'name' ] ? ? 'Unknown' );
$clientNameLower = strtolower ( $clientName );
2026-05-20 09:20:27 +02:00
$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>' ;
2026-05-20 11:46:28 +02:00
echo '<td>' . htmlspecialchars ( $clientName ) . '</td>' ;
2026-05-20 09:20:27 +02:00
echo '<td>' . htmlspecialchars ( $clientVersion ) . '</td>' ;
echo '<td>' . $statusHtml . '</td>' ;
echo '<td>' . htmlspecialchars ( $lastBackup ? : '-' ) . '</td>' ;
echo '<td>' . htmlspecialchars ( $clientIp ? : '-' ) . '</td>' ;
2026-05-20 11:46:28 +02:00
echo '<td>' ;
if ( isset ( $linkableAssets [ $clientNameLower ])) {
$match = $linkableAssets [ $clientNameLower ];
$formAction = PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php' ;
echo '<form method="post" action="' . htmlspecialchars ( $formAction ) . '" class="d-inline">' ;
echo Html :: hidden ( '_glpi_csrf_token' , [ 'value' => Session :: getNewCSRFToken ()]);
echo Html :: hidden ( 'itemtype' , [ 'value' => $match [ 'itemtype' ]]);
echo Html :: hidden ( 'items_id' , [ 'value' => $match [ 'items_id' ]]);
echo Html :: hidden ( 'id' , [ 'value' => ( int ) $server -> fields [ 'id' ]]);
echo '<button type="submit" name="link_asset" value="1" class="btn btn-primary btn-sm">' ;
echo htmlspecialchars ( __ ( 'Connect' ));
echo '</button>' ;
Html :: closeForm ();
} else {
echo '<span class="text-muted">—</span>' ;
}
echo '</td>' ;
2026-05-20 09:20:27 +02:00
echo '</tr>' ;
}
echo '</tbody>' ;
echo '</table>' ;
}
2026-05-20 14:08:23 +02:00
public static function showMissingClientsTab ( 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 ;
}
$serverLocationId = ( int ) ( $server -> fields [ 'locations_id' ] ? ? 0 );
if ( $serverLocationId <= 0 ) {
echo '<div class="alert alert-info">' ;
echo htmlspecialchars ( __ ( 'No location configured for this server.' , 'urbackup' ));
echo '</div>' ;
return ;
}
$rootLocationId = LocationHelper :: getRootLocationId ( $serverLocationId );
$linkedIterator = $DB -> request ([
'FROM' => ServerAsset :: getTable (),
]);
$linkedAssetKeys = [];
foreach ( $linkedIterator as $row ) {
$linkedAssetKeys [ $row [ 'itemtype' ] . ':' . $row [ 'items_id' ]] = true ;
}
try {
$client = new UrbackupApiClient ( $server );
$urbackupClients = $client -> getStatus ();
} catch ( \Throwable $e ) {
echo '<div class="alert alert-danger">' ;
echo htmlspecialchars ( $e -> getMessage ());
echo '</div>' ;
return ;
}
$urbackupClientNames = [];
foreach ( $urbackupClients as $uc ) {
$name = strtolower (( string ) ( $uc [ 'name' ] ? ? $uc [ 'clientname' ] ? ? $uc [ 'hostname' ] ? ? '' ));
if ( $name !== '' ) {
$urbackupClientNames [ $name ] = true ;
}
}
2026-05-20 14:17:45 +02:00
$cacheEntity = [];
$cacheLocation = [];
$cacheState = [];
$cacheUser = [];
$cacheGroup = [];
2026-05-20 14:08:23 +02:00
$itemtypes = Config :: getEnabledItemtypes ();
$missingAssets = [];
foreach ( $itemtypes as $itemtype ) {
if ( ! class_exists ( $itemtype )) {
continue ;
}
$assetItem = new $itemtype ();
if ( ! $assetItem instanceof CommonDBTM ) {
continue ;
}
$table = $assetItem -> getTable ();
if ( ! $DB -> tableExists ( $table )) {
continue ;
}
$iterator = $DB -> request ([
'FROM' => $table ,
'WHERE' => [
'is_deleted' => 0 ,
],
]);
foreach ( $iterator as $assetRow ) {
$name = ( string ) ( $assetRow [ 'name' ] ? ? '' );
if ( $name === '' ) {
continue ;
}
$nameLower = strtolower ( $name );
$assetLocationId = ( int ) ( $assetRow [ 'locations_id' ] ? ? 0 );
$assetRootLocationId = LocationHelper :: getRootLocationId ( $assetLocationId );
if ( $assetRootLocationId !== $rootLocationId ) {
continue ;
}
$key = $itemtype . ':' . $assetRow [ 'id' ];
2026-05-20 14:17:45 +02:00
$assetId = ( int ) $assetRow [ 'id' ];
2026-05-20 14:08:23 +02:00
if ( isset ( $linkedAssetKeys [ $key ])) {
continue ;
}
if ( isset ( $urbackupClientNames [ $nameLower ])) {
continue ;
}
2026-05-20 14:17:45 +02:00
$entityName = self :: getCachedName ( 'Entity' , ( int ) ( $assetRow [ 'entities_id' ] ? ? 0 ), $cacheEntity );
$locationName = self :: getCachedLocationName ( $assetLocationId , $cacheLocation );
$stateName = self :: getCachedName ( 'State' , ( int ) ( $assetRow [ 'states_id' ] ? ? 0 ), $cacheState );
$userName = self :: getCachedName ( 'User' , ( int ) ( $assetRow [ 'users_id' ] ? ? 0 ), $cacheUser );
$groupName = self :: getCachedName ( 'Group' , ( int ) ( $assetRow [ 'groups_id' ] ? ? 0 ), $cacheGroup );
$ip = self :: getAssetIp ( $itemtype , $assetId );
2026-05-20 14:08:23 +02:00
$missingAssets [] = [
2026-05-20 14:17:45 +02:00
'itemtype' => $itemtype ,
'items_id' => $assetId ,
'name' => $name ,
'entity' => $entityName ,
'location' => $locationName ,
'otherserial' => ( string ) ( $assetRow [ 'otherserial' ] ? ? '' ),
'ip' => $ip ,
'state' => $stateName ,
'user' => $userName ,
'group' => $groupName ,
2026-05-20 14:08:23 +02:00
];
}
}
if ( count ( $missingAssets ) === 0 ) {
echo '<div class="alert alert-success">' ;
echo htmlspecialchars ( __ ( 'All assets in this location are linked or already on the UrBackup server.' , 'urbackup' ));
echo '</div>' ;
return ;
}
$formAction = PLUGIN_URBACKUP_WEB_DIR . '/front/server.form.php' ;
echo '<table class="table table-striped table-hover">' ;
echo '<thead><tr>' ;
echo '<th>' . htmlspecialchars ( __ ( 'Name' )) . '</th>' ;
2026-05-20 14:17:45 +02:00
echo '<th>' . htmlspecialchars ( Entity :: getTypeName ( 1 )) . '</th>' ;
echo '<th>' . htmlspecialchars ( Location :: getTypeName ( 1 )) . '</th>' ;
echo '<th>' . htmlspecialchars ( __ ( 'Inventory number' )) . '</th>' ;
echo '<th>' . htmlspecialchars ( __ ( 'IP address' , 'urbackup' )) . '</th>' ;
echo '<th>' . htmlspecialchars ( State :: getTypeName ( 1 )) . '</th>' ;
echo '<th>' . htmlspecialchars ( User :: getTypeName ( 1 )) . '</th>' ;
echo '<th>' . htmlspecialchars ( Group :: getTypeName ( 1 )) . '</th>' ;
2026-05-20 14:08:23 +02:00
echo '<th>' . htmlspecialchars ( __ ( 'Actions' , 'urbackup' )) . '</th>' ;
echo '</tr></thead>' ;
echo '<tbody>' ;
foreach ( $missingAssets as $asset ) {
echo '<tr>' ;
echo '<td>' . htmlspecialchars ( $asset [ 'name' ]) . '</td>' ;
2026-05-20 14:17:45 +02:00
echo '<td>' . htmlspecialchars ( $asset [ 'entity' ]) . '</td>' ;
echo '<td>' . htmlspecialchars ( $asset [ 'location' ]) . '</td>' ;
echo '<td>' . htmlspecialchars ( $asset [ 'otherserial' ]) . '</td>' ;
echo '<td>' . htmlspecialchars ( $asset [ 'ip' ]) . '</td>' ;
echo '<td>' . htmlspecialchars ( $asset [ 'state' ]) . '</td>' ;
echo '<td>' . htmlspecialchars ( $asset [ 'user' ]) . '</td>' ;
echo '<td>' . htmlspecialchars ( $asset [ 'group' ]) . '</td>' ;
2026-05-20 14:08:23 +02:00
echo '<td>' ;
echo '<form method="post" action="' . htmlspecialchars ( $formAction ) . '" class="d-inline">' ;
echo Html :: hidden ( '_glpi_csrf_token' , [ 'value' => Session :: getNewCSRFToken ()]);
echo Html :: hidden ( 'itemtype' , [ 'value' => $asset [ 'itemtype' ]]);
echo Html :: hidden ( 'items_id' , [ 'value' => $asset [ 'items_id' ]]);
echo Html :: hidden ( 'id' , [ 'value' => ( int ) $server -> fields [ 'id' ]]);
echo '<button type="submit" name="link_asset" value="1" class="btn btn-primary btn-sm">' ;
echo htmlspecialchars ( __ ( 'Connect' ));
echo '</button>' ;
Html :: closeForm ();
echo '</td>' ;
echo '</tr>' ;
}
echo '</tbody>' ;
echo '</table>' ;
}
2026-05-20 14:17:45 +02:00
private static function getCachedName ( string $classname , int $id , array & $cache ) : string
{
if ( $id <= 0 ) {
return '' ;
}
if ( ! isset ( $cache [ $id ])) {
$obj = new $classname ();
if ( $obj -> getFromDB ( $id )) {
$cache [ $id ] = ( string ) ( $obj -> fields [ 'name' ] ? ? '' );
} else {
$cache [ $id ] = '' ;
}
}
return $cache [ $id ];
}
private static function getCachedLocationName ( int $id , array & $cache ) : string
{
if ( $id <= 0 ) {
return '' ;
}
if ( ! isset ( $cache [ $id ])) {
$obj = new Location ();
if ( $obj -> getFromDB ( $id )) {
$cache [ $id ] = ( string ) ( $obj -> fields [ 'completename' ] ? ? $obj -> fields [ 'name' ] ? ? '' );
} else {
$cache [ $id ] = '' ;
}
}
return $cache [ $id ];
}
private static function getAssetIp ( string $itemtype , int $items_id ) : string
{
global $DB ;
$iterator = $DB -> request ([
'SELECT' => [ 'ipa.name' ],
'FROM' => 'glpi_ipaddresses AS ipa' ,
'INNER JOIN' => [
'glpi_networknames AS nn' => [
'ON' => [
'nn' => 'items_id' ,
'ipa' => 'id' ,
[ 'AND' => [ 'ipa.itemtype' => 'NetworkName' ]],
],
],
'glpi_networkports AS np' => [
'ON' => [
'np' => 'id' ,
'nn' => 'items_id' ,
[ 'AND' => [ 'nn.itemtype' => 'NetworkPort' ]],
],
],
],
'WHERE' => [
'np.itemtype' => $itemtype ,
'np.items_id' => $items_id ,
],
'LIMIT' => 1 ,
]);
foreach ( $iterator as $row ) {
$ip = ( string ) ( $row [ 'name' ] ? ? '' );
if ( $ip !== '' ) {
return $ip ;
}
}
return '' ;
}
2026-05-20 09:20:27 +02:00
}