16b7e227cSYamadaMiz<?php 2*47595f9cSYamadaMiz 3*47595f9cSYamadaMizuse dokuwiki\Extension\ActionPlugin; 4*47595f9cSYamadaMizuse dokuwiki\Extension\EventHandler; 5*47595f9cSYamadaMizuse dokuwiki\Extension\Event; 6*47595f9cSYamadaMiz 7*47595f9cSYamadaMizclass action_plugin_mizarproofchecker extends ActionPlugin 8*47595f9cSYamadaMiz{ 9*47595f9cSYamadaMiz /** 10*47595f9cSYamadaMiz * Registers a callback function for a given event 11*47595f9cSYamadaMiz * 12*47595f9cSYamadaMiz * @param EventHandler $controller DokuWiki's event controller object 13*47595f9cSYamadaMiz * @return void 14*47595f9cSYamadaMiz */ 15*47595f9cSYamadaMiz public function register(EventHandler $controller) 16*47595f9cSYamadaMiz { 176b7e227cSYamadaMiz $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call'); 186b7e227cSYamadaMiz } 196b7e227cSYamadaMiz 20*47595f9cSYamadaMiz /** 21*47595f9cSYamadaMiz * Handles AJAX requests 22*47595f9cSYamadaMiz * 23*47595f9cSYamadaMiz * @param Event $event 24*47595f9cSYamadaMiz * @param $param 25*47595f9cSYamadaMiz */ 26*47595f9cSYamadaMiz public function handle_ajax_call(Event $event, $param) 27*47595f9cSYamadaMiz { 286b7e227cSYamadaMiz unset($param); // 未使用のパラメータを無効化 29*47595f9cSYamadaMiz 30*47595f9cSYamadaMiz switch ($event->data) { 31*47595f9cSYamadaMiz case 'clear_temp_files': 32*47595f9cSYamadaMiz $event->preventDefault(); 33*47595f9cSYamadaMiz $event->stopPropagation(); 34*47595f9cSYamadaMiz $this->clearTempFiles(); // 追加: 一時ファイル削除の呼び出し 35*47595f9cSYamadaMiz break; 36*47595f9cSYamadaMiz case 'source_sse': 376b7e227cSYamadaMiz $event->preventDefault(); 386b7e227cSYamadaMiz $event->stopPropagation(); 396b7e227cSYamadaMiz $this->handleSourceSSERequest(); 40*47595f9cSYamadaMiz break; 41*47595f9cSYamadaMiz case 'source_compile': 426b7e227cSYamadaMiz $event->preventDefault(); 436b7e227cSYamadaMiz $event->stopPropagation(); 446b7e227cSYamadaMiz $this->handleSourceCompileRequest(); 45*47595f9cSYamadaMiz break; 46*47595f9cSYamadaMiz case 'view_compile': 476b7e227cSYamadaMiz $event->preventDefault(); 486b7e227cSYamadaMiz $event->stopPropagation(); 496b7e227cSYamadaMiz $this->handleViewCompileRequest(); 50*47595f9cSYamadaMiz break; 51*47595f9cSYamadaMiz case 'view_sse': 526b7e227cSYamadaMiz $event->preventDefault(); 536b7e227cSYamadaMiz $event->stopPropagation(); 546b7e227cSYamadaMiz $this->handleViewSSERequest(); 55*47595f9cSYamadaMiz break; 566b7e227cSYamadaMiz } 576b7e227cSYamadaMiz } 586b7e227cSYamadaMiz 596b7e227cSYamadaMiz // source用のコンパイルリクエスト処理 60*47595f9cSYamadaMiz private function handleSourceCompileRequest() 61*47595f9cSYamadaMiz { 626b7e227cSYamadaMiz global $INPUT; 636b7e227cSYamadaMiz $pageContent = $INPUT->post->str('content'); 646b7e227cSYamadaMiz $mizarData = $this->extractMizarContent($pageContent); 656b7e227cSYamadaMiz 666b7e227cSYamadaMiz if ($mizarData === null) { 676b7e227cSYamadaMiz $this->sendAjaxResponse(false, 'Mizar content not found'); 686b7e227cSYamadaMiz return; 696b7e227cSYamadaMiz } 706b7e227cSYamadaMiz 716b7e227cSYamadaMiz $filePath = $this->saveMizarContent($mizarData); 726b7e227cSYamadaMiz 736b7e227cSYamadaMiz session_start(); 746b7e227cSYamadaMiz $_SESSION['source_filepath'] = $filePath; 756b7e227cSYamadaMiz 766b7e227cSYamadaMiz $this->sendAjaxResponse(true, 'Mizar content processed successfully'); 776b7e227cSYamadaMiz } 786b7e227cSYamadaMiz 796b7e227cSYamadaMiz // source用のSSEリクエスト処理 80*47595f9cSYamadaMiz private function handleSourceSSERequest() 81*47595f9cSYamadaMiz { 826b7e227cSYamadaMiz header('Content-Type: text/event-stream'); 836b7e227cSYamadaMiz header('Cache-Control: no-cache'); 846b7e227cSYamadaMiz 856b7e227cSYamadaMiz session_start(); 866b7e227cSYamadaMiz if (!isset($_SESSION['source_filepath'])) { 876b7e227cSYamadaMiz echo "data: Mizar file path not found in session\n\n"; 886b7e227cSYamadaMiz ob_flush(); 896b7e227cSYamadaMiz flush(); 906b7e227cSYamadaMiz return; 916b7e227cSYamadaMiz } 926b7e227cSYamadaMiz 936b7e227cSYamadaMiz $filePath = $_SESSION['source_filepath']; 946b7e227cSYamadaMiz $this->streamSourceOutput($filePath); 956b7e227cSYamadaMiz 966b7e227cSYamadaMiz echo "event: end\n"; 976b7e227cSYamadaMiz echo "data: Compilation complete\n\n"; 986b7e227cSYamadaMiz ob_flush(); 996b7e227cSYamadaMiz flush(); 1006b7e227cSYamadaMiz } 1016b7e227cSYamadaMiz 102*47595f9cSYamadaMiz // view用のコンパイルリクエスト処理 103*47595f9cSYamadaMiz private function handleViewCompileRequest() 104*47595f9cSYamadaMiz { 105*47595f9cSYamadaMiz global $INPUT; 106*47595f9cSYamadaMiz $content = $INPUT->post->str('content'); 107*47595f9cSYamadaMiz 108*47595f9cSYamadaMiz $filePath = $this->createTempFile($content); 109*47595f9cSYamadaMiz 110*47595f9cSYamadaMiz session_start(); 111*47595f9cSYamadaMiz $_SESSION['view_filepath'] = $filePath; 112*47595f9cSYamadaMiz 113*47595f9cSYamadaMiz $this->sendAjaxResponse(true, 'Mizar content processed successfully'); 114*47595f9cSYamadaMiz } 115*47595f9cSYamadaMiz 116*47595f9cSYamadaMiz // view用のSSEリクエスト処理 117*47595f9cSYamadaMiz private function handleViewSSERequest() 118*47595f9cSYamadaMiz { 119*47595f9cSYamadaMiz header('Content-Type: text/event-stream'); 120*47595f9cSYamadaMiz header('Cache-Control: no-cache'); 121*47595f9cSYamadaMiz 122*47595f9cSYamadaMiz session_start(); 123*47595f9cSYamadaMiz if (!isset($_SESSION['view_filepath'])) { 124*47595f9cSYamadaMiz echo "data: Mizar file path not found in session\n\n"; 125*47595f9cSYamadaMiz ob_flush(); 126*47595f9cSYamadaMiz flush(); 127*47595f9cSYamadaMiz return; 128*47595f9cSYamadaMiz } 129*47595f9cSYamadaMiz 130*47595f9cSYamadaMiz $filePath = $_SESSION['view_filepath']; 131*47595f9cSYamadaMiz $this->streamCompileOutput($filePath); 132*47595f9cSYamadaMiz 133*47595f9cSYamadaMiz echo "event: end\n"; 134*47595f9cSYamadaMiz echo "data: Compilation complete\n\n"; 135*47595f9cSYamadaMiz ob_flush(); 136*47595f9cSYamadaMiz flush(); 137*47595f9cSYamadaMiz } 138*47595f9cSYamadaMiz 139*47595f9cSYamadaMiz // Mizarコンテンツの抽出 140*47595f9cSYamadaMiz private function extractMizarContent($pageContent) 141*47595f9cSYamadaMiz { 1426b7e227cSYamadaMiz $pattern = '/<mizar\s+([^>]+)>(.*?)<\/mizar>/s'; 1436b7e227cSYamadaMiz preg_match_all($pattern, $pageContent, $matches, PREG_SET_ORDER); 1446b7e227cSYamadaMiz 1456b7e227cSYamadaMiz if (empty($matches)) { 146*47595f9cSYamadaMiz return null; 1476b7e227cSYamadaMiz } 1486b7e227cSYamadaMiz 149*47595f9cSYamadaMiz $fileName = trim($matches[0][1]); 1506b7e227cSYamadaMiz $combinedContent = ''; 1516b7e227cSYamadaMiz 1526b7e227cSYamadaMiz foreach ($matches as $match) { 1536b7e227cSYamadaMiz if (trim($match[1]) !== $fileName) { 1546b7e227cSYamadaMiz return ['error' => 'File name mismatch in <mizar> tags']; 1556b7e227cSYamadaMiz } 1566b7e227cSYamadaMiz $combinedContent .= trim($match[2]) . "\n"; 1576b7e227cSYamadaMiz } 1586b7e227cSYamadaMiz 1596b7e227cSYamadaMiz return ['fileName' => $fileName, 'content' => $combinedContent]; 1606b7e227cSYamadaMiz } 1616b7e227cSYamadaMiz 162*47595f9cSYamadaMiz // Mizarコンテンツの保存 163*47595f9cSYamadaMiz private function saveMizarContent($mizarData) 164*47595f9cSYamadaMiz { 1656b7e227cSYamadaMiz $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\'); 1666b7e227cSYamadaMiz $filePath = $workPath . "/TEXT/" . $mizarData['fileName']; 1676b7e227cSYamadaMiz file_put_contents($filePath, $mizarData['content']); 1686b7e227cSYamadaMiz return $filePath; 1696b7e227cSYamadaMiz } 1706b7e227cSYamadaMiz 171*47595f9cSYamadaMiz // source用の出力をストリーム 172*47595f9cSYamadaMiz private function streamSourceOutput($filePath) 173*47595f9cSYamadaMiz { 1746b7e227cSYamadaMiz $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\'); 1756b7e227cSYamadaMiz chdir($workPath); 1766b7e227cSYamadaMiz 1776b7e227cSYamadaMiz $command = "miz2prel " . escapeshellarg($filePath); 1786b7e227cSYamadaMiz $process = proc_open($command, array(1 => array("pipe", "w")), $pipes); 1796b7e227cSYamadaMiz 1806b7e227cSYamadaMiz if (is_resource($process)) { 1816b7e227cSYamadaMiz while ($line = fgets($pipes[1])) { 1826b7e227cSYamadaMiz echo "data: " . $line . "\n\n"; 1836b7e227cSYamadaMiz ob_flush(); 1846b7e227cSYamadaMiz flush(); 1856b7e227cSYamadaMiz } 1866b7e227cSYamadaMiz fclose($pipes[1]); 1876b7e227cSYamadaMiz proc_close($process); 1886b7e227cSYamadaMiz } 1896b7e227cSYamadaMiz } 1906b7e227cSYamadaMiz 191*47595f9cSYamadaMiz // view用の一時ファイル作成 192*47595f9cSYamadaMiz private function createTempFile($content) 193*47595f9cSYamadaMiz { 1946b7e227cSYamadaMiz $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\') . '/TEXT/'; 195*47595f9cSYamadaMiz $uniqueName = str_replace('.', '_', uniqid('tmp', true)); 1966b7e227cSYamadaMiz $tempFilename = $workPath . $uniqueName . ".miz"; 1976b7e227cSYamadaMiz file_put_contents($tempFilename, $content); 1986b7e227cSYamadaMiz return $tempFilename; 1996b7e227cSYamadaMiz } 2006b7e227cSYamadaMiz 201*47595f9cSYamadaMiz // Clear all temporary files in the TEXT folder 202*47595f9cSYamadaMiz private function clearTempFiles() 203*47595f9cSYamadaMiz { 204*47595f9cSYamadaMiz $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\') . '/TEXT/'; 205*47595f9cSYamadaMiz $files = glob($workPath . '*'); // TEXTフォルダ内のすべてのファイルを取得 206*47595f9cSYamadaMiz 207*47595f9cSYamadaMiz $errors = []; 208*47595f9cSYamadaMiz foreach ($files as $file) { 209*47595f9cSYamadaMiz if (is_file($file)) { 210*47595f9cSYamadaMiz // ファイルが使用中かどうか確認 211*47595f9cSYamadaMiz if (!$this->is_file_locked($file)) { 212*47595f9cSYamadaMiz $retries = 3; // 最大3回リトライ 213*47595f9cSYamadaMiz while ($retries > 0) { 214*47595f9cSYamadaMiz if (unlink($file)) { 215*47595f9cSYamadaMiz break; // 削除成功 216*47595f9cSYamadaMiz } 217*47595f9cSYamadaMiz $retries--; 218*47595f9cSYamadaMiz sleep(1); // 1秒待ってリトライ 219*47595f9cSYamadaMiz } 220*47595f9cSYamadaMiz if ($retries === 0) { 221*47595f9cSYamadaMiz $errors[] = "Failed to delete: $file"; // 削除失敗 222*47595f9cSYamadaMiz } 223*47595f9cSYamadaMiz } else { 224*47595f9cSYamadaMiz $errors[] = "File is locked: $file"; // ファイルがロックされている 225*47595f9cSYamadaMiz } 226*47595f9cSYamadaMiz } 227*47595f9cSYamadaMiz } 228*47595f9cSYamadaMiz 229*47595f9cSYamadaMiz if (empty($errors)) { 230*47595f9cSYamadaMiz $this->sendAjaxResponse(true, 'Temporary files cleared successfully'); 231*47595f9cSYamadaMiz } else { 232*47595f9cSYamadaMiz $this->sendAjaxResponse(false, 'Some files could not be deleted', $errors); 233*47595f9cSYamadaMiz } 234*47595f9cSYamadaMiz } 235*47595f9cSYamadaMiz 236*47595f9cSYamadaMiz // ファイルがロックされているかをチェックする関数 237*47595f9cSYamadaMiz private function is_file_locked($file) 238*47595f9cSYamadaMiz { 239*47595f9cSYamadaMiz $fileHandle = @fopen($file, "r+"); 240*47595f9cSYamadaMiz 241*47595f9cSYamadaMiz if ($fileHandle === false) { 242*47595f9cSYamadaMiz return true; // ファイルが開けない、つまりロックされているかアクセス権がない 243*47595f9cSYamadaMiz } 244*47595f9cSYamadaMiz 245*47595f9cSYamadaMiz $locked = !flock($fileHandle, LOCK_EX | LOCK_NB); // ロックの取得を試みる(非ブロッキングモード) 246*47595f9cSYamadaMiz 247*47595f9cSYamadaMiz fclose($fileHandle); 248*47595f9cSYamadaMiz return $locked; // ロックが取得できなければファイルはロックされている 249*47595f9cSYamadaMiz } 250*47595f9cSYamadaMiz 251*47595f9cSYamadaMiz // コンパイル出力のストリーム 252*47595f9cSYamadaMiz private function streamCompileOutput($filePath) 253*47595f9cSYamadaMiz { 2546b7e227cSYamadaMiz $workPath = $this->getConf('mizar_work_dir'); 2556b7e227cSYamadaMiz $sharePath = rtrim($this->getConf('mizar_share_dir'), '/\\') . '/'; 2566b7e227cSYamadaMiz 2576b7e227cSYamadaMiz chdir($workPath); 2586b7e227cSYamadaMiz 2596b7e227cSYamadaMiz $tempErrFilename = str_replace('.miz', '.err', $filePath); 2606b7e227cSYamadaMiz $command = "makeenv " . escapeshellarg($filePath); 2616b7e227cSYamadaMiz $process = proc_open($command, array(1 => array("pipe", "w"), 2 => array("pipe", "w")), $pipes); 2626b7e227cSYamadaMiz 2636b7e227cSYamadaMiz if (is_resource($process)) { 2646b7e227cSYamadaMiz while ($line = fgets($pipes[1])) { 2656b7e227cSYamadaMiz echo "data: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 2666b7e227cSYamadaMiz ob_flush(); 2676b7e227cSYamadaMiz flush(); 2686b7e227cSYamadaMiz } 2696b7e227cSYamadaMiz fclose($pipes[1]); 2706b7e227cSYamadaMiz 2716b7e227cSYamadaMiz while ($line = fgets($pipes[2])) { 2726b7e227cSYamadaMiz echo "data: ERROR: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 2736b7e227cSYamadaMiz ob_flush(); 2746b7e227cSYamadaMiz flush(); 2756b7e227cSYamadaMiz } 2766b7e227cSYamadaMiz fclose($pipes[2]); 2776b7e227cSYamadaMiz proc_close($process); 2786b7e227cSYamadaMiz 279*47595f9cSYamadaMiz // エラー処理 2806b7e227cSYamadaMiz if (file_exists($tempErrFilename)) { 2816b7e227cSYamadaMiz $errors = []; 2826b7e227cSYamadaMiz $errorLines = file($tempErrFilename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 2836b7e227cSYamadaMiz foreach ($errorLines as $errorLine) { 2846b7e227cSYamadaMiz if (preg_match('/(\d+)\s+(\d+)\s+(\d+)/', $errorLine, $matches)) { 2856b7e227cSYamadaMiz $errors[] = [ 2866b7e227cSYamadaMiz 'code' => intval($matches[3]), 2876b7e227cSYamadaMiz 'line' => intval($matches[1]), 2886b7e227cSYamadaMiz 'column' => intval($matches[2]), 2896b7e227cSYamadaMiz 'message' => $this->getMizarErrorMessages($sharePath . '/mizar.msg')[intval($matches[3])] ?? 'Unknown error' 2906b7e227cSYamadaMiz ]; 2916b7e227cSYamadaMiz } 2926b7e227cSYamadaMiz } 2936b7e227cSYamadaMiz if (!empty($errors)) { 2946b7e227cSYamadaMiz echo "event: compileErrors\n"; 2956b7e227cSYamadaMiz echo "data: " . json_encode($errors) . "\n\n"; 2966b7e227cSYamadaMiz ob_flush(); 2976b7e227cSYamadaMiz flush(); 2986b7e227cSYamadaMiz return; 2996b7e227cSYamadaMiz } 3006b7e227cSYamadaMiz } 3016b7e227cSYamadaMiz 3026b7e227cSYamadaMiz // verifierの実行 3036b7e227cSYamadaMiz $exePath = rtrim($this->getConf('mizar_exe_dir'), '/\\') . '/'; 3046b7e227cSYamadaMiz $verifierPath = escapeshellarg($exePath . "verifier"); 3056b7e227cSYamadaMiz if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 3066b7e227cSYamadaMiz $verifierPath .= ".exe"; 3076b7e227cSYamadaMiz } 3086b7e227cSYamadaMiz $verifierCommand = $verifierPath . " -q -l " . escapeshellarg("TEXT/" . basename($filePath)); 3096b7e227cSYamadaMiz 3106b7e227cSYamadaMiz $verifierProcess = proc_open($verifierCommand, array(1 => array("pipe", "w"), 2 => array("pipe", "w")), $verifierPipes); 3116b7e227cSYamadaMiz 3126b7e227cSYamadaMiz if (is_resource($verifierProcess)) { 3136b7e227cSYamadaMiz while ($line = fgets($verifierPipes[1])) { 3146b7e227cSYamadaMiz echo "data: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 3156b7e227cSYamadaMiz ob_flush(); 3166b7e227cSYamadaMiz flush(); 3176b7e227cSYamadaMiz } 3186b7e227cSYamadaMiz fclose($verifierPipes[1]); 3196b7e227cSYamadaMiz 3206b7e227cSYamadaMiz while ($line = fgets($verifierPipes[2])) { 3216b7e227cSYamadaMiz echo "data: ERROR: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 3226b7e227cSYamadaMiz ob_flush(); 3236b7e227cSYamadaMiz flush(); 3246b7e227cSYamadaMiz } 3256b7e227cSYamadaMiz fclose($verifierPipes[2]); 3266b7e227cSYamadaMiz proc_close($verifierProcess); 3276b7e227cSYamadaMiz } else { 3286b7e227cSYamadaMiz echo "data: ERROR: Failed to execute verifier command.\n\n"; 3296b7e227cSYamadaMiz ob_flush(); 3306b7e227cSYamadaMiz flush(); 3316b7e227cSYamadaMiz } 3326b7e227cSYamadaMiz } else { 3336b7e227cSYamadaMiz echo "data: ERROR: Failed to execute makeenv command.\n\n"; 3346b7e227cSYamadaMiz ob_flush(); 3356b7e227cSYamadaMiz flush(); 3366b7e227cSYamadaMiz } 3376b7e227cSYamadaMiz } 3386b7e227cSYamadaMiz 339*47595f9cSYamadaMiz private function getMizarErrorMessages($mizarMsgFile) 340*47595f9cSYamadaMiz { 3416b7e227cSYamadaMiz $errorMessages = []; 3426b7e227cSYamadaMiz $lines = file($mizarMsgFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 3436b7e227cSYamadaMiz 3446b7e227cSYamadaMiz $isReadingErrorMsg = false; 3456b7e227cSYamadaMiz $key = 0; 3466b7e227cSYamadaMiz 3476b7e227cSYamadaMiz foreach ($lines as $line) { 3486b7e227cSYamadaMiz if (preg_match('/# (\d+)/', $line, $matches)) { 3496b7e227cSYamadaMiz $isReadingErrorMsg = true; 3506b7e227cSYamadaMiz $key = intval($matches[1]); 3516b7e227cSYamadaMiz } elseif ($isReadingErrorMsg) { 3526b7e227cSYamadaMiz $errorMessages[$key] = $line; 3536b7e227cSYamadaMiz $isReadingErrorMsg = false; 3546b7e227cSYamadaMiz } 3556b7e227cSYamadaMiz } 3566b7e227cSYamadaMiz 3576b7e227cSYamadaMiz return $errorMessages; 3586b7e227cSYamadaMiz } 3596b7e227cSYamadaMiz 360*47595f9cSYamadaMiz private function sendAjaxResponse($success, $message, $data = '') 361*47595f9cSYamadaMiz { 3626b7e227cSYamadaMiz header('Content-Type: application/json'); 3636b7e227cSYamadaMiz echo json_encode(['success' => $success, 'message' => $message, 'data' => $data]); 3646b7e227cSYamadaMiz exit; 3656b7e227cSYamadaMiz } 3666b7e227cSYamadaMiz} 367