1<?php
2/**
3 * RRDGraph Plugin: Helper classes
4 *
5 * @author Daniel Goß <developer@flashsystems.de>
6 * @license MIT
7 */
8
9if (!defined('DOKU_INC')) die();
10
11/**
12 * Implements rrd class plugin's syntax plugin.
13 *
14 */
15class syntax_plugin_rrdgraph extends DokuWiki_Syntax_Plugin {
16    /** Constant that indicates that a recipe is used for cerating graphs. */
17    const RT_GRAPH = 0;
18    /** Constant that indicates that a recipe is used for inclusion in other recipes. */
19    const RT_TEMPLATE = 1;
20    /** Constant that indicates that a recipe is used for bound svg graphics. */
21    const RT_BOUNDSVG = 2;
22
23    /** Array index of the graph type within the parsed recipe. */
24    const R_TYPE = 'type';
25    /** Array index of the graph name within the parsed recipe. */
26    const R_NAME = 'name';
27    /** Array index of a flag that indicates if the results of this recipe should be included within the generated xhtml output. */
28    const R_SHOW = 'show';
29    /** Array index of the recipe data within the parsed recipe. */
30    const R_DATA = 'data';
31    /** Array index of the ganged flag within the parsed recipe. */
32    const R_GANGED = 'ganged';
33    /** Array index of the name of the bound svg file if the parsed recipe is of type RT_BOUNDSVG. */
34    const R_BSOURCE = 'bsource';
35
36    /**
37     * Stores the rrd recipe while it's parsed. This variable is reset every time a new recipe starts.
38     * @var Array
39     */
40    private $rrdRecipe;
41
42    /**
43     * Returns the syntax mode of this plugin.
44     * @return String Syntax mode type
45     */
46    public function getType() {
47        return 'substition';
48    }
49
50    /**
51     * Returns the paragraph type of this plugin.
52     * @return String Paragraph type
53     */
54    public function getPType() {
55        return array ();
56    }
57
58    /**
59     * Returns the sort order for this plugin.
60     * @return Integer Sort order - Low numbers go before high numbers
61     */
62    public function getSort() {
63        return 320;
64    }
65
66    /**
67     * Connect lookup pattern to lexer.
68     *
69     * @param string $mode Parser mode
70     */
71    public function connectTo($mode) {
72        $this->Lexer->addEntryPattern('<rrd.*?>(?=.*?</rrd>)', $mode, 'plugin_rrdgraph');
73    }
74
75    /**
76     * Adds some patterns after the start pattern was found.
77     */
78    public function postConnect() {
79        $this->Lexer->addPattern('\n[ \t]*(?:[a-z0-9,<>=&|]+\?)?[A-Z0-9]+:[^\n]+', 'plugin_rrdgraph'); //TODO: Parser Regex mit der weiter untern verschmelzen und in eine Konstante packen!
80        $this->Lexer->addExitPattern('</rrd>', 'plugin_rrdgraph');
81    }
82
83    /**
84     * Parses the given string as a boolean value.
85     * @param String $value String to be parsed.
86     * @return boolean If the string is "yes", "on" or "true" true is returned. If the stirng is anything else, false is returned.
87     */
88    private function parseBoolean($value) {
89        $value = strtolower(trim($value));
90        if (is_numeric($value)) return (intval($value) > 0);
91
92        switch ($value) {
93        case 'yes' :
94        case 'on' :
95        case 'true' :
96            return true;
97        default :
98            return false;
99        }
100    }
101
102    /**
103     * Extracts the range tags from a given recipe.
104     * @param Array $recipe The rrd recipe that should be parsed.
105     * @return Array An array of arrays is returned. For each RANGE tag an array is created containing three values: (0) The range name, (1) the start time, (2) the end time.
106     */
107    private function getRanges($recipe) {
108        $ranges = array ();
109        foreach ($recipe as $option) {
110            list ($condition, $key, $value) = $option;
111            switch ($key) {
112            case "RANGE" :
113                $range = explode(":", $value, 3);
114                if (count($range) == 3) $ranges[] = $range;
115                break;
116            }
117        }
118
119        return $ranges;
120    }
121
122    /**
123     * Generates the XHTML markup for the tabs based on a range definition generated by getRanges().
124     * @param Array $ranges The range definition generated by getRanges().
125     * @param Integer $selectedTab The number of the selected tab. (zero based).
126     * @param String $graphId The id-value (hex-hash) of the graph this tab markup is generated for.
127     * @param Boolean $initiallyGanged If the "ganged" checkbox shlould be initially ticked.
128     * @return String Returns the XHTML markup that should be inserted into the page..
129     */
130    private function generateTabs($ranges, $selectedTab, $graphId, $initiallyGanged) {
131        //-- Define the tabs for bigger streen resolutions...
132        $xhtml = '<ul class="rrdTabBar" id="' . "__T$graphId" . '">';
133        $tabCounter = 0;
134        foreach ($ranges as $number => $range) {
135            $rangeName = $range[0];
136
137            $xhtml .= '<li id="';
138            $xhtml .= '__TI' . $graphId . 'X' . $number;
139            $xhtml .= '"';
140            if ($tabCounter ++ == $selectedTab) $xhtml .= ' class="rrdActiveTab"';
141            $xhtml .= '><a href="javascript:rrdSwitchRange(';
142            $xhtml .= "'$graphId', $number";
143            $xhtml .= ')">';
144            $xhtml .= htmlentities($rangeName);
145            $xhtml .= '</a></li>';
146        }
147
148        $xhtml .= '</ul>';
149
150        //-- ...and a drop down list for small resultions and mobile devices. Theo two are switched by CSS.
151        $xhtml .= '<select id="' . "__T$graphId" . '" OnChange="rrdDropDownSelected(' . "'$graphId'" . ', this)">';
152
153        $tabCounter = 0;
154        foreach ($ranges as $number => $range) {
155            $rangeName = $range[0];
156
157            $xhtml .= '<option id="';
158            $xhtml .= '__TI' . $graphId . 'X' . $number;
159            $xhtml .= '" value=' . $number;
160            if ($tabCounter ++ == $selectedTab) $xhtml .= ' selected="true"';
161            $xhtml .= '>';
162            $xhtml .= htmlentities($rangeName);
163            $xhtml .= '</option>';
164        }
165        $xhtml .= '</select>';
166
167        $xhtml .= '<div class="rrdGangCheckbox"><input type="checkbox" value="' . $graphId . '" name="rrdgraph_gang"' . ($initiallyGanged?'checked="checked"':'') . '/></div>';
168        $xhtml .= '<div class="rrdClearFloat"></div>';
169
170        return $xhtml;
171    }
172
173    /**
174     * Parses the given tag and extracts the attributes.
175     * @param String $tag A tag <xxx> given within the DokuWiki page.
176     * @param Array $defaults An array containing the default values for non existing attributes. The attribute name is used as the array key. If the attribute is not explicitly supplied whtin $tag the value from this array is returned.
177     * @return Array Returns an array that contains the tags as key, value pairs. The key is used as the arrays key value.
178     */
179    private function parseAttributes($tag, $defaults) {
180        if (preg_match('/<[[:^space:]]+(.*?)>/', $tag, $matches) != 1) return false;
181
182        $attributes = array ();
183
184        if (($numMatches = preg_match_all('/([[:alpha:]]+)[[:space:]]*=[[:space:]]*[\'"]?([[:alnum:]:.-_]+)[\'"]?/', $matches[1], $parts, PREG_SET_ORDER)) > 0) {
185            foreach ($parts as $part) {
186                $key = strtolower(trim($part[1]));
187                $value = trim($part[2]);
188                if (! empty($value)) $attributes[$key] = $value;
189            }
190        }
191
192        foreach ($defaults as $key => $value) {
193            if (! array_key_exists($key, $attributes)) $attributes[$key] = $value;
194        }
195
196        return $attributes;
197    }
198
199    /**
200     * Recreates the line of a rrd recipe from the parsed recipe data.
201     * This is used to recreate the recipe for showing template code.
202     * This method is called by array_reduce so the parameters are documented on the php website.
203     * @param String $carry The output of the last runs.
204     * @param Array $item The element of the rrd recipe.
205     * @return String The stringified version of the passed array.
206     */
207    private function reduceRecipeLine($carry, $item) {
208        if (empty($item[0]))
209            return $carry . "\n" . $item[1] . ':' . $item[2];
210        else
211            return $carry . "\n" . $item[0] . '?' . $item[1] . ':' . $item[2];
212    }
213
214    /**
215     * Handle matches of the rrdgraph syntax
216     *
217     * @param String $match The match of the syntax
218     * @param Integer $state The state of the handler
219     * @param Integer $pos The position in the document
220     * @param Doku_Handler $handler The handler
221     * @return Array Data for the renderer
222     */
223    public function handle($match, $state, $pos, Doku_Handler $handler) {
224        //-- Do not handle comments!
225        if (isset($_REQUEST['comment'])) return false;
226
227        switch ($state) {
228
229        case DOKU_LEXER_ENTER :
230            //-- Clear the last recipe.
231            $this->rrdRecipe = array ();
232
233            $attributes = $this->parseAttributes($match, array("show" => true, "ganged" => false));
234
235            if (array_key_exists("template", $attributes)) {
236                $this->rrdRecipe[self::R_TYPE] = self::RT_TEMPLATE;
237                $this->rrdRecipe[self::R_NAME] = $attributes['template'];
238                $this->rrdRecipe[self::R_SHOW] = $this->parseBoolean($attributes['show']);
239                $this->rrdRecipe[self::R_GANGED] = false;
240            } else if (array_key_exists("bind", $attributes)) {
241                $this->rrdRecipe[self::R_TYPE] = self::RT_BOUNDSVG;
242                $this->rrdRecipe[self::R_SHOW] = true;  // Bound SVG images will never be ganged and always visible.
243                $this->rrdRecipe[self::R_GANGED] = false;
244                $this->rrdRecipe[self::R_BSOURCE] = $attributes['bind'];
245            } else {
246                $this->rrdRecipe[self::R_TYPE] = self::RT_GRAPH;
247                // The name if left empty. In this case it will be set by DOKU_LEXER_EXIT.
248                $this->rrdRecipe[self::R_SHOW] = true;
249                $this->rrdRecipe[self::R_GANGED] = $this->parseBoolean($attributes['ganged']);
250            }
251
252            break;
253
254        case DOKU_LEXER_MATCHED :
255            if (preg_match('/^(?:([a-z0-9,<>=&|]+)\?)?([A-Z0-9]+):(.*)$/', trim($match, "\r\n \t"), $matches) == 1) {
256                list ($line, $condition, $key, $value) = $matches;
257
258                //-- A rrd recipe line consists of 3 array elements. The (0) condition (may be empty), (1) the key and (2) the value.
259                $this->rrdRecipe[self::R_DATA][] = array (
260                        $condition,
261                        trim($key),
262                        trim($value)
263                );
264            }
265            break;
266
267        case DOKU_LEXER_EXIT :
268
269            //-- If no Name is set for this recipe. Create one by hashing its content.
270            if (! isset($this->rrdRecipe[self::R_NAME])) $this->rrdRecipe[self::R_NAME] = md5(serialize($this->rrdRecipe[self::R_DATA]));
271
272            return $this->rrdRecipe;
273        }
274
275        return array ();
276    }
277
278    /**
279     * Render xhtml output or metadata
280     *
281     * @param String $mode Renderer mode (supported modes: xhtml)
282     * @param Doku_Renderer $renderer The renderer
283     * @param Array $data The data from the handler() function
284     * @return boolean If rendering was successful.
285     */
286    public function render($mode, Doku_Renderer $renderer, $data) {
287        global $ID;
288
289        //-- Don't render empty data.
290        if (count($data) == 0) return false;
291
292        //-- Initialize the helper plugin. It contains functions that are used by the graph generator and the syntax plugin.
293        $rrdGraphHelper = $this->loadHelper('rrdgraph');
294
295        if ($mode == 'metadata') {
296        	//-- If metadata is rendered get the dependencies of the current recipe and merge them with the dependencies of the previous graphs.
297        	if (!is_array($renderer->meta['plugin_' . $this->getPluginName()]['dependencies'])) $renderer->meta['plugin_' . $this->getPluginName()]['dependencies'] = array();
298
299            $renderer->meta['plugin_' . $this->getPluginName()]['dependencies'] = array_unique(array_merge($renderer->meta['plugin_' . $this->getPluginName()]['dependencies'], $rrdGraphHelper->getDependencies($data[self::R_DATA])), SORT_STRING);
300        } else if ($mode == 'xhtml') {
301        	//-- If xhtml is rendered. Generate the tab bar and the images.
302        	//   Every graph gehts an id that is dereived from the md5-checksum of the recipe. This way a graph with a different recipe
303        	//   gets a new and different graphId.
304            $rrdGraphHelper = $this->loadHelper('rrdgraph');
305            $rrdGraphHelper->storeRecipe($ID, $data[self::R_NAME], $data[self::R_DATA]);
306
307            $mediaNamespace = $this->getConf('graph_media_namespace');
308
309            if ($data[self::R_SHOW]) {
310                switch ($data[self::R_TYPE]) {
311                //-- Graphs are generated and shown.
312                case self::RT_GRAPH :
313                    try {
314                        $newDoc = "";
315
316                        $graphId = $data[self::R_NAME];
317                        $imageURL = DOKU_BASE . '_media/' . $mediaNamespace . ':' . $ID . ':' . $graphId;
318                        $inflatedRecipe = $rrdGraphHelper->inflateRecipe($data[self::R_DATA]);
319                        $ranges = $this->getRanges($inflatedRecipe);
320
321                        $mainDivAttributes = array (
322                                'class' => 'rrdImage',
323                                'data-graphid' => $graphId,
324                                'data-ranges' => count($ranges)
325                        );
326                        $imageAttributes = array (
327                                'src' => $imageURL,
328                                'id' => '__I' . $graphId
329                        );
330                        $linkAttributes = array (
331                                'href' => $imageURL . '?mode=fs',
332                                'target' => 'rrdimage',
333                                'id' => '__L' . $graphId
334                        );
335
336                        $newDoc .= '<div ' . buildAttributes($mainDivAttributes) . '>';
337
338                        $newDoc .= $this->generateTabs($ranges, 0, $graphId, $data[self::R_GANGED]);
339                        $newDoc .= '<div class="rrdLoader" id="__LD' . $graphId . '"></div>';
340                        $newDoc .= '<a ' . buildAttributes($linkAttributes) . '><img ' . buildAttributes($imageAttributes) . '/></a>';
341
342                        $newDoc .= '</div>';
343
344                        $renderer->doc .= $newDoc;
345                        unset($newDoc);
346                    }
347                    catch (Exception $ex) {
348                        $renderer->doc .= '<div class="rrdError">' . htmlentities($ex->getMessage()) . '</div>';
349                    }
350                    break;
351
352                //-- Graph templates are output as text. They may be hidden via the show attribute.
353                case self::RT_TEMPLATE :
354                    $renderer->doc .= '<h2>RRD Template &quot;' . htmlentities($data[self::R_NAME]) . '&quot;</h2>';
355                    $renderer->doc .= '<pre>';
356                    $renderer->doc .= array_reduce($data[self::R_DATA], array (
357                            $this,
358                            "reduceRecipeLine"
359                    ));
360                    $renderer->doc .= '</pre>';
361                    break;
362
363                //-- This is a bound SVG file. They are processed by the graph.php file and embedded as images.
364                case self::RT_BOUNDSVG:
365                        $newDoc = "";
366
367                        $graphId = $data[self::R_NAME];
368                        $bindingSource = $data[self::R_BSOURCE];
369                        $imageURL = DOKU_BASE . '_media/' . $mediaNamespace . ':' . $ID . ':' . $graphId . '?mode=' . helper_plugin_rrdgraph::MODE_BINDSVG . '&bind=' . $bindingSource;
370
371                        $imageAttributes = array (
372                                'src' => $imageURL,
373                                'id' => '__I' . $graphId
374                        );
375
376                        $newDoc .= '<img ' . buildAttributes($imageAttributes) . '/>';
377
378                        $renderer->doc .= $newDoc;
379                        unset($newDoc);
380                        break;
381
382                }
383            }
384        }
385
386        return true;
387    }
388}
389