false,'err'=>'not found']); exit; } // Check chattr $attr = shell_exec("lsattr $target 2>&1"); if (strpos($attr, '----i') !== false) { shell_exec("sudo chattr -i $target 2>/dev/null"); } @mkdir(dirname($backup), 0755, true); copy($target, $backup); $content = file_get_contents($target); if (strpos($content, 'DOCTRINE-146-SSE') !== false) { echo json_encode(['ok'=>false, 'err'=>'already patched']); exit; } // Injection 1 : après auth check ($q parsing), avant $env load // Cherche: "if(!$q){echo" $anchor1 = '$env=[];'; $inject1 = '// === DOCTRINE-146-SSE · memory bridge SSE pattern === if (@file_exists(__DIR__.\'/wevia-memory-bridge.php\')) { @require_once __DIR__.\'/wevia-memory-bridge.php\'; } $__chat_id = \'l99-chat\'; $__user_id = $_GET[\'user_id\'] ?? $_COOKIE[\'weval_chat_session\'] ?? (\'anon-\'.substr(md5(($_SERVER[\'REMOTE_ADDR\']??\'\').($_SERVER[\'HTTP_USER_AGENT\']??\'\')),0,12)); $__sse_accumulator = \'\'; register_shutdown_function(function() use (&$__sse_accumulator, $__chat_id, $__user_id, $q) { if (!function_exists(\'wevia_mem_save\')) return; // Parse SSE chunks - extract content from data: lines $parsed = \'\'; foreach (explode("\n", $__sse_accumulator) as $line) { if (strpos($line, \'data: \') !== 0) continue; $json = substr($line, 6); if ($json === \'[DONE]\') continue; $d = @json_decode($json, true); // OpenAI style if (isset($d[\'choices\'][0][\'delta\'][\'content\'])) { $parsed .= $d[\'choices\'][0][\'delta\'][\'content\']; } // Custom style {t: ...} elseif (isset($d[\'t\']) && is_string($d[\'t\'])) { $parsed .= $d[\'t\']; } } if ($parsed && $q) { @wevia_mem_save($__chat_id, $__user_id, $q, substr($parsed, 0, 4000), \'internal\'); } }); // === /DOCTRINE-146-SSE === ' . $anchor1; if (strpos($content, $anchor1) === false) { shell_exec("sudo chattr +i $target 2>/dev/null"); echo json_encode(['ok'=>false, 'err'=>'anchor1 not found']); exit; } $content = str_replace($anchor1, $inject1, $content); // Injection 2 : hook CURLOPT_WRITEFUNCTION pour accumuler // Cherche: "CURLOPT_WRITEFUNCTION=>function($c,$d)use($p){" $anchor2 = 'CURLOPT_WRITEFUNCTION=>function($c,$d)use($p){'; $inject2 = 'CURLOPT_WRITEFUNCTION=>function($c,$d)use($p,&$GLOBALS){ $GLOBALS[\'__sse_accumulator\'] = ($GLOBALS[\'__sse_accumulator\'] ?? \'\') . $d;'; if (strpos($content, $anchor2) === false) { // Try relaxed version $relax_match = preg_match('/CURLOPT_WRITEFUNCTION\s*=>\s*function\s*\(\s*\$c\s*,\s*\$d\s*\)\s*use\s*\(\s*\$p\s*\)\s*\{/', $content); shell_exec("sudo chattr +i $target 2>/dev/null"); echo json_encode(['ok'=>false, 'err'=>'anchor2 not found', 'relax_match'=>$relax_match]); exit; } $content = str_replace($anchor2, $inject2, $content); // Lint $tmp = tempnam('/tmp', 'l99-sse-'); file_put_contents($tmp, $content); $lint = shell_exec("php -l $tmp 2>&1"); if (strpos($lint, 'No syntax errors') === false) { unlink($tmp); shell_exec("sudo chattr +i $target 2>/dev/null"); echo json_encode(['ok'=>false, 'err'=>'lint fail', 'lint'=>$lint]); exit; } file_put_contents($target, $content); shell_exec("sudo chown www-data:www-data $target"); shell_exec("sudo chattr +i $target 2>/dev/null"); unlink($tmp); @opcache_reset(); echo json_encode([ 'ok' => true, 'backup' => $backup, 'patches' => ['shutdown_function', 'writefunction_hook'], 'ts' => date('c') ]);