Passed
Push — master ( 5cdfb5...e6e6ab )
by Bjørn
02:42
created

Cwebp::getCwebpVersions()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 40
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.4227

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 22
nc 10
nop 0
dl 0
loc 40
ccs 17
cts 22
cp 0.7727
crap 6.4227
rs 8.9457
c 1
b 0
f 0
1
<?php
2
3
namespace WebPConvert\Convert\Converters;
4
5
use WebPConvert\Convert\Converters\AbstractConverter;
6
use WebPConvert\Convert\Converters\ConverterTraits\EncodingAutoTrait;
7
use WebPConvert\Convert\Converters\ConverterTraits\ExecTrait;
8
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
9
use WebPConvert\Convert\Exceptions\ConversionFailedException;
10
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException;
11
use WebPConvert\Options\BooleanOption;
12
use WebPConvert\Options\SensitiveStringOption;
13
use WebPConvert\Options\StringOption;
14
15
/**
16
 * Convert images to webp by calling cwebp binary.
17
 *
18
 * @package    WebPConvert
19
 * @author     Bjørn Rosell <[email protected]>
20
 * @since      Class available since Release 2.0.0
21
 */
22
class Cwebp extends AbstractConverter
23
{
24
25
    use EncodingAutoTrait;
26
    use ExecTrait;
27
28
    protected function getUnsupportedDefaultOptions()
29
    {
30
        return [];
31
    }
32
33 8
    protected function createOptions()
34
    {
35 8
        parent::createOptions();
36
37 8
        $this->options2->addOptions(
38 8
            new StringOption('command-line-options', ''),
39 8
            new SensitiveStringOption('rel-path-to-precompiled-binaries', './Binaries'),
40 8
            new BooleanOption('try-cwebp', true),
41 8
            new BooleanOption('try-common-system-paths', true),
42 8
            new BooleanOption('try-supplied-binary-for-os', true)
43
        );
44 8
    }
45
46
    // System paths to look for cwebp binary
47
    private static $cwebpDefaultPaths = [
48
        //'cwebp',
49
        '/usr/bin/cwebp',
50
        '/usr/local/bin/cwebp',
51
        '/usr/gnu/bin/cwebp',
52
        '/usr/syno/bin/cwebp'
53
    ];
54
55
    // OS-specific binaries included in this library, along with hashes
56
    // If other binaries are going to be added, notice that the first argument is what PHP_OS returns.
57
    // (possible values, see here: https://stackoverflow.com/questions/738823/possible-values-for-php-os)
58
    // Got the precompiled binaries here: https://developers.google.com/speed/webp/docs/precompiled
59
    private static $suppliedBinariesInfo = [
60
        'WINNT' => [
61
            ['cwebp-1.0.3-windows-x64.exe', 'b3aaab03ca587e887f11f6ae612293d034ee04f4f7f6bc7a175321bb47a10169'],
62
        ],
63
        'Darwin' => [
64
            ['cwebp-1.0.3-mac-10.14', '7332ed5f0d4091e2379b1eaa32a764f8c0d51b7926996a1dc8b4ef4e3c441a12'],
65
        ],
66
        'SunOS' => [
67
            // Got this from ewww Wordpress plugin, which unfortunately still uses the old 0.6.0 versions
68
            // Can you help me get a 1.0.3 version?
69
            ['cwebp-0.6.0-solaris', '1febaffbb18e52dc2c524cda9eefd00c6db95bc388732868999c0f48deb73b4f']
70
        ],
71
        'FreeBSD' => [
72
            // Got this from ewww Wordpress plugin, which unfortunately still uses the old 0.6.0 versions
73
            // Can you help me get a 1.0.3 version?
74
            ['cwebp-0.6.0-fbsd', 'e5cbea11c97fadffe221fdf57c093c19af2737e4bbd2cb3cd5e908de64286573']
75
        ],
76
        'Linux' => [
77
            // Dynamically linked executable.
78
            // It seems it is slightly faster than the statically linked
79
            ['cwebp-1.0.3-linux-x86-64', 'a663215a46d347f63e1ca641c18527a1ae7a2c9a0ae85ca966a97477ea13dfe0'],
80
81
            // Statically linked executable
82
            // It may be that it on some systems works, where the dynamically linked does not (see #196)
83
            ['cwebp-1.0.3-linux-x86-64-static', 'ab96f01b49336da8b976c498528080ff614112d5985da69943b48e0cb1c5228a'],
84
85
            // Old executable for systems in case both of the above fails
86
            ['cwebp-0.6.1-linux-x86-64', '916623e5e9183237c851374d969aebdb96e0edc0692ab7937b95ea67dc3b2568'],
87
        ]
88
    ];
89
90
    /**
91
     *  Check all hashes of the precompiled binaries.
92
     *
93
     *  This isn't used when converting, but can be used as a startup check.
94
     */
95
    public function checkAllHashes()
96
    {
97
        foreach (self::$suppliedBinariesInfo as $os => $arr) {
98
            foreach ($arr as $i => list($filename, $hash)) {
99
                if ($hash != hash_file("sha256", __DIR__ . '/Binaries/' . $filename)) {
100
                    throw new \Exception('Hash for ' . $filename . ' is incorrect!');
101
                }
102
            }
103
        }
104
    }
105
106 3
    public function checkOperationality()
107
    {
108 3
        $this->checkOperationalityExecTrait();
109
110 3
        $options = $this->options;
111 3
        if (!$options['try-supplied-binary-for-os'] && !$options['try-common-system-paths'] && !$options['try-cwebp']) {
112 1
            throw new ConverterNotOperationalException(
113
                'Configured to neither try pure cwebp command, ' .
114
                'nor look for cweb binaries in common system locations and ' .
115
                'nor to use one of the supplied precompiled binaries. ' .
116 1
                'But these are the only ways this converter can convert images. No conversion can be made!'
117
            );
118
        }
119 2
    }
120
121 2
    private function executeBinary($binary, $commandOptions, $useNice)
122
    {
123
        //$version = $this->detectVersion($binary);
124
125 2
        $command = ($useNice ? 'nice ' : '') . $binary . ' ' . $commandOptions;
126
127
        //$logger->logLn('command options:' . $commandOptions);
128 2
        $this->logLn('Trying to convert by executing the following command:');
129 2
        $this->logLn($command);
130 2
        exec($command, $output, $returnCode);
131 2
        $this->logExecOutput($output);
132
        /*
133
        if ($returnCode == 255) {
134
            if (isset($output[0])) {
135
                // Could be an error like 'Error! Cannot open output file' or 'Error! ...preset... '
136
                $this->logLn(print_r($output[0], true));
137
            }
138
        }*/
139
        //$logger->logLn(self::msgForExitCode($returnCode));
140 2
        return intval($returnCode);
141
    }
142
143
    /**
144
     *  Use "escapeshellarg()" on all arguments in a commandline string of options
145
     *
146
     *  For example, passing '-sharpness 5 -crop 10 10 40 40 -low_memory' will result in:
147
     *  [
148
     *    "-sharpness '5'"
149
     *    "-crop '10' '10' '40' '40'"
150
     *    "-low_memory"
151
     *  ]
152
     * @param  string $commandLineOptions  string which can contain multiple commandline options
153
     * @return array  Array of command options
154
     */
155 1
    private static function escapeShellArgOnCommandLineOptions($commandLineOptions)
156
    {
157 1
        if (!ctype_print($commandLineOptions)) {
158
            throw new ConversionFailedException(
159
                'Non-printable characters are not allowed in the extra command line options'
160
            );
161
        }
162
163 1
        if (preg_match('#[^a-zA-Z0-9_\s\-]#', $commandLineOptions)) {
164
            throw new ConversionFailedException('The extra command line options contains inacceptable characters');
165
        }
166
167 1
        $cmdOptions = [];
168 1
        $arr = explode(' -', ' ' . $commandLineOptions);
169 1
        foreach ($arr as $cmdOption) {
170 1
            $pos = strpos($cmdOption, ' ');
171 1
            $cName = '';
172 1
            if (!$pos) {
173 1
                $cName = $cmdOption;
174 1
                if ($cName == '') {
175 1
                    continue;
176
                }
177 1
                $cmdOptions[] = '-' . $cName;
178
            } else {
179 1
                $cName = substr($cmdOption, 0, $pos);
180 1
                $cValues = substr($cmdOption, $pos + 1);
181 1
                $cValuesArr = explode(' ', $cValues);
182 1
                foreach ($cValuesArr as &$cArg) {
183 1
                    $cArg = escapeshellarg($cArg);
184
                }
185 1
                $cValues = implode(' ', $cValuesArr);
186 1
                $cmdOptions[] = '-' . $cName . ' ' . $cValues;
187
            }
188
        }
189 1
        return $cmdOptions;
190
    }
191
192
    /**
193
     * Build command line options for a given version of cwebp.
194
     *
195
     * The "-near_lossless" param is not supported on older versions of cwebp, so skip on those.
196
     *
197
     * @param  string $version  Version of cwebp (ie "1.0.3")
198
     * @return string
199
     */
200 6
    private function createCommandLineOptions($version)
201
    {
202
203 6
        $this->logLn('Creating command line options for version: ' . $version);
204
205
        // we only need two decimal places for version.
206
        // convert to number to make it easier to compare
207 6
        $version = preg_match('#^\d+\.\d+#', $version, $matches);
208 6
        $versionNum = 0;
209 6
        if (isset($matches[0])) {
210 6
            $versionNum = floatval($matches[0]);
211
        } else {
212
            $this->logLn(
213
                'Could not extract version number from the following version string: ' . $version,
214
                'bold'
215
            );
216
        }
217
218
        //$this->logLn('version:' . strval($versionNum));
219
220 6
        $options = $this->options;
221
222 6
        $cmdOptions = [];
223
224
        // Metadata (all, exif, icc, xmp or none (default))
225
        // Comma-separated list of existing metadata to copy from input to output
226 6
        if ($versionNum >= 0.3) {
227 6
            $cmdOptions[] = '-metadata ' . $options['metadata'];
228
        }
229
230
        // preset. Appears first in the list as recommended in the docs
231 6
        if (!is_null($options['preset'])) {
232 6
            if ($options['preset'] != 'none') {
233 1
                $cmdOptions[] = '-preset ' . $options['preset'];
234
            }
235
        }
236
237
        // Size
238 6
        $addedSizeOption = false;
239 6
        if (!is_null($options['size-in-percentage'])) {
240 1
            $sizeSource = filesize($this->source);
241 1
            if ($sizeSource !== false) {
242 1
                $targetSize = floor($sizeSource * $options['size-in-percentage'] / 100);
243 1
                $cmdOptions[] = '-size ' . $targetSize;
244 1
                $addedSizeOption = true;
245
            }
246
        }
247
248
        // quality
249 6
        if (!$addedSizeOption) {
250 5
            $cmdOptions[] = '-q ' . $this->getCalculatedQuality();
251
        }
252
253
        // alpha-quality
254 6
        if ($this->options['alpha-quality'] !== 100) {
255 6
            $cmdOptions[] = '-alpha_q ' . escapeshellarg($this->options['alpha-quality']);
256
        }
257
258
        // Losless PNG conversion
259 6
        if ($options['encoding'] == 'lossless') {
260
            // No need to add -lossless when near-lossless is used (on version >= 0.5)
261 4
            if (($options['near-lossless'] === 100) || ($versionNum < 0.5)) {
262 1
                $cmdOptions[] = '-lossless';
263
            }
264
        }
265
266
        // Near-lossles
267 6
        if ($options['near-lossless'] !== 100) {
268 5
            if ($versionNum < 0.5) {
269
                $this->logLn(
270
                    'The near-lossless option is not supported on this (rather old) version of cwebp' .
271
                        '- skipping it.',
272
                    'italic'
273
                );
274
            } else {
275
                // We only let near_lossless have effect when encoding is set to "lossless"
276
                // otherwise encoding=auto would not work as expected
277
278 5
                if ($options['encoding'] == 'lossless') {
279 3
                    $cmdOptions[] ='-near_lossless ' . $options['near-lossless'];
280
                } else {
281 4
                    $this->logLn(
282 4
                        'The near-lossless option ignored for lossy'
283
                    );
284
                }
285
            }
286
        }
287
288 6
        if ($options['auto-filter'] === true) {
289 1
            $cmdOptions[] = '-af';
290
        }
291
292
        // Built-in method option
293 6
        $cmdOptions[] = '-m ' . strval($options['method']);
294
295
        // Built-in low memory option
296 6
        if ($options['low-memory']) {
297 1
            $cmdOptions[] = '-low_memory';
298
        }
299
300
        // command-line-options
301 6
        if ($options['command-line-options']) {
302
            /*
303
            In some years, we can use the splat instead (requires PHP 5.6)
304
            array_push(
305
                $cmdOptions,
306
                ...self::escapeShellArgOnCommandLineOptions($options['command-line-options'])
307
            );
308
            */
309 1
            foreach (self::escapeShellArgOnCommandLineOptions($options['command-line-options']) as $cmdLineOption) {
310 1
                array_push($cmdOptions, $cmdLineOption);
311
            }
312
        }
313
314
        // Source file
315 6
        $cmdOptions[] = escapeshellarg($this->source);
316
317
        // Output
318 6
        $cmdOptions[] = '-o ' . escapeshellarg($this->destination);
319
320
        // Redirect stderr to same place as stdout
321
        // https://www.brianstorti.com/understanding-shell-script-idiom-redirect/
322 6
        $cmdOptions[] = '2>&1';
323
324 6
        $commandOptions = implode(' ', $cmdOptions);
325
        //$this->logLn('command line options:' . $commandOptions);
326
327 6
        return $commandOptions;
328
    }
329
330
    /**
331
     *  Get path for supplied binary for current OS - and validate hash.
332
     *
333
     *  @return  array  Array of supplied binaries (which actually exists, and where hash validates)
334
     */
335 2
    private function getSuppliedBinaryPathForOS()
336
    {
337 2
        $this->log('Checking if we have a supplied precompiled binary for your OS (' . PHP_OS . ')... ');
338
339
        // Try supplied binary (if available for OS, and hash is correct)
340 2
        $options = $this->options;
341 2
        if (!isset(self::$suppliedBinariesInfo[PHP_OS])) {
342
            $this->logLn('No we dont - not for that OS');
343
            return [];
344
        }
345
346 2
        $result = [];
347 2
        $files = self::$suppliedBinariesInfo[PHP_OS];
348 2
        if (count($files) == 1) {
349
            $this->logLn('We do.');
350
        } else {
351 2
            $this->logLn('We do. We in fact have ' . count($files));
352
        }
353
354 2
        foreach ($files as $i => list($file, $hash)) {
355
            //$file = $info[0];
356
            //$hash = $info[1];
357
358 2
            $binaryFile = __DIR__ . '/' . $options['rel-path-to-precompiled-binaries'] . '/' . $file;
359
360
            // Replace "/./" with "/" in path (we could alternatively use realpath)
361
            //$binaryFile = preg_replace('#\/\.\/#', '/', $binaryFile);
362
            // The file should exist, but may have been removed manually.
363
            /*
364
            if (!file_exists($binaryFile)) {
365
                $this->logLn('Supplied binary not found! It ought to be here:' . $binaryFile, 'italic');
366
                return false;
367
            }*/
368
369 2
            $realPathResult = realpath($binaryFile);
370 2
            if ($realPathResult === false) {
371
                $this->logLn('Supplied binary not found! It ought to be here:' . $binaryFile, 'italic');
372
                continue;
373
            }
374 2
            $binaryFile = $realPathResult;
375
376
            // File exists, now generate its hash
377
            // hash_file() is normally available, but it is not always
378
            // - https://stackoverflow.com/questions/17382712/php-5-3-20-undefined-function-hash
379
            // If available, validate that hash is correct.
380
381 2
            if (function_exists('hash_file')) {
382 2
                $binaryHash = hash_file('sha256', $binaryFile);
383
384 2
                if ($binaryHash != $hash) {
385
                    $this->logLn(
386
                        'Binary checksum of supplied binary is invalid! ' .
387
                        'Did you transfer with FTP, but not in binary mode? ' .
388
                        'File:' . $binaryFile . '. ' .
389
                        'Expected checksum: ' . $hash . '. ' .
390
                        'Actual checksum:' . $binaryHash . '.',
391
                        'bold'
392
                    );
393
                    continue;
394
                }
395
            }
396 2
            $result[] = $binaryFile;
397
        }
398
399 2
        return $result;
400
    }
401
402
    private function who()
403
    {
404
        exec('whoami', $whoOutput, $whoReturnCode);
405
        if (($whoReturnCode == 0) && (isset($whoOutput[0]))) {
406
            return $whoOutput[0];
407
        } else {
408
            return 'the user that the command was run with';
409
        }
410
    }
411
412
    /**
413
     *
414
     * @return  string|int  Version string (ie "1.0.2") OR return code, in case of failure
415
     */
416 2
    private function detectVersion($binary)
417
    {
418 2
        $command = $binary . ' -version';
419 2
        $this->log('- Executing: ' . $command);
420 2
        exec($command, $output, $returnCode);
421
422 2
        if ($returnCode == 0) {
423 2
            if (isset($output[0])) {
424 2
                $this->logLn('. Result: version: *' . $output[0] . '*');
425 2
                return $output[0];
426
            }
427
        } else {
428 2
            $this->log('. Result: ');
429 2
            if ($returnCode == 127) {
430 2
                $this->logLn('*Exec failed* (the cwebp binary was not found at path: ' . $binary. ')');
431
            } else {
432
                if ($returnCode == 126) {
433
                    $this->logLn(
434
                        '*Exec failed*. ' .
435
                        'Permission denied (' . $this->who() . ' does not have permission to execute that binary)'
436
                    );
437
                } else {
438
                    $this->logLn(
439
                        '*Exec failed* (return code: ' . $returnCode . ')'
440
                    );
441
                    $this->logExecOutput($output);
442
                }
443
            }
444 2
            return $returnCode;
445
        }
446
    }
447
448
    /**
449
     *  Check versions for an array of binaries.
450
     *
451
     *  @return  array  the "detected" key holds working binaries and their version numbers, the
452
     *                  the "failed" key holds failed binaries and their error codes.
453
     */
454 2
    private function detectVersions($binaries)
455
    {
456 2
        $binariesWithVersions = [];
457 2
        $binariesWithFailCodes = [];
458
459 2
        foreach ($binaries as $binary) {
460 2
            $versionStringOrFailCode = $this->detectVersion($binary);
461
        //    $this->logLn($binary . ': ' . $versionString);
462 2
            if (gettype($versionStringOrFailCode) == 'string') {
463 2
                $binariesWithVersions[$binary] = $versionStringOrFailCode;
464
            } else {
465 2
                $binariesWithFailCodes[$binary] = $versionStringOrFailCode;
466
            }
467
        }
468 2
        return ['detected' => $binariesWithVersions, 'failed' => $binariesWithFailCodes];
469
    }
470
471
    /**
472
     *  Detect versions of all cwebps that are of relevance (according to configuration).
473
     *
474
     *  @return  array  the "detected" key holds working binaries and their version numbers, the
475
     *                  the "failed" key holds failed binaries and their error codes.
476
     */
477 2
    private function getCwebpVersions()
478
    {
479
        // TODO: Check out if exec('whereis cwebp'); would be a good idea
480
481 2
        if (defined('WEBPCONVERT_CWEBP_PATH')) {
482
            $this->logLn('WEBPCONVERT_CWEBP_PATH was defined, so using that path and ignoring any other');
483
            return $this->detectVersions([constant('WEBPCONVERT_CWEBP_PATH')]);
484
        }
485 2
        if (!empty(getenv('WEBPCONVERT_CWEBP_PATH'))) {
486
            $this->logLn(
487
                'WEBPCONVERT_CWEBP_PATH environment variable was set, so using that path and ignoring any other'
488
            );
489
            return $this->detectVersions([getenv('WEBPCONVERT_CWEBP_PATH')]);
490
        }
491
492 2
        $versions = [];
493 2
        if ($this->options['try-cwebp']) {
494 2
            $this->logLn(
495 2
                'Detecting version of cwebp command (it may not be available, but we try nonetheless)'
496
            );
497 2
            $versions = $this->detectVersions(['cwebp']);
498
        }
499 2
        if ($this->options['try-common-system-paths']) {
500
            // Note:
501
            // We used to do a file_exists($binary) check.
502
            // That was not a good idea because it could trigger open_basedir errors. The open_basedir
503
            // restriction does not operate on the exec command. So note to self: Do not do that again.
504 1
            $this->logLn(
505
                'Detecting versions of the cwebp binaries in common system paths ' .
506 1
                '(some may not be found, that is to be expected)'
507
            );
508 1
            $versions = array_merge_recursive($versions, $this->detectVersions(self::$cwebpDefaultPaths));
509
        }
510 2
        if ($this->options['try-supplied-binary-for-os']) {
511 2
            $versions = array_merge_recursive(
512 2
                $versions,
513 2
                $this->detectVersions($this->getSuppliedBinaryPathForOS())
514
            );
515
        }
516 2
        return $versions;
517
    }
518
519
    /**
520
     * Try executing a cwebp binary (or command, like: "cwebp")
521
     *
522
     * @param  string  $binary
523
     * @param  string  $version  Version of cwebp (ie "1.0.3")
524
     * @param  boolean $useNice  Whether to use "nice" command or not
525
     *
526
     * @return boolean  success or not.
527
     */
528 2
    private function tryCwebpBinary($binary, $version, $useNice)
529
    {
530
531
        //$this->logLn('Trying binary: ' . $binary);
532 2
        $commandOptions = $this->createCommandLineOptions($version);
533
534 2
        $returnCode = $this->executeBinary($binary, $commandOptions, $useNice);
535 2
        if ($returnCode == 0) {
536
            // It has happened that even with return code 0, there was no file at destination.
537 2
            if (!file_exists($this->destination)) {
538
                $this->logLn('executing cweb returned success code - but no file was found at destination!');
539
                return false;
540
            } else {
541 2
                $this->logLn('Success');
542 2
                return true;
543
            }
544
        } else {
545
            $this->logLn(
546
                'Exec failed (return code: ' . $returnCode . ')'
547
            );
548
            return false;
549
        }
550
    }
551
552
    /**
553
     *  Helper for composing an error message when no converters are working.
554
     *
555
     *  @param  array  $versions  The array which we get from calling ::getCwebpVersions()
556
     *  @return string  An informative and to the point error message.
557
     */
558
    private function composeMeaningfullErrorMessageNoVersionsWorking($versions)
559
    {
560
561
        $uniqueFailCodes = array_unique(array_values($versions['failed']));
562
        $justOne = (count($versions['failed']) == 1);
563
564
        if (count($uniqueFailCodes) == 1) {
565
            if ($uniqueFailCodes[0] == 127) {
566
                return 'No cwebp binaries located. Check the conversion log for details.';
567
            }
568
        }
569
        // If there are more failures than 127, the 127 failures are unintesting.
570
        // It is to be expected that some of the common system paths does not contain a cwebp.
571
        $uniqueFailCodesBesides127 = array_diff($uniqueFailCodes, [127]);
572
573
        if (count($uniqueFailCodesBesides127) == 1) {
574
            if ($uniqueFailCodesBesides127[0] == 126) {
575
                return 'No cwebp binaries could be executed (permission denied for ' . $this->who() . ').';
576
            }
577
        }
578
579
        $errorMsg = '';
580
        if ($justOne) {
581
            $errorMsg .= 'The cwebp file found cannot be can be executed ';
582
        } else {
583
            $errorMsg .= 'None of the cwebp files can be executed ';
584
        }
585
        if (count($uniqueFailCodesBesides127) == 1) {
586
            $errorMsg .= '(failure code: ' . $uniqueFailCodesBesides127[0] . ')';
587
        } else {
588
            $errorMsg .= '(failure codes: ' . implode(', ', $uniqueFailCodesBesides127) . ')';
589
        }
590
        return $errorMsg;
591
    }
592
593 2
    protected function doActualConvert()
594
    {
595 2
        $versions = $this->getCwebpVersions();
596 2
        $binaryVersions = $versions['detected'];
597 2
        if (count($binaryVersions) == 0) {
598
            // No working cwebp binaries found.
599
600
            throw new SystemRequirementsNotMetException(
601
                $this->composeMeaningfullErrorMessageNoVersionsWorking($versions)
602
            );
603
        }
604
605
        // Sort binaries so those with highest numbers comes first
606 2
        arsort($binaryVersions);
607 2
        $this->logLn(
608 2
            'Here is what we found, ordered by version number.'
609
        );
610 2
        foreach ($binaryVersions as $binary => $version) {
611 2
            $this->logLn('- ' . $binary . ': (version: ' . $version .')');
612
        }
613
614
        // Execute!
615 2
        $this->logLn(
616 2
            'Trying the first of these. If that should fail (it should not), the next will be tried and so on.'
617
        );
618 2
        $useNice = (($this->options['use-nice']) && self::hasNiceSupport());
619 2
        $success = false;
620 2
        foreach ($binaryVersions as $binary => $version) {
621 2
            if ($this->tryCwebpBinary($binary, $version, $useNice)) {
622 2
                $success = true;
623 2
                break;
624
            }
625
        }
626
627
        // cwebp sets file permissions to 664 but instead ..
628
        // .. $destination's parent folder's permissions should be used (except executable bits)
629
        // (or perhaps the current umask instead? https://www.php.net/umask)
630
631 2
        if ($success) {
632 2
            $destinationParent = dirname($this->destination);
633 2
            $fileStatistics = stat($destinationParent);
634 2
            if ($fileStatistics !== false) {
635
                // Apply same permissions as parent folder but strip off the executable bits
636 2
                $permissions = $fileStatistics['mode'] & 0000666;
637 2
                chmod($this->destination, $permissions);
638
            }
639
        } else {
640
            throw new SystemRequirementsNotMetException('Failed converting. Check the conversion log for details.');
641
        }
642 2
    }
643
}
644