xref: /plugin/mizarverifiabledocs/action.php (revision 3f7dd076be52992d1f40215ecd797f9d17132992)
16b7e227cSYamadaMiz<?php
247595f9cSYamadaMiz
347595f9cSYamadaMizuse dokuwiki\Extension\ActionPlugin;
447595f9cSYamadaMizuse dokuwiki\Extension\EventHandler;
547595f9cSYamadaMizuse dokuwiki\Extension\Event;
647595f9cSYamadaMiz
7f9af2148SYamadaMiz/**
8b68e3724SYamadaMiz * DokuWiki Plugin Mizar Verifiable Docs (Action Component)
9f9af2148SYamadaMiz *
10f9af2148SYamadaMiz * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
114f65af2cSYamadaMiz * @author  Yamada, M.
12f9af2148SYamadaMiz */
13f9af2148SYamadaMiz
14b68e3724SYamadaMizclass action_plugin_mizarverifiabledocs extends ActionPlugin
1547595f9cSYamadaMiz{
1647595f9cSYamadaMiz    /**
174754b0a7SYamadaMiz     * Registers a callback function for a given event
1847595f9cSYamadaMiz     *
1947595f9cSYamadaMiz     * @param EventHandler $controller DokuWiki's event controller object
2047595f9cSYamadaMiz     * @return void
2147595f9cSYamadaMiz     */
2247595f9cSYamadaMiz    public function register(EventHandler $controller)
2347595f9cSYamadaMiz    {
246b7e227cSYamadaMiz        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call');
254f65af2cSYamadaMiz        $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'handle_tpl_content_display');
264f65af2cSYamadaMiz    }
274f65af2cSYamadaMiz
284f65af2cSYamadaMiz    /**
294f65af2cSYamadaMiz     * Handles the TPL_CONTENT_DISPLAY event to insert "Hide All" button
304f65af2cSYamadaMiz     *
314f65af2cSYamadaMiz     * @param Event $event
324f65af2cSYamadaMiz     * @param mixed $_param Unused parameter
334f65af2cSYamadaMiz     * @return void
344f65af2cSYamadaMiz     */
354f65af2cSYamadaMiz    public function handle_tpl_content_display(Event $event, $_param)
364f65af2cSYamadaMiz    {
374f65af2cSYamadaMiz        // データが文字列かどうか確認
384f65af2cSYamadaMiz        if (!is_string($event->data)) {
394f65af2cSYamadaMiz            error_log('handle_tpl_content_display: data is not a string! ' . print_r($event->data, true));
404f65af2cSYamadaMiz            return;
414f65af2cSYamadaMiz        }
424f65af2cSYamadaMiz
434f65af2cSYamadaMiz        $html = $event->data;
444f65af2cSYamadaMiz
454f65af2cSYamadaMiz        // "mizarWrapper" クラスが存在するか確認
464f65af2cSYamadaMiz        if (strpos($html, 'mizarWrapper') !== false) {
474f65af2cSYamadaMiz            // 既にボタンが挿入されているか確認(複数回挿入しないため)
484f65af2cSYamadaMiz            if (strpos($html, 'id="hideAllButton"') === false) {
494f65af2cSYamadaMiz                $buttonHtml = '<div class="hideAllContainer">'
504f65af2cSYamadaMiz                            . '<button id="hideAllButton" class="hide-all-button">Hide All</button>'
514f65af2cSYamadaMiz                            . '<button id="showAllButton" class="hide-all-button" style="display:none;">Show All</button>'
52fcd231cdSYamadaMiz                            . '<button id="resetAllButton" class="reset-all-button">Reset All</button>'    // ★ 追加
534f65af2cSYamadaMiz                            . '</div>';
544f65af2cSYamadaMiz
554f65af2cSYamadaMiz                // 先頭にボタンを挿入
564f65af2cSYamadaMiz                $html = $buttonHtml . $html;
574f65af2cSYamadaMiz                $event->data = $html;
584f65af2cSYamadaMiz            }
594f65af2cSYamadaMiz        }
606b7e227cSYamadaMiz    }
616b7e227cSYamadaMiz
6247595f9cSYamadaMiz    /**
6347595f9cSYamadaMiz     * Handles AJAX requests
6447595f9cSYamadaMiz     *
6547595f9cSYamadaMiz     * @param Event $event
6647595f9cSYamadaMiz     * @param $param
6747595f9cSYamadaMiz     */
6847595f9cSYamadaMiz    public function handle_ajax_call(Event $event, $param)
6947595f9cSYamadaMiz    {
706b7e227cSYamadaMiz        unset($param); // 未使用のパラメータを無効化
7147595f9cSYamadaMiz
7247595f9cSYamadaMiz        switch ($event->data) {
7347595f9cSYamadaMiz            case 'clear_temp_files':
7447595f9cSYamadaMiz                $event->preventDefault();
7547595f9cSYamadaMiz                $event->stopPropagation();
7642c7ffb2SYamadaMiz                $this->clearTempFiles();
7747595f9cSYamadaMiz                break;
7847595f9cSYamadaMiz            case 'source_sse':
796b7e227cSYamadaMiz                $event->preventDefault();
806b7e227cSYamadaMiz                $event->stopPropagation();
816b7e227cSYamadaMiz                $this->handleSourceSSERequest();
8247595f9cSYamadaMiz                break;
8347595f9cSYamadaMiz            case 'source_compile':
846b7e227cSYamadaMiz                $event->preventDefault();
856b7e227cSYamadaMiz                $event->stopPropagation();
866b7e227cSYamadaMiz                $this->handleSourceCompileRequest();
8747595f9cSYamadaMiz                break;
8847595f9cSYamadaMiz            case 'view_compile':
896b7e227cSYamadaMiz                $event->preventDefault();
906b7e227cSYamadaMiz                $event->stopPropagation();
916b7e227cSYamadaMiz                $this->handleViewCompileRequest();
9247595f9cSYamadaMiz                break;
9347595f9cSYamadaMiz            case 'view_sse':
946b7e227cSYamadaMiz                $event->preventDefault();
956b7e227cSYamadaMiz                $event->stopPropagation();
966b7e227cSYamadaMiz                $this->handleViewSSERequest();
9747595f9cSYamadaMiz                break;
989fc5dc4bSYamadaMiz            case 'create_combined_file':
999fc5dc4bSYamadaMiz                $event->preventDefault();
1009fc5dc4bSYamadaMiz                $event->stopPropagation();
1019fc5dc4bSYamadaMiz                $this->handle_create_combined_file();
1029fc5dc4bSYamadaMiz                break;
103*3f7dd076SYamadaMiz            case 'view_graph':
104*3f7dd076SYamadaMiz                $event->preventDefault();
105*3f7dd076SYamadaMiz                $event->stopPropagation();
106*3f7dd076SYamadaMiz                $this->handleViewGraphRequest();
107*3f7dd076SYamadaMiz            break;
1086b7e227cSYamadaMiz        }
1096b7e227cSYamadaMiz    }
1106b7e227cSYamadaMiz
1116b7e227cSYamadaMiz    // source用のコンパイルリクエスト処理
11247595f9cSYamadaMiz    private function handleSourceCompileRequest()
11347595f9cSYamadaMiz    {
1146b7e227cSYamadaMiz        global $INPUT;
1156b7e227cSYamadaMiz        $pageContent = $INPUT->post->str('content');
1166b7e227cSYamadaMiz        $mizarData = $this->extractMizarContent($pageContent);
1176b7e227cSYamadaMiz
11842c7ffb2SYamadaMiz        // エラーチェックを追加
1196b7e227cSYamadaMiz        if ($mizarData === null) {
1206b7e227cSYamadaMiz            $this->sendAjaxResponse(false, 'Mizar content not found');
1216b7e227cSYamadaMiz            return;
12242c7ffb2SYamadaMiz        } elseif (isset($mizarData['error'])) {
12342c7ffb2SYamadaMiz            $this->sendAjaxResponse(false, $mizarData['error']);
12442c7ffb2SYamadaMiz            return;
1256b7e227cSYamadaMiz        }
1266b7e227cSYamadaMiz
1276b7e227cSYamadaMiz        $filePath = $this->saveMizarContent($mizarData);
1286b7e227cSYamadaMiz
1296b7e227cSYamadaMiz        session_start();
1306b7e227cSYamadaMiz        $_SESSION['source_filepath'] = $filePath;
1316b7e227cSYamadaMiz
1326b7e227cSYamadaMiz        $this->sendAjaxResponse(true, 'Mizar content processed successfully');
1336b7e227cSYamadaMiz    }
1346b7e227cSYamadaMiz
1356b7e227cSYamadaMiz    // source用のSSEリクエスト処理
13647595f9cSYamadaMiz    private function handleSourceSSERequest()
13747595f9cSYamadaMiz    {
1386b7e227cSYamadaMiz        header('Content-Type: text/event-stream');
13914f6cf5bSYamadaMiz        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
14014f6cf5bSYamadaMiz        header('Pragma: no-cache');
14114f6cf5bSYamadaMiz        header('Expires: 0');
1426b7e227cSYamadaMiz
1436b7e227cSYamadaMiz        session_start();
1446b7e227cSYamadaMiz        if (!isset($_SESSION['source_filepath'])) {
1456b7e227cSYamadaMiz            echo "data: Mizar file path not found in session\n\n";
1466b7e227cSYamadaMiz            ob_flush();
1476b7e227cSYamadaMiz            flush();
1486b7e227cSYamadaMiz            return;
1496b7e227cSYamadaMiz        }
1506b7e227cSYamadaMiz
1516b7e227cSYamadaMiz        $filePath = $_SESSION['source_filepath'];
1526b7e227cSYamadaMiz        $this->streamSourceOutput($filePath);
1536b7e227cSYamadaMiz
1546b7e227cSYamadaMiz        echo "event: end\n";
1556b7e227cSYamadaMiz        echo "data: Compilation complete\n\n";
1566b7e227cSYamadaMiz        ob_flush();
1576b7e227cSYamadaMiz        flush();
1586b7e227cSYamadaMiz    }
1596b7e227cSYamadaMiz
16047595f9cSYamadaMiz    // view用のコンパイルリクエスト処理
16147595f9cSYamadaMiz    private function handleViewCompileRequest()
16247595f9cSYamadaMiz    {
16347595f9cSYamadaMiz        global $INPUT;
16447595f9cSYamadaMiz        $content = $INPUT->post->str('content');
16547595f9cSYamadaMiz
16647595f9cSYamadaMiz        $filePath = $this->createTempFile($content);
16747595f9cSYamadaMiz
16847595f9cSYamadaMiz        session_start();
16947595f9cSYamadaMiz        $_SESSION['view_filepath'] = $filePath;
17047595f9cSYamadaMiz
17147595f9cSYamadaMiz        $this->sendAjaxResponse(true, 'Mizar content processed successfully');
17247595f9cSYamadaMiz    }
17347595f9cSYamadaMiz
17447595f9cSYamadaMiz    // view用のSSEリクエスト処理
17547595f9cSYamadaMiz    private function handleViewSSERequest()
17647595f9cSYamadaMiz    {
17747595f9cSYamadaMiz        header('Content-Type: text/event-stream');
17814f6cf5bSYamadaMiz        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
17914f6cf5bSYamadaMiz        header('Pragma: no-cache');
18014f6cf5bSYamadaMiz        header('Expires: 0');
18147595f9cSYamadaMiz
18247595f9cSYamadaMiz        session_start();
18347595f9cSYamadaMiz        if (!isset($_SESSION['view_filepath'])) {
18447595f9cSYamadaMiz            echo "data: Mizar file path not found in session\n\n";
18547595f9cSYamadaMiz            ob_flush();
18647595f9cSYamadaMiz            flush();
18747595f9cSYamadaMiz            return;
18847595f9cSYamadaMiz        }
18947595f9cSYamadaMiz
19047595f9cSYamadaMiz        $filePath = $_SESSION['view_filepath'];
1919fc5dc4bSYamadaMiz        $this->streamViewCompileOutput($filePath);
19247595f9cSYamadaMiz
19347595f9cSYamadaMiz        echo "event: end\n";
19447595f9cSYamadaMiz        echo "data: Compilation complete\n\n";
19547595f9cSYamadaMiz        ob_flush();
19647595f9cSYamadaMiz        flush();
19747595f9cSYamadaMiz    }
19847595f9cSYamadaMiz
199*3f7dd076SYamadaMiz    /*****  view_graph: 上から該当ブロックまでを結合し SVG を返す *****/
200*3f7dd076SYamadaMiz    private function handleViewGraphRequest()
201*3f7dd076SYamadaMiz    {
202*3f7dd076SYamadaMiz        global $INPUT;
203*3f7dd076SYamadaMiz        $content = $INPUT->post->str('content', '');
204*3f7dd076SYamadaMiz
205*3f7dd076SYamadaMiz        // 空チェック
206*3f7dd076SYamadaMiz        if ($content === '') {
207*3f7dd076SYamadaMiz            $this->sendAjaxResponse(false, 'Empty content');
208*3f7dd076SYamadaMiz            return;
209*3f7dd076SYamadaMiz        }
210*3f7dd076SYamadaMiz
211*3f7dd076SYamadaMiz        // 一時 .miz ファイルを作成
212*3f7dd076SYamadaMiz        $tmp = tempnam(sys_get_temp_dir(), 'miz');
213*3f7dd076SYamadaMiz        $miz = $tmp . '.miz';
214*3f7dd076SYamadaMiz        rename($tmp, $miz);                // tempnam が拡張子無しなのでリネーム
215*3f7dd076SYamadaMiz        file_put_contents($miz, $content);
216*3f7dd076SYamadaMiz
217*3f7dd076SYamadaMiz        // Python 可視化スクリプト呼び出し
218*3f7dd076SYamadaMiz        $parser = escapeshellarg(__DIR__ . '/script/miz2svg.py');
219*3f7dd076SYamadaMiz        $py     = escapeshellcmd($this->getConf('py_cmd') ?: 'python');
220*3f7dd076SYamadaMiz        $svg    = shell_exec("$py $parser ".escapeshellarg($miz));
221*3f7dd076SYamadaMiz        unlink($miz);                       // 後片付け
222*3f7dd076SYamadaMiz
223*3f7dd076SYamadaMiz        // 結果返却
224*3f7dd076SYamadaMiz        if ($svg) {
225*3f7dd076SYamadaMiz            $this->sendAjaxResponse(true, 'success', ['svg' => $svg]);
226*3f7dd076SYamadaMiz        } else {
227*3f7dd076SYamadaMiz            $this->sendAjaxResponse(false, 'conversion failed');
228*3f7dd076SYamadaMiz        }
229*3f7dd076SYamadaMiz    }
230*3f7dd076SYamadaMiz
23147595f9cSYamadaMiz    // Mizarコンテンツの抽出
23247595f9cSYamadaMiz    private function extractMizarContent($pageContent)
23347595f9cSYamadaMiz    {
2346b7e227cSYamadaMiz        $pattern = '/<mizar\s+([^>]+)>(.*?)<\/mizar>/s';
2356b7e227cSYamadaMiz        preg_match_all($pattern, $pageContent, $matches, PREG_SET_ORDER);
2366b7e227cSYamadaMiz
2376b7e227cSYamadaMiz        if (empty($matches)) {
23847595f9cSYamadaMiz            return null;
2396b7e227cSYamadaMiz        }
2406b7e227cSYamadaMiz
24142c7ffb2SYamadaMiz        // 最初のファイル名を取得し、拡張子を除去
24247595f9cSYamadaMiz        $fileName = trim($matches[0][1]);
24342c7ffb2SYamadaMiz        $fileNameWithoutExt = preg_replace('/\.miz$/i', '', $fileName);
24442c7ffb2SYamadaMiz
24542c7ffb2SYamadaMiz        // ファイル名のバリデーションを追加
24642c7ffb2SYamadaMiz        if (!$this->isValidFileName($fileNameWithoutExt)) {
24742c7ffb2SYamadaMiz            return ['error' => "Invalid characters in file name: '{$fileNameWithoutExt}'. Only letters, numbers, underscores (_), and apostrophes (') are allowed, up to 8 characters."];
24842c7ffb2SYamadaMiz        }
24942c7ffb2SYamadaMiz
2506b7e227cSYamadaMiz        $combinedContent = '';
2516b7e227cSYamadaMiz
2526b7e227cSYamadaMiz        foreach ($matches as $match) {
25342c7ffb2SYamadaMiz            $currentFileName = trim($match[1]);
25442c7ffb2SYamadaMiz            $currentFileNameWithoutExt = preg_replace('/\.miz$/i', '', $currentFileName);
25542c7ffb2SYamadaMiz
25642c7ffb2SYamadaMiz            if ($currentFileNameWithoutExt !== $fileNameWithoutExt) {
25742c7ffb2SYamadaMiz                return ['error' => "File name mismatch in <mizar> tags: '{$fileNameWithoutExt}' and '{$currentFileNameWithoutExt}'"];
2586b7e227cSYamadaMiz            }
25942c7ffb2SYamadaMiz
26042c7ffb2SYamadaMiz            // バリデーションを各ファイル名にも適用
26142c7ffb2SYamadaMiz            if (!$this->isValidFileName($currentFileNameWithoutExt)) {
26242c7ffb2SYamadaMiz                return ['error' => "Invalid characters in file name: '{$currentFileNameWithoutExt}'. Only letters, numbers, underscores (_), and apostrophes (') are allowed, up to 8 characters."];
26342c7ffb2SYamadaMiz            }
26442c7ffb2SYamadaMiz
2656b7e227cSYamadaMiz            $combinedContent .= trim($match[2]) . "\n";
2666b7e227cSYamadaMiz        }
2676b7e227cSYamadaMiz
26842c7ffb2SYamadaMiz        // ファイル名に拡張子を付加
26942c7ffb2SYamadaMiz        $fullFileName = $fileNameWithoutExt . '.miz';
27042c7ffb2SYamadaMiz
27142c7ffb2SYamadaMiz        return ['fileName' => $fullFileName, 'content' => $combinedContent];
27242c7ffb2SYamadaMiz    }
27342c7ffb2SYamadaMiz
27442c7ffb2SYamadaMiz    // ファイル名のバリデーション関数を追加
27542c7ffb2SYamadaMiz    private function isValidFileName($fileName)
27642c7ffb2SYamadaMiz    {
27742c7ffb2SYamadaMiz        // ファイル名の長さをチェック(最大8文字)
27842c7ffb2SYamadaMiz        if (strlen($fileName) > 8) {
27942c7ffb2SYamadaMiz            return false;
28042c7ffb2SYamadaMiz        }
28142c7ffb2SYamadaMiz
28242c7ffb2SYamadaMiz        // 許可される文字のみを含むかチェック
28342c7ffb2SYamadaMiz        if (!preg_match('/^[A-Za-z0-9_\']+$/', $fileName)) {
28442c7ffb2SYamadaMiz            return false;
28542c7ffb2SYamadaMiz        }
28642c7ffb2SYamadaMiz
28742c7ffb2SYamadaMiz        return true;
2886b7e227cSYamadaMiz    }
2896b7e227cSYamadaMiz
29047595f9cSYamadaMiz    // Mizarコンテンツの保存
29147595f9cSYamadaMiz    private function saveMizarContent($mizarData)
29247595f9cSYamadaMiz    {
2936b7e227cSYamadaMiz        $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\');
2946b7e227cSYamadaMiz        $filePath = $workPath . "/TEXT/" . $mizarData['fileName'];
2956b7e227cSYamadaMiz        file_put_contents($filePath, $mizarData['content']);
2966b7e227cSYamadaMiz        return $filePath;
2976b7e227cSYamadaMiz    }
2986b7e227cSYamadaMiz
29947595f9cSYamadaMiz    // source用の出力をストリーム
30047595f9cSYamadaMiz    private function streamSourceOutput($filePath)
30147595f9cSYamadaMiz    {
3026b7e227cSYamadaMiz        $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\');
303a16ae7e3SYamadaMiz        $sharePath = rtrim($this->getConf('mizar_share_dir'), '/\\');
304a16ae7e3SYamadaMiz
305a16ae7e3SYamadaMiz        // ★追加:環境変数 MIZFILESをPHP内で設定
306a16ae7e3SYamadaMiz        putenv("MIZFILES=$sharePath");
307a16ae7e3SYamadaMiz
3086b7e227cSYamadaMiz        chdir($workPath);
3096b7e227cSYamadaMiz
3106b7e227cSYamadaMiz        $command = "miz2prel " . escapeshellarg($filePath);
3116b7e227cSYamadaMiz        $process = proc_open($command, array(1 => array("pipe", "w")), $pipes);
3126b7e227cSYamadaMiz
3136b7e227cSYamadaMiz        if (is_resource($process)) {
3146b7e227cSYamadaMiz            while ($line = fgets($pipes[1])) {
3156b7e227cSYamadaMiz                echo "data: " . $line . "\n\n";
3166b7e227cSYamadaMiz                ob_flush();
3176b7e227cSYamadaMiz                flush();
3186b7e227cSYamadaMiz            }
3196b7e227cSYamadaMiz            fclose($pipes[1]);
3209fc5dc4bSYamadaMiz
3219fc5dc4bSYamadaMiz            // エラー処理の追加
3229fc5dc4bSYamadaMiz            $errFilename = str_replace('.miz', '.err', $filePath);
3239fc5dc4bSYamadaMiz            if ($this->handleCompilationErrors($errFilename, rtrim($this->getConf('mizar_share_dir'), '/\\') . '/mizar.msg')) {
3249fc5dc4bSYamadaMiz                // エラーがあった場合は処理を終了
3259fc5dc4bSYamadaMiz                proc_close($process);
3269fc5dc4bSYamadaMiz                return;
3279fc5dc4bSYamadaMiz            }
3289fc5dc4bSYamadaMiz
3296b7e227cSYamadaMiz            proc_close($process);
3306b7e227cSYamadaMiz        }
3316b7e227cSYamadaMiz    }
3326b7e227cSYamadaMiz
33347595f9cSYamadaMiz    // view用の一時ファイル作成
33447595f9cSYamadaMiz    private function createTempFile($content)
33547595f9cSYamadaMiz    {
3366b7e227cSYamadaMiz        $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\') . '/TEXT/';
33747595f9cSYamadaMiz        $uniqueName = str_replace('.', '_', uniqid('tmp', true));
3386b7e227cSYamadaMiz        $tempFilename = $workPath . $uniqueName . ".miz";
3396b7e227cSYamadaMiz        file_put_contents($tempFilename, $content);
3406b7e227cSYamadaMiz        return $tempFilename;
3416b7e227cSYamadaMiz    }
3426b7e227cSYamadaMiz
34314f6cf5bSYamadaMiz    // 一時ファイルの削除 (clearTempFiles関数の修正版)
34447595f9cSYamadaMiz    private function clearTempFiles()
34547595f9cSYamadaMiz    {
34647595f9cSYamadaMiz        $workPath = rtrim($this->getConf('mizar_work_dir'), '/\\') . '/TEXT/';
34747595f9cSYamadaMiz        $files = glob($workPath . '*');  // TEXTフォルダ内のすべてのファイルを取得
34847595f9cSYamadaMiz
34947595f9cSYamadaMiz        $errors = [];
35047595f9cSYamadaMiz        foreach ($files as $file) {
35147595f9cSYamadaMiz            if (is_file($file)) {
35247595f9cSYamadaMiz                // ファイルが使用中かどうか確認
35347595f9cSYamadaMiz                if (!$this->is_file_locked($file)) {
35414f6cf5bSYamadaMiz                    $retries = 5; // リトライを増やす
35547595f9cSYamadaMiz                    while ($retries > 0) {
35614f6cf5bSYamadaMiz                        if (@unlink($file)) {  // ← エラー抑制を追加
35747595f9cSYamadaMiz                            break; // 削除成功
35847595f9cSYamadaMiz                        }
3595cbf3a53SYamadaMiz                        $errors[] = "Error deleting $file: " . error_get_last()['message'];
36047595f9cSYamadaMiz                        $retries--;
36114f6cf5bSYamadaMiz                        sleep(2); // 2秒待ってリトライ
36247595f9cSYamadaMiz                    }
36347595f9cSYamadaMiz                    if ($retries === 0) {
36447595f9cSYamadaMiz                        $errors[] = "Failed to delete: $file";  // 削除失敗
36547595f9cSYamadaMiz                    }
36647595f9cSYamadaMiz                } else {
36747595f9cSYamadaMiz                    $errors[] = "File is locked: $file";  // ファイルがロックされている
36847595f9cSYamadaMiz                }
36947595f9cSYamadaMiz            }
37047595f9cSYamadaMiz        }
37147595f9cSYamadaMiz
37247595f9cSYamadaMiz        if (empty($errors)) {
37347595f9cSYamadaMiz            $this->sendAjaxResponse(true, 'Temporary files cleared successfully');
37447595f9cSYamadaMiz        } else {
37547595f9cSYamadaMiz            $this->sendAjaxResponse(false, 'Some files could not be deleted', $errors);
37647595f9cSYamadaMiz        }
37747595f9cSYamadaMiz    }
37847595f9cSYamadaMiz
37914f6cf5bSYamadaMiz
38047595f9cSYamadaMiz    // ファイルがロックされているかをチェックする関数
38147595f9cSYamadaMiz    private function is_file_locked($file)
38247595f9cSYamadaMiz    {
38347595f9cSYamadaMiz        $fileHandle = @fopen($file, "r+");
38447595f9cSYamadaMiz
38547595f9cSYamadaMiz        if ($fileHandle === false) {
38647595f9cSYamadaMiz            return true; // ファイルが開けない、つまりロックされているかアクセス権がない
38747595f9cSYamadaMiz        }
38847595f9cSYamadaMiz
38947595f9cSYamadaMiz        $locked = !flock($fileHandle, LOCK_EX | LOCK_NB); // ロックの取得を試みる(非ブロッキングモード)
39047595f9cSYamadaMiz
39147595f9cSYamadaMiz        fclose($fileHandle);
39247595f9cSYamadaMiz        return $locked; // ロックが取得できなければファイルはロックされている
39347595f9cSYamadaMiz    }
39447595f9cSYamadaMiz
3959fc5dc4bSYamadaMiz    // View用コンパイル出力のストリーム
3969fc5dc4bSYamadaMiz    private function streamViewCompileOutput($filePath)
39747595f9cSYamadaMiz    {
3986b7e227cSYamadaMiz        $workPath = $this->getConf('mizar_work_dir');
3996b7e227cSYamadaMiz        $sharePath = rtrim($this->getConf('mizar_share_dir'), '/\\') . '/';
4006b7e227cSYamadaMiz
401a16ae7e3SYamadaMiz        // ★追加:環境変数 MIZFILESをPHP内で設定
402a16ae7e3SYamadaMiz        putenv("MIZFILES=$sharePath");
403a16ae7e3SYamadaMiz
4046b7e227cSYamadaMiz        chdir($workPath);
4056b7e227cSYamadaMiz
4069fc5dc4bSYamadaMiz        $errFilename = str_replace('.miz', '.err', $filePath);
4076b7e227cSYamadaMiz        $command = "makeenv " . escapeshellarg($filePath);
4086b7e227cSYamadaMiz        $process = proc_open($command, array(1 => array("pipe", "w"), 2 => array("pipe", "w")), $pipes);
4096b7e227cSYamadaMiz
4106b7e227cSYamadaMiz        if (is_resource($process)) {
4116b7e227cSYamadaMiz            while ($line = fgets($pipes[1])) {
4126b7e227cSYamadaMiz                echo "data: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n";
4136b7e227cSYamadaMiz                ob_flush();
4146b7e227cSYamadaMiz                flush();
4156b7e227cSYamadaMiz            }
4166b7e227cSYamadaMiz            fclose($pipes[1]);
4176b7e227cSYamadaMiz
4186b7e227cSYamadaMiz            while ($line = fgets($pipes[2])) {
4196b7e227cSYamadaMiz                echo "data: ERROR: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n";
4206b7e227cSYamadaMiz                ob_flush();
4216b7e227cSYamadaMiz                flush();
4226b7e227cSYamadaMiz            }
4236b7e227cSYamadaMiz            fclose($pipes[2]);
4246b7e227cSYamadaMiz            proc_close($process);
4256b7e227cSYamadaMiz
4261174571dSYamadaMiz            // makeenvのエラー処理
4279fc5dc4bSYamadaMiz            if ($this->handleCompilationErrors($errFilename, $sharePath . '/mizar.msg')) {
4286b7e227cSYamadaMiz                return;
4296b7e227cSYamadaMiz            }
4306b7e227cSYamadaMiz
4316b7e227cSYamadaMiz            // verifierの実行
4326b7e227cSYamadaMiz            $exePath = rtrim($this->getConf('mizar_exe_dir'), '/\\') . '/';
4336b7e227cSYamadaMiz            $verifierPath = escapeshellarg($exePath . "verifier");
4346b7e227cSYamadaMiz            if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
4356b7e227cSYamadaMiz                $verifierPath .= ".exe";
4366b7e227cSYamadaMiz            }
4376b7e227cSYamadaMiz            $verifierCommand = $verifierPath . " -q -l " . escapeshellarg("TEXT/" . basename($filePath));
4386b7e227cSYamadaMiz
4396b7e227cSYamadaMiz            $verifierProcess = proc_open($verifierCommand, array(1 => array("pipe", "w"), 2 => array("pipe", "w")), $verifierPipes);
4406b7e227cSYamadaMiz
4416b7e227cSYamadaMiz            if (is_resource($verifierProcess)) {
4426b7e227cSYamadaMiz                while ($line = fgets($verifierPipes[1])) {
4436b7e227cSYamadaMiz                    echo "data: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n";
4446b7e227cSYamadaMiz                    ob_flush();
4456b7e227cSYamadaMiz                    flush();
4466b7e227cSYamadaMiz                }
4476b7e227cSYamadaMiz                fclose($verifierPipes[1]);
4486b7e227cSYamadaMiz
4496b7e227cSYamadaMiz                while ($line = fgets($verifierPipes[2])) {
4506b7e227cSYamadaMiz                    echo "data: ERROR: " . mb_convert_encoding($line, 'UTF-8', 'SJIS') . "\n\n";
4516b7e227cSYamadaMiz                    ob_flush();
4526b7e227cSYamadaMiz                    flush();
4536b7e227cSYamadaMiz                }
4546b7e227cSYamadaMiz                fclose($verifierPipes[2]);
4556b7e227cSYamadaMiz                proc_close($verifierProcess);
4561174571dSYamadaMiz
4571174571dSYamadaMiz                // verifierのエラー処理
4581174571dSYamadaMiz                if ($this->handleCompilationErrors($errFilename, $sharePath . '/mizar.msg')) {
4591174571dSYamadaMiz                    return;
4601174571dSYamadaMiz                }
4616b7e227cSYamadaMiz            } else {
4626b7e227cSYamadaMiz                echo "data: ERROR: Failed to execute verifier command.\n\n";
4636b7e227cSYamadaMiz                ob_flush();
4646b7e227cSYamadaMiz                flush();
4656b7e227cSYamadaMiz            }
4666b7e227cSYamadaMiz        } else {
4676b7e227cSYamadaMiz            echo "data: ERROR: Failed to execute makeenv command.\n\n";
4686b7e227cSYamadaMiz            ob_flush();
4696b7e227cSYamadaMiz            flush();
4706b7e227cSYamadaMiz        }
4716b7e227cSYamadaMiz    }
4726b7e227cSYamadaMiz
47347595f9cSYamadaMiz    private function getMizarErrorMessages($mizarMsgFile)
47447595f9cSYamadaMiz    {
4756b7e227cSYamadaMiz        $errorMessages = [];
4766b7e227cSYamadaMiz        $lines = file($mizarMsgFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
4776b7e227cSYamadaMiz
4786b7e227cSYamadaMiz        $isReadingErrorMsg = false;
4796b7e227cSYamadaMiz        $key = 0;
4806b7e227cSYamadaMiz
4816b7e227cSYamadaMiz        foreach ($lines as $line) {
4826b7e227cSYamadaMiz            if (preg_match('/# (\d+)/', $line, $matches)) {
4836b7e227cSYamadaMiz                $isReadingErrorMsg = true;
4846b7e227cSYamadaMiz                $key = intval($matches[1]);
4856b7e227cSYamadaMiz            } elseif ($isReadingErrorMsg) {
4866b7e227cSYamadaMiz                $errorMessages[$key] = $line;
4876b7e227cSYamadaMiz                $isReadingErrorMsg = false;
4886b7e227cSYamadaMiz            }
4896b7e227cSYamadaMiz        }
4906b7e227cSYamadaMiz
4916b7e227cSYamadaMiz        return $errorMessages;
4926b7e227cSYamadaMiz    }
4936b7e227cSYamadaMiz
49447595f9cSYamadaMiz    private function sendAjaxResponse($success, $message, $data = '')
49547595f9cSYamadaMiz    {
4966b7e227cSYamadaMiz        header('Content-Type: application/json');
49714f6cf5bSYamadaMiz        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
49814f6cf5bSYamadaMiz        header('Pragma: no-cache');
49914f6cf5bSYamadaMiz        header('Expires: 0');
5006b7e227cSYamadaMiz        echo json_encode(['success' => $success, 'message' => $message, 'data' => $data]);
5016b7e227cSYamadaMiz        exit;
5026b7e227cSYamadaMiz    }
5039fc5dc4bSYamadaMiz
5049fc5dc4bSYamadaMiz    private function handleCompilationErrors($errFilename, $mizarMsgFilePath)
5059fc5dc4bSYamadaMiz    {
5069fc5dc4bSYamadaMiz        if (file_exists($errFilename)) {
5079fc5dc4bSYamadaMiz            $errors = [];
5089fc5dc4bSYamadaMiz            $errorLines = file($errFilename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
5099fc5dc4bSYamadaMiz            foreach ($errorLines as $errorLine) {
5109fc5dc4bSYamadaMiz                if (preg_match('/(\d+)\s+(\d+)\s+(\d+)/', $errorLine, $matches)) {
5119fc5dc4bSYamadaMiz                    $errorCode = intval($matches[3]);
5129fc5dc4bSYamadaMiz                    $errors[] = [
5139fc5dc4bSYamadaMiz                        'code' => $errorCode,
5149fc5dc4bSYamadaMiz                        'line' => intval($matches[1]),
5159fc5dc4bSYamadaMiz                        'column' => intval($matches[2]),
5169fc5dc4bSYamadaMiz                        'message' => $this->getMizarErrorMessages($mizarMsgFilePath)[$errorCode] ?? 'Unknown error'
5179fc5dc4bSYamadaMiz                    ];
5189fc5dc4bSYamadaMiz                }
5199fc5dc4bSYamadaMiz            }
5209fc5dc4bSYamadaMiz            if (!empty($errors)) {
5219fc5dc4bSYamadaMiz                echo "event: compileErrors\n";
5229fc5dc4bSYamadaMiz                echo "data: " . json_encode($errors) . "\n\n";
5239fc5dc4bSYamadaMiz                ob_flush();
5249fc5dc4bSYamadaMiz                flush();
5259fc5dc4bSYamadaMiz                return true;
5269fc5dc4bSYamadaMiz            }
5279fc5dc4bSYamadaMiz        }
5289fc5dc4bSYamadaMiz        return false;
5299fc5dc4bSYamadaMiz    }
5309fc5dc4bSYamadaMiz
5319fc5dc4bSYamadaMiz    private function handle_create_combined_file()
5329fc5dc4bSYamadaMiz    {
5339fc5dc4bSYamadaMiz        global $INPUT;
5349fc5dc4bSYamadaMiz
5359fc5dc4bSYamadaMiz        // 投稿されたコンテンツを取得
5369fc5dc4bSYamadaMiz        $combinedContent = $INPUT->post->str('content');
537a0444036SYamadaMiz        $filename = $INPUT->post->str('filename', 'combined_file.miz'); // デフォルトのファイル名を指定
5389fc5dc4bSYamadaMiz
539a0444036SYamadaMiz        // ファイルを保存せず、コンテンツを直接返す
540a0444036SYamadaMiz        if (!empty($combinedContent)) {
541a0444036SYamadaMiz            // ファイルの内容をレスポンスで返す(PHP側でファイルを作成しない)
542a0444036SYamadaMiz            $this->sendAjaxResponse(true, 'File created successfully', [
543a0444036SYamadaMiz                'filename' => $filename,
544a0444036SYamadaMiz                'content' => $combinedContent
545a0444036SYamadaMiz            ]);
546a0444036SYamadaMiz            error_log("File content sent: " . $filename);
5479fc5dc4bSYamadaMiz        } else {
548a0444036SYamadaMiz            $this->sendAjaxResponse(false, 'Content is empty, no file created');
5499fc5dc4bSYamadaMiz        }
5509fc5dc4bSYamadaMiz    }
5516b7e227cSYamadaMiz}
552