1#============================================================= -*-perl-*-
2#
3# BackupPC::CGI::HostInfo package
4#
5# DESCRIPTION
6#
7#   This module implements the HostInfo action for the CGI interface.
8#
9# AUTHOR
10#   Craig Barratt  <cbarratt@users.sourceforge.net>
11#
12# COPYRIGHT
13#   Copyright (C) 2003-2018  Craig Barratt
14#
15#   This program is free software: you can redistribute it and/or modify
16#   it under the terms of the GNU General Public License as published by
17#   the Free Software Foundation, either version 3 of the License, or
18#   (at your option) any later version.
19#
20#   This program is distributed in the hope that it will be useful,
21#   but WITHOUT ANY WARRANTY; without even the implied warranty of
22#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23#   GNU General Public License for more details.
24#
25#   You should have received a copy of the GNU General Public License
26#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
27#
28#========================================================================
29#
30# Version 4.2.2, released 21 Oct 2018.
31#
32# See http://backuppc.sourceforge.net.
33#
34#========================================================================
35
36package BackupPC::CGI::HostInfo;
37
38use strict;
39use BackupPC::CGI::Lib qw(:all);
40
41sub action
42{
43    my $host = $1 if ( $In{host} =~ /(.*)/ );
44    my($statusStr, $startIncrStr);
45
46    $host =~ s/^\s+//;
47    $host =~ s/\s+$//;
48    if ( $host eq "" ) {
49	ErrorExit(eval("qq{$Lang->{Unknown_host_or_user}}"));
50    }
51    $host = lc($host)
52               if ( !-d "$TopDir/pc/$host" && -d "$TopDir/pc/" . lc($host) );
53    if ( $host =~ /\.\./ || !-d "$TopDir/pc/$host" ) {
54        #
55        # try to lookup by user name
56        #
57        if ( $host eq "" || !defined($Hosts->{$host}) ) {
58            foreach my $h ( keys(%$Hosts) ) {
59                if ( $Hosts->{$h}{user} eq $host
60                        || lc($Hosts->{$h}{user}) eq lc($host) ) {
61                    $host = $h;
62                    last;
63                }
64            }
65            CheckPermission();
66            ErrorExit(eval("qq{$Lang->{Unknown_host_or_user}}"))
67                               if ( !defined($Hosts->{$host}) );
68        }
69        $In{host} = $host;
70    }
71    GetStatusInfo("host(${EscURI($host)})");
72    $bpc->ConfigRead($host);
73    %Conf = $bpc->Conf();
74    my $Privileged = CheckPermission($host);
75    if ( !$Privileged ) {
76        ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_view_information_about}}"));
77    }
78    my $deleteEnabled = $PrivAdmin || ($Conf{CgiUserDeleteBackupEnable} > 0 && $Privileged);
79    $deleteEnabled = 0 if ( $Conf{CgiUserDeleteBackupEnable} < 0 );
80    ReadUserEmailInfo();
81
82    if ( $Conf{XferMethod} eq "archive" ) {
83        my @Archives = $bpc->ArchiveInfoRead($host);
84        my ($ArchiveStr,$warnStr);
85
86        for ( my $i = 0 ; $i < @Archives ; $i++ ) {
87            my $startTime = timeStamp2($Archives[$i]{startTime});
88            my $dur       = $Archives[$i]{endTime} - $Archives[$i]{startTime};
89            $dur          = 1 if ( $dur <= 0 );
90            my $duration  = sprintf("%.1f", $dur / 60);
91            my $Archives_Result = $Lang->{failed};
92            if ($Archives[$i]{result} ne "failed") { $Archives_Result = $Lang->{success}; }
93            $ArchiveStr  .= <<EOF;
94<tr><td align="center"><a href="$MyURL?action=archiveInfo&num=$Archives[$i]{num}&host=${EscURI($host)}">$Archives[$i]{num}</a> </td>
95    <td align="center"> $Archives_Result </td>
96    <td align="right" data-date_format="$Conf{CgiDateFormatMMDD}"> $startTime </td>
97    <td align="right"> $duration </td>
98</tr>
99EOF
100        }
101        if ( $ArchiveStr ne "" ) {
102            $ArchiveStr = eval("qq{$Lang->{Archive_Summary}}");
103        }
104        if ( @Archives == 0 ) {
105            $warnStr = $Lang->{There_have_been_no_archives};
106        }
107        if ( $StatusHost{BgQueueOn} ) {
108            $statusStr .= eval("qq{$Lang->{Host_host_is_queued_on_the_background_queue_will_be_backed_up_soon}}");
109        }
110        if ( $StatusHost{UserQueueOn} ) {
111            $statusStr .= eval("qq{$Lang->{Host_host_is_queued_on_the_user_queue__will_be_backed_up_soon}}");
112        }
113        if ( $StatusHost{CmdQueueOn} ) {
114            $statusStr .= eval("qq{$Lang->{A_command_for_host_is_on_the_command_queue_will_run_soon}}");
115        }
116
117        my $content = eval("qq{$Lang->{Host__host_Archive_Summary2}}");
118        Header(eval("qq{$Lang->{Host__host_Archive_Summary}}"), $content, 1);
119        Trailer();
120        return;
121    }
122
123    #
124    # Normal, non-archive case
125    #
126    my @Backups = $bpc->BackupInfoRead($host);
127    my(@bkpRows, @sizeRows, @compRows, @errRows, @warnRows, $deleteHdrStr);
128    $deleteHdrStr = '<td align="center"> </td>' if ( $deleteEnabled );
129    for ( my $i = 0 ; $i < @Backups ; $i++ ) {
130        my($MBExistComp, $ExistComp, $MBNewComp, $NewComp);
131        my($dur, $duration, $MB, $MBperSec, $MBExist, $MBNew);
132        my $startTime = timeStamp2($Backups[$i]{startTime});
133
134        #
135        # if a backup is active, but there is no job, then force it to be displayed
136        # as partial.  this handles case of a forced exit of BackupPC without normal
137        # cleanup
138        #
139        $Backups[$i]{type} = "partial" if ( $Backups[$i]{type} eq "active" && !defined($StatusHost{Job}) );
140        if ( $Backups[$i]{type} ne "active" ) {
141            $dur       = $Backups[$i]{endTime} - $Backups[$i]{startTime};
142            $dur          = 1 if ( $dur <= 0 );
143            $duration  = sprintf("%.1f", $dur / 60);
144            $MB        = sprintf("%.1f", $Backups[$i]{size} / (1024*1024));
145            $MBperSec  = sprintf("%.2f", $Backups[$i]{size} / (1024*1024*$dur));
146            $MBExist   = sprintf("%.1f", $Backups[$i]{sizeExist} / (1024*1024));
147            $MBNew     = sprintf("%.1f", $Backups[$i]{sizeNew} / (1024*1024));
148            if ( $Backups[$i]{sizeExist} && $Backups[$i]{sizeExistComp} ) {
149                $MBExistComp = sprintf("%.1f", $Backups[$i]{sizeExistComp}
150                                                    / (1024 * 1024));
151                $ExistComp = sprintf("%.1f%%", 100 *
152                      (1 - $Backups[$i]{sizeExistComp} / $Backups[$i]{sizeExist}));
153            }
154            if ( $Backups[$i]{sizeNew} && $Backups[$i]{sizeNewComp} ) {
155                $MBNewComp = sprintf("%.1f", $Backups[$i]{sizeNewComp}
156                                                    / (1024 * 1024));
157                $NewComp = sprintf("%.1f%%", 100 *
158                      (1 - $Backups[$i]{sizeNewComp} / $Backups[$i]{sizeNew}));
159            }
160        }
161        my $age = sprintf("%.1f", (time - $Backups[$i]{startTime}) / (24*3600));
162        my $browseURL = "$MyURL?action=browse&host=${EscURI($host)}&num=$Backups[$i]{num}";
163        my $level  = $Backups[$i]{level};
164        my $filled = $Backups[$i]{noFill} ? $Lang->{No} : $Lang->{Yes};
165        $filled .= " ($Backups[$i]{fillFromNum}) "
166                            if ( $Backups[$i]{fillFromNum} ne "" );
167        my $ltype = $Lang->{"backupType_$Backups[$i]{type}"};
168        my $deleteStr;
169        if ( $deleteEnabled ) {
170            $deleteStr = <<EOF;
171    <td align="center" class="border"><form name="DeleteForm" action="$MyURL" method="get" style="margin-bottom: 0px;">
172        <input type="hidden" name="action" value="deleteBackup">
173        <input type="hidden" name="host"   value="$host">
174        <input type="hidden" name="num"    value="$Backups[$i]{num}">
175        <input type="hidden" name="nofill" value="$Backups[$i]{noFill}">
176        <input type="hidden" name="type"   value="$Backups[$i]{type}">
177        <input type="submit" value="${EscHTML($Lang->{CfgEdit_Button_Delete})}">
178    </form></td>
179EOF
180        }
181        push @bkpRows, <<EOF;
182<tr>
183    <td align="center" class="border"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
184    <td align="center" class="border"> $ltype </td>
185    <td align="center" class="border"> $filled </td>
186    <td align="center" class="border"> $level </td>
187    <td align="right" class="border" data-date_format="$Conf{CgiDateFormatMMDD}"> $startTime </td>
188    <td align="right" class="border">  $duration </td>
189    <td align="right" class="border">  $age </td>
190    $deleteStr
191    <td align="left" class="border">   <tt>$TopDir/pc/$host/$Backups[$i]{num}</tt> </td></tr>
192EOF
193        push @sizeRows, <<EOF;
194<tr><td align="center" class="border"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
195    <td align="center" class="border"> $ltype </td>
196    <td align="right" class="border">  $Backups[$i]{nFiles} </td>
197    <td align="right" class="border">  $MB </td>
198    <td align="right" class="border">  $MBperSec </td>
199    <td align="right" class="border">  $Backups[$i]{nFilesExist} </td>
200    <td align="right" class="border">  $MBExist </td>
201    <td align="right" class="border">  $Backups[$i]{nFilesNew} </td>
202    <td align="right" class="border">  $MBNew </td>
203</tr>
204EOF
205        my $is_compress = $Backups[$i]{compress} || $Lang->{off};
206        if (! $ExistComp) { $ExistComp = "&nbsp;"; }
207        if (! $MBExistComp) { $MBExistComp = "&nbsp;"; }
208        push @compRows, <<EOF;
209<tr><td align="center" class="border"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
210    <td align="center" class="border"> $ltype </td>
211    <td align="center" class="border"> $is_compress </td>
212    <td align="right" class="border">  $MBExist </td>
213    <td align="right" class="border">  $MBExistComp </td>
214    <td align="right" class="border">  $ExistComp </td>
215    <td align="right" class="border">  $MBNew </td>
216    <td align="right" class="border">  $MBNewComp </td>
217    <td align="right" class="border">  $NewComp </td>
218</tr>
219EOF
220        push @errRows, <<EOF;
221<tr><td align="center" class="border"> <a href="$browseURL">$Backups[$i]{num}</a> </td>
222    <td align="center" class="border"> $ltype </td>
223    <td align="center" class="border"> <a href="$MyURL?action=view&type=XferLOG&num=$Backups[$i]{num}&host=${EscURI($host)}">$Lang->{XferLOG}</a>,
224                      <a href="$MyURL?action=view&type=XferErr&num=$Backups[$i]{num}&host=${EscURI($host)}">$Lang->{Errors}</a> </td>
225    <td align="right" class="border">  $Backups[$i]{xferErrs} </td>
226    <td align="right" class="border">  $Backups[$i]{xferBadFile} </td>
227    <td align="right" class="border">  $Backups[$i]{xferBadShare} </td>
228    <td align="right" class="border">  $Backups[$i]{tarErrs} </td></tr>
229EOF
230    }
231    my $str = join("\n", reverse(@bkpRows));
232    my $sizeStr = join("\n", reverse(@sizeRows));
233    my $compStr = join("\n", reverse(@compRows));
234    my $errStr = join("\n", reverse(@errRows));
235    my $warnStr = join("\n", reverse(@warnRows));
236
237    my @Restores = $bpc->RestoreInfoRead($host);
238    my @restoreRows;
239
240    for ( my $i = 0 ; $i < @Restores ; $i++ ) {
241        my $startTime = timeStamp2($Restores[$i]{startTime});
242        my $dur       = $Restores[$i]{endTime} - $Restores[$i]{startTime};
243        $dur          = 1 if ( $dur <= 0 );
244        my $duration  = sprintf("%.1f", $dur / 60);
245        my $MB        = sprintf("%.1f", $Restores[$i]{size} / (1024*1024));
246        my $MBperSec  = sprintf("%.2f", $Restores[$i]{size} / (1024*1024*$dur));
247        my $Restores_Result = $Lang->{failed};
248        if ($Restores[$i]{result} ne "failed") { $Restores_Result = $Lang->{success}; }
249        push @restoreRows, <<EOF;
250<tr><td align="center" class="border"><a href="$MyURL?action=restoreInfo&num=$Restores[$i]{num}&host=${EscURI($host)}">$Restores[$i]{num}</a> </td>
251    <td align="center" class="border"> $Restores_Result </td>
252    <td align="right" class="border" data-date_format="$Conf{CgiDateFormatMMDD}"> $startTime </td>
253    <td align="right" class="border"> $duration </td>
254    <td align="right" class="border"> $Restores[$i]{nFiles} </td>
255    <td align="right" class="border"> $MB </td>
256    <td align="right" class="border"> $Restores[$i]{tarCreateErrs} </td>
257    <td align="right" class="border"> $Restores[$i]{xferErrs} </td>
258</tr>
259EOF
260    }
261    my $restoreStr = join("\n", reverse(@restoreRows));
262    if ( $restoreStr ne "" ) {
263        $restoreStr = eval("qq{$Lang->{Restore_Summary}}");
264    }
265    if ( @Backups == 0 ) {
266        $warnStr = $Lang->{This_PC_has_never_been_backed_up};
267    }
268    if ( defined($Hosts->{$host}) ) {
269        my $user = $Hosts->{$host}{user};
270        my @moreUsers = sort(keys(%{$Hosts->{$host}{moreUsers}}));
271        my $moreUserStr;
272        foreach my $u ( sort(keys(%{$Hosts->{$host}{moreUsers}})) ) {
273            $moreUserStr .= ", " if ( $moreUserStr ne "" );
274            $moreUserStr .= "${UserLink($u)}";
275        }
276        if ( $moreUserStr ne "" ) {
277            $moreUserStr = " ($Lang->{and} $moreUserStr).\n";
278        } else {
279            $moreUserStr = ".\n";
280        }
281        if ( $user ne "" ) {
282            $statusStr .= eval("qq{$Lang->{This_PC_is_used_by}$moreUserStr}");
283        }
284        if ( defined($UserEmailInfo{$user})
285                && defined($UserEmailInfo{$user}{$host})
286                && defined($UserEmailInfo{$user}{$host}{lastSubj}) ) {
287            my $mailTime = timeStamp2($UserEmailInfo{$user}{$host}{lastTime});
288            my $subj     = $UserEmailInfo{$user}{$host}{lastSubj};
289            $statusStr  .= eval("qq{$Lang->{Last_email_sent_to__was_at___subject}}");
290        } elsif ( defined($UserEmailInfo{$user})
291                && $UserEmailInfo{$user}{lastHost} eq $host
292                && defined($UserEmailInfo{$user}{lastSubj}) ) {
293            #
294            # Old format %UserEmailInfo - pre 3.2.0.
295            #
296            my $mailTime = timeStamp2($UserEmailInfo{$user}{lastTime});
297            my $subj     = $UserEmailInfo{$user}{lastSubj};
298            $statusStr  .= eval("qq{$Lang->{Last_email_sent_to__was_at___subject}}");
299        }
300    }
301    if ( defined($StatusHost{Job}) ) {
302        my $startTime = timeStamp2($StatusHost{Job}{startTime});
303        (my $cmd = $StatusHost{Job}{cmd}) =~ s/$BinDir\///g;
304        $statusStr .= eval("qq{$Lang->{The_command_cmd_is_currently_running_for_started}}");
305    }
306    if ( $StatusHost{BgQueueOn} ) {
307        $statusStr .= eval("qq{$Lang->{Host_host_is_queued_on_the_background_queue_will_be_backed_up_soon}}");
308    }
309    if ( $StatusHost{UserQueueOn} ) {
310        $statusStr .= eval("qq{$Lang->{Host_host_is_queued_on_the_user_queue__will_be_backed_up_soon}}");
311    }
312    if ( $StatusHost{CmdQueueOn} ) {
313        $statusStr .= eval("qq{$Lang->{A_command_for_host_is_on_the_command_queue_will_run_soon}}");
314    }
315    my $startTime = timeStamp2($StatusHost{endTime} == 0 ?
316                $StatusHost{startTime} : $StatusHost{endTime});
317    my $reason = "";
318    if ( $StatusHost{reason} ne "" ) {
319        $reason = " ($Lang->{$StatusHost{reason}})";
320    }
321    $statusStr .= eval("qq{$Lang->{Last_status_is_state_StatusHost_state_reason_as_of_startTime}}");
322
323    if ( $StatusHost{state} ne "Status_backup_in_progress"
324            && $StatusHost{state} ne "Status_restore_in_progress"
325            && $StatusHost{error} ne "" ) {
326        $statusStr .= eval("qq{$Lang->{Last_error_is____EscHTML_StatusHost_error}}");
327    }
328    my $priorStr = "Pings";
329    if ( $StatusHost{deadCnt} > 0 ) {
330        $statusStr .= eval("qq{$Lang->{Pings_to_host_have_failed_StatusHost_deadCnt__consecutive_times}}");
331        $priorStr = $Lang->{Prior_to_that__pings};
332    }
333    if ( $StatusHost{aliveCnt} > 0 ) {
334        $statusStr .= eval("qq{$Lang->{priorStr_to_host_have_succeeded_StatusHostaliveCnt_consecutive_times}}");
335
336        if ( (@{$Conf{BlackoutPeriods}} || defined($Conf{BlackoutHourBegin}))
337		&& $StatusHost{aliveCnt} >= $Conf{BlackoutGoodCnt}
338                && $Conf{BlackoutGoodCnt} >= 0 ) {
339            #
340            # Handle backward compatibility with original separate scalar
341            # blackout parameters.
342            #
343            if ( defined($Conf{BlackoutHourBegin}) ) {
344                push(@{$Conf{BlackoutPeriods}},
345                     {
346                         hourBegin => $Conf{BlackoutHourBegin},
347                         hourEnd   => $Conf{BlackoutHourEnd},
348                         weekDays  => $Conf{BlackoutWeekDays},
349                     }
350                );
351            }
352
353            #
354            # TODO: this string needs i18n.  Also, comma-separated
355            # list with "and" for the last element might not translate
356            # correctly.
357            #
358            my(@days) = qw(Sun Mon Tue Wed Thu Fri Sat);
359            my $blackoutStr;
360            my $periodCnt = 0;
361            foreach my $p ( @{$Conf{BlackoutPeriods}} ) {
362                next if ( ref($p->{weekDays}) ne "ARRAY"
363                            || !defined($p->{hourBegin})
364                            || !defined($p->{hourEnd})
365                        );
366                my $days = join(", ", @days[@{$p->{weekDays}}]);
367                my $t0   = sprintf("%d:%02d", $p->{hourBegin},
368                              60 * ($p->{hourBegin} - int($p->{hourBegin})));
369                my $t1   = sprintf("%d:%02d", $p->{hourEnd},
370                              60 * ($p->{hourEnd} - int($p->{hourEnd})));
371                if ( $periodCnt ) {
372                    $blackoutStr .= ", ";
373                    if ( $periodCnt == @{$Conf{BlackoutPeriods}} - 1 ) {
374                        $blackoutStr .= eval("qq{$Lang->{and}}");
375                        $blackoutStr .= " ";
376                    }
377                }
378                $blackoutStr
379                        .= eval("qq{$Lang->{__time0_to__time1_on__days}}");
380                $periodCnt++;
381            }
382            $statusStr .= eval("qq{$Lang->{Because__host_has_been_on_the_network_at_least__Conf_BlackoutGoodCnt_consecutive_times___}}");
383        }
384    }
385    if ( $StatusHost{backoffTime} > time ) {
386        my $hours = sprintf("%.1f", ($StatusHost{backoffTime} - time) / 3600);
387        $statusStr .= eval("qq{$Lang->{Backups_are_deferred_for_hours_hours_change_this_number}}");
388
389    }
390    if ( length($Conf{ClientComment}) ) {
391        $statusStr .= "<li>${EscHTML($Conf{ClientComment})}\n";
392    }
393    if ( @Backups ) {
394        # only allow incremental if there are already some backups
395        $startIncrStr = <<EOF;
396<input type="button" value="$Lang->{Start_Incr_Backup}"
397 onClick="document.StartStopForm.action.value='Start_Incr_Backup';
398          document.StartStopForm.submit();">
399EOF
400    }
401
402    $startIncrStr = eval("qq{$startIncrStr}");
403    my $content = eval("qq{$Lang->{Host__host_Backup_Summary2}}");
404    Header(eval("qq{$Lang->{Host__host_Backup_Summary}}"), $content);
405    Trailer();
406}
407
4081;
409