Passed
Pull Request — master (#67)
by Wilmer
02:33
created

RequirementsChecker::render()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 4
nop 0
dl 0
loc 12
ccs 0
cts 8
cp 0
crap 12
rs 10
c 0
b 0
f 0
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 = array(
17
 *     array(
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
 *     ->chekYii()
27
 *     ->check($requirements)
28
 *     ->render();
29
 * ```
30
 *
31
 * If you wish to render the report with your own representation, use [[getResult()]] instead of [[render()]]
32
 *
33
 * Requirement condition could be in format "eval:PHP expression".
34
 * In this case specified PHP expression will be evaluated in the context of this class instance.
35
 * For example:
36
 *
37
 * ```php
38
 * $requirements = array(
39
 *     array(
40
 *         'name' => 'Upload max file size',
41
 *         'condition' => 'eval:$this->checkUploadMaxFileSize("5M")',
42
 *     ),
43
 * );
44
 * ```
45
 *
46
 * Note: this class definition does not match ordinary Yii style, because it should match PHP 4.3
47
 * and should not use features from newer PHP versions!
48
 *
49
 * @property array|null $result the check results, this property is for internal usage only.
50
 */
51
class RequirementsChecker
52
{
53
    /**
54
     * Check the given requirements, collecting results into internal field.
55
     * This method can be invoked several times checking different requirement sets.
56
     * Use [[getResult()]] or [[render()]] to get the results.
57
     * @param array|string $requirements requirements to be checked.
58
     * If an array, it is treated as the set of requirements;
59
     * If a string, it is treated as the path of the file, which contains the requirements;
60
     * @return $this self instance.
61
     */
62 3
    public function check($requirements)
63
    {
64 3
        if (version_compare(PHP_VERSION, '4.3', '<')) {
65
            echo 'At least PHP 4.3 is required to run this script!';
66
            exit(1);
0 ignored issues
show
Best Practice introduced by
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...
67
        }
68 3
        if (is_string($requirements)) {
69
            $requirements = require $requirements;
70
        }
71 3
        if (!is_array($requirements)) {
72
            $this->usageError('Requirements must be an array, "' . gettype($requirements) . '" has been given!');
73
        }
74 3
        if (!isset($this->result) || !is_array($this->result)) {
75 3
            $this->result = [
76 3
                'summary' => [
77 3
                    'total' => 0,
78 3
                    'errors' => 0,
79 3
                    'warnings' => 0,
80 3
                ],
81 3
                'requirements' => [],
82 3
            ];
83
        }
84 3
        foreach ($requirements as $key => $rawRequirement) {
85 3
            $requirement = $this->normalizeRequirement($rawRequirement, $key);
86 3
            $this->result['summary']['total']++;
87 3
            if (!$requirement['condition']) {
88 2
                if ($requirement['mandatory']) {
89 2
                    $requirement['error'] = true;
90 2
                    $requirement['warning'] = true;
91 2
                    $this->result['summary']['errors']++;
92
                } else {
93 1
                    $requirement['error'] = false;
94 1
                    $requirement['warning'] = true;
95 2
                    $this->result['summary']['warnings']++;
96
                }
97
            } else {
98 3
                $requirement['error'] = false;
99 3
                $requirement['warning'] = false;
100
            }
101 3
            $this->result['requirements'][] = $requirement;
102
        }
103
104 3
        return $this;
105
    }
106
107
    /**
108
     * Performs the check for the Yii core requirements.
109
     * @return RequirementsChecker self instance.
110
     */
111
    public function checkYii()
112
    {
113
        return $this->check(__DIR__ . DIRECTORY_SEPARATOR . 'requirements.php');
114
    }
115
116
    /**
117
     * Return the check results.
118
     * @return array|null check results in format:
119
     *
120
     * ```php
121
     * array(
122
     *     'summary' => array(
123
     *         'total' => total number of checks,
124
     *         'errors' => number of errors,
125
     *         'warnings' => number of warnings,
126
     *     ),
127
     *     'requirements' => array(
128
     *         array(
129
     *             ...
130
     *             'error' => is there an error,
131
     *             'warning' => is there a warning,
132
     *         ),
133
     *         ...
134
     *     ),
135
     * )
136
     * ```
137
     */
138 3
    public function getResult()
139
    {
140 3
        if (isset($this->result)) {
141 3
            return $this->result;
142
        }
143
        return null;
144
    }
145
146
    /**
147
     * Renders the requirements check result.
148
     * The output will vary depending is a script running from web or from console.
149
     */
150
    public function render()
151
    {
152
        if (!isset($this->result)) {
153
            $this->usageError('Nothing to render!');
154
        }
155
        $baseViewFilePath = __DIR__ . DIRECTORY_SEPARATOR . 'views';
156
        if (!empty($_SERVER['argv'])) {
157
            $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'console' . DIRECTORY_SEPARATOR . 'index.php';
158
        } else {
159
            $viewFileName = $baseViewFilePath . DIRECTORY_SEPARATOR . 'web' . DIRECTORY_SEPARATOR . 'index.php';
160
        }
161
        $this->renderViewFile($viewFileName, $this->result);
162
    }
163
164
    /**
165
     * Checks if the given PHP extension is available and its version matches the given one.
166
     * @param string $extensionName PHP extension name.
167
     * @param string $version required PHP extension version.
168
     * @param string $compare comparison operator, by default '>='
169
     * @return bool if PHP extension version matches.
170
     */
171 1
    public function checkPhpExtensionVersion($extensionName, $version, $compare = '>=')
172
    {
173 1
        if (!extension_loaded($extensionName)) {
174 1
            return false;
175
        }
176 1
        $extensionVersion = phpversion($extensionName);
177 1
        if (empty($extensionVersion)) {
178
            return false;
179
        }
180 1
        if (strncasecmp($extensionVersion, 'PECL-', 5) === 0) {
181
            $extensionVersion = substr($extensionVersion, 5);
182
        }
183
184 1
        return version_compare($extensionVersion, $version, $compare);
0 ignored issues
show
Bug Best Practice introduced by
The expression return version_compare($...on, $version, $compare) also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
185
    }
186
187
    /**
188
     * Checks if PHP configuration option (from php.ini) is on.
189
     * @param string $name configuration option name.
190
     * @return bool option is on.
191
     */
192
    public function checkPhpIniOn($name)
193
    {
194
        $value = ini_get($name);
195
        if (empty($value)) {
196
            return false;
197
        }
198
199
        return (int) $value === 1 || strtolower($value) === 'on';
200
    }
201
202
    /**
203
     * Checks if PHP configuration option (from php.ini) is off.
204
     * @param string $name configuration option name.
205
     * @return bool option is off.
206
     */
207
    public function checkPhpIniOff($name)
208
    {
209
        $value = ini_get($name);
210
        if (empty($value)) {
211
            return true;
212
        }
213
214
        return strtolower($value) === 'off';
215
    }
216
217
    /**
218
     * Compare byte sizes of values given in the verbose representation,
219
     * like '5M', '15K' etc.
220
     * @param string $a first value.
221
     * @param string $b second value.
222
     * @param string $compare comparison operator, by default '>='.
223
     * @return bool comparison result.
224
     */
225 5
    public function compareByteSize($a, $b, $compare = '>=')
226
    {
227 5
        $compareExpression = '(' . $this->getByteSize($a) . $compare . $this->getByteSize($b) . ')';
228
229 5
        return $this->evaluateExpression($compareExpression);
230
    }
231
232
    /**
233
     * Gets the size in bytes from verbose size representation.
234
     * For example: '5K' => 5*1024
235
     * @param string $verboseSize verbose size representation.
236
     * @return int actual size in bytes.
237
     */
238 12
    public function getByteSize($verboseSize)
239
    {
240 12
        if (empty($verboseSize)) {
241
            return 0;
242
        }
243 12
        if (is_numeric($verboseSize)) {
244 2
            return (int) $verboseSize;
245
        }
246 11
        $sizeUnit = trim($verboseSize, '0123456789');
247 11
        $size = trim(str_replace($sizeUnit, '', $verboseSize));
248 11
        if (!is_numeric($size)) {
249
            return 0;
250
        }
251 11
        switch (strtolower($sizeUnit)) {
252 11
            case 'kb':
253 10
            case 'k':
254 5
                return $size * 1024;
255 8
            case 'mb':
256 7
            case 'm':
257 6
                return $size * 1024 * 1024;
258 2
            case 'gb':
259 1
            case 'g':
260 2
                return $size * 1024 * 1024 * 1024;
261
            default:
262
                return 0;
263
        }
264
    }
265
266
    /**
267
     * Checks if upload max file size matches the given range.
268
     * @param string|null $min verbose file size minimum required value, pass null to skip minimum check.
269
     * @param string|null $max verbose file size maximum required value, pass null to skip maximum check.
270
     * @return bool success.
271
     */
272
    public function checkUploadMaxFileSize($min = null, $max = null)
273
    {
274
        $postMaxSize = ini_get('post_max_size');
275
        $uploadMaxFileSize = ini_get('upload_max_filesize');
276
        if ($min !== null) {
277
            $minCheckResult = $this->compareByteSize($postMaxSize, $min, '>=') && $this->compareByteSize($uploadMaxFileSize, $min, '>=');
278
        } else {
279
            $minCheckResult = true;
280
        }
281
        if ($max !== null) {
282
            $maxCheckResult = $this->compareByteSize($postMaxSize, $max, '<=') && $this->compareByteSize($uploadMaxFileSize, $max, '<=');
283
        } else {
284
            $maxCheckResult = true;
285
        }
286
287
        return $minCheckResult && $maxCheckResult;
288
    }
289
290
    /**
291
     * Renders a view file.
292
     * This method includes the view file as a PHP script
293
     * and captures the display result if required.
294
     * @param string $_viewFile_ view file
295
     * @param array $_data_ data to be extracted and made available to the view file
296
     * @param bool $_return_ whether the rendering result should be returned as a string
297
     * @return string the rendering result. Null if the rendering result is not required.
298
     */
299
    public function renderViewFile($_viewFile_, $_data_ = null, $_return_ = false)
300
    {
301
        // we use special variable names here to avoid conflict when extracting data
302
        if (is_array($_data_)) {
303
            extract($_data_, EXTR_PREFIX_SAME, 'data');
304
        } else {
305
            $data = $_data_;
306
        }
307
        if ($_return_) {
308
            ob_start();
309
            PHP_VERSION_ID >= 80000 ? ob_implicit_flush(false) : ob_implicit_flush(0);
310
            require $_viewFile_;
311
312
            return ob_get_clean();
313
        }
314
        require $_viewFile_;
315
    }
316
317
    /**
318
     * Normalizes requirement ensuring it has correct format.
319
     * @param array $requirement raw requirement.
320
     * @param int $requirementKey requirement key in the list.
321
     * @return array normalized requirement.
322
     */
323 3
    public function normalizeRequirement($requirement, $requirementKey = 0)
324
    {
325 3
        if (!is_array($requirement)) {
0 ignored issues
show
introduced by
The condition is_array($requirement) is always true.
Loading history...
326
            $this->usageError('Requirement must be an array!');
327
        }
328 3
        if (!array_key_exists('condition', $requirement)) {
329
            $this->usageError("Requirement '{$requirementKey}' has no condition!");
330
        } else {
331 3
            $evalPrefix = 'eval:';
332 3
            if (is_string($requirement['condition']) && strpos($requirement['condition'], $evalPrefix) === 0) {
333 1
                $expression = substr($requirement['condition'], strlen($evalPrefix));
334 1
                $requirement['condition'] = $this->evaluateExpression($expression);
335
            }
336
        }
337 3
        if (!array_key_exists('name', $requirement)) {
338
            $requirement['name'] = is_numeric($requirementKey) ? 'Requirement #' . $requirementKey : $requirementKey;
0 ignored issues
show
introduced by
The condition is_numeric($requirementKey) is always true.
Loading history...
339
        }
340 3
        if (!array_key_exists('mandatory', $requirement)) {
341
            if (array_key_exists('required', $requirement)) {
342
                $requirement['mandatory'] = $requirement['required'];
343
            } else {
344
                $requirement['mandatory'] = false;
345
            }
346
        }
347 3
        if (!array_key_exists('by', $requirement)) {
348
            $requirement['by'] = 'Unknown';
349
        }
350 3
        if (!array_key_exists('memo', $requirement)) {
351
            $requirement['memo'] = '';
352
        }
353
354 3
        return $requirement;
355
    }
356
357
    /**
358
     * Displays a usage error.
359
     * This method will then terminate the execution of the current application.
360
     * @param string $message the error message
361
     */
362
    public function usageError($message)
363
    {
364
        echo "Error: $message\n\n";
365
        exit(1);
0 ignored issues
show
Best Practice introduced by
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...
366
    }
367
368
    /**
369
     * Evaluates a PHP expression under the context of this class.
370
     * @param string $expression a PHP expression to be evaluated.
371
     * @return mixed the expression result.
372
     */
373 6
    public function evaluateExpression($expression)
374
    {
375 6
        return eval('return ' . $expression . ';');
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
376
    }
377
378
    /**
379
     * Returns the server information.
380
     * @return string server information.
381
     */
382
    public function getServerInfo()
383
    {
384
        return $_SERVER['SERVER_SOFTWARE'] ?? '';
385
    }
386
387
    /**
388
     * Returns the now date if possible in string representation.
389
     * @return string now date.
390
     */
391
    public function getNowDate()
392
    {
393
        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 also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
394
    }
395
396
    public function ecs()
397
    {
398
    }
399
}
400