1<?php 2 3namespace dokuwiki\template\sprintdoc; 4 5if(!defined('DOKU_INC')) define('DOKU_INC', dirname(__FILE__) . '/../../../'); 6require_once(DOKU_INC . 'inc/init.php'); 7 8/** 9 * Custom XML node that allows prepending 10 */ 11class SvgNode extends \SimpleXMLElement { 12 /** 13 * @param string $name Name of the new node 14 * @param null|string $value 15 * @return SvgNode 16 */ 17 public function prependChild($name, $value = null) { 18 $dom = dom_import_simplexml($this); 19 20 $new = $dom->insertBefore( 21 $dom->ownerDocument->createElement($name, $value), 22 $dom->firstChild 23 ); 24 25 return simplexml_import_dom($new, get_class($this)); 26 } 27 28 /** 29 * @param \SimpleXMLElement $node the node to be added 30 * @return \SimpleXMLElement 31 */ 32 public function appendNode(\SimpleXMLElement $node) { 33 $dom = dom_import_simplexml($this); 34 $domNode = dom_import_simplexml($node); 35 36 $newNode = $dom->appendChild($domNode); 37 return simplexml_import_dom($newNode, get_class($this)); 38 } 39 40 /** 41 * @param \SimpleXMLElement $node the child to remove 42 * @return \SimpleXMLElement 43 */ 44 public function removeChild(\SimpleXMLElement $node) { 45 $dom = dom_import_simplexml($node); 46 $dom->parentNode->removeChild($dom); 47 return $node; 48 } 49 50 /** 51 * Wraps all elements of $this in a `<g>` tag 52 * 53 * @return SvgNode 54 */ 55 public function groupChildren() { 56 $dom = dom_import_simplexml($this); 57 58 $g = $dom->ownerDocument->createElement('g'); 59 while($dom->childNodes->length > 0) { 60 $child = $dom->childNodes->item(0); 61 $dom->removeChild($child); 62 $g->appendChild($child); 63 } 64 $g = $dom->appendChild($g); 65 66 return simplexml_import_dom($g, get_class($this)); 67 } 68 69 /** 70 * Add new style definitions to this element 71 * @param string $style 72 */ 73 public function addStyle($style) { 74 $defs = $this->defs; 75 if(!$defs) { 76 $defs = $this->prependChild('defs'); 77 } 78 $defs->addChild('style', $style); 79 } 80} 81 82/** 83 * Manage SVG recoloring 84 */ 85class SVG { 86 87 const IMGDIR = __DIR__ . '/img/'; 88 const BACKGROUNDCLASS = 'sprintdoc-background'; 89 const CDNBASE = 'https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/'; 90 91 protected $file; 92 protected $replacements; 93 94 /** 95 * SVG constructor 96 */ 97 public function __construct() { 98 global $INPUT; 99 100 $svg = cleanID($INPUT->str('svg')); 101 if(blank($svg)) $this->abort(404); 102 103 // try local file first 104 $file = self::IMGDIR . $svg; 105 if(!file_exists($file)) { 106 // try media file 107 $file = mediaFN($svg); 108 if(file_exists($file)) { 109 // media files are ACL protected 110 if(auth_quickaclcheck($svg) < AUTH_READ) $this->abort(403); 111 } else { 112 // get it from material design icons 113 $file = getCacheName($svg, '.svg'); 114 if (!file_exists($file)) { 115 io_download(self::CDNBASE . $svg, $file); 116 } 117 } 118 119 } 120 // check if media exists 121 if(!file_exists($file)) $this->abort(404); 122 123 $this->file = $file; 124 } 125 126 /** 127 * Generate and output 128 */ 129 public function out() { 130 global $conf; 131 $file = $this->file; 132 $params = $this->getParameters(); 133 134 header('Content-Type: image/svg+xml'); 135 $cachekey = md5($file . serialize($params) . $conf['template'] . filemtime(__FILE__)); 136 $cache = new \dokuwiki\Cache\Cache($cachekey, '.svg'); 137 $cache->setEvent('SVG_CACHE'); 138 139 http_cached($cache->cache, $cache->useCache(array('files' => array($file, __FILE__)))); 140 if($params['e']) { 141 $content = $this->embedSVG($file); 142 } else { 143 $content = $this->generateSVG($file, $params); 144 } 145 http_cached_finish($cache->cache, $content); 146 } 147 148 /** 149 * Generate a new SVG based on the input file and the parameters 150 * 151 * @param string $file the SVG file to load 152 * @param array $params the parameters as returned by getParameters() 153 * @return string the new XML contents 154 */ 155 protected function generateSVG($file, $params) { 156 /** @var SvgNode $xml */ 157 $xml = simplexml_load_file($file, SvgNode::class); 158 $xml->addStyle($this->makeStyle($params)); 159 $this->createBackground($xml); 160 $xml->groupChildren(); 161 162 return $xml->asXML(); 163 } 164 165 /** 166 * Return the absolute minimum path definition for direct embedding 167 * 168 * No styles will be applied. They have to be done in CSS 169 * 170 * @param string $file the SVG file to load 171 * @return string the new XML contents 172 */ 173 protected function embedSVG($file) { 174 /** @var SvgNode $xml */ 175 $xml = simplexml_load_file($file, SvgNode::class); 176 177 $def = hsc((string) $xml->path['d']); 178 $w = hsc($xml['width'] ?? '100%'); 179 $h = hsc($xml['height'] ?? '100%'); 180 $v = hsc($xml['viewBox']); 181 182 // if viewbox is not defined, construct it from width and height, if available 183 if (empty($v) && !empty($w) && !empty($h)) { 184 $v = hsc("0 0 $w $h"); 185 } 186 187 return "<svg viewBox=\"$v\"><path d=\"$def\" /></svg>"; 188 } 189 190 /** 191 * Get the supported parameters from request 192 * 193 * @return array 194 */ 195 protected function getParameters() { 196 global $INPUT; 197 198 $params = array( 199 'e' => $INPUT->bool('e', false), 200 's' => $this->fixColor($INPUT->str('s')), 201 'f' => $this->fixColor($INPUT->str('f')), 202 'b' => $this->fixColor($INPUT->str('b')), 203 'sh' => $this->fixColor($INPUT->str('sh')), 204 'fh' => $this->fixColor($INPUT->str('fh')), 205 'bh' => $this->fixColor($INPUT->str('bh')), 206 ); 207 208 return $params; 209 } 210 211 /** 212 * Generate a style setting from the input variables 213 * 214 * @param array $params associative array with the given parameters 215 * @return string 216 */ 217 protected function makeStyle($params) { 218 $element = 'path'; // FIXME configurable? 219 220 if(empty($params['b'])) { 221 $params['b'] = $this->fixColor('00000000'); 222 } 223 224 $style = 'g rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['b'] . ';}'; 225 226 if($params['bh']) { 227 $style .= 'g:hover rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['bh'] . ';}'; 228 } 229 230 if($params['s'] || $params['f']) { 231 $style .= 'g ' . $element . '{'; 232 if($params['s']) $style .= 'stroke:' . $params['s'] . ';'; 233 if($params['f']) $style .= 'fill:' . $params['f'] . ';'; 234 $style .= '}'; 235 } 236 237 if($params['sh'] || $params['fh']) { 238 $style .= 'g:hover ' . $element . '{'; 239 if($params['sh']) $style .= 'stroke:' . $params['sh'] . ';'; 240 if($params['fh']) $style .= 'fill:' . $params['fh'] . ';'; 241 $style .= '}'; 242 } 243 244 return $style; 245 } 246 247 /** 248 * Takes a hexadecimal color string in the following forms: 249 * 250 * RGB 251 * RRGGBB 252 * RRGGBBAA 253 * 254 * Converts it to rgba() form. 255 * 256 * Alternatively takes a replacement name from the current template's style.ini 257 * 258 * @param string $color 259 * @return string 260 */ 261 protected function fixColor($color) { 262 if($color === '') return ''; 263 if(preg_match('/^([0-9a-f])([0-9a-f])([0-9a-f])$/i', $color, $m)) { 264 $r = hexdec($m[1] . $m[1]); 265 $g = hexdec($m[2] . $m[2]); 266 $b = hexdec($m[3] . $m[3]); 267 $a = hexdec('ff'); 268 } elseif(preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i', $color, $m)) { 269 $r = hexdec($m[1]); 270 $g = hexdec($m[2]); 271 $b = hexdec($m[3]); 272 if(isset($m[4])) { 273 $a = hexdec($m[4]); 274 } else { 275 $a = hexdec('ff'); 276 } 277 } else { 278 if(is_null($this->replacements)) $this->initReplacements(); 279 if(isset($this->replacements[$color])) { 280 return $this->replacements[$color]; 281 } 282 if(isset($this->replacements['__' . $color . '__'])) { 283 return $this->replacements['__' . $color . '__']; 284 } 285 return ''; 286 } 287 288 return "rgba($r,$g,$b,$a)"; 289 } 290 291 /** 292 * sets a rectangular background of the size of the svg/this itself 293 * 294 * @param SvgNode $g 295 * @return SvgNode 296 */ 297 protected function createBackground(SvgNode $g) { 298 $rect = $g->prependChild('rect'); 299 $rect->addAttribute('class', self::BACKGROUNDCLASS); 300 301 $rect->addAttribute('x', '0'); 302 $rect->addAttribute('y', '0'); 303 $rect->addAttribute('height', '100%'); 304 $rect->addAttribute('width', '100%'); 305 return $rect; 306 } 307 308 /** 309 * Abort processing with given status code 310 * 311 * @param int $status 312 */ 313 protected function abort($status) { 314 http_status($status); 315 exit; 316 } 317 318 /** 319 * Initialize the available replacement patterns 320 * 321 * Loads the style.ini from the template (and various local locations) 322 * via a core function only available through some hack. 323 */ 324 protected function initReplacements() { 325 global $conf; 326 if (!class_exists('\dokuwiki\StyleUtils')) { 327 // Pre-Greebo Compatibility 328 329 define('SIMPLE_TEST', 1); // hacky shit 330 include DOKU_INC . 'lib/exe/css.php'; 331 $ini = css_styleini($conf['template']); 332 $this->replacements = $ini['replacements']; 333 return; 334 } 335 336 $stuleUtils = new \dokuwiki\StyleUtils(); 337 $ini = $stuleUtils->cssStyleini('sprintdoc'); 338 $this->replacements = $ini['replacements']; 339 } 340} 341 342// main 343$svg = new SVG(); 344$svg->out(); 345 346 347