Completed
Push — master ( b29d6b...03436d )
by Bjørn
03:06
created

AbstractConverter::getConverterDisplayName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
// TODO:
4
// Read this: https://sourcemaking.com/design_patterns/strategy
5
6
namespace WebPConvert\Convert\BaseConverters;
7
8
use WebPConvert\Convert\Exceptions\ConversionFailedException;
9
use WebPConvert\Convert\Exceptions\ConversionFailed\UnhandledException;
10
use WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblems\CreateDestinationFileException;
11
use WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblems\CreateDestinationFolderException;
12
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\ConverterNotFoundException;
13
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\InvalidImageTypeException;
14
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\TargetNotFoundException;
15
use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
16
use WebPConvert\Convert\BaseConverters\BaseTraits\AutoQualityTrait;
17
use WebPConvert\Convert\BaseConverters\BaseTraits\LoggerTrait;
18
use WebPConvert\Convert\BaseConverters\BaseTraits\OptionsTrait;
19
use WebPConvert\Convert\BaseConverters\BaseTraits\WarningLoggerTrait;
20
use WebPConvert\Loggers\BaseLogger;
21
22
use ImageMimeTypeGuesser\ImageMimeTypeGuesser;
23
24
/**
25
 * Base for all converter classes.
26
 *
27
 * @package    WebPConvert
28
 * @author     Bjørn Rosell <[email protected]>
29
 * @since      Class available since Release 2.0.0
30
 */
