" "# Fitzaro: photos are served through PHP only.\n\n\tRequire all denied\n\n\n\tDeny from all\n\n",
'web.config' => "\n\n",
);
/**
* Absolute root folder, without a trailing slash.
*
* @var string
*/
private $root;
/**
* Creates a store rooted at $root. Folders are created on first write.
*
* @param string $root Absolute folder path.
*/
public function __construct( string $root ) {
$this->root = rtrim( $root, '/' );
}
/**
* Stores bytes under a new name and returns the relative path.
*
* @param string $kind self::UPLOADS or self::RESULTS.
* @param string $bytes File content (already sanitised).
* @throws \InvalidArgumentException When $kind is unknown.
* @throws \RuntimeException When the file cannot be written.
*/
public function put( string $kind, string $bytes ): string {
if ( self::UPLOADS !== $kind && self::RESULTS !== $kind ) {
throw new \InvalidArgumentException( 'Unknown storage kind.' );
}
$this->ensure_folder( $this->root );
$this->ensure_folder( $this->root . '/' . $kind );
$path = $kind . '/' . Uuid::v4() . '.jpg';
if ( false === file_put_contents( $this->root . '/' . $path, $bytes, LOCK_EX ) ) {
throw new \RuntimeException( 'Could not write the file.' );
}
return $path;
}
/**
* File content, or null when the path is invalid or the file is gone.
*
* @param string $path Relative path returned by put().
*/
public function read( string $path ): ?string {
$file = $this->absolute( $path );
if ( null === $file || ! is_file( $file ) ) {
return null;
}
$bytes = file_get_contents( $file );
return false === $bytes ? null : $bytes;
}
/**
* Absolute path of a stored file, or null when the path is invalid or the file is gone.
*
* @param string $path Relative path returned by put().
*/
public function file( string $path ): ?string {
$file = $this->absolute( $path );
return null !== $file && is_file( $file ) ? $file : null;
}
/**
* Deletes a stored file. True when a file was removed.
*
* @param string $path Relative path returned by put().
*/
public function delete( string $path ): bool {
$file = $this->absolute( $path );
return null !== $file && is_file( $file ) && unlink( $file );
}
/**
* Stored files of one kind last modified before $before, oldest first, for the cleanup job.
*
* @param string $kind self::UPLOADS or self::RESULTS.
* @param int $before Unix time.
* @param int $limit Maximum paths.
* @return list Relative paths.
*/
public function stale( string $kind, int $before, int $limit ): array {
$found = array();
foreach ( (array) glob( $this->root . '/' . $kind . '/*.jpg' ) as $file ) {
$path = $kind . '/' . basename( (string) $file );
$mtime = null !== $this->absolute( $path ) ? filemtime( (string) $file ) : false;
if ( false !== $mtime && $mtime < $before ) {
$found[ $path ] = $mtime;
}
}
asort( $found );
return array_slice( array_keys( $found ), 0, max( 0, $limit ) );
}
/**
* Removes the files this store created, then its folders when they are empty. Used on uninstall.
*
* Only names this store could have made are deleted, so a folder shared with the site owner's
* own files (FITZARO_STORAGE_DIR) keeps them.
*/
public function destroy(): void {
foreach ( array( self::UPLOADS, self::RESULTS ) as $kind ) {
$folder = $this->root . '/' . $kind;
if ( ! is_dir( $folder ) ) {
continue;
}
foreach ( (array) glob( $folder . '/*.jpg' ) as $file ) {
if ( null !== $this->absolute( $kind . '/' . basename( (string) $file ) ) ) {
unlink( (string) $file );
}
}
$this->remove_folder( $folder );
}
if ( is_dir( $this->root ) ) {
$this->remove_folder( $this->root );
}
}
/**
* Deletes the deny files of a folder, then the folder if nothing else is left in it.
*
* @param string $folder Absolute path.
*/
private function remove_folder( string $folder ): void {
foreach ( array_keys( self::DENY_FILES ) as $name ) {
if ( is_file( $folder . '/' . $name ) ) {
unlink( $folder . '/' . $name );
}
}
if ( array( '.', '..' ) === (array) scandir( $folder ) ) {
rmdir( $folder );
}
}
/**
* Absolute path for a relative one, or null when it is not a name this store could have made.
*
* @param string $path Relative path.
*/
private function absolute( string $path ): ?string {
return 1 === preg_match( self::VALID_PATH, $path ) ? $this->root . '/' . $path : null;
}
/**
* Creates a folder with its deny files.
*
* @param string $folder Absolute path.
* @throws \RuntimeException When the folder cannot be created.
*/
private function ensure_folder( string $folder ): void {
if ( ! is_dir( $folder ) && ! mkdir( $folder, 0755, true ) && ! is_dir( $folder ) ) {
throw new \RuntimeException( 'Could not create the storage folder.' );
}
foreach ( self::DENY_FILES as $name => $content ) {
if ( ! is_file( $folder . '/' . $name ) ) {
file_put_contents( $folder . '/' . $name, $content );
}
}
}
}