xref: /plugin/mizarverifiabledocs/action.php (revision 47595f9cd8561fbcdf3e998c36a8f1c1be7116a8)
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