| 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 V12 — server-rendered, reviewed translations of visible text in public static pages.
* Do not translate blog posts/menu data here; those have their own V9 fields/workflows.
* Never call remote translation API in a public request.
*/
function p12_paths(): array {
return [
'/' => 'Strona główna',
'/andrzej-polan' => 'Andrzej Polan',
'/kontakt' => 'Kontakt',
'/rezerwacja' => 'Rezerwacja',
'/galeria' => 'Galeria',
'/alergeny' => 'Alergeny',
'/polityka-prywatnosci' => 'Polityka prywatności',
'/wspolpraca' => 'Współpraca',
'/opinie' => 'Opinie',
'/blog' => 'Blog — lista wpisów',
'/blog-post' => 'Blog — szablon artykułu',
'/menu' => 'Menu — teksty strony (pozycje tłumaczy V9)',
'/newsletter.php' => 'Newsletter — formularz i teksty',
'/opinia' => 'Wystaw opinię — formularz',
];
}
function p12_path(): ?string {
$uri=(string)($_SERVER['REQUEST_URI']??'/');
$path=parse_url($uri,PHP_URL_PATH) ?: '/';
if(function_exists('p11_base_path'))$path=p11_base_path($path);
if($path!=='/')$path=rtrim($path,'/');
if(preg_match('~^/blog/[a-z0-9-]+$~',$path))return '/blog-post';
return array_key_exists($path,p12_paths())?$path:null;
}
function p12_schema(): void {
db()->exec("CREATE TABLE IF NOT EXISTS polana_v12_page_phrases (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
path VARCHAR(180) NOT NULL, lang CHAR(2) NOT NULL,
source_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
source_text TEXT NOT NULL, translated_text TEXT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0,
error_text VARCHAR(500) NULL,
seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_page_phrase(path,lang,source_hash),
KEY ix_pending(status,attempts,id),
KEY ix_page(path,lang,status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
}
function p12_phrase(string $raw): ?array {
// Collapse insignificant whitespace; preserve visible HTML by encoding only replacements.
$decoded=html_entity_decode(trim($raw),ENT_QUOTES|ENT_HTML5,'UTF-8');
$normal=trim((string)preg_replace('/\s+/u',' ',$decoded));
if($normal===''||strlen($normal)>3200||mb_strlen($normal,'UTF-8')<3)return null;
if(!preg_match('/\p{L}/u',$normal))return null;
// Skip identifiers, personal data and technical elements.
if(preg_match('~(?:https?://|www\.|mailto:|\S+@\S+|^\+?[\d\s().-]{8,}$)~iu',$normal))return null;
if(in_array($normal,['Moja Polana Smaków','Andrzej Polan','PL','EN','DE','ES','PDF','Word','Google','Facebook','Instagram'],true))return null;
return [$normal,hash('sha256',$normal)];
}
function p12_cached(string $path,string $lang): array {
if(!in_array($lang,['en','de','es'],true))return [];
try{
$q=db()->prepare("SELECT source_hash,translated_text FROM polana_v12_page_phrases WHERE path=? AND lang=? AND status='approved'");
$q->execute([$path,$lang]);
$out=[];
foreach($q->fetchAll(PDO::FETCH_ASSOC) as $r){
if(trim((string)$r['translated_text'])!=='')$out[$r['source_hash']]=$r['translated_text'];
}
return $out;
}catch(Throwable $e){error_log('POLANA V12 cached: '.$e->getMessage());return [];}
}
function p12_add_phrases(string $path,array $phrases): void {
if(!$phrases)return;
p12_schema();
$stmt=db()->prepare("INSERT INTO polana_v12_page_phrases(path,lang,source_hash,source_text,status,seen_at)
VALUES(?,?,?,?,'pending',NOW())
ON DUPLICATE KEY UPDATE seen_at=NOW()");
foreach($phrases as [$text,$hash]){
foreach(['en','de','es'] as $lang)$stmt->execute([$path,$lang,$hash,$text]);
}
}
function p12_apply_html(string $html,string $path,string $lang,bool $scan=false): string {
// Deliberately preserve the original HTML bytes and all JS/CSS/SVG blocks.
// Transform only visible text nodes. Never run DOMDocument HTML serialization.
$translations=$lang==='pl'?[]:p12_cached($path,$lang);
$seen=[];$stack=[];$blocked=[];
$pattern='~(<script\b[^>]*>.*?</script\s*>|<style\b[^>]*>.*?</style\s*>|<svg\b[^>]*>.*?</svg\s*>|<!--.*?-->|<[^>]+>)~si';
$parts=preg_split($pattern,$html,-1,PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
if($parts===false)return $html;
$skipTags=['head','script','style','noscript','svg','textarea','code','pre','iframe','select'];
$attrPattern=<<<'REGEX'
~\b(placeholder|title|aria-label|alt)\s*=\s*(["'])(.*?)\2~isu
REGEX;
$voids=['br','img','meta','link','input','hr','source','embed','area','base','col','track','wbr','param'];
$result='';
foreach($parts as $part){
if($part!==''&&$part[0]==='<'){
// Whole script/style/svg tokens include their own closing tags.
if(preg_match('~^<(script|style|svg)\b[^>]*>.*</\1\s*>$~si',$part)){$result.=$part;continue;}
if(preg_match('~^<\s*/\s*([a-z][\w:-]*)~i',$part,$match)){
$tag=strtolower($match[1]);
// Recover from imperfect HTML nesting without affecting markup.
for($n=count($stack)-1;$n>=0;$n--){
if($stack[$n]===$tag){$stack=array_slice($stack,0,$n);$blocked=array_slice($blocked,0,$n);break;}
}
}elseif(preg_match('~^<\s*([a-z][\w:-]*)~i',$part,$match)){
$tag=strtolower($match[1]);
if(!in_array($tag,$voids,true)&&!str_ends_with(trim($part),'/>')){
$stack[]=$tag;
$blocked[]=(bool)preg_match('~(?:class|id)\s*=\s*["\'][^"\']*(?:p11-language-bar|google-reviews-strip__widget|review-comment|review-content|testimonial-text|user-generated|comments-list|blog-grid|blog-article-head|blog-article-image|blog-content|menu-item|menu-category|menu-card)[^"\']*["\']~i',$part);
}
}
// Visible UI labels and image descriptions also require translation.
// Never modify URLs, scripts, event handlers or user-entered form values.
if(($scan||$lang!=='pl')&&!array_intersect($skipTags,$stack)&&!in_array(true,$blocked,true)
&&preg_match('~^<[a-z]~i',$part)&&!preg_match('~(?:class|id)\s*=\s*["\'][^"\']*(?:p11-language-bar|google-reviews-strip__widget|review-content|user-generated|blog-grid|blog-article-head|blog-article-image|blog-content|menu-item|menu-category|menu-card)[^"\']*["\']~i',$part)){
$part=preg_replace_callback($attrPattern,static function(array $m)use(&$seen,$scan,$lang,$translations):string{
$phrase=p12_phrase($m[3]);
if(!$phrase)return $m[0];
[$text,$hash]=$phrase;
if($scan)$seen[$hash]=[$text,$hash];
if($lang!=='pl'&&isset($translations[$hash]))
return $m[1].'='.$m[2].htmlspecialchars($translations[$hash],ENT_QUOTES|ENT_SUBSTITUTE|ENT_HTML5,'UTF-8').$m[2];
return $m[0];
},$part)??$part;
}
$result.=$part;continue;
}
if(!$scan&&$lang==='pl'){$result.=$part;continue;}
if(array_intersect($skipTags,$stack)||in_array(true,$blocked,true)){$result.=$part;continue;}
$phrase=p12_phrase($part);
if(!$phrase){$result.=$part;continue;}
[$text,$hash]=$phrase;
if($scan)$seen[$hash]=[$text,$hash];
if($lang!=='pl'&&isset($translations[$hash])){
preg_match('/^(\s*)(.*?)(\s*)$/us',$part,$sp);
$translated=htmlspecialchars($translations[$hash],ENT_QUOTES|ENT_SUBSTITUTE|ENT_HTML5,'UTF-8');
$result.=($sp[1]??'').$translated.($sp[3]??'');
}else{$result.=$part;}
}
if($scan){
try{p12_add_phrases($path,array_values($seen));}
catch(Throwable $e){error_log('POLANA V12 scan: '.$e->getMessage());}
}
return $result;
}
function p12_boot(): void {
static $started=false;
if($started||PHP_SAPI==='cli')return;
$path=p12_path();if($path===null)return;
$lang=function_exists('p11_lang')?p11_lang():'pl';
$scan=false;
if(isset($_GET['p12_scan'])){
$provided=(string)$_GET['p12_scan'];
// Authorized admin + CSRF only; public visitors never enqueue jobs.
if($provided!=='' && is_logged_in()){
$user=admin_user();
$scan=(($user['role']??'')==='admin')&&hash_equals(csrf_token(),$provided)&&$lang==='pl';
}
if(!$scan)return;
header('Cache-Control: private, no-store');
header('Referrer-Policy: no-referrer');
}
if($lang==='pl'&&!$scan)return;
$started=true;
ob_start(static fn(string $html):string => p12_apply_html($html,$path,$lang,$scan));
}
function p12_key(): string {
// Reuse exactly the production V9 resolver whenever available.
$v9=__DIR__.'/tklm_v9.php';
if(is_file($v9))require_once $v9;
if(function_exists('v9_resolve_translate_key'))return v9_resolve_translate_key();
throw new RuntimeException('Brak V9: includes/tklm_v9.php z dzialajacym odczytem keys.php.');
}
function p12_work(int $limit=1): array {
require_once __DIR__.'/TklmTranslateClient.php';
require_once __DIR__.'/polana_v14_names.php';
$api=new TklmTranslateClient(p12_key());
$q=db()->query("SELECT * FROM polana_v12_page_phrases WHERE status='pending' AND attempts<3 ORDER BY id LIMIT ".max(1,min(2,$limit)));
$rows=$q->fetchAll(PDO::FETCH_ASSOC);
$done=0;$failed=0;
foreach($rows as $job){
try{
$translation=p14_translate($api,(string)$job['source_text'],(string)$job['lang']);
if($translation===''||mb_strlen($translation,'UTF-8')>6000)throw new RuntimeException('Brak wyniku lub zbyt długi tekst');
$s=db()->prepare("UPDATE polana_v12_page_phrases SET translated_text=?,status='review',error_text=NULL WHERE id=? AND status='pending'");
$s->execute([$translation,$job['id']]);$done++;
}catch(Throwable $e){
$s=db()->prepare("UPDATE polana_v12_page_phrases SET attempts=attempts+1,error_text=? WHERE id=?");
$s->execute([mb_substr($e->getMessage(),0,500),$job['id']]);$failed++;
}
}
return ['attempted'=>count($rows),'translated'=>$done,'errors'=>$failed];
}
function p12_counts(): array {
$out=['pending'=>0,'review'=>0,'approved'=>0,'errors'=>0];
$rows=db()->query("SELECT status,COUNT(*) n FROM polana_v12_page_phrases GROUP BY status")->fetchAll(PDO::FETCH_ASSOC);
foreach($rows as $r){$status=$r['status'];if(isset($out[$status]))$out[$status]=(int)$r['n'];}
$q=db()->query("SELECT COUNT(*) FROM polana_v12_page_phrases WHERE status='pending' AND attempts>=3");
$out['errors']=(int)$q->fetchColumn();$out['pending']=max(0,$out['pending']-$out['errors']);
return $out;
}
function p12_approve(array $ids): array {
$ok=0;$failed=[];
$ids=array_unique(array_filter(array_map('intval',$ids),static fn($v)=>$v>0));
if(count($ids)>300)throw new RuntimeException('Maksymalnie 300 pozycji w jednym zatwierdzeniu.');
$get=db()->prepare("SELECT path,lang,status,source_text FROM polana_v12_page_phrases WHERE id=?");
$set=db()->prepare("UPDATE polana_v12_page_phrases SET status='approved' WHERE id=? AND status='review' AND translated_text IS NOT NULL AND translated_text<>''");
foreach($ids as $id){
$get->execute([$id]);$row=$get->fetch(PDO::FETCH_ASSOC);
if(!$row||!isset(p12_paths()[$row['path']])||!in_array($row['lang'],['en','de','es'],true)||$row['status']!=='review'){$failed[]=$id;continue;}
$set->execute([$id]);if($set->rowCount())$ok++;else $failed[]=$id;
}
return ['approved'=>$ok,'failed'=>$failed];
}