MediaWiki master
UploadedFileStream.php
Go to the documentation of this file.
1<?php
2
4
5use Psr\Http\Message\StreamInterface;
6use RuntimeException;
7use Stringable;
8use Throwable;
9use Wikimedia\AtEase\AtEase;
10
21class UploadedFileStream implements Stringable, StreamInterface {
22
24 private $fp;
25
27 private $size = false;
28
38 private static function quietCall( callable $func, array $args, $fail, $msg ) {
39 error_clear_last();
40 $ret = AtEase::quietCall( $func, ...$args );
41 if ( $ret === $fail ) {
42 $err = error_get_last();
43 throw new RuntimeException( "$msg: " . ( $err['message'] ?? 'Unknown error' ) );
44 }
45 return $ret;
46 }
47
51 public function __construct( $filename ) {
52 $this->fp = self::quietCall( 'fopen', [ $filename, 'r' ], false, 'Failed to open file' );
53 }
54
59 private function checkOpen() {
60 if ( !$this->fp ) {
61 throw new RuntimeException( 'Stream is not open' );
62 }
63 }
64
65 public function __destruct() {
66 $this->close();
67 }
68
69 public function __toString() {
70 try {
71 $this->seek( 0 );
72 return $this->getContents();
73 } catch ( Throwable $ex ) {
74 // Not allowed to throw
75 return '';
76 }
77 }
78
79 public function close() {
80 if ( $this->fp ) {
81 // Spec doesn't care about close errors.
82 try {
83 // PHP 7 emits warnings, suppress
84 AtEase::quietCall( 'fclose', $this->fp );
85 } catch ( \TypeError $unused ) {
86 // While PHP 8 throws exceptions, ignore
87 }
88 $this->fp = null;
89 }
90 }
91
92 public function detach() {
93 $ret = $this->fp;
94 $this->fp = null;
95 return $ret;
96 }
97
98 public function getSize() {
99 if ( $this->size === false ) {
100 $this->size = null;
101
102 if ( $this->fp ) {
103 // Spec doesn't care about errors here.
104 try {
105 $stat = AtEase::quietCall( 'fstat', $this->fp );
106 } catch ( \TypeError $unused ) {
107 }
108 $this->size = $stat['size'] ?? null;
109 }
110 }
111
112 return $this->size;
113 }
114
115 public function tell() {
116 $this->checkOpen();
117 return self::quietCall( 'ftell', [ $this->fp ], -1, 'Cannot determine stream position' );
118 }
119
120 public function eof() {
121 // Spec doesn't care about errors here.
122 try {
123 return !$this->fp || AtEase::quietCall( 'feof', $this->fp );
124 } catch ( \TypeError $unused ) {
125 return true;
126 }
127 }
128
129 public function isSeekable() {
130 return (bool)$this->fp;
131 }
132
133 public function seek( $offset, $whence = SEEK_SET ) {
134 $this->checkOpen();
135 self::quietCall( 'fseek', [ $this->fp, $offset, $whence ], -1, 'Seek failed' );
136 }
137
138 public function rewind() {
139 $this->seek( 0 );
140 }
141
142 public function isWritable() {
143 return false;
144 }
145
146 public function write( $string ) {
147 // @phan-suppress-previous-line PhanPluginNeverReturnMethod
148 $this->checkOpen();
149 throw new RuntimeException( 'Stream is read-only' );
150 }
151
152 public function isReadable() {
153 return (bool)$this->fp;
154 }
155
156 public function read( $length ) {
157 $this->checkOpen();
158 return self::quietCall( 'fread', [ $this->fp, $length ], false, 'Read failed' );
159 }
160
161 public function getContents() {
162 $this->checkOpen();
163 return self::quietCall( 'stream_get_contents', [ $this->fp ], false, 'Read failed' );
164 }
165
166 public function getMetadata( $key = null ) {
167 $this->checkOpen();
168 $ret = self::quietCall( 'stream_get_meta_data', [ $this->fp ], false, 'Metadata fetch failed' );
169 if ( $key !== null ) {
170 $ret = $ret[$key] ?? null;
171 }
172 return $ret;
173 }
174
175}
Implementation of StreamInterface for a file in $_FILES.