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

ServeBase   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Test Coverage

Coverage 58.42%

Importance

Changes 0
Metric Value
eloc 95
dl 0
loc 216
ccs 59
cts 101
cp 0.5842
rs 9.28
c 0
b 0
f 0
wmc 39

12 Methods

Rating   Name   Duplication   Size   Complexity  
A decideWhatToServe() 0 5 1
A addXStatusHeader() 0 4 2
A __construct() 0 8 1
A addCacheControlHeader() 0 4 2
C doDecideWhatToServe() 0 44 14
A addVaryHeader() 0 4 2
A addContentTypeHeader() 0 4 2
A header() 0 3 1
A addLastModifiedHeader() 0 4 2
A serveExisting() 0 17 3
A callAboutToServeImageCallBack() 0 12 2
B setErrorReporting() 0 12 7
1
<?php
2
namespace WebPConvert\Serve;
3
4
//use WebPConvert\Serve\Report;
5
6
class ServeBase
7
{
8
    public $source;
9
    public $destination;
10
    public $options;
11
12
    // These two fellows are first set when decideWhatToServe is called
13
    // However, if it is decided to serve a fresh conversion, they might get modified.
14
    // If that for example results in a file larger than source, $whatToServe will change
15
    // from 'fresh-conversion' to 'original', and $whyServingThis will change to 'source-lighter'
16
    public $whatToServe = '';
17
    public $whyServingThis = '';
18
19 3
    public function __construct($source, $destination, $options)
20
    {
21
22 3
        $this->source = $source;
23 3
        $this->destination = $destination;
24 3
        $this->options = array_merge(self::$defaultOptions, $options);
25
26 3
        $this->setErrorReporting();
27 3
    }
28
29
    public static $defaultOptions = [
30
        'add-content-type-header' => true,
31
        'add-last-modified-header' => true,
32
        'add-vary-header' => true,
33
        'add-x-header-status' => true,
34
        'add-x-header-options' => false,
35
        'aboutToServeImageCallBack' => null,
36
        'aboutToPerformFailAction' => null,
37
        'cache-control-header' => 'public, max-age=86400',
38
        'converters' =>  ['cwebp', 'gd', 'imagick'],
39
        'error-reporting' => 'auto',
40
        'fail' => 'original',
41
        'fail-when-original-unavailable' => '404',
42
        'reconvert' => false,
43
        'serve-original' => false,
44
        'show-report' => false,
45
    ];
46
47 3
    protected function setErrorReporting()
48
    {
49 3
        if (($this->options['error-reporting'] === true) ||
50 3
            (($this->options['error-reporting'] === 'auto') && ($this->options['show-report'] === true))
51 3
        ) {
52
            error_reporting(E_ALL);
53
            ini_set('display_errors', 'On');
54 3
        } elseif (($this->options['error-reporting'] === false) ||
55 3
            (($this->options['error-reporting'] === 'auto') && ($this->options['show-report'] === false))
56 3
        ) {
57 3
            error_reporting(0);
58 3
            ini_set('display_errors', 'Off');
59 3
        }
60 3
    }
61
62
    protected function header($header, $replace = true)
63
    {
64
        header($header, $replace);
65
    }
66
67 2
    public function addXStatusHeader($text)
68
    {
69 2
        if ($this->options['add-x-header-status']) {
70 2
            $this->header('X-WebP-Convert-Status: ' . $text, true);
71 2
        }
72 2
    }
73
74
    public function addVaryHeader()
75
    {
76
        if ($this->options['add-vary-header']) {
77
            $this->header('Vary: Accept');
78
        }
79
    }
80
81
    public function addContentTypeHeader($cType)
82
    {
83
        if ($this->options['add-content-type-header']) {
84
            $this->header('Content-type: ' . $cType);
85
        }
86
    }
87
88
    /* $timestamp  Unix timestamp */
89
    public function addLastModifiedHeader($timestamp)
90
    {
91
        if ($this->options['add-last-modified-header']) {
92
            $this->header("Last-Modified: " . gmdate("D, d M Y H:i:s", $timestamp) ." GMT", true);
93
        }
94
    }
95
96
    public function addCacheControlHeader()
97
    {
98
        if (!empty($this->options['cache-control-header'])) {
99
            $this->header('Cache-Control: ' . $this->options['cache-control-header'], true);
100
        }
101
    }
102
103 1
    public function serveExisting()
104
    {
105 1
        if (!$this->callAboutToServeImageCallBack('destination')) {
106 1
            return;
107
        }
108
109
        $this->addXStatusHeader('Serving existing converted image');
110
        $this->addVaryHeader();
111
        $this->addContentTypeHeader('image/webp');
112
        $this->addCacheControlHeader();
113
        $this->addLastModifiedHeader(@filemtime($this->destination));
114
115
        if (@readfile($this->destination) === false) {
116
            $this->header('X-WebP-Convert-Error: Could not read file');
117
            return false;
118
        }
119
        return true;
120
    }
121
122
    /**
123
     *   Called immidiately before serving image (either original, already converted or fresh)
124
     *   $whatToServe can be 'source' | 'destination' | 'fresh-conversion'
125
     *   $whyServingThis can be:
126
     *   for 'source':
127
     *       - "explicitly-told-to"     (when the "original" option is set)
128
     *       - "source-lighter"         (when original image is actually smaller than the converted)
129
     *   for 'fresh-conversion':
130
     *       - "explicitly-told-to"     (when the "reconvert" option is set)
131
     *       - "source-modified"        (when source is newer than existing)
132
     *       - "no-existing"            (when there is no existing at the destination)
133
     *   for 'destination':
134
     *       - "no-reason-not-to"       (it is lighter than source, its not older,
135
     *                                   and we were not told to do otherwise)
136
     */
137 3
    protected function callAboutToServeImageCallBack($whatToServe)
138
    {
139 3
        if (!isset($this->options['aboutToServeImageCallBack'])) {
140
            return true;
141
        }
142 3
        $result = call_user_func(
143 3
            $this->options['aboutToServeImageCallBack'],
144 3
            $whatToServe,
145 3
            $this->whyServingThis,
146
            $this
147 3
        );
148 3
        return ($result !== false);
149
    }
150
151
    /**
152
     *  Decides what to serve.
153
     *  Returns array. First item is what to do, second is additional info.
154
     *  First item can be one of these:
155
     *  - "destination"  (serve existing converted image at the destination path)
156
     *       - "no-reason-not-to"
157
     *  - "source"
158
     *       - "explicitly-told-to"
159
     *       - "source-lighter"
160
     *  - "fresh-conversion" (note: this may still fail)
161
     *       - "explicitly-told-to"
162
     *       - "source-modified"
163
     *       - "no-existing"
164
     *  - "fail"
165
     *        - "Missing destination argument"
166
     *  - "critical-fail"   (a failure where the source file cannot be served)
167
     *        - "Missing source argument"
168
     *        - "Source file was not found!"
169
     *  - "report"
170
     */
171 3
    public function decideWhatToServe()
172
    {
173 3
        $decisionArr = $this->doDecideWhatToServe();
174 3
        $this->whatToServe = $decisionArr[0];
175 3
        $this->whyServingThis = $decisionArr[1];
176 3
    }
177
178 3
    private function doDecideWhatToServe()
179
    {
180 3
        if (empty($this->source)) {
181
            return ['critical-fail', 'Missing source argument'];
182
        }
183 3
        if (@!file_exists($this->source)) {
184
            return ['critical-fail', 'Source file was not found!'];
185
        }
186 3
        if (empty($this->destination)) {
187
            return ['fail', 'Missing destination argument'];
188
        }
189 3
        if ($this->options['show-report']) {
190
            return ['report', ''];
191
        }
192 3
        if ($this->options['serve-original']) {
193 1
            return ['source', 'explicitly-told-to'];
194
        }
195 2
        if ($this->options['reconvert']) {
196
            return ['fresh-conversion', 'explicitly-told-to'];
197
        }
198
199 2
        if (@file_exists($this->destination)) {
200
            // Reconvert if source file is newer than destination
201 2
            $timestampSource = @filemtime($this->source);
202 2
            $timestampDestination = @filemtime($this->destination);
203 2
            if (($timestampSource !== false) &&
204 2
                ($timestampDestination !== false) &&
205 2
                ($timestampSource > $timestampDestination)) {
206
                return ['fresh-conversion', 'source-modified'];
207
            }
208
209
            // Serve source if it is smaller than destination
210 2
            $filesizeDestination = @filesize($this->destination);
211 2
            $filesizeSource = @filesize($this->source);
212 2
            if (($filesizeSource !== false) &&
213 2
                ($filesizeDestination !== false) &&
214 2
                ($filesizeDestination > $filesizeSource)) {
215 1
                return ['source', 'source-lighter'];
216
            }
217
218
            // Destination exists, and there is no reason left not to serve it
219 1
            return ['destination', 'no-reason-not-to'];
220
        } else {
221
            return ['fresh-conversion', 'no-existing'];
222
        }
223
    }
224
}
225