Completed
Push — master ( 5f5fca...2fa1e0 )
by Bjørn
12:28 queued 02:24
created

ServeConverted::serve()   B

Complexity

Conditions 9
Paths 10

Size

Total Lines 35
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 20.2909

Importance

Changes 0
Metric Value
cc 9
eloc 26
nc 10
nop 0
dl 0
loc 35
ccs 13
cts 27
cp 0.4815
crap 20.2909
rs 8.0555
c 0
b 0
f 0
1
<?php
2
namespace WebPConvert\Serve;
3
4
use WebPConvert\WebPConvert;
5
use WebPConvert\Convert\Exceptions\ConversionFailedException;
6
use WebPConvert\Convert\Exceptions\ConversionFailed\ConversionDeclinedException;
7
use WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblems\CreateDestinationFileException;
8
use WebPConvert\Convert\Exceptions\ConversionFailed\FileSystemProblems\CreateDestinationFolderException;
9
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\ConverterNotFoundException;
10
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\InvalidImageTypeException;
11
use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInput\TargetNotFoundException;
12
13
use WebPConvert\Loggers\BufferLogger;
14
use WebPConvert\Serve\Report;
15
16
/**
17
 * This class must serves a converted image (either a fresh convertion, the destionation, or
18
 * the original). Upon failure, the fail action given in the options will be exectuted
19
 */
