1<?php
2/**
3 * PlantUML-Plugin: Parses plantuml blocks to render images and html
4 *
5 * @license GPL v2 (http://www.gnu.org/licenses/gpl.html)
6 * @author  Andreone
7 * @author  Willi Schönborn <w.schoenborn@googlemail.com>
8 */
9
10if (!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/');
11require_once(DOKU_INC . 'inc/init.php');
12if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
13require_once(DOKU_PLUGIN . 'syntax.php');
14
15class syntax_plugin_plantuml extends DokuWiki_Syntax_Plugin {
16
17    /**
18     * What kind of syntax are we?
19     */
20    function getType() {
21        return 'substition';
22    }
23
24    /**
25     * Where to sort in?
26     */
27    function getSort() {
28        return 200;
29    }
30
31    /**
32     * Connect pattern to lexer
33     */
34    function connectTo($mode) {
35        $this->Lexer->addSpecialPattern('<uml.*?>\n.*?\n</uml>', $mode, 'plugin_plantuml');
36    }
37
38    /**
39     * Handle the match
40     */
41    function handle($match, $state, $pos, &$handler) {
42        // echo "handle: state=$state<br>";
43        // echo "handle: match=$match<br>";
44        // echo "handle: pos=$pos<br>";
45
46        $info = $this->getInfo();
47
48        // prepare default data
49        $return = array(
50            'width' => 0,
51            'height' => 0,
52            'title' => 'PlantUML Graph',
53            'align' => '',
54            'version' => $info['date'],
55        );
56
57        // prepare input
58        $lines = explode("\n", $match);
59        $conf = array_shift($lines);
60        array_pop($lines);
61
62        // alignment
63        if (preg_match('/\b(left|center|right)\b/i', $conf, $matches)) {
64            $return['align'] = $matches[1];
65        }
66
67        // size
68        if (preg_match('/\b(\d+)x(\d+)\b/', $conf, $matches)) {
69            $return['width'] = $matches[1];
70            $return['height'] = $matches[2];
71        } else {
72            if (preg_match('/\b(?:width|w)=([0-9]+)(%?)/i', $conf, $matches)) {
73                $return['width'] = $matches[1];
74                $return['percent'] = $matches[2];
75            }
76            if (preg_match('/\b(?:height|h)=([0-9]+)\b/i', $conf, $matches)) {
77                $return['height'] = $matches[1];
78            }
79        }
80
81        // title
82        if (preg_match('/\b(?:title|t)=(\w+)\b/i', $conf, $matches)) {
83            // single word titles
84            $return['title'] = $matches[1];
85        } else if (preg_match('/(?:title|t)="([\w+\s+]+)"/i', $conf, $matches)) {
86            // multi word titles
87            $return['title'] = $matches[1];
88        }
89
90        $input = join("\n", $lines);
91        $return['md5'] = md5($input);
92
93        io_saveFile($this->_cachename($return, 'txt'), "@startuml\n$input\n@enduml");
94
95        return $return;
96    }
97
98    /**
99     * Cache file is based on parameters that influence the result image
100     */
101    function _cachename($data, $ext){
102        unset($data['width']);
103        unset($data['height']);
104        unset($data['align']);
105        unset($data['title']);
106        return getcachename(join('x', array_values($data)), ".plantuml.$ext");
107    }
108
109    /**
110     * Create output
111     */
112    function render($mode, &$renderer, $data) {
113        if ($mode == 'xhtml') {
114            $img = DOKU_BASE . 'lib/plugins/plantuml/img.php?' . buildURLParams($data);
115
116            if($data['width']) {
117                $temp = $data['width'];
118                $data['width'] = 0;
119                $img_unresized = DOKU_BASE . 'lib/plugins/plantuml/img.php?' . buildURLParams($data);
120                $data['width'] = $temp;
121            } else {
122                $img_unresized = $img;
123            }
124
125            $renderer->doc .= '<a title="' . $data['title'] . '" class="media" href="' . $img_unresized . '">';
126            $renderer->doc .= '<img src="' . $img . '" class="media' . $data['align'] . '" title="' . $data['title'] . '" alt="' . $data['title'] .  '"';
127            if ($data['width']) {
128                $renderer->doc .= ' width="' . $data['width'] . $data['percent'] . '"';
129            }
130            if ($data['height']) {
131                $renderer->doc .= ' height="' . $data['height'] . '"';
132            }
133            if ($data['align'] == 'left') {
134                $renderer-> doc .= ' align="left"';
135            }
136            if ($data['align'] == 'right') {
137                $renderer->doc .= ' align="right"';
138            }
139            $renderer->doc .= '/></a>';
140            return true;
141        } else if ($mode == 'odt') {
142            $src = $this->_imgfile($data);
143            $renderer->_odtAddImage($src, $data['width'], $data['height'], $data['align']);
144            return true;
145        } else {
146            return false;
147        }
148    }
149
150    /**
151     * Return path to the rendered image on our local system
152     * Note this is also called by img.php
153     */
154    function _imgfile($data) {
155        $cache = $this->_cachename($data, 'png');
156
157        // create the file if needed
158        if (!file_exists($cache)) {
159            $in = $this->_cachename($data, 'txt');
160            if ($this->getConf('render_local') == '0' && $this->getConf('remote_url')) {
161                $ok = $this->_remote($data, $in, $cache);
162            } else if ($this->getConf('render_local') == '1' && $this->getConf('java')) {
163                $ok = $this->_local($data, $in, $cache);
164            } else {
165                return false;
166            }
167
168            if (!$ok) return false;
169            clearstatcache();
170        }
171
172        if ($data['width'] && $data['percent'] != '%') {
173            $cache = media_resize_image($cache, 'png', $data['width'], $data['height']);
174        }
175
176        return file_exists($cache) ? $cache : false;
177    }
178
179    /**
180     * Render the output remotely at plantuml.no-ip.org
181     */
182    function _remote($data, $in, $out) {
183        if (!file_exists($in)) {
184            dbglog($in, 'No such plantuml input file');
185            return false;
186        }
187
188        $http = new DokuHTTPClient();
189        $http->timeout = 30;
190
191        $remote_url = $this->getConf('remote_url');
192        // strip trailing "/" if present
193        $base_url = preg_replace('/(.+?)\/$/', '$1', $remote_url);
194
195        $java = $this->getConf('java');
196        if ($java) {
197            // use url compression if java is available
198            $jar = $this->getConf('jar');
199            $jar = realpath($jar);
200            $jar = escapeshellarg($jar);
201
202            $command = $java;
203            $command .= ' -Djava.awt.headless=true';
204            $command .= ' -Dfile.encoding=UTF-8';
205            $command .= " -jar $jar";
206            $command .= ' -charset UTF-8';
207            $command .= ' -encodeurl';
208            $command .= ' ' . escapeshellarg($in);
209            $command .= ' 2>&1';
210
211            $encoded = exec($command, $output, $return_value);
212
213            if ($return_value == 0) {
214               $url = "$base_url/image/$encoded";
215            } else {
216                dbglog(join("\n", $output), "Encoding url failed: $command");
217                return false;
218            }
219        } else {
220            $uml = io_readFile($in);
221            // remove @startuml and @enduml, as they are not required by the webservice
222            $uml = str_replace("@startuml\n", '', $uml);
223            $uml = str_replace("\n@enduml", '', $uml);
224            $uml = str_replace("\n", '/', $uml);
225            $uml = urlencode($uml);
226            // decode encoded slashes (or plantuml server won't understand)
227            $uml = str_replace('%2F', '/', $uml);
228
229            $url = "$base_url/startuml/$uml";
230        }
231
232        $img = $http->get($url);
233        return $img ? io_saveFile($out, $img) : false;
234    }
235
236    /**
237     * Render the output locally using the plantuml.jar
238     */
239    function _local($data, $in, $out) {
240        if (!file_exists($in)) {
241            dbglog($in, 'No such plantuml input file');
242            return false;
243        }
244
245        $java = $this->getConf('java');
246        $jar = $this->getConf('jar');
247        $jar = realpath($jar);
248        $jar = escapeshellarg($jar);
249
250        // we are not specifying the output here, because plantuml will generate a file with the same
251        // name as the input but with .png extension, which is exactly what we want
252        $command = $java;
253        $command .= ' -Djava.awt.headless=true';
254        $command .= ' -Dfile.encoding=UTF-8';
255        $command .= " -jar $jar";
256        $command .= ' -charset UTF-8';
257        $command .= ' ' . escapeshellarg($in);
258        $command .= ' 2>&1';
259
260        exec($command, $output, $return_value);
261
262        if ($return_value == 0) {
263            return true;
264        } else {
265            dbglog(join("\n", $output), "PlantUML execution failed: $command");
266            return false;
267        }
268    }
269
270    /**
271     * Dumps a message in a log file (named dokuwiki_plantuml.log and located in the Dokuwidi's cache directory)
272     */
273    function _log($text) {
274        global $conf;
275        $hFile = fopen($conf['cachedir'].'/dokuwiki_plantuml.log', a);
276        if(hFile) {
277            fwrite($hFile, $text . "\r\n");
278            fclose($hFile);
279        }
280    }
281}
282