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

Cwebp::discoverCwebpsInCommonSystemPaths()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
nc 3
nop 0
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
namespace WebPConvert\Convert\Converters;
4
5
use WebPConvert\Convert\Converters\AbstractConverter;
6
use WebPConvert\Convert\Converters\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