1<?php 2 3use dokuwiki\Extension\ActionPlugin; 4use dokuwiki\Extension\EventHandler; 5use dokuwiki\Extension\Event; 6 7class action_plugin_mizarproofchecker extends ActionPlugin 8{ 9 /** 10 * Registers a callback function for a given event test 11 * 12 * @param EventHandler $controller DokuWiki's event controller object 13 * @return void 14 */ 15 public function register(EventHandler $controller) 16 { 17 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call'); 18 } 19 20 /** 21 * Handles AJAX requests 22 * 23 * @param Event $event 24 * @param $param 25 */ 26 public function handle_ajax_call(Event $event, $param) 27 { 28 unset($param); // 未使用のパラメータを無効化 29 30 switch ($event->data) { 31 case 'clear_temp_files': 32 $event->preventDefault(); 33 $event->stopPropagation(); 34 $this->clearTempFiles(); // 追加: 一時ファイル削除の呼び出し 35 break; 36 case 'source_sse': 37 $event->preventDefault(); 38 $event->stopPropagation(); 39 $this->handleSourceSSERequest(); 40 break; 41 case 'source_compile': 42 $event->preventDefault(); 43 $event->stopPropagation(); 44 $this->handleSourceCompileRequest(); 45 break; 46 case 'view_compile': 47 $event->preventDefault(); 48 $event->stopPropagation(); 49 $this->handleViewCompileRequest(); 50 break; 51 case 'view_sse': 52 $event->preventDefault(); 53 $event->stopPropagation(); 54 $this->handleViewSSERequest(); 55 break; 56 case 'create_combined_file': 57 $event->preventDefault(); 58 $event->stopPropagation(); 59 $this->handle_create_combined_file(); 60 break; 61 } 62 } 63 64 // source用のコンパイルリクエスト処理 65 private function handleSourceCompileRequest() 66 { 67 global $INPUT; 68 $pageContent = $INPUT->post->str('content'); 69 $mizarData = $this->extractMizarContent($pageContent); 70 71 if ($mizarData === null) { 72 $this->sendAjaxResponse(false, 'Mizar content not found'); 73 return; 74 } 75 76 $filePath = $this->saveMizarContent($mizarData); 77 78 session_start(); 79 $_SESSION['source_filepath'] = $filePath; 80 81 $this->sendAjaxResponse(true, 'Mizar content processed successfully'); 82 } 83 84 // source用のSSEリクエスト処理 85 private function handleSourceSSERequest() 86 { 87 header('Content-Type: text/event-stream'); 88 header('Cache-Control: no-cache'); 89 90 session_start(); 91 if (!isset($_SESSION['source_filepath'])) { 92 echo "data: Mizar file path not found in session\n\n"; 93 ob_flush(); 94 flush(); 95 return; 96 } 97 98 $filePath = $_SESSION['source_filepath']; 99 $this->streamSourceOutput($filePath); 100 101 echo "event: end\n"; 102 echo "data: Compilation complete\n\n"; 103 ob_flush(); 104 flush(); 105 } 106 107 // view用のコンパイルリクエスト処理 108 private function handleViewCompileRequest() 109 { 110 global $INPUT; 111 $content = $INPUT->post->str('content'); 112 113 $filePath = $this->createTempFile($content); 114 115 session_start(); 116 $_SESSION['view_filepath'] = $filePath; 117 118 $this->sendAjaxResponse(true, 'Mizar content processed successfully'); 119 } 120 121 // view用のSSEリクエスト処理 122 private function handleViewSSERequest() 123 { 124 header('Content-Type: text/event-stream'); 125 header('Cache-Control: no-cache'); 126 127 session_start(); 128 if (!isset($_SESSION['view_filepath'])) { 129 echo "data: Mizar file path not found in session\n\n"; 130 ob_flush(); 131 flush(); 132 return; 133 } 134 135 $filePath = $_SESSION['view_filepath']; 136 $this->streamViewCompileOutput($filePath); 137 138 echo "event: end\n"; 139 echo "data: Compilation complete\n\n"; 140 ob_flush(); 141 flush(); 142 } 143 144 // Mizarコンテンツの抽出 145 private function extractMizarContent($pageContent) 146 { 147 $pattern = '/<mizar\s+([^>]+)>(.*?)<\/mizar>/s'; 148 preg_match_all($pattern, $pageContent, $matches, PREG_SET_ORDER); 149 150 if (empty($matches)) { 151 return null; 152 } 153 154 $fileName = trim($matches[0][1]); 155 $combinedContent = ''; 156 157 foreach ($matches as $match) { 158 if (trim($match[1]) !== $fileName) { 159 return ['error' => 'File name mismatch in <mizar> tags']; 160 } 161 $combinedContent .= trim($match[2]) . "\n"; 162 } 163 164 return ['fileName' => $fileName, 'content' => $combinedContent]; 165 } 166 167 // Mizarコンテンツの保存 168 private function saveMizarContent($mizarData) 169 { 170 $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\'); 171 $filePath = $workPath . "/TEXT/" . $mizarData['fileName']; 172 file_put_contents($filePath, $mizarData['content']); 173 return $filePath; 174 } 175 176 // source用の出力をストリーム 177 private function streamSourceOutput($filePath) 178 { 179 $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\'); 180 chdir($workPath); 181 182 $command = "miz2prel " . escapeshellarg($filePath); 183 $process = proc_open($command, array(1 => array("pipe", "w")), $pipes); 184 185 if (is_resource($process)) { 186 while ($line = fgets($pipes[1])) { 187 echo "data: " . $line . "\n\n"; 188 ob_flush(); 189 flush(); 190 } 191 fclose($pipes[1]); 192 193 // エラー処理の追加 194 $errFilename = str_replace('.miz', '.err', $filePath); 195 if ($this->handleCompilationErrors($errFilename, rtrim($this->getConf('mizar_share_dir'), '/\\') . '/mizar.msg')) { 196 // エラーがあった場合は処理を終了 197 proc_close($process); 198 return; 199 } 200 201 proc_close($process); 202 } 203 } 204 205 // view用の一時ファイル作成 206 private function createTempFile($content) 207 { 208 $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\') . '/TEXT/'; 209 $uniqueName = str_replace('.', '_', uniqid('tmp', true)); 210 $tempFilename = $workPath . $uniqueName . ".miz"; 211 file_put_contents($tempFilename, $content); 212 return $tempFilename; 213 } 214 215 // Clear all temporary files in the TEXT folder 216 private function clearTempFiles() 217 { 218 $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\') . '/TEXT/'; 219 $files = glob($workPath . '*'); // TEXTフォルダ内のすべてのファイルを取得 220 221 $errors = []; 222 foreach ($files as $file) { 223 if (is_file($file)) { 224 // ファイルが使用中かどうか確認 225 if (!$this->is_file_locked($file)) { 226 $retries = 3; // 最大3回リトライ 227 while ($retries > 0) { 228 if (unlink($file)) { 229 break; // 削除成功 230 } 231 $errors[] = "Error deleting $file: " . error_get_last()['message']; 232 $retries--; 233 sleep(1); // 1秒待ってリトライ 234 } 235 if ($retries === 0) { 236 $errors[] = "Failed to delete: $file"; // 削除失敗 237 } 238 } else { 239 $errors[] = "File is locked: $file"; // ファイルがロックされている 240 } 241 } 242 } 243 244 if (empty($errors)) { 245 $this->sendAjaxResponse(true, 'Temporary files cleared successfully'); 246 } else { 247 $this->sendAjaxResponse(false, 'Some files could not be deleted', $errors); 248 } 249 } 250 251 // ファイルがロックされているかをチェックする関数 252 private function is_file_locked($file) 253 { 254 $fileHandle = @fopen($file, "r+"); 255 256 if ($fileHandle === false) { 257 return true; // ファイルが開けない、つまりロックされているかアクセス権がない 258 } 259 260 $locked = !flock($fileHandle, LOCK_EX | LOCK_NB); // ロックの取得を試みる(非ブロッキングモード) 261 262 fclose($fileHandle); 263 return $locked; // ロックが取得できなければファイルはロックされている 264 } 265 266 // View用コンパイル出力のストリーム 267 private function streamViewCompileOutput($filePath) 268 { 269 $workPath = $this->getConf('mizar_work_dir'); 270 $sharePath = rtrim($this->getConf('mizar_share_dir'), '/\\') . '/'; 271 272 chdir($workPath); 273 274 $errFilename = str_replace('.miz', '.err', $filePath); 275 $command = "makeenv " . escapeshellarg($filePath); 276 $process = proc_open($command, array(1 => array("pipe", "w"), 2 => array("pipe", "w")), $pipes); 277 278 if (is_resource($process)) { 279 while ($line = fgets($pipes[1])) { 280 echo "data: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 281 ob_flush(); 282 flush(); 283 } 284 fclose($pipes[1]); 285 286 while ($line = fgets($pipes[2])) { 287 echo "data: ERROR: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 288 ob_flush(); 289 flush(); 290 } 291 fclose($pipes[2]); 292 proc_close($process); 293 294 // エラー処理を別の関数に移動 295 if ($this->handleCompilationErrors($errFilename, $sharePath . '/mizar.msg')) { 296 return; 297 } 298 299 // verifierの実行 300 $exePath = rtrim($this->getConf('mizar_exe_dir'), '/\\') . '/'; 301 $verifierPath = escapeshellarg($exePath . "verifier"); 302 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 303 $verifierPath .= ".exe"; 304 } 305 $verifierCommand = $verifierPath . " -q -l " . escapeshellarg("TEXT/" . basename($filePath)); 306 307 $verifierProcess = proc_open($verifierCommand, array(1 => array("pipe", "w"), 2 => array("pipe", "w")), $verifierPipes); 308 309 if (is_resource($verifierProcess)) { 310 while ($line = fgets($verifierPipes[1])) { 311 echo "data: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 312 ob_flush(); 313 flush(); 314 } 315 fclose($verifierPipes[1]); 316 317 while ($line = fgets($verifierPipes[2])) { 318 echo "data: ERROR: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n"; 319 ob_flush(); 320 flush(); 321 } 322 fclose($verifierPipes[2]); 323 proc_close($verifierProcess); 324 } else { 325 echo "data: ERROR: Failed to execute verifier command.\n\n"; 326 ob_flush(); 327 flush(); 328 } 329 } else { 330 echo "data: ERROR: Failed to execute makeenv command.\n\n"; 331 ob_flush(); 332 flush(); 333 } 334 } 335 336 private function getMizarErrorMessages($mizarMsgFile) 337 { 338 $errorMessages = []; 339 $lines = file($mizarMsgFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 340 341 $isReadingErrorMsg = false; 342 $key = 0; 343 344 foreach ($lines as $line) { 345 if (preg_match('/# (\d+)/', $line, $matches)) { 346 $isReadingErrorMsg = true; 347 $key = intval($matches[1]); 348 } elseif ($isReadingErrorMsg) { 349 $errorMessages[$key] = $line; 350 $isReadingErrorMsg = false; 351 } 352 } 353 354 return $errorMessages; 355 } 356 357 private function sendAjaxResponse($success, $message, $data = '') 358 { 359 header('Content-Type: application/json'); 360 echo json_encode(['success' => $success, 'message' => $message, 'data' => $data]); 361 exit; 362 } 363 364 private function handleCompilationErrors($errFilename, $mizarMsgFilePath) 365 { 366 if (file_exists($errFilename)) { 367 $errors = []; 368 $errorLines = file($errFilename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 369 if (empty($errorLines)) { 370 } 371 foreach ($errorLines as $errorLine) { 372 if (preg_match('/(\d+)\s+(\d+)\s+(\d+)/', $errorLine, $matches)) { 373 $errorCode = intval($matches[3]); 374 $errors[] = [ 375 'code' => $errorCode, 376 'line' => intval($matches[1]), 377 'column' => intval($matches[2]), 378 'message' => $this->getMizarErrorMessages($mizarMsgFilePath)[$errorCode] ?? 'Unknown error' 379 ]; 380 } 381 } 382 if (!empty($errors)) { 383 echo "event: compileErrors\n"; 384 echo "data: " . json_encode($errors) . "\n\n"; 385 ob_flush(); 386 flush(); 387 return true; 388 } 389 } 390 return false; 391 } 392 393 private function handle_create_combined_file() 394 { 395 global $INPUT; 396 397 // 投稿されたコンテンツを取得 398 $combinedContent = $INPUT->post->str('content'); 399 $filename = $INPUT->post->str('filename', 'combined_file.miz'); // ファイル名を取得、デフォルト名も設定 400 401 // ファイルパスを指定 402 $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\') . '/TEXT/'; 403 $filePath = $workPath . $filename; 404 405 // ファイルを書き込み 406 if (file_put_contents($filePath, $combinedContent) !== false) { 407 // コピー先ディレクトリのパスを指定 408 $copyDir = DOKU_INC . 'lib/plugins/mizarproofchecker/TEXT/'; 409 410 // コピー先ディレクトリが存在しない場合は作成する 411 if (!is_dir($copyDir)) { 412 mkdir($copyDir, 0777, true); 413 } 414 415 // ファイルをコピー 416 if (copy($filePath, $copyDir . basename($filePath))) { 417 $fileUrl = DOKU_BASE . 'lib/plugins/mizarproofchecker/TEXT/' . urlencode($filename); 418 $this->sendAjaxResponse(true, 'File created successfully', ['fileUrl' => $fileUrl]); 419 error_log("Generated file URL: " . $fileUrl); 420 } else { 421 $this->sendAjaxResponse(false, 'Failed to copy file'); 422 } 423 } else { 424 $this->sendAjaxResponse(false, 'Failed to create file'); 425 } 426 } 427}