| 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);
/** V16: adapter istniejących kolejek V9/V12/V13/V15, bez zmian struktury tabel. */
require_once __DIR__.'/polana_v15_menu_queue.php';
require_once __DIR__.'/polana_v13_blog.php';
require_once __DIR__.'/polana_v12_pages.php';
if (is_file(__DIR__.'/polana_v14_names.php')) require_once __DIR__.'/polana_v14_names.php';
function v16_init(): void { v9_init(); p12_schema(); }
function v16_area(string $area): string {
if (!in_array($area,['menu','blog','pages'],true)) throw new RuntimeException('Nieprawidłowy moduł');
return $area;
}
function v16_lang(string $lang): string {
return in_array($lang,['all','en','de','es'],true)?$lang:'all';
}
function v16_menu_ids(): array { return p15_active_menu_ids(); }
function v16_menu_id(int $requested): int {
$menus=v16_menu_ids();
if (!$menus) return 0;
return isset($menus[$requested])?$requested:(int)array_key_first($menus);
}
function v16_status(string $area,int $menuId=0):array {
v16_init();$area=v16_area($area);
$out=['pending'=>0,'review'=>0,'approved'=>0,'errors'=>0,'total'=>0];
if($area==='menu') {
$menus=v16_menu_ids();
$ids=$menuId>0?($menus && isset($menus[$menuId])?[$menuId]:[]):array_keys($menus);
if($ids){
$placeholders=implode(',',array_fill(0,count($ids),'?'));
$sql="SELECT j.status,j.attempts,COUNT(*) n FROM tklm_v9_jobs j
LEFT JOIN menu_categories c ON j.entity='menu_categories' AND c.id=j.entity_id
LEFT JOIN menu_items i ON j.entity='menu_items' AND i.id=j.entity_id
WHERE ((j.entity='menu_categories' AND c.menu_id IN ($placeholders) AND c.active=1)
OR (j.entity='menu_items' AND i.category_id IN (SELECT id FROM menu_categories WHERE menu_id IN ($placeholders) AND active=1)
AND i.active=1 AND i.available=1)) GROUP BY j.status,j.attempts";
$q=db()->prepare($sql);$q->execute(array_merge($ids,$ids));
foreach($q->fetchAll(PDO::FETCH_ASSOC) as $r){
$n=(int)$r['n'];$status=(string)$r['status'];
if($status==='pending'){
if((int)$r['attempts']>=5)$out['errors']+=$n;
else $out['pending']+=$n;
}elseif(isset($out[$status]))$out[$status]+=$n;
}
}
} elseif($area==='blog') {
$s=p13_blog_stats();
foreach(['pending','review','approved','errors'] as $k) $out[$k]=(int)($s[$k]??0);
}else{
$s=p12_counts();
foreach(['pending','review','approved','errors'] as $k) $out[$k]=(int)($s[$k]??0);
}
$out['total']=array_sum(array_intersect_key($out,array_flip(['pending','review','approved','errors'])));
return $out;
}
function v16_jobs(string $area,string $lang,int $menuId,int $page=1,int $limit=35):array {
v16_init();$area=v16_area($area);$lang=v16_lang($lang);
$page=max(1,$page);$limit=max(1,min(50,$limit));$offset=($page-1)*$limit;
$params=[];
if($area==='menu'){
if(!isset(v16_menu_ids()[$menuId])) return [];
$sql="SELECT j.id,j.entity,j.entity_id,j.field,j.lang,j.source_text,j.translated_text,j.status,j.last_error, j.attempts
FROM tklm_v9_jobs j
LEFT JOIN menu_categories c ON j.entity='menu_categories' AND c.id=j.entity_id
LEFT JOIN menu_items i ON j.entity='menu_items' AND i.id=j.entity_id
WHERE j.status='review' AND ((j.entity='menu_categories' AND c.menu_id=? AND c.active=1)
OR (j.entity='menu_items' AND i.category_id IN (SELECT id FROM menu_categories WHERE menu_id=? AND active=1)
AND i.active=1 AND i.available=1))";
$params=[$menuId,$menuId];
} elseif($area==='blog') {
$sql="SELECT j.id,j.entity,j.entity_id,j.field,j.lang,j.source_text,j.translated_text,j.status,j.last_error,j.attempts,b.title AS record_name
FROM tklm_v9_jobs j LEFT JOIN blog_posts b ON b.id=j.entity_id
WHERE j.entity='blog_posts' AND j.status='review'";
} else {
$sql="SELECT id,'pages' AS entity,0 AS entity_id,path AS field,lang,source_text,translated_text,status,error_text AS last_error,attempts,path AS record_name
FROM polana_v12_page_phrases WHERE status='review'";
}
if($lang!=='all'){$sql.=' AND '.($area==='pages'?'lang':'j.lang').'=?';$params[]=$lang;}
$sql.=' ORDER BY '.($area==='pages'?'id':'j.id').' ASC LIMIT '.$limit.' OFFSET '.$offset;
$q=db()->prepare($sql);$q->execute($params);
return $q->fetchAll(PDO::FETCH_ASSOC);
}
function v16_job_validate(string $area,int $id,int $menuId):array {
$area=v16_area($area);
if($area==='pages'){
$q=db()->prepare("SELECT id,path,lang,source_text,translated_text,status FROM polana_v12_page_phrases WHERE id=? AND status='review'");
$q->execute([$id]);$job=$q->fetch(PDO::FETCH_ASSOC);
if(!$job||!isset(p12_paths()[$job['path']])||!in_array((string)$job['lang'],['en','de','es'],true))throw new RuntimeException('Fraza nie należy do aktywnego modułu');
return $job;
}
if($area==='blog'){
$q=db()->prepare("SELECT j.* FROM tklm_v9_jobs j JOIN blog_posts b ON b.id=j.entity_id WHERE j.id=? AND j.entity='blog_posts' AND j.status='review'");
$q->execute([$id]);$job=$q->fetch(PDO::FETCH_ASSOC);
}else{
if(!isset(v16_menu_ids()[$menuId]))throw new RuntimeException('Nieaktualna karta');
$q=db()->prepare("SELECT j.* FROM tklm_v9_jobs j
LEFT JOIN menu_categories c ON j.entity='menu_categories' AND c.id=j.entity_id
LEFT JOIN menu_items i ON j.entity='menu_items' AND i.id=j.entity_id
WHERE j.id=? AND j.status='review' AND
((j.entity='menu_categories' AND c.menu_id=? AND c.active=1)
OR (j.entity='menu_items' AND i.category_id IN(SELECT id FROM menu_categories WHERE menu_id=? AND active=1)
AND i.active=1 AND i.available=1))");
$q->execute([$id,$menuId,$menuId]);$job=$q->fetch(PDO::FETCH_ASSOC);
}
if(!$job || !in_array((string)$job['lang'],['en','de','es'],true))throw new RuntimeException('Zadanie nie jest gotowe do zatwierdzenia');
$allow=$area==='blog'?['title','excerpt','content']:($job['entity']==='menu_categories'?['name']:['name','description','allergens']);
if(!in_array((string)$job['field'],$allow,true))throw new RuntimeException('Niedozwolone pole');
return $job;
}
function v16_approve(string $area,array $ids,array $values,int $menuId,string $lang):array {
$area=v16_area($area);$lang=v16_lang($lang);
$ids=array_values(array_unique(array_filter(array_map('intval',$ids),static fn($v)=>$v>0)));
if(!$ids)throw new RuntimeException('Zaznacz tłumaczenia');
if(count($ids)>50)throw new RuntimeException('Jednorazowo maksymalnie 50 tłumaczeń');
$ok=0;$bad=[];
foreach($ids as $id){
try{
$job=v16_job_validate($area,$id,$menuId);
if($lang!=='all' && $job['lang']!==$lang)throw new RuntimeException('Inny język niż w filtrze');
if(!array_key_exists((string)$id,$values) || !is_string($values[(string)$id])) throw new RuntimeException('Brak tekstu z formularza');
$text=trim($values[(string)$id]);
$field=(string)($job['field']??'');
$max= $area==='pages'?6000:($field==='title'?255:($field==='name'?255:($field==='allergens'?500:45000)));
if($text==='' || mb_strlen($text,'UTF-8')>$max)throw new RuntimeException('Pusty lub zbyt długi tekst');
if($area==='pages'){
$update=db()->prepare("UPDATE polana_v12_page_phrases SET translated_text=? WHERE id=? AND status='review'");
$update->execute([$text,$id]);
$res=p12_approve([$id]);
if(($res['approved']??0)!==1)throw new RuntimeException('Zatwierdzenie nie powiodło się');
}else{
// Nie zapisujemy wprost w publicznych kolumnach: v9_approve kontroluje hash oryginału.
$current=db()->prepare("SELECT `$field` FROM `".($area==='blog'?'blog_posts':$job['entity'])."` WHERE id=?");
$current->execute([(int)$job['entity_id']]);
$original=$current->fetchColumn();
if(!is_string($original)||hash('sha256',trim($original))!==(string)$job['source_hash'])
throw new RuntimeException('Oryginał zmienił się; przetłumacz ponownie');
$update=db()->prepare("UPDATE tklm_v9_jobs SET translated_text=? WHERE id=? AND status='review'");
$update->execute([$text,$id]);
v9_approve($id);
}
$ok++;
}catch(Throwable $e){$bad[]=['id'=>$id,'reason'=>$e->getMessage()];}
}
return ['approved'=>$ok,'failed'=>$bad];
}
/** List only failed/temporarily failed jobs (no secrets), limited to current module/card. */
function v16_problem_jobs(string $area,int $menuId=0,int $limit=15):array {
$area=v16_area($area);$limit=max(1,min(30,$limit));
if($area==='pages'){
$q=db()->query("SELECT id,lang,path AS item,source_text,attempts,error_text AS message,IF(attempts>=3,1,0) AS final_error
FROM polana_v12_page_phrases WHERE status='pending' AND attempts>0 AND error_text IS NOT NULL
ORDER BY attempts DESC,id DESC LIMIT $limit");
}elseif($area==='blog'){
$q=db()->query("SELECT id,lang,CONCAT('blog_posts #',entity_id,' · ',field) AS item,source_text,attempts,last_error AS message,IF(attempts>=5,1,0) AS final_error
FROM tklm_v9_jobs WHERE entity='blog_posts' AND status='pending' AND attempts>0 AND last_error IS NOT NULL
ORDER BY attempts DESC,updated_at DESC,id DESC LIMIT $limit");
}else{
if(!isset(v16_menu_ids()[$menuId]))return [];
$q=db()->prepare("SELECT j.id,j.lang,CONCAT(j.entity,' #',j.entity_id,' · ',j.field) AS item,j.source_text,j.attempts,
j.last_error AS message,IF(j.attempts>=5,1,0) AS final_error
FROM tklm_v9_jobs j LEFT JOIN menu_categories c ON j.entity='menu_categories' AND c.id=j.entity_id
LEFT JOIN menu_items i ON j.entity='menu_items' AND i.id=j.entity_id
WHERE j.status='pending' AND j.attempts>0 AND j.last_error IS NOT NULL
AND ((j.entity='menu_categories' AND c.menu_id=? AND c.active=1)
OR (j.entity='menu_items' AND i.category_id IN(SELECT id FROM menu_categories WHERE menu_id=? AND active=1) AND i.active=1 AND i.available=1))
ORDER BY j.attempts DESC,j.updated_at DESC,j.id DESC LIMIT $limit");
$q->execute([$menuId,$menuId]);
}
$rows=$q->fetchAll(PDO::FETCH_ASSOC);
return array_map(static function(array $r):array{
$r['source_text']=mb_strimwidth((string)$r['source_text'],0,160,'…','UTF-8');
$r['message']=mb_strimwidth((string)$r['message'],0,350,'…','UTF-8');
$r['message']=preg_replace('/[a-f0-9]{48}/i','[ukryty klucz]',$r['message']);
$r['final_error']=(int)$r['final_error'];
return $r;
},$rows);
}
/** Retry only exhausted errors, preserving approved/review jobs and translations. */
function v16_retry_failed(string $area,int $menuId=0):int {
$area=v16_area($area);
if($area==='pages'){
return db()->exec("UPDATE polana_v12_page_phrases SET attempts=0,error_text=NULL WHERE status='pending' AND attempts>=3");
}
if($area==='blog'){
return db()->exec("UPDATE tklm_v9_jobs SET attempts=0,last_error=NULL WHERE entity='blog_posts' AND status='pending' AND attempts>=5");
}
if(!isset(v16_menu_ids()[$menuId]))throw new RuntimeException('Nieprawidłowa aktualna karta menu');
$q=db()->prepare("UPDATE tklm_v9_jobs j
LEFT JOIN menu_categories c ON j.entity='menu_categories' AND c.id=j.entity_id
LEFT JOIN menu_items i ON j.entity='menu_items' AND i.id=j.entity_id
SET j.attempts=0,j.last_error=NULL
WHERE j.status='pending' AND j.attempts>=5
AND ((j.entity='menu_categories' AND c.menu_id=? AND c.active=1)
OR (j.entity='menu_items' AND i.category_id IN(SELECT id FROM menu_categories WHERE menu_id=? AND active=1) AND i.active=1 AND i.available=1))");
$q->execute([$menuId,$menuId]);return $q->rowCount();
}
/** V16.3: bezpepieczny, stronicowany odczyt zatwierdzonych do przeglądu pozycji. */
function v163_bulk_sql(string $area,string $lang,int $menuId,int $after,int $limit,bool $count=false):array {
$area=v16_area($area);$lang=v16_lang($lang);
$args=[];
$fields=$count?'COUNT(*) AS n':($area==='pages'?
"p.id,p.lang,p.path AS field,p.source_text,p.translated_text,p.status,0 AS related_menu_id":
"j.id,j.lang,j.field,j.source_text,j.translated_text,j.status".($area==='menu'?',COALESCE(c.menu_id,cm.menu_id) AS related_menu_id':',0 AS related_menu_id'));
if($area==='menu'){
$ids=v16_menu_ids();
if($menuId && !isset($ids[$menuId]))throw new RuntimeException('Nieaktualna karta menu');
if(!$ids)return ['sql'=>'SELECT 0 AS n WHERE 1=0','params'=>[]];
$filter=$menuId?[$menuId]:array_map('intval',array_keys($ids));
$place=implode(',',array_fill(0,count($filter),'?'));
$sql="SELECT $fields FROM tklm_v9_jobs j
LEFT JOIN menu_categories c ON j.entity='menu_categories' AND c.id=j.entity_id
LEFT JOIN menu_items i ON j.entity='menu_items' AND i.id=j.entity_id
LEFT JOIN menu_categories cm ON i.category_id=cm.id
WHERE j.status='review' AND j.lang IN ('en','de','es')
AND ((j.entity='menu_categories' AND c.active=1 AND c.menu_id IN ($place))
OR (j.entity='menu_items' AND i.active=1 AND i.available=1 AND cm.active=1 AND cm.menu_id IN ($place)))";
$args=array_merge($filter,$filter);
$pk='j.id';$langField='j.lang';
}elseif($area==='blog'){
$sql="SELECT $fields FROM tklm_v9_jobs j JOIN blog_posts b ON b.id=j.entity_id
WHERE j.entity='blog_posts' AND j.field IN ('title','excerpt','content')
AND j.status='review' AND j.lang IN ('en','de','es')";
$pk='j.id';$langField='j.lang';
}else{
$sql="SELECT $fields FROM polana_v12_page_phrases p WHERE p.status='review'
AND p.lang IN ('en','de','es')";
$pk='p.id';$langField='p.lang';
}
if($lang!=='all'){$sql.=" AND $langField=?";$args[]=$lang;}
if(!$count){
$sql.=" AND $pk>? ORDER BY $pk ASC LIMIT ".max(1,min(25,$limit));$args[]=$after;
}
return ['sql'=>$sql,'params'=>$args];
}
function v163_bulk_fetch(string $area,string $lang,int $menuId,int $cursor,int $limit=15):array{
$v=v163_bulk_sql($area,$lang,$menuId,max(0,$cursor),$limit);
$q=db()->prepare($v['sql']);$q->execute($v['params']);return $q->fetchAll(PDO::FETCH_ASSOC);
}
function v163_bulk_preview(string $area,string $lang,int $menuId):array{
$v=v163_bulk_sql($area,$lang,$menuId,0,1,true);
$q=db()->prepare($v['sql']);$q->execute($v['params']);$count=(int)$q->fetchColumn();
$rows=v163_bulk_fetch($area,$lang,$menuId,0,8);
$examples=[];
foreach($rows as $r){
$examples[]=['id'=>(int)$r['id'],'lang'=>(string)$r['lang'],
'field'=>(string)$r['field'],
'source'=>mb_strimwidth((string)$r['source_text'],0,140,'…','UTF-8'),
'translated'=>mb_strimwidth((string)$r['translated_text'],0,140,'…','UTF-8')];
}
return ['total'=>$count,'examples'=>$examples];
}
function v163_bulk_next(string $area,string $lang,int $menuId,int $cursor):array{
$area=v16_area($area);$lang=v16_lang($lang);
$rows=v163_bulk_fetch($area,$lang,$menuId,$cursor,15);
$approved=0;$skipped=[];
foreach($rows as $r){
$id=(int)$r['id'];$cursor=max($cursor,$id);
$source=(string)$r['source_text'];$translation=trim((string)$r['translated_text']);
if($translation===''){$skipped[]=['id'=>$id,'reason'=>'Puste tłumaczenie'];continue;}
if(function_exists('p14_missing_terms')){
$missing=p14_missing_terms($source,$translation);
if($missing){$skipped[]=['id'=>$id,'reason'=>'Brak chronionych nazw: '.implode(', ',$missing)];continue;}
}
$targetMenu=$area==='menu'?(int)$r['related_menu_id']:0;
// Używamy oryginalnego bezpiecznego zatwierdzania (kontrola hash PL, aktywnej karty i statusu).
$res=v16_approve($area,[$id],[(string)$id=>$translation],$targetMenu,$lang);
$approved+=(int)($res['approved']??0);
foreach($res['failed']??[] as $f){
$skipped[]=['id'=>(int)($f['id']??$id),'reason'=>mb_strimwidth((string)($f['reason']??'Błąd'),0,160,'…','UTF-8')];
}
}
return ['approved'=>$approved,'cursor'=>$cursor,'processed'=>count($rows),
'skipped'=>$skipped,'finished'=>count($rows)<15];
}