Completed
Push — master ( d5fca1...7488d9 )
by Bjørn
03:06
created

Cwebp::createCommandLineOptions()   F

Complexity

Conditions 19
Paths 13824

Size

Total Lines 128
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 19.5547

Importance

Changes 11
Bugs 1 Features 2
Metric Value
cc 19
eloc 55
c 11
b 1
f 2
nc 13824
nop 1
dl 0
loc 128
ccs 46
cts 52
cp 0.8846
crap 19.5547
rs 0.3499

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