1<?php
2
3namespace dokuwiki;
4
5/**
6 * Class StyleUtils
7 *
8 * Reads and applies the template's style.ini settings
9 */
10class StyleUtils
11{
12
13    /** @var string current template */
14    protected $tpl;
15    /** @var bool reinitialize styles config */
16    protected $reinit;
17    /** @var bool $preview preview mode */
18    protected $preview;
19    /** @var array default replacements to be merged with custom style configs */
20    protected $defaultReplacements = array(
21        '__text__' => "#000",
22        '__background__' => "#fff",
23        '__text_alt__' => "#999",
24        '__background_alt__' => "#eee",
25        '__text_neu__' => "#666",
26        '__background_neu__' => "#ddd",
27        '__border__' => "#ccc",
28        '__highlight__' => "#ff9",
29        '__link__' => "#00f",
30    );
31
32    /**
33     * StyleUtils constructor.
34     * @param string $tpl template name: if not passed as argument, the default value from $conf will be used
35     * @param bool $preview
36     * @param bool $reinit whether static style conf should be reinitialized
37     */
38    public function __construct($tpl = '', $preview = false, $reinit = false)
39    {
40        if (!$tpl) {
41            global $conf;
42            $tpl = $conf['template'];
43        }
44        $this->tpl = $tpl;
45        $this->reinit = $reinit;
46        $this->preview = $preview;
47    }
48
49    /**
50     * Load style ini contents
51     *
52     * Loads and merges style.ini files from template and config and prepares
53     * the stylesheet modes
54     *
55     * @author Andreas Gohr <andi@splitbrain.org>
56     * @author Anna Dabrowska <info@cosmocode.de>
57     *
58     * @return array with keys 'stylesheets' and 'replacements'
59     */
60    public function cssStyleini()
61    {
62        static $combined = [];
63        if (!empty($combined) && !$this->reinit) {
64            return $combined;
65        }
66
67        global $conf;
68        global $config_cascade;
69        $stylesheets = array(); // mode, file => base
70
71        // guaranteed placeholder => value
72        $replacements = $this->defaultReplacements;
73
74        // merge all styles from config cascade
75        if (!is_array($config_cascade['styleini'])) {
76            trigger_error('Missing config cascade for styleini', E_USER_WARNING);
77        }
78
79        // allow replacement overwrites in preview mode
80        if ($this->preview) {
81            $config_cascade['styleini']['local'][] = $conf['cachedir'] . '/preview.ini';
82        }
83
84        $combined['stylesheets'] = [];
85        $combined['replacements'] = [];
86
87        foreach (array('default', 'local', 'protected') as $config_group) {
88            if (empty($config_cascade['styleini'][$config_group])) continue;
89
90            // set proper server dirs
91            $webbase = $this->getWebbase($config_group);
92
93            foreach ($config_cascade['styleini'][$config_group] as $inifile) {
94                // replace the placeholder with the name of the current template
95                $inifile = str_replace('%TEMPLATE%', $this->tpl, $inifile);
96
97                $incbase = dirname($inifile) . '/';
98
99                if (file_exists($inifile)) {
100                    $config = parse_ini_file($inifile, true);
101
102                    if (isset($config['stylesheets']) && is_array($config['stylesheets'])) {
103                        foreach ($config['stylesheets'] as $inifile => $mode) {
104                            // validate and include style files
105                            $stylesheets = array_merge(
106                                $stylesheets,
107                                $this->getValidatedStyles($stylesheets, $inifile, $mode, $incbase, $webbase)
108                            );
109                            $combined['stylesheets'] = array_merge($combined['stylesheets'], $stylesheets);
110                        }
111                    }
112
113                    if (isset($config['replacements']) && is_array($config['replacements'])) {
114                        $replacements = array_replace(
115                            $replacements,
116                            $this->cssFixreplacementurls($config['replacements'], $webbase)
117                        );
118                        $combined['replacements'] = array_merge($combined['replacements'], $replacements);
119                    }
120                }
121            }
122        }
123
124        return $combined;
125    }
126
127    /**
128     * Checks if configured style files exist and, if necessary, adjusts file extensions in config
129     *
130     * @param array $stylesheets
131     * @param string $file
132     * @param string $mode
133     * @param string $incbase
134     * @param string $webbase
135     * @return mixed
136     */
137    protected function getValidatedStyles($stylesheets, $file, $mode, $incbase, $webbase)
138    {
139        global $conf;
140        if (!file_exists($incbase . $file)) {
141            list($extension, $basename) = array_map('strrev', sexplode('.', strrev($file), 2, ''));
142            $newExtension = $extension === 'css' ? 'less' : 'css';
143            if (file_exists($incbase . $basename . '.' . $newExtension)) {
144                $stylesheets[$mode][$incbase . $basename . '.' . $newExtension] = $webbase;
145                if ($conf['allowdebug']) {
146                    msg("Stylesheet $file not found, using $basename.$newExtension instead. " .
147                        "Please contact developer of \"$this->tpl\" template.", 2);
148                }
149            } elseif ($conf['allowdebug']) {
150                msg("Stylesheet $file not found, please contact the developer of \"$this->tpl\" template.", 2);
151            }
152        }
153        $stylesheets[$mode][fullpath($incbase . $file)] = $webbase;
154        return $stylesheets;
155    }
156
157    /**
158     * Returns the web base path for the given level/group in config cascade.
159     * Style resources are relative to the template directory for the main (default) styles
160     * but relative to DOKU_BASE for everything else"
161     *
162     * @param string $config_group
163     * @return string
164     */
165    protected function getWebbase($config_group)
166    {
167        if ($config_group === 'default') {
168            return tpl_basedir($this->tpl);
169        } else {
170            return DOKU_BASE;
171        }
172    }
173
174    /**
175     * Amend paths used in replacement relative urls, refer FS#2879
176     *
177     * @author Chris Smith <chris@jalakai.co.uk>
178     *
179     * @param array $replacements with key-value pairs
180     * @param string $location
181     * @return array
182     */
183    protected function cssFixreplacementurls($replacements, $location)
184    {
185        foreach ($replacements as $key => $value) {
186            $replacements[$key] = preg_replace(
187                '#(url\([ \'"]*)(?!/|data:|http://|https://| |\'|")#',
188                '\\1' . $location,
189                $value
190            );
191        }
192        return $replacements;
193    }
194}
195