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