Files
luxtools-plugin/Path.php
luxick dbc9de37e0
Some checks failed
DokuWiki Default Tasks / all (push) Has been cancelled
Allow opening folder lisings on client
2026-01-06 10:13:12 +01:00

222 lines
6.6 KiB
PHP

<?php
namespace dokuwiki\plugin\luxtools;
class Path
{
protected $paths = [];
/**
* @param string $pathConfig The path configuration ftom the plugin settings
*/
public function __construct($pathConfig)
{
$this->paths = $this->parsePathConfig($pathConfig);
}
/**
* Access the parsed paths
*
* @return array
*/
public function getPaths()
{
return $this->paths;
}
/**
* Parse the path configuration into an internal array
*
* roots (and aliases) are always saved with a trailing slash
*
* @return array
*/
protected function parsePathConfig($pathConfig)
{
$paths = [];
$lines = explode("\n", $pathConfig);
$lastRoot = '';
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
if (str_starts_with($line, 'A>')) {
// this is an alias for the last read root
$line = trim(substr($line, 2));
if (!isset($paths[$lastRoot])) continue; // no last root, no alias
$alias = static::cleanPath($line);
$paths[$lastRoot]['alias'] = $alias;
$paths[$alias] = &$paths[$lastRoot]; // alias references the original
} elseif (str_starts_with($line, 'W>')) {
// this is a web path for the last read root
$line = trim(substr($line, 2));
if (!isset($paths[$lastRoot])) continue; // no last path, no web path
$paths[$lastRoot]['web'] = $line;
} else {
// this is a new path
$line = static::cleanPath($line);
$lastRoot = $line;
$paths[$line] = [
'root' => $line,
'web' => DOKU_BASE . 'lib/plugins/luxtools/file.php?root=' . rawurlencode($line) . '&file=',
];
}
}
return $paths;
}
/**
* Check if a given path is listable and return it's configuration
*
* @param string $path
* @param bool $addTrailingSlash
* @return array
* @throws \Exception if the given path is not allowed
*/
public function getPathInfo($path, $addTrailingSlash = true)
{
$path = static::cleanPath($path, $addTrailingSlash);
$paths = $this->paths;
if ($paths === []) {
throw new \Exception('No paths configured');
}
$allowed = array_keys($paths);
usort($allowed, static fn($a, $b) => strlen($a) - strlen($b));
$allowed = array_map('preg_quote_cb', $allowed);
$regex = '/^(' . implode('|', $allowed) . ')/';
if (!preg_match($regex, $path, $matches)) {
throw new \Exception('Path not allowed: ' . $path);
}
$match = $matches[1];
$pathInfo = $paths[$match];
$pathInfo['local'] = substr($path, strlen($match));
$pathInfo['path'] = $pathInfo['root'] . $pathInfo['local'];
return $pathInfo;
}
/**
* Map a real filesystem path back to a configured alias, if available.
*
* Example: root "/share/Datascape/" with alias "/Scape/" maps
* "/share/Datascape/some/folder" -> "/Scape/some/folder".
*
* If no alias matches, the input path is returned unchanged (except for
* normalization of slashes and dot-segments).
*
* @param string $path
* @return string
*/
public function mapToAliasPath($path)
{
if (!is_string($path) || $path === '') return $path;
// normalize input for matching, but do not force a trailing slash
$normalized = static::cleanPath($path, false);
// collect root->alias mappings (avoid alias keys that reference the same config)
$mappings = [];
foreach ($this->paths as $key => $info) {
if (!isset($info['root']) || $key !== $info['root']) continue;
if (empty($info['alias'])) continue;
$mappings[$info['root']] = $info['alias'];
}
if ($mappings === []) return $normalized;
// Prefer the longest matching root (handles nested/overlapping roots)
uksort($mappings, static fn($a, $b) => strlen($b) - strlen($a));
foreach ($mappings as $root => $alias) {
if (str_starts_with($normalized, $root)) {
$suffix = substr($normalized, strlen($root));
$alias = static::cleanPath($alias, true);
return rtrim($alias, '/') . '/' . $suffix;
}
}
return $normalized;
}
/**
* Clean a path for better comparison
*
* Converts all backslashes to forward slashes
* Keeps leading double backslashes for UNC paths
* Ensure a single trailing slash unless disabled
*
* @param string $path
* @return string
*/
public static function cleanPath($path, $addTrailingSlash = true)
{
if (str_starts_with($path, '\\\\')) {
$unc = '\\\\';
} else {
$unc = '';
}
$path = ltrim($path, '\\');
$path = str_replace('\\', '/', $path);
$path = self::realpath($path);
if ($addTrailingSlash) {
$path = rtrim($path, '/');
$path .= '/';
}
return $unc . $path;
}
/**
* Canonicalizes a given path. A bit like realpath, but without the resolving of symlinks.
*
* @author anonymous
* @see <http://www.php.net/manual/en/function.realpath.php#73563>
*/
public static function realpath($path)
{
$path = explode('/', $path);
$output = [];
$counter = count($path);
for ($i = 0; $i < $counter; $i++) {
if ('.' == $path[$i]) continue;
if ('' === $path[$i] && $i > 0) continue;
if ('..' == $path[$i] && '..' != ($output[count($output) - 1] ?? '')) {
array_pop($output);
continue;
}
$output[] = $path[$i];
}
return implode('/', $output);
}
/**
* Check if the given path is within the data or dokuwiki dir
*
* This whould prevent accidental or deliberate circumvention of the ACLs
*
* @param string $path and already cleaned path
* @return bool
*/
public static function isWikiControlled($path)
{
global $conf;
$dataPath = self::cleanPath($conf['savedir']);
if (str_starts_with($path, $dataPath)) {
return true;
}
$wikiDir = self::cleanPath(DOKU_INC);
if (str_starts_with($path, $wikiDir)) {
return true;
}
return false;
}
}