1<?php 2 3// phpcs:disable PSR1.Files.SideEffects 4 5/** 6 * Core Manager for the Farm functionality 7 * 8 * This class is initialized before any other DokuWiki code runs. Therefore it is 9 * completely selfcontained and does not use any of DokuWiki's utility functions. 10 * 11 * It's registered as a global $FARMCORE variable but you should not interact with 12 * it directly. Instead use the Farmer plugin's helper component. 13 * 14 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 15 * @author Andreas Gohr <gohr@cosmocode.de> 16 */ 17class DokuWikiFarmCore 18{ 19 /** 20 * @var array The default config - changed by loadConfig 21 */ 22 protected $config = [ 23 'base' => [ 24 'farmdir' => '', 25 'farmhost' => '', 26 'basedomain' => '' 27 ], 28 'notfound' => [ 29 'show' => 'farmer', 30 'url' => '' 31 ], 32 'inherit' => [ 33 'main' => 1, 34 'acronyms' => 1, 35 'entities' => 1, 36 'interwiki' => 1, 37 'license' => 1, 38 'mime' => 1, 39 'scheme' => 1, 40 'smileys' => 1, 41 'wordblock' => 1, 42 'users' => 0, 43 'plugins' => 0, 44 'userstyle' => 0, 45 'userscript' => 0, 46 'styleini' => 0 47 ] 48 ]; 49 50 /** @var string|false The current animal, false for farmer */ 51 protected $animal = false; 52 /** @var bool true if an animal was requested but was not found */ 53 protected $notfound = false; 54 /** @var bool true if the current animal was requested by host */ 55 protected $hostbased = false; 56 57 /** 58 * DokuWikiFarmCore constructor. 59 * 60 * This initializes the whole farm by loading the configuration and setting 61 * DOKU_CONF depending on the requested animal 62 */ 63 public function __construct() 64 { 65 $this->loadConfig(); 66 if ($this->config['base']['farmdir'] === '') return; // farm setup not complete 67 $this->config['base']['farmdir'] = rtrim($this->config['base']['farmdir'], '/') . '/'; // trailing slash always 68 define('DOKU_FARMDIR', $this->config['base']['farmdir']); 69 70 // animal? 71 $this->detectAnimal(); 72 73 // setup defines 74 define('DOKU_FARM_ANIMAL', $this->animal); 75 if ($this->animal) { 76 define('DOKU_CONF', DOKU_FARMDIR . $this->animal . '/conf/'); 77 } else { 78 define('DOKU_CONF', DOKU_INC . '/conf/'); 79 } 80 81 $this->setupCascade(); 82 $this->adjustCascade(); 83 } 84 85 /** 86 * @return array the current farm configuration 87 */ 88 public function getConfig() 89 { 90 return $this->config; 91 } 92 93 /** 94 * @return false|string 95 */ 96 public function getAnimal() 97 { 98 return $this->animal; 99 } 100 101 /** 102 * @return boolean 103 */ 104 public function isHostbased() 105 { 106 return $this->hostbased; 107 } 108 109 /** 110 * @return boolean 111 */ 112 public function wasNotfound() 113 { 114 return $this->notfound; 115 } 116 117 /** 118 * @return string 119 */ 120 public function getAnimalDataDir() 121 { 122 return DOKU_FARMDIR . $this->getAnimal() . '/data/'; 123 } 124 125 /** 126 * @return string 127 */ 128 public function getAnimalBaseDir() 129 { 130 if ($this->isHostbased()) return '/'; 131 return getBaseURL() . '!' . $this->getAnimal(); 132 } 133 134 /** 135 * Set the animal 136 * 137 * Checks if the animal exists and is a valid directory name. 138 * 139 * @param mixed $animal the animal name 140 * @return bool returns true if the animal was set successfully, false otherwise 141 */ 142 protected function setAnimal($animal) 143 { 144 $farmdir = $this->config['base']['farmdir']; 145 146 // invalid animal stuff is always a not found 147 if (!is_string($animal) || strpbrk($animal, '\\/') !== false) { 148 $this->notfound = true; 149 return false; 150 } 151 $animal = strtolower($animal); 152 153 // check if animal exists 154 if (is_dir("$farmdir/$animal/conf")) { 155 $this->animal = $animal; 156 $this->notfound = false; 157 return true; 158 } else { 159 $this->notfound = true; 160 return false; 161 } 162 } 163 164 /** 165 * Detect the animal from the given query string 166 * 167 * This removes the animal parameter from the given string and sets the animal 168 * 169 * @param string $queryString The query string to extract the animal from, will be modified 170 * @return bool true if the animal was set successfully, false otherwise 171 */ 172 protected function detectAnimalFromQueryString(string &$queryString): bool 173 { 174 $params = []; 175 parse_str($queryString, $params); 176 if (!isset($params['animal'])) return false; 177 $animal = $params['animal']; 178 unset($params['animal']); 179 $queryString = http_build_query($params); 180 181 $this->hostbased = false; 182 return $this->setAnimal($animal); 183 } 184 185 /** 186 * Detect the animal from the bang path 187 * 188 * This is used to detect the animal from a bang path like `/!animal/my:page` or '/dokuwiki/!animal/my:page'. 189 * 190 * @param string $path The bang path to extract the animal from 191 * @return bool true if the animal was set successfully, false otherwise 192 */ 193 protected function detectAnimalFromBangPath(string $path): bool 194 { 195 $bangregex = '#^(/(?:[^/]*/)*)!([^/]+)/#'; 196 if (preg_match($bangregex, $path, $matches)) { 197 // found a bang path 198 $animal = $matches[2]; 199 200 $this->hostbased = false; 201 return $this->setAnimal($animal); 202 } 203 return false; 204 } 205 206 /** 207 * Detect the animal from the host name 208 * 209 * @param string $host The hostname 210 * @return bool true if the animal was set successfully, false otherwise 211 */ 212 protected function detectAnimalFromHostName(string $host): bool 213 { 214 $possible = $this->getAnimalNamesForHost($host); 215 foreach ($possible as $animal) { 216 if ($this->setAnimal($animal)) { 217 $this->hostbased = true; 218 return true; 219 } 220 } 221 return false; 222 } 223 224 /** 225 * Detect the current animal 226 * 227 * Sets internal members $animal, $notfound and $hostbased 228 * 229 * This borrows form DokuWiki's inc/farm.php but does not support a default conf dir 230 * 231 * @params string|null $sapi the SAPI to use. Only changed during testing 232 */ 233 protected function detectAnimal($sapi = null) 234 { 235 $sapi = $sapi ?: PHP_SAPI; 236 $farmhost = $this->config['base']['farmhost']; 237 238 if ('cli' == $sapi) { 239 if (!isset($_SERVER['animal'])) return; // no animal parameter given - we're the farmer 240 241 if (preg_match('#^https?://#i', $_SERVER['animal'])) { 242 // CLI animal parameter is a URL 243 $urlparts = parse_url($_SERVER['animal']); 244 $urlparts['query'] ??= ''; 245 246 // detect the animal from the URL 247 $this->detectAnimalFromQueryString($urlparts['query']) || 248 $this->detectAnimalFromBangPath($urlparts['path']) || 249 $this->detectAnimalFromHostName($urlparts['host']); 250 251 // fake baseurl etc. 252 $this->injectServerEnvironment($urlparts); 253 } else { 254 // CLI animal parameter is just a name 255 $this->setAnimal(strtolower($_SERVER['animal'])); 256 } 257 } else { 258 // an animal url parameter has been set 259 if (isset($_GET['animal'])) { 260 $this->detectAnimalFromQueryString($_SERVER['QUERY_STRING']); 261 unset($_GET['animal']); 262 return; 263 } 264 265 // no host - no host based setup. if we're still here then it's the farmer 266 if (empty($_SERVER['HTTP_HOST'])) return; 267 268 // is this the farmer? 269 if (strtolower($_SERVER['HTTP_HOST']) == $farmhost) { 270 return; 271 } 272 273 // we're in host based mode now 274 $this->hostbased = true; 275 276 // we should get an animal now 277 if (!$this->detectAnimalFromHostName($_SERVER['HTTP_HOST'])) { 278 $this->notfound = true; 279 } 280 } 281 } 282 283 /** 284 * Create Server environment variables for the current animal 285 * 286 * This is called when the animal is initialized on the command line using a full URL. 287 * Since the initialization is running before any configuration is loaded, we instead 288 * set the $_SERVER variables that will later be used to autodetect the base URL. This 289 * way a manually set base URL will still take precedence. 290 * 291 * @param array $urlparts A parse_url() result array 292 * @return void 293 * @see is_ssl() 294 * @see getBaseURL() 295 */ 296 protected function injectServerEnvironment(array $urlparts) 297 { 298 // prepare data for DOKU_REL 299 $path = $urlparts['path'] ?? '/'; 300 if (($bangpos = strpos($path, '!')) !== false) { 301 // strip from the bang path 302 $path = substr($path, 0, $bangpos); 303 } 304 if (!str_ends_with($path, '.php')) { 305 // make sure we have a script name 306 $path = rtrim($path, '/') . '/doku.php'; 307 } 308 $_SERVER['SCRIPT_NAME'] = $path; 309 310 // prepare data for is_ssl() 311 if (($urlparts['scheme'] ?? '') === 'https') { 312 $_SERVER['HTTPS'] = 'on'; 313 } else { 314 $_SERVER['HTTPS'] = 'off'; 315 } 316 317 // prepare data for DOKU_URL 318 $_SERVER['HTTP_HOST'] = $urlparts['host'] ?? ''; 319 if (isset($urlparts['port'])) { 320 $_SERVER['HTTP_HOST'] .= ':' . $urlparts['port']; 321 } 322 } 323 324 /** 325 * Return a list of possible animal names for the given host 326 * 327 * @param string $host the HTTP_HOST header 328 * @return array 329 */ 330 protected function getAnimalNamesForHost($host) 331 { 332 $animals = []; 333 $parts = explode('.', implode('.', explode(':', rtrim($host, '.')))); 334 for ($j = count($parts); $j > 0; $j--) { 335 // strip from the end 336 $animals[] = implode('.', array_slice($parts, 0, $j)); 337 // strip from the end without host part 338 $animals[] = implode('.', array_slice($parts, 1, $j)); 339 } 340 $animals = array_unique($animals); 341 $animals = array_filter($animals); 342 usort( 343 $animals, 344 // compare by length, then alphabet 345 function ($a, $b) { 346 $ret = strlen($b) - strlen($a); 347 if ($ret != 0) return $ret; 348 return $a <=> $b; 349 } 350 ); 351 return $animals; 352 } 353 354 /** 355 * This sets up the default farming config cascade 356 */ 357 protected function setupCascade() 358 { 359 global $config_cascade; 360 $config_cascade = [ 361 'main' => [ 362 'default' => [DOKU_INC . 'conf/dokuwiki.php'], 363 'local' => [DOKU_CONF . 'local.php'], 364 'protected' => [DOKU_CONF . 'local.protected.php'] 365 ], 366 'acronyms' => [ 367 'default' => [DOKU_INC . 'conf/acronyms.conf'], 368 'local' => [DOKU_CONF . 'acronyms.local.conf'] 369 ], 370 'entities' => [ 371 'default' => [DOKU_INC . 'conf/entities.conf'], 372 'local' => [DOKU_CONF . 'entities.local.conf'] 373 ], 374 'interwiki' => [ 375 'default' => [DOKU_INC . 'conf/interwiki.conf'], 376 'local' => [DOKU_CONF . 'interwiki.local.conf'] 377 ], 378 'license' => [ 379 'default' => [DOKU_INC . 'conf/license.php'], 380 'local' => [DOKU_CONF . 'license.local.php'] 381 ], 382 'manifest' => [ 383 'default' => [DOKU_INC . 'conf/manifest.json'], 384 'local' => [DOKU_CONF . 'manifest.local.json'] 385 ], 386 'mediameta' => [ 387 'default' => [DOKU_INC . 'conf/mediameta.php'], 388 'local' => [DOKU_CONF . 'mediameta.local.php'] 389 ], 390 'mime' => [ 391 'default' => [DOKU_INC . 'conf/mime.conf'], 392 'local' => [DOKU_CONF . 'mime.local.conf'] 393 ], 394 'scheme' => [ 395 'default' => [DOKU_INC . 'conf/scheme.conf'], 396 'local' => [DOKU_CONF . 'scheme.local.conf'] 397 ], 398 'smileys' => [ 399 'default' => [DOKU_INC . 'conf/smileys.conf'], 400 'local' => [DOKU_CONF . 'smileys.local.conf'] 401 ], 402 'wordblock' => [ 403 'default' => [DOKU_INC . 'conf/wordblock.conf'], 404 'local' => [DOKU_CONF . 'wordblock.local.conf'] 405 ], 406 'acl' => [ 407 'default' => DOKU_CONF . 'acl.auth.php' 408 ], 409 'plainauth.users' => [ 410 'default' => DOKU_CONF . 'users.auth.php' 411 ], 412 'plugins' => [ 413 'default' => [DOKU_INC . 'conf/plugins.php'], 414 'local' => [DOKU_CONF . 'plugins.local.php'], 415 'protected' => [ 416 DOKU_INC . 'conf/plugins.required.php', 417 DOKU_CONF . 'plugins.protected.php' 418 ] 419 ], 420 'userstyle' => [ 421 'screen' => [ 422 DOKU_CONF . 'userstyle.css', 423 DOKU_CONF . 'userstyle.less' 424 ], 425 'print' => [ 426 DOKU_CONF . 'userprint.css', 427 DOKU_CONF . 'userprint.less' 428 ], 429 'feed' => [ 430 DOKU_CONF . 'userfeed.css', 431 DOKU_CONF . 'userfeed.less' 432 ], 433 'all' => [ 434 DOKU_CONF . 'userall.css', 435 DOKU_CONF . 'userall.less' 436 ] 437 ], 438 'userscript' => [ 439 'default' => [DOKU_CONF . 'userscript.js'] 440 ], 441 'styleini' => [ 442 'default' => [DOKU_INC . 'lib/tpl/%TEMPLATE%/' . 'style.ini'], 443 'local' => [DOKU_CONF . 'tpl/%TEMPLATE%/' . 'style.ini'] 444 ] 445 ]; 446 } 447 448 /** 449 * This adds additional files to the config cascade based on the inheritence settings 450 * 451 * These are only added for animals, not the farmer 452 */ 453 protected function adjustCascade() 454 { 455 // nothing to do when on the farmer: 456 if (!$this->animal) return; 457 458 global $config_cascade; 459 foreach ($this->config['inherit'] as $key => $val) { 460 if (!$val) continue; 461 462 // prepare what is to append or prepend 463 $append = []; 464 $prepend = []; 465 if ($key == 'main') { 466 $prepend = [ 467 'protected' => [DOKU_INC . 'conf/local.protected.php'] 468 ]; 469 $append = [ 470 'default' => [DOKU_INC . 'conf/local.php'], 471 'protected' => [DOKU_INC . 'lib/plugins/farmer/includes/config.php'] 472 ]; 473 } elseif ($key == 'license') { 474 $append = [ 475 'default' => [DOKU_INC . 'conf/' . $key . '.local.php'] 476 ]; 477 } elseif ($key == 'userscript') { 478 $prepend = [ 479 'default' => [DOKU_INC . 'conf/userscript.js'] 480 ]; 481 } elseif ($key == 'userstyle') { 482 $prepend = [ 483 'screen' => [ 484 DOKU_INC . 'conf/userstyle.css', 485 DOKU_INC . 'conf/userstyle.less' 486 ], 487 'print' => [ 488 DOKU_INC . 'conf/userprint.css', 489 DOKU_INC . 'conf/userprint.less' 490 ], 491 'feed' => [ 492 DOKU_INC . 'conf/userfeed.css', 493 DOKU_INC . 'conf/userfeed.less' 494 ], 495 'all' => [ 496 DOKU_INC . 'conf/userall.css', 497 DOKU_INC . 'conf/userall.less' 498 ] 499 ]; 500 } elseif ($key == 'styleini') { 501 $append = [ 502 'local' => [DOKU_INC . 'conf/tpl/%TEMPLATE%/style.ini'] 503 ]; 504 } elseif ($key == 'users') { 505 $config_cascade['plainauth.users']['protected'] = DOKU_INC . 'conf/users.auth.php'; 506 } elseif ($key == 'plugins') { 507 $prepend = [ 508 'protected' => [DOKU_INC . 'conf/local.protected.php'] 509 ]; 510 $append = [ 511 'default' => [DOKU_INC . 'conf/plugins.local.php'] 512 ]; 513 } else { 514 $append = [ 515 'default' => [DOKU_INC . 'conf/' . $key . '.local.conf'] 516 ]; 517 } 518 519 // add to cascade 520 foreach ($prepend as $section => $data) { 521 $config_cascade[$key][$section] = array_merge($data, $config_cascade[$key][$section]); 522 } 523 foreach ($append as $section => $data) { 524 $config_cascade[$key][$section] = array_merge($config_cascade[$key][$section], $data); 525 } 526 } 527 528 // add plugin overrides 529 $config_cascade['plugins']['protected'][] = DOKU_INC . 'lib/plugins/farmer/includes/plugins.php'; 530 } 531 532 /** 533 * Loads the farm config 534 */ 535 protected function loadConfig() 536 { 537 $ini = DOKU_INC . 'conf/farm.ini'; 538 if (file_exists($ini)) { 539 $config = parse_ini_file($ini, true); 540 foreach (array_keys($this->config) as $section) { 541 if (isset($config[$section])) { 542 $this->config[$section] = array_merge( 543 $this->config[$section], 544 $config[$section] 545 ); 546 } 547 } 548 } 549 550 // farmdir setup can be done via environment 551 if ($this->config['base']['farmdir'] === '' && isset($_ENV['DOKU_FARMDIR'])) { 552 $this->config['base']['farmdir'] = $_ENV['DOKU_FARMDIR']; 553 } 554 555 $this->config['base']['farmdir'] = trim($this->config['base']['farmdir']); 556 $this->config['base']['farmhost'] = strtolower(trim($this->config['base']['farmhost'])); 557 } 558} 559 560// initialize it globally 561if (!defined('DOKU_UNITTEST')) { 562 global $FARMCORE; 563 $FARMCORE = new DokuWikiFarmCore(); 564} 565