Passed
Push — master ( 1ada49...6f88dc )
by Bjørn
02:29
created

Cwebp::getOptionDefinitionsExtra()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 0
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace WebPConvert\Convert\Converters;
4
5
use WebPConvert\Convert\BaseConverters\AbstractExecConverter;
6
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
7
use WebPConvert\Convert\Exceptions\ConversionFailedException;
8
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException;
9
10
class Cwebp extends AbstractExecConverter
11
{
12
    protected function getOptionDefinitionsExtra()
13
    {
14
        return [
15
            ['use-nice', 'boolean', false],
16
            ['try-common-system-paths', 'boolean', true],
17
            ['try-supplied-binary-for-os', 'boolean', true],
18
            ['size-in-percentage', 'number', null],
19
            ['command-line-options', 'string', ''],
20
            ['rel-path-to-precompiled-binaries', 'string', './Binaries'],
21
        ];
22
    }
23
24
    // System paths to look for cwebp binary
25
    private static $cwebpDefaultPaths = [
26
        '/usr/bin/cwebp',
27
        '/usr/local/bin/cwebp',
28
        '/usr/gnu/bin/cwebp',
29
        '/usr/syno/bin/cwebp'
30
    ];
31
32
    // OS-specific binaries included in this library, along with hashes
33
    // If other binaries are going to be added, notice that the first argument is what PHP_OS returns.
34
    // (possible values, see here: https://stackoverflow.com/questions/738823/possible-values-for-php-os)
35
    private static $suppliedBinariesInfo = [
36
        'WINNT' => [ 'cwebp.exe', '49e9cb98db30bfa27936933e6fd94d407e0386802cb192800d9fd824f6476873'],
37
        'Darwin' => [ 'cwebp-mac12', 'a06a3ee436e375c89dbc1b0b2e8bd7729a55139ae072ed3f7bd2e07de0ebb379'],
38
        'SunOS' => [ 'cwebp-sol', '1febaffbb18e52dc2c524cda9eefd00c6db95bc388732868999c0f48deb73b4f'],
39
        'FreeBSD' => [ 'cwebp-fbsd', 'e5cbea11c97fadffe221fdf57c093c19af2737e4bbd2cb3cd5e908de64286573'],
40
        'Linux' => [ 'cwebp-linux', '916623e5e9183237c851374d969aebdb96e0edc0692ab7937b95ea67dc3b2568']
41
    ];
42
43
44
    private function executeBinary($binary, $commandOptions, $useNice)
45
    {
46
        $command = ($useNice ? 'nice ' : '') . $binary . ' ' . $commandOptions;
47
48
        //$logger->logLn('command options:' . $commandOptions);
49
        //$logger->logLn('Trying to execute binary:' . $binary);
50
        exec($command, $output, $returnCode);
51
        //$logger->logLn(self::msgForExitCode($returnCode));
52
        return intval($returnCode);
53
    }
54
55
    /**
56
     * Build command line options
57
     *
58
     * @return string
59
     */
60
    private function createCommandLineOptions()
61
    {
62
        $options = $this->options;
63
64
        $commandOptionsArray = [];
65
66
        // Metadata (all, exif, icc, xmp or none (default))
67
        // Comma-separated list of existing metadata to copy from input to output
68
        $commandOptionsArray[] = '-metadata ' . $options['metadata'];
69
70
        // Size
71
        if (!is_null($options['size-in-percentage'])) {
72
            $sizeSource =  filesize($this->source);
73
            if ($sizeSource !== false) {
74
                $targetSize = floor($sizeSource * $options['size-in-percentage'] / 100);
75
            }
76
        }
77
        if (isset($targetSize)) {
78
            $commandOptionsArray[] = '-size ' . $targetSize;
79
        } else {
80
            // Image quality
81
            $commandOptionsArray[] = '-q ' . $this->getCalculatedQuality();
82
        }
83
84
85
        // Losless PNG conversion
86
        $commandOptionsArray[] = ($options['lossless'] ? '-lossless' : '');
87
88
        // Built-in method option
89
        $commandOptionsArray[] = '-m ' . strval($options['method']);
90
91
        // Built-in low memory option
92
        if ($options['low-memory']) {
93
            $commandOptionsArray[] = '-low_memory';
94
        }
95
96
        // command-line-options
97
        if ($options['command-line-options']) {
98
            $arr = explode(' -', ' ' . $options['command-line-options']);
99
            foreach ($arr as $cmdOption) {
100
                $pos = strpos($cmdOption, ' ');
101
                $cName = '';
102
                $cValue = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $cValue is dead and can be removed.
Loading history...
103
                if (!$pos) {
104
                    $cName = $cmdOption;
105
                    if ($cName == '') {
106
                        continue;
107
                    }
108
                    $commandOptionsArray[] = '-' . $cName;
109
                } else {
110
                    $cName = substr($cmdOption, 0, $pos);
111
                    $cValues = substr($cmdOption, $pos + 1);
112
                    $cValuesArr = explode(' ', $cValues);
113
                    foreach ($cValuesArr as &$cArg) {
114
                        $cArg = escapeshellarg($cArg);
115
                    }
116
                    $cValues = implode(' ', $cValuesArr);
117
                    $commandOptionsArray[] = '-' . $cName . ' ' . $cValues;
118
                }
119
            }
120
        }
121
122
        // Source file
123
        $commandOptionsArray[] = escapeshellarg($this->source);
124
125
        // Output
126
        $commandOptionsArray[] = '-o ' . escapeshellarg($this->destination);
127
128
        // Redirect stderr to same place as stdout
129
        // https://www.brianstorti.com/understanding-shell-script-idiom-redirect/
130
        $commandOptionsArray[] = '2>&1';
131
132
        $commandOptions = implode(' ', $commandOptionsArray);
133
        $this->logLn('command line options:' . $commandOptions);
134
135
        return $commandOptions;
136
    }
137
138
    protected function checkOperationality()
139
    {
140
        $options = $this->options;
141
        if (!$options['try-supplied-binary-for-os'] && !$options['try-common-system-paths']) {
142
            throw new ConverterNotOperationalException(
143
                'Configured to neither look for cweb binaries in common system locations, ' .
144
                'nor to use one of the supplied precompiled binaries. But these are the only ways ' .
145
                'this converter can convert images. No conversion can be made!'
146
            );
147
        }
148
    }
149
150
151
    protected function doActualConvert()
152
    {
153
        $errorMsg = '';
154
        $options = $this->options;
155
        $useNice = (($options['use-nice']) && self::hasNiceSupport());
156
157
        $commandOptions = $this->createCommandLineOptions();
158
159
160
        // Init with common system paths
161
        $cwebpPathsToTest = self::$cwebpDefaultPaths;
162
163
        // Remove paths that doesn't exist
164
        /*
165
        $cwebpPathsToTest = array_filter($cwebpPathsToTest, function ($binary) {
166
            //return file_exists($binary);
167
            return @is_readable($binary);
168
        });
169
        */
170
171
        // Try all common paths that exists
172
        $success = false;
173
        $failures = [];
174
        $failureCodes = [];
175
176
177
        $returnCode = 0;
178
        $majorFailCode = 0;
179
        if ($options['try-common-system-paths']) {
180
            foreach ($cwebpPathsToTest as $index => $binary) {
181
                $returnCode = $this->executeBinary($binary, $commandOptions, $useNice);
182
                if ($returnCode == 0) {
183
                    $this->logLn('Successfully executed binary: ' . $binary);
184
                    $success = true;
185
                    break;
186
                } else {
187
                    $failures[] = [$binary, $returnCode];
188
                    if (!in_array($returnCode, $failureCodes)) {
189
                        $failureCodes[] = $returnCode;
190
                    }
191
                }
192
            }
193
194
            if (!$success) {
195
                if (count($failureCodes) == 1) {
196
                    $majorFailCode = $failureCodes[0];
197
                    switch ($majorFailCode) {
198
                        case 126:
199
                            $errorMsg = 'Permission denied. The user that the command was run with (' .
200
                                shell_exec('whoami') . ') does not have permission to execute any of the ' .
201
                                'cweb binaries found in common system locations. ';
202
                            break;
203
                        case 127:
204
                            $errorMsg .= 'Found no cwebp binaries in any common system locations. ';
205
                            break;
206
                        default:
207
                            $errorMsg .= 'Tried executing cwebp binaries in common system locations. ' .
208
                                'All failed (exit code: ' . $majorFailCode . '). ';
209
                    }
210
                } else {
211
                    /**
212
                     * $failureCodesBesides127 is used to check first position ($failureCodesBesides127[0])
213
                     * however position can vary as index can be 1 or something else. array_values() would
214
                     * always start from 0.
215
                     */
216
                    $failureCodesBesides127 = array_values(array_diff($failureCodes, [127]));
217
218
                    if (count($failureCodesBesides127) == 1) {
219
                        $majorFailCode = $failureCodesBesides127[0];
220
                        switch ($returnCode) {
221
                            case 126:
222
                                $errorMsg = 'Permission denied. The user that the command was run with (' .
223
                                shell_exec('whoami') . ') does not have permission to execute any of the cweb ' .
224
                                'binaries found in common system locations. ';
225
                                break;
226
                            default:
227
                                $errorMsg .= 'Tried executing cwebp binaries in common system locations. ' .
228
                                'All failed (exit code: ' . $majorFailCode . '). ';
229
                        }
230
                    } else {
231
                        $errorMsg .= 'None of the cwebp binaries in the common system locations could be executed ' .
232
                        '(mixed results - got the following exit codes: ' . implode(',', $failureCodes) . '). ';
233
                    }
234
                }
235
            }
236
        }
237
238
        if (!$success && $options['try-supplied-binary-for-os']) {
239
          // Try supplied binary (if available for OS, and hash is correct)
240
            if (isset(self::$suppliedBinariesInfo[PHP_OS])) {
241
                $info = self::$suppliedBinariesInfo[PHP_OS];
242
243
                $file = $info[0];
244
                $hash = $info[1];
245
246
                $binaryFile = __DIR__ . '/' . $options['rel-path-to-precompiled-binaries'] . '/' . $file;
247
248
                // The file should exist, but may have been removed manually.
249
                if (file_exists($binaryFile)) {
250
                    // File exists, now generate its hash
251
252
                    // hash_file() is normally available, but it is not always
253
                    // - https://stackoverflow.com/questions/17382712/php-5-3-20-undefined-function-hash
254
                    // If available, validate that hash is correct.
255
                    $proceedAfterHashCheck = true;
256
                    if (function_exists('hash_file')) {
257
                        $binaryHash = hash_file('sha256', $binaryFile);
258
259
                        if ($binaryHash != $hash) {
260
                            $errorMsg .= 'Binary checksum of supplied binary is invalid! ' .
261
                                'Did you transfer with FTP, but not in binary mode? ' .
262
                                'File:' . $binaryFile . '. ' .
263
                                'Expected checksum: ' . $hash . '. ' .
264
                                'Actual checksum:' . $binaryHash . '.';
265
                            $proceedAfterHashCheck = false;
266
                        }
267
                    }
268
                    if ($proceedAfterHashCheck) {
269
                        $returnCode = $this->executeBinary($binaryFile, $commandOptions, $useNice);
270
                        if ($returnCode == 0) {
271
                            $success = true;
272
                        } else {
273
                            $errorMsg .= 'Tried executing supplied binary for ' . PHP_OS . ', ' .
274
                                ($options['try-common-system-paths'] ? 'but that failed too' : 'but failed');
275
                            if ($options['try-common-system-paths'] && ($majorFailCode > 0)) {
276
                                $errorMsg .= ' (same error)';
277
                            } else {
278
                                if ($returnCode > 128) {
279
                                    $errorMsg .= '. The binary did not work (exit code: ' . $returnCode . '). ' .
280
                                        'Check out https://github.com/rosell-dk/webp-convert/issues/92';
281
                                } else {
282
                                    switch ($returnCode) {
283
                                        case 0:
284
                                            $success = true;
285
                                            ;
286
                                            break;
287
                                        case 126:
288
                                            $errorMsg .= ': Permission denied. The user that the command was run' .
289
                                                ' with (' . shell_exec('whoami') . ') does not have permission to ' .
290
                                                'execute that binary.';
291
                                            break;
292
                                        case 127:
293
                                            $errorMsg .= '. The binary was not found! ' .
294
                                                'It ought to be here: ' . $binaryFile;
295
                                            break;
296
                                        default:
297
                                            $errorMsg .= ' (exit code:' .  $returnCode . ').';
298
                                    }
299
                                }
300
                            }
301
                        }
302
                    }
303
                } else {
304
                    $errorMsg .= 'Supplied binary not found! It ought to be here:' . $binaryFile;
305
                }
306
            } else {
307
                $errorMsg .= 'No supplied binaries found for OS:' . PHP_OS;
308
            }
309
        }
310
311
        // cwebp sets file permissions to 664 but instead ..
312
        // .. $destination's parent folder's permissions should be used (except executable bits)
313
        // (or perhaps the current umask instead? https://www.php.net/umask)
314
315
        if ($success) {
316
            $destinationParent = dirname($this->destination);
317
            $fileStatistics = stat($destinationParent);
318
            if ($fileStatistics !== false) {
319
                // Apply same permissions as parent folder but strip off the executable bits
320
                $permissions = $fileStatistics['mode'] & 0000666;
321
                chmod($this->destination, $permissions);
322
            }
323
        }
324
325
        if (!$success) {
326
            throw new SystemRequirementsNotMetException($errorMsg);
327
        }
328
    }
329
}
330