path = $path; $this->mode = $mode; } /** * Picks the storage folder. * * @param string|null $override FITZARO_STORAGE_DIR, or null when not defined. * @param string $abspath ABSPATH. * @param string $document_root $_SERVER['DOCUMENT_ROOT'], or '' when unknown (CLI, cron). * @param string $uploads_basedir wp_upload_dir()['basedir']. * @param string $suffix Random folder suffix for the uploads fallback. * @param callable $is_writable Receives a directory path; true when it exists and is writable. */ public static function choose( ?string $override, string $abspath, string $document_root, string $uploads_basedir, string $suffix, callable $is_writable ): self { if ( null !== $override && '' !== $override && self::is_absolute( $override ) ) { return new self( self::trim( $override ), self::MODE_CUSTOM ); } $outside = self::outside_path( $abspath, $document_root ); if ( null !== $outside && ( $is_writable( $outside ) || $is_writable( dirname( $outside ) ) ) ) { return new self( $outside, self::MODE_OUTSIDE ); } if ( 1 === preg_match( '/^[a-z0-9]{6,32}$/', $suffix ) && $is_writable( self::trim( $uploads_basedir ) ) ) { return new self( self::trim( $uploads_basedir ) . '/fitzaro-' . $suffix, self::MODE_UPLOADS ); } return new self( '', self::MODE_NONE ); } /** * A location chosen earlier and stored in an option. * * @param string $path Absolute path. * @param string $mode Mode recorded with it. */ public static function remembered( string $path, string $mode ): self { $known = array( self::MODE_CUSTOM, self::MODE_OUTSIDE, self::MODE_UPLOADS ); return in_array( $mode, $known, true ) && '' !== $path ? new self( self::trim( $path ), $mode ) : new self( '', self::MODE_NONE ); } /** * Absolute folder path, without a trailing slash. */ public function path(): string { return $this->path; } /** * How the folder was chosen. */ public function mode(): string { return $this->mode; } /** * Whether photos can be stored at all. */ public function is_usable(): bool { return self::MODE_NONE !== $this->mode; } /** * Whether the web server cannot reach the folder even without deny rules. */ public function is_outside_web_root(): bool { return self::MODE_OUTSIDE === $this->mode; } /** * The folder next to WordPress, or null when it might be inside the public web root. * * @param string $abspath ABSPATH. * @param string $document_root Web server document root, or ''. */ private static function outside_path( string $abspath, string $document_root ): ?string { $document_root = self::trim( $document_root ); if ( '' === $document_root ) { return null; } $candidate = dirname( self::trim( $abspath ) ) . '/' . self::OUTSIDE_FOLDER; if ( $candidate === $document_root || 0 === strpos( $candidate, $document_root . '/' ) ) { return null; } return $candidate; } /** * Whether a path is absolute on Unix or Windows. * * @param string $path Path. */ private static function is_absolute( string $path ): bool { return '/' === $path[0] || 1 === preg_match( '#^[A-Za-z]:[\\\\/]#', $path ); } /** * Normalises slashes and removes the trailing one. * * @param string $path Path. */ private static function trim( string $path ): string { $path = str_replace( '\\', '/', $path ); return '/' === $path ? $path : rtrim( $path, '/' ); } }