Passed
Push — master ( bfa019...94a547 )
by Bjørn
02:30
created

Cwebp   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 343
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 200
dl 0
loc 343
rs 8.72
c 0
b 0
f 0
wmc 46

3 Methods

Rating   Name   Duplication   Size   Complexity  
A executeBinary() 0 9 2
B createCommandLineOptions() 0 77 11
F doActualConvert() 0 179 33

How to fix   Complexity   

Complex Class

Complex classes like Cwebp often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Cwebp, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace WebPConvert\Convert\Converters;
4
5
use WebPConvert\Convert\BaseConverters\AbstractExecConverter;
6
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
7
use WebPConvert\Convert\Exceptions\ConversionFailedException;
8
9
class Cwebp extends AbstractExecConverter
10
{
11
    public static $extraOptions = [
12
        [
13
            'name' => 'use-nice',
14
            'type' => 'boolean',
15
            'sensitive' => false,
16
            'default' => false,
17
            'required' => false
18
        ],
19
        // low-memory is defined for all, in ConverterHelper
20
        [
21
            'name' => 'try-common-system-paths',
22
            'type' => 'boolean',
23
            'sensitive' => false,
24
            'default' => true,
25
            'required' => false
26
        ],
27
        [
28
            'name' => 'try-supplied-binary-for-os',
29
            'type' => 'boolean',
30
            'sensitive' => false,
31
            'default' => true,
32
            'required' => false
33
        ],
34
        [
35
            'name' => 'size-in-percentage',
36
            'type' => 'number',
37
            'sensitive' => false,
38
            'default' => null,
39
            'required' => false
40
        ],
41
        [
42
            'name' => 'command-line-options',
43
            'type' => 'string',
44
            'sensitive' => false,
45
            'default' => '',
46
            'required' => false
47
        ],
48
        [
49
            'name' => 'rel-path-to-precompiled-binaries',
50
            'type' => 'string',
51
            'sensitive' => false,
52
            'default' => './Binaries',
53
            'required' => false
54
        ],
55
    ];
56
57
    // System paths to look for cwebp binary
58
    private static $cwebpDefaultPaths = [
59
        '/usr/bin/cwebp',
60
        '/usr/local/bin/cwebp',
61
        '/usr/gnu/bin/cwebp',
62
        '/usr/syno/bin/cwebp'
63
    ];
64
65
    // OS-specific binaries included in this library, along with hashes
66
    // If other binaries are going to be added, notice that the first argument is what PHP_OS returns.
67
    // (possible values, see here: https://stackoverflow.com/questions/738823/possible-values-for-php-os)
68
    private static $suppliedBinariesInfo = [
69
        'WINNT' => [ 'cwebp.exe', '49e9cb98db30bfa27936933e6fd94d407e0386802cb192800d9fd824f6476873'],
70
        'Darwin' => [ 'cwebp-mac12', 'a06a3ee436e375c89dbc1b0b2e8bd7729a55139ae072ed3f7bd2e07de0ebb379'],
71
        'SunOS' => [ 'cwebp-sol', '1febaffbb18e52dc2c524cda9eefd00c6db95bc388732868999c0f48deb73b4f'],
72
        'FreeBSD' => [ 'cwebp-fbsd', 'e5cbea11c97fadffe221fdf57c093c19af2737e4bbd2cb3cd5e908de64286573'],
73
        'Linux' => [ 'cwebp-linux', '916623e5e9183237c851374d969aebdb96e0edc0692ab7937b95ea67dc3b2568']
74
    ];
75
76
77
    private static function executeBinary($binary, $commandOptions, $useNice, $logger)
0 ignored issues
show
Unused Code introduced by
The parameter $logger is not used and could be removed. ( Ignorable by Annotation )

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

77
    private static function executeBinary($binary, $commandOptions, $useNice, /** @scrutinizer ignore-unused */ $logger)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
78
    {
79
        $command = ($useNice ? 'nice ' : '') . $binary . ' ' . $commandOptions;
80
81
        //$logger->logLn('command options:' . $commandOptions);
82
        //$logger->logLn('Trying to execute binary:' . $binary);
83
        exec($command, $output, $returnCode);
84
        //$logger->logLn(self::msgForExitCode($returnCode));
85
        return intval($returnCode);
86
    }
87
88
    /**
89
     * Build command line options
90
     *
91
     * @return string
92
     */
93
    private function createCommandLineOptions()
94
    {
95
        $options = $this->options;
96
97
        $commandOptionsArray = [];
98
99
        // Metadata (all, exif, icc, xmp or none (default))
100
        // Comma-separated list of existing metadata to copy from input to output
101
        $commandOptionsArray[] = '-metadata ' . $options['metadata'];
102
103
        // Size
104
        if (!is_null($options['size-in-percentage'])) {
105
            $sizeSource =  @filesize($this->source);
106
            if ($sizeSource !== false) {
107
                $targetSize = floor($sizeSource * $options['size-in-percentage'] / 100);
108
            }
109
        }
110
        if (isset($targetSize)) {
111
            $commandOptionsArray[] = '-size ' . $targetSize;
112
        } else {
113
            // Image quality
114
            $commandOptionsArray[] = '-q ' . $this->getCalculatedQuality();
115
        }
116
117
118
        // Losless PNG conversion
119
        $commandOptionsArray[] = ($options['lossless'] ? '-lossless' : '');
120
121
        // Built-in method option
122
        $commandOptionsArray[] = '-m ' . strval($options['method']);
123
124
        // Built-in low memory option
125
        if ($options['low-memory']) {
126
            $commandOptionsArray[] = '-low_memory';
127
        }
128
129
        // command-line-options
130
        if ($options['command-line-options']) {
131
            $arr = explode(' -', ' ' . $options['command-line-options']);
132
            foreach ($arr as $cmdOption) {
133
                $pos = strpos($cmdOption, ' ');
134
                $cName = '';
135
                $cValue = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $cValue is dead and can be removed.
Loading history...
136
                if (!$pos) {
137
                    $cName = $cmdOption;
138
                    if ($cName == '') {
139
                        continue;
140
                    }
141
                    $commandOptionsArray[] = '-' . $cName;
142
                } else {
143
                    $cName = substr($cmdOption, 0, $pos);
144
                    $cValues = substr($cmdOption, $pos + 1);
145
                    $cValuesArr = explode(' ', $cValues);
146
                    foreach ($cValuesArr as &$cArg) {
147
                        $cArg = escapeshellarg($cArg);
148
                    }
149
                    $cValues = implode(' ', $cValuesArr);
150
                    $commandOptionsArray[] = '-' . $cName . ' ' . $cValues;
151
                }
152
            }
153
        }
154
155
        // Source file
156
        $commandOptionsArray[] = escapeshellarg($this->source);
157
158
        // Output
159
        $commandOptionsArray[] = '-o ' . escapeshellarg($this->destination);
160
161
162
        // Redirect stderr to same place as stdout
163
        // https://www.brianstorti.com/understanding-shell-script-idiom-redirect/
164
        $commandOptionsArray[] = '2>&1';
165
166
        $commandOptions = implode(' ', $commandOptionsArray);
167
        $this->logLn('cwebp options:' . $commandOptions);
168
169
        return $commandOptions;
170
    }
171
172
173
    protected function doActualConvert()
174
    {
175
        $errorMsg = '';
176
        $options = $this->options;
177
        $useNice = (($options['use-nice']) && self::hasNiceSupport()) ? true : false;
178
179
        $commandOptions = $this->createCommandLineOptions();
180
181
182
        // Init with common system paths
183
        $cwebpPathsToTest = self::$cwebpDefaultPaths;
184
185
        // Remove paths that doesn't exist
186
        /*
187
        $cwebpPathsToTest = array_filter($cwebpPathsToTest, function ($binary) {
188
            //return file_exists($binary);
189
            return @is_readable($binary);
190
        });
191
        */
192
193
        // Try all common paths that exists
194
        $success = false;
195
        $failures = [];
196
        $failureCodes = [];
197
198
        if (!$options['try-supplied-binary-for-os'] && !$options['try-common-system-paths']) {
199
            $errorMsg .= 'Configured to neither look for cweb binaries in common system locations, ' .
200
                'nor to use one of the supplied precompiled binaries. But these are the only ways ' .
201
                'this converter can convert images. No conversion can be made!';
202
        }
203
204
        $returnCode = 0;
205
        $majorFailCode = 0;
206
        if ($options['try-common-system-paths']) {
207
            foreach ($cwebpPathsToTest as $index => $binary) {
208
                $returnCode = self::executeBinary($binary, $commandOptions, $useNice, $this);
209
                if ($returnCode == 0) {
210
                    $this->logLn('Successfully executed binary: ' . $binary);
211
                    $success = true;
212
                    break;
213
                } else {
214
                    $failures[] = [$binary, $returnCode];
215
                    if (!in_array($returnCode, $failureCodes)) {
216
                        $failureCodes[] = $returnCode;
217
                    }
218
                }
219
            }
220
221
            if (!$success) {
222
                if (count($failureCodes) == 1) {
223
                    $majorFailCode = $failureCodes[0];
224
                    switch ($majorFailCode) {
225
                        case 126:
226
                            $errorMsg = 'Permission denied. The user that the command was run with (' .
227
                                shell_exec('whoami') . ') does not have permission to execute any of the ' .
228
                                'cweb binaries found in common system locations. ';
229
                            break;
230
                        case 127:
231
                            $errorMsg .= 'Found no cwebp binaries in any common system locations. ';
232
                            break;
233
                        default:
234
                            $errorMsg .= 'Tried executing cwebp binaries in common system locations. ' .
235
                                'All failed (exit code: ' . $majorFailCode . '). ';
236
                    }
237
                } else {
238
                    /**
239
                     * $failureCodesBesides127 is used to check first position ($failureCodesBesides127[0])
240
                     * however position can vary as index can be 1 or something else. array_values() would
241
                     * always start from 0.
242
                     */
243
                    $failureCodesBesides127 = array_values(array_diff($failureCodes, [127]));
244
245
                    if (count($failureCodesBesides127) == 1) {
246
                        $majorFailCode = $failureCodesBesides127[0];
247
                        switch ($returnCode) {
248
                            case 126:
249
                                $errorMsg = 'Permission denied. The user that the command was run with (' .
250
                                shell_exec('whoami') . ') does not have permission to execute any of the cweb ' .
251
                                'binaries found in common system locations. ';
252
                                break;
253
                            default:
254
                                $errorMsg .= 'Tried executing cwebp binaries in common system locations. ' .
255
                                'All failed (exit code: ' . $majorFailCode . '). ';
256
                        }
257
                    } else {
258
                        $errorMsg .= 'None of the cwebp binaries in the common system locations could be executed ' .
259
                        '(mixed results - got the following exit codes: ' . implode(',', $failureCodes) . '). ';
260
                    }
261
                }
262
            }
263
        }
264
265
        if (!$success && $options['try-supplied-binary-for-os']) {
266
          // Try supplied binary (if available for OS, and hash is correct)
267
            if (isset(self::$suppliedBinariesInfo[PHP_OS])) {
268
                $info = self::$suppliedBinariesInfo[PHP_OS];
269
270
                $file = $info[0];
271
                $hash = $info[1];
272
273
                $binaryFile = __DIR__ . '/' . $options['rel-path-to-precompiled-binaries'] . '/' . $file;
274
275
                // The file should exist, but may have been removed manually.
276
                if (@file_exists($binaryFile)) {
277
                    // File exists, now generate its hash
278
279
                    // hash_file() is normally available, but it is not always
280
                    // - https://stackoverflow.com/questions/17382712/php-5-3-20-undefined-function-hash
281
                    // If available, validate that hash is correct.
282
                    $proceedAfterHashCheck = true;
283
                    if (function_exists('hash_file')) {
284
                        $binaryHash = hash_file('sha256', $binaryFile);
285
286
                        if ($binaryHash != $hash) {
287
                            $errorMsg .= 'Binary checksum of supplied binary is invalid! ' .
288
                                'Did you transfer with FTP, but not in binary mode? ' .
289
                                'File:' . $binaryFile . '. ' .
290
                                'Expected checksum: ' . $hash . '. ' .
291
                                'Actual checksum:' . $binaryHash . '.';
292
                            $proceedAfterHashCheck = false;
293
                        }
294
                    }
295
                    if ($proceedAfterHashCheck) {
296
                        $returnCode = self::executeBinary($binaryFile, $commandOptions, $useNice, $this);
297
                        if ($returnCode == 0) {
298
                            $success = true;
299
                        } else {
300
                            $errorMsg .= 'Tried executing supplied binary for ' . PHP_OS . ', ' .
301
                                ($options['try-common-system-paths'] ? 'but that failed too' : 'but failed');
302
                            if ($options['try-common-system-paths'] && ($majorFailCode > 0)) {
303
                                $errorMsg .= ' (same error)';
304
                            } else {
305
                                if ($returnCode > 128) {
306
                                    $errorMsg .= '. The binary did not work (exit code: ' . $returnCode . '). ' .
307
                                        'Check out https://github.com/rosell-dk/webp-convert/issues/92';
308
                                } else {
309
                                    switch ($returnCode) {
310
                                        case 0:
311
                                            $success = true;
312
                                            ;
313
                                            break;
314
                                        case 126:
315
                                            $errorMsg .= ': Permission denied. The user that the command was run' .
316
                                                ' with (' . shell_exec('whoami') . ') does not have permission to ' .
317
                                                'execute that binary.';
318
                                            break;
319
                                        case 127:
320
                                            $errorMsg .= '. The binary was not found! ' .
321
                                                'It ought to be here: ' . $binaryFile;
322
                                            break;
323
                                        default:
324
                                            $errorMsg .= ' (exit code:' .  $returnCode . ').';
325
                                    }
326
                                }
327
                            }
328
                        }
329
                    }
330
                } else {
331
                    $errorMsg .= 'Supplied binary not found! It ought to be here:' . $binaryFile;
332
                }
333
            } else {
334
                $errorMsg .= 'No supplied binaries found for OS:' . PHP_OS;
335
            }
336
        }
337
338
        // cwebp sets file permissions to 664 but instead ..
339
        // .. $destination's parent folder's permissions should be used (except executable bits)
340
        if ($success) {
341
            $destinationParent = dirname($this->destination);
342
            $fileStatistics = @stat($destinationParent);
343
            if ($fileStatistics !== false) {
344
                // Apply same permissions as parent folder but strip off the executable bits
345
                $permissions = $fileStatistics['mode'] & 0000666;
346
                @chmod($this->destination, $permissions);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). 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

346
                /** @scrutinizer ignore-unhandled */ @chmod($this->destination, $permissions);

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...
347
            }
348
        }
349
350
        if (!$success) {
351
            throw new SystemRequirementsNotMetException($errorMsg);
352
        }
353
    }
354
}
355