Passed
Push — master ( aa493d...38332a )
by Bjørn
02:32
created

Cwebp   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Test Coverage

Coverage 65.99%

Importance

Changes 0
Metric Value
eloc 191
dl 0
loc 344
ccs 97
cts 147
cp 0.6599
rs 7.44
c 0
b 0
f 0
wmc 52

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getOptionDefinitionsExtra() 0 13 1
F createCommandLineOptions() 0 96 16
A checkOperationality() 0 8 3
A executeBinary() 0 9 2
F doActualConvert() 0 176 30

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
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException;
9
10
class Cwebp extends AbstractExecConverter
11
{
12
    protected $supportsLossless = true;
13
14 3
    protected function getOptionDefinitionsExtra()
15
    {
16
        return [
17 3
            ['autofilter', 'boolean', false],
18
            ['command-line-options', 'string', ''],
19
            ['low-memory', 'boolean', false],
20
            ['method', 'number', 6],
21
            ['near-lossless', 'integer', 60],
22
            ['rel-path-to-precompiled-binaries', 'string', './Binaries'],
23
            ['size-in-percentage', 'number', null],
24
            ['try-common-system-paths', 'boolean', true],
25
            ['try-supplied-binary-for-os', 'boolean', true],
26
            ['use-nice', 'boolean', false],
27
28
        ];
29
    }
30
31
    // System paths to look for cwebp binary
32
    private static $cwebpDefaultPaths = [
33
        '/usr/bin/cwebp',
34
        '/usr/local/bin/cwebp',
35
        '/usr/gnu/bin/cwebp',
36
        '/usr/syno/bin/cwebp'
37
    ];
38
39
    // OS-specific binaries included in this library, along with hashes
40
    // If other binaries are going to be added, notice that the first argument is what PHP_OS returns.
41
    // (possible values, see here: https://stackoverflow.com/questions/738823/possible-values-for-php-os)
42
    private static $suppliedBinariesInfo = [
43
        'WINNT' => [ 'cwebp.exe', '49e9cb98db30bfa27936933e6fd94d407e0386802cb192800d9fd824f6476873'],
44
        'Darwin' => [ 'cwebp-mac12', 'a06a3ee436e375c89dbc1b0b2e8bd7729a55139ae072ed3f7bd2e07de0ebb379'],
45
        'SunOS' => [ 'cwebp-sol', '1febaffbb18e52dc2c524cda9eefd00c6db95bc388732868999c0f48deb73b4f'],
46
        'FreeBSD' => [ 'cwebp-fbsd', 'e5cbea11c97fadffe221fdf57c093c19af2737e4bbd2cb3cd5e908de64286573'],
47
        'Linux' => [ 'cwebp-linux', '916623e5e9183237c851374d969aebdb96e0edc0692ab7937b95ea67dc3b2568']
48
    ];
49
50
51 1
    private function executeBinary($binary, $commandOptions, $useNice)
52
    {
53 1
        $command = ($useNice ? 'nice ' : '') . $binary . ' ' . $commandOptions;
54
55
        //$logger->logLn('command options:' . $commandOptions);
56
        //$logger->logLn('Trying to execute binary:' . $binary);
57 1
        exec($command, $output, $returnCode);
58
        //$logger->logLn(self::msgForExitCode($returnCode));
59 1
        return intval($returnCode);
60
    }
61
62
    /**
63
     * Build command line options
64
     *
65
     * @return string
66
     */
67 2
    private function createCommandLineOptions()
68
    {
69 2
        $options = $this->options;
70
71 2
        $commandOptionsArray = [];
72
73
        // Metadata (all, exif, icc, xmp or none (default))
74
        // Comma-separated list of existing metadata to copy from input to output
75 2
        $commandOptionsArray[] = '-metadata ' . $options['metadata'];
76
77
        // Size
78 2
        if (!is_null($options['size-in-percentage'])) {
79
            $sizeSource =  filesize($this->source);
80
            if ($sizeSource !== false) {
81
                $targetSize = floor($sizeSource * $options['size-in-percentage'] / 100);
82
            }
83
        }
84 2
        if (isset($targetSize)) {
85
            $commandOptionsArray[] = '-size ' . $targetSize;
86
        } else {
87
            // Image quality
88 2
            $commandOptionsArray[] = '-q ' . $this->getCalculatedQuality();
89
        }
90
91
92 2
        $commandOptionsArray[] = ($options['lossless'] ? '-lossless' : '');
93
94
        // Losless PNG conversion
95 2
        if ($options['lossless'] === true) {
96
            // No need to add -lossless when near-lossless is used
97
            if ($options['near-lossless'] === 100) {
98
                $commandOptionsArray[] = '-lossless';
99
            }
100
        }
101
102
        // Near-lossles
103 2
        if ($options['near-lossless'] !== 100) {
104
            // We only let near_lossless have effect when lossless is set.
105
            // otherwise lossless auto would not work as expected
106 2
            if ($options['lossless'] === true) {
107
                $commandOptionsArray[] ='-near_lossless ' . $options['near-lossless'];
108
            }
109
        }
110
111 2
        if ($options['autofilter'] === true) {
112
            $commandOptionsArray[] = '-af';
113
        }
114
115
        // Built-in method option
116 2
        $commandOptionsArray[] = '-m ' . strval($options['method']);
117
118
        // Built-in low memory option
119 2
        if ($options['low-memory']) {
120
            $commandOptionsArray[] = '-low_memory';
121
        }
122
123
        // command-line-options
124 2
        if ($options['command-line-options']) {
125 1
            $arr = explode(' -', ' ' . $options['command-line-options']);
126 1
            foreach ($arr as $cmdOption) {
127 1
                $pos = strpos($cmdOption, ' ');
128 1
                $cName = '';
129 1
                $cValue = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $cValue is dead and can be removed.
Loading history...
130 1
                if (!$pos) {
131 1
                    $cName = $cmdOption;
132 1
                    if ($cName == '') {
133 1
                        continue;
134
                    }
135
                    $commandOptionsArray[] = '-' . $cName;
136
                } else {
137 1
                    $cName = substr($cmdOption, 0, $pos);
138 1
                    $cValues = substr($cmdOption, $pos + 1);
139 1
                    $cValuesArr = explode(' ', $cValues);
140 1
                    foreach ($cValuesArr as &$cArg) {
141 1
                        $cArg = escapeshellarg($cArg);
142
                    }
143 1
                    $cValues = implode(' ', $cValuesArr);
144 1
                    $commandOptionsArray[] = '-' . $cName . ' ' . $cValues;
145
                }
146
            }
147
        }
148
149
        // Source file
150 2
        $commandOptionsArray[] = escapeshellarg($this->source);
151
152
        // Output
153 2
        $commandOptionsArray[] = '-o ' . escapeshellarg($this->destination);
154
155
        // Redirect stderr to same place as stdout
156
        // https://www.brianstorti.com/understanding-shell-script-idiom-redirect/
157 2
        $commandOptionsArray[] = '2>&1';
158
159 2
        $commandOptions = implode(' ', $commandOptionsArray);
160 2
        $this->logLn('command line options:' . $commandOptions);
161
162 2
        return $commandOptions;
163
    }
164
165 1
    protected function checkOperationality()
166
    {
167 1
        $options = $this->options;
168 1
        if (!$options['try-supplied-binary-for-os'] && !$options['try-common-system-paths']) {
169
            throw new ConverterNotOperationalException(
170
                'Configured to neither look for cweb binaries in common system locations, ' .
171
                'nor to use one of the supplied precompiled binaries. But these are the only ways ' .
172
                'this converter can convert images. No conversion can be made!'
173
            );
174
        }
175 1
    }
176
177
178 1
    protected function doActualConvert()
179
    {
180 1
        $errorMsg = '';
181 1
        $options = $this->options;
182 1
        $useNice = (($options['use-nice']) && self::hasNiceSupport());
183
184 1
        $commandOptions = $this->createCommandLineOptions();
185
186
187
        // Init with common system paths
188 1
        $cwebpPathsToTest = self::$cwebpDefaultPaths;
189
190
        // Remove paths that doesn't exist
191
        /*
192
        $cwebpPathsToTest = array_filter($cwebpPathsToTest, function ($binary) {
193
            //return file_exists($binary);
194
            return @is_readable($binary);
195
        });
196
        */
197
198
        // Try all common paths that exists
199 1
        $success = false;
200 1
        $failures = [];
201 1
        $failureCodes = [];
202
203
204 1
        $returnCode = 0;
205 1
        $majorFailCode = 0;
206 1
        if ($options['try-common-system-paths']) {
207 1
            foreach ($cwebpPathsToTest as $index => $binary) {
208 1
                $returnCode = $this->executeBinary($binary, $commandOptions, $useNice);
209 1
                if ($returnCode == 0) {
210
                    $this->logLn('Successfully executed binary: ' . $binary);
211
                    $success = true;
212
                    break;
213
                } else {
214 1
                    $failures[] = [$binary, $returnCode];
215 1
                    if (!in_array($returnCode, $failureCodes)) {
216 1
                        $failureCodes[] = $returnCode;
217
                    }
218
                }
219
            }
220
221 1
            if (!$success) {
222 1
                if (count($failureCodes) == 1) {
223 1
                    $majorFailCode = $failureCodes[0];
224
                    switch ($majorFailCode) {
225 1
                        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 1
                        case 127:
231 1
                            $errorMsg .= 'Found no cwebp binaries in any common system locations. ';
232 1
                            break;
233
                        default:
234
                            $errorMsg .= 'Tried executing cwebp binaries in common system locations. ' .
235 1
                                '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 1
        if (!$success && $options['try-supplied-binary-for-os']) {
266
          // Try supplied binary (if available for OS, and hash is correct)
267 1
            if (isset(self::$suppliedBinariesInfo[PHP_OS])) {
268 1
                $info = self::$suppliedBinariesInfo[PHP_OS];
269
270 1
                $file = $info[0];
271 1
                $hash = $info[1];
272
273 1
                $binaryFile = __DIR__ . '/' . $options['rel-path-to-precompiled-binaries'] . '/' . $file;
274
275
                // The file should exist, but may have been removed manually.
276 1
                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 1
                    $proceedAfterHashCheck = true;
283 1
                    if (function_exists('hash_file')) {
284 1
                        $binaryHash = hash_file('sha256', $binaryFile);
285
286 1
                        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 1
                    if ($proceedAfterHashCheck) {
296 1
                        $returnCode = $this->executeBinary($binaryFile, $commandOptions, $useNice);
297 1
                        if ($returnCode == 0) {
298 1
                            $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 1
                                            $errorMsg .= ' (exit code:' .  $returnCode . ').';
325
                                    }
326
                                }
327
                            }
328
                        }
329
                    }
330
                } else {
331 1
                    $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
        // (or perhaps the current umask instead? https://www.php.net/umask)
341
342 1
        if ($success) {
343 1
            $destinationParent = dirname($this->destination);
344 1
            $fileStatistics = stat($destinationParent);
345 1
            if ($fileStatistics !== false) {
346
                // Apply same permissions as parent folder but strip off the executable bits
347 1
                $permissions = $fileStatistics['mode'] & 0000666;
348 1
                chmod($this->destination, $permissions);
349
            }
350
        }
351
352 1
        if (!$success) {
353
            throw new SystemRequirementsNotMetException($errorMsg);
354
        }
355 1
    }
356
}
357