Passed
Push — master ( 7488d9...c79d57 )
by Bjørn
02:54
created

Cwebp::discoverCwebpBinaries()   B

Complexity

Conditions 8
Paths 26

Size

Total Lines 61
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 8.4334

Importance

Changes 0
Metric Value
cc 8
eloc 39
c 0
b 0
f 0
nc 26
nop 0
dl 0
loc 61
ccs 30
cts 37
cp 0.8108
crap 8.4334
rs 8.0515

How to fix   Long Method   

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