| Server IP : 188.114.96.3 / Your IP : 216.73.216.143 Web Server : Apache System : Linux sv058.home.net.pl 6.8.2 husak_dirty_pages_fix #1 SMP PREEMPT_DYNAMIC Tue Apr 2 22:06:51 CEST 2024 x86_64 User : ( 3194769) PHP Version : 8.5.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /polana/includes/ |
Upload File : |
<?php
declare(strict_types=1);
/**
* POLANA V18.6 - optional push notifications, stored only outside public WWW.
* This library never changes reservations, calendar OAuth, or backup configuration.
*/
require_once __DIR__ . '/polana_backup_v182.php';
function pnt186_config_path(): string {
return pbk_private() . '/polana_ntfy_v186.json';
}
function pnt186_config(): array {
$defaults = ['enabled' => false, 'url' => '', 'topic' => '', 'token_enc' => ''];
$path = pnt186_config_path();
if (!is_file($path)) return $defaults;
$data = json_decode((string)file_get_contents($path), true);
if (!is_array($data)) throw new RuntimeException('Nieprawidłowy format ustawień ntfy.');
return array_merge($defaults, array_intersect_key($data, $defaults));
}
function pnt186_validate_url(string $url): string {
$url = trim($url);
$parsed = parse_url($url);
if (!is_array($parsed) || strtolower((string)($parsed['scheme'] ?? '')) !== 'https'
|| empty($parsed['host']) || isset($parsed['user']) || isset($parsed['pass'])
|| isset($parsed['query']) || isset($parsed['fragment'])
|| isset($parsed['port']) || !in_array(($parsed['path'] ?? ''), ['', '/'], true)) {
throw new RuntimeException('Podaj adres HTTPS serwera ntfy, np. https://ntfy.example.com, bez portu i ścieżki.');
}
$host = strtolower((string)$parsed['host']);
if (!preg_match('/^(?=.{1,253}$)[a-z0-9](?:[a-z0-9.-]*[a-z0-9])$/D', $host)
|| !str_contains($host, '.') || str_ends_with($host, '.local') || str_ends_with($host, '.internal')
|| str_ends_with($host, '.localhost') || str_ends_with($host, '.test')
|| filter_var($host, FILTER_VALIDATE_IP) !== false) {
throw new RuntimeException('Wymagana publiczna domena DNS serwera ntfy.');
}
return 'https://' . $host;
}
function pnt186_validate_topic(string $topic): string {
$topic = trim($topic);
if (!preg_match('/^[A-Za-z0-9_-]{12,80}$/D', $topic)) {
throw new RuntimeException('Temat ntfy musi mieć 12-80 znaków: litery, cyfry, - lub _. Użyj trudnego do odgadnięcia tematu.');
}
return $topic;
}
function pnt186_save(array $settings): void {
$path = pnt186_config_path();
$tmp = dirname($path) . '/.ntfy-v186-' . bin2hex(random_bytes(8)) . '.tmp';
$bytes = json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
$handle = @fopen($tmp, 'xb');
if (!$handle) throw new RuntimeException('Nie można zapisać konfiguracji ntfy poza WWW.');
@chmod($tmp, 0600);
try {
if (fwrite($handle, $bytes) !== strlen($bytes)) throw new RuntimeException('Nie zapisano całej konfiguracji.');
} catch (Throwable $e) { fclose($handle); @unlink($tmp); throw $e; }
fclose($handle);
if (!@rename($tmp, $path)) { @unlink($tmp); throw new RuntimeException('Nie zapisano ustawień ntfy.'); }
}
function pnt186_send(string $title, string $message, string $tag = 'warning'): void {
$cfg = pnt186_config();
if (empty($cfg['enabled'])) throw new RuntimeException('Powiadomienia ntfy są wyłączone.');
$base = pnt186_validate_url((string)$cfg['url']);
$topic = pnt186_validate_topic((string)$cfg['topic']);
if (!function_exists('curl_init')) throw new RuntimeException('Hosting nie udostępnia rozszerzenia cURL.');
$title = preg_replace('/[^\x20-\x7E]/', '', $title);
$title = substr(trim((string)$title), 0, 100);
$tag = preg_match('/^[a-z0-9_-]{1,24}$/D', $tag) ? $tag : 'warning';
$message = trim($message);
if ($message === '' || strlen($message) > 1500) throw new RuntimeException('Nieprawidłowy rozmiar komunikatu.');
$headers = ['Content-Type: text/plain; charset=utf-8', 'Title: ' . $title, 'Tags: ' . $tag, 'Priority: 3'];
if (!empty($cfg['token_enc'])) {
$token = pbk_secret_decrypt((string)$cfg['token_enc']);
if (!preg_match('/^[\x21-\x7E]{1,500}$/D', $token)) throw new RuntimeException('Niepoprawny token ntfy.');
$headers[] = 'Authorization: Bearer ' . $token;
}
$ch = curl_init($base . '/' . rawurlencode($topic));
if ($ch === false) throw new RuntimeException('Nie można uruchomić HTTP ntfy.');
$options = [
CURLOPT_POST => true, CURLOPT_POSTFIELDS => $message, CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_FOLLOWLOCATION => false, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'POLANA-notify/18.6',
];
if (defined('CURLOPT_PROTOCOLS_STR')) $options[CURLOPT_PROTOCOLS_STR] = 'https';
elseif (defined('CURLOPT_PROTOCOLS')) $options[CURLOPT_PROTOCOLS] = CURLPROTO_HTTPS;
curl_setopt_array($ch, $options);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($body === false || $status < 200 || $status >= 300) {
// Do not expose request URL, authorization header or raw provider body.
throw new RuntimeException('Wysyłka ntfy nieudana (HTTP ' . (int)$status . ($err ? ', połączenie/TLS' : '') . ').');
}
}
/** Returns a redacted snapshot. This does not create archives or modify reservations. */
function pnt186_backup_state(): array {
$folder = pbk_private() . '/daily';
$files = glob($folder . '/*.pbackup') ?: [];
$latest = 0;
foreach ($files as $file) {
if (is_file($file)) $latest = max($latest, (int)(filemtime($file) ?: 0));
}
$age = $latest ? max(0, time() - $latest) : null;
$valid = $latest !== 0 && $age <= 30 * 3600;
return ['healthy' => $valid, 'count' => count($files), 'latest_ts' => $latest,
'age_h' => $age === null ? null : round($age / 3600, 1)];
}
function pnt186_state_path(): string { return pbk_private() . '/polana_ntfy_v186_state.json'; }
/** At-most-one notification per unhealthy episode, plus one recovery message. */
function pnt186_check_backup(): array {
$lockFile = pbk_private() . '/polana_ntfy_v186.lock';
$lock = @fopen($lockFile, 'c+');
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) throw new RuntimeException('Kontrola ntfy już działa.');
try {
$cfg = pnt186_config();
if (empty($cfg['enabled'])) return ['status' => 'disabled'];
$snap = pnt186_backup_state();
$current = $snap['healthy'] ? 'ok' : 'alert';
$path = pnt186_state_path();
$old = is_file($path) ? json_decode((string)file_get_contents($path), true) : [];
if (!is_array($old)) $old = [];
$previous = (string)($old['status'] ?? '');
$lastAlertAt = (int)($old['last_alert_at'] ?? 0);
// Re-alert after 24h, not every CRON run. On change to healthy send a single recovery message.
$shouldSend = $current === 'alert' ? ($previous !== 'alert' || (time() - $lastAlertAt) >= 86400)
: $previous === 'alert';
if ($shouldSend) {
if ($current === 'alert') {
pnt186_send('POLANA backup', 'Brak aktualnej kopii SQL (maksymalny wiek 30 godzin). Sprawdź Centrum backupów w panelu Polany.', 'warning');
$lastAlertAt = time();
} else {
pnt186_send('POLANA backup OK', 'Ponownie wykryto aktualną kopię SQL. Sprawdź integralność i odzyskanie w panelu.', 'white_check_mark');
}
}
$state = ['status' => $current, 'last_alert_at' => $lastAlertAt, 'checked_at' => date(DATE_ATOM)];
$tmp = $path . '.' . bin2hex(random_bytes(4)) . '.tmp';
if (file_put_contents($tmp, json_encode($state, JSON_THROW_ON_ERROR), LOCK_EX) === false) {
throw new RuntimeException('Nie zapisano stanu monitoringu.');
}
@chmod($tmp, 0600);
if (!@rename($tmp, $path)) { @unlink($tmp); throw new RuntimeException('Błąd zapisu stanu monitoringu.'); }
return ['status' => $current, 'sent' => $shouldSend, 'snap' => $snap];
} finally {
flock($lock, LOCK_UN);
fclose($lock);
}
}