| 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 V17.2: safe, limited-scope patcher of the existing PHP app. */
require_once __DIR__ . '/functions.php';
require_once __DIR__ . '/polana_v172_newsletter.php';
require_admin_role();
header('Cache-Control: private, no-store, max-age=0');
header('X-Robots-Tag: noindex,nofollow');
function pi172_root():string { return dirname(__DIR__); }
function pi172_backupdir():string { return dirname(pi172_root()).'/.polana_v172_private_backups'; }
function pi172_marker():string { return 'POLANA_V172_NEWSLETTER_ADAPTER'; }
function pi172_targets():array {
$root=pi172_root();
return [
'includes/newsletter.php'=>$root.'/includes/newsletter.php',
'newsletter.php'=>$root.'/newsletter.php',
'rezerwacja.php'=>$root.'/rezerwacja.php',
'includes/reservations.php'=>$root.'/includes/reservations.php',
'includes/functions.php'=>$root.'/includes/functions.php',
];
}
function pi172_one(string $contents,string $needle,string $replacement,string $label):string {
if (strpos($contents,$needle)===false) throw new RuntimeException('Nie znaleziono wzorca: '.$label.'. Instalacja przerwana, bez nadpisywania nieznanego kodu.');
if (substr_count($contents,$needle)!==1) throw new RuntimeException('Wzorzec niejednoznaczny: '.$label);
return str_replace($needle,$replacement,$contents);
}
function pi172_patch(string $relative,string $content):string {
if ($relative==='includes/newsletter.php') {
if (str_contains($content,pi172_marker())) return $content;
$regex='~function newsletter_subscribe\(.*?\): array\s*\{.*?\n\}\s*(?=function newsletter_unsubscribe\()~s';
if(preg_match_all($regex,$content,$m)!==1) throw new RuntimeException('Niezgodna struktura funkcji newsletter_subscribe.');
$replacement = <<<'REPLACE'
/** POLANA_V172_NEWSLETTER_ADAPTER: double opt-in instead of automatic activation */
function newsletter_subscribe(string $email, string $name='', string $source='footer'): array {
require_once __DIR__ . '/polana_v172_newsletter.php';
return pv172_subscribe($email, $name, $source);
}
REPLACE;
return preg_replace_callback($regex,static fn($match)=>$replacement,$content,1) ?? throw new RuntimeException('Nie udało się dostosować newslettera.');
}
if ($relative==='newsletter.php') {
if(str_contains($content,'POLANA_V172_CONFIRM_FLOW')) return $content;
$content=pi172_one($content,"require_once __DIR__ . '/includes/newsletter.php';", "require_once __DIR__ . '/includes/newsletter.php';\nrequire_once __DIR__ . '/includes/polana_v172_newsletter.php';\n/* POLANA_V172_CONFIRM_FLOW */\nheader('Cache-Control: private, no-store');\nheader('Referrer-Policy: no-referrer');\nheader('X-Robots-Tag: noindex,nofollow');
start_session();",'newsletter public include');
$branch=<<<'BRANCH'
if (isset($_GET['confirm'])) {
$mode = 'confirm';
$confirmToken = trim((string)$_GET['confirm']);
$message = preg_match('/^[a-f0-9]{64}$/D', $confirmToken)
? 'Kliknij przycisk poniżej, aby potwierdzić zapis. Samo otwarcie linku nie aktywuje adresu.'
: 'Nieprawidłowy link potwierdzenia.';
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['newsletter_confirm_token'])) {
$mode = 'confirm';
verify_csrf();
try {
$ok = pv172_confirm(trim((string)$_POST['newsletter_confirm_token']));
$message = $ok ? 'Adres potwierdzony. Dziękujemy za zapis!' : 'Link już wykorzystany, wygasły lub nieprawidłowy.';
} catch (Throwable $ex) {
$message = 'Nie udało się potwierdzić adresu. Spróbuj ponownie.';
error_log('[POLANA NEWSLETTER confirm] '.$ex->getMessage());
}
} elseif (isset($_GET['unsubscribe']))
BRANCH;
$content=pi172_one($content,"if (isset(\$_GET['unsubscribe']))",$branch,'newsletter confirmation routing');
$content=pi172_one($content,"<h1><?= \$mode === 'unsubscribe' ? 'Newsletter' : 'Dołącz do newslettera' ?></h1>","<h1><?= \$mode === 'unsubscribe' ? 'Newsletter' : (\$mode === 'confirm' ? 'Potwierdź zapis' : 'Dołącz do newslettera') ?></h1>",'newsletter confirmation title');
$form=<<<'FORM'
<?php if (!$ok && $mode === 'confirm' && !empty($confirmToken) && preg_match('/^[a-f0-9]{64}$/D', $confirmToken)): ?>
<form method="post" action="/newsletter.php" class="newsletter-page__form">
<?= csrf_field() ?>
<input type="hidden" name="newsletter_confirm_token" value="<?= e($confirmToken) ?>">
<button type="submit">Potwierdzam zapis na newsletter</button>
</form>
<?php elseif (!$ok && $mode !== 'unsubscribe' && $mode !== 'confirm'): ?>
FORM;
$content=pi172_one($content,"<?php if (!\$ok && \$mode !== 'unsubscribe'): ?>",$form,'newsletter form confirmation');
return $content;
}
if ($relative==='rezerwacja.php') {
// FIX V17.3.1: nie wymagamy identycznego komentarza/formatowania starszej wstawki.
// Usuwamy tylko znane, przedwczesne wywołanie przed zapisem rezerwacji.
if (str_contains($content, 'POLANA_V172_RESERVATION_OPTIN_AFTER_SAVE')) return $content;
$createNeedle = '$reservation = reservation_create(';
if (substr_count($content, $createNeedle) !== 1) {
throw new RuntimeException('Rezerwacja: niejednoznaczny zapis reservation_create — nic nie zmieniono.');
}
$createAt = strpos($content, $createNeedle);
$sendAt = strpos($content, 'reservation_send_after_create(', $createAt);
if ($sendAt === false || $sendAt <= $createAt) {
throw new RuntimeException('Rezerwacja: brak bezpiecznego punktu po zapisaniu rezerwacji — nic nie zmieniono.');
}
$prefix = substr($content, 0, $createAt);
// Obsługiwane są pojedyncze wywołania w wariancie 1-liniowym lub blokowym,
// z komentarzem V1.1 lub bez. Nigdy nie usuwamy dowolnego nieznanego kodu.
$earlyCall = '~^[ \\t]*(?:/\\*\\s*POLANA_NEWSLETTER_RESERVATION_POST_V1_1\\s*\\*/[ \\t]*\\n)?[ \\t]*if\\s*\\(\\s*function_exists\\s*\\(\\s*[\\\'\"]newsletter_handle_reservation_optin[\\\'\"]\\s*\\)\\s*\\)\\s*(?:\\{[ \\t]*\\n[ \\t]*)?newsletter_handle_reservation_optin\\s*\\(\\s*\\)\\s*;[ \\t]*(?:\\n[ \\t]*\\})?[ \\t]*(?:\\n|$)~m';
$callsInPrefix = preg_match_all('~\\bnewsletter_handle_reservation_optin\\s*\\(\\s*\\)\\s*;~', $prefix);
if ($callsInPrefix === false) throw new RuntimeException('Błąd analizy newslettera.');
if ($callsInPrefix > 1) throw new RuntimeException('Wiele przedwczesnych wywołań newslettera — wymagana kontrola kodu.');
if ($callsInPrefix === 1) {
$matches = [];
if (preg_match_all($earlyCall, $prefix, $matches) !== 1) {
throw new RuntimeException('Nieznany wariant wczesnego zapisu newslettera — bez zmian.');
}
$prefix = preg_replace($earlyCall, '', $prefix, 1, $removed);
if (!is_string($prefix) || $removed !== 1) throw new RuntimeException('Nie można bezpiecznie usunąć starego wywołania newslettera.');
$content = $prefix . substr($content, $createAt);
$createAt = strpos($content, $createNeedle);
$sendAt = strpos($content, 'reservation_send_after_create(', $createAt);
}
$successBlock = substr($content, $createAt, $sendAt - $createAt);
$successCalls = preg_match_all('~\\bnewsletter_handle_reservation_optin\\s*\\(\\s*\\)\\s*;~', $successBlock);
if ($successCalls === false || $successCalls > 1) {
throw new RuntimeException('Niejednoznaczny zapis newslettera po rezerwacji.');
}
// Jeśli wcześniejsza poprawka już przeniosła wywołanie, nie duplikujemy go.
if ($successCalls === 1) return $content;
$sessionLine = '~(?m)^([ \\t]*)(\\$_SESSION\\[\\s*[\\\'\"]reservation_last_submit[\\\'\"]\\s*\\]\\s*=\\s*time\\(\\)\\s*;)~';
if (preg_match_all($sessionLine, $successBlock, $m, PREG_SET_ORDER) !== 1) {
throw new RuntimeException('Nie znaleziono jednoznacznego miejsca po poprawnym zapisie rezerwacji.');
}
$indent = $m[0][1];
$old = $m[0][0];
$new = $old . "\\n" . $indent . '/* POLANA_V172_RESERVATION_OPTIN_AFTER_SAVE: zgoda dopiero po zapisie */' . "\\n" . $indent . "if (function_exists('newsletter_handle_reservation_optin')) newsletter_handle_reservation_optin();";
$updated = substr_replace($successBlock, str_replace('\\n', "\n", $new), strpos($successBlock, $old), strlen($old));
return substr($content, 0, $createAt) . $updated . substr($content, $sendAt);
}
if ($relative==='includes/reservations.php') {
if(str_contains($content,'POLANA_V172_REPLY_TO')) return $content;
$before=<<<'BEFORE'
smtp_send_mail(
(string)$reservation['email'],
$subject,
reservation_email_html($subject, $body),
$body
);
BEFORE;
$after=<<<'AFTER'
/* POLANA_V172_REPLY_TO: gość odpisuje do rezerwacji, nie do no-reply */
smtp_send_mail(
(string)$reservation['email'],
$subject,
reservation_email_html($subject, $body),
$body,
trim(reservation_setting('reservation_admin_email')) ?: null
);
AFTER;
return pi172_one($content,$before,$after,'guest autoresponder Reply-To');
}
if ($relative==='includes/functions.php') {
if(str_contains($content,'POLANA_V172_ADMIN_TOP_LINKS')) return $content;
$needle='<a href="/admin/">Pulpit</a>';
$insert=$needle."\n <!-- POLANA_V172_ADMIN_TOP_LINKS -->\n <?php if (\$role === 'admin'): ?>\n <a href=\"/admin/polana-komunikacja.php\">Poczta i komunikacja</a>\n <a href=\"/admin/polana-newsletter.php\">Newsletter</a>\n <?php endif; ?>";
return pi172_one($content,$needle,$insert,'admin top navigation');
}
throw new RuntimeException('Nieobsługiwany plik '.$relative);
}
function pi172_write_atomic(string $file,string $contents):void {
$temp=$file.'.p172_'.bin2hex(random_bytes(6)).'.tmp';
if(@file_put_contents($temp,$contents,LOCK_EX)!==strlen($contents)){
@unlink($temp);throw new RuntimeException('Nie można zapisać pliku '.basename($file));
}
$mode=@fileperms($file);if($mode!==false)@chmod($temp,$mode&0777);
if(!@rename($temp,$file)){
@unlink($temp);throw new RuntimeException('Nie można podmienić pliku '.basename($file));
}
}
function pi172_settings_targets():array {
return [
'reservation_admin_email'=>'[email protected]',
'reservation_auto_reply_enabled'=>'1',
];
}
function pi172_setting_snapshot(array $settings):array {
$select=db()->prepare('SELECT setting_value FROM settings WHERE setting_key=? LIMIT 1');
$rows=[];
foreach($settings as $k=>$value){$select->execute([$k]);$old=$select->fetchColumn();$rows[$k]=['had'=>$old!==false,'before'=>$old!==false?(string)$old:null,'after'=>$value];}
return $rows;
}
function pi172_save_settings(array $settings):void {
$stmt=db()->prepare('INSERT INTO settings(setting_key,setting_value) VALUES(?,?) ON DUPLICATE KEY UPDATE setting_value=VALUES(setting_value)');
foreach($settings as $key=>$value)$stmt->execute([$key,$value]);
}
function pi172_install():string {
if(!is_file(pi172_root().'/includes/polana_v172_newsletter.php') || !is_file(pi172_root().'/admin/polana-newsletter.php') || !is_file(pi172_root().'/admin/polana-komunikacja.php')){
throw new RuntimeException('Najpierw wgraj WSZYSTKIE pliki ZIP do odpowiednich katalogów.');
}
$changes=[];
foreach(pi172_targets() as $relative=>$file){
if(!is_file($file) || !is_readable($file) || !is_writable($file))throw new RuntimeException('Brak odczytu/zapisu: '.$relative);
$before=file_get_contents($file);
if(!is_string($before))throw new RuntimeException('Brak odczytu: '.$relative);
$normalized=str_replace("\r\n","\n",$before);
$patched=pi172_patch($relative,$normalized);
$after=str_contains($before,"\r\n") ? str_replace("\n","\r\n",$patched) : $patched;
if($after!==$before)$changes[$relative]=['file'=>$file,'before'=>$before,'after'=>$after];
}
if (!$changes) {
return 'V17.2 jest już zainstalowana — nie utworzono kolejnego backupu ani nie zmieniono ustawień.';
}
$settingSnapshot=pi172_setting_snapshot(pi172_settings_targets());
$dir=pi172_backupdir();
if(!is_dir($dir)&&!@mkdir($dir,0700,true))throw new RuntimeException('Nie można utworzyć prywatnych backupów poza WWW.');
if(!is_writable($dir))throw new RuntimeException('Katalog backupu nie jest zapisywalny.');
$path=$dir.'/'.date('Ymd_His').'_'.bin2hex(random_bytes(4));
if(!@mkdir($path,0700))throw new RuntimeException('Nie można utworzyć folderu kopii.');
$manifest=['version'=>'17.2','files'=>[],'settings'=>$settingSnapshot];
foreach($changes as $relative=>$c){
$backup='original-'.count($manifest['files']).'.bak';
if(@file_put_contents($path.'/'.$backup,$c['before'],LOCK_EX)!==strlen($c['before']))throw new RuntimeException('Nie udało się zapisać backupu. Nic nie zmieniono.');
@chmod($path.'/'.$backup,0600);
$manifest['files'][$relative]=['backup'=>$backup,'before_sha'=>hash('sha256',$c['before']),'after_sha'=>hash('sha256',$c['after'])];
}
$manifestJson=json_encode($manifest,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);
if(!$manifestJson || @file_put_contents($path.'/manifest.json',$manifestJson,LOCK_EX)!==strlen($manifestJson))throw new RuntimeException('Brak zapisu manifestu. Nic nie zmieniono.');
@chmod($path.'/manifest.json',0600);
// MariaDB DDL is additive. It does not alter existing subscriber status, consent or data.
pv172_upgrade_schema();
$written=[];
try {
foreach($changes as $relative=>$c){
$current=@file_get_contents($c['file']);
if($current!==$c['before'])throw new RuntimeException('Plik zmienił się w trakcie instalacji: '.$relative);
pi172_write_atomic($c['file'],$c['after']);$written[$relative]=$c;
}
$pdo=db();$pdo->beginTransaction();
try{pi172_save_settings(pi172_settings_targets());$pdo->commit();}catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
}catch(Throwable $e){
foreach(array_reverse($written) as $c){
if(@file_get_contents($c['file'])===$c['after']){
try{pi172_write_atomic($c['file'],$c['before']);}catch(Throwable $ignored){}
}
}
throw $e;
}
try { admin_log('polana_v172_install','Aktywowano newsletter double opt-in, komunikację i Reply-To (bez haseł)'); } catch(Throwable $ignored) {}
return 'Instalacja ukończona. Zmieniono '.count($changes).' plików i skonfigurowano odbiorcę rezerwacji. Kliknij „Poczta i komunikacja”.';
}
function pi172_rollback():string {
$paths=glob(pi172_backupdir().'/*/manifest.json')?:[];rsort($paths);
$last=null;
foreach($paths as $candidate){if(str_ends_with($candidate,'/manifest.json')){$last=$candidate;break;}}
if(!$last)throw new RuntimeException('Nie znaleziono wykonanej kopii V17.2.');
$manifest=json_decode((string)file_get_contents($last),true);
if(!is_array($manifest)||($manifest['version']??'')!=='17.2')throw new RuntimeException('Nieprawidłowa kopia V17.2.');
$restores=[];$dir=dirname($last);
foreach(($manifest['files']??[]) as $relative=>$item){
$targets=pi172_targets();
if(!isset($targets[$relative]))throw new RuntimeException('Nieznany plik w kopii: '.$relative);
$target=$targets[$relative];
$saved=@file_get_contents($dir.'/'.basename((string)$item['backup']));
$current=@file_get_contents($target);
if(!is_string($saved)||!is_string($current)||hash('sha256',$saved)!==$item['before_sha']||hash('sha256',$current)!==$item['after_sha']){
throw new RuntimeException('Plik '.$relative.' został później zmieniony lub kopia jest uszkodzona. Nie cofnięto niczego.');
}
$restores[]=[$target,$saved];
}
$currentSettings=pi172_setting_snapshot(pi172_settings_targets());
foreach(($manifest['settings']??[]) as $key=>$v){
if(!isset($currentSettings[$key]) || $currentSettings[$key]['before']!==(string)$v['after'])throw new RuntimeException('Ustawienie '.$key.' było zmieniane później — rollback zablokowany.');
}
foreach($restores as [$path,$saved])pi172_write_atomic($path,$saved);
$pdo=db();$pdo->beginTransaction();
try{
$up=$pdo->prepare('INSERT INTO settings(setting_key,setting_value) VALUES(?,?) ON DUPLICATE KEY UPDATE setting_value=VALUES(setting_value)');
$del=$pdo->prepare('DELETE FROM settings WHERE setting_key=?');
foreach(($manifest['settings']??[]) as $key=>$v){if($v['had'])$up->execute([$key,$v['before']]);else $del->execute([$key]);}
$pdo->commit();
}catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
@rename($last,$last.'.restored');
return 'Przywrócono pliki i ustawienia z poprzedniej wersji. Dane zgód newslettera zostały zachowane. Dodatkowe kolumny bazy pozostają bezpiecznie w tabeli.';
}