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 "' . htmlentities($data[self::R_NAME]) . '"</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