xref: /plugin/mizarverifiabledocs/action.php (revision 4754b0a76a6bc23f6462b21189b836e85a2d37b9)
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
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}