Passed
Pull Request — master (#215)
by Dmitriy
13:45
created

HttpStreamProxy::__get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Debug\Collector\Stream;
6
7
use Yiisoft\Yii\Debug\Helper\BacktraceIgnoreMatcher;
8
use Yiisoft\Yii\Debug\Helper\StreamWrapper\StreamWrapper;
9
use Yiisoft\Yii\Debug\Helper\StreamWrapper\StreamWrapperInterface;
10
11
use const SEEK_SET;
12
13
class HttpStreamProxy implements StreamWrapperInterface
14
{
15
    public static bool $registered = false;
16
    public static array $ignoredPathPatterns = [];
17
    public static array $ignoredClasses = [];
18
    public static array $ignoredUrls = [];
19
    /**
20
     * @var resource|null
21
     */
22
    public $context;
23
    public StreamWrapperInterface $decorated;
24
    public bool $ignored = false;
25
26
    public static ?HttpStreamCollector $collector = null;
27
    public array $operations = [];
28
29
    public function __construct()
30
    {
31
        $this->decorated = new StreamWrapper();
32
        $this->decorated->context = $this->context;
0 ignored issues
show
Bug introduced by
Accessing context on the interface Yiisoft\Yii\Debug\Helper...\StreamWrapperInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
33
    }
34
35
    public function __call(string $name, array $arguments)
36
    {
37
        try {
38
            self::unregister();
39
            return $this->decorated->{$name}(...$arguments);
40
        } finally {
41
            self::register();
42
        }
43
    }
44
45
    public function __destruct()
46
    {
47
        foreach ($this->operations as $name => $operation) {
48
            self::$collector->collect(
0 ignored issues
show
Bug introduced by
The method collect() does not exist on null. ( Ignorable by Annotation )

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

48
            self::$collector->/** @scrutinizer ignore-call */ 
49
                              collect(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
49
                operation: $name,
50
                path: $operation['path'],
51
                args: $operation['args'],
52
            );
53
        }
54
    }
55
56
    public function __get(string $name)
57
    {
58
        return $this->decorated->{$name};
59
    }
60
61
    public static function register(): void
62
    {
63
        if (self::$registered) {
64
            return;
65
        }
66
        /**
67
         * It's important to trigger autoloader before unregistering the file stream handler
68
         */
69
        class_exists(StreamWrapper::class);
70
        stream_wrapper_unregister('http');
71
        stream_wrapper_register('http', self::class, STREAM_IS_URL);
72
        stream_wrapper_unregister('https');
73
        stream_wrapper_register('https', self::class, STREAM_IS_URL);
74
        self::$registered = true;
75
    }
76
77
    public static function unregister(): void
78
    {
79
        if (!self::$registered) {
80
            return;
81
        }
82
        @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

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