1<?php 2class action_plugin_mizarproofchecker extends \dokuwiki\Extension\ActionPlugin { 3 /** @inheritDoc */ 4 function register(Doku_Event_Handler $controller) { 5 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call'); 6 } 7 8 function handle_ajax_call(Doku_Event $event, $param) { 9 unset($param); // 未使用のパラメータを無効化 10 if ($event->data === 'source_sse') { 11 $event->preventDefault(); 12 $event->stopPropagation(); 13 $this->handleSourceSSERequest(); 14 return; 15 } elseif ($event->data === 'source_compile') { 16 $event->preventDefault(); 17 $event->stopPropagation(); 18 $this->handleSourceCompileRequest(); 19 return; 20 } elseif ($event->data === 'view_compile') { 21 $event->preventDefault(); 22 $event->stopPropagation(); 23 $this->handleViewCompileRequest(); 24 return; 25 } elseif ($event->data === 'view_sse') { 26 $event->preventDefault(); 27 $event->stopPropagation(); 28 $this->handleViewSSERequest(); 29 return; 30 } 31 } 32 33 // source用のコンパイルリクエスト処理 34 private function handleSourceCompileRequest() { 35 global $INPUT; 36 $pageContent = $INPUT->post->str('content'); 37 38 $mizarData = $this->extractMizarContent($pageContent); 39 40 if ($mizarData === null) { 41 $this->sendAjaxResponse(false, 'Mizar content not found'); 42 return; 43 } 44 45 $filePath = $this->saveMizarContent($mizarData); 46 47 // セッションにファイルパスを保存 48 session_start(); 49 $_SESSION['source_filepath'] = $filePath; 50 51 // コンパイルが成功したことをクライアントに通知 52 $this->sendAjaxResponse(true, 'Mizar content processed successfully'); 53 } 54 55 // source用のSSEリクエスト処理 56 private function handleSourceSSERequest() { 57 header('Content-Type: text/event-stream'); 58 header('Cache-Control: no-cache'); 59 60 session_start(); 61 if (!isset($_SESSION['source_filepath'])) { 62 echo "data: Mizar file path not found in session\n\n"; 63 ob_flush(); 64 flush(); 65 return; 66 } 67 68 $filePath = $_SESSION['source_filepath']; 69 70 // miz2prelコマンドを実行し、結果をストリーミング 71 $this->streamSourceOutput($filePath); 72 73 echo "event: end\n"; 74 echo "data: Compilation complete\n\n"; 75 ob_flush(); 76 flush(); 77 } 78 79 private function extractMizarContent($pageContent) { 80 $pattern = '/<mizar\s+([^>]+)>(.*?)<\/mizar>/s'; 81 preg_match_all($pattern, $pageContent, $matches, PREG_SET_ORDER); 82 83 if (empty($matches)) { 84 return null; // <mizar>タグが見つからない場合 85 } 86 87 $fileName = trim($matches[0][1]); // 最初の<mizar>タグからファイル名を取得 88 $combinedContent = ''; 89 90 foreach ($matches as $match) { 91 // 各<mizar>タグのファイル名が一致することを確認 92 if (trim($match[1]) !== $fileName) { 93 return ['error' => 'File name mismatch in <mizar> tags']; 94 } 95 96 // 各<mizar>タグの内容を連結 97 $combinedContent .= trim($match[2]) . "\n"; 98 } 99 100 return ['fileName' => $fileName, 'content' => $combinedContent]; 101 } 102 103 private function saveMizarContent($mizarData) { 104 // ワークパスを取得し、末尾にスラッシュやバックスラッシュがあれば削除 105 $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\'); 106 $filePath = $workPath . "/TEXT/" . $mizarData['fileName']; 107 file_put_contents($filePath, $mizarData['content']); 108 return $filePath; 109 } 110 111 private function streamSourceOutput($filePath) { 112 $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\'); 113 chdir($workPath); 114 115 $command = "miz2prel " . escapeshellarg($filePath); 116 $process = proc_open($command, array(1 => array("pipe", "w")), $pipes); 117 118 if (is_resource($process)) { 119 while ($line = fgets($pipes[1])) { 120 echo "data: " . $line . "\n\n"; 121 ob_flush(); 122 flush(); 123 } 124 fclose($pipes[1]); 125 proc_close($process); 126 } 127 } 128 129 // view用のコンパイルリクエスト処理 130 private function handleViewCompileRequest() { 131 global $INPUT; 132 $content = $INPUT->post->str('content'); 133 134 // 一時ファイルの作成 135 $filePath = $this->createTempFile($content); 136 137 // セッションにファイルパスを保存 138 session_start(); 139 $_SESSION['view_filepath'] = $filePath; 140 141 // コンパイル準備完了をレスポンス 142 $this->sendAjaxResponse(true, 'Mizar content processed successfully'); 143 } 144 145 // view用のSSEリクエスト処理 146 private function handleViewSSERequest() { 147 header('Content-Type: text/event-stream'); 148 header('Cache-Control: no-cache'); 149 150 session_start(); 151 if (!isset($_SESSION['view_filepath'])) { 152 echo "data: Mizar file path not found in session\n\n"; 153 ob_flush(); 154 flush(); 155 return; 156 } 157 158 $filePath = $_SESSION['view_filepath']; 159 160 // コンパイル結果をストリーミング 161 $this->streamCompileOutput($filePath); 162 163 echo "event: end\n"; 164 echo "data: Compilation complete\n\n"; 165 ob_flush(); 166 flush(); 167 } 168 169 // 一時ファイル作成はviewでのみ使用 170 private function createTempFile($content) { 171 $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\') . '/TEXT/'; 172 $uniqueName = str_replace('.', '_', uniqid('mizar', true)); 173 $tempFilename = $workPath . $uniqueName . ".miz"; 174 file_put_contents($tempFilename, $content); 175 return $tempFilename; 176 } 177 178 private function streamCompileOutput($filePath) { 179 $workPath = $this->getConf('mizar_work_dir'); 180 $sharePath = rtrim($this->getConf('mizar_share_dir'), '/\\') . '/'; 181 182 chdir($workPath); 183 184 $tempErrFilename = str_replace('.miz', '.err', $filePath); 185 $command = "makeenv " . escapeshellarg($filePath); 186 $process = proc_open($command, array(1 => array("pipe", "w"), 2 => array("pipe", "w")), $pipes); 187 188 if (is_resource($process)) { 189 while ($line = fgets($pipes[1])) { 190 echo "data: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 191 ob_flush(); 192 flush(); 193 } 194 fclose($pipes[1]); 195 196 while ($line = fgets($pipes[2])) { 197 echo "data: ERROR: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 198 ob_flush(); 199 flush(); 200 } 201 fclose($pipes[2]); 202 proc_close($process); 203 204 if (file_exists($tempErrFilename)) { 205 $errors = []; 206 $errorLines = file($tempErrFilename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 207 foreach ($errorLines as $errorLine) { 208 if (preg_match('/(\d+)\s+(\d+)\s+(\d+)/', $errorLine, $matches)) { 209 $errors[] = [ 210 'code' => intval($matches[3]), 211 'line' => intval($matches[1]), 212 'column' => intval($matches[2]), 213 'message' => $this->getMizarErrorMessages($sharePath . '/mizar.msg')[intval($matches[3])] ?? 'Unknown error' 214 ]; 215 } 216 } 217 if (!empty($errors)) { 218 echo "event: compileErrors\n"; 219 echo "data: " . json_encode($errors) . "\n\n"; 220 ob_flush(); 221 flush(); 222 return; 223 } 224 } 225 226 // verifierの実行 227 $exePath = rtrim($this->getConf('mizar_exe_dir'), '/\\') . '/'; 228 $verifierPath = escapeshellarg($exePath . "verifier"); 229 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 230 $verifierPath .= ".exe"; 231 } 232 $verifierCommand = $verifierPath . " -q -l " . escapeshellarg("TEXT/" . basename($filePath)); 233 234 $verifierProcess = proc_open($verifierCommand, array(1 => array("pipe", "w"), 2 => array("pipe", "w")), $verifierPipes); 235 236 if (is_resource($verifierProcess)) { 237 while ($line = fgets($verifierPipes[1])) { 238 echo "data: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 239 ob_flush(); 240 flush(); 241 } 242 fclose($verifierPipes[1]); 243 244 while ($line = fgets($verifierPipes[2])) { 245 echo "data: ERROR: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 246 ob_flush(); 247 flush(); 248 } 249 fclose($verifierPipes[2]); 250 proc_close($verifierProcess); 251 252 if (file_exists($tempErrFilename)) { 253 $errors = []; 254 $errorLines = file($tempErrFilename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 255 foreach ($errorLines as $errorLine) { 256 if (preg_match('/(\d+)\s+(\d+)\s+(\d+)/', $errorLine, $matches)) { 257 $errors[] = [ 258 'code' => intval($matches[3]), 259 'line' => intval($matches[1]), 260 'column' => intval($matches[2]), 261 'message' => $this->getMizarErrorMessages($workPath . '/MIZAR/mizar.msg')[intval($matches[3])] ?? 'Unknown error' 262 ]; 263 } 264 } 265 echo "event: compileErrors\n"; 266 echo "data: " . json_encode($errors) . "\n\n"; 267 ob_flush(); 268 flush(); 269 } 270 } else { 271 echo "data: ERROR: Failed to execute verifier command.\n\n"; 272 ob_flush(); 273 flush(); 274 } 275 } else { 276 echo "data: ERROR: Failed to execute makeenv command.\n\n"; 277 ob_flush(); 278 flush(); 279 } 280 } 281 282 // Mizarエラーメッセージを取得するメソッド 283 private function getMizarErrorMessages($mizarMsgFile) { 284 $errorMessages = []; 285 $lines = file($mizarMsgFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 286 287 $isReadingErrorMsg = false; 288 $key = 0; 289 290 foreach ($lines as $line) { 291 if (preg_match('/# (\d+)/', $line, $matches)) { 292 $isReadingErrorMsg = true; 293 $key = intval($matches[1]); 294 } elseif ($isReadingErrorMsg) { 295 $errorMessages[$key] = $line; 296 $isReadingErrorMsg = false; 297 } 298 } 299 300 return $errorMessages; 301 } 302 303 private function sendAjaxResponse($success, $message, $data = '') { 304 header('Content-Type: application/json'); 305 echo json_encode(['success' => $success, 'message' => $message, 'data' => $data]); 306 exit; 307 } 308} 309