Root key => real path with a trailing slash ('' when unusable). */ private array $paths; /** * @param array $paths Root key => directory. */ public function __construct( array $paths ) { $this->paths = array(); foreach ( array( self::CORE, self::PLUGINS, self::THEMES, self::UPLOADS ) as $key ) { $real = isset( $paths[ $key ] ) && '' !== $paths[ $key ] ? realpath( $paths[ $key ] ) : false; $this->paths[ $key ] = is_string( $real ) && is_dir( $real ) ? rtrim( $real, '/' ) . '/' : ''; } } /** The site's own directories. */ public static function from_wordpress(): self { $uploads = wp_upload_dir( null, false ); return new self( array( self::CORE => ABSPATH, self::PLUGINS => WP_PLUGIN_DIR, self::THEMES => get_theme_root(), self::UPLOADS => (string) $uploads['basedir'], ) ); } /** * A root's real path with a trailing slash, or '' when it does not exist. * * @param string $key Root key. */ public function path( string $key ): string { return $this->paths[ $key ] ?? ''; } /** * The absolute path of a file or directory under a root, or null when * it is outside the root once resolved, or the relative path is unsafe. * Does not require the path to exist; when it exists, its real path is * what is checked. * * @param string $key Root key. * @param string $relative Path within the root ('' for the root itself). */ public function resolve( string $key, string $relative ): ?string { $root = $this->path( $key ); if ( '' === $root ) { return null; } if ( '' === $relative ) { return $root; } if ( ! Comparison::is_safe_relative( $relative ) ) { return null; } $absolute = $root . $relative; if ( ! file_exists( $absolute ) && ! is_link( $absolute ) ) { return $absolute; } $real = realpath( $absolute ); if ( ! is_string( $real ) ) { return null; // A dangling link. } return $this->inside( $key, $real ) ? $absolute : null; } /** * Whether a real path lies inside a root. * * @param string $key Root key. * @param string $real A real path. */ public function inside( string $key, string $real ): bool { $root = $this->path( $key ); return '' !== $root && ( rtrim( $real, '/' ) . '/' === $root || 0 === strpos( $real, $root ) ); } /** * How the screen names a path: relative to ABSPATH when the root is * inside it (`wp-content/plugins/akismet/akismet.php`), otherwise the * root's name in brackets (`[plugins]/akismet/akismet.php`). Never the * server's absolute path. * * @param string $key Root key. * @param string $relative Path within the root. */ public function display( string $key, string $relative ): string { $core = $this->path( self::CORE ); $root = $this->path( $key ); if ( '' !== $core && '' !== $root && 0 === strpos( $root, $core ) ) { return substr( $root, strlen( $core ) ) . $relative; } return '[' . $key . ']/' . $relative; } }