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