16b7e227cSYamadaMiz<?php 247595f9cSYamadaMiz 347595f9cSYamadaMizuse dokuwiki\Extension\ActionPlugin; 447595f9cSYamadaMizuse dokuwiki\Extension\EventHandler; 547595f9cSYamadaMizuse dokuwiki\Extension\Event; 647595f9cSYamadaMiz 7f9af2148SYamadaMiz/** 8b68e3724SYamadaMiz * DokuWiki Plugin Mizar Verifiable Docs (Action Component) 9f9af2148SYamadaMiz * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 10*e813ba0eSYamada, M. * @author Yamada 11f9af2148SYamadaMiz */ 12b68e3724SYamadaMizclass action_plugin_mizarverifiabledocs extends ActionPlugin 1347595f9cSYamadaMiz{ 14*e813ba0eSYamada, M. /* ===================== Register ===================== */ 15*e813ba0eSYamada, M. 1647595f9cSYamadaMiz public function register(EventHandler $controller) 1747595f9cSYamadaMiz { 186b7e227cSYamadaMiz $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call'); 194f65af2cSYamadaMiz $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'handle_tpl_content_display'); 204f65af2cSYamadaMiz } 214f65af2cSYamadaMiz 224f65af2cSYamadaMiz public function handle_tpl_content_display(Event $event, $_param) 234f65af2cSYamadaMiz { 24*e813ba0eSYamada, M. if (!is_string($event->data)) return; 254f65af2cSYamadaMiz 264f65af2cSYamadaMiz $html = $event->data; 27*e813ba0eSYamada, M. if (strpos($html, 'mizarWrapper') !== false && strpos($html, 'id="hideAllButton"') === false) { 284f65af2cSYamadaMiz $buttonHtml = '<div class="hideAllContainer">' 294f65af2cSYamadaMiz . '<button id="hideAllButton" class="hide-all-button">Hide All</button>' 304f65af2cSYamadaMiz . '<button id="showAllButton" class="hide-all-button" style="display:none;">Show All</button>' 31*e813ba0eSYamada, M. . '<button id="resetAllButton" class="reset-all-button">Reset All</button>' 324f65af2cSYamadaMiz . '</div>'; 33*e813ba0eSYamada, M. $event->data = $buttonHtml . $html; 34*e813ba0eSYamada, M. } 35*e813ba0eSYamada, M. } 364f65af2cSYamadaMiz 37*e813ba0eSYamada, M. public function handle_ajax_call(Event $event, $param) 38*e813ba0eSYamada, M. { 39*e813ba0eSYamada, M. unset($param); 40*e813ba0eSYamada, M. switch ($event->data) { 41*e813ba0eSYamada, M. case 'clear_temp_files': 42*e813ba0eSYamada, M. $event->preventDefault(); $event->stopPropagation(); 43*e813ba0eSYamada, M. $this->clearTempFiles(); break; 44*e813ba0eSYamada, M. 45*e813ba0eSYamada, M. case 'source_compile': 46*e813ba0eSYamada, M. $event->preventDefault(); $event->stopPropagation(); 47*e813ba0eSYamada, M. $this->handleSourceCompileRequest(); break; 48*e813ba0eSYamada, M. 49*e813ba0eSYamada, M. case 'source_sse': 50*e813ba0eSYamada, M. $event->preventDefault(); $event->stopPropagation(); 51*e813ba0eSYamada, M. $this->handleSourceSSERequest(); break; 52*e813ba0eSYamada, M. 53*e813ba0eSYamada, M. case 'view_compile': 54*e813ba0eSYamada, M. $event->preventDefault(); $event->stopPropagation(); 55*e813ba0eSYamada, M. $this->handleViewCompileRequest(); break; 56*e813ba0eSYamada, M. 57*e813ba0eSYamada, M. case 'view_sse': 58*e813ba0eSYamada, M. $event->preventDefault(); $event->stopPropagation(); 59*e813ba0eSYamada, M. $this->handleViewSSERequest(); break; 60*e813ba0eSYamada, M. 61*e813ba0eSYamada, M. case 'create_combined_file': 62*e813ba0eSYamada, M. $event->preventDefault(); $event->stopPropagation(); 63*e813ba0eSYamada, M. $this->handle_create_combined_file(); break; 64*e813ba0eSYamada, M. 65*e813ba0eSYamada, M. case 'view_graph': 66*e813ba0eSYamada, M. $event->preventDefault(); $event->stopPropagation(); 67*e813ba0eSYamada, M. $this->handleViewGraphRequest(); break; 684f65af2cSYamadaMiz } 694f65af2cSYamadaMiz } 70*e813ba0eSYamada, M. 71*e813ba0eSYamada, M. /* ===================== Helpers ===================== */ 72*e813ba0eSYamada, M. 73*e813ba0eSYamada, M. private function isWindows(): bool { 74*e813ba0eSYamada, M. return strncasecmp(PHP_OS, 'WIN', 3) === 0; 75*e813ba0eSYamada, M. } 76*e813ba0eSYamada, M. 77*e813ba0eSYamada, M. /** 設定→未設定なら htdocs(= DOKU_INC の1つ上) 相対にフォールバックして正規化 */ 78*e813ba0eSYamada, M. private function resolvePaths(): array 79*e813ba0eSYamada, M. { 80*e813ba0eSYamada, M. // DokuWiki ルート(末尾スラッシュなしに正規化) 81*e813ba0eSYamada, M. $dokuroot = rtrim(realpath(DOKU_INC), '/\\'); 82*e813ba0eSYamada, M. 83*e813ba0eSYamada, M. // htdocs を「dokuwiki の 1つ上」から求める 84*e813ba0eSYamada, M. $htdocs = realpath($dokuroot . DIRECTORY_SEPARATOR . '..'); 85*e813ba0eSYamada, M. if ($htdocs === false) $htdocs = dirname($dokuroot); 86*e813ba0eSYamada, M. 87*e813ba0eSYamada, M. $defM = $htdocs . DIRECTORY_SEPARATOR . 'MIZAR'; 88*e813ba0eSYamada, M. $defW = $htdocs . DIRECTORY_SEPARATOR . 'work'; 89*e813ba0eSYamada, M. 90*e813ba0eSYamada, M. // 設定値取得(空ならフォールバック)。相対指定が来たら htdocs 基準に解決 91*e813ba0eSYamada, M. $exe = trim((string)$this->getConf('mizar_exe_dir')); 92*e813ba0eSYamada, M. $share = trim((string)$this->getConf('mizar_share_dir')); 93*e813ba0eSYamada, M. $work = trim((string)$this->getConf('mizar_work_dir')); 94*e813ba0eSYamada, M. 95*e813ba0eSYamada, M. $isAbs = function(string $p): bool { 96*e813ba0eSYamada, M. // Windowsドライブ/UNC/Unix ざっくり対応 97*e813ba0eSYamada, M. return $p !== '' && (preg_match('~^[A-Za-z]:[\\/]|^\\\\\\\\|^/~', $p) === 1); 98*e813ba0eSYamada, M. }; 99*e813ba0eSYamada, M. 100*e813ba0eSYamada, M. if ($exe !== '' && !$isAbs($exe)) $exe = $htdocs . DIRECTORY_SEPARATOR . $exe; 101*e813ba0eSYamada, M. if ($share !== '' && !$isAbs($share)) $share = $htdocs . DIRECTORY_SEPARATOR . $share; 102*e813ba0eSYamada, M. if ($work !== '' && !$isAbs($work)) $work = $htdocs . DIRECTORY_SEPARATOR . $work; 103*e813ba0eSYamada, M. 104*e813ba0eSYamada, M. $exe = rtrim($exe ?: $defM, '/\\'); 105*e813ba0eSYamada, M. $share = rtrim($share ?: $defM, '/\\'); 106*e813ba0eSYamada, M. $work = rtrim($work ?: $defW, '/\\'); 107*e813ba0eSYamada, M. 108*e813ba0eSYamada, M. return ['exe' => $exe, 'share' => $share, 'work' => $work]; 109*e813ba0eSYamada, M. } 110*e813ba0eSYamada, M. 111*e813ba0eSYamada, M. /** exeDir 直下 or exeDir\bin から実行ファイルを探す(.exe/.bat/.cmd 対応) */ 112*e813ba0eSYamada, M. private function findExe(string $exeDir, string $name): ?string 113*e813ba0eSYamada, M. { 114*e813ba0eSYamada, M. if ($this->isWindows()) { 115*e813ba0eSYamada, M. $candidates = [ 116*e813ba0eSYamada, M. "$exeDir\\$name.exe", "$exeDir\\bin\\$name.exe", 117*e813ba0eSYamada, M. "$exeDir\\$name.bat", "$exeDir\\bin\\$name.bat", 118*e813ba0eSYamada, M. "$exeDir\\$name.cmd", "$exeDir\\bin\\$name.cmd", 119*e813ba0eSYamada, M. "$exeDir\\windows\\bin\\$name.exe", 120*e813ba0eSYamada, M. "$exeDir\\win\\bin\\$name.exe", 121*e813ba0eSYamada, M. ]; 122*e813ba0eSYamada, M. } else { 123*e813ba0eSYamada, M. $candidates = [ 124*e813ba0eSYamada, M. "$exeDir/$name", "$exeDir/bin/$name", 125*e813ba0eSYamada, M. ]; 126*e813ba0eSYamada, M. } 127*e813ba0eSYamada, M. foreach ($candidates as $p) { 128*e813ba0eSYamada, M. if (is_file($p)) return $p; 129*e813ba0eSYamada, M. } 130*e813ba0eSYamada, M. return null; 131*e813ba0eSYamada, M. } 132*e813ba0eSYamada, M. 133*e813ba0eSYamada, M. /** 出力をUTF-8へ(WinはSJIS-WIN想定) */ 134*e813ba0eSYamada, M. private function outUTF8(string $s): string 135*e813ba0eSYamada, M. { 136*e813ba0eSYamada, M. return $this->isWindows() ? mb_convert_encoding($s, 'UTF-8', 'SJIS-WIN') : $s; 1376b7e227cSYamadaMiz } 1386b7e227cSYamadaMiz 13947595f9cSYamadaMiz /** 140*e813ba0eSYamada, M. * .exe は配列+bypass_shell、.bat/.cmd は「cmd /C ""...""」の文字列+shell経由で起動 141*e813ba0eSYamada, M. * @return array [$proc, $pipes] 失敗時は [null, []] 14247595f9cSYamadaMiz */ 143*e813ba0eSYamada, M. private function openProcess(string $exeOrBat, array $args, string $cwd): array 14447595f9cSYamadaMiz { 145*e813ba0eSYamada, M. $des = [1 => ['pipe','w'], 2 => ['pipe','w']]; 14647595f9cSYamadaMiz 147*e813ba0eSYamada, M. if ($this->isWindows() && preg_match('/\.(bat|cmd)$/i', $exeOrBat)) { 148*e813ba0eSYamada, M. // cmd /C ""C:\path\tool.bat" "arg1" "arg2"" 149*e813ba0eSYamada, M. $cmd = 'cmd.exe /C "' 150*e813ba0eSYamada, M. . '"' . $exeOrBat . '"'; 151*e813ba0eSYamada, M. foreach ($args as $a) { 152*e813ba0eSYamada, M. $cmd .= ' ' . escapeshellarg($a); 1536b7e227cSYamadaMiz } 154*e813ba0eSYamada, M. $cmd .= '"'; 155*e813ba0eSYamada, M. $proc = proc_open($cmd, $des, $pipes, $cwd); // shell経由(bypass_shell=false) 156*e813ba0eSYamada, M. } else { 157*e813ba0eSYamada, M. $cmd = array_merge([$exeOrBat], $args); 158*e813ba0eSYamada, M. $proc = proc_open($cmd, $des, $pipes, $cwd, null, ['bypass_shell' => true]); 1596b7e227cSYamadaMiz } 1606b7e227cSYamadaMiz 161*e813ba0eSYamada, M. if (!is_resource($proc)) return [null, []]; 162*e813ba0eSYamada, M. return [$proc, $pipes]; 163*e813ba0eSYamada, M. } 164*e813ba0eSYamada, M. 165*e813ba0eSYamada, M. /* ===================== Source ===================== */ 166*e813ba0eSYamada, M. 16747595f9cSYamadaMiz private function handleSourceCompileRequest() 16847595f9cSYamadaMiz { 1696b7e227cSYamadaMiz global $INPUT; 1706b7e227cSYamadaMiz $pageContent = $INPUT->post->str('content'); 1716b7e227cSYamadaMiz $mizarData = $this->extractMizarContent($pageContent); 1726b7e227cSYamadaMiz 173*e813ba0eSYamada, M. if ($mizarData === null) { $this->sendAjaxResponse(false, 'Mizar content not found'); return; } 174*e813ba0eSYamada, M. if (isset($mizarData['error'])) { $this->sendAjaxResponse(false, $mizarData['error']); return; } 1756b7e227cSYamadaMiz 1766b7e227cSYamadaMiz $filePath = $this->saveMizarContent($mizarData); 177*e813ba0eSYamada, M. if (session_status() !== PHP_SESSION_ACTIVE) session_start(); 1786b7e227cSYamadaMiz $_SESSION['source_filepath'] = $filePath; 1796b7e227cSYamadaMiz 1806b7e227cSYamadaMiz $this->sendAjaxResponse(true, 'Mizar content processed successfully'); 1816b7e227cSYamadaMiz } 1826b7e227cSYamadaMiz 18347595f9cSYamadaMiz private function handleSourceSSERequest() 18447595f9cSYamadaMiz { 1856b7e227cSYamadaMiz header('Content-Type: text/event-stream'); 18614f6cf5bSYamadaMiz header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); 18714f6cf5bSYamadaMiz header('Pragma: no-cache'); 18814f6cf5bSYamadaMiz header('Expires: 0'); 1896b7e227cSYamadaMiz 190*e813ba0eSYamada, M. if (session_status() !== PHP_SESSION_ACTIVE) session_start(); 191*e813ba0eSYamada, M. if (empty($_SESSION['source_filepath'])) { 192*e813ba0eSYamada, M. echo "data: Mizar file path not found in session\n\n"; @ob_flush(); @flush(); return; 1936b7e227cSYamadaMiz } 1946b7e227cSYamadaMiz 195*e813ba0eSYamada, M. $this->streamSourceOutput($_SESSION['source_filepath']); 1966b7e227cSYamadaMiz 1976b7e227cSYamadaMiz echo "event: end\n"; 1986b7e227cSYamadaMiz echo "data: Compilation complete\n\n"; 199*e813ba0eSYamada, M. @ob_flush(); @flush(); 2006b7e227cSYamadaMiz } 2016b7e227cSYamadaMiz 202*e813ba0eSYamada, M. /* ===================== View ===================== */ 203*e813ba0eSYamada, M. 20447595f9cSYamadaMiz private function handleViewCompileRequest() 20547595f9cSYamadaMiz { 20647595f9cSYamadaMiz global $INPUT; 20747595f9cSYamadaMiz $content = $INPUT->post->str('content'); 20847595f9cSYamadaMiz $filePath = $this->createTempFile($content); 20947595f9cSYamadaMiz 210*e813ba0eSYamada, M. if (session_status() !== PHP_SESSION_ACTIVE) session_start(); 21147595f9cSYamadaMiz $_SESSION['view_filepath'] = $filePath; 21247595f9cSYamadaMiz 21347595f9cSYamadaMiz $this->sendAjaxResponse(true, 'Mizar content processed successfully'); 21447595f9cSYamadaMiz } 21547595f9cSYamadaMiz 21647595f9cSYamadaMiz private function handleViewSSERequest() 21747595f9cSYamadaMiz { 21847595f9cSYamadaMiz header('Content-Type: text/event-stream'); 21914f6cf5bSYamadaMiz header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); 22014f6cf5bSYamadaMiz header('Pragma: no-cache'); 22114f6cf5bSYamadaMiz header('Expires: 0'); 22247595f9cSYamadaMiz 223*e813ba0eSYamada, M. if (session_status() !== PHP_SESSION_ACTIVE) session_start(); 224*e813ba0eSYamada, M. if (empty($_SESSION['view_filepath'])) { 225*e813ba0eSYamada, M. echo "data: Mizar file path not found in session\n\n"; @ob_flush(); @flush(); return; 22647595f9cSYamadaMiz } 22747595f9cSYamadaMiz 228*e813ba0eSYamada, M. $this->streamViewCompileOutput($_SESSION['view_filepath']); 22947595f9cSYamadaMiz 23047595f9cSYamadaMiz echo "event: end\n"; 23147595f9cSYamadaMiz echo "data: Compilation complete\n\n"; 232*e813ba0eSYamada, M. @ob_flush(); @flush(); 23347595f9cSYamadaMiz } 23447595f9cSYamadaMiz 235*e813ba0eSYamada, M. /***** view_graph: SVG を返す *****/ 2363f7dd076SYamadaMiz private function handleViewGraphRequest() 2373f7dd076SYamadaMiz { 2383f7dd076SYamadaMiz global $INPUT; 2393f7dd076SYamadaMiz $content = $INPUT->post->str('content', ''); 240*e813ba0eSYamada, M. if ($content === '') { $this->sendAjaxResponse(false, 'Empty content'); return; } 2413f7dd076SYamadaMiz 2423f7dd076SYamadaMiz $tmp = tempnam(sys_get_temp_dir(), 'miz'); 2433f7dd076SYamadaMiz $miz = $tmp . '.miz'; 244*e813ba0eSYamada, M. rename($tmp, $miz); 2453f7dd076SYamadaMiz file_put_contents($miz, $content); 2463f7dd076SYamadaMiz 247*e813ba0eSYamada, M. $parser = __DIR__ . '/script/miz2svg.py'; 248*e813ba0eSYamada, M. $py = $this->getConf('py_cmd') ?: 'python'; 249*e813ba0eSYamada, M. $svg = shell_exec(escapeshellcmd($py) . ' ' . escapeshellarg($parser) . ' ' . escapeshellarg($miz)); 250*e813ba0eSYamada, M. @unlink($miz); 2513f7dd076SYamadaMiz 252*e813ba0eSYamada, M. if ($svg) $this->sendAjaxResponse(true, 'success', ['svg' => $svg]); 253*e813ba0eSYamada, M. else $this->sendAjaxResponse(false, 'conversion failed'); 2543f7dd076SYamadaMiz } 2553f7dd076SYamadaMiz 256*e813ba0eSYamada, M. /* ===================== Content utils ===================== */ 257*e813ba0eSYamada, M. 25847595f9cSYamadaMiz private function extractMizarContent($pageContent) 25947595f9cSYamadaMiz { 2606b7e227cSYamadaMiz $pattern = '/<mizar\s+([^>]+)>(.*?)<\/mizar>/s'; 261*e813ba0eSYamada, M. preg_match_all($pattern, $pageContent, $m, PREG_SET_ORDER); 262*e813ba0eSYamada, M. if (empty($m)) return null; 2636b7e227cSYamadaMiz 264*e813ba0eSYamada, M. $fn = trim($m[0][1]); 265*e813ba0eSYamada, M. $stem = preg_replace('/\.miz$/i', '', $fn); 266*e813ba0eSYamada, M. if (!$this->isValidFileName($stem)) { 267*e813ba0eSYamada, M. return ['error' => "Invalid characters in file name: '{$stem}'. Only letters, numbers, underscores (_), and apostrophes (') are allowed, up to 8 characters."]; 2686b7e227cSYamadaMiz } 2696b7e227cSYamadaMiz 270*e813ba0eSYamada, M. $combined = ''; 271*e813ba0eSYamada, M. foreach ($m as $mm) { 272*e813ba0eSYamada, M. $cur = preg_replace('/\.miz$/i', '', trim($mm[1])); 273*e813ba0eSYamada, M. if ($cur !== $stem) return ['error' => "File name mismatch in <mizar> tags: '{$stem}' and '{$cur}'"]; 274*e813ba0eSYamada, M. if (!$this->isValidFileName($cur)) return ['error' => "Invalid characters in file name: '{$cur}'."]; 275*e813ba0eSYamada, M. $combined .= trim($mm[2]) . "\n"; 276*e813ba0eSYamada, M. } 277*e813ba0eSYamada, M. return ['fileName' => $stem . '.miz', 'content' => $combined]; 27842c7ffb2SYamadaMiz } 27942c7ffb2SYamadaMiz 28042c7ffb2SYamadaMiz private function isValidFileName($fileName) 28142c7ffb2SYamadaMiz { 282*e813ba0eSYamada, M. if (strlen($fileName) > 8) return false; 283*e813ba0eSYamada, M. return (bool)preg_match('/^[A-Za-z0-9_\']+$/', $fileName); 28442c7ffb2SYamadaMiz } 28542c7ffb2SYamadaMiz 28647595f9cSYamadaMiz private function saveMizarContent($mizarData) 28747595f9cSYamadaMiz { 288*e813ba0eSYamada, M. $paths = $this->resolvePaths(); 289*e813ba0eSYamada, M. $textDir = $paths['work'] . DIRECTORY_SEPARATOR . 'TEXT'; 290*e813ba0eSYamada, M. if (!is_dir($textDir)) @mkdir($textDir, 0777, true); 291*e813ba0eSYamada, M. 292*e813ba0eSYamada, M. $filePath = $textDir . DIRECTORY_SEPARATOR . $mizarData['fileName']; 2936b7e227cSYamadaMiz file_put_contents($filePath, $mizarData['content']); 2946b7e227cSYamadaMiz return $filePath; 2956b7e227cSYamadaMiz } 2966b7e227cSYamadaMiz 29747595f9cSYamadaMiz private function createTempFile($content) 29847595f9cSYamadaMiz { 299*e813ba0eSYamada, M. $paths = $this->resolvePaths(); 300*e813ba0eSYamada, M. $textDir = $paths['work'] . DIRECTORY_SEPARATOR . 'TEXT'; 301*e813ba0eSYamada, M. if (!is_dir($textDir)) @mkdir($textDir, 0777, true); 302*e813ba0eSYamada, M. 303*e813ba0eSYamada, M. $tempFilename = $textDir . DIRECTORY_SEPARATOR . str_replace('.', '_', uniqid('tmp', true)) . ".miz"; 3046b7e227cSYamadaMiz file_put_contents($tempFilename, $content); 3056b7e227cSYamadaMiz return $tempFilename; 3066b7e227cSYamadaMiz } 3076b7e227cSYamadaMiz 30847595f9cSYamadaMiz private function clearTempFiles() 30947595f9cSYamadaMiz { 310*e813ba0eSYamada, M. $paths = $this->resolvePaths(); 311*e813ba0eSYamada, M. $dir = $paths['work'] . DIRECTORY_SEPARATOR . 'TEXT' . DIRECTORY_SEPARATOR; 312*e813ba0eSYamada, M. $files = glob($dir . '*'); 31347595f9cSYamadaMiz 31447595f9cSYamadaMiz $errors = []; 315*e813ba0eSYamada, M. foreach ($files as $f) { 316*e813ba0eSYamada, M. if (is_file($f)) { 317*e813ba0eSYamada, M. if (!$this->is_file_locked($f)) { 318*e813ba0eSYamada, M. $ok = false; $retries = 5; 319*e813ba0eSYamada, M. while ($retries-- > 0) { if (@unlink($f)) { $ok = true; break; } sleep(2); } 320*e813ba0eSYamada, M. if (!$ok) $errors[] = "Failed to delete: $f"; 32147595f9cSYamadaMiz } else { 322*e813ba0eSYamada, M. $errors[] = "File is locked: $f"; 32347595f9cSYamadaMiz } 32447595f9cSYamadaMiz } 32547595f9cSYamadaMiz } 326*e813ba0eSYamada, M. if ($errors) $this->sendAjaxResponse(false, 'Some files could not be deleted', $errors); 327*e813ba0eSYamada, M. else $this->sendAjaxResponse(true, 'Temporary files cleared successfully'); 328*e813ba0eSYamada, M. } 32947595f9cSYamadaMiz 33047595f9cSYamadaMiz private function is_file_locked($file) 33147595f9cSYamadaMiz { 332*e813ba0eSYamada, M. $fp = @fopen($file, "r+"); 333*e813ba0eSYamada, M. if ($fp === false) return true; 334*e813ba0eSYamada, M. $locked = !flock($fp, LOCK_EX | LOCK_NB); 335*e813ba0eSYamada, M. fclose($fp); 336*e813ba0eSYamada, M. return $locked; 33747595f9cSYamadaMiz } 33847595f9cSYamadaMiz 339*e813ba0eSYamada, M. /* ===================== Run (miz2prel/makeenv/verifier) ===================== */ 34047595f9cSYamadaMiz 341*e813ba0eSYamada, M. private function streamSourceOutput($filePath) 34247595f9cSYamadaMiz { 343*e813ba0eSYamada, M. $paths = $this->resolvePaths(); 344*e813ba0eSYamada, M. $workPath = $paths['work']; 345*e813ba0eSYamada, M. $sharePath = $paths['share']; 346*e813ba0eSYamada, M. putenv("MIZFILES={$sharePath}"); 3476b7e227cSYamadaMiz 348*e813ba0eSYamada, M. $exe = $this->findExe($paths['exe'], 'miz2prel'); 349*e813ba0eSYamada, M. if ($exe === null) { 350*e813ba0eSYamada, M. echo "data: ERROR: miz2prel not found under {$paths['exe']} (or bin)\n\n"; @ob_flush(); @flush(); return; 351*e813ba0eSYamada, M. } 352a16ae7e3SYamadaMiz 353*e813ba0eSYamada, M. [$proc, $pipes] = $this->openProcess($exe, [$filePath], $workPath); 354*e813ba0eSYamada, M. if (!$proc) { echo "data: ERROR: Failed to execute miz2prel.\n\n"; @ob_flush(); @flush(); return; } 355*e813ba0eSYamada, M. 356*e813ba0eSYamada, M. while (($line = fgets($pipes[1])) !== false) { echo "data: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); } 357*e813ba0eSYamada, M. fclose($pipes[1]); 358*e813ba0eSYamada, M. while (($line = fgets($pipes[2])) !== false) { echo "data: ERROR: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); } 359*e813ba0eSYamada, M. fclose($pipes[2]); 360*e813ba0eSYamada, M. proc_close($proc); 3616b7e227cSYamadaMiz 3629fc5dc4bSYamadaMiz $errFilename = str_replace('.miz', '.err', $filePath); 363*e813ba0eSYamada, M. $this->handleCompilationErrors($errFilename, $sharePath . DIRECTORY_SEPARATOR . 'mizar.msg'); 3646b7e227cSYamadaMiz } 365*e813ba0eSYamada, M. 366*e813ba0eSYamada, M. private function streamViewCompileOutput($filePath) 367*e813ba0eSYamada, M. { 368*e813ba0eSYamada, M. $paths = $this->resolvePaths(); 369*e813ba0eSYamada, M. $workPath = $paths['work']; 370*e813ba0eSYamada, M. $sharePath = $paths['share']; 371*e813ba0eSYamada, M. putenv("MIZFILES={$sharePath}"); 372*e813ba0eSYamada, M. 373*e813ba0eSYamada, M. // makeenv 374*e813ba0eSYamada, M. $makeenv = $this->findExe($paths['exe'], 'makeenv'); 375*e813ba0eSYamada, M. if ($makeenv === null) { echo "data: ERROR: makeenv not found under {$paths['exe']} (or bin)\n\n"; @ob_flush(); @flush(); return; } 376*e813ba0eSYamada, M. [$proc, $pipes] = $this->openProcess($makeenv, [$filePath], $workPath); 377*e813ba0eSYamada, M. if (!$proc) { echo "data: ERROR: Failed to execute makeenv.\n\n"; @ob_flush(); @flush(); return; } 378*e813ba0eSYamada, M. while (($line = fgets($pipes[1])) !== false) { echo "data: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); } 3796b7e227cSYamadaMiz fclose($pipes[1]); 380*e813ba0eSYamada, M. while (($line = fgets($pipes[2])) !== false) { echo "data: ERROR: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); } 3816b7e227cSYamadaMiz fclose($pipes[2]); 382*e813ba0eSYamada, M. proc_close($proc); 3836b7e227cSYamadaMiz 384*e813ba0eSYamada, M. $errFilename = str_replace('.miz', '.err', $filePath); 385*e813ba0eSYamada, M. if ($this->handleCompilationErrors($errFilename, $sharePath . DIRECTORY_SEPARATOR . 'mizar.msg')) return; 386*e813ba0eSYamada, M. 387*e813ba0eSYamada, M. // verifier 388*e813ba0eSYamada, M. $verifier = $this->findExe($paths['exe'], 'verifier'); 389*e813ba0eSYamada, M. if ($verifier === null) { echo "data: ERROR: verifier not found under {$paths['exe']} (or bin)\n\n"; @ob_flush(); @flush(); return; } 390*e813ba0eSYamada, M. $rel = 'TEXT' . DIRECTORY_SEPARATOR . basename($filePath); 391*e813ba0eSYamada, M. [$proc, $pipes] = $this->openProcess($verifier, ['-q','-l',$rel], $workPath); 392*e813ba0eSYamada, M. if (!$proc) { echo "data: ERROR: Failed to execute verifier.\n\n"; @ob_flush(); @flush(); return; } 393*e813ba0eSYamada, M. while (($line = fgets($pipes[1])) !== false) { echo "data: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); } 394*e813ba0eSYamada, M. fclose($pipes[1]); 395*e813ba0eSYamada, M. while (($line = fgets($pipes[2])) !== false) { echo "data: ERROR: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); } 396*e813ba0eSYamada, M. fclose($pipes[2]); 397*e813ba0eSYamada, M. proc_close($proc); 398*e813ba0eSYamada, M. 399*e813ba0eSYamada, M. $this->handleCompilationErrors($errFilename, $sharePath . DIRECTORY_SEPARATOR . 'mizar.msg'); 4006b7e227cSYamadaMiz } 4016b7e227cSYamadaMiz 402*e813ba0eSYamada, M. /* ===================== Errors ===================== */ 4036b7e227cSYamadaMiz 40447595f9cSYamadaMiz private function getMizarErrorMessages($mizarMsgFile) 40547595f9cSYamadaMiz { 406*e813ba0eSYamada, M. if (!is_file($mizarMsgFile)) return []; 4076b7e227cSYamadaMiz $errorMessages = []; 4086b7e227cSYamadaMiz $lines = file($mizarMsgFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 409*e813ba0eSYamada, M. $isReading = false; $key = 0; 4106b7e227cSYamadaMiz foreach ($lines as $line) { 411*e813ba0eSYamada, M. if (preg_match('/# (\d+)/', $line, $m)) { $isReading = true; $key = (int)$m[1]; } 412*e813ba0eSYamada, M. elseif ($isReading) { $errorMessages[$key] = $line; $isReading = false; } 4136b7e227cSYamadaMiz } 414*e813ba0eSYamada, M. return $errorMessages; 4156b7e227cSYamadaMiz } 4166b7e227cSYamadaMiz 417*e813ba0eSYamada, M. private function handleCompilationErrors($errFilename, $mizarMsgFilePath) 418*e813ba0eSYamada, M. { 419*e813ba0eSYamada, M. if (!file_exists($errFilename)) return false; 420*e813ba0eSYamada, M. $errs = []; $lines = file($errFilename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 421*e813ba0eSYamada, M. foreach ($lines as $ln) { 422*e813ba0eSYamada, M. if (preg_match('/(\d+)\s+(\d+)\s+(\d+)/', $ln, $m)) { 423*e813ba0eSYamada, M. $code = (int)$m[3]; 424*e813ba0eSYamada, M. $errs[] = [ 425*e813ba0eSYamada, M. 'code' => $code, 426*e813ba0eSYamada, M. 'line' => (int)$m[1], 427*e813ba0eSYamada, M. 'column' => (int)$m[2], 428*e813ba0eSYamada, M. 'message' => $this->getMizarErrorMessages($mizarMsgFilePath)[$code] ?? 'Unknown error' 429*e813ba0eSYamada, M. ]; 430*e813ba0eSYamada, M. } 431*e813ba0eSYamada, M. } 432*e813ba0eSYamada, M. if ($errs) { 433*e813ba0eSYamada, M. echo "event: compileErrors\n"; 434*e813ba0eSYamada, M. echo "data: " . json_encode($errs) . "\n\n"; 435*e813ba0eSYamada, M. @ob_flush(); @flush(); 436*e813ba0eSYamada, M. return true; 437*e813ba0eSYamada, M. } 438*e813ba0eSYamada, M. return false; 4396b7e227cSYamadaMiz } 4406b7e227cSYamadaMiz 44147595f9cSYamadaMiz private function sendAjaxResponse($success, $message, $data = '') 44247595f9cSYamadaMiz { 4436b7e227cSYamadaMiz header('Content-Type: application/json'); 44414f6cf5bSYamadaMiz header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); 44514f6cf5bSYamadaMiz header('Pragma: no-cache'); 44614f6cf5bSYamadaMiz header('Expires: 0'); 4476b7e227cSYamadaMiz echo json_encode(['success' => $success, 'message' => $message, 'data' => $data]); 4486b7e227cSYamadaMiz exit; 4496b7e227cSYamadaMiz } 4509fc5dc4bSYamadaMiz 4519fc5dc4bSYamadaMiz private function handle_create_combined_file() 4529fc5dc4bSYamadaMiz { 4539fc5dc4bSYamadaMiz global $INPUT; 4549fc5dc4bSYamadaMiz $combinedContent = $INPUT->post->str('content'); 455*e813ba0eSYamada, M. $filename = $INPUT->post->str('filename', 'combined_file.miz'); 4569fc5dc4bSYamadaMiz 457a0444036SYamadaMiz if (!empty($combinedContent)) { 458a0444036SYamadaMiz $this->sendAjaxResponse(true, 'File created successfully', [ 459a0444036SYamadaMiz 'filename' => $filename, 460a0444036SYamadaMiz 'content' => $combinedContent 461a0444036SYamadaMiz ]); 4629fc5dc4bSYamadaMiz } else { 463a0444036SYamadaMiz $this->sendAjaxResponse(false, 'Content is empty, no file created'); 4649fc5dc4bSYamadaMiz } 4659fc5dc4bSYamadaMiz } 4666b7e227cSYamadaMiz} 467