|
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
|
|
|
|