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