Issues (4)

src/RequirementsChecker.php (4 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Requirements;
6
7
/**
8
 * YiiRequirementChecker allows checking, if current system meets the requirements for running the Yii application.
9
 * This class allows rendering of the check report for the web and console application interface.
10
 *
11
 * Example:
12
 *
13
 * ```php
14
 * require_once 'path/to/YiiRequirementChecker.php';
15
 * $requirementsChecker = new YiiRequirementChecker();
16
 * $requirements = [
17
 *     [
18
 *         'name' => 'PHP Some Extension',
19
 *         'mandatory' => true`,
20
 *         'condition' => extension_loaded('some_extension'),
21
 *         'by' => 'Some application feature',
22
 *         'memo' => 'PHP extension "some_extension" required',
23
 *     ],
24
 * ];
25
 * $requirementsChecker
26
 *     ->check($requirements)
27
 *     ->render();
28
 * ```
29
 *
30
 * If you wish to render the report with your own representation, use {@see getResult()} instead of {@see render()}.
31
 *
32
 * Requirement condition could be in format "eval:PHP expression".
33
 * In this case specified PHP expression will be evaluated in the context of this class instance.
34
 * For example:
35
 *
36
 * ```php
37
 * $requirements = [
38
 *     [
39
 *         'name' => 'Upload max file size',
40
 *         'condition' => 'eval:$this->checkUploadMaxFileSize("5M")',
41
 *     ],
42
 * ];
43
 * ```
44
 *
45
 * @property array|null $result The check results, this property is for internal usage only.
46
 */
47
final class RequirementsChecker
48
{
49
    /**
50
     * Check the given requirements, collecting results into internal field.
51
     * This method can be invoked several times checking different requirement sets.
52
     * Use {@see getResult()} or {@see render()} to get the results.
53
     * @param array|string $requirements Requirements to be checked.
54
     * If an array, it is treated as the set of requirements;
55
     * If a string, it is treated as the path of the file, which contains the requirements;
56
     * @return $this Self instance.
57
     */
58 3
    public function check($requirements): self
59
    {
60 3
        if (is_string($requirements)) {
61
            $requirements = require $requirements;
62
        }
63 3
        if (!is_array($requirements)) {
64
            $this->usageError('Requirements must be an array, "' . gettype($requirements) . '" has been given!');
65
        }
66 3
        if (!isset($this->result) || !is_array($this->result)) {
67 3
            $this->result = [
68 3
                'summary' => [
69 3
                    'total' => 0,
70 3
                    'errors' => 0,
71 3
                    'warnings' => 0,
72 3
                ],
73 3
                'requirements' => [],
74 3
            ];
75
        }
76 3
        foreach ($requirements as $key => $rawRequirement) {
77 3
            $requirement = $this->normalizeRequirement($rawRequirement, $key);
78 3
            $this->result['summary']['total']++;
79 3
            if (!$requirement['condition']) {
80 2
                if ($requirement['mandatory']) {
81 2
                    $requirement['error'] = true;
82 2
                    $requirement['warning'] = true;
83 2
                    $this->result['summary']['errors']++;
84
                } else {
85 1
                    $requirement['error'] = false;
86 1
                    $requirement['warning'] = true;
87 2
                    $this->result['summary']['warnings']++;
88
                }
89
            } else {
90 3
                $requirement['error'] = false;
91 3
                $requirement['warning'] = false;
92
            }
93 3
            $this->result['requirements'][] = $requirement;
94
        }
95
96 3
        return $this;
97
    }
98
99
    /**
100
     * Return the check results.
101
     * @return array|null check results in format:
102
     *
103
     * ```php
104
     * [
105
     *     'summary' => [
106
     *         'total' => total number of checks,
107
     *         'errors' => number of errors,
108
     *         'warnings' => number of warnings,
109
     *     ],
110
     *     'requirements' => [
111
     *         [
112
     *             ...
113
     *             'error' => is there an error,
114
     *             'warning' => is there a warning,
115
     *         ],
116
     *         ...
117
     *     ],
118
     * ]
119
     * ```
120
     */
121 3
    public function getResult(): ?array
122
    {
123 3
        return $this->result ?? null;
124
    }
125
126
    /**
127
     * Renders the requirements check result.
128
     * The output will vary depending on if a script running from web or from console.
129
     */
130
    public function render(): void
131
    {
132
        if (!isset($this->result)) {
133
            $this->usageError('Nothing to render!');
134
        }
135
        $baseViewFilePath = __DIR__ . DIRECTORY_SEPARATOR . 'views';
136
        if (!empty($_SERVER['argv'])) {
137
            $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'console' . DIRECTORY_SEPARATOR . 'index.php';
138
        } else {
139
            $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'web' . DIRECTORY_SEPARATOR . 'index.php';
140
        }
141
        $this->renderViewFile($viewFileName, $this->result);
142
    }
143
144
    /**
145
     * Checks if the given PHP extension is available and its version matches the given one.
146
     * @param string $extensionName PHP extension name.
147
     * @param string $version Required PHP extension version.
148
     * @param string $compare Comparison operator, by default '>='
149
     * @return bool If PHP extension version matches.
150
     */
151 1
    public function checkPhpExtensionVersion(string $extensionName, string $version, string $compare = '>='): bool
152
    {
153 1
        if (!extension_loaded($extensionName)) {
154 1
            return false;
155
        }
156 1
        $extensionVersion = phpversion($extensionName);
157 1
        if (empty($extensionVersion)) {
158
            return false;
159
        }
160 1
        if (strncasecmp($extensionVersion, 'PECL-', 5) === 0) {
161
            $extensionVersion = substr($extensionVersion, 5);
162
        }
163
164 1
        return version_compare($extensionVersion, $version, $compare);
0 ignored issues
show
Bug Best Practice introduced by
The expression return version_compare($...on, $version, $compare) could return the type integer which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
165
    }
166
167
    /**
168
     * Checks if PHP configuration option (from `php.ini`) is on.
169
     * @param string $name Configuration option name.
170
     * @return bool Whether option is on.
171
     */
172
    public function checkPhpIniOn(string $name): bool
173
    {
174
        $value = ini_get($name);
175
        if (empty($value)) {
176
            return false;
177
        }
178
179
        return ((int) $value === 1 || strtolower($value) === 'on');
180
    }
181
182
    /**
183
     * Checks if PHP configuration option (from `php.ini`) is off.
184
     * @param string $name Configuration option name.
185
     * @return bool Whether option is off.
186
     */
187
    public function checkPhpIniOff(string $name): bool
188
    {
189
        $value = ini_get($name);
190
        if (empty($value)) {
191
            return true;
192
        }
193
194
        return (strtolower($value) === 'off');
195
    }
196
197
    /**
198
     * Compare byte sizes of values given in the verbose representation,
199
     * like '5M', '15K' etc.
200
     * @param string $a First value.
201
     * @param string $b Second value.
202
     * @param string $compare Comparison operator, by default '>='.
203
     * @return bool Comparison result.
204
     */
205 5
    public function compareByteSize(string $a, string $b, string $compare = '>='): bool
206
    {
207 5
        $compareExpression = '(' . $this->getByteSize($a) . $compare . $this->getByteSize($b) . ')';
208
209 5
        return $this->evaluateExpression($compareExpression);
210
    }
211
212
    /**
213
     * Gets the size in bytes from verbose size representation.
214
     * For example: '5K' => 5*1024
215
     * @param string $verboseSize Verbose size representation.
216
     * @return int Actual size in bytes.
217
     */
218 12
    public function getByteSize(string $verboseSize): int
219
    {
220 12
        if (empty($verboseSize)) {
221
            return 0;
222
        }
223 12
        if (is_numeric($verboseSize)) {
224 2
            return (int) $verboseSize;
225
        }
226 11
        $sizeUnit = trim($verboseSize, '0123456789');
227 11
        $size = trim(str_replace($sizeUnit, '', $verboseSize));
228 11
        if (!is_numeric($size)) {
229
            return 0;
230
        }
231 11
        switch (strtolower($sizeUnit)) {
232 11
            case 'kb':
233 10
            case 'k':
234 5
                return $size * 1024;
235 8
            case 'mb':
236 7
            case 'm':
237 6
                return $size * 1024 * 1024;
238 2
            case 'gb':
239 1
            case 'g':
240 2
                return $size * 1024 * 1024 * 1024;
241
            default:
242
                return 0;
243
        }
244
    }
245
246
    /**
247
     * Checks if upload max file size matches the given range.
248
     * @param string|null $min Verbose file size minimum required value, pass null to skip minimum check.
249
     * @param string|null $max Verbose file size maximum required value, pass null to skip maximum check.
250
     * @return bool True on success.
251
     */
252
    public function checkUploadMaxFileSize(?string $min = null, ?string $max = null): bool
253
    {
254
        $postMaxSize = ini_get('post_max_size');
255
        $uploadMaxFileSize = ini_get('upload_max_filesize');
256
        if ($min !== null) {
257
            $minCheckResult = $this->compareByteSize($postMaxSize, $min, '>=') && $this->compareByteSize($uploadMaxFileSize, $min, '>=');
258
        } else {
259
            $minCheckResult = true;
260
        }
261
        if ($max !== null) {
262
            $maxCheckResult = $this->compareByteSize($postMaxSize, $max, '<=') && $this->compareByteSize($uploadMaxFileSize, $max, '<=');
263
        } else {
264
            $maxCheckResult = true;
265
        }
266
267
        return ($minCheckResult && $maxCheckResult);
268
    }
269
270
    /**
271
     * Renders a view file.
272
     * This method includes the view file as a PHP script
273
     * and captures the display result if required.
274
     * @param string $_viewFile_ View file.
275
     * @param array|null $_data_ Data to be extracted and made available to the view file.
276
     * @param bool $_return_ Whether the rendering result should be returned as a string.
277
     * @return string The rendering result. Null if the rendering result is not required.
278
     */
279
    public function renderViewFile(string $_viewFile_, array $_data_ = null, bool $_return_ = false): ?string
280
    {
281
        // we use special variable names here to avoid conflict when extracting data
282
        if (is_array($_data_)) {
283
            extract($_data_, EXTR_PREFIX_SAME, 'data');
284
        } else {
285
            $data = $_data_;
286
        }
287
        if ($_return_) {
288
            ob_start();
289
            PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
290
            require $_viewFile_;
291
292
            return ob_get_clean();
293
        }
294
295
        require $_viewFile_;
296
        return null;
297
    }
298
299
    /**
300
     * Normalizes requirement ensuring it has correct format.
301
     * @param array $requirement Raw requirement.
302
     * @param int|string $requirementKey Requirement key in the list.
303
     * @return array Normalized requirement.
304
     */
305 3
    public function normalizeRequirement(array $requirement, $requirementKey = 0): array
306
    {
307 3
        if (!array_key_exists('condition', $requirement)) {
308
            $this->usageError("Requirement \"$requirementKey\" has no condition!");
309
        } else {
310 3
            $evalPrefix = 'eval:';
311 3
            if (is_string($requirement['condition']) && strpos($requirement['condition'], $evalPrefix) === 0) {
312 1
                $expression = substr($requirement['condition'], strlen($evalPrefix));
313 1
                $requirement['condition'] = $this->evaluateExpression($expression);
314
            }
315
        }
316 3
        if (!array_key_exists('name', $requirement)) {
317
            $requirement['name'] = is_numeric($requirementKey) ? 'Requirement #' . $requirementKey : $requirementKey;
318
        }
319 3
        if (!array_key_exists('mandatory', $requirement)) {
320
            if (array_key_exists('required', $requirement)) {
321
                $requirement['mandatory'] = $requirement['required'];
322
            } else {
323
                $requirement['mandatory'] = false;
324
            }
325
        }
326 3
        if (!array_key_exists('by', $requirement)) {
327
            $requirement['by'] = 'Unknown';
328
        }
329 3
        if (!array_key_exists('memo', $requirement)) {
330
            $requirement['memo'] = '';
331
        }
332
333 3
        return $requirement;
334
    }
335
336
    /**
337
     * Displays a usage error.
338
     * This method will then terminate the execution of the current application.
339
     * @param string $message the error message
340
     */
341
    public function usageError(string $message): void
342
    {
343
        echo "Error: $message\n\n";
344
        exit(1);
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
345
    }
346
347
    /**
348
     * Evaluates a PHP expression under the context of this class.
349
     * @param string $expression A PHP expression to be evaluated.
350
     * @return mixed The expression result.
351
     */
352 6
    public function evaluateExpression(string $expression)
353
    {
354 6
        return eval('return ' . $expression . ';');
0 ignored issues
show
The use of eval() is discouraged.
Loading history...
355
    }
356
357
    /**
358
     * Returns the server information.
359
     * @return string server information.
360
     */
361
    public function getServerInfo(): string
362
    {
363
        return $_SERVER['SERVER_SOFTWARE'] ?? '';
364
    }
365
366
    /**
367
     * Returns the now date if possible in string representation.
368
     * @return string now date.
369
     */
370
    public function getNowDate(): string
371
    {
372
        return @strftime('%Y-%m-%d %H:%M', time());
0 ignored issues
show
Bug Best Practice introduced by
The expression return @strftime('%Y-%m-%d %H:%M', time()) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
373
    }
374
}
375