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 237 $farmdir = $this->config['base']['farmdir']; 238 $farmhost = $this->config['base']['farmhost']; 239 240 if ('cli' == $sapi) { 241 if (!isset($_SERVER['animal'])) return; // no animal parameter given - we're the farmer 242 243 if (preg_match('#^https?://#i', $_SERVER['animal'])) { 244 // CLI animal parameter is a URL 245 $urlparts = parse_url($_SERVER['animal']); 246 $urlparts['query'] ??= ''; 247 248 // detect the animal from the URL 249 $this->detectAnimalFromQueryString($urlparts['query']) || 250 $this->detectAnimalFromBangPath($urlparts['path']) || 251 $this->detectAnimalFromHostName($urlparts['host']); 252 253 // fake baseurl etc. 254 $this->injectServerEnvironment($urlparts); 255 } else { 256 // CLI animal parameter is just a name 257 $this->setAnimal(strtolower($_SERVER['animal'])); 258 } 259 260 } else { 261 // an animal url parameter has been set 262 if (isset($_GET['animal'])) { 263 $this->detectAnimalFromQueryString($_SERVER['QUERY_STRING']); 264 unset($_GET['animal']); 265 return; 266 } 267 268 // no host - no host based setup. if we're still here then it's the farmer 269 if (empty($_SERVER['HTTP_HOST'])) return; 270 271 // is this the farmer? 272 if (strtolower($_SERVER['HTTP_HOST']) == $farmhost) { 273 return; 274 } 275 276 // we're in host based mode now 277 $this->hostbased = true; 278 279 // we should get an animal now 280 if (!$this->detectAnimalFromHostName($_SERVER['HTTP_HOST'])) { 281 $this->notfound = true; 282 } 283 } 284 } 285 286 /** 287 * Create Server environment variables for the current animal 288 * 289 * This is called when the animal is initialized on the command line using a full URL. 290 * Since the initialization is running before any configuration is loaded, we instead 291 * set the $_SERVER variables that will later be used to autodetect the base URL. This 292 * way a manually set base URL will still take precedence. 293 * 294 * @param array $urlparts A parse_url() result array 295 * @return void 296 * @see is_ssl() 297 * @see getBaseURL() 298 */ 299 protected function injectServerEnvironment(array $urlparts) 300 { 301 // prepare data for DOKU_REL 302 $path = $urlparts['path'] ?? '/'; 303 if (($bangpos = strpos($path, '!')) !== false) { 304 // strip from the bang path 305 $path = substr($path, 0, $bangpos); 306 } 307 if (!str_ends_with($path, '.php')) { 308 // make sure we have a script name 309 $path = rtrim($path, '/') . '/doku.php'; 310 } 311 $_SERVER['SCRIPT_NAME'] = $path; 312 313 // prepare data for is_ssl() 314 if (($urlparts['scheme'] ?? '') === 'https') { 315 $_SERVER['HTTPS'] = 'on'; 316 } else { 317 $_SERVER['HTTPS'] = 'off'; 318 } 319 320 // prepare data for DOKU_URL 321 $_SERVER['HTTP_HOST'] = $urlparts['host'] ?? ''; 322 if (isset($urlparts['port'])) { 323 $_SERVER['HTTP_HOST'] .= ':' . $urlparts['port']; 324 } 325 } 326 327 /** 328 * Return a list of possible animal names for the given host 329 * 330 * @param string $host the HTTP_HOST header 331 * @return array 332 */ 333 protected function getAnimalNamesForHost($host) 334 { 335 $animals = []; 336 $parts = explode('.', implode('.', explode(':', rtrim($host, '.')))); 337 for ($j = count($parts); $j > 0; $j--) { 338 // strip from the end 339 $animals[] = implode('.', array_slice($parts, 0, $j)); 340 // strip from the end without host part 341 $animals[] = implode('.', array_slice($parts, 1, $j)); 342 } 343 $animals = array_unique($animals); 344 $animals = array_filter($animals); 345 usort( 346 $animals, 347 // compare by length, then alphabet 348 function ($a, $b) { 349 $ret = strlen($b) - strlen($a); 350 if ($ret != 0) return $ret; 351 return $a <=> $b; 352 } 353 ); 354 return $animals; 355 } 356 357 /** 358 * This sets up the default farming config cascade 359 */ 360 protected function setupCascade() 361 { 362 global $config_cascade; 363 $config_cascade = [ 364 'main' => [ 365 'default' => [DOKU_INC . 'conf/dokuwiki.php'], 366 'local' => [DOKU_CONF . 'local.php'], 367 'protected' => [DOKU_CONF . 'local.protected.php'] 368 ], 369 'acronyms' => [ 370 'default' => [DOKU_INC . 'conf/acronyms.conf'], 371 'local' => [DOKU_CONF . 'acronyms.local.conf'] 372 ], 373 'entities' => [ 374 'default' => [DOKU_INC . 'conf/entities.conf'], 375 'local' => [DOKU_CONF . 'entities.local.conf'] 376 ], 377 'interwiki' => [ 378 'default' => [DOKU_INC . 'conf/interwiki.conf'], 379 'local' => [DOKU_CONF . 'interwiki.local.conf'] 380 ], 381 'license' => [ 382 'default' => [DOKU_INC . 'conf/license.php'], 383 'local' => [DOKU_CONF . 'license.local.php'] 384 ], 385 'manifest' => [ 386 'default' => [DOKU_INC . 'conf/manifest.json'], 387 'local' => [DOKU_CONF . 'manifest.local.json'] 388 ], 389 'mediameta' => [ 390 'default' => [DOKU_INC . 'conf/mediameta.php'], 391 'local' => [DOKU_CONF . 'mediameta.local.php'] 392 ], 393 'mime' => [ 394 'default' => [DOKU_INC . 'conf/mime.conf'], 395 'local' => [DOKU_CONF . 'mime.local.conf'] 396 ], 397 'scheme' => [ 398 'default' => [DOKU_INC . 'conf/scheme.conf'], 399 'local' => [DOKU_CONF . 'scheme.local.conf'] 400 ], 401 'smileys' => [ 402 'default' => [DOKU_INC . 'conf/smileys.conf'], 403 'local' => [DOKU_CONF . 'smileys.local.conf'] 404 ], 405 'wordblock' => [ 406 'default' => [DOKU_INC . 'conf/wordblock.conf'], 407 'local' => [DOKU_CONF . 'wordblock.local.conf'] 408 ], 409 'acl' => [ 410 'default' => DOKU_CONF . 'acl.auth.php' 411 ], 412 'plainauth.users' => [ 413 'default' => DOKU_CONF . 'users.auth.php' 414 ], 415 'plugins' => [ 416 'default' => [DOKU_INC . 'conf/plugins.php'], 417 'local' => [DOKU_CONF . 'plugins.local.php'], 418 'protected' => [ 419 DOKU_INC . 'conf/plugins.required.php', 420 DOKU_CONF . 'plugins.protected.php' 421 ] 422 ], 423 'userstyle' => [ 424 'screen' => [ 425 DOKU_CONF . 'userstyle.css', 426 DOKU_CONF . 'userstyle.less' 427 ], 428 'print' => [ 429 DOKU_CONF . 'userprint.css', 430 DOKU_CONF . 'userprint.less' 431 ], 432 'feed' => [ 433 DOKU_CONF . 'userfeed.css', 434 DOKU_CONF . 'userfeed.less' 435 ], 436 'all' => [ 437 DOKU_CONF . 'userall.css', 438 DOKU_CONF . 'userall.less' 439 ] 440 ], 441 'userscript' => [ 442 'default' => [DOKU_CONF . 'userscript.js'] 443 ], 444 'styleini' => [ 445 'default' => [DOKU_INC . 'lib/tpl/%TEMPLATE%/' . 'style.ini'], 446 'local' => [DOKU_CONF . 'tpl/%TEMPLATE%/' . 'style.ini'] 447 ] 448 ]; 449 } 450 451 /** 452 * This adds additional files to the config cascade based on the inheritence settings 453 * 454 * These are only added for animals, not the farmer 455 */ 456 protected function adjustCascade() 457 { 458 // nothing to do when on the farmer: 459 if (!$this->animal) return; 460 461 global $config_cascade; 462 foreach ($this->config['inherit'] as $key => $val) { 463 if (!$val) continue; 464 465 // prepare what is to append or prepend 466 $append = []; 467 $prepend = []; 468 if ($key == 'main') { 469 $prepend = [ 470 'protected' => [DOKU_INC . 'conf/local.protected.php'] 471 ]; 472 $append = [ 473 'default' => [DOKU_INC . 'conf/local.php'], 474 'protected' => [DOKU_INC . 'lib/plugins/farmer/includes/config.php'] 475 ]; 476 } elseif ($key == 'license') { 477 $append = [ 478 'default' => [DOKU_INC . 'conf/' . $key . '.local.php'] 479 ]; 480 } elseif ($key == 'userscript') { 481 $prepend = [ 482 'default' => [DOKU_INC . 'conf/userscript.js'] 483 ]; 484 } elseif ($key == 'userstyle') { 485 $prepend = [ 486 'screen' => [ 487 DOKU_INC . 'conf/userstyle.css', 488 DOKU_INC . 'conf/userstyle.less' 489 ], 490 'print' => [ 491 DOKU_INC . 'conf/userprint.css', 492 DOKU_INC . 'conf/userprint.less' 493 ], 494 'feed' => [ 495 DOKU_INC . 'conf/userfeed.css', 496 DOKU_INC . 'conf/userfeed.less' 497 ], 498 'all' => [ 499 DOKU_INC . 'conf/userall.css', 500 DOKU_INC . 'conf/userall.less' 501 ] 502 ]; 503 } elseif ($key == 'styleini') { 504 $append = [ 505 'local' => [DOKU_INC . 'conf/tpl/%TEMPLATE%/style.ini'] 506 ]; 507 } elseif ($key == 'users') { 508 $config_cascade['plainauth.users']['protected'] = DOKU_INC . 'conf/users.auth.php'; 509 } elseif ($key == 'plugins') { 510 $prepend = [ 511 'protected' => [DOKU_INC . 'conf/local.protected.php'] 512 ]; 513 $append = [ 514 'default' => [DOKU_INC . 'conf/plugins.local.php'] 515 ]; 516 } else { 517 $append = [ 518 'default' => [DOKU_INC . 'conf/' . $key . '.local.conf'] 519 ]; 520 } 521 522 // add to cascade 523 foreach ($prepend as $section => $data) { 524 $config_cascade[$key][$section] = array_merge($data, $config_cascade[$key][$section]); 525 } 526 foreach ($append as $section => $data) { 527 $config_cascade[$key][$section] = array_merge($config_cascade[$key][$section], $data); 528 } 529 } 530 531 // add plugin overrides 532 $config_cascade['plugins']['protected'][] = DOKU_INC . 'lib/plugins/farmer/includes/plugins.php'; 533 } 534 535 /** 536 * Loads the farm config 537 */ 538 protected function loadConfig() 539 { 540 $ini = DOKU_INC . 'conf/farm.ini'; 541 if (file_exists($ini)) { 542 $config = parse_ini_file($ini, true); 543 foreach (array_keys($this->config) as $section) { 544 if (isset($config[$section])) { 545 $this->config[$section] = array_merge( 546 $this->config[$section], 547 $config[$section] 548 ); 549 } 550 } 551 } 552 553 // farmdir setup can be done via environment 554 if ($this->config['base']['farmdir'] === '' && isset($_ENV['DOKU_FARMDIR'])) { 555 $this->config['base']['farmdir'] = $_ENV['DOKU_FARMDIR']; 556 } 557 558 $this->config['base']['farmdir'] = trim($this->config['base']['farmdir']); 559 $this->config['base']['farmhost'] = strtolower(trim($this->config['base']['farmhost'])); 560 } 561} 562 563// initialize it globally 564if (!defined('DOKU_UNITTEST')) { 565 global $FARMCORE; 566 $FARMCORE = new DokuWikiFarmCore(); 567} 568