Passed
Push — master ( 92b8a3...77b4d2 )
by Bjørn
02:48
created

Cwebp::detectVersion()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6.2499

Importance

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