Completed
Push — master ( 0235df...db4a8b )
by Bjørn
12:10 queued 01:07
created

AbstractConverter::doConvert()   B

Complexity

Conditions 10
Paths 48

Size

Total Lines 70
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
cc 10
eloc 45
nc 48
nop 0
dl 0
loc 70
ccs 0
cts 51
cp 0
crap 110
rs 7.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
// 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
    public function checkOperationality()
90
    {
91
    }
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
    public function checkConvertability()
102
    {
103
    }
104
105
    public function __construct($source, $destination, $options = [], $logger = null)
106
    {
107
        $this->source = $source;
108
        $this->destination = $destination;
109
110
        $this->setLogger($logger);
111
        $this->setProvidedOptions($options);
112
    }
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
    protected static function getConverterDisplayName()
121
    {
122
        // https://stackoverflow.com/questions/19901850/how-do-i-get-an-objects-unqualified-short-class-name/25308464
123
        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
    public static function createInstance($source, $destination, $options = [], $logger = null)
137
    {
138
        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
    public function doConvert()
145
    {
146
        $beginTime = microtime(true);
147
148
        $this->activateWarningLogger();
149
        //set_error_handler(array($this, "errorHandler"));
150
151
        try {
152
            // Prepare options
153
            //$this->prepareOptions();
154
155
            $this->checkOptions();
156
157
            // Prepare destination folder
158
            $this->createWritableDestinationFolder();
159
            $this->removeExistingDestinationIfExists();
160
161
            if (!isset($this->options['_skip_input_check'])) {
162
                // Run basic input validations (if source exists and if file extension is valid)
163
                $this->checkInput();
164
165
                // Check that a file can be written to destination
166
                $this->checkFileSystem();
167
            }
168
169
            $this->checkOperationality();
170
            $this->checkConvertability();
171
            $this->runActualConvert();
172
        } catch (ConversionFailedException $e) {
173
            $this->deactivateWarningLogger();
174
            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
        $this->deactivateWarningLogger();
185
186
        $source = $this->source;
187
        $destination = $this->destination;
188
189
        if (!@file_exists($destination)) {
190
            throw new ConversionFailedException('Destination file is not there: ' . $destination);
191
        } elseif (@filesize($destination) === 0) {
192
            unlink($destination);
193
            throw new ConversionFailedException('Destination file was completely empty');
194
        } else {
195
            if (!isset($this->options['_suppress_success_message'])) {
196
                $this->ln();
197
                $msg = 'Converted image in ' .
198
                    round((microtime(true) - $beginTime) * 1000) . ' ms';
199
200
                $sourceSize = @filesize($source);
201
                if ($sourceSize !== false) {
202
                    $msg .= ', reducing file size with ' .
203
                        round((filesize($source) - filesize($destination))/filesize($source) * 100) . '% ';
204
205
                    if ($sourceSize < 10000) {
206
                        $msg .= '(went from ' . round(filesize($source)) . ' bytes to ';
207
                        $msg .= round(filesize($destination)) . ' bytes)';
208
                    } else {
209
                        $msg .= '(went from ' . round(filesize($source)/1024) . ' kb to ';
210
                        $msg .= round(filesize($destination)/1024) . ' kb)';
211
                    }
212
                }
213
                $this->logLn($msg);
214
            }
215
        }
216
    }
217
218
219
    private function runActualConvert()
220
    {
221
        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
            $this->doActualConvert();
260
        }
261
    }
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
     * @return  void
271
     */
272
    public static function convert($source, $destination, $options = [], $logger = null)
273
    {
274
        $instance = self::createInstance($source, $destination, $options, $logger);
275
        $instance->doConvert();
276
        //echo $instance->id;
277
    }
278
279
    /**
280
     * Get mime type for image (best guess).
281
     *
282
     * It falls back to using file extension. If that fails too, false is returned
283
     *
284
     * PS: Is it a security risk to fall back on file extension?
285
     * - By setting file extension to "jpg", one can lure our library into trying to convert a file, which isn't a jpg.
286
     * hmm, seems very unlikely, though not unthinkable that one of the converters could be exploited
287
     *
288
     * @return  string|false|null mimetype (if it is an image, and type could be determined / guessed),
289
     *    false (if it is not an image type that the server knowns about)
290
     *    or null (if nothing can be determined)
291
     */
292
    public function getMimeTypeOfSource()
293
    {
294
        if (!isset($this->sourceMimeType)) {
295
            $this->sourceMimeType = ImageMimeTypeGuesser::lenientGuess($this->source);
296
        }
297
        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...
298
    }
299
300
    /**
301
     *  Note: As the input validations are only run one time in a stack,
302
     *  this method is not overridable
303
     */
304
    private function checkInput()
305
    {
306
        // Check if source exists
307
        if (!@file_exists($this->source)) {
308
            throw new TargetNotFoundException('File or directory not found: ' . $this->source);
309
        }
310
311
        // Check if the provided file's mime type is valid
312
        $fileMimeType = $this->getMimeTypeOfSource();
313
        if (is_null($fileMimeType)) {
314
            throw new InvalidImageTypeException('Image type could not be detected');
315
        } elseif ($fileMimeType === false) {
0 ignored issues
show
introduced by
The condition $fileMimeType === false is always false.
Loading history...
316
            throw new InvalidImageTypeException('File seems not to be an image.');
317
        } elseif (!in_array($fileMimeType, self::$allowedMimeTypes)) {
318
            throw new InvalidImageTypeException('Unsupported mime type: ' . $fileMimeType);
319
        }
320
    }
321
322
    private function checkFileSystem()
323
    {
324
        // TODO:
325
        // Instead of creating dummy file,
326
        // perhaps something like this ?
327
        // if (@is_writable($dirName) && @is_executable($dirName) || self::isWindows() )
328
        // Or actually, probably best with a mix.
329
        // First we test is_writable and is_executable. If that fails and we are on windows, we can do the dummy
330
        // function isWindows(){
331
        // return (boolean) preg_match('/^win/i', PHP_OS);
332
        //}
333
334
        // Try to create a dummy file here, with that name, just to see if it is possible (we delete it again)
335
        file_put_contents($this->destination, '');
336
        if (file_put_contents($this->destination, '') === false) {
337
            throw new CreateDestinationFileException(
338
                'Cannot create file: ' . basename($this->destination) . ' in dir:' . dirname($this->destination)
339
            );
340
        }
341
        unlink($this->destination);
342
    }
343
344
    private function removeExistingDestinationIfExists()
345
    {
346
        if (file_exists($this->destination)) {
347
            // A file already exists in this folder...
348
            // We delete it, to make way for a new webp
349
            if (!unlink($this->destination)) {
350
                throw new CreateDestinationFileException(
351
                    'Existing file cannot be removed: ' . basename($this->destination)
352
                );
353
            }
354
        }
355
    }
356
357
    // Creates folder in provided path & sets correct permissions
358
    // also deletes the file at filePath (if it already exists)
359
    private function createWritableDestinationFolder()
360
    {
361
        $filePath = $this->destination;
362
363
        $folder = dirname($filePath);
364
        if (!file_exists($folder)) {
365
            $this->logLn('Destination folder does not exist. Creating folder: ' . $folder);
366
            // TODO: what if this is outside open basedir?
367
            // see http://php.net/manual/en/ini.core.php#ini.open-basedir
368
369
            // Trying to create the given folder (recursively)
370
            if (!mkdir($folder, 0777, true)) {
371
                throw new CreateDestinationFolderException('Failed creating folder: ' . $folder);
372
            }
373
        }
374
    }
375
}
376