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

Cwebp::doActualConvert()   C

Complexity

Conditions 12
Paths 43

Size

Total Lines 79
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 12.2614

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 12
eloc 47
c 3
b 0
f 0
nc 43
nop 0
dl 0
loc 79
ccs 36
cts 41
cp 0.878
crap 12.2614
rs 6.9666

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace WebPConvert\Convert\Converters;
4
5
use WebPConvert\Convert\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