3 namespace Wikimedia\ParamValidator\Util
;
5 use Psr\Http\Message\StreamInterface
;
9 use Wikimedia\AtEase\AtEase
;
12 * Implementation of StreamInterface for a file in $_FILES
14 * This exists so ParamValidator needn't depend on any specific PSR-7
15 * implementation for a class implementing UploadedFileInterface. It shouldn't
16 * be used directly by other code.
21 class UploadedFileStream
implements Stringable
, StreamInterface
{
23 /** @var resource|null File handle */
26 /** @var int|false|null File size. False if not set yet. */
27 private $size = false;
30 * Call, throwing on error
31 * @param callable $func Callable to call
32 * @param array $args Arguments
33 * @param mixed $fail Failure return value
34 * @param string $msg Message prefix
36 * @throws RuntimeException if $func returns $fail
38 private static function quietCall( callable
$func, array $args, $fail, $msg ) {
40 $ret = AtEase
::quietCall( $func, ...$args );
41 if ( $ret === $fail ) {
42 $err = error_get_last();
43 throw new RuntimeException( "$msg: " . ( $err['message'] ??
'Unknown error' ) );
49 * @param string $filename
51 public function __construct( $filename ) {
52 $this->fp
= self
::quietCall( 'fopen', [ $filename, 'r' ], false, 'Failed to open file' );
56 * Check if the stream is open
57 * @throws RuntimeException if closed
59 private function checkOpen() {
61 throw new RuntimeException( 'Stream is not open' );
65 public function __destruct() {
69 public function __toString() {
72 return $this->getContents();
73 } catch ( Throwable
$ex ) {
74 // Not allowed to throw
79 public function close() {
81 // Spec doesn't care about close errors.
83 // PHP 7 emits warnings, suppress
84 AtEase
::quietCall( 'fclose', $this->fp
);
85 } catch ( \TypeError
$unused ) {
86 // While PHP 8 throws exceptions, ignore
92 public function detach() {
98 public function getSize() {
99 if ( $this->size
=== false ) {
103 // Spec doesn't care about errors here.
105 $stat = AtEase
::quietCall( 'fstat', $this->fp
);
106 } catch ( \TypeError
$unused ) {
108 $this->size
= $stat['size'] ??
null;
115 public function tell() {
117 return self
::quietCall( 'ftell', [ $this->fp
], -1, 'Cannot determine stream position' );
120 public function eof() {
121 // Spec doesn't care about errors here.
123 return !$this->fp || AtEase
::quietCall( 'feof', $this->fp
);
124 } catch ( \TypeError
$unused ) {
129 public function isSeekable() {
130 return (bool)$this->fp
;
133 public function seek( $offset, $whence = SEEK_SET
) {
135 self
::quietCall( 'fseek', [ $this->fp
, $offset, $whence ], -1, 'Seek failed' );
138 public function rewind() {
142 public function isWritable() {
146 public function write( $string ) {
147 // @phan-suppress-previous-line PhanPluginNeverReturnMethod
149 throw new RuntimeException( 'Stream is read-only' );
152 public function isReadable() {
153 return (bool)$this->fp
;
156 public function read( $length ) {
158 return self
::quietCall( 'fread', [ $this->fp
, $length ], false, 'Read failed' );
161 public function getContents() {
163 return self
::quietCall( 'stream_get_contents', [ $this->fp
], false, 'Read failed' );
166 public function getMetadata( $key = null ) {
168 $ret = self
::quietCall( 'stream_get_meta_data', [ $this->fp
], false, 'Metadata fetch failed' );
169 if ( $key !== null ) {
170 $ret = $ret[$key] ??
null;