Passed
Push — master ( 26c56b...7d441f )
by Bjørn
08:31
created

Cwebp   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Test Coverage

Coverage 67.35%

Importance

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

5 Methods

Rating   Name   Duplication   Size   Complexity  
A checkOperationality() 0 8 3
A executeBinary() 0 9 2
A getOptionDefinitionsExtra() 0 13 1
F createCommandLineOptions() 0 96 16
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
/**
11
 * Convert images to webp by calling cwebp binary.
12
 *
13
 * @package    WebPConvert
14
 * @author     Bjørn Rosell <[email protected]>
15
 * @since      Class available since Release 2.0.0
16
 */
17
class Cwebp extends AbstractExecConverter
18
{
19
    protected $supportsLossless = true;
20
21 3
    protected function getOptionDefinitionsExtra()
22
    {
23
        return [
24 3
            ['autofilter', 'boolean', false],
25
            ['command-line-options', 'string', ''],
26
            ['low-memory', 'boolean', false],
27
            ['method', 'number', 6],
28
            ['near-lossless', 'integer', 60],
29
            ['rel-path-to-precompiled-binaries', 'string', './Binaries'],
30
            ['size-in-percentage', 'number', null],
31
            ['try-common-system-paths', 'boolean', true],
32
            ['try-supplied-binary-for-os', 'boolean', true],
33
            ['use-nice', 'boolean', false],
34
35
        ];
36
    }
37
38
    // System paths to look for cwebp binary
39
    private static $cwebpDefaultPaths = [
40
        '/usr/bin/cwebp',
41
        '/usr/local/bin/cwebp',
42
        '/usr/gnu/bin/cwebp',
43
        '/usr/syno/bin/cwebp'
44
    ];
45
46
    // OS-specific binaries included in this library, along with hashes
47
    // If other binaries are going to be added, notice that the first argument is what PHP_OS returns.
48
    // (possible values, see here: https://stackoverflow.com/questions/738823/possible-values-for-php-os)
49
    private static $suppliedBinariesInfo = [
50
        'WINNT' => [ 'cwebp.exe', '49e9cb98db30bfa27936933e6fd94d407e0386802cb192800d9fd824f6476873'],
51
        'Darwin' => [ 'cwebp-mac12', 'a06a3ee436e375c89dbc1b0b2e8bd7729a55139ae072ed3f7bd2e07de0ebb379'],
52
        'SunOS' => [ 'cwebp-sol', '1febaffbb18e52dc2c524cda9eefd00c6db95bc388732868999c0f48deb73b4f'],
53
        'FreeBSD' => [ 'cwebp-fbsd', 'e5cbea11c97fadffe221fdf57c093c19af2737e4bbd2cb3cd5e908de64286573'],
54
        'Linux' => [ 'cwebp-linux', '916623e5e9183237c851374d969aebdb96e0edc0692ab7937b95ea67dc3b2568']
55
    ];
56
57
58 1
    private function executeBinary($binary, $commandOptions, $useNice)
59
    {
60 1
        $command = ($useNice ? 'nice ' : '') . $binary . ' ' . $commandOptions;
61
62
        //$logger->logLn('command options:' . $commandOptions);
63
        //$logger->logLn('Trying to execute binary:' . $binary);
64 1
        exec($command, $output, $returnCode);
65
        //$logger->logLn(self::msgForExitCode($returnCode));
66 1
        return intval($returnCode);
67
    }
68
69
    /**
70
     * Build command line options
71
     *
72
     * @return string
73
     */
74 2
    private function createCommandLineOptions()
75
    {
76 2
        $options = $this->options;
77
78 2
        $commandOptionsArray = [];
79
80
        // Metadata (all, exif, icc, xmp or none (default))
81
        // Comma-separated list of existing metadata to copy from input to output
82 2
        $commandOptionsArray[] = '-metadata ' . $options['metadata'];
83
84
        // Size
85 2
        if (!is_null($options['size-in-percentage'])) {
86
            $sizeSource =  filesize($this->source);
87
            if ($sizeSource !== false) {
88
                $targetSize = floor($sizeSource * $options['size-in-percentage'] / 100);
89
            }
90
        }
91 2
        if (isset($targetSize)) {
92
            $commandOptionsArray[] = '-size ' . $targetSize;
93
        } else {
94
            // Image quality
95 2
            $commandOptionsArray[] = '-q ' . $this->getCalculatedQuality();
96
        }
97
98
99 2
        $commandOptionsArray[] = ($options['lossless'] ? '-lossless' : '');
100
101
        // Losless PNG conversion
102 2
        if ($options['lossless'] === true) {
103
            // No need to add -lossless when near-lossless is used
104 1
            if ($options['near-lossless'] === 100) {
105
                $commandOptionsArray[] = '-lossless';
106
            }
107
        }
108
109
        // Near-lossles
110 2
        if ($options['near-lossless'] !== 100) {
111
            // We only let near_lossless have effect when lossless is set.
112
            // otherwise lossless auto would not work as expected
113 2
            if ($options['lossless'] === true) {
114 1
                $commandOptionsArray[] ='-near_lossless ' . $options['near-lossless'];
115
            }
116
        }
117
118 2
        if ($options['autofilter'] === true) {
119
            $commandOptionsArray[] = '-af';
120
        }
121
122
        // Built-in method option
123 2
        $commandOptionsArray[] = '-m ' . strval($options['method']);
124
125
        // Built-in low memory option
126 2
        if ($options['low-memory']) {
127
            $commandOptionsArray[] = '-low_memory';
128
        }
129
130
        // command-line-options
131 2
        if ($options['command-line-options']) {
132 1
            $arr = explode(' -', ' ' . $options['command-line-options']);
133 1
            foreach ($arr as $cmdOption) {
134 1
                $pos = strpos($cmdOption, ' ');
135 1
                $cName = '';
136 1
                $cValue = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $cValue is dead and can be removed.
Loading history...
137 1
                if (!$pos) {
138 1
                    $cName = $cmdOption;
139 1
                    if ($cName == '') {
140 1
                        continue;
141
                    }
142
                    $commandOptionsArray[] = '-' . $cName;
143
                } else {
144 1
                    $cName = substr($cmdOption, 0, $pos);
145 1
                    $cValues = substr($cmdOption, $pos + 1);
146 1
                    $cValuesArr = explode(' ', $cValues);
147 1
                    foreach ($cValuesArr as &$cArg) {
148 1
                        $cArg = escapeshellarg($cArg);
149
                    }
150 1
                    $cValues = implode(' ', $cValuesArr);
151 1
                    $commandOptionsArray[] = '-' . $cName . ' ' . $cValues;
152
                }
153
            }
154
        }
155
156
        // Source file
157 2
        $commandOptionsArray[] = escapeshellarg($this->source);
158
159
        // Output
160 2
        $commandOptionsArray[] = '-o ' . escapeshellarg($this->destination);
161
162
        // Redirect stderr to same place as stdout
163
        // https://www.brianstorti.com/understanding-shell-script-idiom-redirect/
164 2
        $commandOptionsArray[] = '2>&1';
165
166 2
        $commandOptions = implode(' ', $commandOptionsArray);
167 2
        $this->logLn('command line options:' . $commandOptions);
168
169 2
        return $commandOptions;
170
    }
171
172 1
    public function checkOperationality()
173
    {
174 1
        $options = $this->options;
175 1
        if (!$options['try-supplied-binary-for-os'] && !$options['try-common-system-paths']) {
176
            throw new ConverterNotOperationalException(
177
                'Configured to neither look for cweb binaries in common system locations, ' .
178
                'nor to use one of the supplied precompiled binaries. But these are the only ways ' .
179
                'this converter can convert images. No conversion can be made!'
180
            );
181
        }
182 1
    }
183
184
185 1
    protected function doActualConvert()
186
    {
187 1
        $errorMsg = '';
188 1
        $options = $this->options;
189 1
        $useNice = (($options['use-nice']) && self::hasNiceSupport());
190
191 1
        $commandOptions = $this->createCommandLineOptions();
192
193
194
        // Init with common system paths
195 1
        $cwebpPathsToTest = self::$cwebpDefaultPaths;
196
197
        // Remove paths that doesn't exist
198
        /*
199
        $cwebpPathsToTest = array_filter($cwebpPathsToTest, function ($binary) {
200
            //return file_exists($binary);
201
            return @is_readable($binary);
202
        });
203
        */
204
205
        // Try all common paths that exists
206 1
        $success = false;
207 1
        $failures = [];
208 1
        $failureCodes = [];
209
210
211 1
        $returnCode = 0;
212 1
        $majorFailCode = 0;
213 1
        if ($options['try-common-system-paths']) {
214 1
            foreach ($cwebpPathsToTest as $index => $binary) {
215 1
                $returnCode = $this->executeBinary($binary, $commandOptions, $useNice);
216 1
                if ($returnCode == 0) {
217
                    $this->logLn('Successfully executed binary: ' . $binary);
218
                    $success = true;
219
                    break;
220
                } else {
221 1
                    $failures[] = [$binary, $returnCode];
222 1
                    if (!in_array($returnCode, $failureCodes)) {
223 1
                        $failureCodes[] = $returnCode;
224
                    }
225
                }
226
            }
227
228 1
            if (!$success) {
229 1
                if (count($failureCodes) == 1) {
230 1
                    $majorFailCode = $failureCodes[0];
231
                    switch ($majorFailCode) {
232 1
                        case 126:
233
                            $errorMsg = 'Permission denied. The user that the command was run with (' .
234
                                shell_exec('whoami') . ') does not have permission to execute any of the ' .
235
                                'cweb binaries found in common system locations. ';
236
                            break;
237 1
                        case 127:
238 1
                            $errorMsg .= 'Found no cwebp binaries in any common system locations. ';
239 1
                            break;
240
                        default:
241
                            $errorMsg .= 'Tried executing cwebp binaries in common system locations. ' .
242 1
                                'All failed (exit code: ' . $majorFailCode . '). ';
243
                    }
244
                } else {
245
                    /**
246
                     * $failureCodesBesides127 is used to check first position ($failureCodesBesides127[0])
247
                     * however position can vary as index can be 1 or something else. array_values() would
248
                     * always start from 0.
249
                     */
250
                    $failureCodesBesides127 = array_values(array_diff($failureCodes, [127]));
251
252
                    if (count($failureCodesBesides127) == 1) {
253
                        $majorFailCode = $failureCodesBesides127[0];
254
                        switch ($returnCode) {
255
                            case 126:
256
                                $errorMsg = 'Permission denied. The user that the command was run with (' .
257
                                shell_exec('whoami') . ') does not have permission to execute any of the cweb ' .
258
                                'binaries found in common system locations. ';
259
                                break;
260
                            default:
261
                                $errorMsg .= 'Tried executing cwebp binaries in common system locations. ' .
262
                                'All failed (exit code: ' . $majorFailCode . '). ';
263
                        }
264
                    } else {
265
                        $errorMsg .= 'None of the cwebp binaries in the common system locations could be executed ' .
266
                        '(mixed results - got the following exit codes: ' . implode(',', $failureCodes) . '). ';
267
                    }
268
                }
269
            }
270
        }
271
272 1
        if (!$success && $options['try-supplied-binary-for-os']) {
273
          // Try supplied binary (if available for OS, and hash is correct)
274 1
            if (isset(self::$suppliedBinariesInfo[PHP_OS])) {
275 1
                $info = self::$suppliedBinariesInfo[PHP_OS];
276
277 1
                $file = $info[0];
278 1
                $hash = $info[1];
279
280 1
                $binaryFile = __DIR__ . '/' . $options['rel-path-to-precompiled-binaries'] . '/' . $file;
281
282
                // The file should exist, but may have been removed manually.
283 1
                if (file_exists($binaryFile)) {
284
                    // File exists, now generate its hash
285
286
                    // hash_file() is normally available, but it is not always
287
                    // - https://stackoverflow.com/questions/17382712/php-5-3-20-undefined-function-hash
288
                    // If available, validate that hash is correct.
289 1
                    $proceedAfterHashCheck = true;
290 1
                    if (function_exists('hash_file')) {
291 1
                        $binaryHash = hash_file('sha256', $binaryFile);
292
293 1
                        if ($binaryHash != $hash) {
294
                            $errorMsg .= 'Binary checksum of supplied binary is invalid! ' .
295
                                'Did you transfer with FTP, but not in binary mode? ' .
296
                                'File:' . $binaryFile . '. ' .
297
                                'Expected checksum: ' . $hash . '. ' .
298
                                'Actual checksum:' . $binaryHash . '.';
299
                            $proceedAfterHashCheck = false;
300
                        }
301
                    }
302 1
                    if ($proceedAfterHashCheck) {
303 1
                        $returnCode = $this->executeBinary($binaryFile, $commandOptions, $useNice);
304 1
                        if ($returnCode == 0) {
305 1
                            $success = true;
306
                        } else {
307
                            $errorMsg .= 'Tried executing supplied binary for ' . PHP_OS . ', ' .
308
                                ($options['try-common-system-paths'] ? 'but that failed too' : 'but failed');
309
                            if ($options['try-common-system-paths'] && ($majorFailCode > 0)) {
310
                                $errorMsg .= ' (same error)';
311
                            } else {
312
                                if ($returnCode > 128) {
313
                                    $errorMsg .= '. The binary did not work (exit code: ' . $returnCode . '). ' .
314
                                        'Check out https://github.com/rosell-dk/webp-convert/issues/92';
315
                                } else {
316
                                    switch ($returnCode) {
317
                                        case 0:
318
                                            $success = true;
319
                                            ;
320
                                            break;
321
                                        case 126:
322
                                            $errorMsg .= ': Permission denied. The user that the command was run' .
323
                                                ' with (' . shell_exec('whoami') . ') does not have permission to ' .
324
                                                'execute that binary.';
325
                                            break;
326
                                        case 127:
327
                                            $errorMsg .= '. The binary was not found! ' .
328
                                                'It ought to be here: ' . $binaryFile;
329
                                            break;
330
                                        default:
331 1
                                            $errorMsg .= ' (exit code:' .  $returnCode . ').';
332
                                    }
333
                                }
334
                            }
335
                        }
336
                    }
337
                } else {
338 1
                    $errorMsg .= 'Supplied binary not found! It ought to be here:' . $binaryFile;
339
                }
340
            } else {
341
                $errorMsg .= 'No supplied binaries found for OS:' . PHP_OS;
342
            }
343
        }
344
345
        // cwebp sets file permissions to 664 but instead ..
346
        // .. $destination's parent folder's permissions should be used (except executable bits)
347
        // (or perhaps the current umask instead? https://www.php.net/umask)
348
349 1
        if ($success) {
350 1
            $destinationParent = dirname($this->destination);
351 1
            $fileStatistics = stat($destinationParent);
352 1
            if ($fileStatistics !== false) {
353
                // Apply same permissions as parent folder but strip off the executable bits
354 1
                $permissions = $fileStatistics['mode'] & 0000666;
355 1
                chmod($this->destination, $permissions);
356
            }
357
        }
358
359 1
        if (!$success) {
360
            throw new SystemRequirementsNotMetException($errorMsg);
361
        }
362 1
    }
363
}
364