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