| 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';
require_once dirname(__DIR__).'/includes/polana_translate_client.php';
require_login();
require_admin_role();
$id = (int)($_GET['id'] ?? $_POST['menu_id'] ?? 0);
$stmt = db()->prepare('SELECT * FROM menus WHERE id=? LIMIT 1');
$stmt->execute([$id]);
$menu = $stmt->fetch();
if (!$menu) { http_response_code(404); exit('Nie znaleziono karty.'); }
start_session();
$sessionKey = 'polana_mt_menu_'.$id;
$error = '';
function polana_mt_job_rows(int $id): array
{
$rows = [];
foreach (menu_categories_with_items($id, true) as $cat) {
if (trim((string)($cat['name_en'] ?? '')) === '' && trim((string)$cat['name']) !== '') {
$rows['c:'.$cat['id'].':name'] = ['pl'=>(string)$cat['name'],'en'=>'', 'table'=>'c','id'=>(int)$cat['id'], 'field'=>'name_en'];
}
foreach ($cat['items'] as $item) {
foreach (['name'=>'name_en','description'=>'description_en', 'ingredients'=>'ingredients_en', 'allergens'=>'allergens_en'] as $src=>$dst) {
if (trim((string)($item[$dst] ?? '')) !== '' || trim((string)($item[$src] ?? '')) === '') continue;
$rows['i:'.$item['id'].':'.$src] = [
'pl'=>(string)$item[$src], 'en'=>'','table'=>'i','id'=>(int)$item['id'],'field'=>$dst,
];
}
}
}
return $rows;
}
function polana_mt_json(array $payload, int $code=200): never
{
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode($payload, JSON_UNESCAPED_UNICODE|JSON_INVALID_UTF8_SUBSTITUTE);
exit;
}
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
verify_csrf();
$action = (string)($_POST['action'] ?? '');
try {
if ($action === 'begin') {
if (!polana_mt_configured()) throw new RuntimeException('Najpierw skonfiguruj HTTPS i token serwera tłumaczeń. Instrukcja jest w paczce.');
$rows = polana_mt_job_rows($id);
if (!$rows) throw new RuntimeException('Ta karta ma już uzupełnione pola angielskie lub nie ma pozycji do tłumaczenia.');
$_SESSION[$sessionKey] = ['created_at'=>time(), 'rows'=>$rows];
redirect('/admin/translate-menu.php?id='.$id);
}
if ($action === 'step') {
$job = $_SESSION[$sessionKey] ?? null;
if (!is_array($job) || time()-(int)($job['created_at'] ?? 0)>3600) throw new RuntimeException('Sesja tłumaczenia wygasła. Rozpocznij ponownie.');
$pending = [];
foreach ($job['rows'] as $key=>$row) {
if ($row['en'] === '') $pending[$key] = $row['pl'];
if (count($pending) >= 5) break;
}
if ($pending) {
$translated = polana_mt_batch(array_values($pending));
foreach (array_keys($pending) as $i=>$key) {
$_SESSION[$sessionKey]['rows'][$key]['en'] = $translated[$i];
}
}
$left = count(array_filter($_SESSION[$sessionKey]['rows'], static fn($r):bool => $r['en']===''));
polana_mt_json(['ok'=>true,'remaining'=>$left,'total'=>count($_SESSION[$sessionKey]['rows'])]);
}
if ($action === 'apply') {
$job = $_SESSION[$sessionKey] ?? null;
if (!is_array($job) || time()-(int)($job['created_at'] ?? 0)>3600) throw new RuntimeException('Sesja tłumaczenia wygasła.');
$all = $job['rows'];
if (array_filter($all,static fn($r):bool => $r['en']==='')) throw new RuntimeException('Nie wszystkie tłumaczenia są gotowe.');
if (empty($_POST['verified'])) throw new RuntimeException('Przed zapisaniem EN musisz potwierdzić ręczne sprawdzenie tłumaczeń.');
$posted = $_POST['translation'] ?? [];
if (!is_array($posted)) throw new RuntimeException('Brak tłumaczeń.');
$pdo = db(); $pdo->beginTransaction();
try {
foreach ($all as $key=>$row) {
$translated = trim((string)($posted[$key] ?? ''));
if ($translated==='' || strlen($translated)>2500) throw new RuntimeException('Brakuje tekstu EN dla: '.$row['pl']);
if ($row['table']==='c') {
// Nazwa kategorii ma limit varchar(150).
if (strlen($translated)>150) throw new RuntimeException('Za długa angielska nazwa kategorii.');
$q=$pdo->prepare('UPDATE menu_categories SET name_en=? WHERE id=? AND menu_id=? AND name=? AND (name_en IS NULL OR name_en=\'\')');
$q->execute([$translated,$row['id'],$id,$row['pl']]);
if ($q->rowCount()!==1) throw new RuntimeException('Pozycja zmieniła się w trakcie tłumaczenia. Odśwież podgląd EN.');
} else {
if ($row['field']==='name_en' && strlen($translated)>255) throw new RuntimeException('Za długa angielska nazwa pozycji.');
// Field comes only from server-built whitelist, never from $_POST.
$allowed=['name_en','description_en','ingredients_en','allergens_en'];
if (!in_array($row['field'],$allowed,true)) throw new RuntimeException('Błędne pole tłumaczenia.');
$sourceFields=['name_en'=>'name','description_en'=>'description','ingredients_en'=>'ingredients','allergens_en'=>'allergens'];
$src=$sourceFields[$row['field']];
$q=$pdo->prepare('UPDATE menu_items i JOIN menu_categories c ON c.id=i.category_id SET i.'.$row['field'].'=? WHERE i.id=? AND c.menu_id=? AND i.'.$src.'=? AND (i.'.$row['field'].' IS NULL OR i.'.$row['field'].'=\'\')');
$q->execute([$translated,$row['id'],$id,$row['pl']]);
if ($q->rowCount()!==1) throw new RuntimeException('Polski tekst zmienił się podczas tłumaczenia. Rozpocznij ponownie.');
}
}
$pdo->commit();
} catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack();throw $e; }
unset($_SESSION[$sessionKey]);
admin_log('menu_auto_en', 'Zatwierdzono EN karty '.$id);
flash('success','Zapisano tłumaczenia EN. Polskie nazwy i ceny nie zostały zmienione. Sprawdź nazwy win i alergeny.');
redirect('/admin/menu.php?id='.$id);
}
if ($action === 'cancel') {
unset($_SESSION[$sessionKey]);
redirect('/admin/translate-menu.php?id='.$id);
}
throw new RuntimeException('Nieznana operacja.');
} catch (Throwable $e) {
if ($action==='step') polana_mt_json(['ok'=>false,'message'=>$e->getMessage()],502);
$error=$e->getMessage();
}
}
$job = $_SESSION[$sessionKey] ?? null;
if (is_array($job) && time()-(int)$job['created_at']>3600) {unset($_SESSION[$sessionKey]);$job=null;}
$remaining = is_array($job) ? count(array_filter($job['rows'],static fn($r):bool =>$r['en']==='')) : 0;
render_admin_header('Docker → EN');
?>
<div class="admin-title-row"><div><h1>🇬🇧 Automatyczne tłumaczenie PL → EN</h1><p><?= e($menu['name']) ?> · własny kontener LibreTranslate · bez zmiany polskich nazw i cen</p></div><a class="btn btn-light" href="/admin/menu.php?id=<?= $id ?>">← Karta</a></div>
<?php if ($error): ?><section class="form-card" style="color:#9b2424"><strong><?= e($error) ?></strong></section><?php endif; ?>
<section class="form-card">
<p>Polskie nazwy potraw, win, składniki i alergeny trafiają do Twojego kontenera. Wyniki przejrzyj przed zatwierdzeniem; nazwy własne i alergeny wymagają kontroli człowieka.</p>
<p>Status API: <strong><?= polana_mt_configured() ? 'Skonfigurowano URL i klucz (łączność sprawdzana podczas tłumaczenia)' : 'Nie skonfigurowano – zobacz instrukcję Docker' ?></strong></p>
<?php if (!$job): ?>
<form method="post"><?= csrf_field() ?><input type="hidden" name="menu_id" value="<?= $id ?>"><input type="hidden" name="action" value="begin"><button class="btn btn-green" type="submit">Przetłumacz brakujące pola na EN</button></form>
<?php else: ?>
<div id="polana-mt-progress" role="status" aria-live="polite" style="font-weight:700;padding:10px 0"><?= $remaining ? 'Tłumaczenie: pozostało '.$remaining.' pól' : 'Gotowe do weryfikacji i zatwierdzenia' ?></div>
<?php if ($remaining): ?>
<button class="btn btn-primary" id="polana-mt-run" type="button"><?= 'Rozpocznij / wznów automatyczne tłumaczenie' ?></button>
<?php else: ?>
<form method="post"><?= csrf_field() ?><input type="hidden" name="menu_id" value="<?= $id ?>"><input type="hidden" name="action" value="apply">
<h2>Podgląd EN – sprawdź i popraw</h2>
<?php foreach ($job['rows'] as $key=>$row): ?>
<label style="display:block;margin-bottom:12px;font-weight:600"><?= e($row['pl']) ?>
<textarea name="translation[<?= e($key) ?>]" rows="2" maxlength="2500" style="width:100%;display:block"><?= e($row['en']) ?></textarea></label>
<?php endforeach; ?>
<p style="padding:14px;border:1px solid #dfa31a;border-radius:10px"><strong>⚠️ Tłumaczenie maszynowe — sprawdź przed publikacją.</strong> Zwróć uwagę na nazwy szczepów win, składniki i alergeny. Dla już opublikowanej karty zapis EN będzie od razu widoczny na stronie angielskiej.</p>
<label style="display:block;margin:12px 0"><input type="checkbox" name="verified" value="1" required> <strong>Sprawdziłem wszystkie tłumaczenia i zatwierdzam ich publikację.</strong></label>
<button class="btn btn-green" type="submit">Zatwierdź i zapisz EN</button>
</form>
<?php endif; ?>
<form method="post" style="margin-top:16px"><?= csrf_field() ?><input type="hidden" name="menu_id" value="<?= $id ?>"><input type="hidden" name="action" value="cancel"><button class="btn btn-light" type="submit">Anuluj podgląd (bez zmiany menu)</button></form>
<?php endif; ?>
</section>
<?php if ($job && $remaining): ?>
<script>
(() => {
const btn=document.getElementById('polana-mt-run');const progress=document.getElementById('polana-mt-progress');
if(!btn||!progress)return;let working=false;
btn.addEventListener('click',async()=>{
if(working)return;working=true;btn.disabled=true;
try{
while(true){
const form=new URLSearchParams({csrf_token:<?= json_encode(csrf_token()) ?>,menu_id:<?= json_encode((string)$id) ?>,action:'step'});
const res=await fetch(location.pathname+'?id=<?= $id ?>',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:form});
const answer=await res.json();if(!res.ok||!answer.ok)throw new Error(answer.message||'HTTP '+res.status);
progress.textContent='Przetłumaczono '+(answer.total-answer.remaining)+' / '+answer.total+' pól';
if(answer.remaining===0){location.reload();return;}
}
}catch(ex){progress.textContent='Błąd: '+(ex.message||ex)+'. Możesz wznowić bez utraty wcześniejszych wyników.';btn.disabled=false;working=false;}
});
})();
</script>
<?php endif; ?>
<?php render_admin_footer(); ?>