Completed
Push — master ( 874f59...e69b41 )
by Bjørn
09:48
created

Cwebp::tryCommonSystemPaths()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 18.6936

Importance

Changes 0
Metric Value
cc 5
eloc 18
nc 6
nop 2
dl 0
loc 30
ccs 2
cts 11
cp 0.1818
crap 18.6936
rs 9.3554
c 0
b 0
f 0
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
     *
186
     *
187 1
     * @return  string  Error message if failure, empty string if successful
188 1
     */
189 1
    private function composeErrorMessageForCommonSystemPathsFailures($failureCodes)
190
    {
191 1
        if (count($failureCodes) == 1) {
192
            switch ($failureCodes[0]) {
193
                case 126:
194
                    return 'Permission denied. The user that the command was run with (' .
195 1
                        shell_exec('whoami') . ') does not have permission to execute any of the ' .
196
                        'cweb binaries found in common system locations. ';
197
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
198
                case 127:
199
                    return 'Found no cwebp binaries in any common system locations. ';
200
                    break;
201
                default:
202
                    return 'Tried executing cwebp binaries in common system locations. ' .
203
                        'All failed (exit code: ' . $failureCodes[0] . '). ';
204
            }
205
        } else {
206 1
            /**
207 1
             * $failureCodesBesides127 is used to check first position ($failureCodesBesides127[0])
208 1
             * however position can vary as index can be 1 or something else. array_values() would
209
             * always start from 0.
210
             */
211 1
            $failureCodesBesides127 = array_values(array_diff($failureCodes, [127]));
212 1
213 1
            if (count($failureCodesBesides127) == 1) {
214 1
                switch ($failureCodesBesides127[0]) {
215 1
                    case 126:
216 1
                        return 'Permission denied. The user that the command was run with (' .
217
                        shell_exec('whoami') . ') does not have permission to execute any of the cweb ' .
218
                        'binaries found in common system locations. ';
219
                        break;
220
                    default:
221 1
                        return 'Tried executing cwebp binaries in common system locations. ' .
222 1
                        'All failed (exit code: ' . $failureCodesBesides127[0] . '). ';
223 1
                }
224
            } else {
225
                return 'None of the cwebp binaries in the common system locations could be executed ' .
226
                '(mixed results - got the following exit codes: ' . implode(',', $failureCodes) . '). ';
227
            }
228 1
        }
229 1
    }
230 1
231
    /**
232 1
     * Try executing cwebp in common system paths
233
     *
234
     * @param  boolean  $useNice          Whether to use nice
235
     * @param  string   $commandOptions   for the exec call
236
     *
237 1
     * @return  array  Unique failure codes in case of failure, empty array in case of success
238 1
     */
239 1
    private function tryCommonSystemPaths($useNice, $commandOptions)
240
    {
241
        $errorMsg = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $errorMsg is dead and can be removed.
Loading history...
242 1
        $majorFailCode = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $majorFailCode is dead and can be removed.
Loading history...
243
        //$failures = [];
244
        $failureCodes = [];
245
246
        // Loop through paths
247
        foreach (self::$cwebpDefaultPaths as $index => $binary) {
248
            $returnCode = $this->executeBinary($binary, $commandOptions, $useNice);
249
            if ($returnCode == 0) {
250
                $this->logLn('Successfully executed binary: ' . $binary);
251
                return [];
252
            } else {
253
                //$failures[] = [$binary, $returnCode];
254
                if ($returnCode == 127) {
255
                    $this->logLn(
256
                        'Trying to execute binary: ' . $binary . '. Failed (not found)'
257
                    );
258
                } else {
259
                    $this->logLn(
260
                        'Trying to execute binary: ' . $binary . '. Failed (return code: ' . $returnCode . ')'
261
                    );
262
                }
263
                if (!in_array($returnCode, $failureCodes)) {
264
                    $failureCodes[] = $returnCode;
265
                }
266
            }
267
        }
268
        return $failureCodes;
269
    }
270
271
    /**
272 1
     * Try executing supplied cwebp for PHP_OS.
273
     *
274 1
     * @param  boolean  $useNice          Whether to use nice
275 1
     * @param  string   $commandOptions   for the exec call
276
     * @param  array    $failureCodesForCommonSystemPaths  Return codes from the other attempt
277 1
     *                                                     (in order to produce short error message)
278 1
     *
279
     * @return  string  Error message if failure, empty string if successful
280 1
     */
281
    private function trySuppliedBinaryForOS($useNice, $commandOptions, $failureCodesForCommonSystemPaths)
