| 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/admin/ |
Upload File : |
<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/includes/functions.php';
$user = require_blog_manager();
ensure_blog_schema();
blog_promote_due_posts();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
try {
$action = (string)($_POST['action'] ?? '');
if ($action === 'bulk_schedule') {
$ids = $_POST['post_ids'] ?? [];
if (!is_array($ids)) $ids = [];
$ids = array_values(array_unique(array_filter(array_map('intval',$ids))));
if (!$ids) throw new RuntimeException('Zaznacz co najmniej jeden szkic.');
$rawStart = trim((string)($_POST['start_at'] ?? ''));
$start = DateTimeImmutable::createFromFormat('Y-m-d\TH:i',$rawStart);
if (!$start) throw new RuntimeException('Podaj datę startową harmonogramu.');
if ($start <= new DateTimeImmutable('now')) throw new RuntimeException('Start harmonogramu musi być w przyszłości.');
$minGap = max(1,min(60,(int)($_POST['gap_min'] ?? 7)));
$maxGap = max($minGap,min(90,(int)($_POST['gap_max'] ?? 12)));
$current = $start;
$stmt = db()->prepare(
"UPDATE blog_posts
SET status='scheduled',publish_at=?,updated_by_user_id=?,updated_at=NOW()
WHERE id=? AND status='draft'"
);
$scheduled = 0;
foreach ($ids as $idx=>$id) {
if ($idx > 0) {
$current = $current->modify('+' . random_int($minGap,$maxGap) . ' days');
}
$stmt->execute([$current->format('Y-m-d H:i:s'),(int)$user['id'],$id]);
$scheduled += $stmt->rowCount();
}
admin_log('blog_bulk_schedule','Zaplanowano wpisów: '.$scheduled);
flash('success','Zaplanowano wpisów: '.$scheduled.'. Publikacja wykona się automatycznie przy pierwszym wejściu po terminie – bez crona.');
redirect('/admin/blog.php');
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
$post = blog_post_by_id($id);
if (!$post) throw new RuntimeException('Nie znaleziono wpisu.');
db()->prepare('DELETE FROM blog_posts WHERE id=?')->execute([$id]);
/* PIN77_AUTO_BLOG_DELETE */
require_once dirname(__DIR__) . '/includes/polana_indexnow.php'; pin77_after_blog_delete($post);
admin_log('blog_delete','Usunięto wpis: '.$post['title']);
flash('success','Wpis został usunięty.');
redirect('/admin/blog.php');
}
} catch (Throwable $e) {
flash('error',$e->getMessage());
redirect('/admin/blog.php');
}
}
/* POLANA V18: filtered, paginated blog. Scheduler operates on visible draft rows. */
$blogStatus=(string)($_GET['status']??'all');if(!in_array($blogStatus,['all','draft','scheduled','published'],true))$blogStatus='all';
$blogSearch=trim((string)($_GET['q']??''));if(strlen($blogSearch)>100)$blogSearch=substr($blogSearch,0,100);
$blogPer=(int)($_GET['per_page']??20);if(!in_array($blogPer,[20,50,100],true))$blogPer=20;
$blogPage=max(1,(int)($_GET['page']??1));
$blogCounts=['draft'=>0,'scheduled'=>0,'published'=>0];
foreach(db()->query('SELECT status,COUNT(*) AS n FROM blog_posts GROUP BY status')->fetchAll(PDO::FETCH_ASSOC) as $r)if(isset($blogCounts[$r['status']]))$blogCounts[$r['status']]=(int)$r['n'];
$draftCount=$blogCounts['draft'];$scheduledCount=$blogCounts['scheduled'];$publishedCount=$blogCounts['published'];
$where=[];$args=[];
if($blogStatus!=='all'){$where[]='p.status=?';$args[]=$blogStatus;}
if($blogSearch!==''){$where[]='(p.title LIKE ? OR p.slug LIKE ?)';$args[]='%'.$blogSearch.'%';$args[]='%'.$blogSearch.'%';}
$clause=$where?' WHERE '.implode(' AND ',$where):'';
$countQ=db()->prepare('SELECT COUNT(*) FROM blog_posts p'.$clause);$countQ->execute($args);
$blogTotal=(int)$countQ->fetchColumn();$blogPages=max(1,(int)ceil($blogTotal/$blogPer));$blogPage=min($blogPage,$blogPages);
$sql="SELECT p.*,u.name AS author_name,u.username AS author_username FROM blog_posts p LEFT JOIN users u ON u.id=p.updated_by_user_id".$clause." ORDER BY COALESCE(p.publish_at,p.created_at) DESC,p.id DESC LIMIT ".$blogPer." OFFSET ".(($blogPage-1)*$blogPer);
$listQ=db()->prepare($sql);$listQ->execute($args);$posts=$listQ->fetchAll(PDO::FETCH_ASSOC);
function pv18_blog_link(int $page):string {$params=$_GET;$params['page']=$page;return '/admin/blog.php?'.http_build_query($params);}
$defaultStart = (new DateTimeImmutable('tomorrow 10:00'))->format('Y-m-d\TH:i');
render_admin_header('Blog');
?>
<div class="admin-title-row">
<div>
<h1>Blog</h1>
<p>Szkice, publikacja i harmonogram jak na mechanik.gomibud.pl – bez crona i bez SSH.</p>
</div>
<a class="btn btn-light" href="/admin/polana-v193-auto-harmonogram.php">Automatyczny harmonogram</a>
<a class="btn btn-primary" href="/admin/blog-edit.php">Nowy wpis</a>
</div>
<?php if (setting('nav_blog_enabled','0') !== '1'): ?>
<div class="flash flash-error">Blog jest obecnie ukryty w menu. Możesz przygotować wpisy, a potem włączyć Blog w <a href="/admin/modules.php">Sekcjach</a>.</div>
<?php endif; ?>
<div class="grid-cards">
<div class="card"><h3>Szkice</h3><div class="stat"><?= $draftCount ?></div></div>
<div class="card"><h3>Zaplanowane</h3><div class="stat"><?= $scheduledCount ?></div></div>
<div class="card"><h3>Opublikowane</h3><div class="stat"><?= $publishedCount ?></div></div>
</div>
<section class="form-card" style="margin-top:20px">
<div class="admin-section-title">
<h2>Automatyczny scheduler szkiców</h2>
<p>Zaznacz szkice poniżej. Pierwszy dostanie datę startową, kolejne zostaną rozłożone losowo co podaną liczbę dni. Po terminie wpis publikuje się przy pierwszym wejściu na blog – nie potrzebuje crona.</p>
</div>
<form method="post" id="blog-scheduler-form" class="form-grid">
<?= csrf_field() ?>
<input type="hidden" name="action" value="bulk_schedule">
<label>Start
<input type="datetime-local" name="start_at" value="<?= e($defaultStart) ?>" required>
</label>
<label>Min. odstęp (dni)
<input type="number" name="gap_min" min="1" max="60" value="7">
</label>
<label>Max. odstęp (dni)
<input type="number" name="gap_max" min="1" max="90" value="12">
</label>
<div class="full"><button class="btn btn-green" type="submit">Zaplanuj zaznaczone szkice</button></div>
</form>
</section>
<section class="form-card" style="margin-top:20px">
<div class="admin-section-title"><h2>Wpisy · <?= $blogTotal ?> wyników</h2></div>
<form method="get" style="display:flex;flex-wrap:wrap;gap:10px;align-items:end;margin:12px 0 20px">
<label style="flex:2;min-width:160px">Szukaj<input type="search" name="q" value="<?=e($blogSearch)?>" placeholder="Tytuł lub slug"></label>
<label style="min-width:140px">Status<select name="status"><?php foreach(['all'=>'Wszystkie','draft'=>'Szkice','scheduled'=>'Zaplanowane','published'=>'Opublikowane'] as $k=>$v):?><option value="<?=e($k)?>" <?=$blogStatus===$k?'selected':''?>><?=e($v)?></option><?php endforeach;?></select></label>
<label style="min-width:115px">Na stronę<select name="per_page"><?php foreach([20,50,100] as $n):?><option value="<?=$n?>" <?=$blogPer===$n?'selected':''?>><?=$n?></option><?php endforeach;?></select></label>
<button class="btn btn-primary">Filtruj</button>
</form>
<div class="table-wrap">
<table class="admin-table">
<thead><tr><th></th><th>Tytuł</th><th>Status</th><th>Publikacja</th><th>Kategoria</th><th></th></tr></thead>
<tbody>
<?php if (!$posts): ?><tr><td colspan="6" class="muted">Brak wpisów.</td></tr><?php endif; ?>
<?php foreach ($posts as $post): ?>
<tr>
<td>
<?php if ($post['status']==='draft'): ?>
<input type="checkbox" name="post_ids[]" value="<?= (int)$post['id'] ?>" form="blog-scheduler-form">
<?php endif; ?>
</td>
<td>
<strong><?= e($post['title']) ?></strong><br>
<small class="muted">/blog/<?= e($post['slug']) ?></small>
</td>
<td><span class="tip-status <?= $post['status']==='published' ? 'is-settled' : 'is-open' ?>"><?= e(blog_status_label($post)) ?></span></td>
<td><?= $post['publish_at'] ? e((new DateTimeImmutable($post['publish_at']))->format('d.m.Y H:i')) : '—' ?></td>
<td><?= e((string)($post['category'] ?: '—')) ?></td>
<td>
<div class="tip-actions">
<a class="btn btn-small btn-light" href="/admin/blog-edit.php?id=<?= (int)$post['id'] ?>">Edytuj</a>
<a class="btn btn-small btn-light" href="/blog/<?= e($post['slug']) ?>?preview=1" target="_blank" rel="noopener">Podgląd</a>
<form method="post" onsubmit="return confirm('Usunąć wpis?')">
<?= csrf_field() ?>
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= (int)$post['id'] ?>">
<button class="btn btn-small btn-light" type="submit">Usuń</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if($blogPages>1):?><nav style="display:flex;gap:8px;flex-wrap:wrap;margin:18px 0" aria-label="Paginacja bloga">
<?php if($blogPage>1):?><a class="btn btn-light" href="<?=e(pv18_blog_link($blogPage-1))?>">← Poprzednia</a><?php endif;?>
<?php for($n=max(1,$blogPage-2);$n<=min($blogPages,$blogPage+2);$n++):?><a class="btn btn-light" href="<?=e(pv18_blog_link($n))?>" <?=$n===$blogPage?'aria-current="page"':''?>><?=$n?></a><?php endfor;?>
<?php if($blogPage<$blogPages):?><a class="btn btn-light" href="<?=e(pv18_blog_link($blogPage+1))?>">Następna →</a><?php endif;?></nav><?php endif;?>
</section>
<?php render_admin_footer(); ?>