" "# 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 ); } } } }