20
class ServeConverted extends ServeBase
21
{
22
23
    /*
24
    Not used, currently...
25
    private function addXOptionsHeader()
26
    {
27
        if ($this->options['add-x-header-options']) {
28
            $this->header('X-WebP-Convert-Options:' . Report::getPrintableOptionsAsString($this->options));
29
        }
30
    }
31
    */
32
33
    private function addHeadersPreventingCaching()
34
    {
35
        $this->header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
36
        $this->header("Cache-Control: post-check=0, pre-check=0", false);
37
        $this->header("Pragma: no-cache");
38
    }
39
40
    public function serve404()
41
    {
42
        $protocol = isset($_SERVER["SERVER_PROTOCOL"]) ? $_SERVER["SERVER_PROTOCOL"] : 'HTTP/1.0';
43
        $this->header($protocol . " 404 Not Found");
44
    }
45
46 2
    public function serveOriginal()
47
    {
48 2
        if (!$this->callAboutToServeImageCallBack('source')) {
49 2
            return true;    // we shall not trigger the fail callback
50
        }
51
52
        if ($this->options['add-content-type-header']) {
53
            $arr = explode('.', $this->source);
54
            $ext = array_pop($arr);
55
            switch (strtolower($ext)) {
56
                case 'jpg':
57
                case 'jpeg':
58
                    $this->header('Content-type: image/jpeg');
59
                    break;
60
                case 'png':
61
                    $this->header('Content-type: image/png');
62
                    break;
63
            }
64
        }
65
66
        $this->addVaryHeader();
67
68
        switch ($this->whyServingThis) {
69
            case 'source-lighter':
70
            case 'explicitly-told-to':
71
                $this->addCacheControlHeader();
72
                $this->addLastModifiedHeader(@filemtime($this->source));
73
                break;
74
            default:
75
                $this->addHeadersPreventingCaching();
76
        }
77
78
        if (@readfile($this->source) === false) {
79
            $this->header('X-WebP-Convert: Could not read file');
80
            return false;
81
        }
82
        return true;
83
    }
84
85
    public function serveFreshlyConverted()
86
    {
87
88
        $criticalFail = false;
89
        $bufferLogger = new BufferLogger();
90
91
        try {
92
            WebPConvert::convert($this->source, $this->destination, $this->options, $bufferLogger);
93
94
            // We are here, so it was successful :)
95
96
            // Serve source if it is smaller than destination
97
            $filesizeDestination = @filesize($this->destination);
98
            $filesizeSource = @filesize($this->source);
99
            if (($filesizeSource !== false) &&
100
                ($filesizeDestination !== false) &&
101
                ($filesizeDestination > $filesizeSource)) {
102
                $this->whatToServe = 'original';
103
                $this->whyServingThis = 'source-lighter';
104
                return $this->serveOriginal();
105
            }
106
107
            if (!$this->callAboutToServeImageCallBack('fresh-conversion')) {
108
                return;
109
            }
110
            if ($this->options['add-content-type-header']) {
111
                $this->header('Content-type: image/webp');
112
            }
113
            if ($this->whyServingThis == 'explicitly-told-to') {
114
                $this->addXStatusHeader(
115
                    'Serving freshly converted image (was explicitly told to reconvert)'
116
                );
117
            } elseif ($this->whyServingThis == 'source-modified') {
118
                $this->addXStatusHeader(
119
                    'Serving freshly converted image (the original had changed)'
120
                );
121
            } elseif ($this->whyServingThis == 'no-existing') {
122
                $this->addXStatusHeader(
123
                    'Serving freshly converted image (there were no existing to serve)'
124
                );
125
            } else {
126
                $this->addXStatusHeader(
127
                    'Serving freshly converted image (dont know why!)'
128
                );
129
            }
130
131
            if ($this->options['add-vary-header']) {
132
                $this->header('Vary: Accept');
133
            }
134
135
            if ($this->whyServingThis == 'no-existing') {
136
                $this->addCacheControlHeader();
137
            } else {
138
                $this->addHeadersPreventingCaching();
139
            }
140
            $this->addLastModifiedHeader(time());
141
142
            // Should we add Content-Length header?
143
            // $this->header('Content-Length: ' . filesize($file));
144
            if (@readfile($this->destination)) {
145
                return true;
146
            } else {
147
                $this->fail('Error', 'could not read the freshly converted file');
148
                return false;
149
            }
150
        } catch (InvalidImageTypeException $e) {
151
            $criticalFail = true;
152
            $description = 'Invalid file extension';
153
            $msg = $e->getMessage();
0 ignored issues
show
Unused Code introduced by
The assignment to $msg is dead and can be removed.
Loading history...
154
        } catch (TargetNotFoundException $e) {
155
            $criticalFail = true;
156
            $description = 'Source file not found';
157
            $msg = $e->getMessage();
158
        } catch (ConversionFailedException $e) {
159
            // No converters could convert the image. At least one converter failed, even though it appears to be
160
            // operational
161
            $description = 'No converters could convert the image';
162
            $msg = $e->getMessage();
163
        } catch (ConversionDeclinedException $e) {
164
            // (no converters could convert the image. At least one converter declined
165
            $description = 'No converters could/wanted to convert the image';
166
            $msg = $e->getMessage();
167
        } catch (ConverterNotFoundException $e) {
168
            $description = 'A converter was not found!';
169
            $msg = $e->getMessage();
170
        } catch (CreateDestinationFileException $e) {
171
            $description = 'Cannot create destination file';
172
            $msg = $e->getMessage();
173
        } catch (CreateDestinationFolderException $e) {
174
            $description = 'Cannot create destination folder';
175
            $msg = $e->getMessage();
176
        } catch (\Exception $e) {
177
            $description = 'An unanticipated exception was thrown';
178
            $msg = $e->getMessage();
179
        }
180
181
        // Next line is commented out, because we need to be absolute sure that the details does not violate syntax
182
        // We could either try to filter it, or we could change WebPConvert, such that it only provides safe texts.
183
        // $this->header('X-WebP-Convert-And-Serve-Details: ' . $bufferLogger->getText());
184
185
        $this->fail('Conversion failed', $description, $criticalFail);
186
        return false;
187
        //echo '<p>This is how conversion process went:</p>' . $bufferLogger->getHtml();
188
    }
189
190
    protected function serveErrorMessageImage($msg)
191
    {
192
        // Generate image containing error message
193
        if ($this->options['add-content-type-header']) {
194
            $this->header('Content-type: image/gif');
195
        }
196
197
        try {
198
            if (function_exists('imagecreatetruecolor') &&
199
                function_exists('imagestring') &&
200
                function_exists('imagecolorallocate') &&
201
                function_exists('imagegif')
202
            ) {
203
                $image = imagecreatetruecolor(620, 200);
204
                if ($image !== false) {
205
                    imagestring($image, 1, 5, 5, $msg, imagecolorallocate($image, 233, 214, 291));
206
                    // echo imagewebp($image);
207
                    echo imagegif($image);
208
                    imagedestroy($image);
209
                    return;
210
                }
211
            }
212
        } catch (\Exception $e) {
213
            //
214
        }
215
216
        // Above failed.
217
        // TODO: what to do?
218
    }
219
220
    /**
221
     *
222
     * @return  void
223
     */
224
    protected function fail($title, $description, $critical = false)
225
    {
226
        $action = $critical ? $this->options['fail-when-original-unavailable'] : $this->options['fail'];
227
228
        if (isset($this->options['aboutToPerformFailActionCallback'])) {
229
            if (call_user_func(
230
                $this->options['aboutToPerformFailActionCallback'],
231
                $title,
232
                $description,
233
                $action,
234
                $this
235
            ) === false) {
236
                return;
237
            }
238
        }
239
240
        $this->addXStatusHeader('Failed (' . $description . ')');
241
242
        $this->addHeadersPreventingCaching();
243
244
        $title = 'Conversion failed';
245
        switch ($action) {
246
            case 'serve-original':
247
                if (!$this->serveOriginal()) {
248
                    $this->serve404();
249
                };
250
                break;
251
            case '404':
252
                $this->serve404();
253
                break;
254
            case 'report-as-image':
255
                // todo: handle if this fails
256
                $this->serveErrorMessageImage($title . '. ' . $description);
257
                break;
258
            case 'report':
259
                echo '<h1>' . $title . '</h1>' . $description;
260
                break;
261
        }
262
    }
263
264
    /**
265
     *
266
     * @return  void
267
     */
268
    protected function criticalFail($title, $description)
269
    {
270
        $this->fail($title, $description, true);
271
    }
272
273
    /**
274
     *  Serve the thing specified in $whatToServe and $whyServingThis
275
     *  These are first set my the decideWhatToServe() method, but may later change, if a fresh
276
     *  conversion is made
277
     */
278 3
    public function serve()
279
    {
280
281
        //$this->addXOptionsHeader();
282
283 3
        switch ($this->whatToServe) {
284 3
            case 'destination':
285 1
                return $this->serveExisting();
286 2
            case 'source':
287 2
                if ($this->whyServingThis == 'explicitly-told-to') {
288 1
                    $this->addXStatusHeader(
289
                        'Serving original image (was explicitly told to)'
290 1
                    );
291 1
                } else {
292 1
                    $this->addXStatusHeader(
293
                        'Serving original image (it is smaller than the already converted)'
294 1
                    );
295
                }
296 2
                if (!$this->serveOriginal()) {
297
                    $this->criticalFail('Error', 'could not serve original');
298
                    return false;
299
                }
300 2
                return true;
301
            case 'fresh-conversion':
302
                return $this->serveFreshlyConverted();
303
            case 'critical-fail':
304
                $this->criticalFail('Error', $this->whyServingThis);
305
                return false;
306
            case 'fail':
307
                $this->fail('Error', $this->whyServingThis);
308
                return false;
309
            case 'report':
310
                $this->addXStatusHeader('Reporting...');
311
                Report::convertAndReport($this->source, $this->destination, $this->options);
312
                return true;  // yeah, lets say that a report is always a success, even if conversion is a failure
313
        }
314
    }
315
316 3
    public function decideWhatToServeAndServeIt()
317
    {
318 3
        $this->decideWhatToServe();
319 3
        return $this->serve();
320
    }
321
322
    /**
323
     * Main method
324
     */
325 3
    public static function serveConverted($source, $destination, $options)
326
    {
327 3
        if (isset($options['fail']) && ($options['fail'] == 'original')) {
328
            $options['fail'] = 'serve-original';
329
        }
330
        // For backward compatability:
331 3
        if (isset($options['critical-fail']) && !isset($options['fail-when-original-unavailable'])) {
332
            $options['fail-when-original-unavailable'] = $options['critical-fail'];
333
        }
334
335 3
        $cs = new static($source, $destination, $options);
336
337 3
        return $cs->decideWhatToServeAndServeIt();
338
    }
339
}
340