update admin traffic overview
This commit is contained in:
@@ -26,118 +26,20 @@
|
||||
const AREA = 'admin';
|
||||
require __DIR__ . '/lib/init.php';
|
||||
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\PhpHelper;
|
||||
use Froxlor\Settings;
|
||||
use Froxlor\Traffic\Traffic;
|
||||
use Froxlor\UI\Panel\UI;
|
||||
use Froxlor\UI\Request;
|
||||
use Froxlor\UI\Response;
|
||||
|
||||
$id = (int)Request::get('id');
|
||||
|
||||
$months = [
|
||||
'0' => 'empty',
|
||||
'1' => 'jan',
|
||||
'2' => 'feb',
|
||||
'3' => 'mar',
|
||||
'4' => 'apr',
|
||||
'5' => 'may',
|
||||
'6' => 'jun',
|
||||
'7' => 'jul',
|
||||
'8' => 'aug',
|
||||
'9' => 'sep',
|
||||
'10' => 'oct',
|
||||
'11' => 'nov',
|
||||
'12' => 'dec'
|
||||
];
|
||||
$range = Request::get('range', 'days:30');
|
||||
|
||||
if ($page == 'overview' || $page == 'customers') {
|
||||
$minyear_stmt = Database::query("SELECT `year` FROM `" . TABLE_PANEL_TRAFFIC . "` ORDER BY `year` ASC LIMIT 1");
|
||||
$minyear = $minyear_stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!isset($minyear['year']) || $minyear['year'] == 0) {
|
||||
$maxyears = 0;
|
||||
} else {
|
||||
$maxyears = date("Y") - $minyear['year'];
|
||||
try {
|
||||
$context = Traffic::getCustomerStats($userinfo, $range);
|
||||
} catch (Exception $e) {
|
||||
Response::dynamicError($e->getMessage());
|
||||
}
|
||||
|
||||
$params = [];
|
||||
if ($userinfo['customers_see_all'] == '0') {
|
||||
$params = [
|
||||
'id' => $userinfo['adminid']
|
||||
];
|
||||
}
|
||||
|
||||
$customer_name_list_stmt = Database::prepare("
|
||||
SELECT `customerid`,`company`,`name`,`firstname`
|
||||
FROM `" . TABLE_PANEL_CUSTOMERS . "`
|
||||
WHERE `deactivated`='0'" . ($userinfo['customers_see_all'] ? '' : ' AND `adminid` = :id') . "
|
||||
ORDER BY name");
|
||||
|
||||
$traffic_list_stmt = Database::prepare("
|
||||
SELECT month, SUM(http+ftp_up+ftp_down+mail)*1024 AS traffic
|
||||
FROM `" . TABLE_PANEL_TRAFFIC . "`
|
||||
WHERE year = :year AND `customerid` = :id
|
||||
GROUP BY month ORDER BY month");
|
||||
|
||||
$stats = [];
|
||||
|
||||
for ($years = 0; $years <= $maxyears; $years++) {
|
||||
$totals = [
|
||||
'jan' => 0,
|
||||
'feb' => 0,
|
||||
'mar' => 0,
|
||||
'apr' => 0,
|
||||
'may' => 0,
|
||||
'jun' => 0,
|
||||
'jul' => 0,
|
||||
'aug' => 0,
|
||||
'sep' => 0,
|
||||
'oct' => 0,
|
||||
'nov' => 0,
|
||||
'dec' => 0
|
||||
];
|
||||
|
||||
Database::pexecute($customer_name_list_stmt, $params);
|
||||
|
||||
$data = [];
|
||||
while ($customer_name = $customer_name_list_stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$virtual_host = [
|
||||
'name' => ($customer_name['company'] == '' ? $customer_name['name'] . ", " . $customer_name['firstname'] : $customer_name['company']),
|
||||
'customerid' => $customer_name['customerid'],
|
||||
'jan' => '-',
|
||||
'feb' => '-',
|
||||
'mar' => '-',
|
||||
'apr' => '-',
|
||||
'may' => '-',
|
||||
'jun' => '-',
|
||||
'jul' => '-',
|
||||
'aug' => '-',
|
||||
'sep' => '-',
|
||||
'oct' => '-',
|
||||
'nov' => '-',
|
||||
'dec' => '-'
|
||||
];
|
||||
|
||||
Database::pexecute($traffic_list_stmt, [
|
||||
'year' => (date("Y") - $years),
|
||||
'id' => $customer_name['customerid']
|
||||
]);
|
||||
|
||||
while ($traffic_month = $traffic_list_stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$virtual_host[$months[(int)$traffic_month['month']]] = PhpHelper::sizeReadable($traffic_month['traffic'], 'GiB', 'bi', '%01.' . (int)Settings::Get('panel.decimal_places') . 'f %s');
|
||||
$totals[$months[(int)$traffic_month['month']]] += $traffic_month['traffic'];
|
||||
}
|
||||
|
||||
$data = $virtual_host;
|
||||
}
|
||||
$stats[] = [
|
||||
'year' => date("Y") - $years,
|
||||
'type' => lng('traffic.customer'),
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
UI::view('user/traffic.html.twig', [
|
||||
'stats' => $stats
|
||||
]);
|
||||
// pass metrics to the view
|
||||
UI::view('user/traffic.html.twig', $context);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
*/
|
||||
|
||||
const AREA = 'customer';
|
||||
$intrafficpage = 1;
|
||||
require __DIR__ . '/lib/init.php';
|
||||
|
||||
use Froxlor\Database\Database;
|
||||
@@ -38,131 +37,13 @@ if (Settings::IsInList('panel.customer_hide_options', 'traffic')) {
|
||||
Response::redirectTo('customer_index.php');
|
||||
}
|
||||
|
||||
$traffic = '';
|
||||
$month = null;
|
||||
$year = null;
|
||||
if ($page === null || $page == 'overview') {
|
||||
|
||||
} elseif ($page == 'current') {
|
||||
|
||||
if (Request::exist('month') && Request::exist('year')) {
|
||||
$month = (int)Request::get('month');
|
||||
$year = (int)Request::get('year');
|
||||
} elseif (isset($_GET['page']) && $_GET['page'] == 'current') {
|
||||
if (date('d') != '01') {
|
||||
$month = date('m');
|
||||
$year = date('Y');
|
||||
} elseif (date('m') == '01') {
|
||||
$month = 12;
|
||||
$year = date('Y') - 1;
|
||||
} else {
|
||||
$month = date('m') - 1;
|
||||
$year = date('Y');
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_null($month) && !is_null($year)) {
|
||||
$result_stmt = Database::prepare("SELECT SUM(`http`) as 'http', SUM(`ftp_up`) AS 'ftp_up', SUM(`ftp_down`) as 'ftp_down', SUM(`mail`) as 'mail', `day`, `month`, `year`
|
||||
FROM `" . TABLE_PANEL_TRAFFIC . "`
|
||||
WHERE `customerid`= :customerid
|
||||
AND `month` = :month
|
||||
AND `year` = :year
|
||||
GROUP BY `day`
|
||||
ORDER BY `day` DESC");
|
||||
$params = [
|
||||
"customerid" => $userinfo['customerid'],
|
||||
"month" => $month,
|
||||
"year" => $year
|
||||
];
|
||||
Database::pexecute($result_stmt, $params);
|
||||
$traf['byte'] = 0;
|
||||
$traffic_complete['http'] = 0;
|
||||
$traffic_complete['ftp'] = 0;
|
||||
$traffic_complete['mail'] = 0;
|
||||
$traf['days'] = [];
|
||||
$traf['http_data'] = [];
|
||||
$traf['ftp_data'] = [];
|
||||
$traf['mail_data'] = [];
|
||||
|
||||
while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$http = $row['http'];
|
||||
$traf['http_data'][] = (float)$http;
|
||||
$ftp = $row['ftp_up'] + $row['ftp_down'];
|
||||
$traf['ftp_data'][] = (float)$ftp;
|
||||
$mail = $row['mail'];
|
||||
$traf['mail_data'][] = (float)$mail;
|
||||
$traf['byte'] = $http + $ftp + $mail;
|
||||
$traffic_complete['http'] += $http;
|
||||
$traffic_complete['ftp'] += $ftp;
|
||||
$traffic_complete['mail'] += $mail;
|
||||
$traf['days'][] = $row['day'];
|
||||
}
|
||||
|
||||
UI::view('user/traffic.html.twig', [
|
||||
'traffic_complete_http' => $traffic_complete['http'],
|
||||
'traffic_complete_ftp' => $traffic_complete['ftp'],
|
||||
'traffic_complete_mail' => $traffic_complete['mail'],
|
||||
'traffic_complete_total' => $traf['byte'],
|
||||
'labels' => $traf['days'],
|
||||
'http_data' => $traf['http_data'],
|
||||
'ftp_data' => $traf['ftp_data'],
|
||||
'mail_data' => $traf['mail_data'],
|
||||
]);
|
||||
} else {
|
||||
// default to the last 36 months
|
||||
$from_date = (new DateTime)->modify("last day of last months")->setTime(23, 59, 59)->sub(new \DateInterval('P3Y'))->format('U');
|
||||
$result_stmt = Database::prepare("
|
||||
SELECT `month`, `year`, SUM(`http`) AS http, SUM(`ftp_up`) AS ftp_up, SUM(`ftp_down`) AS ftp_down, SUM(`mail`) AS mail
|
||||
FROM `" . TABLE_PANEL_TRAFFIC . "`
|
||||
WHERE `customerid` = :customerid AND `stamp` = :fromdate
|
||||
GROUP BY `year`, `month`
|
||||
ORDER BY `year` ASC, `month` ASC
|
||||
");
|
||||
Database::pexecute($result_stmt, [
|
||||
"customerid" => $userinfo['customerid'],
|
||||
"fromdate" => $from_date
|
||||
]);
|
||||
$traffic_complete['http'] = 0;
|
||||
$traffic_complete['ftp'] = 0;
|
||||
$traffic_complete['mail'] = 0;
|
||||
$traf['days'] = [];
|
||||
$traf['http_data'] = [];
|
||||
$traf['ftp_data'] = [];
|
||||
$traf['mail_data'] = [];
|
||||
|
||||
while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$http = $row['http'];
|
||||
$traf['http_data'][] = (int)$http / 1024 / 1024;
|
||||
$ftp_up = $row['ftp_up'];
|
||||
$ftp_down = $row['ftp_down'];
|
||||
$traf['ftp_data'][] = (int)($ftp_up + $ftp_down) / 1024 / 1024;
|
||||
$mail = $row['mail'];
|
||||
$traf['mail_data'][] = (int)$mail / 1024 / 1024;
|
||||
$traffic_complete['http'] += $http;
|
||||
$traffic_complete['ftp'] += $ftp_up + $ftp_down;
|
||||
$traffic_complete['mail'] += $mail;
|
||||
$traf['month'] = $row['month'];
|
||||
$traf['year'] = $row['year'];
|
||||
$traf['monthname'] = lng('traffic.months.' . intval($row['month'])) . " " . $row['year'];
|
||||
$traf['byte'] = $http + $ftp_up + $ftp_down + $mail;
|
||||
$traf['byte_total'] = $traf['byte_total'] + $http + $ftp_up + $ftp_down + $mail;
|
||||
$traf['days'][] = $traf['monthname'];
|
||||
}
|
||||
|
||||
UI::view('user/traffic.html.twig', [
|
||||
'traffic_complete_http' => $traffic_complete['http'],
|
||||
'traffic_complete_ftp' => $traffic_complete['ftp'],
|
||||
'traffic_complete_mail' => $traffic_complete['mail'],
|
||||
'traffic_complete_total' => $traf['byte_total'],
|
||||
'labels' => $traf['days'],
|
||||
'http_data' => $traf['http_data'],
|
||||
'ftp_data' => $traf['ftp_data'],
|
||||
'mail_data' => $traf['mail_data'],
|
||||
]);
|
||||
}
|
||||
|
||||
function getReadableTraffic(&$traf, $index, $value, $divisor, $desc = "")
|
||||
{
|
||||
if (extension_loaded('bcmath')) {
|
||||
$traf[$index] = bcdiv($value, $divisor, Settings::Get('panel.decimal_places')) . (!empty($desc) ? " " . $desc : "");
|
||||
} else {
|
||||
$traf[$index] = round($value / $divisor, Settings::Get('panel.decimal_places')) . (!empty($desc) ? " " . $desc : "");
|
||||
}
|
||||
}
|
||||
UI::view('user/traffic.html.twig', [
|
||||
'metrics' => \Froxlor\Traffic\Traffic::getCustomerMetrics($userinfo),
|
||||
'chart' => \Froxlor\Traffic\Traffic::getCustomerChart($userinfo, 30),
|
||||
]);
|
||||
|
||||
100
lib/Froxlor/Traffic/Traffic.php
Normal file
100
lib/Froxlor/Traffic/Traffic.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Froxlor project.
|
||||
* Copyright (c) 2010 the Froxlor Team (see authors).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, you can also view it online at
|
||||
* https://files.froxlor.org/misc/COPYING.txt
|
||||
*
|
||||
* @copyright the authors
|
||||
* @author Froxlor team <team@froxlor.org>
|
||||
* @license https://files.froxlor.org/misc/COPYING.txt GPLv2
|
||||
*/
|
||||
|
||||
namespace Froxlor\Traffic;
|
||||
|
||||
use Froxlor\Api\Commands\Customers;
|
||||
use Froxlor\UI\Collection;
|
||||
|
||||
class Traffic
|
||||
{
|
||||
public static function getCustomerStats($userinfo, $range = null): array
|
||||
{
|
||||
$trafficCollection = (new Collection(\Froxlor\Api\Commands\Traffic::class, $userinfo, self::getParamsByRange($range, ['customer_traffic' => true,])))
|
||||
->has('customer', Customers::class, 'customerid', 'customerid')
|
||||
->get();
|
||||
|
||||
// build stats for each user
|
||||
$users = [];
|
||||
foreach ($trafficCollection['data']['list'] as $item) {
|
||||
$users[$item['customerid']]['loginname'] = $item['customer']['loginname'];
|
||||
$users[$item['customerid']]['total'] += ($item['http'] + $item['ftp_up'] + $item['ftp_down'] + $item['mail']);
|
||||
$users[$item['customerid']]['http'] += $item['http'];
|
||||
$users[$item['customerid']]['ftp'] += ($item['ftp_up'] + $item['ftp_down']);
|
||||
$users[$item['customerid']]['mail'] += $item['mail'];
|
||||
}
|
||||
|
||||
// calculate overview for given range from users
|
||||
$metrics = [];
|
||||
foreach ($users as $user) {
|
||||
$metrics['total'] += $user['total'];
|
||||
$metrics['http'] += $user['http'];
|
||||
$metrics['ftp'] += $user['ftp'];
|
||||
$metrics['mail'] += $user['mail'];
|
||||
}
|
||||
|
||||
return [
|
||||
'metrics' => $metrics,
|
||||
'users' => $users,
|
||||
];
|
||||
}
|
||||
|
||||
private static function getParamsByRange(string $range, array $params = [])
|
||||
{
|
||||
$dateParams = [];
|
||||
|
||||
if (preg_match("/year:([0-9]{4})/", $range, $matches)) {
|
||||
$dateParams = ['year' => $matches[1]];
|
||||
}
|
||||
|
||||
// TODO: get params by range: hours:x, days:x, months:x
|
||||
|
||||
return array_merge($dateParams, $params);
|
||||
}
|
||||
|
||||
public static function getCustomerChart($userinfo, $range = 30): array
|
||||
{
|
||||
// FIXME: this is currently an example for the chart
|
||||
|
||||
$data = [];
|
||||
|
||||
for ($i = 0; $i < $range; $i++) {
|
||||
$data['labels'][] = date("d.m", strtotime('-' . $i . ' days'));
|
||||
|
||||
// put data for given date
|
||||
$data['http'][] = 0;
|
||||
$data['ftp'][] = 0;
|
||||
$data['mail'][] = 0;
|
||||
}
|
||||
|
||||
return [
|
||||
'labels' => array_reverse($data['labels']),
|
||||
'http' => array_reverse($data['http']),
|
||||
'ftp' => array_reverse($data['ftp']),
|
||||
'mail' => array_reverse($data['mail']),
|
||||
'range' => $range,
|
||||
];
|
||||
}
|
||||
}
|
||||
0
lib/Froxlor/Traffic/index.html
Normal file
0
lib/Froxlor/Traffic/index.html
Normal file
@@ -13,84 +13,76 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- Range -->
|
||||
<!-- TODO: set url on change. e.g.: ?param=days:7 -->
|
||||
<div class="d-flex justify-content-end">
|
||||
<select class="form-select mb-4 w-auto mt-n4" aria-label="select the traffic range" name="range">
|
||||
<option value="hours:24">last 24 hours</option>
|
||||
<option value="days:7">last 7 days</option>
|
||||
<option value="days:30">last 30 days</option>
|
||||
<option value="months:3">last 3 months</option>
|
||||
<option value="months:6">last 6 months</option>
|
||||
<option value="months:12">last 12 months</option>
|
||||
<option value="year:2022">2022</option>
|
||||
<option value="year:2021">2021</option>
|
||||
<option value="year:2020">2020</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Overview for given range -->
|
||||
<div class="row row-cols-4 g-0 bg-white rounded shadow-sm mb-4">
|
||||
<div class="col p-3 border-end">
|
||||
<h3>{{ traffic_complete_http|formatBytes }}</h3>
|
||||
<h3>{{ metrics.total|formatBytes }}</h3>
|
||||
<span>Total</span>
|
||||
</div>
|
||||
<div class="col p-3 border-end">
|
||||
<h3>{{ metrics.http|formatBytes }}</h3>
|
||||
<span>HTTP</span>
|
||||
</div>
|
||||
<div class="col p-3 border-end">
|
||||
<h3>{{ traffic_complete_ftp|formatBytes }}</h3>
|
||||
<h3>{{ metrics.ftp|formatBytes }}</h3>
|
||||
<span>FTP</span>
|
||||
</div>
|
||||
<div class="col p-3 border-end">
|
||||
<h3>{{ traffic_complete_mail|formatBytes }}</h3>
|
||||
<h3>{{ metrics.mail|formatBytes }}</h3>
|
||||
<span>Mail</span>
|
||||
</div>
|
||||
<div class="col p-3 border-end">
|
||||
<h3>{{ traffic_complete_total|formatBytes }}</h3>
|
||||
<span>Total</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Traffic
|
||||
<!-- Overview for given range by user -->
|
||||
<h4 class="page-header">Traffic by customers</h4>
|
||||
{% if users is not empty %}
|
||||
<div class="card table-responsive">
|
||||
<table class="table table-borderless table-striped align-middle mb-0 px-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ lng('login.username') }}</th>
|
||||
<th scope="col">Total</th>
|
||||
<th scope="col">HTTP</th>
|
||||
<th scope="col">FTP</th>
|
||||
<th scope="col">Mail
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.loginname }}</td>
|
||||
<td>{{ user.total|formatBytes }}</td>
|
||||
<td>{{ user.http|formatBytes }}</td>
|
||||
<td>{{ user.ftp|formatBytes }}</td>
|
||||
<td>{{ user.mail|formatBytes }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="trafficChart" height="75"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if stats is defined %}
|
||||
{% for yearly_stats in stats %}
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Traffic per {{ yearly_stats.type }} in {{ yearly_stats.year }}
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
{{ yearly_stats.data|json_encode }}
|
||||
<div class="card-body">
|
||||
<p>No data for given range found.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
const data = {
|
||||
labels: {{ labels|json_encode|raw }},
|
||||
datasets: [{
|
||||
label: 'Web',
|
||||
backgroundColor: '#22D3EE',
|
||||
borderColor: '#0891B2',
|
||||
data: {{ http_data|json_encode|raw }},
|
||||
fill: true
|
||||
},{
|
||||
label: 'FTP',
|
||||
backgroundColor: '#34D399',
|
||||
borderColor: '#059669',
|
||||
data: {{ ftp_data|json_encode|raw }},
|
||||
fill: true
|
||||
},{
|
||||
label: 'Mail',
|
||||
backgroundColor: '#FDE047',
|
||||
borderColor: '#CA8A04',
|
||||
data: {{ mail_data|json_encode|raw }},
|
||||
fill: true
|
||||
}]
|
||||
};
|
||||
|
||||
const ctx = document.getElementById('trafficChart');
|
||||
const myChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
stacked: true,
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -67,7 +67,7 @@ a:hover {
|
||||
<span> <img
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAAAQCAYAAAC1MDndAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAy1JREFUeNrs1muIVlUUBuBnvqZkoswudKErZFOG0kWt6WpN0wWCDMOsgX4UmVS/KovKhIKYLhRBJvV1o/5VlJCmYEmUFYijaBYhSRQVBVE0ZnRBJ/vzfnA4nDN+wTh/pgWHc/baa6+z97v3et/d0Ww24SQ8ij5MNL7tN6zBvdjWiZOxDpP8b3JA5qAXPQ0MtAnOm9jZBvqbsQHbsGsMFrQLv+KPUc47CQONlFXZPsNfJd/buBrDFfHDWIDDcAZmohsPjwFAD+EQnL8Xcl/WqOGc+/FJyXcxVuHJivjX8Hx2cwGeweO4fIxOkL10Wg9o1HSsxUcl30V5P1JxnFuxp+M53I67cQ5mYwZexj2YlnarJBdl3BE4FfPxdfofSOwNhX+uwgXxf1Uz/024DpNxFHrwRIEiPs74XnyIq3A8Xion6qxIvj0TX1vyb8x7CJ/jrELfjrx/zgmS0p2bCWzHzejAQTgmO35J+OpInIcv8GLKeROuzcI24lAsRH/yzcGJFfMfDIB/B/ipeD8bNojX8W1hPR+gCxPwTzsAdaGRgTeGrAZLJXd4zc59l1Jr2dzC91S8g+PSXhFwGgGxO4s6Bd9gaQRkAHdhSQHso/FCzRweS56ezLmB1bgCb+DBCg67rwaLSud+uDAAvVLRfw1OqCO18JEKbusvgNMqA/F153sCpgegT+O7Iwt8NwrZwKsh5irbUuDMFoXMquhv2eKRSKiOg54OUFXSt3SEfPvi4Dz77IEAW/8uH+vdpf6dkXGF+F9GyNtRkXe4or8tqwNoWrigbDeFTEfDZhbKckuBy9blu0XkC1Pi++PK+Objy5q80/NeXSDl9wrgnDkaAIlyzCj5zh1FCb00JL07Jd2LKfgBx+LWXE6XJP6pcMiUiMg8/FmRd3GEYHNiZ+H6wgZP/i+T7MzPJo5wkjYU2t01cWfjp6hH2WZn0adVbM7KALAS3weYftwZIVgTIHtxS8YtD6kOYX2Uqi8iIKCsD6CDhXnNi+iI+vXhwD3g83tHs9l8K5JZZbfh2UL7x0jyeLFljVzUhtoc0DWOwBnCoga25s6wrHDhG8+2I1j0YOu/AwBUU7aBHvM/ZwAAAABJRU5ErkJggg=="
|
||||
style="height: 13px; margin: 0 2px 3px 0; vertical-align: middle;" />
|
||||
© 2009-2021 by <a href="http://www.froxlor.org">the Froxlor
|
||||
© 2009-2022 by <a href="http://www.froxlor.org">the Froxlor
|
||||
Team</a>
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user