File: UploadedFileStream.php

package info (click to toggle)
mediawiki 1%3A1.43.3%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 417,464 kB
  • sloc: php: 1,062,949; javascript: 664,290; sql: 9,714; python: 5,458; xml: 3,489; sh: 1,131; makefile: 64
file content (175 lines) | stat: -rw-r--r-- 4,014 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
<?php

namespace Wikimedia\ParamValidator\Util;

use Psr\Http\Message\StreamInterface;
use RuntimeException;
use Stringable;
use Throwable;
use Wikimedia\AtEase\AtEase;

/**
 * Implementation of StreamInterface for a file in $_FILES
 *
 * This exists so ParamValidator needn't depend on any specific PSR-7
 * implementation for a class implementing UploadedFileInterface. It shouldn't
 * be used directly by other code.
 *
 * @internal
 * @since 1.34
 */
class UploadedFileStream implements Stringable, StreamInterface {

	/** @var resource|null File handle */
	private $fp;

	/** @var int|false|null File size. False if not set yet. */
	private $size = false;

	/**
	 * Call, throwing on error
	 * @param callable $func Callable to call
	 * @param array $args Arguments
	 * @param mixed $fail Failure return value
	 * @param string $msg Message prefix
	 * @return mixed
	 * @throws RuntimeException if $func returns $fail
	 */
	private static function quietCall( callable $func, array $args, $fail, $msg ) {
		error_clear_last();
		$ret = AtEase::quietCall( $func, ...$args );
		if ( $ret === $fail ) {
			$err = error_get_last();
			throw new RuntimeException( "$msg: " . ( $err['message'] ?? 'Unknown error' ) );
		}
		return $ret;
	}

	/**
	 * @param string $filename
	 */
	public function __construct( $filename ) {
		$this->fp = self::quietCall( 'fopen', [ $filename, 'r' ], false, 'Failed to open file' );
	}

	/**
	 * Check if the stream is open
	 * @throws RuntimeException if closed
	 */
	private function checkOpen() {
		if ( !$this->fp ) {
			throw new RuntimeException( 'Stream is not open' );
		}
	}

	public function __destruct() {
		$this->close();
	}

	public function __toString() {
		try {
			$this->seek( 0 );
			return $this->getContents();
		} catch ( Throwable $ex ) {
			// Not allowed to throw
			return '';
		}
	}

	public function close() {
		if ( $this->fp ) {
			// Spec doesn't care about close errors.
			try {
				// PHP 7 emits warnings, suppress
				AtEase::quietCall( 'fclose', $this->fp );
			} catch ( \TypeError $unused ) {
				// While PHP 8 throws exceptions, ignore
			}
			$this->fp = null;
		}
	}

	public function detach() {
		$ret = $this->fp;
		$this->fp = null;
		return $ret;
	}

	public function getSize() {
		if ( $this->size === false ) {
			$this->size = null;

			if ( $this->fp ) {
				// Spec doesn't care about errors here.
				try {
					$stat = AtEase::quietCall( 'fstat', $this->fp );
				} catch ( \TypeError $unused ) {
				}
				$this->size = $stat['size'] ?? null;
			}
		}

		return $this->size;
	}

	public function tell() {
		$this->checkOpen();
		return self::quietCall( 'ftell', [ $this->fp ], -1, 'Cannot determine stream position' );
	}

	public function eof() {
		// Spec doesn't care about errors here.
		try {
			return !$this->fp || AtEase::quietCall( 'feof', $this->fp );
		} catch ( \TypeError $unused ) {
			return true;
		}
	}

	public function isSeekable() {
		return (bool)$this->fp;
	}

	public function seek( $offset, $whence = SEEK_SET ) {
		$this->checkOpen();
		self::quietCall( 'fseek', [ $this->fp, $offset, $whence ], -1, 'Seek failed' );
	}

	public function rewind() {
		$this->seek( 0 );
	}

	public function isWritable() {
		return false;
	}

	public function write( $string ) {
		// @phan-suppress-previous-line PhanPluginNeverReturnMethod
		$this->checkOpen();
		throw new RuntimeException( 'Stream is read-only' );
	}

	public function isReadable() {
		return (bool)$this->fp;
	}

	public function read( $length ) {
		$this->checkOpen();
		return self::quietCall( 'fread', [ $this->fp, $length ], false, 'Read failed' );
	}

	public function getContents() {
		$this->checkOpen();
		return self::quietCall( 'stream_get_contents', [ $this->fp ], false, 'Read failed' );
	}

	public function getMetadata( $key = null ) {
		$this->checkOpen();
		$ret = self::quietCall( 'stream_get_meta_data', [ $this->fp ], false, 'Metadata fetch failed' );
		if ( $key !== null ) {
			$ret = $ret[$key] ?? null;
		}
		return $ret;
	}

}