282
    {
283 1
        $this->logLn('Trying to execute supplied binary for OS: ' . PHP_OS);
284
285
        // Try supplied binary (if available for OS, and hash is correct)
286
        $options = $this->options;
287
        if (!isset(self::$suppliedBinariesInfo[PHP_OS])) {
288
            return 'No supplied binaries found for OS:' . PHP_OS;
289 1
        }
290 1
291 1
        $info = self::$suppliedBinariesInfo[PHP_OS];
292
293 1
        $file = $info[0];
294
        $hash = $info[1];
295
296
        $binaryFile = __DIR__ . '/' . $options['rel-path-to-precompiled-binaries'] . '/' . $file;
297
298
299
        // The file should exist, but may have been removed manually.
300
        if (!file_exists($binaryFile)) {
301
            return 'Supplied binary not found! It ought to be here:' . $binaryFile;
302 1
        }
303 1
304 1
        // File exists, now generate its hash
305 1
306
        // hash_file() is normally available, but it is not always
307
        // - https://stackoverflow.com/questions/17382712/php-5-3-20-undefined-function-hash
308
        // If available, validate that hash is correct.
309
310
        if (function_exists('hash_file')) {
311
            $binaryHash = hash_file('sha256', $binaryFile);
312
313
            if ($binaryHash != $hash) {
314
                return 'Binary checksum of supplied binary is invalid! ' .
315
                    'Did you transfer with FTP, but not in binary mode? ' .
316
                    'File:' . $binaryFile . '. ' .
317
                    'Expected checksum: ' . $hash . '. ' .
318
                    'Actual checksum:' . $binaryHash . '.';
319
            }
320
        }
321
322
        $returnCode = $this->executeBinary($binaryFile, $commandOptions, $useNice);
323
        if ($returnCode == 0) {
324
            // yay!
325
            $this->logLn('success!');
326
            return '';
327
        }
328
329
        $errorMsg = 'Tried executing supplied binary for ' . PHP_OS . ', ' .
330
            ($options['try-common-system-paths'] ? 'but that failed too' : 'but failed');
331 1
332
333
        if (($options['try-common-system-paths']) && (count($failureCodesForCommonSystemPaths) > 0)) {
334
            // check if it was the same error
335
            // if it was, simply refer to that with "(same problem)"
336
            $majorFailCode = 0;
337
            if (count($failureCodesForCommonSystemPaths) == 1) {
338 1
                $majorFailCode = $failureCodesForCommonSystemPaths[0];
339
            } else {
340
                $failureCodesBesides127 = array_values(array_diff($failureCodesForCommonSystemPaths, [127]));
341
                if (count($failureCodesBesides127) == 1) {
342
                    $majorFailCode = $failureCodesBesides127[0];
343
                } else {
344
                    // it cannot be summarized into a single code
345
                }
346
            }
347
            if ($majorFailCode != 0) {
348
                $errorMsg .= ' (same problem)';
349 1
                return $errorMsg;
350 1
            }
351 1
        }
352 1
353
        if ($returnCode > 128) {
354 1
            $errorMsg .= '. The binary did not work (exit code: ' . $returnCode . '). ' .
355 1
                'Check out https://github.com/rosell-dk/webp-convert/issues/92';
356
        } else {
357
            switch ($returnCode) {
358
                case 0:
359 1
                    $success = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $success is dead and can be removed.
Loading history...
360
                    break;
361
                case 126:
362 1
                    $errorMsg .= ': Permission denied. The user that the command was run' .
363
                        ' with (' . shell_exec('whoami') . ') does not have permission to ' .
364
                        'execute that binary.';
365
                    break;
366
                case 127:
367
                    $errorMsg .= '. The binary was not found! ' .
368
                        'It ought to be here: ' . $binaryFile;
369
                    break;
370
                default:
371
                    $errorMsg .= ' (exit code:' .  $returnCode . ').';
372
            }
373
        }
374
        return $errorMsg;
375
    }
376
377
    protected function doActualConvert()
378
    {
379
        $errorMsg = '';
380
        $options = $this->options;
381
        $useNice = (($options['use-nice']) && self::hasNiceSupport());
382
383
        $commandOptions = $this->createCommandLineOptions();
384
385
        // Try all common paths that exists
386
        $success = false;
387
388
        $failureCodes = [];
389
390
        if ($options['try-common-system-paths']) {
391
            $failureCodes = $this->tryCommonSystemPaths($useNice, $commandOptions);
0 ignored issues
show
Bug introduced by
The method tryCommonSystemPaths() does not exist on WebPConvert\Convert\Converters\Cwebp. ( Ignorable by Annotation )

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

391
            /** @scrutinizer ignore-call */ 
392
            $failureCodes = $this->tryCommonSystemPaths($useNice, $commandOptions);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
392
            $success = (count($failureCodes) == 0);
393
            $errorMsg = $this->composeErrorMessageForCommonSystemPathsFailures($failureCodes);
0 ignored issues
show
Bug introduced by
The method composeErrorMessageForCommonSystemPathsFailures() does not exist on WebPConvert\Convert\Converters\Cwebp. ( Ignorable by Annotation )

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

393
            /** @scrutinizer ignore-call */ 
394
            $errorMsg = $this->composeErrorMessageForCommonSystemPathsFailures($failureCodes);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
394
        }
395
396
        if (!$success && $options['try-supplied-binary-for-os']) {
397
            $errorMsg2 = $this->trySuppliedBinaryForOS($useNice, $commandOptions, $failureCodes);
0 ignored issues
show
Bug introduced by
The method trySuppliedBinaryForOS() does not exist on WebPConvert\Convert\Converters\Cwebp. ( Ignorable by Annotation )

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

397
            /** @scrutinizer ignore-call */ 
398
            $errorMsg2 = $this->trySuppliedBinaryForOS($useNice, $commandOptions, $failureCodes);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
398
            $errorMsg .= $errorMsg2;
399
            $success = ($errorMsg2 == '');
400
        }
401
402
        // cwebp sets file permissions to 664 but instead ..
403
        // .. $destination's parent folder's permissions should be used (except executable bits)
404
        // (or perhaps the current umask instead? https://www.php.net/umask)
405
406
        if ($success) {
407
            $destinationParent = dirname($this->destination);
408
            $fileStatistics = stat($destinationParent);
409
            if ($fileStatistics !== false) {
410
                // Apply same permissions as parent folder but strip off the executable bits
411
                $permissions = $fileStatistics['mode'] & 0000666;
412
                chmod($this->destination, $permissions);
413
            }
414
        }
415
416
        if (!$success) {
417
            throw new SystemRequirementsNotMetException($errorMsg);
418
        }
419
    }
420
}
421