xref: /plugin/mizarverifiabledocs/action.php (revision e813ba0eff7bb751c51a2fb3ab0a0028c280df1e)
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 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
10*e813ba0eSYamada, M. * @author  Yamada
11f9af2148SYamadaMiz */
12b68e3724SYamadaMizclass action_plugin_mizarverifiabledocs extends ActionPlugin
1347595f9cSYamadaMiz{
14*e813ba0eSYamada, M.    /* ===================== Register ===================== */
15*e813ba0eSYamada, M.
1647595f9cSYamadaMiz    public function register(EventHandler $controller)
1747595f9cSYamadaMiz    {
186b7e227cSYamadaMiz        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call');
194f65af2cSYamadaMiz        $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'handle_tpl_content_display');
204f65af2cSYamadaMiz    }
214f65af2cSYamadaMiz
224f65af2cSYamadaMiz    public function handle_tpl_content_display(Event $event, $_param)
234f65af2cSYamadaMiz    {
24*e813ba0eSYamada, M.        if (!is_string($event->data)) return;
254f65af2cSYamadaMiz
264f65af2cSYamadaMiz        $html = $event->data;
27*e813ba0eSYamada, M.        if (strpos($html, 'mizarWrapper') !== false && strpos($html, 'id="hideAllButton"') === false) {
284f65af2cSYamadaMiz            $buttonHtml = '<div class="hideAllContainer">'
294f65af2cSYamadaMiz                        . '<button id="hideAllButton" class="hide-all-button">Hide All</button>'
304f65af2cSYamadaMiz                        . '<button id="showAllButton" class="hide-all-button" style="display:none;">Show All</button>'
31*e813ba0eSYamada, M.                        . '<button id="resetAllButton" class="reset-all-button">Reset All</button>'
324f65af2cSYamadaMiz                        . '</div>';
33*e813ba0eSYamada, M.            $event->data = $buttonHtml . $html;
34*e813ba0eSYamada, M.        }
35*e813ba0eSYamada, M.    }
364f65af2cSYamadaMiz
37*e813ba0eSYamada, M.    public function handle_ajax_call(Event $event, $param)
38*e813ba0eSYamada, M.    {
39*e813ba0eSYamada, M.        unset($param);
40*e813ba0eSYamada, M.        switch ($event->data) {
41*e813ba0eSYamada, M.            case 'clear_temp_files':
42*e813ba0eSYamada, M.                $event->preventDefault(); $event->stopPropagation();
43*e813ba0eSYamada, M.                $this->clearTempFiles(); break;
44*e813ba0eSYamada, M.
45*e813ba0eSYamada, M.            case 'source_compile':
46*e813ba0eSYamada, M.                $event->preventDefault(); $event->stopPropagation();
47*e813ba0eSYamada, M.                $this->handleSourceCompileRequest(); break;
48*e813ba0eSYamada, M.
49*e813ba0eSYamada, M.            case 'source_sse':
50*e813ba0eSYamada, M.                $event->preventDefault(); $event->stopPropagation();
51*e813ba0eSYamada, M.                $this->handleSourceSSERequest(); break;
52*e813ba0eSYamada, M.
53*e813ba0eSYamada, M.            case 'view_compile':
54*e813ba0eSYamada, M.                $event->preventDefault(); $event->stopPropagation();
55*e813ba0eSYamada, M.                $this->handleViewCompileRequest(); break;
56*e813ba0eSYamada, M.
57*e813ba0eSYamada, M.            case 'view_sse':
58*e813ba0eSYamada, M.                $event->preventDefault(); $event->stopPropagation();
59*e813ba0eSYamada, M.                $this->handleViewSSERequest(); break;
60*e813ba0eSYamada, M.
61*e813ba0eSYamada, M.            case 'create_combined_file':
62*e813ba0eSYamada, M.                $event->preventDefault(); $event->stopPropagation();
63*e813ba0eSYamada, M.                $this->handle_create_combined_file(); break;
64*e813ba0eSYamada, M.
65*e813ba0eSYamada, M.            case 'view_graph':
66*e813ba0eSYamada, M.                $event->preventDefault(); $event->stopPropagation();
67*e813ba0eSYamada, M.                $this->handleViewGraphRequest(); break;
684f65af2cSYamadaMiz        }
694f65af2cSYamadaMiz    }
70*e813ba0eSYamada, M.
71*e813ba0eSYamada, M.    /* ===================== Helpers ===================== */
72*e813ba0eSYamada, M.
73*e813ba0eSYamada, M.    private function isWindows(): bool {
74*e813ba0eSYamada, M.        return strncasecmp(PHP_OS, 'WIN', 3) === 0;
75*e813ba0eSYamada, M.    }
76*e813ba0eSYamada, M.
77*e813ba0eSYamada, M.    /** 設定→未設定なら htdocs(= DOKU_INC の1つ上) 相対にフォールバックして正規化 */
78*e813ba0eSYamada, M.    private function resolvePaths(): array
79*e813ba0eSYamada, M.    {
80*e813ba0eSYamada, M.        // DokuWiki ルート(末尾スラッシュなしに正規化)
81*e813ba0eSYamada, M.        $dokuroot = rtrim(realpath(DOKU_INC), '/\\');
82*e813ba0eSYamada, M.
83*e813ba0eSYamada, M.        // htdocs を「dokuwiki の 1つ上」から求める
84*e813ba0eSYamada, M.        $htdocs = realpath($dokuroot . DIRECTORY_SEPARATOR . '..');
85*e813ba0eSYamada, M.        if ($htdocs === false) $htdocs = dirname($dokuroot);
86*e813ba0eSYamada, M.
87*e813ba0eSYamada, M.        $defM = $htdocs . DIRECTORY_SEPARATOR . 'MIZAR';
88*e813ba0eSYamada, M.        $defW = $htdocs . DIRECTORY_SEPARATOR . 'work';
89*e813ba0eSYamada, M.
90*e813ba0eSYamada, M.        // 設定値取得(空ならフォールバック)。相対指定が来たら htdocs 基準に解決
91*e813ba0eSYamada, M.        $exe   = trim((string)$this->getConf('mizar_exe_dir'));
92*e813ba0eSYamada, M.        $share = trim((string)$this->getConf('mizar_share_dir'));
93*e813ba0eSYamada, M.        $work  = trim((string)$this->getConf('mizar_work_dir'));
94*e813ba0eSYamada, M.
95*e813ba0eSYamada, M.        $isAbs = function(string $p): bool {
96*e813ba0eSYamada, M.            // Windowsドライブ/UNC/Unix ざっくり対応
97*e813ba0eSYamada, M.            return $p !== '' && (preg_match('~^[A-Za-z]:[\\/]|^\\\\\\\\|^/~', $p) === 1);
98*e813ba0eSYamada, M.        };
99*e813ba0eSYamada, M.
100*e813ba0eSYamada, M.        if ($exe   !== '' && !$isAbs($exe))   $exe   = $htdocs . DIRECTORY_SEPARATOR . $exe;
101*e813ba0eSYamada, M.        if ($share !== '' && !$isAbs($share)) $share = $htdocs . DIRECTORY_SEPARATOR . $share;
102*e813ba0eSYamada, M.        if ($work  !== '' && !$isAbs($work))  $work  = $htdocs . DIRECTORY_SEPARATOR . $work;
103*e813ba0eSYamada, M.
104*e813ba0eSYamada, M.        $exe   = rtrim($exe   ?: $defM, '/\\');
105*e813ba0eSYamada, M.        $share = rtrim($share ?: $defM, '/\\');
106*e813ba0eSYamada, M.        $work  = rtrim($work  ?: $defW, '/\\');
107*e813ba0eSYamada, M.
108*e813ba0eSYamada, M.        return ['exe' => $exe, 'share' => $share, 'work' => $work];
109*e813ba0eSYamada, M.    }
110*e813ba0eSYamada, M.
111*e813ba0eSYamada, M.    /** exeDir 直下 or exeDir\bin から実行ファイルを探す(.exe/.bat/.cmd 対応) */
112*e813ba0eSYamada, M.    private function findExe(string $exeDir, string $name): ?string
113*e813ba0eSYamada, M.    {
114*e813ba0eSYamada, M.        if ($this->isWindows()) {
115*e813ba0eSYamada, M.            $candidates = [
116*e813ba0eSYamada, M.                "$exeDir\\$name.exe",          "$exeDir\\bin\\$name.exe",
117*e813ba0eSYamada, M.                "$exeDir\\$name.bat",          "$exeDir\\bin\\$name.bat",
118*e813ba0eSYamada, M.                "$exeDir\\$name.cmd",          "$exeDir\\bin\\$name.cmd",
119*e813ba0eSYamada, M.                "$exeDir\\windows\\bin\\$name.exe",
120*e813ba0eSYamada, M.                "$exeDir\\win\\bin\\$name.exe",
121*e813ba0eSYamada, M.            ];
122*e813ba0eSYamada, M.        } else {
123*e813ba0eSYamada, M.            $candidates = [
124*e813ba0eSYamada, M.                "$exeDir/$name",               "$exeDir/bin/$name",
125*e813ba0eSYamada, M.            ];
126*e813ba0eSYamada, M.        }
127*e813ba0eSYamada, M.        foreach ($candidates as $p) {
128*e813ba0eSYamada, M.            if (is_file($p)) return $p;
129*e813ba0eSYamada, M.        }
130*e813ba0eSYamada, M.        return null;
131*e813ba0eSYamada, M.    }
132*e813ba0eSYamada, M.
133*e813ba0eSYamada, M.    /** 出力をUTF-8へ(WinはSJIS-WIN想定) */
134*e813ba0eSYamada, M.    private function outUTF8(string $s): string
135*e813ba0eSYamada, M.    {
136*e813ba0eSYamada, M.        return $this->isWindows() ? mb_convert_encoding($s, 'UTF-8', 'SJIS-WIN') : $s;
1376b7e227cSYamadaMiz    }
1386b7e227cSYamadaMiz
13947595f9cSYamadaMiz    /**
140*e813ba0eSYamada, M.     * .exe は配列+bypass_shell、.bat/.cmd は「cmd /C ""...""」の文字列+shell経由で起動
141*e813ba0eSYamada, M.     * @return array [$proc, $pipes] 失敗時は [null, []]
14247595f9cSYamadaMiz     */
143*e813ba0eSYamada, M.    private function openProcess(string $exeOrBat, array $args, string $cwd): array
14447595f9cSYamadaMiz    {
145*e813ba0eSYamada, M.        $des = [1 => ['pipe','w'], 2 => ['pipe','w']];
14647595f9cSYamadaMiz
147*e813ba0eSYamada, M.        if ($this->isWindows() && preg_match('/\.(bat|cmd)$/i', $exeOrBat)) {
148*e813ba0eSYamada, M.            // cmd /C ""C:\path\tool.bat" "arg1" "arg2""
149*e813ba0eSYamada, M.            $cmd = 'cmd.exe /C "'
150*e813ba0eSYamada, M.                 . '"' . $exeOrBat . '"';
151*e813ba0eSYamada, M.            foreach ($args as $a) {
152*e813ba0eSYamada, M.                $cmd .= ' ' . escapeshellarg($a);
1536b7e227cSYamadaMiz            }
154*e813ba0eSYamada, M.            $cmd .= '"';
155*e813ba0eSYamada, M.            $proc = proc_open($cmd, $des, $pipes, $cwd); // shell経由(bypass_shell=false)
156*e813ba0eSYamada, M.        } else {
157*e813ba0eSYamada, M.            $cmd = array_merge([$exeOrBat], $args);
158*e813ba0eSYamada, M.            $proc = proc_open($cmd, $des, $pipes, $cwd, null, ['bypass_shell' => true]);
1596b7e227cSYamadaMiz        }
1606b7e227cSYamadaMiz
161*e813ba0eSYamada, M.        if (!is_resource($proc)) return [null, []];
162*e813ba0eSYamada, M.        return [$proc, $pipes];
163*e813ba0eSYamada, M.    }
164*e813ba0eSYamada, M.
165*e813ba0eSYamada, M.    /* ===================== Source ===================== */
166*e813ba0eSYamada, M.
16747595f9cSYamadaMiz    private function handleSourceCompileRequest()
16847595f9cSYamadaMiz    {
1696b7e227cSYamadaMiz        global $INPUT;
1706b7e227cSYamadaMiz        $pageContent = $INPUT->post->str('content');
1716b7e227cSYamadaMiz        $mizarData = $this->extractMizarContent($pageContent);
1726b7e227cSYamadaMiz
173*e813ba0eSYamada, M.        if ($mizarData === null) { $this->sendAjaxResponse(false, 'Mizar content not found'); return; }
174*e813ba0eSYamada, M.        if (isset($mizarData['error'])) { $this->sendAjaxResponse(false, $mizarData['error']); return; }
1756b7e227cSYamadaMiz
1766b7e227cSYamadaMiz        $filePath = $this->saveMizarContent($mizarData);
177*e813ba0eSYamada, M.        if (session_status() !== PHP_SESSION_ACTIVE) session_start();
1786b7e227cSYamadaMiz        $_SESSION['source_filepath'] = $filePath;
1796b7e227cSYamadaMiz
1806b7e227cSYamadaMiz        $this->sendAjaxResponse(true, 'Mizar content processed successfully');
1816b7e227cSYamadaMiz    }
1826b7e227cSYamadaMiz
18347595f9cSYamadaMiz    private function handleSourceSSERequest()
18447595f9cSYamadaMiz    {
1856b7e227cSYamadaMiz        header('Content-Type: text/event-stream');
18614f6cf5bSYamadaMiz        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
18714f6cf5bSYamadaMiz        header('Pragma: no-cache');
18814f6cf5bSYamadaMiz        header('Expires: 0');
1896b7e227cSYamadaMiz
190*e813ba0eSYamada, M.        if (session_status() !== PHP_SESSION_ACTIVE) session_start();
191*e813ba0eSYamada, M.        if (empty($_SESSION['source_filepath'])) {
192*e813ba0eSYamada, M.            echo "data: Mizar file path not found in session\n\n"; @ob_flush(); @flush(); return;
1936b7e227cSYamadaMiz        }
1946b7e227cSYamadaMiz
195*e813ba0eSYamada, M.        $this->streamSourceOutput($_SESSION['source_filepath']);
1966b7e227cSYamadaMiz
1976b7e227cSYamadaMiz        echo "event: end\n";
1986b7e227cSYamadaMiz        echo "data: Compilation complete\n\n";
199*e813ba0eSYamada, M.        @ob_flush(); @flush();
2006b7e227cSYamadaMiz    }
2016b7e227cSYamadaMiz
202*e813ba0eSYamada, M.    /* ===================== View ===================== */
203*e813ba0eSYamada, M.
20447595f9cSYamadaMiz    private function handleViewCompileRequest()
20547595f9cSYamadaMiz    {
20647595f9cSYamadaMiz        global $INPUT;
20747595f9cSYamadaMiz        $content = $INPUT->post->str('content');
20847595f9cSYamadaMiz        $filePath = $this->createTempFile($content);
20947595f9cSYamadaMiz
210*e813ba0eSYamada, M.        if (session_status() !== PHP_SESSION_ACTIVE) session_start();
21147595f9cSYamadaMiz        $_SESSION['view_filepath'] = $filePath;
21247595f9cSYamadaMiz
21347595f9cSYamadaMiz        $this->sendAjaxResponse(true, 'Mizar content processed successfully');
21447595f9cSYamadaMiz    }
21547595f9cSYamadaMiz
21647595f9cSYamadaMiz    private function handleViewSSERequest()
21747595f9cSYamadaMiz    {
21847595f9cSYamadaMiz        header('Content-Type: text/event-stream');
21914f6cf5bSYamadaMiz        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
22014f6cf5bSYamadaMiz        header('Pragma: no-cache');
22114f6cf5bSYamadaMiz        header('Expires: 0');
22247595f9cSYamadaMiz
223*e813ba0eSYamada, M.        if (session_status() !== PHP_SESSION_ACTIVE) session_start();
224*e813ba0eSYamada, M.        if (empty($_SESSION['view_filepath'])) {
225*e813ba0eSYamada, M.            echo "data: Mizar file path not found in session\n\n"; @ob_flush(); @flush(); return;
22647595f9cSYamadaMiz        }
22747595f9cSYamadaMiz
228*e813ba0eSYamada, M.        $this->streamViewCompileOutput($_SESSION['view_filepath']);
22947595f9cSYamadaMiz
23047595f9cSYamadaMiz        echo "event: end\n";
23147595f9cSYamadaMiz        echo "data: Compilation complete\n\n";
232*e813ba0eSYamada, M.        @ob_flush(); @flush();
23347595f9cSYamadaMiz    }
23447595f9cSYamadaMiz
235*e813ba0eSYamada, M.    /***** view_graph: SVG を返す *****/
2363f7dd076SYamadaMiz    private function handleViewGraphRequest()
2373f7dd076SYamadaMiz    {
2383f7dd076SYamadaMiz        global $INPUT;
2393f7dd076SYamadaMiz        $content = $INPUT->post->str('content', '');
240*e813ba0eSYamada, M.        if ($content === '') { $this->sendAjaxResponse(false, 'Empty content'); return; }
2413f7dd076SYamadaMiz
2423f7dd076SYamadaMiz        $tmp = tempnam(sys_get_temp_dir(), 'miz');
2433f7dd076SYamadaMiz        $miz = $tmp . '.miz';
244*e813ba0eSYamada, M.        rename($tmp, $miz);
2453f7dd076SYamadaMiz        file_put_contents($miz, $content);
2463f7dd076SYamadaMiz
247*e813ba0eSYamada, M.        $parser = __DIR__ . '/script/miz2svg.py';
248*e813ba0eSYamada, M.        $py     = $this->getConf('py_cmd') ?: 'python';
249*e813ba0eSYamada, M.        $svg    = shell_exec(escapeshellcmd($py) . ' ' . escapeshellarg($parser) . ' ' . escapeshellarg($miz));
250*e813ba0eSYamada, M.        @unlink($miz);
2513f7dd076SYamadaMiz
252*e813ba0eSYamada, M.        if ($svg) $this->sendAjaxResponse(true, 'success', ['svg' => $svg]);
253*e813ba0eSYamada, M.        else      $this->sendAjaxResponse(false, 'conversion failed');
2543f7dd076SYamadaMiz    }
2553f7dd076SYamadaMiz
256*e813ba0eSYamada, M.    /* ===================== Content utils ===================== */
257*e813ba0eSYamada, M.
25847595f9cSYamadaMiz    private function extractMizarContent($pageContent)
25947595f9cSYamadaMiz    {
2606b7e227cSYamadaMiz        $pattern = '/<mizar\s+([^>]+)>(.*?)<\/mizar>/s';
261*e813ba0eSYamada, M.        preg_match_all($pattern, $pageContent, $m, PREG_SET_ORDER);
262*e813ba0eSYamada, M.        if (empty($m)) return null;
2636b7e227cSYamadaMiz
264*e813ba0eSYamada, M.        $fn   = trim($m[0][1]);
265*e813ba0eSYamada, M.        $stem = preg_replace('/\.miz$/i', '', $fn);
266*e813ba0eSYamada, M.        if (!$this->isValidFileName($stem)) {
267*e813ba0eSYamada, M.            return ['error' => "Invalid characters in file name: '{$stem}'. Only letters, numbers, underscores (_), and apostrophes (') are allowed, up to 8 characters."];
2686b7e227cSYamadaMiz        }
2696b7e227cSYamadaMiz
270*e813ba0eSYamada, M.        $combined = '';
271*e813ba0eSYamada, M.        foreach ($m as $mm) {
272*e813ba0eSYamada, M.            $cur = preg_replace('/\.miz$/i', '', trim($mm[1]));
273*e813ba0eSYamada, M.            if ($cur !== $stem) return ['error' => "File name mismatch in <mizar> tags: '{$stem}' and '{$cur}'"];
274*e813ba0eSYamada, M.            if (!$this->isValidFileName($cur)) return ['error' => "Invalid characters in file name: '{$cur}'."];
275*e813ba0eSYamada, M.            $combined .= trim($mm[2]) . "\n";
276*e813ba0eSYamada, M.        }
277*e813ba0eSYamada, M.        return ['fileName' => $stem . '.miz', 'content' => $combined];
27842c7ffb2SYamadaMiz    }
27942c7ffb2SYamadaMiz
28042c7ffb2SYamadaMiz    private function isValidFileName($fileName)
28142c7ffb2SYamadaMiz    {
282*e813ba0eSYamada, M.        if (strlen($fileName) > 8) return false;
283*e813ba0eSYamada, M.        return (bool)preg_match('/^[A-Za-z0-9_\']+$/', $fileName);
28442c7ffb2SYamadaMiz    }
28542c7ffb2SYamadaMiz
28647595f9cSYamadaMiz    private function saveMizarContent($mizarData)
28747595f9cSYamadaMiz    {
288*e813ba0eSYamada, M.        $paths = $this->resolvePaths();
289*e813ba0eSYamada, M.        $textDir = $paths['work'] . DIRECTORY_SEPARATOR . 'TEXT';
290*e813ba0eSYamada, M.        if (!is_dir($textDir)) @mkdir($textDir, 0777, true);
291*e813ba0eSYamada, M.
292*e813ba0eSYamada, M.        $filePath = $textDir . DIRECTORY_SEPARATOR . $mizarData['fileName'];
2936b7e227cSYamadaMiz        file_put_contents($filePath, $mizarData['content']);
2946b7e227cSYamadaMiz        return $filePath;
2956b7e227cSYamadaMiz    }
2966b7e227cSYamadaMiz
29747595f9cSYamadaMiz    private function createTempFile($content)
29847595f9cSYamadaMiz    {
299*e813ba0eSYamada, M.        $paths = $this->resolvePaths();
300*e813ba0eSYamada, M.        $textDir = $paths['work'] . DIRECTORY_SEPARATOR . 'TEXT';
301*e813ba0eSYamada, M.        if (!is_dir($textDir)) @mkdir($textDir, 0777, true);
302*e813ba0eSYamada, M.
303*e813ba0eSYamada, M.        $tempFilename = $textDir . DIRECTORY_SEPARATOR . str_replace('.', '_', uniqid('tmp', true)) . ".miz";
3046b7e227cSYamadaMiz        file_put_contents($tempFilename, $content);
3056b7e227cSYamadaMiz        return $tempFilename;
3066b7e227cSYamadaMiz    }
3076b7e227cSYamadaMiz
30847595f9cSYamadaMiz    private function clearTempFiles()
30947595f9cSYamadaMiz    {
310*e813ba0eSYamada, M.        $paths = $this->resolvePaths();
311*e813ba0eSYamada, M.        $dir = $paths['work'] . DIRECTORY_SEPARATOR . 'TEXT' . DIRECTORY_SEPARATOR;
312*e813ba0eSYamada, M.        $files = glob($dir . '*');
31347595f9cSYamadaMiz
31447595f9cSYamadaMiz        $errors = [];
315*e813ba0eSYamada, M.        foreach ($files as $f) {
316*e813ba0eSYamada, M.            if (is_file($f)) {
317*e813ba0eSYamada, M.                if (!$this->is_file_locked($f)) {
318*e813ba0eSYamada, M.                    $ok = false; $retries = 5;
319*e813ba0eSYamada, M.                    while ($retries-- > 0) { if (@unlink($f)) { $ok = true; break; } sleep(2); }
320*e813ba0eSYamada, M.                    if (!$ok) $errors[] = "Failed to delete: $f";
32147595f9cSYamadaMiz                } else {
322*e813ba0eSYamada, M.                    $errors[] = "File is locked: $f";
32347595f9cSYamadaMiz                }
32447595f9cSYamadaMiz            }
32547595f9cSYamadaMiz        }
326*e813ba0eSYamada, M.        if ($errors) $this->sendAjaxResponse(false, 'Some files could not be deleted', $errors);
327*e813ba0eSYamada, M.        else         $this->sendAjaxResponse(true, 'Temporary files cleared successfully');
328*e813ba0eSYamada, M.    }
32947595f9cSYamadaMiz
33047595f9cSYamadaMiz    private function is_file_locked($file)
33147595f9cSYamadaMiz    {
332*e813ba0eSYamada, M.        $fp = @fopen($file, "r+");
333*e813ba0eSYamada, M.        if ($fp === false) return true;
334*e813ba0eSYamada, M.        $locked = !flock($fp, LOCK_EX | LOCK_NB);
335*e813ba0eSYamada, M.        fclose($fp);
336*e813ba0eSYamada, M.        return $locked;
33747595f9cSYamadaMiz    }
33847595f9cSYamadaMiz
339*e813ba0eSYamada, M.    /* ===================== Run (miz2prel/makeenv/verifier) ===================== */
34047595f9cSYamadaMiz
341*e813ba0eSYamada, M.    private function streamSourceOutput($filePath)
34247595f9cSYamadaMiz    {
343*e813ba0eSYamada, M.        $paths = $this->resolvePaths();
344*e813ba0eSYamada, M.        $workPath  = $paths['work'];
345*e813ba0eSYamada, M.        $sharePath = $paths['share'];
346*e813ba0eSYamada, M.        putenv("MIZFILES={$sharePath}");
3476b7e227cSYamadaMiz
348*e813ba0eSYamada, M.        $exe = $this->findExe($paths['exe'], 'miz2prel');
349*e813ba0eSYamada, M.        if ($exe === null) {
350*e813ba0eSYamada, M.            echo "data: ERROR: miz2prel not found under {$paths['exe']} (or bin)\n\n"; @ob_flush(); @flush(); return;
351*e813ba0eSYamada, M.        }
352a16ae7e3SYamadaMiz
353*e813ba0eSYamada, M.        [$proc, $pipes] = $this->openProcess($exe, [$filePath], $workPath);
354*e813ba0eSYamada, M.        if (!$proc) { echo "data: ERROR: Failed to execute miz2prel.\n\n"; @ob_flush(); @flush(); return; }
355*e813ba0eSYamada, M.
356*e813ba0eSYamada, M.        while (($line = fgets($pipes[1])) !== false) { echo "data: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); }
357*e813ba0eSYamada, M.        fclose($pipes[1]);
358*e813ba0eSYamada, M.        while (($line = fgets($pipes[2])) !== false) { echo "data: ERROR: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); }
359*e813ba0eSYamada, M.        fclose($pipes[2]);
360*e813ba0eSYamada, M.        proc_close($proc);
3616b7e227cSYamadaMiz
3629fc5dc4bSYamadaMiz        $errFilename = str_replace('.miz', '.err', $filePath);
363*e813ba0eSYamada, M.        $this->handleCompilationErrors($errFilename, $sharePath . DIRECTORY_SEPARATOR . 'mizar.msg');
3646b7e227cSYamadaMiz    }
365*e813ba0eSYamada, M.
366*e813ba0eSYamada, M.    private function streamViewCompileOutput($filePath)
367*e813ba0eSYamada, M.    {
368*e813ba0eSYamada, M.        $paths = $this->resolvePaths();
369*e813ba0eSYamada, M.        $workPath  = $paths['work'];
370*e813ba0eSYamada, M.        $sharePath = $paths['share'];
371*e813ba0eSYamada, M.        putenv("MIZFILES={$sharePath}");
372*e813ba0eSYamada, M.
373*e813ba0eSYamada, M.        // makeenv
374*e813ba0eSYamada, M.        $makeenv = $this->findExe($paths['exe'], 'makeenv');
375*e813ba0eSYamada, M.        if ($makeenv === null) { echo "data: ERROR: makeenv not found under {$paths['exe']} (or bin)\n\n"; @ob_flush(); @flush(); return; }
376*e813ba0eSYamada, M.        [$proc, $pipes] = $this->openProcess($makeenv, [$filePath], $workPath);
377*e813ba0eSYamada, M.        if (!$proc) { echo "data: ERROR: Failed to execute makeenv.\n\n"; @ob_flush(); @flush(); return; }
378*e813ba0eSYamada, M.        while (($line = fgets($pipes[1])) !== false) { echo "data: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); }
3796b7e227cSYamadaMiz        fclose($pipes[1]);
380*e813ba0eSYamada, M.        while (($line = fgets($pipes[2])) !== false) { echo "data: ERROR: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); }
3816b7e227cSYamadaMiz        fclose($pipes[2]);
382*e813ba0eSYamada, M.        proc_close($proc);
3836b7e227cSYamadaMiz
384*e813ba0eSYamada, M.        $errFilename = str_replace('.miz', '.err', $filePath);
385*e813ba0eSYamada, M.        if ($this->handleCompilationErrors($errFilename, $sharePath . DIRECTORY_SEPARATOR . 'mizar.msg')) return;
386*e813ba0eSYamada, M.
387*e813ba0eSYamada, M.        // verifier
388*e813ba0eSYamada, M.        $verifier = $this->findExe($paths['exe'], 'verifier');
389*e813ba0eSYamada, M.        if ($verifier === null) { echo "data: ERROR: verifier not found under {$paths['exe']} (or bin)\n\n"; @ob_flush(); @flush(); return; }
390*e813ba0eSYamada, M.        $rel = 'TEXT' . DIRECTORY_SEPARATOR . basename($filePath);
391*e813ba0eSYamada, M.        [$proc, $pipes] = $this->openProcess($verifier, ['-q','-l',$rel], $workPath);
392*e813ba0eSYamada, M.        if (!$proc) { echo "data: ERROR: Failed to execute verifier.\n\n"; @ob_flush(); @flush(); return; }
393*e813ba0eSYamada, M.        while (($line = fgets($pipes[1])) !== false) { echo "data: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); }
394*e813ba0eSYamada, M.        fclose($pipes[1]);
395*e813ba0eSYamada, M.        while (($line = fgets($pipes[2])) !== false) { echo "data: ERROR: " . $this->outUTF8($line) . "\n\n"; @ob_flush(); @flush(); }
396*e813ba0eSYamada, M.        fclose($pipes[2]);
397*e813ba0eSYamada, M.        proc_close($proc);
398*e813ba0eSYamada, M.
399*e813ba0eSYamada, M.        $this->handleCompilationErrors($errFilename, $sharePath . DIRECTORY_SEPARATOR . 'mizar.msg');
4006b7e227cSYamadaMiz    }
4016b7e227cSYamadaMiz
402*e813ba0eSYamada, M.    /* ===================== Errors ===================== */
4036b7e227cSYamadaMiz
40447595f9cSYamadaMiz    private function getMizarErrorMessages($mizarMsgFile)
40547595f9cSYamadaMiz    {
406*e813ba0eSYamada, M.        if (!is_file($mizarMsgFile)) return [];
4076b7e227cSYamadaMiz        $errorMessages = [];
4086b7e227cSYamadaMiz        $lines = file($mizarMsgFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
409*e813ba0eSYamada, M.        $isReading = false; $key = 0;
4106b7e227cSYamadaMiz        foreach ($lines as $line) {
411*e813ba0eSYamada, M.            if (preg_match('/# (\d+)/', $line, $m)) { $isReading = true; $key = (int)$m[1]; }
412*e813ba0eSYamada, M.            elseif ($isReading) { $errorMessages[$key] = $line; $isReading = false; }
4136b7e227cSYamadaMiz        }
414*e813ba0eSYamada, M.        return $errorMessages;
4156b7e227cSYamadaMiz    }
4166b7e227cSYamadaMiz
417*e813ba0eSYamada, M.    private function handleCompilationErrors($errFilename, $mizarMsgFilePath)
418*e813ba0eSYamada, M.    {
419*e813ba0eSYamada, M.        if (!file_exists($errFilename)) return false;
420*e813ba0eSYamada, M.        $errs = []; $lines = file($errFilename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
421*e813ba0eSYamada, M.        foreach ($lines as $ln) {
422*e813ba0eSYamada, M.            if (preg_match('/(\d+)\s+(\d+)\s+(\d+)/', $ln, $m)) {
423*e813ba0eSYamada, M.                $code = (int)$m[3];
424*e813ba0eSYamada, M.                $errs[] = [
425*e813ba0eSYamada, M.                    'code'    => $code,
426*e813ba0eSYamada, M.                    'line'    => (int)$m[1],
427*e813ba0eSYamada, M.                    'column'  => (int)$m[2],
428*e813ba0eSYamada, M.                    'message' => $this->getMizarErrorMessages($mizarMsgFilePath)[$code] ?? 'Unknown error'
429*e813ba0eSYamada, M.                ];
430*e813ba0eSYamada, M.            }
431*e813ba0eSYamada, M.        }
432*e813ba0eSYamada, M.        if ($errs) {
433*e813ba0eSYamada, M.            echo "event: compileErrors\n";
434*e813ba0eSYamada, M.            echo "data: " . json_encode($errs) . "\n\n";
435*e813ba0eSYamada, M.            @ob_flush(); @flush();
436*e813ba0eSYamada, M.            return true;
437*e813ba0eSYamada, M.        }
438*e813ba0eSYamada, M.        return false;
4396b7e227cSYamadaMiz    }
4406b7e227cSYamadaMiz
44147595f9cSYamadaMiz    private function sendAjaxResponse($success, $message, $data = '')
44247595f9cSYamadaMiz    {
4436b7e227cSYamadaMiz        header('Content-Type: application/json');
44414f6cf5bSYamadaMiz        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
44514f6cf5bSYamadaMiz        header('Pragma: no-cache');
44614f6cf5bSYamadaMiz        header('Expires: 0');
4476b7e227cSYamadaMiz        echo json_encode(['success' => $success, 'message' => $message, 'data' => $data]);
4486b7e227cSYamadaMiz        exit;
4496b7e227cSYamadaMiz    }
4509fc5dc4bSYamadaMiz
4519fc5dc4bSYamadaMiz    private function handle_create_combined_file()
4529fc5dc4bSYamadaMiz    {
4539fc5dc4bSYamadaMiz        global $INPUT;
4549fc5dc4bSYamadaMiz        $combinedContent = $INPUT->post->str('content');
455*e813ba0eSYamada, M.        $filename = $INPUT->post->str('filename', 'combined_file.miz');
4569fc5dc4bSYamadaMiz
457a0444036SYamadaMiz        if (!empty($combinedContent)) {
458a0444036SYamadaMiz            $this->sendAjaxResponse(true, 'File created successfully', [
459a0444036SYamadaMiz                'filename' => $filename,
460a0444036SYamadaMiz                'content'  => $combinedContent
461a0444036SYamadaMiz            ]);
4629fc5dc4bSYamadaMiz        } else {
463a0444036SYamadaMiz            $this->sendAjaxResponse(false, 'Content is empty, no file created');
4649fc5dc4bSYamadaMiz        }
4659fc5dc4bSYamadaMiz    }
4666b7e227cSYamadaMiz}
467