1<?php
2
3use dokuwiki\Extension\SyntaxPlugin;
4use dokuwiki\HTTP\DokuHTTPClient;
5use dokuwiki\Logger;
6
7/**
8 * graphviz-Plugin: Parses graphviz-blocks
9 *
10 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
11 * @author     Carl-Christian Salvesen <calle@ioslo.net>
12 * @author     Andreas Gohr <andi@splitbrain.org>
13 */
14class syntax_plugin_graphviz extends SyntaxPlugin
15{
16    /**
17     * What about paragraphs?
18     */
19    public function getPType()
20    {
21        return 'normal';
22    }
23
24    /**
25     * What kind of syntax are we?
26     */
27    public function getType()
28    {
29        return 'substition';
30    }
31
32    /**
33     * Where to sort in?
34     */
35    public function getSort()
36    {
37        return 200;
38    }
39
40    /**
41     * Connect pattern to lexer
42     */
43    public function connectTo($mode)
44    {
45        $this->Lexer->addSpecialPattern('<graphviz.*?>\n.*?\n</graphviz>', $mode, 'plugin_graphviz');
46    }
47
48    /**
49     * Handle the match
50     */
51    public function handle($match, $state, $pos, Doku_Handler $handler)
52    {
53        $info = $this->getInfo();
54
55        // prepare default data
56        $return = [
57            'width'     => 0,
58            'height'    => 0,
59            'layout'    => 'dot',
60            'align'     => '',
61            'version'   => $info['date']
62        ];
63
64        // prepare input
65        $lines = explode("\n", $match);
66        $conf = array_shift($lines);
67        array_pop($lines);
68
69        // match config options
70        if (preg_match('/\b(left|center|right)\b/i', $conf, $match)) $return['align'] = $match[1];
71        if (preg_match('/\b(\d+)x(\d+)\b/', $conf, $match)) {
72            $return['width']  = $match[1];
73            $return['height'] = $match[2];
74        }
75        if (preg_match('/\b(dot|neato|twopi|circo|fdp)\b/i', $conf, $match)) {
76            $return['layout'] = strtolower($match[1]);
77        }
78        if (preg_match('/\bwidth=([0-9]+)\b/i', $conf, $match)) $return['width'] = $match[1];
79        if (preg_match('/\bheight=([0-9]+)\b/i', $conf, $match)) $return['height'] = $match[1];
80
81
82        $input = implode("\n", $lines);
83        $return['md5'] = md5($input); // we only pass a hash around
84
85        // store input for later use
86        io_saveFile($this->getCachename($return, 'txt'), $input);
87
88        return $return;
89    }
90
91    /**
92     * Create output
93     */
94    public function render($format, Doku_Renderer $R, $data)
95    {
96        if ($format == 'xhtml') {
97            $img = DOKU_BASE . 'lib/plugins/graphviz/img.php?' . buildURLparams($data);
98            $R->doc .= '<img src="' . $img . '" class="media' . $data['align'] . ' plugin_graphviz" alt=""';
99            if ($data['width'])  $R->doc .= ' width="' . $data['width'] . '"';
100            if ($data['height']) $R->doc .= ' height="' . $data['height'] . '"';
101            if ($data['align'] == 'right') $R->doc .= ' align="right"';
102            if ($data['align'] == 'left')  $R->doc .= ' align="left"';
103            $R->doc .= '/>';
104            return true;
105        } elseif ($format == 'odt') {
106            /** @var Doku_Renderer_odt $R */
107            $src = $this->imgFile($data);
108            $R->_odtAddImage($src, $data['width'], $data['height'], $data['align']);
109            return true;
110        }
111        return false;
112    }
113
114    /**
115     * Cache file is based on parameters that influence the result image
116     */
117    protected function getCachename($data, $ext)
118    {
119        unset($data['width']);
120        unset($data['height']);
121        unset($data['align']);
122        return getcachename(implode('x', array_values($data)), '.graphviz.' . $ext);
123    }
124
125    /**
126     * Return path to the rendered image on our local system
127     */
128    public function imgFile($data)
129    {
130        $cache  = $this->getCachename($data, 'svg');
131
132        // create the file if needed
133        if (!file_exists($cache)) {
134            $in = $this->getCachename($data, 'txt');
135            if ($this->getConf('path')) {
136                $ok = $this->renderLocal($data, $in, $cache);
137            } else {
138                $ok = $this->renderRemote($data, $in, $cache);
139            }
140            if (!$ok) return false;
141            clearstatcache();
142        }
143
144        // something went wrong, we're missing the file
145        if (!file_exists($cache)) return false;
146
147        return $cache;
148    }
149
150    /**
151     * Render the output remotely at google
152     *
153     * @param array  $data The graphviz data
154     * @param string $in   The input file path
155     * @param string $out  The output file path
156     */
157    protected function renderRemote($data, $in, $out)
158    {
159        if (!file_exists($in)) {
160            Logger::debug("Graphviz: missing input file $in");
161            return false;
162        }
163
164        $http = new DokuHTTPClient();
165        $http->timeout = 30;
166        $http->headers['Content-Type'] = 'application/json';
167
168        $pass = [];
169        $pass['layout'] = $data['layout'];
170        $pass['graph'] = io_readFile($in);
171        #if($data['width'])  $pass['width']  = (int) $data['width'];
172        #if($data['height']) $pass['height'] = (int) $data['height'];
173
174        $img = $http->post('https://quickchart.io/graphviz', json_encode($pass));
175        if (!$img) {
176            Logger::debug("Graphviz: remote API call failed", $http->resp_body);
177            return false;
178        }
179
180        return io_saveFile($out, $img);
181    }
182
183    /**
184     * Run the graphviz program
185     *
186     * @param array  $data The graphviz data
187     * @param string $in   The input file path
188     * @param string $out  The output file path
189     */
190    public function renderLocal($data, $in, $out)
191    {
192        if (!file_exists($in)) {
193            Logger::debug("Graphviz: missing input file $in");
194            return false;
195        }
196
197        $cmd  = $this->getConf('path');
198        $cmd .= ' -Tsvg';
199        $cmd .= ' -K' . $data['layout'];
200        $cmd .= ' -o' . escapeshellarg($out); //output
201        $cmd .= ' ' . escapeshellarg($in); //input
202
203        exec($cmd, $output, $error);
204
205        if ($error != 0) {
206            Logger::debug("Graphviz: command failed $cmd", implode("\n", $output));
207            return false;
208        }
209        return true;
210    }
211}
212