Passed
Push — master ( cdaf3e...206204 )
by Bjørn
05:37
created

Gd::makeTrueColor()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 18
nc 4
nop 1
dl 0
loc 31
rs 9.0444
c 0
b 0
f 0
1
<?php 
2
3
4
5
6
7
?><?php
8
9
namespace WebPConvert;
10
11
//use WebPConvert\Convert\Converters\ConverterHelper;
12
use WebPConvert\Convert\Converters\Stack;
13
use WebPConvert\Serve\ServeExistingOrHandOver;
14
15
class WebPConvert
16
{
17
18
    /**
19
     * Convert jpeg or png into webp
20
     *
21
     * @param  string  $source  Absolute path to image to be converted (no backslashes). Image must be jpeg or png
22
     * @param  string  $destination  Absolute path (no backslashes)
23
     * @param  object  $options  Array of named options, such as 'quality' and 'metadata'
24
     * @throws \WebPConvert\Exceptions\WebPConvertException
25
     * @return void
26
    */
27
    public static function convert($source, $destination, $options = [], $logger = null)
28
    {
29
        //return ConverterHelper::runConverterStack($source, $destination, $options, $logger);
30
        //return Convert::runConverterStack($source, $destination, $options, $logger);
31
        Stack::convert($source, $destination, $options, $logger);
0 ignored issues
show
Bug introduced by
It seems like $options can also be of type object; however, parameter $options of WebPConvert\Convert\Base...actConverter::convert() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

31
        Stack::convert($source, $destination, /** @scrutinizer ignore-type */ $options, $logger);
Loading history...
32
    }
33
34
    public static function convertAndServe($source, $destination, $options = [])
35
    {
36
        return ServeExistingOrHandOver::serveConverted($source, $destination, $options);
37
    }
38
}
39
40
?><?php
41
42
namespace WebPConvert\Convert\BaseConverters;
43
44
use WebPConvert\Convert\Exceptions\ConversionFailedException;
45
use WebPConvert\Convert\BaseConverters\AbstractConverter;
46
47
abstract class AbstractCloudConverter extends AbstractConverter
48
{
49
    /**
50
     * Parse a shordhandsize string as the ones returned by ini_get()
51
     *
52
     * Parse a shorthandsize string having the syntax allowed in php.ini and returned by ini_get().
53
     * Ie "1K" => 1024.
54
     * Strings without units are also accepted.
55
     * The shorthandbytes syntax is described here: https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
56
     *
57
     * @param  string  $size  A size string of the type returned by ini_get()
58
     * @return float|false  The parsed size (beware: it is float, do not check high numbers for equality),
59
     *                      or false if parse error
60
     */
61
    protected static function parseShortHandSize($shortHandSize)
62
    {
63
64
        $result = preg_match("#^\\s*(\\d+(?:\\.\\d+)?)([bkmgtpezy]?)\\s*$#i", $shortHandSize, $matches);
65
        if ($result !== 1) {
66
            return false;
67
        }
68
69
        // Truncate, because that is what php does.
70
        $digitsValue = floor($matches[1]);
71
72
        if ((count($matches) >= 3) && ($matches[2] != '')) {
73
            $unit = $matches[2];
74
75
            // Find the position of the unit in the ordered string which is the power
76
            // of magnitude to multiply a kilobyte by.
77
            $position = stripos('bkmgtpezy', $unit);
78
79
            return floatval($digitsValue * pow(1024, $position));
80
        } else {
81
            return $digitsValue;
82
        }
83
    }
84
85
    /*
86
    * Get the size of an php.ini option.
87
    *
88
    * Calls ini_get() and parses the size to a number.
89
    * If the configuration option is null, does not exist, or cannot be parsed as a shorthandsize, false is returned
90
    *
91
    * @param  string  $varname  The configuration option name.
92
    * @return float|false  The parsed size or false if the configuration option does not exist
93
    */
94
    protected static function getIniBytes($iniVarName)
95
    {
96
        $iniVarValue = ini_get($iniVarName);
97
        if (($iniVarValue == '') || $iniVarValue === false) {
98
            return false;
99
        }
100
        return self::parseShortHandSize($iniVarValue);
101
    }
102
103
    /**
104
     *  Test that filesize is below "upload_max_filesize" and "post_max_size" values in php.ini
105
     *
106
     * @throws  ConversionFailedException  if filesize is larger than "upload_max_filesize" or "post_max_size"
107
     * @return  void
108
     */
109
    protected function testFilesizeRequirements()
110
    {
111
        $fileSize = @filesize($this->source);
112
        if ($fileSize !== false) {
113
            $uploadMaxSize = self::getIniBytes('upload_max_filesize');
114
            if ($uploadMaxSize === false) {
115
                // Not sure if we should throw an exception here, or not...
116
            } elseif ($uploadMaxSize < $fileSize) {
117
                throw new ConversionFailedException(
118
                    'File is larger than your max upload (set in your php.ini). File size:' .
119
                        round($fileSize/1024) . ' kb. ' .
120
                        'upload_max_filesize in php.ini: ' . ini_get('upload_max_filesize') .
121
                        ' (parsed as ' . round($uploadMaxSize/1024) . ' kb)'
122
                );
123
            }
124
125
            $postMaxSize = self::getIniBytes(ini_get('post_max_size'));
126
            if ($postMaxSize === false) {
127
                // Not sure if we should throw an exception here, or not...
128
            } elseif ($postMaxSize < $fileSize) {
129
                throw new ConversionFailedException(
130
                    'File is larger than your post_max_size limit (set in your php.ini). File size:' .
131
                        round($fileSize/1024) . ' kb. ' .
132
                        'post_max_size in php.ini: ' . ini_get('post_max_size') .
133
                        ' (parsed as ' . round($postMaxSize/1024) . ' kb)'
134
                );
135
            }
136
137
            // Should we worry about memory limit as well?
138
            // ini_get('memory_limit')
139
        }
140
    }
141
142
    /**
143
     * Check if specific file is convertable with current converter / converter settings.
144
     *
145
     */
146
    protected function checkConvertability()
147
    {
148
        $this->testFilesizeRequirements();
149
    }
150
}
151
152
?><?php
153
154
namespace WebPConvert\Convert\BaseConverters;
155
156
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
157
use WebPConvert\Convert\BaseConverters\AbstractConverter;
158
159
abstract class AbstractCloudCurlConverter extends AbstractCloudConverter
160
{
161
162
    /**
163
     * Check basis operationality for converters relying on curl
164
     *
165
     * @throws  SystemRequirementsNotMetException
166
     * @return  void
167
     */
168
    protected function checkOperationality()
169
    {
170
        if (!extension_loaded('curl')) {
171
            throw new SystemRequirementsNotMetException('Required cURL extension is not available.');
172
        }
173
174
        if (!function_exists('curl_init')) {
175
            throw new SystemRequirementsNotMetException('Required url_init() function is not available.');
176
        }
177
178
        if (!function_exists('curl_file_create')) {
179
            throw new SystemRequirementsNotMetException(
180
                'Required curl_file_create() function is not available (requires PHP > 5.5).'
181
            );
182
        }
183
    }
184
185
    /**
186
     *  Init curl.
187
     *
188
     * @throws  SystemRequirementsNotMetException  if curl could not be initialized
189
     * @return  resource  curl handle
190
     */
191
    public static function initCurl()
192
    {
193
        // Get curl handle
194
        $ch = curl_init();
195
        if ($ch === false) {
196
            throw new SystemRequirementsNotMetException('Could not initialise cURL.');
197
        }
198
        return $ch;
199
    }
200
}
201
202
?><?php
203
204
namespace WebPConvert\Convert\BaseConverters;
205
206
use WebPConvert\Convert\Exceptions\ConversionFailedException;
207
use WebPConvert\Convert\Exceptions\ConversionFailed\ConversionDeclinedException;
208
use WebPConvert\Convert\Exceptions\ConversionFailed\UnhandledException;
209
use WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblems\CreateDestinationFileException;
210
use WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblems\CreateDestinationFolderException;
211
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\ConverterNotFoundException;
212
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\InvalidImageTypeException;
213
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\TargetNotFoundException;
214
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
215
use WebPConvert\Convert\QualityProcessor;
216
use WebPConvert\Loggers\BaseLogger;
217
218
use ImageMimeTypeGuesser\ImageMimeTypeGuesser;
219
220
abstract class AbstractConverter
221
{
222
    /**
223
     * The actual conversion must be done by a concrete class.
224
     *
225
     */
226
    abstract protected function doConvert();
227
228
    // The following must be defined in all actual converters.
229
    // Unfortunately properties cannot be declared abstract. TODO: We need to change to using method instead.
230
    public static $extraOptions;
231
232
    public $source;
233
    public $destination;
234
    public $options;
235
    public $logger;
236
    public $beginTime;
237
    public $sourceMimeType;
238
    public static $allowedMimeTypes = ['image/jpeg', 'image/png'];
239
    public static $defaultOptions = [
240
        'quality' => 'auto',
241
        'max-quality' => 85,
242
        'default-quality' => 75,
243
        'metadata' => 'none',
244
        'method' => 6,
245
        'low-memory' => false,
246
        'lossless' => false,
247
        'skip-pngs' => false,
248
    ];
249
    private $qualityProcessor;
250
251
    /**
252
     * Check basis operationality
253
     *
254
     * Converters may override this method for the purpose of performing basic operationaly checks. It is for
255
     * running general operation checks for a conversion method.
256
     * If some requirement is not met, it should throw a ConverterNotOperationalException (or subtype)
257
     *
258
     * The method is called internally right before calling doConvert() method.
259
     * - It SHOULD take options into account when relevant. For example, a missing api key for a
260
     *   cloud converter should be detected here
261
     * - It should NOT take the actual filename into consideration, as the purpose is *general*
262
     *   For that pupose, converters should override checkConvertability
263
     *   Also note that doConvert method is allowed to throw ConverterNotOperationalException too.
264
     *
265
     * @return  void
266
     */
267
    protected function checkOperationality()
268
    {
269
    }
270
271
    /**
272
     * Converters may override this for the purpose of performing checks on the concrete file.
273
     *
274
     * This can for example be used for rejecting big uploads in cloud converters or rejecting unsupported
275
     * image types.
276
     *
277
     * @return  void
278
     */
279
    protected function checkConvertability()
280
    {
281
    }
282
283
    public function __construct($source, $destination, $options = [], $logger = null)
284
    {
285
        if (!isset($logger)) {
286
            $logger = new \WebPConvert\Loggers\VoidLogger();
287
        }
288
        $this->source = $source;
289
        $this->destination = $destination;
290
        $this->options = $options;
291
        $this->logger = $logger;
292
    }
293
294
    /**
295
     *  Default display name is simply the class name (short).
296
     *  Converters can override this.
297
     */
298
    protected static function getConverterDisplayName()
299
    {
300
        // https://stackoverflow.com/questions/19901850/how-do-i-get-an-objects-unqualified-short-class-name/25308464
301
        return substr(strrchr('\\' . static::class, '\\'), 1);
302
    }
303
304
    public static function createInstance($source, $destination, $options = [], $logger = null)
305
    {
306
        return new static($source, $destination, $options, $logger);
307
    }
308
309
    /**
310
     *
311
     *
312
     */
313
    public function errorHandler($errno, $errstr, $errfile, $errline)
314
    {
315
316
        /*
317
        We do NOT do the following (even though it is generally recommended):
318
319
        if (!(error_reporting() & $errno)) {
320
            // This error code is not included in error_reporting, so let it fall
321
            // through to the standard PHP error handler
322
            return false;
323
        }
324
325
        - Because we want to log all warnings and errors (also the ones that was suppressed with @)
326
        https://secure.php.net/manual/en/language.operators.errorcontrol.php
327
        */
328
329
        $errorTypes = [
330
            E_WARNING =>             "Warning",
331
            E_NOTICE =>              "Notice",
332
            E_USER_ERROR =>          "User Error",
333
            E_USER_WARNING =>        "User Warning",
334
            E_USER_NOTICE =>         "User Notice",
335
            E_STRICT =>              "Strict Notice",
336
            E_DEPRECATED =>          "Deprecated",
337
            E_USER_DEPRECATED =>     "User Deprecated",
338
339
            /*
340
            The following can never be catched by a custom error handler:
341
            E_PARSE =>               "Parse Error",
342
            E_ERROR =>               "Error",
343
            E_CORE_ERROR =>          "Core Error",
344
            E_CORE_WARNING =>        "Core Warning",
345
            E_COMPILE_ERROR =>       "Compile Error",
346
            E_COMPILE_WARNING =>     "Compile Warning",
347
            */
348
        ];
349
350
        if (isset($errorTypes[$errno])) {
351
            $errType = $errorTypes[$errno];
352
        } else {
353
            $errType = "Unknown error ($errno)";
354
        }
355
356
        $msg = $errType . ': ' . $errstr . ' in ' . $errfile . ', line ' . $errline . ', PHP ' . PHP_VERSION .
357
            ' (' . PHP_OS . ')';
358
        //$this->logLn($msg);
359
360
        /*
361
        if(function_exists('debug_backtrace')){
362
            //print "backtrace:\n";
363
            $backtrace = debug_backtrace();
364
            array_shift($backtrace);
365
            foreach($backtrace as $i=>$l){
366
                $msg = '';
367
                $msg .= "[$i] in function <b>{$l['class']}{$l['type']}{$l['function']}</b>";
368
                if($l['file']) $msg .= " in <b>{$l['file']}</b>";
369
                if($l['line']) $msg .= " on line <b>{$l['line']}</b>";
370
                $this->logLn($msg);
371
372
            }
373
        }
374
        */
375
        $this->logLn($msg);
376
377
        if ($errno == E_USER_ERROR) {
378
            // trigger error.
379
            // unfortunately, we can only catch user errors
380
            throw new ConversionFailedException('Uncaught error in converter', $msg);
381
        }
382
383
        // We do not return false, because we want to keep this little secret.
384
        //
385
        //return false;   // let PHP handle the error from here
386
    }
387
388
    /**
389
     * Convert an image to webp.
390
     *
391
     * @param   string  $source              path to source file
392
     * @param   string  $destination         path to destination
393
     * @param   array   $options (optional)  options for conversion
394
     * @param   \WebPConvert\Loggers\BaseLogger $logger (optional)
395
     * @return  void
396
     */
397
    public static function convert($source, $destination, $options = [], $logger = null)
398
    {
399
        $instance = self::createInstance($source, $destination, $options, $logger);
400
401
        //$instance->logLn($instance->getConverterDisplayName() . ' converter ignited');
402
        //$instance->logLn(self::getConverterDisplayName() . ' converter ignited');
403
        $instance->prepareConvert();
404
        try {
405
            $instance->checkOperationality();
406
            $instance->checkConvertability();
407
            $instance->doConvert();
408
        } catch (ConversionFailedException $e) {
409
            throw $e;
410
        } catch (\Exception $e) {
411
            throw new UnhandledException('Conversion failed due to uncaught exception', 0, $e);
412
        } catch (\Error $e) {
413
            // https://stackoverflow.com/questions/7116995/is-it-possible-in-php-to-prevent-fatal-error-call-to-undefined-function
414
            throw new UnhandledException('Conversion failed due to uncaught error', 0, $e);
415
        }
416
        $instance->finalizeConvert();
417
418
        //echo $instance->id;
419
    }
420
421
    public function logLn($msg, $style = '')
422
    {
423
        $this->logger->logLn($msg, $style);
424
    }
425
426
    public function logLnLn($msg)
427
    {
428
        $this->logger->logLnLn($msg);
429
    }
430
431
    public function ln()
432
    {
433
        $this->logger->ln();
434
    }
435
436
    public function log($msg)
437
    {
438
        $this->logger->log($msg);
439
    }
440
441
    /**
442
     * Get mime type for image (best guess).
443
     *
444
     * It falls back to using file extension. If that fails too, false is returned
445
     *
446
     * PS: Is it a security risk to fall back on file extension?
447
     * - By setting file extension to "jpg", one can lure our library into trying to convert a file, which isn't a jpg.
448
     * hmm, seems very unlikely, though not unthinkable that one of the converters could be exploited
449
     *
450
     * @return  string|false
451
     */
452
    public function getMimeTypeOfSource()
453
    {
454
        if (!isset($this->sourceMimeType)) {
455
            $this->sourceMimeType = ImageMimeTypeGuesser::lenientGuess($this->source);
456
        }
457
        return $this->sourceMimeType;
458
    }
459
460
    private function prepareConvert()
461
    {
462
        $this->beginTime = microtime(true);
463
464
        //set_error_handler(array($this, "warningHandler"), E_WARNING);
465
        set_error_handler(array($this, "errorHandler"));
466
467
        if (!isset($this->options['_skip_basic_validations'])) {
468
            // Run basic validations (if source exists and if file extension is valid)
469
            $this->runBasicValidations();
470
471
            // Prepare destination folder (may throw exception)
472
            $this->createWritableDestinationFolder();
473
        }
474
475
        // Prepare options
476
        $this->prepareOptions();
477
    }
478
479
    /**
480
     *  Note: As the "basic" validations are only run one time in a stack,
481
     *  this method is not overridable
482
     */
483
    private function runBasicValidations()
484
    {
485
        // Check if source exists
486
        if (!@file_exists($this->source)) {
487
            throw new TargetNotFoundException('File or directory not found: ' . $this->source);
488
        }
489
490
        // Check if the provided file's mime type is valid
491
        $fileMimeType = $this->getMimeTypeOfSource();
492
        if ($fileMimeType === false) {
493
            throw new InvalidImageTypeException('Image type could not be detected');
494
        } elseif (!in_array($fileMimeType, self::$allowedMimeTypes)) {
495
            throw new InvalidImageTypeException('Unsupported mime type: ' . $fileMimeType);
496
        }
497
    }
498
499
    /**
500
     * Prepare options.
501
     */
502
    private function prepareOptions()
503
    {
504
        $defaultOptions = self::$defaultOptions;
505
506
        // -  Merge defaults of the converters extra options into the standard default options.
507
        $defaultOptions = array_merge($defaultOptions, array_column(static::$extraOptions, 'default', 'name'));
508
509
        // -  Merge $defaultOptions into provided options
510
        $this->options = array_merge($defaultOptions, $this->options);
511
512
        if ($this->getMimeTypeOfSource() == 'png') {
513
            // skip png's ?
514
            if ($this->options['skip-pngs']) {
515
                throw new ConversionDeclinedException(
516
                    'PNG file skipped (configured to do so)'
517
                );
518
            }
519
520
            // Force lossless option to true for PNG images
521
            $this->options['lossless'] = true;
522
        }
523
524
        // TODO: Here we could test if quality is 0-100 or auto.
525
        //       and if not, throw something extending InvalidArgumentException (which is a LogicException)
526
    }
527
528
    // Creates folder in provided path & sets correct permissions
529
    // also deletes the file at filePath (if it already exists)
530
    public function createWritableDestinationFolder()
531
    {
532
        $filePath = $this->destination;
533
534
        $folder = dirname($filePath);
535
        if (!@file_exists($folder)) {
536
            // TODO: what if this is outside open basedir?
537
            // see http://php.net/manual/en/ini.core.php#ini.open-basedir
538
539
            // First, we have to figure out which permissions to set.
540
            // We want same permissions as parent folder
541
            // But which parent? - the parent to the first missing folder
542
543
            $parentFolders = explode('/', $folder);
544
            $poppedFolders = [];
545
546
            while (!(@file_exists(implode('/', $parentFolders))) && count($parentFolders) > 0) {
547
                array_unshift($poppedFolders, array_pop($parentFolders));
548
            }
549
550
            // Retrieving permissions of closest existing folder
551
            $closestExistingFolder = implode('/', $parentFolders);
552
            $permissions = @fileperms($closestExistingFolder) & 000777;
553
            $stat = @stat($closestExistingFolder);
554
555
            // Trying to create the given folder (recursively)
556
            if (!@mkdir($folder, $permissions, true)) {
557
                throw new CreateDestinationFolderException('Failed creating folder: ' . $folder);
558
            }
559
560
            // `mkdir` doesn't always respect permissions, so we have to `chmod` each created subfolder
561
            foreach ($poppedFolders as $subfolder) {
562
                $closestExistingFolder .= '/' . $subfolder;
563
                // Setting directory permissions
564
                if ($permissions !== false) {
565
                    @chmod($folder, $permissions);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

565
                    /** @scrutinizer ignore-unhandled */ @chmod($folder, $permissions);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
566
                }
567
                if ($stat !== false) {
568
                    if (isset($stat['uid'])) {
569
                        @chown($folder, $stat['uid']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chown(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

569
                        /** @scrutinizer ignore-unhandled */ @chown($folder, $stat['uid']);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
570
                    }
571
                    if (isset($stat['gid'])) {
572
                        @chgrp($folder, $stat['gid']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chgrp(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

572
                        /** @scrutinizer ignore-unhandled */ @chgrp($folder, $stat['gid']);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
573
                    }
574
                }
575
            }
576
        }
577
578
        if (@file_exists($filePath)) {
579
            // A file already exists in this folder...
580
            // We delete it, to make way for a new webp
581
            if (!@unlink($filePath)) {
582
                throw new CreateDestinationFileException(
583
                    'Existing file cannot be removed: ' . basename($filePath)
584
                );
585
            }
586
        }
587
588
        // Try to create a dummy file here, with that name, just to see if it is possible (we delete it again)
589
        @file_put_contents($filePath, '');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for file_put_contents(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

589
        /** @scrutinizer ignore-unhandled */ @file_put_contents($filePath, '');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
590
        if (@file_put_contents($filePath, '') === false) {
591
            throw new CreateDestinationFileException(
592
                'Cannot create file: ' . basename($filePath) . ' in dir:' . $folder
593
            );
594
        }
595
        @unlink($filePath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

595
        /** @scrutinizer ignore-unhandled */ @unlink($filePath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
596
597
        return true;
598
    }
599
600
    private function getQualityProcessor()
601
    {
602
        if (!isset($this->qualityProcessor)) {
603
            $this->qualityProcessor = new QualityProcessor($this);
604
        }
605
        return $this->qualityProcessor;
606
    }
607
608
    /**
609
     *  Returns quality, as a number.
610
     *  If quality was set to auto, you get the detected quality / fallback quality, otherwise
611
     *  you get whatever it was set to.
612
     *  Use this, if you simply want quality as a number, and have no handling of "auto" quality
613
     */
614
    public function getCalculatedQuality()
615
    {
616
        return $this->getQualityProcessor()->getCalculatedQuality();
617
    }
618
619
    public function isQualityDetectionRequiredButFailing()
620
    {
621
        return $this->getQualityProcessor()->isQualityDetectionRequiredButFailing();
622
    }
623
624
    public function finalizeConvert()
625
    {
626
        restore_error_handler();
627
628
        $source = $this->source;
629
        $destination = $this->destination;
630
631
        if (!@file_exists($destination)) {
632
            throw new ConversionFailedException('Destination file is not there: ' . $destination);
633
        } elseif (@filesize($destination) === 0) {
634
            @unlink($destination);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

634
            /** @scrutinizer ignore-unhandled */ @unlink($destination);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
635
            throw new ConversionFailedException('Destination file was completely empty');
636
        } else {
637
            if (!isset($this->options['_suppress_success_message'])) {
638
                $this->ln();
639
                $msg = 'Successfully converted image in ' .
640
                    round((microtime(true) - $this->beginTime) * 1000) . ' ms';
641
642
                $sourceSize = @filesize($source);
643
                if ($sourceSize !== false) {
644
                    $msg .= ', reducing file size with ' .
645
                        round((filesize($source) - filesize($destination))/filesize($source) * 100) . '% ';
646
647
                    if ($sourceSize < 10000) {
648
                        $msg .= '(went from ' . round(filesize($source)) . ' bytes to ';
649
                        $msg .= round(filesize($destination)) . ' bytes)';
650
                    } else {
651
                        $msg .= '(went from ' . round(filesize($source)/1024) . ' kb to ';
652
                        $msg .= round(filesize($destination)/1024) . ' kb)';
653
                    }
654
                }
655
                $this->logLn($msg);
656
            }
657
        }
658
    }
659
}
660
661
?><?php
662
663
namespace WebPConvert\Convert\BaseConverters;
664
665
use WebPConvert\Convert\BaseConverters\AbstractConverter;
666
667
use WebPConvert\Convert\Exceptions\ConversionFailedException;
668
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
669
670
abstract class AbstractExecConverter extends AbstractConverter
671
{
672
    protected static function escapeFilename($string)
673
    {
674
        // Escaping whitespace
675
        $string = preg_replace('/\s/', '\\ ', $string);
676
677
        // filter_var() is should normally be available, but it is not always
678
        // - https://stackoverflow.com/questions/11735538/call-to-undefined-function-filter-var
679
        if (function_exists('filter_var')) {
680
            // Sanitize quotes
681
            $string = filter_var($string, FILTER_SANITIZE_MAGIC_QUOTES);
682
683
            // Stripping control characters
684
            // see https://stackoverflow.com/questions/12769462/filter-flag-strip-low-vs-filter-flag-strip-high
685
            $string = filter_var($string, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
686
        }
687
688
        return $string;
689
    }
690
691
    protected static function hasNiceSupport()
692
    {
693
        exec("nice 2>&1", $niceOutput);
694
695
        if (is_array($niceOutput) && isset($niceOutput[0])) {
696
            if (preg_match('/usage/', $niceOutput[0]) || (preg_match('/^\d+$/', $niceOutput[0]))) {
697
                /*
698
                 * Nice is available - default niceness (+10)
699
                 * https://www.lifewire.com/uses-of-commands-nice-renice-2201087
700
                 * https://www.computerhope.com/unix/unice.htm
701
                 */
702
703
                return true;
704
            }
705
            return false;
706
        }
707
    }
708
709
    /**
710
     * Check basis operationality of exec converters.
711
     *
712
     * @throws  SystemRequirementsNotMetException
713
     * @return  void
714
     */
715
    protected function checkOperationality()
716
    {
717
        if (!function_exists('exec')) {
718
            throw new SystemRequirementsNotMetException('exec() is not enabled.');
719
        }
720
    }
721
}
722
723
?><?php
724
725
namespace WebPConvert\Convert\Converters;
726
727
use WebPConvert\Convert\BaseConverters\AbstractExecConverter;
728
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
729
use WebPConvert\Convert\Exceptions\ConversionFailedException;
730
731
class Cwebp extends AbstractExecConverter
732
{
733
    public static $extraOptions = [
734
        [
735
            'name' => 'use-nice',
736
            'type' => 'boolean',
737
            'sensitive' => false,
738
            'default' => false,
739
            'required' => false
740
        ],
741
        // low-memory is defined for all, in ConverterHelper
742
        [
743
            'name' => 'try-common-system-paths',
744
            'type' => 'boolean',
745
            'sensitive' => false,
746
            'default' => true,
747
            'required' => false
748
        ],
749
        [
750
            'name' => 'try-supplied-binary-for-os',
751
            'type' => 'boolean',
752
            'sensitive' => false,
753
            'default' => true,
754
            'required' => false
755
        ],
756
        [
757
            'name' => 'size-in-percentage',
758
            'type' => 'number',
759
            'sensitive' => false,
760
            'default' => null,
761
            'required' => false
762
        ],
763
        [
764
            'name' => 'command-line-options',
765
            'type' => 'string',
766
            'sensitive' => false,
767
            'default' => '',
768
            'required' => false
769
        ],
770
        [
771
            'name' => 'rel-path-to-precompiled-binaries',
772
            'type' => 'string',
773
            'sensitive' => false,
774
            'default' => './Binaries',
775
            'required' => false
776
        ],
777
    ];
778
779
    // System paths to look for cwebp binary
780
    private static $cwebpDefaultPaths = [
781
        '/usr/bin/cwebp',
782
        '/usr/local/bin/cwebp',
783
        '/usr/gnu/bin/cwebp',
784
        '/usr/syno/bin/cwebp'
785
    ];
786
787
    // OS-specific binaries included in this library, along with hashes
788
    // If other binaries are going to be added, notice that the first argument is what PHP_OS returns.
789
    // (possible values, see here: https://stackoverflow.com/questions/738823/possible-values-for-php-os)
790
    private static $suppliedBinariesInfo = [
791
        'WINNT' => [ 'cwebp.exe', '49e9cb98db30bfa27936933e6fd94d407e0386802cb192800d9fd824f6476873'],
792
        'Darwin' => [ 'cwebp-mac12', 'a06a3ee436e375c89dbc1b0b2e8bd7729a55139ae072ed3f7bd2e07de0ebb379'],
793
        'SunOS' => [ 'cwebp-sol', '1febaffbb18e52dc2c524cda9eefd00c6db95bc388732868999c0f48deb73b4f'],
794
        'FreeBSD' => [ 'cwebp-fbsd', 'e5cbea11c97fadffe221fdf57c093c19af2737e4bbd2cb3cd5e908de64286573'],
795
        'Linux' => [ 'cwebp-linux', '916623e5e9183237c851374d969aebdb96e0edc0692ab7937b95ea67dc3b2568']
796
    ];
797
798
    /**
799
     * Check operationality of Cwebp converter.
800
     *
801
     */
802
     /*
803
    protected function checkOperationality()
804
    {
805
        parent::checkOperationality();
806
    }*/
807
808
    private static function executeBinary($binary, $commandOptions, $useNice, $logger)
0 ignored issues
show
Unused Code introduced by
The parameter $logger is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

808
    private static function executeBinary($binary, $commandOptions, $useNice, /** @scrutinizer ignore-unused */ $logger)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
809
    {
810
        $command = ($useNice ? 'nice ' : '') . $binary . ' ' . $commandOptions;
811
812
        //$logger->logLn('command options:' . $commandOptions);
813
        //$logger->logLn('Trying to execute binary:' . $binary);
814
        exec($command, $output, $returnCode);
815
        //$logger->logLn(self::msgForExitCode($returnCode));
816
        return intval($returnCode);
817
    }
818
819
    // Although this method is public, do not call directly.
820
    // You should rather call the static convert() function, defined in AbstractConverter, which
821
    // takes care of preparing stuff before calling doConvert, and validating after.
822
    protected function doConvert()
823
    {
824
        $errorMsg = '';
825
        $options = $this->options;
826
827
        /*
828
         * Prepare cwebp options
829
         */
830
831
        $commandOptionsArray = [];
832
833
        // Metadata (all, exif, icc, xmp or none (default))
834
        // Comma-separated list of existing metadata to copy from input to output
835
        $commandOptionsArray[] = '-metadata ' . $options['metadata'];
836
837
        // Size
838
        if (!is_null($options['size-in-percentage'])) {
839
            $sizeSource =  @filesize($this->source);
840
            if ($sizeSource !== false) {
841
                $targetSize = floor($sizeSource * $options['size-in-percentage'] / 100);
842
            }
843
        }
844
        if (isset($targetSize)) {
845
            $commandOptionsArray[] = '-size ' . $targetSize;
846
        } else {
847
            // Image quality
848
            $commandOptionsArray[] = '-q ' . $this->getCalculatedQuality();
849
        }
850
851
852
        // Losless PNG conversion
853
        $commandOptionsArray[] = ($options['lossless'] ? '-lossless' : '');
854
855
        // Built-in method option
856
        $commandOptionsArray[] = '-m ' . strval($options['method']);
857
858
        // Built-in low memory option
859
        if ($options['low-memory']) {
860
            $commandOptionsArray[] = '-low_memory';
861
        }
862
863
        // command-line-options
864
        if ($options['command-line-options']) {
865
            $arr = explode(' -', ' ' . $options['command-line-options']);
866
            foreach ($arr as $cmdOption) {
867
                $pos = strpos($cmdOption, ' ');
868
                $cName = '';
869
                $cValue = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $cValue is dead and can be removed.
Loading history...
870
                if (!$pos) {
871
                    $cName = $cmdOption;
872
                    if ($cName == '') {
873
                        continue;
874
                    }
875
                    $commandOptionsArray[] = '-' . $cName;
876
                } else {
877
                    $cName = substr($cmdOption, 0, $pos);
878
                    $cValues = substr($cmdOption, $pos + 1);
879
                    $cValuesArr = explode(' ', $cValues);
880
                    foreach ($cValuesArr as &$cArg) {
881
                        $cArg = escapeshellarg($cArg);
882
                    }
883
                    $cValues = implode(' ', $cValuesArr);
884
                    $commandOptionsArray[] = '-' . $cName . ' ' . $cValues;
885
                }
886
            }
887
        }
888
889
        // Source file
890
        //$commandOptionsArray[] = self::escapeFilename($this->source);
891
        $commandOptionsArray[] = escapeshellarg($this->source);
892
893
        // Output
894
        //$commandOptionsArray[] = '-o ' . self::escapeFilename($this->destination);
895
        $commandOptionsArray[] = '-o ' . escapeshellarg($this->destination);
896
897
898
        // Redirect stderr to same place as stdout
899
        // https://www.brianstorti.com/understanding-shell-script-idiom-redirect/
900
        $commandOptionsArray[] = '2>&1';
901
902
903
        $useNice = (($options['use-nice']) && self::hasNiceSupport()) ? true : false;
904
905
        $commandOptions = implode(' ', $commandOptionsArray);
906
907
        $this->logLn('cwebp options:' . $commandOptions);
908
909
        // Init with common system paths
910
        $cwebpPathsToTest = self::$cwebpDefaultPaths;
911
912
        // Remove paths that doesn't exist
913
        /*
914
        $cwebpPathsToTest = array_filter($cwebpPathsToTest, function ($binary) {
915
            //return file_exists($binary);
916
            return @is_readable($binary);
917
        });
918
        */
919
920
        // Try all common paths that exists
921
        $success = false;
922
        $failures = [];
923
        $failureCodes = [];
924
925
        if (!$options['try-supplied-binary-for-os'] && !$options['try-common-system-paths']) {
926
            $errorMsg .= 'Configured to neither look for cweb binaries in common system locations, ' .
927
                'nor to use one of the supplied precompiled binaries. But these are the only ways ' .
928
                'this converter can convert images. No conversion can be made!';
929
        }
930
931
        if ($options['try-common-system-paths']) {
932
            foreach ($cwebpPathsToTest as $index => $binary) {
933
                $returnCode = self::executeBinary($binary, $commandOptions, $useNice, $this);
934
                if ($returnCode == 0) {
935
                    $this->logLn('Successfully executed binary: ' . $binary);
936
                    $success = true;
937
                    break;
938
                } else {
939
                    $failures[] = [$binary, $returnCode];
940
                    if (!in_array($returnCode, $failureCodes)) {
941
                        $failureCodes[] = $returnCode;
942
                    }
943
                }
944
            }
945
            $majorFailCode = 0;
946
            if (!$success) {
947
                if (count($failureCodes) == 1) {
948
                    $majorFailCode = $failureCodes[0];
949
                    switch ($majorFailCode) {
950
                        case 126:
951
                            $errorMsg = 'Permission denied. The user that the command was run with (' .
952
                                shell_exec('whoami') . ') does not have permission to execute any of the ' .
953
                                'cweb binaries found in common system locations. ';
954
                            break;
955
                        case 127:
956
                            $errorMsg .= 'Found no cwebp binaries in any common system locations. ';
957
                            break;
958
                        default:
959
                            $errorMsg .= 'Tried executing cwebp binaries in common system locations. ' .
960
                                'All failed (exit code: ' . $majorFailCode . '). ';
961
                    }
962
                } else {
963
                    /**
964
                     * $failureCodesBesides127 is used to check first position ($failureCodesBesides127[0])
965
                     * however position can vary as index can be 1 or something else. array_values() would
966
                     * always start from 0.
967
                     */
968
                    $failureCodesBesides127 = array_values(array_diff($failureCodes, [127]));
969
970
                    if (count($failureCodesBesides127) == 1) {
971
                        $majorFailCode = $failureCodesBesides127[0];
972
                        switch ($returnCode) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $returnCode seems to be defined by a foreach iteration on line 932. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
973
                            case 126:
974
                                $errorMsg = 'Permission denied. The user that the command was run with (' .
975
                                shell_exec('whoami') . ') does not have permission to execute any of the cweb ' .
976
                                'binaries found in common system locations. ';
977
                                break;
978
                            default:
979
                                $errorMsg .= 'Tried executing cwebp binaries in common system locations. ' .
980
                                'All failed (exit code: ' . $majorFailCode . '). ';
981
                        }
982
                    } else {
983
                        $errorMsg .= 'None of the cwebp binaries in the common system locations could be executed ' .
984
                        '(mixed results - got the following exit codes: ' . implode(',', $failureCodes) . '). ';
985
                    }
986
                }
987
            }
988
        }
989
990
        if (!$success && $options['try-supplied-binary-for-os']) {
991
          // Try supplied binary (if available for OS, and hash is correct)
992
            if (isset(self::$suppliedBinariesInfo[PHP_OS])) {
993
                $info = self::$suppliedBinariesInfo[PHP_OS];
994
995
                $file = $info[0];
996
                $hash = $info[1];
997
998
                $binaryFile = __DIR__ . '/' . $options['rel-path-to-precompiled-binaries'] . '/' . $file;
999
1000
                // The file should exist, but may have been removed manually.
1001
                if (@file_exists($binaryFile)) {
1002
                    // File exists, now generate its hash
1003
1004
                    // hash_file() is normally available, but it is not always
1005
                    // - https://stackoverflow.com/questions/17382712/php-5-3-20-undefined-function-hash
1006
                    // If available, validate that hash is correct.
1007
                    $proceedAfterHashCheck = true;
1008
                    if (function_exists('hash_file')) {
1009
                        $binaryHash = hash_file('sha256', $binaryFile);
1010
1011
                        if ($binaryHash != $hash) {
1012
                            $errorMsg .= 'Binary checksum of supplied binary is invalid! ' .
1013
                                'Did you transfer with FTP, but not in binary mode? ' .
1014
                                'File:' . $binaryFile . '. ' .
1015
                                'Expected checksum: ' . $hash . '. ' .
1016
                                'Actual checksum:' . $binaryHash . '.';
1017
                            $proceedAfterHashCheck = false;
1018
                        }
1019
                    }
1020
                    if ($proceedAfterHashCheck) {
1021
                        $returnCode = self::executeBinary($binaryFile, $commandOptions, $useNice, $this);
1022
                        if ($returnCode == 0) {
1023
                            $success = true;
1024
                        } else {
1025
                            $errorMsg .= 'Tried executing supplied binary for ' . PHP_OS . ', ' .
1026
                                ($options['try-common-system-paths'] ? 'but that failed too' : 'but failed');
1027
                            if ($options['try-common-system-paths'] && ($majorFailCode > 0)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $majorFailCode does not seem to be defined for all execution paths leading up to this point.
Loading history...
1028
                                $errorMsg .= ' (same error)';
1029
                            } else {
1030
                                if ($returnCode > 128) {
1031
                                    $errorMsg .= '. The binary did not work (exit code: ' . $returnCode . '). ' .
1032
                                        'Check out https://github.com/rosell-dk/webp-convert/issues/92';
1033
                                } else {
1034
                                    switch ($returnCode) {
1035
                                        case 0:
1036
                                            $success = true;
1037
                                            ;
1038
                                            break;
1039
                                        case 126:
1040
                                            $errorMsg .= ': Permission denied. The user that the command was run' .
1041
                                                ' with (' . shell_exec('whoami') . ') does not have permission to ' .
1042
                                                'execute that binary.';
1043
                                            break;
1044
                                        case 127:
1045
                                            $errorMsg .= '. The binary was not found! ' .
1046
                                                'It ought to be here: ' . $binaryFile;
1047
                                            break;
1048
                                        default:
1049
                                            $errorMsg .= ' (exit code:' .  $returnCode . ').';
1050
                                    }
1051
                                }
1052
                            }
1053
                        }
1054
                    }
1055
                } else {
1056
                    $errorMsg .= 'Supplied binary not found! It ought to be here:' . $binaryFile;
1057
                }
1058
            } else {
1059
                $errorMsg .= 'No supplied binaries found for OS:' . PHP_OS;
1060
            }
1061
        }
1062
1063
        // cwebp sets file permissions to 664 but instead ..
1064
        // .. $destination's parent folder's permissions should be used (except executable bits)
1065
        if ($success) {
1066
            $destinationParent = dirname($this->destination);
1067
            $fileStatistics = @stat($destinationParent);
1068
            if ($fileStatistics !== false) {
1069
                // Apply same permissions as parent folder but strip off the executable bits
1070
                $permissions = $fileStatistics['mode'] & 0000666;
1071
                @chmod($this->destination, $permissions);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

1071
                /** @scrutinizer ignore-unhandled */ @chmod($this->destination, $permissions);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1072
            }
1073
        }
1074
1075
        if (!$success) {
1076
            throw new SystemRequirementsNotMetException($errorMsg);
1077
        }
1078
    }
1079
}
1080
1081
?><?php
1082
1083
namespace WebPConvert\Convert\Converters;
1084
1085
use WebPConvert\Convert\BaseConverters\AbstractCloudCurlConverter;
1086
use WebPConvert\Convert\Exceptions\ConversionFailedException;
1087
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException;
1088
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
1089
1090
class Ewww extends AbstractCloudCurlConverter
1091
{
1092
    public static $extraOptions = [
1093
        [
1094
            'name' => 'key',
1095
            'type' => 'string',
1096
            'sensitive' => true,
1097
            'default' => '',
1098
            'required' => true
1099
        ],
1100
    ];
1101
1102
    /**
1103
     * Check operationality of Ewww converter.
1104
     *
1105
     * @throws SystemRequirementsNotMetException  if system requirements are not met (curl)
1106
     * @throws ConverterNotOperationalException   if key is missing or invalid, or quota has exceeded
1107
     */
1108
    protected function checkOperationality()
1109
    {
1110
        // First check for curl requirements
1111
        parent::checkOperationality();
1112
1113
        $options = $this->options;
1114
1115
        if ($options['key'] == '') {
1116
            throw new ConverterNotOperationalException('Missing API key.');
1117
        }
1118
        if (strlen($options['key']) < 20) {
1119
            throw new ConverterNotOperationalException(
1120
                'Key is invalid. Keys are supposed to be 32 characters long - your key is much shorter'
1121
            );
1122
        }
1123
1124
        $keyStatus = self::getKeyStatus($options['key']);
1125
        switch ($keyStatus) {
1126
            case 'great':
1127
                break;
1128
            case 'exceeded':
1129
                throw new ConverterNotOperationalException('quota has exceeded');
1130
                break;
1131
            case 'invalid':
1132
                throw new ConverterNotOperationalException('key is invalid');
1133
                break;
1134
        }
1135
    }
1136
1137
    // Although this method is public, do not call directly.
1138
    // You should rather call the static convert() function, defined in AbstractConverter, which
1139
    // takes care of preparing stuff before calling doConvert, and validating after.
1140
    protected function doConvert()
1141
    {
1142
1143
        $options = $this->options;
1144
1145
        $ch = self::initCurl();
1146
1147
        $curlOptions = [
1148
            'api_key' => $options['key'],
1149
            'webp' => '1',
1150
            'file' => curl_file_create($this->source),
1151
            'domain' => $_SERVER['HTTP_HOST'],
1152
            'quality' => $this->getCalculatedQuality(),
1153
            'metadata' => ($options['metadata'] == 'none' ? '0' : '1')
1154
        ];
1155
1156
        curl_setopt_array(
1157
            $ch,
1158
            [
1159
            CURLOPT_URL => "https://optimize.exactlywww.com/v2/",
1160
            CURLOPT_HTTPHEADER => [
1161
                'User-Agent: WebPConvert',
1162
                'Accept: image/*'
1163
            ],
1164
            CURLOPT_POSTFIELDS => $curlOptions,
1165
            CURLOPT_BINARYTRANSFER => true,
1166
            CURLOPT_RETURNTRANSFER => true,
1167
            CURLOPT_HEADER => false,
1168
            CURLOPT_SSL_VERIFYPEER => false
1169
            ]
1170
        );
1171
1172
        $response = curl_exec($ch);
1173
1174
        if (curl_errno($ch)) {
1175
            throw new ConversionFailedException(curl_error($ch));
1176
        }
1177
1178
        // The API does not always return images.
1179
        // For example, it may return a message such as '{"error":"invalid","t":"exceeded"}
1180
        // Messages has a http content type of ie 'text/html; charset=UTF-8
1181
        // Images has application/octet-stream.
1182
        // So verify that we got an image back.
1183
        if (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) != 'application/octet-stream') {
1184
            //echo curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
1185
            curl_close($ch);
1186
1187
            /* May return this: {"error":"invalid","t":"exceeded"} */
1188
            $responseObj = json_decode($response);
1189
            if (isset($responseObj->error)) {
1190
                //echo 'error:' . $responseObj->error . '<br>';
1191
                //echo $response;
1192
                //self::blacklistKey($key);
1193
                //throw new SystemRequirementsNotMetException('The key is invalid. Blacklisted it!');
1194
                throw new ConverterNotOperationalException('The key is invalid');
1195
            }
1196
1197
            throw new ConversionFailedException(
1198
                'ewww api did not return an image. It could be that the key is invalid. Response: '
1199
                . $response
1200
            );
1201
        }
1202
1203
        // Not sure this can happen. So just in case
1204
        if ($response == '') {
1205
            throw new ConversionFailedException('ewww api did not return anything');
1206
        }
1207
1208
        $success = file_put_contents($this->destination, $response);
1209
1210
        if (!$success) {
1211
            throw new ConversionFailedException('Error saving file');
1212
        }
1213
    }
1214
1215
    /**
1216
     *  Keep subscription alive by optimizing a jpeg
1217
     *  (ewww closes accounts after 6 months of inactivity - and webp conversions seems not to be counted? )
1218
     */
1219
    public static function keepSubscriptionAlive($source, $key)
1220
    {
1221
        try {
1222
            $ch = curl_init();
1223
        } catch (\Exception $e) {
1224
            return 'curl is not installed';
1225
        }
1226
        if ($ch === false) {
1227
            return 'curl could not be initialized';
1228
        }
1229
        curl_setopt_array(
1230
            $ch,
1231
            [
1232
            CURLOPT_URL => "https://optimize.exactlywww.com/v2/",
1233
            CURLOPT_HTTPHEADER => [
1234
                'User-Agent: WebPConvert',
1235
                'Accept: image/*'
1236
            ],
1237
            CURLOPT_POSTFIELDS => [
1238
                'api_key' => $key,
1239
                'webp' => '0',
1240
                'file' => curl_file_create($source),
1241
                'domain' => $_SERVER['HTTP_HOST'],
1242
                'quality' => 60,
1243
                'metadata' => 0
1244
            ],
1245
            CURLOPT_BINARYTRANSFER => true,
1246
            CURLOPT_RETURNTRANSFER => true,
1247
            CURLOPT_HEADER => false,
1248
            CURLOPT_SSL_VERIFYPEER => false
1249
            ]
1250
        );
1251
1252
        $response = curl_exec($ch);
1253
        if (curl_errno($ch)) {
1254
            return 'curl error' . curl_error($ch);
1255
        }
1256
        if (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) != 'application/octet-stream') {
1257
            curl_close($ch);
1258
1259
            /* May return this: {"error":"invalid","t":"exceeded"} */
1260
            $responseObj = json_decode($response);
1261
            if (isset($responseObj->error)) {
1262
                return 'The key is invalid';
1263
            }
1264
1265
            return 'ewww api did not return an image. It could be that the key is invalid. Response: ' . $response;
1266
        }
1267
1268
        // Not sure this can happen. So just in case
1269
        if ($response == '') {
1270
            return 'ewww api did not return anything';
1271
        }
1272
1273
        return true;
1274
    }
1275
1276
    /*
1277
        public static function blacklistKey($key)
1278
        {
1279
        }
1280
1281
        public static function isKeyBlacklisted($key)
1282
        {
1283
        }*/
1284
1285
    /**
1286
     *  Return "great", "exceeded" or "invalid"
1287
     */
1288
    public static function getKeyStatus($key)
1289
    {
1290
        $ch = self::initCurl();
1291
1292
        curl_setopt($ch, CURLOPT_URL, "https://optimize.exactlywww.com/verify/");
1293
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1294
        curl_setopt(
1295
            $ch,
1296
            CURLOPT_POSTFIELDS,
1297
            [
1298
            'api_key' => $key
1299
            ]
1300
        );
1301
1302
        // The 403 forbidden is avoided with this line.
1303
        curl_setopt(
1304
            $ch,
1305
            CURLOPT_USERAGENT,
1306
            'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.0.3705; .NET CLR 1.1.4322)'
1307
        );
1308
1309
        $response = curl_exec($ch);
1310
        // echo $response;
1311
        if (curl_errno($ch)) {
1312
            throw new \Exception(curl_error($ch));
1313
        }
1314
        curl_close($ch);
1315
1316
        // Possible responses:
1317
        // “great” = verification successful
1318
        // “exceeded” = indicates a valid key with no remaining image credits.
1319
        // an empty response indicates that the key is not valid
1320
1321
        if ($response == '') {
1322
            return 'invalid';
1323
        }
1324
        $responseObj = json_decode($response);
1325
        if (isset($responseObj->error)) {
1326
            if ($responseObj->error == 'invalid') {
1327
                return 'invalid';
1328
            } else {
1329
                throw new \Exception('Ewww returned unexpected error: ' . $response);
1330
            }
1331
        }
1332
        if (!isset($responseObj->status)) {
1333
            throw new \Exception('Ewww returned unexpected response to verify request: ' . $response);
1334
        }
1335
        switch ($responseObj->status) {
1336
            case 'great':
1337
            case 'exceeded':
1338
                return $responseObj->status;
1339
        }
1340
        throw new \Exception('Ewww returned unexpected status to verify request: "' . $responseObj->status . '"');
1341
    }
1342
1343
    public static function isWorkingKey($key)
1344
    {
1345
        return (self::getKeyStatus($key) == 'great');
1346
    }
1347
1348
    public static function isValidKey($key)
1349
    {
1350
        return (self::getKeyStatus($key) != 'invalid');
1351
    }
1352
1353
    public static function getQuota($key)
1354
    {
1355
        $ch = self::initCurl();
1356
1357
        curl_setopt($ch, CURLOPT_URL, "https://optimize.exactlywww.com/quota/");
1358
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1359
        curl_setopt(
1360
            $ch,
1361
            CURLOPT_POSTFIELDS,
1362
            [
1363
            'api_key' => $key
1364
            ]
1365
        );
1366
        curl_setopt(
1367
            $ch,
1368
            CURLOPT_USERAGENT,
1369
            'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.0.3705; .NET CLR 1.1.4322)'
1370
        );
1371
1372
        $response = curl_exec($ch);
1373
        return $response; // ie -830 23. Seems to return empty for invalid keys
1374
        // or empty
1375
        //echo $response;
1376
    }
1377
}
1378
1379
?><?php
1380
1381
namespace WebPConvert\Convert\Converters;
1382
1383
use WebPConvert\Convert\BaseConverters\AbstractConverter;
1384
use WebPConvert\Convert\Exceptions\ConversionFailed\ConversionDeclinedException;
1385
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
1386
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInputException;
1387
use WebPConvert\Convert\Exceptions\ConversionFailedException;
1388
1389
class Gd extends AbstractConverter
1390
{
1391
    private $errorMessageWhileCreating = '';
1392
1393
    public static $extraOptions = [];
1394
1395
    /**
1396
     * Check (general) operationality of Gd converter.
1397
     *
1398
     * @throws SystemRequirementsNotMetException  if system requirements are not met
1399
     */
1400
    protected function checkOperationality()
1401
    {
1402
        if (!extension_loaded('gd')) {
1403
            throw new SystemRequirementsNotMetException('Required Gd extension is not available.');
1404
        }
1405
1406
        if (!function_exists('imagewebp')) {
1407
            throw new SystemRequirementsNotMetException(
1408
                'Gd has been compiled without webp support.'
1409
            );
1410
        }
1411
    }
1412
1413
    /**
1414
     * Check if specific file is convertable with current converter / converter settings.
1415
     *
1416
     * @throws SystemRequirementsNotMetException  if Gd has been compiled without support for image type
1417
     */
1418
    protected function checkConvertability()
1419
    {
1420
        $mimeType = $this->getMimeTypeOfSource();
1421
        switch ($mimeType) {
1422
            case 'image/png':
1423
                if (!function_exists('imagecreatefrompng')) {
1424
                    throw new SystemRequirementsNotMetException(
1425
                        'Gd has been compiled without PNG support and can therefore not convert this PNG image.'
1426
                    );
1427
                }
1428
                break;
1429
1430
            case 'image/jpeg':
1431
                if (!function_exists('imagecreatefromjpeg')) {
1432
                    throw new SystemRequirementsNotMetException(
1433
                        'Gd has been compiled without Jpeg support and can therefore not convert this jpeg image.'
1434
                    );
1435
                }
1436
        }
1437
    }
1438
1439
    /**
1440
     * Find out if all functions exists.
1441
     *
1442
     * @return boolean
1443
     */
1444
    private static function functionsExist($functionNamesArr)
1445
    {
1446
        foreach ($functionNamesArr as $functionName) {
1447
            if (!function_exists($functionName)) {
1448
                return false;
1449
            }
1450
        }
1451
        return true;
1452
    }
1453
1454
    /**
1455
     * Try to convert image pallette to true color.
1456
     *
1457
     * Try to convert image pallette to true color. If imageistruecolor() exists, that is used (available from
1458
     * PHP >= 5.5.0). Otherwise using workaround found on the net.
1459
     *
1460
     * @param  resource  &$image
1461
     * @return boolean  TRUE if the convertion was complete, or if the source image already is a true color image,
1462
     *          otherwise FALSE is returned.
1463
     */
1464
    public static function makeTrueColor(&$image)
1465
    {
1466
        if (function_exists('imagepalettetotruecolor')) {
1467
            return imagepalettetotruecolor($image);
1468
        } else {
1469
            // Got the workaround here: https://secure.php.net/manual/en/function.imagepalettetotruecolor.php
1470
            if ((function_exists('imageistruecolor') && !imageistruecolor($image))
1471
                || !function_exists('imageistruecolor')
1472
            ) {
1473
                if (self::functionsExist(['imagecreatetruecolor', 'imagealphablending', 'imagecolorallocatealpha',
1474
                        'imagefilledrectangle', 'imagecopy', 'imagedestroy', 'imagesx', 'imagesy'])) {
1475
                    $dst = imagecreatetruecolor(imagesx($image), imagesy($image));
1476
1477
                    //prevent blending with default black
1478
                    imagealphablending($dst, false);
0 ignored issues
show
Bug introduced by
It seems like $dst can also be of type false; however, parameter $image of imagealphablending() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1478
                    imagealphablending(/** @scrutinizer ignore-type */ $dst, false);
Loading history...
1479
1480
                     //change the RGB values if you need, but leave alpha at 127
1481
                    $transparent = imagecolorallocatealpha($dst, 255, 255, 255, 127);
0 ignored issues
show
Bug introduced by
It seems like $dst can also be of type false; however, parameter $image of imagecolorallocatealpha() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1481
                    $transparent = imagecolorallocatealpha(/** @scrutinizer ignore-type */ $dst, 255, 255, 255, 127);
Loading history...
1482
1483
                     //simpler than flood fill
1484
                    imagefilledrectangle($dst, 0, 0, imagesx($image), imagesy($image), $transparent);
0 ignored issues
show
Bug introduced by
It seems like $dst can also be of type false; however, parameter $image of imagefilledrectangle() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1484
                    imagefilledrectangle(/** @scrutinizer ignore-type */ $dst, 0, 0, imagesx($image), imagesy($image), $transparent);
Loading history...
1485
                    imagealphablending($dst, true);     //restore default blending
1486
1487
                    imagecopy($dst, $image, 0, 0, 0, 0, imagesx($image), imagesy($image));
0 ignored issues
show
Bug introduced by
It seems like $dst can also be of type false; however, parameter $dst_im of imagecopy() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1487
                    imagecopy(/** @scrutinizer ignore-type */ $dst, $image, 0, 0, 0, 0, imagesx($image), imagesy($image));
Loading history...
1488
                    imagedestroy($image);
1489
1490
                    $image = $dst;
1491
                    return true;
1492
                }
1493
            } else {
1494
                return false;
1495
            }
1496
        }
1497
    }
1498
1499
    private function errorHandlerWhileCreatingWebP($errno, $errstr, $errfile, $errline)
1500
    {
1501
        $this->errorMessageWhileCreating = $errstr . ' in ' . $errfile . ', line ' . $errline .
1502
            ', PHP ' . PHP_VERSION . ' (' . PHP_OS . ')';
1503
    }
1504
1505
    // Although this method is public, do not call directly.
1506
    // You should rather call the static convert() function, defined in AbstractConverter, which
1507
    // takes care of preparing stuff before calling doConvert, and validating after.
1508
    protected function doConvert()
1509
    {
1510
1511
        $this->logLn('GD Version: ' . gd_info()["GD Version"]);
1512
1513
        // Btw: Check out processWebp here:
1514
        // https://github.com/Intervention/image/blob/master/src/Intervention/Image/Gd/Encoder.php
1515
1516
        $mimeType = $this->getMimeTypeOfSource();
1517
        switch ($mimeType) {
1518
            case 'image/png':
1519
                $image = imagecreatefrompng($this->source);
1520
                if (!$image) {
0 ignored issues
show
introduced by
$image is of type false|resource, thus it always evaluated to false.
Loading history...
1521
                    throw new ConversionFailedException(
1522
                        'Gd failed when trying to load/create image (imagecreatefrompng() failed)'
1523
                    );
1524
                }
1525
                break;
1526
1527
            case 'image/jpeg':
1528
                $image = imagecreatefromjpeg($this->source);
1529
                if (!$image) {
0 ignored issues
show
introduced by
$image is of type false|resource, thus it always evaluated to false.
Loading history...
1530
                    throw new ConversionFailedException(
1531
                        'Gd failed when trying to load/create image (imagecreatefromjpeg() failed)'
1532
                    );
1533
                }
1534
                break;
1535
1536
            default:
1537
                throw new InvalidInputException(
1538
                    'Unsupported mime type:' . $mimeType
0 ignored issues
show
Bug introduced by
Are you sure $mimeType of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1538
                    'Unsupported mime type:' . /** @scrutinizer ignore-type */ $mimeType
Loading history...
1539
                );
1540
        }
1541
1542
        // Checks if either imagecreatefromjpeg() or imagecreatefrompng() returned false
1543
1544
        $mustMakeTrueColor = false;
1545
        if (function_exists('imageistruecolor')) {
1546
            if (imageistruecolor($image)) {
1547
                $this->logLn('image is true color');
1548
            } else {
1549
                $this->logLn('image is not true color');
1550
                $mustMakeTrueColor = true;
1551
            }
1552
        } else {
1553
            $this->logLn('It can not be determined if image is true color');
1554
            $mustMakeTrueColor = true;
1555
        }
1556
1557
        if ($mustMakeTrueColor) {
1558
            $this->logLn('converting color palette to true color');
1559
            $success = $this->makeTrueColor($image);
1560
            if (!$success) {
1561
                $this->logLn(
1562
                    'Warning: FAILED converting color palette to true color. ' .
1563
                    'Continuing, but this does not look good.'
1564
                );
1565
            }
1566
        }
1567
1568
        if ($mimeType == 'png') {
1569
            if (function_exists('imagealphablending')) {
1570
                if (!imagealphablending($image, true)) {
1571
                    $this->logLn('Warning: imagealphablending() failed');
1572
                }
1573
            } else {
1574
                $this->logLn(
1575
                    'Warning: imagealphablending() is not available on your system.' .
1576
                    ' Converting PNGs with transparency might fail on some systems'
1577
                );
1578
            }
1579
1580
            if (function_exists('imagesavealpha')) {
1581
                if (!imagesavealpha($image, true)) {
1582
                    $this->logLn('Warning: imagesavealpha() failed');
1583
                }
1584
            } else {
1585
                $this->logLn(
1586
                    'Warning: imagesavealpha() is not available on your system. ' .
1587
                    'Converting PNGs with transparency might fail on some systems'
1588
                );
1589
            }
1590
        }
1591
1592
        // Danger zone!
1593
        //    Using output buffering to generate image.
1594
        //    In this zone, Do NOT do anything that might produce unwanted output
1595
        //    Do NOT call $this->logLn
1596
        // --------------------------------- (start of danger zone)
1597
1598
        $addedZeroPadding = false;
1599
        set_error_handler(array($this, "errorHandlerWhileCreatingWebP"));
1600
1601
        ob_start();
1602
        $success = imagewebp($image);
1603
        if (!$success) {
1604
            ob_end_clean();
1605
            restore_error_handler();
1606
            throw new ConversionFailedException(
1607
                'Failed creating image. Call to imagewebp() failed.',
1608
                $this->errorMessageWhileCreating
1609
            );
1610
        }
1611
1612
1613
        // The following hack solves an `imagewebp` bug
1614
        // See https://stackoverflow.com/questions/30078090/imagewebp-php-creates-corrupted-webp-files
1615
        if (ob_get_length() % 2 == 1) {
1616
            echo "\0";
1617
            $addedZeroPadding = true;
1618
        }
1619
        $output = ob_get_clean();
1620
        restore_error_handler();
1621
1622
        // --------------------------------- (end of danger zone).
1623
1624
        if ($this->errorMessageWhileCreating != '') {
1625
            $this->logLn('An error or warning was produced during conversion: ' . $this->errorMessageWhileCreating);
1626
        }
1627
1628
        if ($addedZeroPadding) {
1629
            $this->logLn(
1630
                'Fixing corrupt webp by adding a zero byte ' .
1631
                '(older versions of Gd had a bug, but this hack fixes it)'
1632
            );
1633
        }
1634
1635
        $success = file_put_contents($this->destination, $output);
1636
1637
        if (!$success) {
1638
            throw new ConversionFailedException(
1639
                'Gd failed when trying to save the image. Check file permissions!'
1640
            );
1641
        }
1642
1643
        /*
1644
        Previous code was much simpler, but on a system, the hack was not activated (a file with uneven number of bytes
1645
        was created). This is puzzeling. And the old code did not provide any insights.
1646
        Also, perhaps having two subsequent writes to the same file could perhaps cause a problem.
1647
        In the new code, there is only one write.
1648
        However, a bad thing about the new code is that the entire webp file is read into memory. This might cause
1649
        memory overflow with big files.
1650
        Perhaps we should check the filesize of the original and only use the new code when it is smaller than
1651
        memory limit set in PHP by a certain factor.
1652
        Or perhaps only use the new code on older versions of Gd
1653
        https://wordpress.org/support/topic/images-not-seen-on-chrome/#post-11390284
1654
1655
        Here is the old code:
1656
1657
        $success = imagewebp($image, $this->destination, $this->getCalculatedQuality());
1658
1659
        if (!$success) {
1660
            throw new ConversionFailedException(
1661
                'Gd failed when trying to save the image as webp (call to imagewebp() failed). ' .
1662
                'It probably failed writing file. Check file permissions!'
1663
            );
1664
        }
1665
1666
1667
        // This hack solves an `imagewebp` bug
1668
        // See https://stackoverflow.com/questions/30078090/imagewebp-php-creates-corrupted-webp-files
1669
        if (filesize($this->destination) % 2 == 1) {
1670
            file_put_contents($this->destination, "\0", FILE_APPEND);
1671
        }
1672
        */
1673
1674
        imagedestroy($image);
1675
    }
1676
}
1677
1678
?><?php
1679
1680
namespace WebPConvert\Convert\Converters;
1681
1682
use WebPConvert\Convert\BaseConverters\AbstractConverter;
1683
use WebPConvert\Convert\Exceptions\ConversionFailedException;
1684
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
1685
1686
//use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\TargetNotFoundException;
1687
1688
class Gmagick extends AbstractConverter
1689
{
1690
    public static $extraOptions = [];
1691
1692
    /**
1693
     * Check (general) operationality of Gmagick converter.
1694
     *
1695
     * Note:
1696
     * It may be that Gd has been compiled without jpeg support or png support.
1697
     * We do not check for this here, as the converter could still be used for the other.
1698
     *
1699
     * @throws SystemRequirementsNotMetException  if system requirements are not met
1700
     */
1701
    protected function checkOperationality()
1702
    {
1703
        if (!extension_loaded('Gmagick')) {
1704
            throw new SystemRequirementsNotMetException('Required Gmagick extension is not available.');
1705
        }
1706
1707
        if (!class_exists('Gmagick')) {
1708
            throw new SystemRequirementsNotMetException(
1709
                'Gmagick is installed, but not correctly. The class Gmagick is not available'
1710
            );
1711
        }
1712
1713
        $im = new \Gmagick($this->source);
1714
1715
        if (!in_array('WEBP', $im->queryformats())) {
1716
            throw new SystemRequirementsNotMetException('Gmagick was compiled without WebP support.');
1717
        }
1718
    }
1719
1720
    /**
1721
     * Check if specific file is convertable with current converter / converter settings.
1722
     *
1723
     * @throws SystemRequirementsNotMetException  if Gmagick does not support image type
1724
     */
1725
    protected function checkConvertability()
1726
    {
1727
        $im = new \Gmagick();
1728
        $mimeType = $this->getMimeTypeOfSource();
1729
        switch ($mimeType) {
1730
            case 'image/png':
1731
                if (!in_array('PNG', $im->queryFormats())) {
1732
                    throw new SystemRequirementsNotMetException(
1733
                        'Imagick has been compiled without PNG support and can therefore not convert this PNG image.'
1734
                    );
1735
                }
1736
                break;
1737
            case 'image/jpeg':
1738
                if (!in_array('JPEG', $im->queryFormats())) {
1739
                    throw new SystemRequirementsNotMetException(
1740
                        'Imagick has been compiled without Jpeg support and can therefore not convert this Jpeg image.'
1741
                    );
1742
                }
1743
                break;
1744
        }
1745
    }
1746
1747
    // Although this method is public, do not call directly.
1748
    // You should rather call the static convert() function, defined in AbstractConverter, which
1749
    // takes care of preparing stuff before calling doConvert, and validating after.
1750
    protected function doConvert()
1751
    {
1752
1753
        $options = $this->options;
1754
1755
        try {
1756
            $im = new \Gmagick($this->source);
1757
        } catch (\Exception $e) {
1758
            throw new ConversionFailedException(
1759
                'Failed creating Gmagick object of file',
1760
                'Failed creating Gmagick object of file: "' . $this->source . '" - Gmagick threw an exception.',
1761
                $e
1762
            );
1763
        }
1764
1765
        /*
1766
        Seems there are currently no way to set webp options
1767
        As noted in the following link, it should probably be done with a $im->addDefinition() method
1768
        - but that isn't exposed (yet)
1769
        (TODO: see if anyone has answered...)
1770
        https://stackoverflow.com/questions/47294962/how-to-write-lossless-webp-files-with-perlmagick
1771
        */
1772
        // The following two does not have any effect... How to set WebP options?
1773
        //$im->setimageoption('webp', 'webp:lossless', $options['lossless'] ? 'true' : 'false');
1774
        //$im->setimageoption('WEBP', 'method', strval($options['method']));
1775
1776
        // It seems there is no COMPRESSION_WEBP...
1777
        // http://php.net/manual/en/imagick.setimagecompression.php
1778
        //$im->setImageCompression(Imagick::COMPRESSION_JPEG);
1779
        //$im->setImageCompression(Imagick::COMPRESSION_UNDEFINED);
1780
1781
1782
1783
        $im->setimageformat('WEBP');
1784
1785
        if ($options['metadata'] == 'none') {
1786
            // Strip metadata and profiles
1787
            $im->stripImage();
1788
        }
1789
1790
        // Ps: Imagick automatically uses same quality as source, when no quality is set
1791
        // This feature is however not present in Gmagick
1792
        $im->setcompressionquality($this->getCalculatedQuality());
1793
1794
        try {
1795
            // We call getImageBlob().
1796
            // That method is undocumented, but it is there!
1797
            // - just like it is in imagick, as pointed out here:
1798
            //   https://www.php.net/manual/ru/gmagick.readimageblob.php
1799
1800
            /** @scrutinizer ignore-call */
1801
            $imageBlob = $im->getImageBlob();
1802
        } catch (\ImagickException $e) {
1803
            throw new ConversionFailedException(
1804
                'Gmagick failed converting - getImageBlob() threw an exception)',
1805
                0,
1806
                $e
1807
            );
1808
        }
1809
1810
1811
        //$success = $im->writeimagefile(fopen($destination, 'wb'));
1812
        $success = @file_put_contents($this->destination, $imageBlob);
1813
1814
        if (!$success) {
1815
            throw new ConversionFailedException('Failed writing file');
1816
        } else {
1817
            //$logger->logLn('sooms we made it!');
1818
        }
1819
    }
1820
}
1821
1822
?><?php
1823
1824
namespace WebPConvert\Convert\Converters;
1825
1826
use WebPConvert\Convert\BaseConverters\AbstractConverter;
1827
use WebPConvert\Convert\Exceptions\ConversionFailedException;
1828
use WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblems\CreateDestinationFileException;
1829
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
1830
1831
//use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\TargetNotFoundException;
1832
1833
class Imagick extends AbstractConverter
1834
{
1835
    public static $extraOptions = [];
1836
1837
1838
    /**
1839
     * Check operationality of Imagick converter.
1840
     *
1841
     * Note:
1842
     * It may be that Gd has been compiled without jpeg support or png support.
1843
     * We do not check for this here, as the converter could still be used for the other.
1844
     *
1845
     * @throws SystemRequirementsNotMetException  if system requirements are not met
1846
     */
1847
    protected function checkOperationality()
1848
    {
1849
        if (!extension_loaded('imagick')) {
1850
            throw new SystemRequirementsNotMetException('Required iMagick extension is not available.');
1851
        }
1852
1853
        if (!class_exists('\\Imagick')) {
1854
            throw new SystemRequirementsNotMetException(
1855
                'iMagick is installed, but not correctly. The class Imagick is not available'
1856
            );
1857
        }
1858
1859
        $im = new \Imagick();
1860
1861
        if (!in_array('WEBP', $im->queryFormats())) {
1862
            throw new SystemRequirementsNotMetException('iMagick was compiled without WebP support.');
1863
        }
1864
    }
1865
1866
    /**
1867
     * Check if specific file is convertable with current converter / converter settings.
1868
     *
1869
     * @throws SystemRequirementsNotMetException  if Imagick does not support image type
1870
     */
1871
    protected function checkConvertability()
1872
    {
1873
        $im = new \Imagick();
1874
        $mimeType = $this->getMimeTypeOfSource();
1875
        switch ($mimeType) {
1876
            case 'image/png':
1877
                if (!in_array('PNG', $im->queryFormats())) {
1878
                    throw new SystemRequirementsNotMetException(
1879
                        'Imagick has been compiled without PNG support and can therefore not convert this PNG image.'
1880
                    );
1881
                }
1882
                break;
1883
            case 'image/jpeg':
1884
                if (!in_array('JPEG', $im->queryFormats())) {
1885
                    throw new SystemRequirementsNotMetException(
1886
                        'Imagick has been compiled without Jpeg support and can therefore not convert this Jpeg image.'
1887
                    );
1888
                }
1889
                break;
1890
        }
1891
    }
1892
1893
    protected function doConvert()
1894
    {
1895
        $options = $this->options;
1896
1897
        try {
1898
            $im = new \Imagick($this->source);
1899
        } catch (\Exception $e) {
1900
            throw new ConversionFailedException(
1901
                'Failed creating Gmagick object of file',
1902
                'Failed creating Gmagick object of file: "' . $this->source . '" - Imagick threw an exception.',
1903
                $e
1904
            );
1905
        }
1906
1907
        //$im = new \Imagick();
1908
        //$im->readImage($this->source);
1909
1910
        $im->setImageFormat('WEBP');
1911
1912
        /*
1913
         * More about iMagick's WebP options:
1914
         * http://www.imagemagick.org/script/webp.php
1915
         * https://developers.google.com/speed/webp/docs/cwebp
1916
         * https://stackoverflow.com/questions/37711492/imagemagick-specific-webp-calls-in-php
1917
         */
1918
1919
        // TODO: We could easily support all webp options with a loop
1920
1921
        /*
1922
        After using getImageBlob() to write image, the following setOption() calls
1923
        makes settings makes imagick fail. So can't use those. But its a small price
1924
        to get a converter that actually makes great quality conversions.
1925
1926
        $im->setOption('webp:method', strval($options['method']));
1927
        $im->setOption('webp:low-memory', strval($options['low-memory']));
1928
        $im->setOption('webp:lossless', strval($options['lossless']));
1929
        */
1930
1931
        if ($options['metadata'] == 'none') {
1932
            // Strip metadata and profiles
1933
            $im->stripImage();
1934
        }
1935
1936
        if ($this->isQualityDetectionRequiredButFailing()) {
1937
            // Luckily imagick is a big boy, and automatically converts with same quality as
1938
            // source, when the quality isn't set.
1939
            // So we simply do not set quality.
1940
            // This actually kills the max-quality functionality. But I deem that this is more important
1941
            // because setting image quality to something higher than source generates bigger files,
1942
            // but gets you no extra quality. When failing to limit quality, you at least get something
1943
            // out of it
1944
            $this->logLn('Converting without setting quality, to achieve auto quality');
1945
        } else {
1946
            $im->setImageCompressionQuality($this->getCalculatedQuality());
1947
        }
1948
1949
        // https://stackoverflow.com/questions/29171248/php-imagick-jpeg-optimization
1950
        // setImageFormat
1951
1952
        // TODO: Read up on
1953
        // https://www.smashingmagazine.com/2015/06/efficient-image-resizing-with-imagemagick/
1954
        // https://github.com/nwtn/php-respimg
1955
1956
        // TODO:
1957
        // Should we set alpha channel for PNG's like suggested here:
1958
        // https://gauntface.com/blog/2014/09/02/webp-support-with-imagemagick-and-php ??
1959
        // It seems that alpha channel works without... (at least I see completely transparerent pixels)
1960
1961
        // TODO: Check out other iMagick methods, see http://php.net/manual/de/imagick.writeimage.php#114714
1962
        // 1. file_put_contents($destination, $im)
1963
        // 2. $im->writeImage($destination)
1964
1965
        // We used to use writeImageFile() method. But we now use getImageBlob(). See issue #43
1966
        //$success = $im->writeImageFile(fopen($destination, 'wb'));
1967
1968
        try {
1969
            $imageBlob = $im->getImageBlob();
1970
        } catch (\ImagickException $e) {
1971
            throw new ConversionFailedException(
1972
                'Imagick failed converting - getImageBlob() threw an exception)',
1973
                0,
1974
                $e
1975
            );
1976
        }
1977
1978
        $success = file_put_contents($this->destination, $imageBlob);
1979
1980
        if (!$success) {
1981
            throw new CreateDestinationFileException('Failed writing file');
1982
        }
1983
1984
        // Btw: check out processWebp() method here:
1985
        // https://github.com/Intervention/image/blob/master/src/Intervention/Image/Imagick/Encoder.php
1986
    }
1987
}
1988
1989
?><?php
1990
// TODO: Quality option
1991
namespace WebPConvert\Convert\Converters;
1992
1993
use WebPConvert\Convert\BaseConverters\AbstractExecConverter;
1994
1995
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
1996
use WebPConvert\Convert\Exceptions\ConversionFailedException;
1997
1998
//use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\TargetNotFoundException;
1999
2000
// To futher improve this converter, I could check out:
2001
// https://github.com/Orbitale/ImageMagickPHP
2002
class ImagickBinary extends AbstractExecConverter
2003
{
2004
    public static $extraOptions = [
2005
        [
2006
            'name' => 'use-nice',
2007
            'type' => 'boolean',
2008
            'sensitive' => false,
2009
            'default' => true,
2010
            'required' => false
2011
        ],
2012
    ];
2013
2014
    public static function imagickInstalled()
2015
    {
2016
        exec('convert -version', $output, $returnCode);
2017
        return ($returnCode == 0);
2018
    }
2019
2020
    // Check if webp delegate is installed
2021
    public static function webPDelegateInstalled()
2022
    {
2023
        /* HM. We should not rely on grep being available
2024
        $command = 'convert -list configure | grep -i "delegates" | grep -i webp';
2025
        exec($command, $output, $returnCode);
2026
        return (count($output) > 0);
2027
        */
2028
        $command = 'convert -version';
2029
        exec($command, $output, $returnCode);
2030
        $hasDelegate = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $hasDelegate is dead and can be removed.
Loading history...
2031
        foreach ($output as $line) {
2032
            if (preg_match('/Delegate.*webp.*/i', $line)) {
2033
                return true;
2034
            }
2035
        }
2036
        return false;
2037
    }
2038
2039
    /**
2040
     * Check (general) operationality of imagack converter executable
2041
     *
2042
     * @throws SystemRequirementsNotMetException  if system requirements are not met
2043
     */
2044
    protected function checkOperationality()
2045
    {
2046
        if (!self::imagickInstalled()) {
2047
            throw new SystemRequirementsNotMetException('imagick is not installed');
2048
        }
2049
        if (!self::webPDelegateInstalled()) {
2050
            throw new SystemRequirementsNotMetException('webp delegate missing');
2051
        }
2052
    }
2053
2054
    protected function doConvert()
2055
    {
2056
        //$this->logLn('Using quality:' . $this->getCalculatedQuality());
2057
        // Should we use "magick" or "convert" command?
2058
        // It seems they do the same. But which is best supported? Which is mostly available (whitelisted)?
2059
        // Should we perhaps try both?
2060
        // For now, we just go with "convert"
2061
2062
2063
        $commandArguments = [];
2064
        if ($this->isQualityDetectionRequiredButFailing()) {
2065
            // quality:auto was specified, but could not be determined.
2066
            // we cannot apply the max-quality logic, but we can provide auto quality
2067
            // simply by not specifying the quality option.
2068
        } else {
2069
            $commandArguments[] = '-quality ' . escapeshellarg($this->getCalculatedQuality());
2070
        }
2071
        $commandArguments[] = escapeshellarg($this->source);
2072
        $commandArguments[] = escapeshellarg('webp:' . $this->destination);
2073
2074
        $command = 'convert ' . implode(' ', $commandArguments);
2075
2076
        $useNice = (($this->options['use-nice']) && self::hasNiceSupport()) ? true : false;
2077
        if ($useNice) {
2078
            $this->logLn('using nice');
2079
            $command = 'nice ' . $command;
2080
        }
2081
        $this->logLn('command: ' . $command);
2082
        exec($command, $output, $returnCode);
2083
        if ($returnCode == 127) {
2084
            throw new SystemRequirementsNotMetException('imagick is not installed');
2085
        }
2086
        if ($returnCode != 0) {
2087
            $this->logLn('command:' . $command);
2088
            $this->logLn('return code:' . $returnCode);
2089
            $this->logLn('output:' . print_r(implode("\n", $output), true));
2090
            throw new SystemRequirementsNotMetException('The exec call failed');
2091
        }
2092
    }
2093
}
2094
2095
?><?php
2096
2097
// TODO: Quality option
2098
2099
namespace WebPConvert\Convert\Converters;
2100
2101
use WebPConvert\Convert\BaseConverters\AbstractConverter;
2102
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\ConverterNotFoundException;
2103
use WebPConvert\Convert\Exceptions\ConversionFailedException;
2104
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException;
2105
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
2106
use WebPConvert\Convert\Exceptions\ConversionFailed\ConversionDeclinedException;
2107
2108
//use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\TargetNotFoundException;
2109
2110
class Stack extends AbstractConverter
2111
{
2112
    public static $extraOptions = [
2113
        [
2114
            'name' => 'converters',
2115
            'type' => 'array',
2116
            'sensitive' => true,
2117
            'default' => ['cwebp', 'gd', 'imagick', 'gmagick', 'imagickbinary'],
2118
            'required' => false
2119
        ],
2120
        /*
2121
        [
2122
            'name' => 'skip-pngs',
2123
            'type' => 'boolean',
2124
            'sensitive' => false,
2125
            'default' => false,
2126
            'required' => false
2127
        ],*/
2128
        /*[
2129
            'name' => 'quality',
2130
            'type' => 'quality',
2131
            'sensitive' => false,
2132
            'default' => 'auto',
2133
            'required' => false
2134
        ],*/
2135
    ];
2136
2137
    public static $availableConverters = ['cwebp', 'gd', 'imagick', 'gmagick', 'imagickbinary', 'wpc', 'ewww'];
2138
    public static $localConverters = ['cwebp', 'gd', 'imagick', 'gmagick', 'imagickbinary'];
2139
2140
    public static function converterIdToClassname($converterId)
2141
    {
2142
        switch ($converterId) {
2143
            case 'imagickbinary':
2144
                $classNameShort = 'ImagickBinary';
0 ignored issues
show
Unused Code introduced by
The assignment to $classNameShort is dead and can be removed.
Loading history...
2145
                break;
2146
            default:
2147
                $classNameShort = ucfirst($converterId);
2148
        }
2149
        $className = 'WebPConvert\\Convert\\Converters\\' . ucfirst($converterId);
2150
        if (is_callable([$className, 'convert'])) {
2151
            return $className;
2152
        } else {
2153
            throw new ConverterNotFoundException('There is no converter with id:' . $converterId);
2154
        }
2155
    }
2156
2157
    public static function getClassNameOfConverter($converterId)
2158
    {
2159
        if (strtolower($converterId) == $converterId) {
2160
            return self::converterIdToClassname($converterId);
2161
        }
2162
        $className = $converterId;
2163
        if (!is_callable([$className, 'convert'])) {
2164
            throw new ConverterNotFoundException('There is no converter with class name:' . $className);
2165
        }
2166
2167
        return $className;
2168
    }
2169
2170
    /**
2171
     * Check (general) operationality of imagack converter executable
2172
     *
2173
     * @throws SystemRequirementsNotMetException  if system requirements are not met
2174
     */
2175
    protected function checkOperationality()
2176
    {
2177
        if (count($this->options) == 0) {
2178
            throw new ConverterNotOperationalException(
2179
                'Converter stack is empty! - no converters to try, no conversion can be made!'
2180
            );
2181
        }
2182
2183
        // TODO: We should test if all converters are found in order to detect problems early
2184
    }
2185
2186
    protected function doConvert()
2187
    {
2188
        $options = $this->options;
2189
2190
        $beginTimeStack = microtime(true);
2191
2192
        $this->logLn('Stack converter ignited');
2193
2194
        // If we have set converter options for a converter, which is not in the converter array,
2195
        // then we add it to the array
2196
        if (isset($options['converter-options'])) {
2197
            foreach ($options['converter-options'] as $converterName => $converterOptions) {
2198
                if (!in_array($converterName, $options['converters'])) {
2199
                    $options['converters'][] = $converterName;
2200
                }
2201
            }
2202
        }
2203
2204
        //$this->logLn('converters: ' . print_r($options['converters'], true));
2205
2206
        $defaultConverterOptions = $options;
2207
2208
        unset($defaultConverterOptions['converters']);
2209
        unset($defaultConverterOptions['converter-options']);
2210
        $defaultConverterOptions['_skip_basic_validations'] = true;
2211
        $defaultConverterOptions['_suppress_success_message'] = true;
2212
2213
        $anyRuntimeErrors = false;
2214
        foreach ($options['converters'] as $converter) {
2215
            if (is_array($converter)) {
2216
                $converterId = $converter['converter'];
2217
                $converterOptions = $converter['options'];
2218
            } else {
2219
                $converterId = $converter;
2220
                $converterOptions = [];
2221
                if (isset($options['converter-options'][$converterId])) {
2222
                    // Note: right now, converter-options are not meant to be used,
2223
                    //       when you have several converters of the same type
2224
                    $converterOptions = $options['converter-options'][$converterId];
2225
                }
2226
            }
2227
2228
            $converterOptions = array_merge($defaultConverterOptions, $converterOptions);
2229
2230
            // TODO:
2231
            // Reuse QualityProcessor of previous, unless quality option is overridden
2232
            // ON the other hand: With the recent change, the quality is not detected until a
2233
            // converter needs it (after operation checks). So such feature will rarely be needed now
2234
2235
            // If quality is different, we must recalculate
2236
            /*
2237
            if ($converterOptions['quality'] != $defaultConverterOptions['quality']) {
2238
                unset($converterOptions['_calculated_quality']);
2239
            }*/
2240
2241
            $beginTime = microtime(true);
2242
2243
            $className = self::getClassNameOfConverter($converterId);
2244
2245
            try {
2246
                $converterDisplayName = call_user_func(
2247
                    [$className, 'getConverterDisplayName']
2248
                );
2249
2250
                $this->ln();
2251
                $this->logLn('Trying: ' . $converterId, 'italic');
2252
2253
                call_user_func(
2254
                    [$className, 'convert'],
2255
                    $this->source,
2256
                    $this->destination,
2257
                    $converterOptions,
2258
                    $this->logger
2259
                );
2260
2261
                //self::runConverterWithTiming($converterId, $source, $destination, $converterOptions, false, $logger);
2262
2263
                $this->logLn($converterDisplayName . ' succeeded :)');
2264
                return;
2265
            } catch (ConverterNotOperationalException $e) {
2266
                $this->logLn($e->getMessage());
2267
            } catch (ConversionFailedException $e) {
2268
                $this->logLn($e->getMessage(), 'italic');
2269
                $prev = $e->getPrevious();
2270
                if (!is_null($prev)) {
2271
                    $this->logLn($prev->getMessage(), 'italic');
2272
                    $this->logLn(' in ' . $prev->getFile() . ', line ' . $prev->getLine(), 'italic');
2273
                    $this->ln();
2274
                }
2275
                //$this->logLn($e->getTraceAsString());
2276
                $anyRuntimeErrors = true;
2277
            } catch (ConversionDeclinedException $e) {
2278
                $this->logLn($e->getMessage());
2279
            }
2280
2281
            $this->logLn($converterDisplayName . ' failed in ' . round((microtime(true) - $beginTime) * 1000) . ' ms');
2282
        }
2283
2284
        $this->ln();
2285
        $this->logLn('Stack failed in ' . round((microtime(true) - $beginTimeStack) * 1000) . ' ms');
2286
2287
        if ($anyRuntimeErrors) {
0 ignored issues
show
introduced by
The condition $anyRuntimeErrors is always false.
Loading history...
2288
            // At least one converter failed
2289
            throw new ConversionFailedException(
2290
                'None of the converters in the stack could convert the image. ' .
2291
                'At least one failed, even though its requirements seemed to be met.'
2292
            );
2293
        } else {
2294
            // All converters threw a SystemRequirementsNotMetException
2295
            throw new ConverterNotOperationalException('None of the converters in the stack are operational');
2296
        }
2297
    }
2298
}
2299
2300
?><?php
2301
2302
namespace WebPConvert\Convert\Converters;
2303
2304
use WebPConvert\Convert\BaseConverters\AbstractCloudCurlConverter;
2305
use WebPConvert\Convert\Exceptions\ConversionFailedException;
2306
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException;
2307
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
2308
2309
class Wpc extends AbstractCloudCurlConverter
2310
{
2311
    public static $extraOptions = [
2312
        [
2313
            'name' => 'api-version',        /* Can currently be 0 or 1 */
2314
            'type' => 'number',
2315
            'sensitive' => false,
2316
            'default' => 0,
2317
            'required' => false
2318
        ],
2319
        [
2320
            'name' => 'secret',        /* only in api v.0 */
2321
            'type' => 'string',
2322
            'sensitive' => true,
2323
            'default' => 'my dog is white',
2324
            'required' => false
2325
        ],
2326
        [
2327
            'name' => 'api-key',        /* new in api v.1 (renamed 'secret' to 'api-key') */
2328
            'type' => 'string',
2329
            'sensitive' => true,
2330
            'default' => 'my dog is white',
2331
            'required' => false
2332
        ],
2333
        [
2334
            'name' => 'url',
2335
            'type' => 'string',
2336
            'sensitive' => true,
2337
            'default' => '',
2338
            'required' => true
2339
        ],
2340
        [
2341
            'name' => 'crypt-api-key-in-transfer',  /* new in api v.1 */
2342
            'type' => 'boolean',
2343
            'sensitive' => false,
2344
            'default' => false,
2345
            'required' => false
2346
        ],
2347
2348
        /*
2349
        [
2350
            'name' => 'web-services',
2351
            'type' => 'array',
2352
            'sensitive' => true,
2353
            'default' => [
2354
                [
2355
                    'label' => 'test',
2356
                    'api-key' => 'my dog is white',
2357
                    'url' => 'http://we0/wordpress/webp-express-server',
2358
                    'crypt-api-key-in-transfer' => true
2359
                ]
2360
            ],
2361
            'required' => true
2362
        ],
2363
        */
2364
    ];
2365
2366
    private static function createRandomSaltForBlowfish()
2367
    {
2368
        $salt = '';
2369
        $validCharsForSalt = array_merge(
2370
            range('A', 'Z'),
2371
            range('a', 'z'),
2372
            range('0', '9'),
2373
            ['.', '/']
2374
        );
2375
2376
        for ($i=0; $i<22; $i++) {
2377
            $salt .= $validCharsForSalt[array_rand($validCharsForSalt)];
2378
        }
2379
        return $salt;
2380
    }
2381
2382
    // Although this method is public, do not call directly.
2383
    // You should rather call the static convert() function, defined in AbstractConverter, which
2384
    // takes care of preparing stuff before calling doConvert, and validating after.
2385
    protected function doConvert()
2386
    {
2387
        $options = $this->options;
2388
2389
        $apiVersion = $options['api-version'];
2390
2391
        if (!function_exists('curl_file_create')) {
2392
            throw new SystemRequirementsNotMetException(
2393
                'Required curl_file_create() PHP function is not available (requires PHP > 5.5).'
2394
            );
2395
        }
2396
2397
        if ($apiVersion == 0) {
2398
            if (!empty($options['secret'])) {
2399
                // if secret is set, we need md5() and md5_file() functions
2400
                if (!function_exists('md5')) {
2401
                    throw new ConverterNotOperationalException(
2402
                        'A secret has been set, which requires us to create a md5 hash from the secret and the file ' .
2403
                        'contents. ' .
2404
                        'But the required md5() PHP function is not available.'
2405
                    );
2406
                }
2407
                if (!function_exists('md5_file')) {
2408
                    throw new ConverterNotOperationalException(
2409
                        'A secret has been set, which requires us to create a md5 hash from the secret and the file ' .
2410
                        'contents. But the required md5_file() PHP function is not available.'
2411
                    );
2412
                }
2413
            }
2414
        }
2415
2416
        if ($apiVersion == 1) {
2417
        /*
2418
                if (count($options['web-services']) == 0) {
2419
                    throw new SystemRequirementsNotMetException('No remote host has been set up');
2420
                }*/
2421
        }
2422
2423
        if ($options['url'] == '') {
2424
            throw new ConverterNotOperationalException(
2425
                'Missing URL. You must install Webp Convert Cloud Service on a server, ' .
2426
                'or the WebP Express plugin for Wordpress - and supply the url.'
2427
            );
2428
        }
2429
2430
        // Got some code here:
2431
        // https://coderwall.com/p/v4ps1a/send-a-file-via-post-with-curl-and-php
2432
2433
        $ch = self::initCurl();
2434
2435
        $optionsToSend = $options;
2436
2437
        if ($this->isQualityDetectionRequiredButFailing()) {
2438
            // quality was set to "auto", but we could not meassure the quality of the jpeg locally
2439
            // Ask the cloud service to do it, rather than using what we came up with.
2440
            $optionsToSend['quality'] = 'auto';
2441
        } else {
2442
            $optionsToSend['quality'] = $this->getCalculatedQuality();
2443
        }
2444
2445
        unset($optionsToSend['converters']);
2446
        unset($optionsToSend['secret']);
2447
2448
        $postData = [
2449
            'file' => curl_file_create($this->source),
2450
            'options' => json_encode($optionsToSend),
2451
            'servername' => (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '')
2452
        ];
2453
2454
        if ($apiVersion == 0) {
2455
            $postData['hash'] = md5(md5_file($this->source) . $options['secret']);
2456
        }
2457
2458
        if ($apiVersion == 1) {
2459
            $apiKey = $options['api-key'];
2460
2461
            if ($options['crypt-api-key-in-transfer']) {
2462
                if (CRYPT_BLOWFISH == 1) {
2463
                    $salt = self::createRandomSaltForBlowfish();
2464
                    $postData['salt'] = $salt;
2465
2466
                    // Strip off the first 28 characters (the first 6 are always "$2y$10$". The next 22 is the salt)
2467
                    $postData['api-key-crypted'] = substr(crypt($apiKey, '$2y$10$' . $salt . '$'), 28);
2468
                } else {
2469
                    if (!function_exists('crypt')) {
2470
                        throw new ConverterNotOperationalException(
2471
                            'Configured to crypt the api-key, but crypt() function is not available.'
2472
                        );
2473
                    } else {
2474
                        throw new ConverterNotOperationalException(
2475
                            'Configured to crypt the api-key. ' .
2476
                            'That requires Blowfish encryption, which is not available on your current setup.'
2477
                        );
2478
                    }
2479
                }
2480
            } else {
2481
                $postData['api-key'] = $apiKey;
2482
            }
2483
        }
2484
2485
2486
        // Try one host at the time
2487
        // TODO: shuffle the array first
2488
        /*
2489
        foreach ($options['web-services'] as $webService) {
2490
2491
        }
2492
        */
2493
2494
2495
        curl_setopt_array($ch, [
2496
            CURLOPT_URL => $options['url'],
2497
            CURLOPT_POST => 1,
2498
            CURLOPT_POSTFIELDS => $postData,
2499
            CURLOPT_BINARYTRANSFER => true,
2500
            CURLOPT_RETURNTRANSFER => true,
2501
            CURLOPT_HEADER => false,
2502
            CURLOPT_SSL_VERIFYPEER => false
2503
        ]);
2504
2505
        $response = curl_exec($ch);
2506
        if (curl_errno($ch)) {
2507
            throw new ConverterNotOperationalException('Curl error:' . curl_error($ch));
2508
        }
2509
2510
        // Check if we got a 404
2511
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
2512
        if ($httpCode == 404) {
2513
            curl_close($ch);
2514
            throw new ConversionFailedException(
2515
                'WPC was not found at the specified URL - we got a 404 response.'
2516
            );
2517
        }
2518
2519
        // The WPC cloud service either returns an image or an error message
2520
        // Images has application/octet-stream.
2521
        // Verify that we got an image back.
2522
        if (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) != 'application/octet-stream') {
2523
            curl_close($ch);
2524
2525
            if (substr($response, 0, 1) == '{') {
2526
                $responseObj = json_decode($response, true);
2527
                if (isset($responseObj['errorCode'])) {
2528
                    switch ($responseObj['errorCode']) {
2529
                        case 0:
2530
                            throw new ConverterNotOperationalException(
2531
                                'There are problems with the server setup: "' .
2532
                                $responseObj['errorMessage'] . '"'
2533
                            );
2534
                        case 1:
2535
                            throw new ConverterNotOperationalException(
2536
                                'Access denied. ' . $responseObj['errorMessage']
2537
                            );
2538
                        default:
2539
                            throw new ConversionFailedException(
2540
                                'Conversion failed: "' . $responseObj['errorMessage'] . '"'
2541
                            );
2542
                    }
2543
                }
2544
            }
2545
2546
            // WPC 0.1 returns 'failed![error messag]' when conversion fails. Handle that.
2547
            if (substr($response, 0, 7) == 'failed!') {
2548
                throw new ConversionFailedException(
2549
                    'WPC failed converting image: "' . substr($response, 7) . '"'
2550
                );
2551
            }
2552
2553
            if (empty($response)) {
2554
                $errorMsg = 'Error: Unexpected result. We got nothing back. HTTP CODE: ' . $httpCode;
2555
                throw new ConversionFailedException($errorMsg);
2556
            } else {
2557
                $errorMsg = 'Error: Unexpected result. We did not receive an image. We received: "';
2558
                $errorMsg .= str_replace("\r", '', str_replace("\n", '', htmlentities(substr($response, 0, 400))));
2559
                throw new ConversionFailedException($errorMsg . '..."');
2560
            }
2561
            //throw new ConverterNotOperationalException($response);
2562
        }
2563
2564
        $success = @file_put_contents($this->destination, $response);
2565
        curl_close($ch);
2566
2567
        if (!$success) {
2568
            throw new ConversionFailedException('Error saving file. Check file permissions');
2569
        }
2570
        /*
2571
                $curlOptions = [
2572
                    'api_key' => $options['key'],
2573
                    'webp' => '1',
2574
                    'file' => curl_file_create($this->source),
2575
                    'domain' => $_SERVER['HTTP_HOST'],
2576
                    'quality' => $options['quality'],
2577
                    'metadata' => ($options['metadata'] == 'none' ? '0' : '1')
2578
                ];
2579
2580
                curl_setopt_array($ch, [
2581
                    CURLOPT_URL => "https://optimize.exactlywww.com/v2/",
2582
                    CURLOPT_HTTPHEADER => [
2583
                        'User-Agent: WebPConvert',
2584
                        'Accept: image/*'
2585
                    ],
2586
                    CURLOPT_POSTFIELDS => $curlOptions,
2587
                    CURLOPT_BINARYTRANSFER => true,
2588
                    CURLOPT_RETURNTRANSFER => true,
2589
                    CURLOPT_HEADER => false,
2590
                    CURLOPT_SSL_VERIFYPEER => false
2591
                ]);*/
2592
    }
2593
}
2594
2595
?><?php
2596
2597
namespace WebPConvert\Convert\Exceptions;
2598
2599
use WebPConvert\Exceptions\WebPConvertException;
2600
2601
/**
2602
 *  ConversionFailedException is the base exception in the hierarchy for conversion errors.
2603
 *
2604
 *  Exception hierarchy from here:
2605
 *
2606
 *  WebpConvertException
2607
 *      ConversionFailedException
2608
 *          ConversionDeclinedException
2609
 *          ConverterNotOperationalException
2610
 *              SystemRequirementsNotMetException
2611
 *          FileSystemProblemsException
2612
 *              CreateDestinationFileException
2613
 *              CreateDestinationFolderException
2614
 *          InvalidInputException
2615
 *              ConverterNotFoundException
2616
 *              InvalidImageTypeException
2617
 *              TargetNotFoundException
2618
 *          UnhandledException
2619
 */
2620
class ConversionFailedException extends WebPConvertException
2621
{
2622
    public $description = 'The converter failed converting, although requirements seemed to be met';
2623
}
2624
2625
?><?php
2626
2627
namespace WebPConvert\Convert\Exceptions\ConversionFailed;
2628
2629
use WebPConvert\Convert\Exceptions\ConversionFailedException;
2630
2631
class ConversionDeclinedException extends ConversionFailedException
2632
{
2633
    public $description = 'The converter declined converting';
2634
}
2635
2636
?><?php
2637
2638
namespace WebPConvert\Convert\Exceptions\ConversionFailed;
2639
2640
use WebPConvert\Convert\Exceptions\ConversionFailedException;
2641
2642
class ConverterNotOperationalException extends ConversionFailedException
2643
{
2644
    public $description = 'The converter is not operational';
2645
}
2646
2647
?><?php
2648
2649
namespace WebPConvert\Convert\Exceptions\ConversionFailed;
2650
2651
use WebPConvert\Convert\Exceptions\ConversionFailedException;
2652
2653
class FileSystemProblemsException extends ConversionFailedException
2654
{
2655
    public $description = 'Filesystem problems';
2656
}
2657
2658
?><?php
2659
2660
namespace WebPConvert\Convert\Exceptions\ConversionFailed;
2661
2662
use WebPConvert\Convert\Exceptions\ConversionFailedException;
2663
2664
class InvalidInputException extends ConversionFailedException
2665
{
2666
    public $description = 'Invalid input';
2667
}
2668
2669
?><?php
2670
2671
namespace WebPConvert\Convert\Exceptions\ConversionFailed;
2672
2673
use WebPConvert\Convert\Exceptions\ConversionFailedException;
2674
2675
class UnhandledException extends ConversionFailedException
2676
{
2677
    public $description = 'The converter failed due to uncaught exception';
2678
2679
    /*
2680
    Nah, do not add message of the uncaught exception to this.
2681
    - because it might be long and contain characters which consumers for example cannot put inside a
2682
    x-webpconvert-error header
2683
    The messages we throw are guaranteed to be short
2684
2685
    public function __construct($message="", $code=0, $previous)
2686
    {
2687
        parent::__construct(
2688
            'The converter failed due to uncaught exception: ' . $previous->getMessage(),
2689
            $code,
2690
            $previous
2691
        );
2692
        //$this->$message = 'hello.' . $message . ' ' . $previous->getMessage();
2693
    }*/
2694
}
2695
2696
?><?php
2697
2698
namespace WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational;
2699
2700
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException;
2701
2702
class SystemRequirementsNotMetException extends ConverterNotOperationalException
2703
{
2704
    public $description = 'The converter is not operational (system requirements not met)';
2705
}
2706
2707
?><?php
2708
2709
namespace WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblems;
2710
2711
use WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblemsException;
2712
2713
class CreateDestinationFileException extends FileSystemProblemsException
2714
{
2715
    public $description = 'The converter could not create destination file. Check file permisions!';
2716
}
2717
2718
?><?php
2719
2720
namespace WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblems;
2721
2722
use WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblemsException;
2723
2724
class CreateDestinationFolderException extends FileSystemProblemsException
2725
{
2726
    public $description = 'The converter could not create destination folder. Check file permisions!';
2727
}
2728
2729
?><?php
2730
2731
namespace WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput;
2732
2733
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInputException;
2734
2735
class ConverterNotFoundException extends InvalidInputException
2736
{
2737
    public $description = 'The converter does not exist.';
2738
}
2739
2740
?><?php
2741
2742
namespace WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput;
2743
2744
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInputException;
2745
2746
class InvalidImageTypeException extends InvalidInputException
2747
{
2748
    public $description = 'The converter does not handle the supplied image type';
2749
}
2750
2751
?><?php
2752
2753
namespace WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput;
2754
2755
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInputException;
2756
2757
class TargetNotFoundException extends InvalidInputException
2758
{
2759
    public $description = 'The converter could not locate source file';
2760
}
2761
2762
?><?php
2763
2764
namespace WebPConvert\Convert;
2765
2766
use WebPConvert\Helpers\JpegQualityDetector;
2767
2768
class QualityProcessor
2769
{
2770
2771
    private $converter;
2772
    private $processed = false;
2773
    private $qualityCouldNotBeDetected = false;
2774
    private $calculatedQuality;
2775
2776
    public function __construct($converter)
2777
    {
2778
        $this->converter = $converter;
2779
    }
2780
2781
    private function processIfNotAlready()
2782
    {
2783
        if (!$this->processed) {
2784
            $this->processed = true;
2785
            $this->proccess();
2786
        }
2787
    }
2788
2789
    /**
2790
     *  Determine if quality detection is required but failing.
2791
     *
2792
     *  It is considered "required" when:
2793
     *  - Mime type is "image/jpeg"
2794
     *  - Quality is set to "auto"
2795
     *
2796
     *  @return  boolean
2797
     */
2798
    public function isQualityDetectionRequiredButFailing()
2799
    {
2800
        $this->processIfNotAlready();
2801
        return $this->qualityCouldNotBeDetected;
2802
    }
2803
2804
    /**
2805
     *  Get calculated quality.
2806
     *
2807
     *  If mime type is something else than "image/jpeg", the "default-quality" option is returned
2808
     *  Same thing for jpeg, when the "quality" option is set to a number (rather than "auto").
2809
     *
2810
     *  Otherwise:
2811
     *  If quality cannot be detetected, the "default-quality" option is returned.
2812
     *  If quality can be detetected, the lowest value of this and the "max-quality" option is returned
2813
     *
2814
     *  @return  int
2815
     */
2816
    public function getCalculatedQuality()
2817
    {
2818
        $this->processIfNotAlready();
2819
        return $this->calculatedQuality;
2820
    }
2821
2822
2823
    private function proccess()
2824
    {
2825
        $options = $this->converter->options;
2826
        $logger = $this->converter->logger;
2827
        $source = $this->converter->source;
2828
2829
        $q = $options['quality'];
2830
        if ($q == 'auto') {
2831
            if (($this->converter->getMimeTypeOfSource() == 'image/jpeg')) {
2832
                $q = JpegQualityDetector::detectQualityOfJpg($source);
2833
                if (is_null($q)) {
2834
                    $q = $options['default-quality'];
2835
                    $logger->logLn(
2836
                        'Quality of source could not be established (Imagick or GraphicsMagick is required)' .
2837
                        ' - Using default instead (' . $options['default-quality'] . ').'
2838
                    );
2839
2840
                    $this->qualityCouldNotBeDetected = true;
2841
                } else {
2842
                    if ($q > $options['max-quality']) {
2843
                        $logger->logLn(
2844
                            'Quality of source is ' . $q . '. ' .
2845
                            'This is higher than max-quality, so using max-quality instead (' .
2846
                                $options['max-quality'] . ')'
2847
                        );
2848
                    } else {
2849
                        $logger->logLn('Quality set to same as source: ' . $q);
2850
                    }
2851
                }
2852
                $q = min($q, $options['max-quality']);
2853
            } else {
2854
                $q = $options['default-quality'];
2855
                $logger->logLn('Quality: ' . $q . '. ');
2856
            }
2857
        } else {
2858
            $logger->logLn(
2859
                'Quality: ' . $q . '. ' .
2860
                'Consider setting quality to "auto" instead. It is generally a better idea'
2861
            );
2862
        }
2863
        $this->calculatedQuality = $q;
2864
    }
2865
}
2866
2867
?><?php
2868
2869
namespace WebPConvert\Exceptions;
2870
2871
use WebPConvert\Exceptions\WebPConvertException;
2872
2873
/**
2874
 *  WebPConvertException is the base exception for all exceptions in this library.
2875
 *
2876
 *  Note that the parameters for the constructor differs from that of the Exception class.
2877
 *  We do not use exception code here, but are instead allowing two version of the error message:
2878
 *  a short version and a long version.
2879
 *  The short version may not contain special characters or dynamic content.
2880
 *  The detailed version may.
2881
 *  If the detailed version isn't provided, getDetailedMessage will return the short version.
2882
 *
2883
 */
2884
class WarningException extends WebPConvertException
2885
{
2886
    public $description = 'A warning was issued and turned into an exception';
2887
}
2888
2889
?><?php
2890
2891
namespace WebPConvert\Exceptions;
2892
2893
/**
2894
 *  WebPConvertException is the base exception for all exceptions in this library.
2895
 *
2896
 *  Note that the parameters for the constructor differs from that of the Exception class.
2897
 *  We do not use exception code here, but are instead allowing two version of the error message:
2898
 *  a short version and a long version.
2899
 *  The short version may not contain special characters or dynamic content.
2900
 *  The detailed version may.
2901
 *  If the detailed version isn't provided, getDetailedMessage will return the short version.
2902
 *
2903
 */
2904
class WebPConvertException extends \Exception
2905
{
2906
    public $description = 'The converter failed converting, although requirements seemed to be met';
2907
    protected $detailedMessage;
2908
    protected $shortMessage;
2909
2910
    public function getDetailedMessage()
2911
    {
2912
        return $this->detailedMessage;
2913
    }
2914
2915
    public function getShortMessage()
2916
    {
2917
        return $this->shortMessage;
2918
    }
2919
2920
    public function __construct($shortMessage = "", $detailedMessage = "", $previous = null)
2921
    {
2922
        $detailedMessage = ($detailedMessage != '') ? $detailedMessage : $shortMessage;
2923
        $this->detailedMessage = $detailedMessage;
2924
        $this->shortMessage = $shortMessage;
2925
2926
        parent::__construct(
2927
            $detailedMessage,
2928
            0,
2929
            $previous
2930
        );
2931
    }
2932
}
2933
2934
?><?php
2935
2936
namespace WebPConvert\Helpers;
2937
2938
use WebPConvert\Helpers\WarningsIntoExceptions;
2939
2940
abstract class JpegQualityDetector
2941
{
2942
2943
    /**
2944
     * Try to detect quality of jpeg using imagick extension
2945
     *
2946
     * @param  string  $filename  A complete file path to file to be examined
2947
     * @return int|null  Quality, or null if it was not possible to detect quality
2948
     */
2949
    private static function detectQualityOfJpgUsingImagickExtension($filename)
2950
    {
2951
        if (extension_loaded('imagick') && class_exists('\\Imagick')) {
2952
            try {
2953
                $img = new \Imagick($filename);
2954
2955
                // The required function is available as from PECL imagick v2.2.2
2956
                if (method_exists($img, 'getImageCompressionQuality')) {
2957
                    return $img->getImageCompressionQuality();
2958
                }
2959
            } catch (\Exception $e) {
2960
                // Well well, it just didn't work out.
2961
                // - But perhaps next method will work...
2962
            }
2963
        }
2964
        return null;
2965
    }
2966
2967
2968
    /**
2969
     * Try to detect quality of jpeg using imagick binary
2970
     *
2971
     * @param  string  $filename  A complete file path to file to be examined
2972
     * @return int|null  Quality, or null if it was not possible to detect quality
2973
     */
2974
    private static function detectQualityOfJpgUsingImagickBinary($filename)
2975
    {
2976
        if (function_exists('exec')) {
2977
            // Try Imagick using exec, and routing stderr to stdout (the "2>$1" magic)
2978
            exec("identify -format '%Q' " . escapeshellarg($filename) . " 2>&1", $output, $returnCode);
2979
            //echo 'out:' . print_r($output, true);
2980
            if ((intval($returnCode) == 0) && (is_array($output)) && (count($output) == 1)) {
2981
                return intval($output[0]);
2982
            }
2983
        }
2984
        return null;
2985
    }
2986
2987
2988
    /**
2989
     * Try to detect quality of jpeg using gmagick binary
2990
     *
2991
     * @param  string  $filename  A complete file path to file to be examined
2992
     * @return int|null  Quality, or null if it was not possible to detect quality
2993
     */
2994
    private static function detectQualityOfJpgUsingGmagickBinary($filename)
2995
    {
2996
        if (function_exists('exec')) {
2997
            // Try GraphicsMagick
2998
            exec("gm identify -format '%Q' " . escapeshellarg($filename) . " 2>&1", $output, $returnCode);
2999
            if ((intval($returnCode) == 0) && (is_array($output)) && (count($output) == 1)) {
3000
                return intval($output[0]);
3001
            }
3002
        }
3003
        return null;
3004
    }
3005
3006
3007
    /**
3008
     * Try to detect quality of jpeg.
3009
     *
3010
     * Note: This method does not throw errors, but might dispatch warnings.
3011
     * You can use the WarningsIntoExceptions class if it is critical to you that nothing gets "printed"
3012
     *
3013
     * @param  string  $filename  A complete file path to file to be examined
3014
     * @return int|null  Quality, or null if it was not possible to detect quality
3015
     */
3016
    public static function detectQualityOfJpg($filename)
3017
    {
3018
3019
        //trigger_error('warning test', E_USER_WARNING);
3020
3021
        // Test that file exists in order not to break things.
3022
        if (!file_exists($filename)) {
3023
            // One could argue that it would be better to throw an Exception...?
3024
            return null;
3025
        }
3026
3027
        // Try Imagick extension, if available
3028
        $quality = self::detectQualityOfJpgUsingImagickExtension($filename);
3029
3030
        if (is_null($quality)) {
3031
            $quality = self::detectQualityOfJpgUsingImagickBinary($filename);
3032
        }
3033
3034
        if (is_null($quality)) {
3035
            $quality = self::detectQualityOfJpgUsingGmagickBinary($filename);
3036
        }
3037
3038
        return $quality;
3039
    }
3040
}
3041
3042
?><?php
3043
3044
namespace WebPConvert\Helpers;
3045
3046
use WebPConvert\Exceptions\WarningException;
3047
3048
abstract class WarningsIntoExceptions
3049
{
3050
3051
    public static function warningHandler($errno, $errstr, $errfile, $errline)
3052
    {
3053
        //echo 'aeothsutsanoheutsnhaoeu: ' . E_USER_WARNING . ':' . E_WARNING;
3054
        throw new WarningException(
3055
            'A warning was issued',
3056
            'A warning was issued: ' . ': ' . $errstr . ' in ' . $errfile . ', line ' . $errline .
3057
                ', PHP ' . PHP_VERSION .
3058
                ' (' . PHP_OS . ')'
3059
        );
3060
3061
        /* Don't execute PHP internal error handler */
3062
        return true;
0 ignored issues
show
Unused Code introduced by
return true is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
3063
    }
3064
3065
    public static function activate()
3066
    {
3067
        set_error_handler(
3068
            array('\\WebPConvert\\Helpers\\WarningsIntoExceptions', "warningHandler"),
3069
            E_WARNING | E_USER_WARNING | E_ALL
3070
        );   // E_USER_WARNING
3071
    }
3072
3073
    public static function deactivate()
3074
    {
3075
        restore_error_handler();
3076
    }
3077
}
3078
3079
?><?php
3080
3081
namespace WebPConvert\Loggers;
3082
3083
abstract class BaseLogger
3084
{
3085
    /*
3086
    $msg: message to log
3087
    $style: null | bold | italic
3088
    */
3089
    abstract public function log($msg, $style = '');
3090
3091
    abstract public function ln();
3092
3093
    public function logLn($msg, $style = '')
3094
    {
3095
        $this->log($msg, $style);
3096
        $this->ln();
3097
    }
3098
3099
    public function logLnLn($msg, $style = '')
3100
    {
3101
        $this->logLn($msg, $style);
3102
        $this->ln();
3103
    }
3104
}
3105
3106
?><?php
3107
3108
namespace WebPConvert\Loggers;
3109
3110
use WebPConvert\Loggers\BaseLogger;
3111
3112
class BufferLogger extends BaseLogger
3113
{
3114
    public $entries = array();
3115
3116
    public function log($msg, $style = '')
3117
    {
3118
        $this->entries[] = [$msg, $style];
3119
    }
3120
3121
    public function ln()
3122
    {
3123
        $this->entries[] = '';
3124
    }
3125
3126
    public function getHtml()
3127
    {
3128
        $html = '';
3129
        foreach ($this->entries as $entry) {
3130
            if ($entry == '') {
3131
                $html .= '<br>';
3132
            } else {
3133
                list($msg, $style) = $entry;
3134
                $msg = htmlspecialchars($msg);
3135
                if ($style == 'bold') {
3136
                    $html .= '<b>' . $msg . '</b>';
3137
                } elseif ($style == 'italic') {
3138
                    $html .= '<i>' . $msg . '</i>';
3139
                } else {
3140
                    $html .= $msg;
3141
                }
3142
            }
3143
        }
3144
        return $html;
3145
    }
3146
3147
    public function getText($newLineChar = ' ')
0 ignored issues
show
Unused Code introduced by
The parameter $newLineChar is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

3147
    public function getText(/** @scrutinizer ignore-unused */ $newLineChar = ' ')

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
3148
    {
3149
        $text = '';
3150
        foreach ($this->entries as $entry) {
3151
            if ($entry == '') {
3152
                if (substr($text, -2) != '. ') {
3153
                    $text .= '. ';
3154
                }
3155
            } else {
3156
                list($msg, $style) = $entry;
3157
                $text .= $msg;
3158
            }
3159
        }
3160
3161
        return $text;
3162
    }
3163
}
3164
3165
?><?php
3166
3167
namespace WebPConvert\Loggers;
3168
3169
class EchoLogger extends BaseLogger
3170
{
3171
    public function log($msg, $style = '')
3172
    {
3173
        $msg = htmlspecialchars($msg);
3174
        if ($style == 'bold') {
3175
            echo '<b>' . $msg . '</b>';
3176
        } elseif ($style == 'italic') {
3177
            echo '<i>' . $msg . '</i>';
3178
        } else {
3179
            echo $msg;
3180
        }
3181
    }
3182
3183
    public function ln()
3184
    {
3185
        echo '<br>';
3186
    }
3187
}
3188
3189
?><?php
3190
3191
namespace WebPConvert\Loggers;
3192
3193
class VoidLogger extends BaseLogger
3194
{
3195
    public function log($msg, $style = '')
3196
    {
3197
    }
3198
3199
    public function ln()
3200
    {
3201
    }
3202
}
3203
3204
?><?php
3205
namespace WebPConvert\Serve;
3206
3207
use WebPConvert\WebPConvert;
3208
use WebPConvert\Loggers\EchoLogger;
3209
3210
//use WebPConvert\Loggers\EchoLogger;
3211
3212
class Report
3213
{
3214
3215
    /**
3216
     *   Input: We have a converter array where the options are defined
3217
     *   Output:  the converter array is "flattened" to be just names.
3218
     *            and the options have been moved to the "converter-options" option.
3219
     */
3220
    public static function flattenConvertersArray($options)
3221
    {
3222
        // TODO: If there are more of the same converters,
3223
        // they should be added as ie 'wpc-2', 'wpc-3', etc
3224
3225
        $result = $options;
3226
        $result['converters'] = [];
3227
        foreach ($options['converters'] as $converter) {
3228
            if (is_array($converter)) {
3229
                $converterName = $converter['converter'];
3230
                if (!isset($options['converter-options'][$converterName])) {
3231
                    if (isset($converter['options'])) {
3232
                        if (!isset($result['converter-options'])) {
3233
                            $result['converter-options'] = [];
3234
                        }
3235
                        $result['converter-options'][$converterName] = $converter['options'];
3236
                    }
3237
                }
3238
                $result['converters'][] = $converterName;
3239
            } else {
3240
                $result['converters'][] = $converter;
3241
            }
3242
        }
3243
        return $result;
3244
    }
3245
3246
    /* Hides sensitive options */
3247
    public static function getPrintableOptions($options)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

3247
    public static function getPrintableOptions(/** @scrutinizer ignore-unused */ $options)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
3248
    {
3249
        $printable_options = [];
3250
3251
        /*
3252
        TODO: This piece of code should be "translated" to work in 2.0
3253
        if (is_callable('ConverterHelper', 'getClassNameOfConverter')) {
3254
            $printable_options = self::flattenConvertersArray($options);
3255
            if (isset($printable_options['converter-options'])) {
3256
                foreach ($printable_options['converter-options'] as $converterName => &$converterOptions) {
3257
                    $className = ConverterHelper::getClassNameOfConverter($converterName);
3258
3259
                    // (pstt: the isset check is needed in order to work with WebPConvert v1.0)
3260
                    if (isset($className::$extraOptions)) {
3261
                        foreach ($className::$extraOptions as $extraOption) {
3262
                            if ($extraOption['sensitive']) {
3263
                                if (isset($converterOptions[$extraOption['name']])) {
3264
                                    $converterOptions[$extraOption['name']] = '*******';
3265
                                }
3266
                            }
3267
                        }
3268
                    }
3269
                }
3270
            }
3271
        }
3272
        */
3273
        return $printable_options;
3274
    }
3275
3276
    public static function getPrintableOptionsAsString($options, $glue = '. ')
3277
    {
3278
        $optionsForPrint = [];
3279
        foreach (self::getPrintableOptions($options) as $optionName => $optionValue) {
3280
            $printValue = '';
3281
            if ($optionName == 'converter-options') {
3282
                $converterNames = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $converterNames is dead and can be removed.
Loading history...
3283
                $extraConvertOptions = $optionValue;
3284
                //print_r($extraConvertOptions);
3285
                /*
3286
                foreach ($optionValue as $converterName => $converterOptions) {
3287
3288
                    if (is_array($converter)) {
3289
                        $converterName = $converter['converter'];
3290
                        if (isset($converter['options'])) {
3291
                            $extraConvertOptions[$converter['converter']] = $converter['options'];
3292
                        }
3293
                    } else {
3294
                        $converterName = $converter;
3295
                    }
3296
                    $converterNames[] = $converterName;
3297
                }*/
3298
                $glueMe = [];
3299
                foreach ($extraConvertOptions as $converter => $extraOptions) {
3300
                    $opt = [];
3301
                    foreach ($extraOptions as $oName => $oValue) {
3302
                        $opt[] = $oName . ':"' . $oValue . '"';
3303
                    }
3304
                    $glueMe[] = '(' . $converter . ': (' . implode($opt, ', ') . '))';
0 ignored issues
show
Unused Code introduced by
The call to implode() has too many arguments starting with ', '. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3304
                    $glueMe[] = '(' . $converter . ': (' . /** @scrutinizer ignore-call */ implode($opt, ', ') . '))';

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
3305
                }
3306
                $printValue = implode(',', $glueMe);
3307
            } elseif ($optionName == 'web-service') {
3308
                $printValue = 'sensitive, so not displaying here...';
3309
            } else {
3310
                switch (gettype($optionValue)) {
3311
                    case 'boolean':
3312
                        if ($optionValue === true) {
3313
                            $printValue = 'true';
3314
                        } elseif ($optionValue === false) {
3315
                            $printValue = 'false';
3316
                        }
3317
                        break;
3318
                    case 'string':
3319
                        $printValue = '"' . $optionValue . '"';
3320
                        break;
3321
                    case 'array':
3322
                        $printValue = implode(', ', $optionValue);
3323
                        break;
3324
                    case 'integer':
3325
                        $printValue = $optionValue;
3326
                        break;
3327
                    default:
3328
                        $printValue = $optionValue;
3329
                }
3330
            }
3331
            $optionsForPrint[] = $optionName . ': ' . $printValue;
3332
        }
3333
        return implode($glue, $optionsForPrint);
3334
    }
3335
3336
    public static function convertAndReport($source, $destination, $options)
3337
    {
3338
        ?>
3339
<html>
3340
    <head>
3341
        <style>td {vertical-align: top} table {color: #666}</style>
3342
        <script>
3343
            function showOptions(elToHide) {
3344
                document.getElementById('options').style.display='block';
3345
                elToHide.style.display='none';
3346
            }
3347
        </script>
3348
    </head>
3349
    <body>
3350
        <table>
3351
            <tr><td><i>source:</i></td><td><?php echo $source ?></td></tr>
3352
            <tr><td><i>destination:</i></td><td><?php echo $destination ?><td></tr>
3353
            <tr>
3354
                <td><i>options:</i></td>
3355
                <td>
3356
                    <i style="text-decoration:underline;cursor:pointer" onclick="showOptions(this)">click to see</i>
3357
                    <pre id="options" style="display:none"><?php
3358
                        echo print_r(self::getPrintableOptionsAsString($options, '<br>'), true);
3359
                    ?></pre>
3360
                    <?php //echo json_encode(self::getPrintableOptions($options)); ?>
3361
                    <?php //echo print_r(self::getPrintableOptions($options), true); ?>
3362
                </td>
3363
            </tr>
3364
        </table>
3365
        <br>
3366
        <?php
3367
        // TODO:
3368
        // We could display warning if unknown options are set
3369
        // but that requires that WebPConvert also describes its general options
3370
3371
        try {
3372
            $echoLogger = new EchoLogger();
3373
            WebPConvert::convert($source, $destination, $options, $echoLogger);
3374
        } catch (\Exception $e) {
3375
            $success = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $success is dead and can be removed.
Loading history...
3376
3377
            $msg = $e->getMessage();
3378
3379
            echo '<b>' . $msg . '</b>';
3380
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
3381
        }
3382
        ?>
3383
    </body>
3384
    </html>
3385
        <?php
3386
    }
3387
}
3388
3389
?><?php
3390
namespace WebPConvert\Serve;
3391
3392
//use WebPConvert\Serve\Report;
3393
3394
class ServeBase
3395
{
3396
    public $source;
3397
    public $destination;
3398
    public $options;
3399
3400
    // These two fellows are first set when decideWhatToServe is called
3401
    // However, if it is decided to serve a fresh conversion, they might get modified.
3402
    // If that for example results in a file larger than source, $whatToServe will change
3403
    // from 'fresh-conversion' to 'original', and $whyServingThis will change to 'source-lighter'
3404
    public $whatToServe = '';
3405
    public $whyServingThis = '';
3406
3407
    public function __construct($source, $destination, $options)
3408
    {
3409
3410
        $this->source = $source;
3411
        $this->destination = $destination;
3412
        $this->options = array_merge(self::$defaultOptions, $options);
3413
3414
        $this->setErrorReporting();
3415
    }
3416
3417
    public static $defaultOptions = [
3418
        'add-content-type-header' => true,
3419
        'add-last-modified-header' => true,
3420
        'add-vary-header' => true,
3421
        'add-x-header-status' => true,
3422
        'add-x-header-options' => false,
3423
        'aboutToServeImageCallBack' => null,
3424
        'aboutToPerformFailAction' => null,
3425
        'cache-control-header' => 'public, max-age=86400',
3426
        'converters' =>  ['cwebp', 'gd', 'imagick'],
3427
        'error-reporting' => 'auto',
3428
        'fail' => 'original',
3429
        'fail-when-original-unavailable' => '404',
3430
        'reconvert' => false,
3431
        'serve-original' => false,
3432
        'show-report' => false,
3433
    ];
3434
3435
    protected function setErrorReporting()
3436
    {
3437
        if (($this->options['error-reporting'] === true) ||
3438
            (($this->options['error-reporting'] === 'auto') && ($this->options['show-report'] === true))
3439
        ) {
3440
            error_reporting(E_ALL);
3441
            ini_set('display_errors', 'On');
3442
        } elseif (($this->options['error-reporting'] === false) ||
3443
            (($this->options['error-reporting'] === 'auto') && ($this->options['show-report'] === false))
3444
        ) {
3445
            error_reporting(0);
3446
            ini_set('display_errors', 'Off');
3447
        }
3448
    }
3449
3450
    protected function header($header, $replace = true)
3451
    {
3452
        header($header, $replace);
3453
    }
3454
3455
    public function addXStatusHeader($text)
3456
    {
3457
        if ($this->options['add-x-header-status']) {
3458
            $this->header('X-WebP-Convert-Status: ' . $text, true);
3459
        }
3460
    }
3461
3462
    public function addVaryHeader()
3463
    {
3464
        if ($this->options['add-vary-header']) {
3465
            $this->header('Vary: Accept');
3466
        }
3467
    }
3468
3469
    public function addContentTypeHeader($cType)
3470
    {
3471
        if ($this->options['add-content-type-header']) {
3472
            $this->header('Content-type: ' . $cType);
3473
        }
3474
    }
3475
3476
    /* $timestamp  Unix timestamp */
3477
    public function addLastModifiedHeader($timestamp)
3478
    {
3479
        if ($this->options['add-last-modified-header']) {
3480
            $this->header("Last-Modified: " . gmdate("D, d M Y H:i:s", $timestamp) ." GMT", true);
3481
        }
3482
    }
3483
3484
    public function addCacheControlHeader()
3485
    {
3486
        if (!empty($this->options['cache-control-header'])) {
3487
            $this->header('Cache-Control: ' . $this->options['cache-control-header'], true);
3488
        }
3489
    }
3490
3491
    public function serveExisting()
3492
    {
3493
        if (!$this->callAboutToServeImageCallBack('destination')) {
3494
            return;
3495
        }
3496
3497
        $this->addXStatusHeader('Serving existing converted image');
3498
        $this->addVaryHeader();
3499
        $this->addContentTypeHeader('image/webp');
3500
        $this->addCacheControlHeader();
3501
        $this->addLastModifiedHeader(@filemtime($this->destination));
3502
3503
        if (@readfile($this->destination) === false) {
3504
            $this->header('X-WebP-Convert-Error: Could not read file');
3505
            return false;
3506
        }
3507
        return true;
3508
    }
3509
3510
    /**
3511
     *   Called immidiately before serving image (either original, already converted or fresh)
3512
     *   $whatToServe can be 'source' | 'destination' | 'fresh-conversion'
3513
     *   $whyServingThis can be:
3514
     *   for 'source':
3515
     *       - "explicitly-told-to"     (when the "original" option is set)
3516
     *       - "source-lighter"         (when original image is actually smaller than the converted)
3517
     *   for 'fresh-conversion':
3518
     *       - "explicitly-told-to"     (when the "reconvert" option is set)
3519
     *       - "source-modified"        (when source is newer than existing)
3520
     *       - "no-existing"            (when there is no existing at the destination)
3521
     *   for 'destination':
3522
     *       - "no-reason-not-to"       (it is lighter than source, its not older,
3523
     *                                   and we were not told to do otherwise)
3524
     */
3525
    protected function callAboutToServeImageCallBack($whatToServe)
3526
    {
3527
        if (!isset($this->options['aboutToServeImageCallBack'])) {
3528
            return true;
3529
        }
3530
        $result = call_user_func(
3531
            $this->options['aboutToServeImageCallBack'],
3532
            $whatToServe,
3533
            $this->whyServingThis,
3534
            $this
3535
        );
3536
        return ($result !== false);
3537
    }
3538
3539
    /**
3540
     *  Decides what to serve.
3541
     *  Returns array. First item is what to do, second is additional info.
3542
     *  First item can be one of these:
3543
     *  - "destination"  (serve existing converted image at the destination path)
3544
     *       - "no-reason-not-to"
3545
     *  - "source"
3546
     *       - "explicitly-told-to"
3547
     *       - "source-lighter"
3548
     *  - "fresh-conversion" (note: this may still fail)
3549
     *       - "explicitly-told-to"
3550
     *       - "source-modified"
3551
     *       - "no-existing"
3552
     *  - "fail"
3553
     *        - "Missing destination argument"
3554
     *  - "critical-fail"   (a failure where the source file cannot be served)
3555
     *        - "Missing source argument"
3556
     *        - "Source file was not found!"
3557
     *  - "report"
3558
     */
3559
    public function decideWhatToServe()
3560
    {
3561
        $decisionArr = $this->doDecideWhatToServe();
3562
        $this->whatToServe = $decisionArr[0];
3563
        $this->whyServingThis = $decisionArr[1];
3564
    }
3565
3566
    private function doDecideWhatToServe()
3567
    {
3568
        if (empty($this->source)) {
3569
            return ['critical-fail', 'Missing source argument'];
3570
        }
3571
        if (@!file_exists($this->source)) {
3572
            return ['critical-fail', 'Source file was not found!'];
3573
        }
3574
        if (empty($this->destination)) {
3575
            return ['fail', 'Missing destination argument'];
3576
        }
3577
        if ($this->options['show-report']) {
3578
            return ['report', ''];
3579
        }
3580
        if ($this->options['serve-original']) {
3581
            return ['source', 'explicitly-told-to'];
3582
        }
3583
        if ($this->options['reconvert']) {
3584
            return ['fresh-conversion', 'explicitly-told-to'];
3585
        }
3586
3587
        if (@file_exists($this->destination)) {
3588
            // Reconvert if source file is newer than destination
3589
            $timestampSource = @filemtime($this->source);
3590
            $timestampDestination = @filemtime($this->destination);
3591
            if (($timestampSource !== false) &&
3592
                ($timestampDestination !== false) &&
3593
                ($timestampSource > $timestampDestination)) {
3594
                return ['fresh-conversion', 'source-modified'];
3595
            }
3596
3597
            // Serve source if it is smaller than destination
3598
            $filesizeDestination = @filesize($this->destination);
3599
            $filesizeSource = @filesize($this->source);
3600
            if (($filesizeSource !== false) &&
3601
                ($filesizeDestination !== false) &&
3602
                ($filesizeDestination > $filesizeSource)) {
3603
                return ['source', 'source-lighter'];
3604
            }
3605
3606
            // Destination exists, and there is no reason left not to serve it
3607
            return ['destination', 'no-reason-not-to'];
3608
        } else {
3609
            return ['fresh-conversion', 'no-existing'];
3610
        }
3611
    }
3612
}
3613
3614
?><?php
3615
namespace WebPConvert\Serve;
3616
3617
use WebPConvert\WebPConvert;
3618
use WebPConvert\Convert\Exceptions\ConversionFailedException;
3619
use WebPConvert\Convert\Exceptions\ConversionFailed\ConversionDeclinedException;
3620
use WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblems\CreateDestinationFileException;
3621
use WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblems\CreateDestinationFolderException;
3622
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\ConverterNotFoundException;
3623
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\InvalidImageTypeException;
3624
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\TargetNotFoundException;
3625
3626
use WebPConvert\Loggers\BufferLogger;
3627
use WebPConvert\Serve\Report;
3628
3629
/**
3630
 * This class must serves a converted image (either a fresh convertion, the destionation, or
3631
 * the original). Upon failure, the fail action given in the options will be exectuted
3632
 */
3633
class ServeConverted extends ServeBase
3634
{
3635
3636
    /*
3637
    Not used, currently...
3638
    private function addXOptionsHeader()
3639
    {
3640
        if ($this->options['add-x-header-options']) {
3641
            $this->header('X-WebP-Convert-Options:' . Report::getPrintableOptionsAsString($this->options));
3642
        }
3643
    }
3644
    */
3645
3646
    private function addHeadersPreventingCaching()
3647
    {
3648
        $this->header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
3649
        $this->header("Cache-Control: post-check=0, pre-check=0", false);
3650
        $this->header("Pragma: no-cache");
3651
    }
3652
3653
    public function serve404()
3654
    {
3655
        $protocol = isset($_SERVER["SERVER_PROTOCOL"]) ? $_SERVER["SERVER_PROTOCOL"] : 'HTTP/1.0';
3656
        $this->header($protocol . " 404 Not Found");
3657
    }
3658
3659
    public function serveOriginal()
3660
    {
3661
        if (!$this->callAboutToServeImageCallBack('source')) {
3662
            return true;    // we shall not trigger the fail callback
3663
        }
3664
3665
        if ($this->options['add-content-type-header']) {
3666
            $arr = explode('.', $this->source);
3667
            $ext = array_pop($arr);
3668
            switch (strtolower($ext)) {
3669
                case 'jpg':
3670
                case 'jpeg':
3671
                    $this->header('Content-type: image/jpeg');
3672
                    break;
3673
                case 'png':
3674
                    $this->header('Content-type: image/png');
3675
                    break;
3676
            }
3677
        }
3678
3679
        $this->addVaryHeader();
3680
3681
        switch ($this->whyServingThis) {
3682
            case 'source-lighter':
3683
            case 'explicitly-told-to':
3684
                $this->addCacheControlHeader();
3685
                $this->addLastModifiedHeader(@filemtime($this->source));
3686
                break;
3687
            default:
3688
                $this->addHeadersPreventingCaching();
3689
        }
3690
3691
        if (@readfile($this->source) === false) {
3692
            $this->header('X-WebP-Convert: Could not read file');
3693
            return false;
3694
        }
3695
        return true;
3696
    }
3697
3698
    public function serveFreshlyConverted()
3699
    {
3700
3701
        $criticalFail = false;
3702
        $bufferLogger = new BufferLogger();
3703
3704
        try {
3705
            WebPConvert::convert($this->source, $this->destination, $this->options, $bufferLogger);
0 ignored issues
show
Bug introduced by
$this->options of type array is incompatible with the type object expected by parameter $options of WebPConvert\WebPConvert::convert(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3705
            WebPConvert::convert($this->source, $this->destination, /** @scrutinizer ignore-type */ $this->options, $bufferLogger);
Loading history...
3706
3707
            // We are here, so it was successful :)
3708
3709
            // Serve source if it is smaller than destination
3710
            $filesizeDestination = @filesize($this->destination);
3711
            $filesizeSource = @filesize($this->source);
3712
            if (($filesizeSource !== false) &&
3713
                ($filesizeDestination !== false) &&
3714
                ($filesizeDestination > $filesizeSource)) {
3715
                $this->whatToServe = 'original';
3716
                $this->whyServingThis = 'source-lighter';
3717
                return $this->serveOriginal();
3718
            }
3719
3720
            if (!$this->callAboutToServeImageCallBack('fresh-conversion')) {
3721
                return;
3722
            }
3723
            if ($this->options['add-content-type-header']) {
3724
                $this->header('Content-type: image/webp');
3725
            }
3726
            if ($this->whyServingThis == 'explicitly-told-to') {
3727
                $this->addXStatusHeader(
3728
                    'Serving freshly converted image (was explicitly told to reconvert)'
3729
                );
3730
            } elseif ($this->whyServingThis == 'source-modified') {
3731
                $this->addXStatusHeader(
3732
                    'Serving freshly converted image (the original had changed)'
3733
                );
3734
            } elseif ($this->whyServingThis == 'no-existing') {
3735
                $this->addXStatusHeader(
3736
                    'Serving freshly converted image (there were no existing to serve)'
3737
                );
3738
            } else {
3739
                $this->addXStatusHeader(
3740
                    'Serving freshly converted image (dont know why!)'
3741
                );
3742
            }
3743
3744
            if ($this->options['add-vary-header']) {
3745
                $this->header('Vary: Accept');
3746
            }
3747
3748
            if ($this->whyServingThis == 'no-existing') {
3749
                $this->addCacheControlHeader();
3750
            } else {
3751
                $this->addHeadersPreventingCaching();
3752
            }
3753
            $this->addLastModifiedHeader(time());
3754
3755
            // Should we add Content-Length header?
3756
            // $this->header('Content-Length: ' . filesize($file));
3757
            if (@readfile($this->destination)) {
3758
                return true;
3759
            } else {
3760
                $this->fail('Error', 'could not read the freshly converted file');
3761
                return false;
3762
            }
3763
        } catch (InvalidImageTypeException $e) {
3764
            $criticalFail = true;
3765
            $description = 'Invalid file extension';
3766
            $msg = $e->getMessage();
0 ignored issues
show
Unused Code introduced by
The assignment to $msg is dead and can be removed.
Loading history...
3767
        } catch (TargetNotFoundException $e) {
3768
            $criticalFail = true;
3769
            $description = 'Source file not found';
3770
            $msg = $e->getMessage();
3771
        } catch (ConversionFailedException $e) {
3772
            // No converters could convert the image. At least one converter failed, even though it appears to be
3773
            // operational
3774
            $description = 'No converters could convert the image';
3775
            $msg = $e->getMessage();
3776
        } catch (ConversionDeclinedException $e) {
3777
            // (no converters could convert the image. At least one converter declined
3778
            $description = 'No converters could/wanted to convert the image';
3779
            $msg = $e->getMessage();
3780
        } catch (ConverterNotFoundException $e) {
3781
            $description = 'A converter was not found!';
3782
            $msg = $e->getMessage();
3783
        } catch (CreateDestinationFileException $e) {
3784
            $description = 'Cannot create destination file';
3785
            $msg = $e->getMessage();
3786
        } catch (CreateDestinationFolderException $e) {
3787
            $description = 'Cannot create destination folder';
3788
            $msg = $e->getMessage();
3789
        } catch (\Exception $e) {
3790
            $description = 'An unanticipated exception was thrown';
3791
            $msg = $e->getMessage();
3792
        }
3793
3794
        // Next line is commented out, because we need to be absolute sure that the details does not violate syntax
3795
        // We could either try to filter it, or we could change WebPConvert, such that it only provides safe texts.
3796
        // $this->header('X-WebP-Convert-And-Serve-Details: ' . $bufferLogger->getText());
3797
3798
        $this->fail('Conversion failed', $description, $criticalFail);
3799
        return false;
3800
        //echo '<p>This is how conversion process went:</p>' . $bufferLogger->getHtml();
3801
    }
3802
3803
    protected function serveErrorMessageImage($msg)
3804
    {
3805
        // Generate image containing error message
3806
        if ($this->options['add-content-type-header']) {
3807
            $this->header('Content-type: image/gif');
3808
        }
3809
3810
        try {
3811
            if (function_exists('imagecreatetruecolor') &&
3812
                function_exists('imagestring') &&
3813
                function_exists('imagecolorallocate') &&
3814
                function_exists('imagegif')
3815
            ) {
3816
                $image = imagecreatetruecolor(620, 200);
3817
                if ($image !== false) {
3818
                    imagestring($image, 1, 5, 5, $msg, imagecolorallocate($image, 233, 214, 291));
3819
                    // echo imagewebp($image);
3820
                    echo imagegif($image);
3821
                    imagedestroy($image);
3822
                    return;
3823
                }
3824
            }
3825
        } catch (\Exception $e) {
3826
            //
3827
        }
3828
3829
        // Above failed.
3830
        // TODO: what to do?
3831
    }
3832
3833
    /**
3834
     *
3835
     * @return  void
3836
     */
3837
    protected function fail($title, $description, $critical = false)
3838
    {
3839
        $action = $critical ? $this->options['fail-when-original-unavailable'] : $this->options['fail'];
3840
3841
        if (isset($this->options['aboutToPerformFailActionCallback'])) {
3842
            if (call_user_func(
3843
                $this->options['aboutToPerformFailActionCallback'],
3844
                $title,
3845
                $description,
3846
                $action,
3847
                $this
3848
            ) === false) {
3849
                return;
3850
            }
3851
        }
3852
3853
        $this->addXStatusHeader('Failed (' . $description . ')');
3854
3855
        $this->addHeadersPreventingCaching();
3856
3857
        $title = 'Conversion failed';
3858
        switch ($action) {
3859
            case 'serve-original':
3860
                if (!$this->serveOriginal()) {
3861
                    $this->serve404();
3862
                };
3863
                break;
3864
            case '404':
3865
                $this->serve404();
3866
                break;
3867
            case 'report-as-image':
3868
                // todo: handle if this fails
3869
                self::serveErrorMessageImage($title . '. ' . $description);
0 ignored issues
show
Bug Best Practice introduced by
The method WebPConvert\Serve\ServeC...erveErrorMessageImage() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3869
                self::/** @scrutinizer ignore-call */ 
3870
                      serveErrorMessageImage($title . '. ' . $description);
Loading history...
3870
                break;
3871
            case 'report':
3872
                echo '<h1>' . $title . '</h1>' . $description;
3873
                break;
3874
        }
3875
    }
3876
3877
    /**
3878
     *
3879
     * @return  void
3880
     */
3881
    protected function criticalFail($title, $description)
3882
    {
3883
        $this->fail($title, $description, true);
3884
    }
3885
3886
    /**
3887
     *  Serve the thing specified in $whatToServe and $whyServingThis
3888
     *  These are first set my the decideWhatToServe() method, but may later change, if a fresh
3889
     *  conversion is made
3890
     */
3891
    public function serve()
3892
    {
3893
3894
        //$this->addXOptionsHeader();
3895
3896
        switch ($this->whatToServe) {
3897
            case 'destination':
3898
                return $this->serveExisting();
3899
            case 'source':
3900
                if ($this->whyServingThis == 'explicitly-told-to') {
3901
                    $this->addXStatusHeader(
3902
                        'Serving original image (was explicitly told to)'
3903
                    );
3904
                } else {
3905
                    $this->addXStatusHeader(
3906
                        'Serving original image (it is smaller than the already converted)'
3907
                    );
3908
                }
3909
                if (!$this->serveOriginal()) {
3910
                    $this->criticalFail('Error', 'could not serve original');
3911
                    return false;
3912
                }
3913
                return true;
3914
            case 'fresh-conversion':
3915
                return $this->serveFreshlyConverted();
3916
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
3917
            case 'critical-fail':
3918
                $this->criticalFail('Error', $this->whyServingThis);
3919
                return false;
3920
            case 'fail':
3921
                $this->fail('Error', $this->whyServingThis);
3922
                return false;
3923
            case 'report':
3924
                $this->addXStatusHeader('Reporting...');
3925
                Report::convertAndReport($this->source, $this->destination, $this->options);
3926
                return true;  // yeah, lets say that a report is always a success, even if conversion is a failure
3927
        }
3928
    }
3929
3930
    public function decideWhatToServeAndServeIt()
3931
    {
3932
        $this->decideWhatToServe();
3933
        return $this->serve();
3934
    }
3935
3936
    /**
3937
     * Main method
3938
     */
3939
    public static function serveConverted($source, $destination, $options)
3940
    {
3941
        if (isset($options['fail']) && ($options['fail'] == 'original')) {
3942
            $options['fail'] = 'serve-original';
3943
        }
3944
        // For backward compatability:
3945
        if (isset($options['critical-fail']) && !isset($options['fail-when-original-unavailable'])) {
3946
            $options['fail-when-original-unavailable'] = $options['critical-fail'];
3947
        }
3948
3949
        $cs = new static($source, $destination, $options);
3950
3951
        return $cs->decideWhatToServeAndServeIt();
3952
    }
3953
}
3954
3955
?><?php
3956
namespace WebPConvert\Serve;
3957
3958
use WebPConvert\Serve\ServeBase;
3959
use WebPConvert\Serve\ServeConverted;
3960
3961
/**
3962
 * This class must determine if an existing converted image can and should be served.
3963
 * If so, it must serve it.
3964
 * If not, it must hand the task over to ConvertAndServe
3965
 *
3966
 * The reason for doing it like this is that we want existing images to be served as fast as
3967
 * possible, because that is the thing that will happen most of the time.
3968
 *
3969
 * Anything else, such as error handling and creating new conversion is handed off
3970
 * (and only autoloaded when needed)
3971
 */
3972
3973
class ServeExistingOrHandOver extends ServeBase
3974
{
3975
3976
    /**
3977
     * Main method
3978
     */
3979
    public static function serveConverted($source, $destination, $options)
3980
    {
3981
        $server = new ServeExistingOrHandOver($source, $destination, $options);
3982
3983
        $server->decideWhatToServe();
3984
        if ($server->whatToServe == 'destination') {
3985
            return $server->serveExisting();
3986
        } else {
3987
            // Load extra php classes, if told to
3988
            if (isset($options['require-for-conversion'])) {
3989
                require($options['require-for-conversion']);
3990
            }
3991
            ServeConverted::serveConverted($source, $destination, $options);
3992
        }
3993
    }
3994
}
3995
3996
?><?php
3997
3998
namespace ImageMimeTypeGuesser\Detectors;
3999
4000
abstract class AbstractDetector
4001
{
4002
4003
    abstract protected function doDetect($filePath);
4004
4005
    public static function createInstance()
4006
    {
4007
        return new static();
4008
    }
4009
4010
    /**
4011
     *
4012
     *
4013
     *  Like all detectors, it returns:
4014
     *  - null  (if it cannot be determined)
4015
     *  - false (if it can be determined that this is not an image)
4016
     *  - mime  (if it is in fact an image, and type could be determined)
4017
     *  @return  mime | null | false.
0 ignored issues
show
Documentation Bug introduced by
The doc comment mime | null | false. at position 4 could not be parsed: Unknown type name 'false.' at position 4 in mime | null | false..
Loading history...
4018
     */
4019
    public static function detect($filePath)
4020
    {
4021
        if (!@file_exists($filePath)) {
4022
            return false;
4023
        }
4024
        return self::createInstance()->doDetect($filePath);
4025
    }
4026
}
4027
4028
?><?php
4029
4030
/**
4031
 * ImageMimeTypeGuesser - Detect / guess mime type of an image
4032
 *
4033
 * @link https://github.com/rosell-dk/image-mime-type-guesser
4034
 * @license MIT
4035
 */
4036
4037
namespace ImageMimeTypeGuesser;
4038
4039
class GuessFromExtension
4040
{
4041
4042
4043
    /**
4044
     *  Make a wild guess based on file extension
4045
     *  - and I mean wild!
4046
     *
4047
     *  Only most popular image types are recognized.
4048
     *  Many are not. See this list: https://www.iana.org/assignments/media-types/media-types.xhtml
4049
     *                - and the constants here: https://secure.php.net/manual/en/function.exif-imagetype.php
4050
     *  TODO: jp2, jpx, ...
4051
     */
4052
    public static function guess($filePath)
4053
    {
4054
        $fileExtension = pathinfo($filePath, PATHINFO_EXTENSION);
4055
        $fileExtension = strtolower($fileExtension);
4056
4057
        switch ($fileExtension) {
4058
            case 'bmp':
4059
            case 'gif':
4060
            case 'jpeg':
4061
            case 'png':
4062
            case 'tiff':
4063
            case 'webp':
4064
                return 'image/' . $fileExtension;
4065
4066
            case 'ico':
4067
                return 'image/vnd.microsoft.icon';      // or perhaps 'x-icon' ?
4068
4069
            case 'jpg':
4070
                return 'image/jpeg';
4071
4072
            case 'svg':
4073
                return 'image/svg+xml';
4074
4075
            case 'tif':
4076
                return 'image/tiff';
4077
        }
4078
        return false;
4079
    }
4080
}
4081
4082
?><?php
4083
4084
/**
4085
 * ImageMimeTypeGuesser - Detect / guess mime type of an image
4086
 *
4087
 * @link https://github.com/rosell-dk/image-mime-type-guesser
4088
 * @license MIT
4089
 */
4090
4091
namespace ImageMimeTypeGuesser;
4092
4093
use \ImageMimeTypeGuesser\Detectors\Stack;
4094
4095
class ImageMimeTypeGuesser
4096
{
4097
4098
4099
    /**
4100
     *  Try to detect mime type of image using "stack" detector (all available methods, until one succeeds)
4101
     *
4102
     *  returns:
4103
     *  - null  (if it cannot be determined)
4104
     *  - false (if it is not an image that the server knows about)
4105
     *  - mime  (if it is in fact an image, and type could be determined)
4106
     *  @return  mime type | null | false.
0 ignored issues
show
Bug introduced by
The type ImageMimeTypeGuesser\mime was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
4107
     */
4108
    public static function detect($filePath)
4109
    {
4110
        // Result of the discussion here:
4111
        // https://github.com/rosell-dk/webp-convert/issues/98
4112
4113
        return Stack::detect($filePath);
0 ignored issues
show
Bug Best Practice introduced by
The expression return ImageMimeTypeGues...tack::detect($filePath) could also return false which is incompatible with the documented return type ImageMimeTypeGuesser\mime. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
4114
    }
4115
4116
    /**
4117
     *  Try to detect mime type of image using "stack" detector (all available methods, until one succeeds)
4118
     *  If that fails, fall back to wild west guessing based solely on file extension, which always has an answer
4119
     *  (this method never returns null)
4120
     *
4121
     *  returns:
4122
     *  - false (if it is not an image that the server knows about)
4123
     *  - mime  (if it looks like an image)
4124
     *  @return  mime type | false.
4125
     */
4126
    public static function guess($filePath)
4127
    {
4128
        $detectionResult = self::detect($filePath);
4129
        if (!is_null($detectionResult)) {
4130
            return $detectionResult;
4131
        }
4132
4133
        // fall back to the wild west method
4134
        return GuessFromExtension::guess($filePath);
0 ignored issues
show
Bug Best Practice introduced by
The expression return ImageMimeTypeGues...nsion::guess($filePath) returns the type false|string which is incompatible with the documented return type ImageMimeTypeGuesser\mime.
Loading history...
4135
    }
4136
4137
    /**
4138
     *  Try to detect mime type of image using "stack" detector (all available methods, until one succeeds)
4139
     *  But do not take no for an answer, as "no", really only means that the server has not registred that mime type
4140
     *
4141
     *  (this method never returns null)
4142
     *
4143
     *  returns:
4144
     *  - false (if it can be determined that this is not an image)
4145
     *  - mime  (if it looks like an image)
4146
     *  @return  mime type | false.
4147
     */
4148
    public static function lenientGuess($filePath)
4149
    {
4150
        $detectResult = self::detect($filePath);
4151
        if ($detectResult === false) {
0 ignored issues
show
introduced by
The condition $detectResult === false is always false.
Loading history...
4152
            // The server does not recognize this image type.
4153
            // - but perhaps it is because it does not know about this image type.
4154
            // - so we turn to mapping the file extension
4155
            return GuessFromExtension::guess($filePath);
4156
        } elseif (is_null($detectResult)) {
4157
            // the mime type could not be determined
4158
            // perhaps we also in this case want to turn to mapping the file extension
4159
            return GuessFromExtension::guess($filePath);
0 ignored issues
show
Bug Best Practice introduced by
The expression return ImageMimeTypeGues...nsion::guess($filePath) returns the type false|string which is incompatible with the documented return type ImageMimeTypeGuesser\mime.
Loading history...
4160
        }
4161
        return $detectResult;
4162
    }
4163
4164
4165
4166
    public static function guessIsIn($filePath, $mimeTypes)
4167
    {
4168
        return in_array(self::guess($filePath), $mimeTypes);
4169
    }
4170
4171
    public static function detectIsIn($filePath, $mimeTypes)
4172
    {
4173
        return in_array(self::detect($filePath), $mimeTypes);
4174
    }
4175
4176
    public static function lenientGuessIsIn($filePath, $mimeTypes)
4177
    {
4178
        return in_array(self::lenientGuess($filePath), $mimeTypes);
4179
    }
4180
}
4181
4182
?><?php
4183
4184
namespace ImageMimeTypeGuesser\Detectors;
4185
4186
use \ImageMimeTypeGuesser\Detectors\AbstractDetector;
4187
4188
class ExifImageType extends AbstractDetector
4189
{
4190
4191
    /**
4192
     *  Try to detect mime type of image using "exif_imagetype"
4193
     *
4194
     *  Like all detectors, it returns:
4195
     *  - null  (if it cannot be determined)
4196
     *  - false (if it can be determined that this is not an image)
4197
     *  - mime  (if it is in fact an image, and type could be determined)
4198
     *  @return  mime | null | false.
0 ignored issues
show
Documentation Bug introduced by
The doc comment mime | null | false. at position 4 could not be parsed: Unknown type name 'false.' at position 4 in mime | null | false..
Loading history...
4199
     */
4200
    protected function doDetect($filePath)
4201
    {
4202
        // exif_imagetype is fast, however not available on all systems,
4203
        // It may return false. In that case we can rely on that the file is not an image (and return false)
4204
        if (function_exists('exif_imagetype')) {
4205
            try {
4206
                $imageType = exif_imagetype($filePath);
4207
                return ($imageType ? image_type_to_mime_type($imageType) : false);
4208
            } catch (\Exception $e) {
4209
                // Might for example get "Read error!"
4210
                // well well, don't let this stop us
4211
                //echo $e->getMessage();
4212
//                throw($e);
4213
            }
4214
        }
4215
        return;
4216
    }
4217
}
4218
4219
?><?php
4220
4221
namespace ImageMimeTypeGuesser\Detectors;
4222
4223
class FInfo extends AbstractDetector
4224
{
4225
4226
    /**
4227
     *  Try to detect mime type of image using finfo
4228
     *
4229
     *  Like all detectors, it returns:
4230
     *  - null  (if it cannot be determined)
4231
     *  - false (if it can be determined that this is not an image)
4232
     *  - mime  (if it is in fact an image, and type could be determined)
4233
     *  @return  mime | null | false.
0 ignored issues
show
Documentation Bug introduced by
The doc comment mime | null | false. at position 4 could not be parsed: Unknown type name 'false.' at position 4 in mime | null | false..
Loading history...
4234
     */
4235
    protected function doDetect($filePath)
4236
    {
4237
4238
        if (class_exists('finfo')) {
4239
            // phpcs:ignore PHPCompatibility.PHP.NewClasses.finfoFound
4240
            $finfo = new \finfo(FILEINFO_MIME);
4241
            $mime = explode('; ', $finfo->file($filePath));
4242
            $result = $mime[0];
4243
4244
            if (strpos($result, 'image/') === 0) {
4245
                return $result;
4246
            } else {
4247
                return false;
4248
            }
4249
4250
            return $type;
0 ignored issues
show
Unused Code introduced by
return $type is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
4251
        }
4252
    }
4253
}
4254
4255
?><?php
4256
4257
namespace ImageMimeTypeGuesser\Detectors;
4258
4259
class GetImageSize extends AbstractDetector
4260
{
4261
4262
    /**
4263
     *  Try to detect mime type of image using "getimagesize"
4264
     *
4265
     *  Like all detectors, it returns:
4266
     *  - null  (if it cannot be determined)
4267
     *  - false (if it can be determined that this is not an image)
4268
     *  - mime  (if it is in fact an image, and type could be determined)
4269
     *  @return  mime | null | false.
0 ignored issues
show
Documentation Bug introduced by
The doc comment mime | null | false. at position 4 could not be parsed: Unknown type name 'false.' at position 4 in mime | null | false..
Loading history...
4270
     */
4271
    protected function doDetect($filePath)
4272
    {
4273
        // getimagesize is slower than exif_imagetype
4274
        // It may not return "mime". In that case we can rely on that the file is not an image (and return false)
4275
        if (function_exists('getimagesize')) {
4276
            try {
4277
                $imageSize = getimagesize($filePath);
4278
                return (isset($imageSize['mime']) ? $imageSize['mime'] : false);
4279
            } catch (\Exception $e) {
4280
                // well well, don't let this stop us either
4281
            }
4282
        }
4283
    }
4284
}
4285
4286
?><?php
4287
4288
namespace ImageMimeTypeGuesser\Detectors;
4289
4290
class MimeContentType extends AbstractDetector
4291
{
4292
4293
    /**
4294
     *  Try to detect mime type of image using "mime_content_type"
4295
     *
4296
     *  Like all detectors, it returns:
4297
     *  - null  (if it cannot be determined)
4298
     *  - false (if it can be determined that this is not an image)
4299
     *  - mime  (if it is in fact an image, and type could be determined)
4300
     *  @return  mime | null | false.
0 ignored issues
show
Documentation Bug introduced by
The doc comment mime | null | false. at position 4 could not be parsed: Unknown type name 'false.' at position 4 in mime | null | false..
Loading history...
4301
     */
4302
    protected function doDetect($filePath)
4303
    {
4304
        // mime_content_type supposedly used to be deprecated, but it seems it isn't anymore
4305
        // it may return false on failure.
4306
        if (function_exists('mime_content_type')) {
4307
            try {
4308
                $result = mime_content_type($filePath);
4309
                if ($result !== false) {
4310
                    if (strpos($result, 'image/') === 0) {
4311
                        return $result;
4312
                    } else {
4313
                        return false;
4314
                    }
4315
                }
4316
            } catch (\Exception $e) {
4317
                // we are unstoppable!
4318
            }
4319
        }
4320
    }
4321
}
4322
4323
?><?php
4324
4325
namespace ImageMimeTypeGuesser\Detectors;
4326
4327
class Stack extends AbstractDetector
4328
{
4329
4330
    /**
4331
     *  Try to detect mime type of image using all available detectors
4332
     *  returns:
4333
     *  - null  (if it cannot be determined)
4334
     *  - false (if it can be determined that this is not an image)
4335
     *  - mime  (if it is in fact an image, and type could be determined)
4336
     *  @return  mime | null | false.
0 ignored issues
show
Documentation Bug introduced by
The doc comment mime | null | false. at position 4 could not be parsed: Unknown type name 'false.' at position 4 in mime | null | false..
Loading history...
4337
     */
4338
    protected function doDetect($filePath)
4339
    {
4340
        $detectors = [
4341
            'ExifImageType',
4342
            'GetImageSize',
4343
            'FInfo',
4344
            'MimeContentType'
4345
        ];
4346
4347
        foreach ($detectors as $className) {
4348
            $result = call_user_func(
4349
                array("\\ImageMimeTypeGuesser\\Detectors\\" . $className, 'detect'),
4350
                $filePath
4351
            );
4352
            if (!is_null($result)) {
4353
                return $result;
4354
            }
4355
        }
4356
4357
        return;     // undetermined
4358
    }
4359
}
4360
4361