Issues (158)

src/Collector/Stream/HttpStreamProxy.php (20 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Debug\Collector\Stream;
6
7
use Yiisoft\Strings\CombinedRegexp;
8
use Yiisoft\Yii\Debug\Helper\BacktraceIgnoreMatcher;
9
use Yiisoft\Yii\Debug\Helper\StreamWrapper\StreamWrapper;
10
use Yiisoft\Yii\Debug\Helper\StreamWrapper\StreamWrapperInterface;
11
12
use function stream_get_wrappers;
13
14
use const SEEK_SET;
15
16
final class HttpStreamProxy implements StreamWrapperInterface
17
{
18
    public static bool $registered = false;
19
    public static array $ignoredPathPatterns = [];
20
    public static array $ignoredClasses = [];
21
    public static array $ignoredUrls = [];
22
    /**
23
     * @var resource|null
24
     */
25
    public $context;
26
    public StreamWrapper $decorated;
27
    public bool $ignored = false;
28
29
    public static ?HttpStreamCollector $collector = null;
30
    public array $operations = [];
31
32
    public function __construct()
33
    {
34
        $this->decorated = new StreamWrapper();
35
        $this->decorated->context = $this->context;
36
    }
37
38
    public function __call(string $name, array $arguments)
39
    {
40
        try {
41
            self::unregister();
42
            return $this->decorated->{$name}(...$arguments);
43
        } finally {
44
            self::register();
45
        }
46
    }
47
48
    public function __destruct()
49
    {
50
        if (self::$collector === null) {
51
            return;
52
        }
53
        foreach ($this->operations as $name => $operation) {
54
            self::$collector->collect(
55
                operation: $name,
56
                path: $operation['path'],
57
                args: $operation['args'],
58
            );
59
        }
60
        self::unregister();
61
    }
62
63
    public function __get(string $name)
64
    {
65
        return $this->decorated->{$name};
66
    }
67
68
    public static function register(): void
69
    {
70
        if (self::$registered) {
71
            return;
72
        }
73
        /**
74
         * It's important to trigger autoloader before unregistering the file stream handler
75
         */
76
        class_exists(BacktraceIgnoreMatcher::class);
77
        class_exists(StreamWrapper::class);
78
        class_exists(CombinedRegexp::class);
79
        stream_wrapper_unregister('http');
80
        stream_wrapper_register('http', self::class, STREAM_IS_URL);
81
82
        if (in_array('https', stream_get_wrappers(), true)) {
83
            stream_wrapper_unregister('https');
84
            stream_wrapper_register('https', self::class, STREAM_IS_URL);
85
        }
86
87
        self::$registered = true;
88
    }
89
90
    public static function unregister(): void
91
    {
92
        if (!self::$registered) {
93
            return;
94
        }
95
        @stream_wrapper_restore('http');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for stream_wrapper_restore(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

95
        /** @scrutinizer ignore-unhandled */ @stream_wrapper_restore('http');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
96
        @stream_wrapper_restore('https');
97
        self::$registered = false;
98
    }
99
100
    public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
0 ignored issues
show
Method name "HttpStreamProxy::stream_open" is not in camel caps format
Loading history...
101
    {
102
        $this->ignored = $this->isIgnored($path);
103
        return $this->__call(__FUNCTION__, func_get_args());
104
    }
105
106
    public function stream_read(int $count): string|false
0 ignored issues
show
Method name "HttpStreamProxy::stream_read" is not in camel caps format
Loading history...
107
    {
108
        if (!$this->ignored) {
109
            /**
110
             * @psalm-suppress PossiblyNullArgument
111
             */
112
            $metadata = stream_get_meta_data($this->decorated->stream);
113
            $context = $this->decorated->context === null
114
                ? null
115
                : stream_context_get_options($this->decorated->context);
116
            /**
117
             * @link https://www.php.net/manual/en/context.http.php
118
             */
119
            $method = $context['http']['method'] ?? $context['https']['method'] ?? 'GET';
120
            $headers = (array) ($context['http']['header'] ?? $context['https']['header'] ?? []);
121
122
            $this->operations['read'] = [
123
                'path' => $this->decorated->filename,
124
                'args' => [
125
                    'method' => $method,
126
                    'response_headers' => $metadata['wrapper_data'],
127
                    'request_headers' => $headers,
128
                ],
129
            ];
130
        }
131
        return $this->__call(__FUNCTION__, func_get_args());
132
    }
133
134
    public function stream_set_option(int $option, int $arg1, ?int $arg2): bool
0 ignored issues
show
Method name "HttpStreamProxy::stream_set_option" is not in camel caps format
Loading history...
135
    {
136
        return $this->__call(__FUNCTION__, func_get_args());
137
    }
138
139
    public function stream_tell(): int
0 ignored issues
show
Method name "HttpStreamProxy::stream_tell" is not in camel caps format
Loading history...
140
    {
141
        return $this->__call(__FUNCTION__, func_get_args());
142
    }
143
144
    public function stream_eof(): bool
0 ignored issues
show
Method name "HttpStreamProxy::stream_eof" is not in camel caps format
Loading history...
145
    {
146
        return $this->__call(__FUNCTION__, func_get_args());
147
    }
148
149
    public function stream_seek(int $offset, int $whence = SEEK_SET): bool
0 ignored issues
show
Method name "HttpStreamProxy::stream_seek" is not in camel caps format
Loading history...
150
    {
151
        return $this->__call(__FUNCTION__, func_get_args());
152
    }
153
154
    public function stream_cast(int $castAs)
0 ignored issues
show
Method name "HttpStreamProxy::stream_cast" is not in camel caps format
Loading history...
155
    {
156
        return $this->__call(__FUNCTION__, func_get_args());
157
    }
158
159
    public function stream_stat(): array|false
0 ignored issues
show
Method name "HttpStreamProxy::stream_stat" is not in camel caps format
Loading history...
160
    {
161
        return $this->__call(__FUNCTION__, func_get_args());
162
    }
163
164
    public function dir_closedir(): bool
0 ignored issues
show
Method name "HttpStreamProxy::dir_closedir" is not in camel caps format
Loading history...
165
    {
166
        return $this->__call(__FUNCTION__, func_get_args());
167
    }
168
169
    public function dir_opendir(string $path, int $options): bool
0 ignored issues
show
Method name "HttpStreamProxy::dir_opendir" is not in camel caps format
Loading history...
170
    {
171
        return $this->__call(__FUNCTION__, func_get_args());
172
    }
173
174
    public function dir_readdir(): false|string
0 ignored issues
show
Method name "HttpStreamProxy::dir_readdir" is not in camel caps format
Loading history...
175
    {
176
        if (!$this->ignored) {
177
            $this->operations[__FUNCTION__] = [
178
                'path' => $this->decorated->filename,
179
                'args' => [],
180
            ];
181
        }
182
        return $this->__call(__FUNCTION__, func_get_args());
183
    }
184
185
    public function dir_rewinddir(): bool
0 ignored issues
show
Method name "HttpStreamProxy::dir_rewinddir" is not in camel caps format
Loading history...
186
    {
187
        return $this->__call(__FUNCTION__, func_get_args());
188
    }
189
190
    public function mkdir(string $path, int $mode, int $options): bool
191
    {
192
        if (!$this->ignored) {
193
            $this->operations[__FUNCTION__] = [
194
                'path' => $path,
195
                'args' => [
196
                    'mode' => $mode,
197
                    'options' => $options,
198
                ],
199
            ];
200
        }
201
        return $this->__call(__FUNCTION__, func_get_args());
202
    }
203
204
    public function rename(string $path_from, string $path_to): bool
205
    {
206
        if (!$this->ignored) {
207
            $this->operations[__FUNCTION__] = [
208
                'path' => $path_from,
209
                'args' => [
210
                    'path_to' => $path_to,
211
                ],
212
            ];
213
        }
214
        return $this->__call(__FUNCTION__, func_get_args());
215
    }
216
217
    public function rmdir(string $path, int $options): bool
218
    {
219
        if (!$this->ignored) {
220
            $this->operations[__FUNCTION__] = [
221
                'path' => $path,
222
                'args' => [
223
                    'options' => $options,
224
                ],
225
            ];
226
        }
227
        return $this->__call(__FUNCTION__, func_get_args());
228
    }
229
230
    public function stream_close(): void
0 ignored issues
show
Method name "HttpStreamProxy::stream_close" is not in camel caps format
Loading history...
231
    {
232
        $this->__call(__FUNCTION__, func_get_args());
233
    }
234
235
    public function stream_flush(): bool
0 ignored issues
show
Method name "HttpStreamProxy::stream_flush" is not in camel caps format
Loading history...
236
    {
237
        return $this->__call(__FUNCTION__, func_get_args());
238
    }
239
240
    public function stream_lock(int $operation): bool
0 ignored issues
show
Method name "HttpStreamProxy::stream_lock" is not in camel caps format
Loading history...
241
    {
242
        return $this->__call(__FUNCTION__, func_get_args());
243
    }
244
245
    public function stream_metadata(string $path, int $option, mixed $value): bool
0 ignored issues
show
Method name "HttpStreamProxy::stream_metadata" is not in camel caps format
Loading history...
246
    {
247
        return $this->__call(__FUNCTION__, func_get_args());
248
    }
249
250
    public function stream_truncate(int $new_size): bool
0 ignored issues
show
Method name "HttpStreamProxy::stream_truncate" is not in camel caps format
Loading history...
251
    {
252
        return $this->__call(__FUNCTION__, func_get_args());
253
    }
254
255
    public function stream_write(string $data): int
0 ignored issues
show
Method name "HttpStreamProxy::stream_write" is not in camel caps format
Loading history...
256
    {
257
        if (!$this->ignored) {
258
            $this->operations['write'] = [
259
                'path' => $this->decorated->filename,
260
                'args' => [],
261
            ];
262
        }
263
264
        return $this->__call(__FUNCTION__, func_get_args());
265
    }
266
267
    public function unlink(string $path): bool
268
    {
269
        if (!$this->ignored) {
270
            $this->operations[__FUNCTION__] = [
271
                'path' => $path,
272
                'args' => [],
273
            ];
274
        }
275
        return $this->__call(__FUNCTION__, func_get_args());
276
    }
277
278
    public function url_stat(string $path, int $flags): array|false
0 ignored issues
show
Method name "HttpStreamProxy::url_stat" is not in camel caps format
Loading history...
279
    {
280
        return $this->__call(__FUNCTION__, func_get_args());
281
    }
282
283
    private function isIgnored(string $url): bool
284
    {
285
        if (BacktraceIgnoreMatcher::doesStringMatchPattern($url, self::$ignoredUrls)) {
286
            return true;
287
        }
288
289
        $backtrace = debug_backtrace();
290
        return BacktraceIgnoreMatcher::isIgnoredByClass($backtrace, self::$ignoredClasses)
291
            || BacktraceIgnoreMatcher::isIgnoredByFile($backtrace, self::$ignoredPathPatterns);
292
    }
293
}
294