1<?php 2/** 3 * RRDGraph Plugin: Helper classes 4 * 5 * @author Daniel Goß <developer@flashsystems.de> 6 * @license MIT 7 */ 8 9require_once ('inc/errorimage.php'); 10require_once ('inc/rpncomputer.php'); 11require_once ('inc/svgbinding.php'); 12require_once ('inc/contenttypes.php'); 13 14/** 15 * Base class for all cache implementations within the RRDGraph plugin. 16 * This class is derived from the DokuWiki cache class. 17 * It implements the dependency handling mechanism that is needed for the 18 * rrd INCLUDE tag. 19 */ 20abstract class cache_rrdgraphbase extends cache { 21 /** @var String Page-Number of the page that is managed by this cache instance. */ 22 private $pageId; 23 24 /** @var String Name of the plugin using this cache. This value is used to get the dependencies metadata. */ 25 private $pluginName; 26 27 /** 28 * C'tor 29 * @param String $pluginName The name of the plugin. This can be retrieved by getPluginName() and makes the plugin more robust to renaming. 30 * @param String $pageId The wiki id of the page the cached content is on. 31 * @param String $key Uniq value identifying the cached content on the page provied by $pageId. The identifier is hashed before being used. 32 * @param String $ext The extension of the cache file. 33 */ 34 public function __construct($pluginName, $pageId, $key, $ext) { 35 $this->pageId = $pageId; 36 $this->pluginName = $pluginName; 37 38 parent::__construct($pageId . '/' . $key, $ext); 39 } 40 41 /** 42 * Adds the dependencies from the plugin_[name] -> dependencies metadata element. 43 * This way the included dependencies of the rrd graphs on a page can be tracked. 44 */ 45 protected function _addDependencies() { 46 $files = array ( 47 wikiFN($this->pageId) 48 ); 49 50 //-- We oversimplify this a litte and add all dependencies of the current page to very image 51 // without distinction between the recipies. 52 // But if one include is changed recalculating all images only generates litte overhead because 53 // they are regenerated every time after a cache timeout. 54 $dependencies = p_get_metadata($this->pageId, 'plugin_' . $this->pluginName . ' dependencies'); 55 56 if (! empty($dependencies)) { 57 foreach ($dependencies as $dependency) { 58 $files[] = wikiFN($dependency); 59 } 60 } 61 62 if (! array_key_exists('files', $this->depends)) 63 $this->depends['files'] = $files; 64 else 65 $this->depends['files'] = array_merge($files, $this->depends['files']); 66 67 parent::_addDependencies(); 68 } 69} 70 71/** 72 * This cache class manages the rrd recipe cache. 73 * This cache only times out if the recipe changes. 74 * 75 */ 76class cache_rrdgraph extends cache_rrdgraphbase { 77 /** 78 * C'tor 79 * @param String $pluginName The name of the plugin. This can be retrieved by getPluginName() and makes the plugin more robust to renaming. 80 * @param String $pageId The wiki id of the page the cached content is on. 81 * @param String $recipeName An identifier used to identify the cache recipe on the page provied by pageId. The identifier is hashed before being used. 82 */ 83 public function __construct($pluginName, $pageId, $recipeName) { 84 $this->pluginName = $pluginName; 85 86 parent::__construct($pluginName, $pageId, $recipeName, ".rrd"); 87 } 88} 89 90/** 91 * This cache class manages the images generated by the plugin. 92 * The cached images are used as long as the recipe does not change and the maximum age (config) is not reached. 93 * 94 */ 95class cache_rrdgraphimage extends cache_rrdgraphbase { 96 /** @var Integer Maximum age of the image to be considered usable. */ 97 private $maxAge; 98 99 /** 100 * C'tor 101 * @param String $pluginName The name of the plugin. This can be retrieved by getPluginName() and makes the plugin more robust to renaming. 102 * @param String $pageId The wiki id of the page the cached content is on. 103 * @param String $extension The extension of the image file without a trailing dot. 104 * @param String $recipeName An identifier used to identify the cache recipe on the page provied by pageId. The identifier is hashed before being used. 105 * @param Integer $rangeNr ID of the time range this image is cached for. 106 * @param String $conditions An identifier for the conditions used for creating the image (fullscreen, etc.). 107 * @param Integer $maxAge Maximum age of the image in seconds. If the image is older than the given age, it is not used and must be recreated. 108 */ 109 public function __construct($pluginName, $pageId, $extension, $recipeName, $rangeNr, $conditions, $maxAge) { 110 $this->maxAge = $maxAge; 111 $extension = strtolower($extension); 112 113 parent::__construct($pluginName, $pageId, $recipeName . '/' . $conditions . '/' . $rangeNr, "." . $extension); 114 } 115 116 /** 117 * Determins the name of the file used for caching. This name can be used to pass it to other functions to update the content of the cache. 118 * @returns Returns the name and path of the cache file. 119 */ 120 public function getCacheFileName() { 121 return $this->cache; 122 } 123 124 /** 125 * (non-PHPdoc) 126 * @see cache_rrdgraphbase::_addDependencies() 127 */ 128 protected function _addDependencies() { 129 //-- Set maximum age. 130 $this->depends['age'] = $this->maxAge; 131 132 parent::_addDependencies(); 133 } 134 135 /** 136 * Returns the time until this image is valid. 137 * If the cache file does not exist (the data was never cached) 0 is returned. 138 * @return Integer Unix timestamp when this image is no longer valid. 139 */ 140 public function getValidUntil() { 141 if ($this->useCache()) { 142 return $this->_time + $this->maxAge; 143 } else { 144 return 0; 145 } 146 } 147 148 /** 149 * Determins the last modification time of the cache data. 150 * If the cache file does not exist (the data was never cached) the current time is returned. 151 * @return Integer Unix timestamp of the last modification time of the cached file. 152 */ 153 public function getLastModified() { 154 if (empty($this->_time)) 155 return time(); 156 else 157 return $this->_time; 158 } 159} 160 161/** 162 * Stores information about a rrd image. This information can be used to update the 163 * cache image file. To load the image file and to construct HTTP headers for tramsmission. 164 * 165 */ 166class rrdgraph_image_info { 167 /** @var String Name of the rrd image file within the cache. */ 168 private $fileName; 169 170 /** @var Resource File handle used to lock the file. */ 171 private $fileHandle; 172 173 /** @var Integer Timestamp until the file named by $fileName ist considered valid. */ 174 private $validUntil; 175 176 /** @var Integer Timestamp when the file named by $fileName was last updated. */ 177 private $lastModified; 178 179 /** 180 * C'tor 181 * @param String $fileName Sets the $fileName value. 182 * @param Integer $validUntil Sets the $validUntil value. 183 * @param Integer $lastModified Sets the $lastModfiied value. 184 */ 185 public function __construct($fileName, $validUntil, $lastModified) { 186 $this->fileName = $fileName; 187 $this->validUntil = $validUntil; 188 $this->lastModified = $lastModified; 189 190 //-- Get a shared lock on the lock-file. 191 $this->fileHandle = fopen($fileName . ".lock", "w+"); 192 flock($this->fileHandle, LOCK_SH); 193 } 194 195 /** 196 * D'tor 197 */ 198 public function __destruct() { 199 fclose($this->fileHandle); 200 } 201 202 /** 203 * @see cache_rrdgraphimage::getCacheFileName() 204 */ 205 public function getFileName() { 206 return $this->fileName; 207 } 208 209 /** 210 * @see cache_rrdgraphimage::getValidUntil() 211 */ 212 public function getValidUntil() { 213 return $this->validUntil; 214 } 215 216 /** 217 * @see cache_rrdgraphimage::getLastModified() 218 */ 219 public function getLastModified() { 220 return $this->lastModified; 221 } 222 223 /** 224 * Checks if the cached file returned by getFileName() is still valid. 225 * @return boolean Returns "true" if the cached file should still be used or "false" if it must be recreated. 226 */ 227 public function isValid() { 228 return $this->validUntil > time(); 229 } 230 231 public function upgradeLock() { 232 flock($this->fileHandle, LOCK_EX); 233 } 234} 235 236/** 237 * DokiWuki helper plugin class. This class supplies some methods used throughout the other RRDGraph plugin modules. 238 * 239 */ 240class helper_plugin_rrdgraph extends DokuWiki_Plugin { 241 /** @var string Mode for embedding the graph into a rendered HTML page. */ 242 const MODE_GRAPH_EMBEDDED = 'e'; 243 /** @var string Mode for showing the graph fullscreen. */ 244 const MODE_GRAPH_FULLSCREEN = 'fs'; 245 /** @var string Mode for generating a SVG image with data binding.. */ 246 const MODE_BINDSVG = 'b'; 247 248 /** @var Array Cache for already loaded and inflated recipes. This speeds up loading the same recipe multiple times on the same wiki page */ 249 private $localRecipeCache; 250 251 /** 252 * Returns an array of method declarations for docuwiki. 253 * @see https://www.dokuwiki.org/devel:helper_plugins 254 * @return Returns the declaration array. 255 */ 256 public function getMethods() { 257 //-- Non of the contained functions are for public use! 258 return array(); 259 } 260 261 /** 262 * Stores a rrd recipe for the given page. 263 * @param String $pageId Wiki page id. 264 * @param String $recipeName Name of the recipe to store. 265 * @param Array $recipeData Array of recipe data to be stored. 266 */ 267 public function storeRecipe($pageId, $recipeName, $recipeData) { 268 //-- Put the file into the cache. 269 $cache = new cache_rrdgraph($this->getPluginName(), $pageId, $recipeName); 270 $cache->storeCache(serialize($recipeData)); 271 272 $this->localRecipeCache[$pageId . "/" . $recipeName] = $recipeData; 273 } 274 275 /** 276 * Load a gieven rrd recipe. If the recipe is not available within the cache or needs to be updated the wiki page is rendered 277 * to give the syntax plugin a chance to create and cache the rrd data. 278 * @param String $pageId Wiki page id. 279 * @param String $recipeName Name of the recipe to load. 280 * @returns Array Returns an array containing an rrd recipe. If the recipe can not be found or recreated this method returns null. 281 */ 282 public function fetchRecipe($pageId, $recipeName) { 283 if (! isset($this->localRecipeCache[$pageId . "/" . $recipeName])) { 284 $cache = new cache_rrdgraph($this->getPluginName(), $pageId, $recipeName); 285 if ($cache->useCache()) { 286 $this->localRecipeCache[$pageId . "/" . $recipeName] = unserialize($cache->retrieveCache()); 287 } else { 288 //-- The rrd-information is not cached. Render the page 289 // to refresh the stored rrd information. 290 p_wiki_xhtml($pageId); 291 292 //-- Try again to get the data 293 $this->localRecipeCache[$pageId . "/" . $recipeName] = unserialize($cache->retrieveCache()); 294 } 295 } 296 297 if (empty($this->localRecipeCache[$pageId . "/" . $recipeName])) $this->localRecipeCache[$pageId . "/" . $recipeName] = null; 298 299 return $this->localRecipeCache[$pageId . "/" . $recipeName]; 300 } 301 302 /** 303 * Inflates a given recipe. 304 * When a recipe is inflated, included recipes are automatically loaded (and rendered if necessary) and included into the given recipe. 305 * @param Array $recipe A rrd recipe. If this value is not an array, null is returned. 306 * @return Array If the recipe could be successfully inflate, the recipe is returned with all includes replaced by the included elements. 307 * @throws Exception If an error occures (if the ACL does not allow loading an included recpipe) an exception is thrown. 308 */ 309 public function inflateRecipe($recipe) { 310 if (! is_array($recipe)) return null; 311 312 //-- Cache the setting if ACLs should be checked for includes. 313 $checkACL = ($this->getConf('include_acl') > 0); 314 315 //-- Resolve includes 316 $inflatedRecipe = array (); 317 $includeDone = false; 318 foreach ($recipe as $element) { 319 switch (strtoupper($element[1])) { 320 case 'INCLUDE' : 321 list ($incPageId, $incTmplName) = explode('>', $element[2], 2); 322 $incPageId = trim($incPageId); 323 $incTmplName = trim($incTmplName); 324 325 if ($checkACL) { 326 if (auth_quickaclcheck($incPageId) < AUTH_READ) throw new Exception("Access denied by ACL."); 327 } 328 329 $includedPageRecipe = $this->fetchRecipe($incPageId, $incTmplName); 330 if ($includedPageRecipe !== null) { 331 $inflatedRecipe = array_merge($inflatedRecipe, $includedPageRecipe); 332 } 333 break; 334 default : 335 $inflatedRecipe[] = $element; 336 } 337 } 338 339 $recipe = $inflatedRecipe; 340 341 return $recipe; 342 } 343 344 /** 345 * Parses a recipe and returns the wiki page ids of all included recipes. 346 * @param Array $recipe The rrd recipe to parse. 347 * @return Array A string array continaing all page ids included by the given recipe. 348 */ 349 public function getDependencies($recipe) { 350 $depPageIds = array (); 351 352 foreach ($recipe as $element) { 353 if (strcasecmp($element[1], 'INCLUDE') == 0) { 354 list ($incPageId, $incTmplName) = explode('>', $element[2], 2); 355 $incPageId = trim($incPageId); 356 357 $depPageIds[$incPageId] = $incPageId; 358 break; 359 } 360 } 361 362 return array_values($depPageIds); 363 } 364 365 /** 366 * Returns a rrdgraph_image_info instance contianing the information needed to deliver or recreate the given png rrd image. 367 * @param String $pageId The wiki id of the page the cached content is on. 368 * @param String $recipeName An identifier used to identify the cache recipe on the page provied by pageId. The identifier is hashed before being used. 369 * @param String $extension The extension of the image file without a trailing dot. 370 * @param Integer $rangeNr ID of the time range this image is cached for. 371 * @param String $conditions An identifier for the conditions used for creating the image (fullscreen, etc.). 372 */ 373 public function getImageCacheInfo($pageId, $recipeName, $extension, $rangeNr, $conditions) { 374 $cache = new cache_rrdgraphimage($this->getPluginName(), $pageId, $extension, $recipeName, $rangeNr, $conditions, $this->getConf('cache_timeout')); 375 376 return new rrdgraph_image_info($cache->getCacheFileName(), $cache->getValidUntil(), $cache->getLastModified()); 377 } 378 379 /** 380 * Sends the Graph specified by its parameters to the webbrowser. Make sure that after calling this function no 381 * other output is transmitted or the image will be corrupted. 382 * This function does cache management and all other stuff, too. 383 * @param string $pageId The wiki id of the page the rrd graph is defined in. 384 * @param string $graphId The identifier of the rrd graph to render and send. 385 * @param integer $rangeNr ID of the time range to send the graph for. 386 * @param string $mode Mode to use for generating the graph (MODE_GRAPH_EMBEDDED, MODE_GRAPH_FULLSCREEN, MODE_BINDSVG). 387 * @param string $bindingSource For MODE_BINDSVG the source SVG image for data binding must be specified as a DokuWiki media ressource. 388 * @throws Exception 389 * @return Ambigous <> 390 */ 391 public function sendRrdImage($pageId, $graphId, $rangeNr = 0, $mode = helper_plugin_rrdgraph::MODE_GRAPH_EMBEDDED, $bindingSource = null) 392 { 393 //-- User abort must be ignored because we're building new images for the cache. If the 394 // user aborts this process, the cache may be corrupted. 395 @ignore_user_abort(true); 396 397 try { 398 //-- ACL-Check 399 if (auth_quickaclcheck($pageId) < AUTH_READ) throw new Exception("Access denied by ACL."); 400 401 //-- Currently only fs, b and e are supported modes. 402 if (($mode != helper_plugin_rrdgraph::MODE_GRAPH_FULLSCREEN) && ($mode != helper_plugin_rrdgraph::MODE_BINDSVG)) $mode = helper_plugin_rrdgraph::MODE_GRAPH_EMBEDDED; 403 404 //-- If the mode is "b" then $bindingSource must be set and accessible 405 if ($mode == helper_plugin_rrdgraph::MODE_BINDSVG) { 406 if ($bindingSource == null) throw new Exception("Binding source missing."); 407 if (auth_quickaclcheck($bindingSource) < AUTH_READ) throw new Exception("Access denied by ACL."); 408 } 409 410 //-- Check if the cached image is still valid. If this is not the case, recreate it. 411 $cacheInfo = $this->getImageCacheInfo($pageId, $graphId, ($mode == helper_plugin_rrdgraph::MODE_BINDSVG)?"svg":"png", $rangeNr, $mode); 412 if (! $cacheInfo->isValid()) { 413 414 //-- We found we should update the file. Upgrade our lock to an exclusive one. 415 // This way we OWN the lockfile and nobody else can get confused while we do our thing. 416 $cacheInfo->upgradeLock(); 417 418 $recipe = $this->fetchRecipe($pageId, $graphId); 419 if ($recipe === null) throw new Exception("The graph " . $graphId . " is not defined on page " . $pageId); 420 421 $recipe = $this->inflateRecipe($recipe); 422 if ($recipe === null) throw new Exception("Inflating the graph " . $graphId . " on page " . $pageId . " failed."); 423 424 //-- Initialize the RPN-Computer for conditions 425 $rpncomp = new RPNComputer(); 426 $rpncomp->addConst("true", true); 427 $rpncomp->addConst("false", false); 428 $rpncomp->addConst("fullscreen", $mode == helper_plugin_rrdgraph::MODE_GRAPH_FULLSCREEN); 429 $rpncomp->addConst("range", $rangeNr); 430 $rpncomp->addConst("page", $pageId); 431 432 $options = array (); 433 $graphCommands = array (); 434 $ranges = array (); 435 $variables = array(); 436 if ($mode == helper_plugin_rrdgraph::MODE_BINDSVG) $svgBinding = new SvgBinding(); 437 foreach ($recipe as $element) { 438 439 //-- If a condition was supplied, check it. 440 if ((! empty($element[0])) && (! ($rpncomp->compute($element[0])))) { 441 continue; 442 } 443 444 //-- Replace variable references with values 445 if (! empty($element[2])) { 446 $element[2] = preg_replace_callback("/{{([^}]+)}}/", function ($match) use ($variables) { 447 if (array_key_exists($match[1], $variables)) { 448 return $variables[$match[1]]; 449 } else { 450 throw new Exception('Variable "' . $match[1] . '" not set.'); 451 } 452 }, $element[2]); 453 } 454 455 //-- Process the special options and pass the rest on to rrdtool. 456 switch (strtoupper($element[1])) { 457 //-- RANGE:[Range Name]:[Start time]:[End time] 458 case 'RANGE' : 459 if (($mode == helper_plugin_rrdgraph::MODE_BINDSVG) && (count($ranges) == 1)) throw new Exception("For SVG binding only one RANGE can be specified."); 460 $parts = explode(':', $element[2], 3); 461 if (count($parts) == 3) $ranges[] = $parts; 462 break; 463 464 //-- SET:[Variable name]=[Veriable value] 465 case 'SET' : 466 $parts = explode('=', $element[2], 2); 467 $key = trim($parts[0]); 468 $value = trim($parts[1]); 469 470 $variables[$key]=$value; 471 break; 472 473 //-- OPT:[Option]=[Optional value] 474 case 'OPT' : 475 $parts = explode('=', $element[2], 2); 476 $key = trim($parts[0]); 477 $value = trim($parts[1]); 478 479 if (strlen($value) == 0) 480 $options[$key] = null; 481 else 482 $options[$key] = $value; 483 484 break; 485 486 //-- BDEF:[Binding]=[Variable]:[Aggregation function] 487 case 'BDEF': 488 if ($mode != helper_plugin_rrdgraph::MODE_BINDSVG) throw new Exception("BDEF only allowed if the recipe is used for binding."); 489 $parts = explode('=', $element[2], 2); 490 if (count($parts) != 2) throw new Exception("BDEF is missing r-value."); 491 $rparts = explode(':', $parts[1], 2); 492 if (count($rparts) != 2) throw new Exception("BDEF is missing aggregation function"); 493 $binding = $parts[0]; 494 $variable = $rparts[0]; 495 $aggFkt = $rparts[1]; 496 497 //-- Put the binding into the list of the SvgBinding class and output an XPORT command 498 // for RRDtool to export the used variable. 499 $svgBinding->setAggregate($binding, $aggFkt); 500 $graphCommands[] = "XPORT:" . $variable . ':' . $binding; 501 502 break; 503 504 //-- The XPORT-Keyword is not allowed. 505 case 'XPORT': 506 throw new Exception("The XPORT statement must no be used. Use BDEF instead."); 507 break; 508 509 //-- INCLUDE:[Wiki Page]>[Template] 510 case 'INCLUDE' : 511 throw new Exception("Recursive inclusion detected. Only graphs can contain inclusions."); 512 break; 513 514 default : 515 $graphCommands[] = $element[1] . ":" . $element[2]; 516 break; 517 } 518 } 519 520 //-- Bounds-Check for Ranges 521 if (count($ranges) == 0) throw new Exception("No time ranges defined for this graph."); 522 if (($rangeNr < 0) || ($rangeNr >= count($ranges))) $rangeNr = 0; 523 524 //-- The following options are not allowed because they disturbe the function of the plugin. 525 // They are filtered. 526 $badOptions = array ( 527 'a', 528 'imgformat', 529 'lazy', 530 'z' 531 ); 532 533 $options = array_diff_key($options, array_flip($badOptions)); 534 535 //-- Set/overwrite some of the options 536 $options['start'] = $ranges[$rangeNr][1]; 537 $options['end'] = $ranges[$rangeNr][2]; 538 539 //-- If we're not only doing SVG-Binding some more defaults have to be set. 540 if ($mode != helper_plugin_rrdgraph::MODE_BINDSVG) 541 { 542 $options['imgformat'] = 'PNG'; 543 $options['999color'] = "SHADEA#C0C0C0"; 544 $options['998color'] = "SHADEB#C0C0C0"; 545 $options['border'] = 1; 546 } 547 548 //-- Encode the options 549 $commandLine = array (); 550 foreach ($options as $option => $value) { 551 $option = ltrim($option, "0123456789"); 552 if (strlen($option) == 1) 553 $dashes = '-'; 554 else 555 $dashes = '--'; 556 557 $commandLine[] = $dashes . $option; 558 559 if ($value != null) { 560 $value = trim($value, " \"\t\r\n"); 561 $commandLine[] .= $value; 562 } 563 } 564 565 //-- Correct the filename of the graph in case the rangeNr was modified by the range check. 566 unset($cacheInfo); 567 $cacheInfo = $this->getImageCacheInfo($pageId, $graphId, ($mode == helper_plugin_rrdgraph::MODE_BINDSVG)?"svg":"png", $rangeNr, $mode); 568 569 //-- We've to reupgrade the lock, because we got a new cacheInfo instance. 570 $cacheInfo->UpgradeLock(); 571 572 //-- Depending on the current mode create a new PNG or SVG image. 573 switch ($mode) { 574 case helper_plugin_rrdgraph::MODE_GRAPH_EMBEDDED: 575 case helper_plugin_rrdgraph::MODE_GRAPH_FULLSCREEN: 576 //-- Render the RRD-Graph 577 if (rrd_graph($cacheInfo->getFilename(), array_merge($commandLine, $graphCommands)) === false) throw new Exception(rrd_error()); 578 break; 579 580 case helper_plugin_rrdgraph::MODE_BINDSVG: 581 $bindingSourceFile = mediaFN(cleanID($bindingSource)); 582 $svgBinding->createSVG($cacheInfo->getFileName(), array_merge($commandLine, $graphCommands), $bindingSourceFile); 583 break; 584 } 585 586 //-- Get the new cache info of the image to send the correct headers. 587 unset($cacheInfo); 588 $cacheInfo = $this->getImageCacheInfo($pageId, $graphId, ($mode == helper_plugin_rrdgraph::MODE_BINDSVG)?"svg":"png", $rangeNr, $mode); 589 } 590 591 if (is_file($cacheInfo->getFilename())) { 592 //-- Output the image. The content length is determined via the output buffering because 593 // on newly generated images (and with the cache on some non standard filesystem) the 594 // size given by filesize is incorrect 595 $contentType = ContentType::get_content_type($cacheInfo->getFilename()); 596 if ($contentType === null) throw new Exception("Unexpected file extension."); 597 header("Content-Type: " . $contentType); 598 599 header('Expires: ' . gmdate('D, d M Y H:i:s', $cacheInfo->getValidUntil()) . ' GMT'); 600 header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $cacheInfo->getLastModified()) . ' GMT'); 601 602 ob_start(); 603 readfile($cacheInfo->getFilename()); 604 header("Content-Length: " . ob_get_length()); 605 ob_end_flush(); 606 } else { 607 ErrorImage::outputErrorImage("File not found", $cacheInfo->getFilename()); 608 } 609 } 610 catch (Exception $ex) { 611 ErrorImage::outputErrorImage("Graph generation failed", $ex->getMessage()); 612 } 613 614 if (isset($cacheInfo)) unset($cacheInfo); 615 } 616}