Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.00% covered (success)
92.00%
46 / 50
84.21% covered (warning)
84.21%
16 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
UploadedFileStream
92.00% covered (success)
92.00%
46 / 50
84.21% covered (warning)
84.21%
16 / 19
30.46
0.00% covered (danger)
0.00%
0 / 1
 quietCall
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkOpen
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 __destruct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 close
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 detach
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getSize
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 tell
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 eof
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
5.67
 isSeekable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 seek
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 rewind
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWritable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 write
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isReadable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 read
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getContents
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMetadata
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Wikimedia\ParamValidator\Util;
4
5use Psr\Http\Message\StreamInterface;
6use RuntimeException;
7use Throwable;
8use Wikimedia\AtEase\AtEase;
9
10/**
11 * Implementation of StreamInterface for a file in $_FILES
12 *
13 * This exists so ParamValidator needn't depend on any specific PSR-7
14 * implementation for a class implementing UploadedFileInterface. It shouldn't
15 * be used directly by other code.
16 *
17 * @internal
18 * @since 1.34
19 */
20class UploadedFileStream implements StreamInterface {
21
22    /** @var resource|null File handle */
23    private $fp;
24
25    /** @var int|false|null File size. False if not set yet. */
26    private $size = false;
27
28    /**
29     * Call, throwing on error
30     * @param callable $func Callable to call
31     * @param array $args Arguments
32     * @param mixed $fail Failure return value
33     * @param string $msg Message prefix
34     * @return mixed
35     * @throws RuntimeException if $func returns $fail
36     */
37    private static function quietCall( callable $func, array $args, $fail, $msg ) {
38        error_clear_last();
39        $ret = AtEase::quietCall( $func, ...$args );
40        if ( $ret === $fail ) {
41            $err = error_get_last();
42            throw new RuntimeException( "$msg" . ( $err['message'] ?? 'Unknown error' ) );
43        }
44        return $ret;
45    }
46
47    /**
48     * @param string $filename
49     */
50    public function __construct( $filename ) {
51        $this->fp = self::quietCall( 'fopen', [ $filename, 'r' ], false, 'Failed to open file' );
52    }
53
54    /**
55     * Check if the stream is open
56     * @throws RuntimeException if closed
57     */
58    private function checkOpen() {
59        if ( !$this->fp ) {
60            throw new RuntimeException( 'Stream is not open' );
61        }
62    }
63
64    public function __destruct() {
65        $this->close();
66    }
67
68    public function __toString() {
69        try {
70            $this->seek( 0 );
71            return $this->getContents();
72        } catch ( Throwable $ex ) {
73            // Not allowed to throw
74            return '';
75        }
76    }
77
78    public function close() {
79        if ( $this->fp ) {
80            // Spec doesn't care about close errors.
81            try {
82                // PHP 7 emits warnings, suppress
83                AtEase::quietCall( 'fclose', $this->fp );
84            } catch ( \TypeError $unused ) {
85                // While PHP 8 throws exceptions, ignore
86            }
87            $this->fp = null;
88        }
89    }
90
91    public function detach() {
92        $ret = $this->fp;
93        $this->fp = null;
94        return $ret;
95    }
96
97    public function getSize() {
98        if ( $this->size === false ) {
99            $this->size = null;
100
101            if ( $this->fp ) {
102                // Spec doesn't care about errors here.
103                try {
104                    $stat = AtEase::quietCall( 'fstat', $this->fp );
105                } catch ( \TypeError $unused ) {
106                }
107                $this->size = $stat['size'] ?? null;
108            }
109        }
110
111        return $this->size;
112    }
113
114    public function tell() {
115        $this->checkOpen();
116        return self::quietCall( 'ftell', [ $this->fp ], -1, 'Cannot determine stream position' );
117    }
118
119    public function eof() {
120        // Spec doesn't care about errors here.
121        try {
122            return !$this->fp || AtEase::quietCall( 'feof', $this->fp );
123        } catch ( \TypeError $unused ) {
124            return true;
125        }
126    }
127
128    public function isSeekable() {
129        return (bool)$this->fp;
130    }
131
132    public function seek( $offset, $whence = SEEK_SET ) {
133        $this->checkOpen();
134        self::quietCall( 'fseek', [ $this->fp, $offset, $whence ], -1, 'Seek failed' );
135    }
136
137    public function rewind() {
138        $this->seek( 0 );
139    }
140
141    public function isWritable() {
142        return false;
143    }
144
145    public function write( $string ) {
146        // @phan-suppress-previous-line PhanPluginNeverReturnMethod
147        $this->checkOpen();
148        throw new RuntimeException( 'Stream is read-only' );
149    }
150
151    public function isReadable() {
152        return (bool)$this->fp;
153    }
154
155    public function read( $length ) {
156        $this->checkOpen();
157        return self::quietCall( 'fread', [ $this->fp, $length ], false, 'Read failed' );
158    }
159
160    public function getContents() {
161        $this->checkOpen();
162        return self::quietCall( 'stream_get_contents', [ $this->fp ], false, 'Read failed' );
163    }
164
165    public function getMetadata( $key = null ) {
166        $this->checkOpen();
167        $ret = self::quietCall( 'stream_get_meta_data', [ $this->fp ], false, 'Metadata fetch failed' );
168        if ( $key !== null ) {
169            $ret = $ret[$key] ?? null;
170        }
171        return $ret;
172    }
173
174}