Merge "Type hint against LinkTarget in WatchedItemStore"
[lhc/web/wiklou.git] / includes / libs / ParamValidator / Util / UploadedFileStream.php
1 <?php
2
3 namespace Wikimedia\ParamValidator\Util;
4
5 use Exception;
6 use Psr\Http\Message\StreamInterface;
7 use RuntimeException;
8 use Throwable;
9 use Wikimedia\AtEase\AtEase;
10
11 /**
12 * Implementation of StreamInterface for a file in $_FILES
13 *
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.
17 *
18 * @internal
19 * @since 1.34
20 */
21 class UploadedFileStream implements StreamInterface {
22
23 /** @var resource File handle */
24 private $fp;
25
26 /** @var int|false|null File size. False if not set yet. */
27 private $size = false;
28
29 /**
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
35 * @return mixed
36 * @throws RuntimeException if $func returns $fail
37 */
38 private static function quietCall( callable $func, array $args, $fail, $msg ) {
39 // TODO remove the function_exists check once we drop HHVM support
40 if ( function_exists( 'error_clear_last' ) ) {
41 error_clear_last();
42 }
43 $ret = AtEase::quietCall( $func, ...$args );
44 if ( $ret === $fail ) {
45 $err = error_get_last();
46 throw new RuntimeException( "$msg: " . ( $err['message'] ?? 'Unknown error' ) );
47 }
48 return $ret;
49 }
50
51 /**
52 * @param string $filename
53 */
54 public function __construct( $filename ) {
55 $this->fp = self::quietCall( 'fopen', [ $filename, 'r' ], false, 'Failed to open file' );
56 }
57
58 /**
59 * Check if the stream is open
60 * @throws RuntimeException if closed
61 */
62 private function checkOpen() {
63 if ( !$this->fp ) {
64 throw new RuntimeException( 'Stream is not open' );
65 }
66 }
67
68 public function __destruct() {
69 $this->close();
70 }
71
72 public function __toString() {
73 try {
74 $this->seek( 0 );
75 return $this->getContents();
76 } catch ( Exception $ex ) {
77 // Not allowed to throw
78 return '';
79 } catch ( Throwable $ex ) {
80 // Not allowed to throw
81 return '';
82 }
83 }
84
85 public function close() {
86 if ( $this->fp ) {
87 // Spec doesn't care about close errors.
88 AtEase::quietCall( 'fclose', $this->fp );
89 $this->fp = null;
90 }
91 }
92
93 public function detach() {
94 $ret = $this->fp;
95 $this->fp = null;
96 return $ret;
97 }
98
99 public function getSize() {
100 if ( $this->size === false ) {
101 $this->size = null;
102
103 if ( $this->fp ) {
104 // Spec doesn't care about errors here.
105 $stat = AtEase::quietCall( 'fstat', $this->fp );
106 $this->size = $stat['size'] ?? null;
107 }
108 }
109
110 return $this->size;
111 }
112
113 public function tell() {
114 $this->checkOpen();
115 return self::quietCall( 'ftell', [ $this->fp ], -1, 'Cannot determine stream position' );
116 }
117
118 public function eof() {
119 // Spec doesn't care about errors here.
120 return !$this->fp || AtEase::quietCall( 'feof', $this->fp );
121 }
122
123 public function isSeekable() {
124 return (bool)$this->fp;
125 }
126
127 public function seek( $offset, $whence = SEEK_SET ) {
128 $this->checkOpen();
129 self::quietCall( 'fseek', [ $this->fp, $offset, $whence ], -1, 'Seek failed' );
130 }
131
132 public function rewind() {
133 $this->seek( 0 );
134 }
135
136 public function isWritable() {
137 return false;
138 }
139
140 public function write( $string ) {
141 $this->checkOpen();
142 throw new RuntimeException( 'Stream is read-only' );
143 }
144
145 public function isReadable() {
146 return (bool)$this->fp;
147 }
148
149 public function read( $length ) {
150 $this->checkOpen();
151 return self::quietCall( 'fread', [ $this->fp, $length ], false, 'Read failed' );
152 }
153
154 public function getContents() {
155 $this->checkOpen();
156 return self::quietCall( 'stream_get_contents', [ $this->fp ], false, 'Read failed' );
157 }
158
159 public function getMetadata( $key = null ) {
160 $this->checkOpen();
161 $ret = self::quietCall( 'stream_get_meta_data', [ $this->fp ], false, 'Metadata fetch failed' );
162 if ( $key !== null ) {
163 $ret = $ret[$key] ?? null;
164 }
165 return $ret;
166 }
167
168 }