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