Completed
Push — master ( 5f5fca...2fa1e0 )
by Bjørn
12:28 queued 02:24
created

Cwebp::createCommandLineOptions()   F

Complexity

Conditions 15
Paths 432

Size

Total Lines 93
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 20.9663

Importance

Changes 0
Metric Value
cc 15
eloc 46
nc 432
nop 0
dl 0
loc 93
ccs 40
cts 57
cp 0.7018
crap 20.9663
rs 2.5388
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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