1<?php
2
3/**
4 * Core Manager for the Farm functionality
5 *
6 * This class is initialized before any other DokuWiki code runs. Therefore it is
7 * completely selfcontained and does not use any of DokuWiki's utility functions.
8 *
9 * It's registered as a global $FARMCORE variable but you should not interact with
10 * it directly. Instead use the Farmer plugin's helper component.
11 *
12 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
13 * @author  Andreas Gohr <gohr@cosmocode.de>
14 */
15class DokuWikiFarmCore {
16    /**
17     * @var array The default config - changed by loadConfig
18     */
19    protected $config = array(
20        'base' => array(
21            'farmdir' => '',
22            'farmhost' => '',
23            'basedomain' => '',
24        ),
25        'notfound' => array(
26            'show' => 'farmer',
27            'url' => ''
28        ),
29        'inherit' => array(
30            'main' => 1,
31            'acronyms' => 1,
32            'entities' => 1,
33            'interwiki' => 1,
34            'license' => 1,
35            'mime' => 1,
36            'scheme' => 1,
37            'smileys' => 1,
38            'wordblock' => 1,
39            'users' => 0,
40            'plugins' => 0,
41            'userstyle' => 0,
42            'userscript' => 0,
43            'styleini' => 0
44        )
45    );
46
47    /** @var string|false The current animal, false for farmer */
48    protected $animal = false;
49    /** @var bool true if an animal was requested but was not found */
50    protected $notfound = false;
51    /** @var bool true if the current animal was requested by host */
52    protected $hostbased = false;
53
54    /**
55     * DokuWikiFarmCore constructor.
56     *
57     * This initializes the whole farm by loading the configuration and setting
58     * DOKU_CONF depending on the requested animal
59     */
60    public function __construct() {
61        $this->loadConfig();
62        if($this->config['base']['farmdir'] === '') return; // farm setup not complete
63        $this->config['base']['farmdir'] = rtrim($this->config['base']['farmdir'], '/').'/'; // trailing slash always
64        define('DOKU_FARMDIR', $this->config['base']['farmdir']);
65
66        // animal?
67        $this->detectAnimal();
68
69        // setup defines
70        define('DOKU_FARM_ANIMAL', $this->animal);
71        if($this->animal) {
72            define('DOKU_CONF', DOKU_FARMDIR . $this->animal . '/conf/');
73        } else {
74            define('DOKU_CONF', DOKU_INC . '/conf/');
75        }
76
77        $this->setupCascade();
78        $this->adjustCascade();
79    }
80
81    /**
82     * @return array the current farm configuration
83     */
84    public function getConfig() {
85        return $this->config;
86    }
87
88    /**
89     * @return false|string
90     */
91    public function getAnimal() {
92        return $this->animal;
93    }
94
95    /**
96     * @return boolean
97     */
98    public function isHostbased() {
99        return $this->hostbased;
100    }
101
102    /**
103     * @return boolean
104     */
105    public function wasNotfound() {
106        return $this->notfound;
107    }
108
109    /**
110     * @return string
111     */
112    public function getAnimalDataDir() {
113        return DOKU_FARMDIR . $this->getAnimal() . '/data/';
114    }
115
116    /**
117     * @return string
118     */
119    public function getAnimalBaseDir() {
120        if($this->isHostbased()) return '/';
121        return getBaseURL() . '!' . $this->getAnimal();
122    }
123
124    /**
125     * Detect the current animal
126     *
127     * Sets internal members $animal, $notfound and $hostbased
128     *
129     * This borrows form DokuWiki's inc/farm.php but does not support a default conf dir
130     */
131    protected function detectAnimal() {
132        $farmdir = $this->config['base']['farmdir'];
133        $farmhost = $this->config['base']['farmhost'];
134
135        // check if animal was set via rewrite parameter
136        $animal = '';
137        if (isset($_GET['animal'])) {
138            $animal = $_GET['animal'];
139            // now unset the parameter to not leak into new queries
140            unset($_GET['animal']);
141            $params = [];
142            parse_str($_SERVER['QUERY_STRING'], $params);
143            if (isset($params['animal'])) unset($params['animal']);
144            $_SERVER['QUERY_STRING'] = http_build_query($params);
145        }
146        // get animal from CLI parameter
147        if('cli' == php_sapi_name() && isset($_SERVER['animal'])) $animal = $_SERVER['animal'];
148        if($animal) {
149            // check that $animal is a string and just a directory name and not a path
150            if(!is_string($animal) || strpbrk($animal, '\\/') !== false) {
151                $this->notfound = true;
152                return;
153            };
154            $animal = strtolower($animal);
155
156            // check if animal exists
157            if(is_dir("$farmdir/$animal/conf")) {
158                $this->animal = $animal;
159                return;
160            } else {
161                $this->notfound = true;
162                return;
163            }
164        }
165
166        // no host - no host based setup. if we're still here then it's the farmer
167        if(!isset($_SERVER['HTTP_HOST'])) return;
168
169        // is this the farmer?
170        if(strtolower($_SERVER['HTTP_HOST']) == $farmhost) {
171            return;
172        }
173
174        // still here? check for host based
175        $this->hostbased = true;
176        $possible = $this->getAnimalNamesForHost($_SERVER['HTTP_HOST']);
177        foreach($possible as $animal) {
178            if(is_dir("$farmdir/$animal/conf/")) {
179                $this->animal = $animal;
180                return;
181            }
182        }
183
184        // no hit
185        $this->notfound = true;
186        return;
187    }
188
189    /**
190     * Return a list of possible animal names for the given host
191     *
192     * @param string $host the HTTP_HOST header
193     * @return array
194     */
195    protected function getAnimalNamesForHost($host) {
196        $animals = array();
197        $parts = explode('.', implode('.', explode(':', rtrim($host, '.'))));
198        for($j = count($parts); $j > 0; $j--) {
199            // strip from the end
200            $animals[] = implode('.', array_slice($parts, 0, $j));
201            // strip from the end without host part
202            $animals[] = implode('.', array_slice($parts, 1, $j));
203        }
204        $animals = array_unique($animals);
205        $animals = array_filter($animals);
206        usort(
207            $animals,
208            // compare by length, then alphabet
209            function ($a, $b) {
210                $ret = strlen($b) - strlen($a);
211                if($ret != 0) return $ret;
212                return $a > $b;
213            }
214        );
215        return $animals;
216    }
217
218    /**
219     * This sets up the default farming config cascade
220     */
221    protected function setupCascade() {
222        global $config_cascade;
223        $config_cascade = array(
224            'main' => array(
225                'default' => array(DOKU_INC . 'conf/dokuwiki.php',),
226                'local' => array(DOKU_CONF . 'local.php',),
227                'protected' => array(DOKU_CONF . 'local.protected.php',),
228            ),
229            'acronyms' => array(
230                'default' => array(DOKU_INC . 'conf/acronyms.conf',),
231                'local' => array(DOKU_CONF . 'acronyms.local.conf',),
232            ),
233            'entities' => array(
234                'default' => array(DOKU_INC . 'conf/entities.conf',),
235                'local' => array(DOKU_CONF . 'entities.local.conf',),
236            ),
237            'interwiki' => array(
238                'default' => array(DOKU_INC . 'conf/interwiki.conf',),
239                'local' => array(DOKU_CONF . 'interwiki.local.conf',),
240            ),
241            'license' => array(
242                'default' => array(DOKU_INC . 'conf/license.php',),
243                'local' => array(DOKU_CONF . 'license.local.php',),
244            ),
245            'manifest' => array(
246                'default' => array(DOKU_INC . 'conf/manifest.json',),
247                'local' => array(DOKU_CONF . 'manifest.local.json',),
248            ),
249            'mediameta' => array(
250                'default' => array(DOKU_INC . 'conf/mediameta.php',),
251                'local' => array(DOKU_CONF . 'mediameta.local.php',),
252            ),
253            'mime' => array(
254                'default' => array(DOKU_INC . 'conf/mime.conf',),
255                'local' => array(DOKU_CONF . 'mime.local.conf',),
256            ),
257            'scheme' => array(
258                'default' => array(DOKU_INC . 'conf/scheme.conf',),
259                'local' => array(DOKU_CONF . 'scheme.local.conf',),
260            ),
261            'smileys' => array(
262                'default' => array(DOKU_INC . 'conf/smileys.conf',),
263                'local' => array(DOKU_CONF . 'smileys.local.conf',),
264            ),
265            'wordblock' => array(
266                'default' => array(DOKU_INC . 'conf/wordblock.conf',),
267                'local' => array(DOKU_CONF . 'wordblock.local.conf',),
268            ),
269            'acl' => array(
270                'default' => DOKU_CONF . 'acl.auth.php',
271            ),
272            'plainauth.users' => array(
273                'default' => DOKU_CONF . 'users.auth.php',
274            ),
275            'plugins' => array(
276                'default' => array(DOKU_INC . 'conf/plugins.php',),
277                'local' => array(DOKU_CONF . 'plugins.local.php',),
278                'protected' => array(
279                    DOKU_INC . 'conf/plugins.required.php',
280                    DOKU_CONF . 'plugins.protected.php',
281                ),
282            ),
283            'userstyle' => array(
284                'screen' => array(DOKU_CONF . 'userstyle.css', DOKU_CONF . 'userstyle.less',),
285                'print' => array(DOKU_CONF . 'userprint.css', DOKU_CONF . 'userprint.less',),
286                'feed' => array(DOKU_CONF . 'userfeed.css', DOKU_CONF . 'userfeed.less',),
287                'all' => array(DOKU_CONF . 'userall.css', DOKU_CONF . 'userall.less',),
288            ),
289            'userscript' => array(
290                'default' => array(DOKU_CONF . 'userscript.js',),
291            ),
292            'styleini' => array(
293                'default'   => array(DOKU_INC . 'lib/tpl/%TEMPLATE%/' . 'style.ini'),
294                'local'     => array(DOKU_CONF . 'tpl/%TEMPLATE%/' . 'style.ini')
295            ),
296        );
297    }
298
299    /**
300     * This adds additional files to the config cascade based on the inheritence settings
301     *
302     * These are only added for animals, not the farmer
303     */
304    protected function adjustCascade() {
305        // nothing to do when on the farmer:
306        if(!$this->animal) return;
307
308        global $config_cascade;
309        foreach($this->config['inherit'] as $key => $val) {
310            if(!$val) continue;
311
312            // prepare what is to append or prepend
313            $append = array();
314            $prepend = array();
315            if($key == 'main') {
316                $append = array(
317                    'default' => array(DOKU_INC . 'conf/local.php'),
318                    'protected' => array(DOKU_INC . 'lib/plugins/farmer/includes/config.php')
319                );
320            } elseif($key == 'license') {
321                $append = array('default' => array(DOKU_INC . 'conf/' . $key . '.local.php'));
322            } elseif($key == 'userscript') {
323                $prepend = array('default' => array(DOKU_INC . 'conf/userscript.js'));
324            } elseif($key == 'userstyle') {
325                $prepend = array(
326                    'screen' => array(DOKU_INC . 'conf/userstyle.css', DOKU_INC . 'conf/userstyle.less',),
327                    'print' => array(DOKU_INC . 'conf/userprint.css', DOKU_INC . 'conf/userprint.less',),
328                    'feed' => array(DOKU_INC . 'conf/userfeed.css', DOKU_INC . 'conf/userfeed.less',),
329                    'all' => array(DOKU_INC . 'conf/userall.css', DOKU_INC . 'conf/userall.less',),
330                );
331            } elseif ($key == 'styleini') {
332                $append = array(
333                    'local' => array(
334                        DOKU_INC . 'conf/tpl/%TEMPLATE%/style.ini'
335                    )
336                );
337            } elseif($key == 'users') {
338                $config_cascade['plainauth.users']['protected'] = DOKU_INC . 'conf/users.auth.php';
339            } elseif($key == 'plugins') {
340                $append = array('default' => array(DOKU_INC . 'conf/plugins.local.php'));
341            } else {
342                $append = array('default' => array(DOKU_INC . 'conf/' . $key . '.local.conf'));
343            }
344
345            // add to cascade
346            foreach($prepend as $section => $data) {
347                $config_cascade[$key][$section] = array_merge($data, $config_cascade[$key][$section]);
348            }
349            foreach($append as $section => $data) {
350                $config_cascade[$key][$section] = array_merge($config_cascade[$key][$section], $data);
351            }
352        }
353
354        // add plugin overrides
355        $config_cascade['plugins']['protected'][] = DOKU_INC . 'lib/plugins/farmer/includes/plugins.php';
356    }
357
358    /**
359     * Loads the farm config
360     */
361    protected function loadConfig() {
362        $ini = DOKU_INC . 'conf/farm.ini';
363        if(!file_exists($ini)) return;
364        $config = parse_ini_file($ini, true);
365        foreach(array_keys($this->config) as $section) {
366            if(isset($config[$section])) {
367                $this->config[$section] = array_merge(
368                    $this->config[$section],
369                    $config[$section]
370                );
371            }
372        }
373
374        $this->config['base']['farmdir'] = trim($this->config['base']['farmdir']);
375        $this->config['base']['farmhost'] = strtolower(trim($this->config['base']['farmhost']));
376    }
377
378}
379
380// initialize it globally
381if(!defined('DOKU_UNITTEST')) {
382    global $FARMCORE;
383    $FARMCORE = new DokuWikiFarmCore();
384}
385