31
abstract class AbstractConverter
32
{
33
    use AutoQualityTrait;
34
    use LoggerTrait;
35
    use OptionsTrait;
36
    use WarningLoggerTrait;
37
38
    /**
39
     * The actual conversion must be done by a concrete class.
40
     *
41
     * At the stage this method is called, the abstract converter has taken preparational steps.
42
     * - It has created the destination folder (if neccesary)
43
     * - It has checked the input (valid mime type)
44
     * - It has set up an error handler, mostly in order to catch and log warnings during the doConvert fase
45
     *
46
     * Note: This method is not meant to be called from the outside. Use the *convert* method for that.
47
     *
48
     */
49
    abstract protected function doActualConvert();
50
51
    /**
52
     *  Set to false for converters that should hand the lossless option over (stack and wpc)
53
     */
54
    protected $processLosslessAuto = true;
55
56
    /**
57
     *  Set this on all converters (unfortunately, properties cannot be declared abstract)
58
     */
59
    protected $supportsLossless = false;
60
61
62
    /** @var string  The filename of the image to convert (complete path) */
63
    protected $source;
64
65
    /** @var string  Where to save the webp (complete path) */
66
    protected $destination;
67
68
    /** @var string|false|null  Where to save the webp (complete path) */
69
    private $sourceMimeType;
70
71
    public static $allowedMimeTypes = ['image/jpeg', 'image/png'];
72
73
    /**
74
     * Check basis operationality
75
     *
76
     * Converters may override this method for the purpose of performing basic operationaly checks. It is for
77
     * running general operation checks for a conversion method.
78
     * If some requirement is not met, it should throw a ConverterNotOperationalException (or subtype)
79
     *
80
     * The method is called internally right before calling doActualConvert() method.
81
     * - It SHOULD take options into account when relevant. For example, a missing api key for a
82
     *   cloud converter should be detected here
83
     * - It should NOT take the actual filename into consideration, as the purpose is *general*
84
     *   For that pupose, converters should override checkConvertability
85
     *   Also note that doConvert method is allowed to throw ConverterNotOperationalException too.
86
     *
87
     * @return  void
88
     */
89 5
    public function checkOperationality()
90
    {
91 5
    }
92
93
    /**
94
     * Converters may override this for the purpose of performing checks on the concrete file.
95
     *
96
     * This can for example be used for rejecting big uploads in cloud converters or rejecting unsupported
97
     * image types.
98
     *
99
     * @return  void
100
     */
101 5
    public function checkConvertability()
102
    {
103 5
    }
104
105 33
    public function __construct($source, $destination, $options = [], $logger = null)
106
    {
107 33
        $this->source = $source;
108 33
        $this->destination = $destination;
109
110 33
        $this->setLogger($logger);
111 33
        $this->setProvidedOptions($options);
112 33
    }
113
114
    /**
115
     *  Default display name is simply the class name (short).
116
     *  Converters can override this.
117
     *
118
     * @return string  A display name, ie "Gd"
119
     */
120 1
    protected static function getConverterDisplayName()
121
    {
122
        // https://stackoverflow.com/questions/19901850/how-do-i-get-an-objects-unqualified-short-class-name/25308464
123 1
        return substr(strrchr('\\' . static::class, '\\'), 1);
124
    }
125
126
    /**
127
     * Create an instance of this class
128
     *
129
     * @param  string  $source       The path to the file to convert
130
     * @param  string  $destination  The path to save the converted file to
131
     * @param  array   $options      (optional)
132
     * @param  \WebPConvert\Loggers\BaseLogger   $logger       (optional)
133
     *
134
     * @return static
135
     */
136 22
    public static function createInstance($source, $destination, $options = [], $logger = null)
137
    {
138 22
        return new static($source, $destination, $options, $logger);
139
    }
140
141
    //$instance->logLn($instance->getConverterDisplayName() . ' converter ignited');
142
    //$instance->logLn(self::getConverterDisplayName() . ' converter ignited');
143
144 13
    public function doConvert()
145
    {
146 13
        $beginTime = microtime(true);
147
148 13
        $this->activateWarningLogger();
149
        //set_error_handler(array($this, "errorHandler"));
150
151
        try {
152
            // Prepare options
153
            //$this->prepareOptions();
154
155 13
            $this->checkOptions();
156
157
            // Prepare destination folder
158 13
            $this->createWritableDestinationFolder();
159 12
            $this->removeExistingDestinationIfExists();
160
161 12
            if (!isset($this->options['_skip_input_check'])) {
162
                // Run basic input validations (if source exists and if file extension is valid)
163 12
                $this->checkInput();
164
165
                // Check that a file can be written to destination
166 11
                $this->checkFileSystem();
167
            }
168
169 11
            $this->checkOperationality();
170 6
            $this->checkConvertability();
171 6
            $this->runActualConvert();
172 11
        } catch (ConversionFailedException $e) {
173 11
            $this->deactivateWarningLogger();
174 11
            throw $e;
175
        } catch (\Exception $e) {
176
            $this->deactivateWarningLogger();
177
            throw new UnhandledException('Conversion failed due to uncaught exception', 0, $e);
178
        } catch (\Error $e) {
179
            $this->deactivateWarningLogger();
180
            // https://stackoverflow.com/questions/7116995/is-it-possible-in-php-to-prevent-fatal-error-call-to-undefined-function
181
            //throw new UnhandledException('Conversion failed due to uncaught error', 0, $e);
182
            throw $e;
183
        }
184 3
        $this->deactivateWarningLogger();
185
186 3
        $source = $this->source;
187 3
        $destination = $this->destination;
188
189 3
        if (!@file_exists($destination)) {
190
            throw new ConversionFailedException('Destination file is not there: ' . $destination);
191 3
        } elseif (@filesize($destination) === 0) {
192
            unlink($destination);
193
            throw new ConversionFailedException('Destination file was completely empty');
194
        } else {
195 3
            if (!isset($this->options['_suppress_success_message'])) {
196 3
                $this->ln();
197
                $msg = 'Converted image in ' .
198 3
                    round((microtime(true) - $beginTime) * 1000) . ' ms';
199
200 3
                $sourceSize = @filesize($source);
201 3
                if ($sourceSize !== false) {
202
                    $msg .= ', reducing file size with ' .
203 3
                        round((filesize($source) - filesize($destination))/filesize($source) * 100) . '% ';
204
205 3
                    if ($sourceSize < 10000) {
206 1
                        $msg .= '(went from ' . round(filesize($source)) . ' bytes to ';
207 1
                        $msg .= round(filesize($destination)) . ' bytes)';
208
                    } else {
209 3
                        $msg .= '(went from ' . round(filesize($source)/1024) . ' kb to ';
210 3
                        $msg .= round(filesize($destination)/1024) . ' kb)';
211
                    }
212
                }
213 3
                $this->logLn($msg);
214
            }
215
        }
216 3
    }
217
218
219 6
    private function runActualConvert()
220
    {
221 6
        if ($this->processLosslessAuto && ($this->options['lossless'] === 'auto') && $this->supportsLossless) {
222
            $destination = $this->destination;
223
            $destinationLossless =  $this->destination . '.lossless.webp';
224
            $destinationLossy =  $this->destination . '.lossy.webp';
225
226
            $this->logLn(
227
                'Lossless is set to auto. Converting to both lossless and lossy and selecting the smallest file'
228
            );
229
230
231
            $this->ln();
232
            $this->logLn('Converting to lossy');
233
            $this->destination = $destinationLossy;
234
            $this->options['lossless'] = false;
235
            $this->doActualConvert();
236
            $this->logLn('Reduction: ' .
237
                round((filesize($this->source) - filesize($this->destination))/filesize($this->source) * 100) . '% ');
238
239
            $this->ln();
240
            $this->logLn('Converting to lossless');
241
            $this->destination = $destinationLossless;
242
            $this->options['lossless'] = true;
243
            $this->doActualConvert();
244
            $this->logLn('Reduction: ' .
245
                round((filesize($this->source) - filesize($this->destination))/filesize($this->source) * 100) . '% ');
246
247
            $this->ln();
248
            if (filesize($destinationLossless) > filesize($destinationLossy)) {
249
                $this->logLn('Picking lossy');
250
                unlink($destinationLossless);
251
                rename($destinationLossy, $destination);
252
            } else {
253
                $this->logLn('Picking lossless');
254
                unlink($destinationLossy);
255
                rename($destinationLossless, $destination);
256
            }
257
            $this->destination = $destination;
258
        } else {
259 6
            $this->doActualConvert();
260
        }
261 3
    }
262
263
    /**
264
     * Convert an image to webp.
265
     *
266
     * @param   string  $source              path to source file
267
     * @param   string  $destination         path to destination
268
     * @param   array   $options (optional)  options for conversion
269
     * @param   \WebPConvert\Loggers\BaseLogger $logger (optional)
270
     *
271
     * @throws  \WebPConvert\Convert\Exceptions\ConversionFailedException   in case conversion fails
272
     * @return  void
273
     */
274 13
    public static function convert($source, $destination, $options = [], $logger = null)
275
    {
276 13
        $instance = self::createInstance($source, $destination, $options, $logger);
277 13
        $instance->doConvert();
278
        //echo $instance->id;
279 3
    }
280
281
    /**
282
     * Get mime type for image (best guess).
283
     *
284
     * It falls back to using file extension. If that fails too, false is returned
285
     *
286
     * PS: Is it a security risk to fall back on file extension?
287
     * - By setting file extension to "jpg", one can lure our library into trying to convert a file, which isn't a jpg.
288
     * hmm, seems very unlikely, though not unthinkable that one of the converters could be exploited
289
     *
290
     * @return  string|false|null mimetype (if it is an image, and type could be determined / guessed),
291
     *    false (if it is not an image type that the server knowns about)
292
     *    or null (if nothing can be determined)
293
     */
294 33
    public function getMimeTypeOfSource()
295
    {
296 33
        if (!isset($this->sourceMimeType)) {
297 33
            $this->sourceMimeType = ImageMimeTypeGuesser::lenientGuess($this->source);
298
        }
299 33
        return $this->sourceMimeType;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->sourceMimeType also could return the type boolean which is incompatible with the documented return type false|null|string.
Loading history...
300
    }
301
302
    /**
303
     *  Note: As the input validations are only run one time in a stack,
304
     *  this method is not overridable
305
     */
306 12
    private function checkInput()
307
    {
308
        // Check if source exists
309 12
        if (!@file_exists($this->source)) {
310 5
            throw new TargetNotFoundException('File or directory not found: ' . $this->source);
311
        }
312
313
        // Check if the provided file's mime type is valid
314 11
        $fileMimeType = $this->getMimeTypeOfSource();
315 11
        if (is_null($fileMimeType)) {
316
            throw new InvalidImageTypeException('Image type could not be detected');
317 11
        } elseif ($fileMimeType === false) {
0 ignored issues
show
introduced by
The condition $fileMimeType === false is always false.
Loading history...
318
            throw new InvalidImageTypeException('File seems not to be an image.');
319 11
        } elseif (!in_array($fileMimeType, self::$allowedMimeTypes)) {
320
            throw new InvalidImageTypeException('Unsupported mime type: ' . $fileMimeType);
321
        }
322 11
    }
323
324 11
    private function checkFileSystem()
325
    {
326
        // TODO:
327
        // Instead of creating dummy file,
328
        // perhaps something like this ?
329
        // if (@is_writable($dirName) && @is_executable($dirName) || self::isWindows() )
330
        // Or actually, probably best with a mix.
331
        // First we test is_writable and is_executable. If that fails and we are on windows, we can do the dummy
332
        // function isWindows(){
333
        // return (boolean) preg_match('/^win/i', PHP_OS);
334
        //}
335
336
        // Try to create a dummy file here, with that name, just to see if it is possible (we delete it again)
337 11
        file_put_contents($this->destination, '');
338 11
        if (file_put_contents($this->destination, '') === false) {
339
            throw new CreateDestinationFileException(
340
                'Cannot create file: ' . basename($this->destination) . ' in dir:' . dirname($this->destination)
341
            );
342
        }
343 11
        unlink($this->destination);
344 11
    }
345
346 12
    private function removeExistingDestinationIfExists()
347
    {
348 12
        if (file_exists($this->destination)) {
349
            // A file already exists in this folder...
350
            // We delete it, to make way for a new webp
351 6
            if (!unlink($this->destination)) {
352
                throw new CreateDestinationFileException(
353
                    'Existing file cannot be removed: ' . basename($this->destination)
354
                );
355
            }
356
        }
357 12
    }
358
359
    // Creates folder in provided path & sets correct permissions
360
    // also deletes the file at filePath (if it already exists)
361 13
    private function createWritableDestinationFolder()
362
    {
363 13
        $filePath = $this->destination;
364
365 13
        $folder = dirname($filePath);
366 13
        if (!file_exists($folder)) {
367 1
            $this->logLn('Destination folder does not exist. Creating folder: ' . $folder);
368
            // TODO: what if this is outside open basedir?
369
            // see http://php.net/manual/en/ini.core.php#ini.open-basedir
370
371
            // Trying to create the given folder (recursively)
372 1
            if (!mkdir($folder, 0777, true)) {
373 1
                throw new CreateDestinationFolderException(
374 1
                    'Failed creating folder. Check the permissions!',
375 1
                    'Failed creating folder: ' . $folder . '. Check permissions!'
376
                );
377
            }
378
        }
379 12
    }
380
}
381