| 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/import_menu.php';
require_login();
if (!polana_schema_ready()) {
flash('error', 'Najpierw wykonaj migrację bazy PL/EN.');
redirect('/admin/polana-bilingual-install.php');
}
$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.'); }
$error = '';
$preview = null;
$token = '';
$comparison = [];
/** Compare the English file by category and dish order, never by untrusted names. */
function polana_english_matches(int $menuId, array $parsed): array
{
$original = menu_categories_with_items($menuId, true);
$incoming = $parsed['categories'] ?? [];
if (count($original) !== count($incoming)) {
throw new RuntimeException('Liczba kategorii w EN musi być taka sama jak w polskiej karcie. PL: '.count($original).', EN: '.count($incoming).'.');
}
$mapped = [];
foreach ($original as $i => $cat) {
$in = $incoming[$i];
if (count($cat['items']) !== count($in['items'])) {
throw new RuntimeException('Kategoria '.($i+1).' ('.$cat['name'].'): różna liczba pozycji: PL '.count($cat['items']).', EN '.count($in['items']).'.');
}
$parts = [];
foreach ($cat['items'] as $j => $item) {
$en = $in['items'][$j];
// Karta win: porównaj obie ceny z price_text, nie pojedyncze pole price.
$plPair = [];
$enPair = [];
preg_match_all('~(\d+(?:[,.]\d{1,2})?)\s*zł~u', (string)($item['price_text'] ?? ''), $plPair);
preg_match_all('~(\d+(?:[,.]\d{1,2})?)\s*zł~u', (string)($en['price_text'] ?? ''), $enPair);
if (!empty($plPair[1]) && array_map(static fn($x) => (float)str_replace(',', '.', $x), $plPair[1])
!== array_map(static fn($x) => (float)str_replace(',', '.', $x), $enPair[1])) {
throw new RuntimeException('Różne ceny kieliszka/butelki w kategorii '.($i+1).', pozycja '.($j+1).'.');
}
if ($item['price'] !== null && abs((float)$item['price'] - (float)($en['price'] ?? 0)) > 0.009) {
throw new RuntimeException('Różna cena w kategorii '.($i+1).', pozycja '.($j+1).': PL '.$item['price'].', EN '.($en['price'] ?? '').'. Kolejność i ceny muszą być identyczne.');
}
$parts[] = ['id'=>(int)$item['id'], 'pl'=>(string)$item['name'], 'en'=>(string)$en['name'], 'description_en'=>(string)($en['description'] ?? '')];
}
$mapped[] = ['id'=>(int)$cat['id'], 'pl'=>(string)$cat['name'], 'en'=>(string)$in['name'], 'items'=>$parts];
}
return $mapped;
}
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
verify_csrf();
try {
$action = (string)($_POST['action'] ?? 'preview');
if ($action === 'preview') {
$uploadId = strtolower((string)($_POST['docx_upload_token'] ?? ''));
$rawText = trim((string)($_POST['menu_text'] ?? ''));
if ($uploadId !== '') {
$stage = $_SESSION['polana_word_uploads'][$uploadId] ?? null;
if (!preg_match('/^[a-f0-9]{32}$/D', $uploadId) || !is_array($stage)
|| empty($stage['complete']) || (int)($stage['started_at'] ?? 0) < time() - 1800
|| !is_readable((string)($stage['path'] ?? ''))
|| filesize((string)$stage['path']) !== (int)$stage['total_size']) {
throw new RuntimeException('Przesłany Word wygasł lub jest niekompletny. Spróbuj ponownie.');
}
$lines = docx_to_lines((string)$stage['path']);
@unlink((string)$stage['path']);
unset($_SESSION['polana_word_uploads'][$uploadId]);
} elseif (isset($_FILES['docx']) && ($_FILES['docx']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
if (strtolower(pathinfo((string)$_FILES['docx']['name'], PATHINFO_EXTENSION)) !== 'docx') throw new RuntimeException('Wybierz .docx.');
$lines = docx_to_lines((string)$_FILES['docx']['tmp_name']);
} elseif ($rawText !== '') {
$lines = text_to_lines($rawText);
} else {
throw new RuntimeException('Wybierz angielski DOCX lub wklej angielskie menu. Kod 6 PHP wymaga przeglądarki z włączonym JavaScript.');
}
$parsed = parse_menu_lines($lines);
if (empty($parsed['categories'])) throw new RuntimeException('Nie rozpoznano angielskich pozycji i cen.');
$comparison = polana_english_matches($id, $parsed);
$token = bin2hex(random_bytes(16));
$_SESSION['polana_english_preview'] = ['token'=>$token,'menu_id'=>$id,'created_at'=>time(),'parsed'=>$parsed];
$preview = $parsed;
} elseif ($action === 'apply') {
$st = $_SESSION['polana_english_preview'] ?? null;
if (!is_array($st) || (int)($st['menu_id'] ?? 0) !== $id
|| (int)($st['created_at'] ?? 0) < time()-1800
|| !hash_equals((string)($st['token'] ?? ''), (string)($_POST['preview_token'] ?? ''))) {
throw new RuntimeException('Podgląd EN wygasł. Wczytaj dokument ponownie.');
}
// Lock every edited category/item while comparing, so no silent order mismatch.
db()->beginTransaction();
try {
$comparison = polana_english_matches($id, $st['parsed']);
$catUpdate = db()->prepare('UPDATE menu_categories SET name_en=? WHERE id=? AND menu_id=?');
$itemUpdate = db()->prepare('UPDATE menu_items SET name_en=?, description_en=? WHERE id=? AND category_id=?');
foreach ($comparison as $cat) {
$catUpdate->execute([$cat['en'], $cat['id'], $id]);
foreach ($cat['items'] as $item) {
$itemUpdate->execute([$item['en'], $item['description_en'], $item['id'], $cat['id']]);
}
}
db()->commit();
} catch (Throwable $e) {
db()->rollBack();
throw $e;
}
unset($_SESSION['polana_english_preview']);
admin_log('menu_english_import', 'Wczytano angielskie tłumaczenie menu #'.$id);
flash('success', 'Angielskie nazwy zostały zapisane. Polskie nazwy i ceny pozostały bez zmian.');
redirect('/admin/menu.php?id='.$id);
} else throw new RuntimeException('Nieznana akcja.');
} catch (Throwable $e) {
$error = $e->getMessage();
}
}
render_admin_header('Import tłumaczenia EN');
?>
<div class="admin-title-row"><div><h1>🇬🇧 Import angielskiej wersji</h1><p>Istniejąca karta: <?= e($menu['name']) ?> (<?= e(polana_type_label((string)($menu['menu_type'] ?? 'food'),'pl')) ?>)</p></div><a class="btn btn-light" href="/admin/menu.php?id=<?= $id ?>">← Karta</a></div>
<?php if ($error): ?><div class="form-card" style="border:2px solid #c53535"><strong><?= e($error) ?></strong></div><?php endif; ?>
<section class="form-card"><p>Word EN musi mieć <strong>identyczną kolejność kategorii, dań i te same ceny</strong> co polska karta. System porówna dokumenty i pokaże pary PL ↔ EN; przy rozbieżnościach niczego nie zapisze.</p></section>
<form id="polana-word-import-form" method="post" enctype="multipart/form-data" class="form-card form-grid">
<?= csrf_field() ?>
<input type="hidden" name="action" value="preview">
<input type="hidden" name="menu_id" value="<?= $id ?>">
<input type="hidden" name="docx_upload_token" id="polana-docx-upload-token" value="">
<label class="full">English Word (.docx)
<input type="file" name="docx" id="polana-docx-file" accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document">
<span class="muted" id="polana-docx-upload-progress" role="status" aria-live="polite"></span>
</label>
<label class="full">Albo wklej tekst po angielsku
<textarea name="menu_text" rows="12" placeholder="Starters Beef tartare with fermented vegetables 58 Soups Green soup with roast apple 31"><?= e($_POST['menu_text'] ?? '') ?></textarea>
</label>
<div class="full"><button class="btn btn-primary" type="submit" id="polana-docx-preview-btn">Sprawdź pary PL / EN</button></div>
</form>
<?php if ($preview): ?>
<section class="form-card"><h2>Podgląd dopasowania – <?= count($comparison) ?> kategorii</h2>
<?php foreach ($comparison as $cat): ?>
<div class="list-card"><h3><?= e($cat['pl']) ?> → <?= e($cat['en']) ?></h3>
<?php foreach ($cat['items'] as $item): ?><p><?= e($item['pl']) ?> → <strong><?= e($item['en']) ?></strong></p><?php endforeach; ?>
</div>
<?php endforeach; ?>
<form method="post"><?= csrf_field() ?><input type="hidden" name="menu_id" value="<?= $id ?>"><input type="hidden" name="action" value="apply"><input type="hidden" name="preview_token" value="<?= e($token) ?>"><button class="btn btn-green" type="submit">Zapisz angielskie nazwy do tej karty</button></form>
</section>
<?php endif; ?>
<script>
(() => {
'use strict';
const form = document.getElementById('polana-word-import-form');
const fileInput = document.getElementById('polana-docx-file');
const tokenInput = document.getElementById('polana-docx-upload-token');
const progress = document.getElementById('polana-docx-upload-progress');
const button = document.getElementById('polana-docx-preview-btn');
if (!form || !fileInput || !tokenInput || !progress || !window.fetch) return;
let uploading = false;
const SIZE = 15 * 1024;
form.addEventListener('submit', async e => {
const file = fileInput.files && fileInput.files[0];
if (!file || uploading) { if (uploading) e.preventDefault(); return; }
e.preventDefault();
if (!/\.docx$/i.test(file.name) || file.size < 1 || file.size > 20*1024*1024) { progress.textContent='DOCX musi mieć 1 B – 20 MB.';return; }
if (!window.crypto?.getRandomValues) { progress.textContent='Przeglądarka nie obsługuje bezpiecznego przesyłania.'; return; }
uploading=true;button.disabled=true;
const bytes=new Uint8Array(16);crypto.getRandomValues(bytes);
const id=Array.from(bytes,b=>b.toString(16).padStart(2,'0')).join('');
const count=Math.ceil(file.size/SIZE);
const csrf=form.querySelector('[name="csrf_token"]').value;
try {
for(let i=0;i<count;i++) {
progress.textContent='Przesyłanie Worda EN: '+Math.round(100*i/count)+'%';
const response=await fetch('/admin/import-upload.php',{
method:'POST',credentials:'same-origin',cache:'no-store',
headers:{'Content-Type':'application/octet-stream','X-Polana-Csrf':csrf,'X-Polana-Upload':id,
'X-Polana-Index':String(i),'X-Polana-Count':String(count),'X-Polana-Size':String(file.size),
'X-Polana-Name':encodeURIComponent(file.name)},
body:file.slice(i*SIZE,Math.min((i+1)*SIZE,file.size))
});
let body;try{body=await response.json();}catch(_){throw new Error('HTTP '+response.status);}
if(!response.ok||!body.ok)throw new Error(body.message||'HTTP '+response.status);
if(i===count-1&&!body.complete)throw new Error('Niekompletny plik.');
}
progress.textContent='DOCX EN przesłany. Sprawdzam zgodność kategorii i cen…';
tokenInput.value=id;fileInput.disabled=true;form.submit();
}catch(err){progress.textContent='Błąd: '+(err?.message||'Nieznany błąd');uploading=false;button.disabled=false;}
});
})();
</script>
<?php render_admin_footer(); ?>