|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
/** |
|
4
|
|
|
* FileUpload.php - This class implements file upload with Ajax. |
|
5
|
|
|
* |
|
6
|
|
|
* @package jaxon-core |
|
7
|
|
|
* @author Thierry Feuzeu <[email protected]> |
|
8
|
|
|
* @copyright 2017 Thierry Feuzeu <[email protected]> |
|
9
|
|
|
* @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License |
|
10
|
|
|
* @link https://github.com/jaxon-php/jaxon-core |
|
11
|
|
|
*/ |
|
12
|
|
|
|
|
13
|
|
|
namespace Jaxon\Request\Plugin; |
|
14
|
|
|
|
|
15
|
|
|
use Jaxon\Jaxon; |
|
16
|
|
|
use Jaxon\Plugin\Request as RequestPlugin; |
|
17
|
|
|
use Jaxon\Request\Support\UploadedFile; |
|
18
|
|
|
use Jaxon\Response\UploadResponse; |
|
19
|
|
|
|
|
20
|
|
|
use Exception; |
|
21
|
|
|
use Closure; |
|
22
|
|
|
|
|
23
|
|
|
class FileUpload extends RequestPlugin |
|
24
|
|
|
{ |
|
25
|
|
|
use \Jaxon\Features\Config; |
|
26
|
|
|
use \Jaxon\Features\Validator; |
|
27
|
|
|
use \Jaxon\Features\Translator; |
|
28
|
|
|
|
|
29
|
|
|
/** |
|
30
|
|
|
* The uploaded files copied in the user dir |
|
31
|
|
|
* |
|
32
|
|
|
* @var array |
|
33
|
|
|
*/ |
|
34
|
|
|
protected $aUserFiles = []; |
|
35
|
|
|
|
|
36
|
|
|
/** |
|
37
|
|
|
* The name of file containing upload data |
|
38
|
|
|
* |
|
39
|
|
|
* @var string |
|
40
|
|
|
*/ |
|
41
|
|
|
protected $sTempFile = ''; |
|
42
|
|
|
|
|
43
|
|
|
/** |
|
44
|
|
|
* The subdir where uploaded files are stored |
|
45
|
|
|
* |
|
46
|
|
|
* @var string |
|
47
|
|
|
*/ |
|
48
|
|
|
protected $sUploadSubdir = ''; |
|
49
|
|
|
|
|
50
|
|
|
/** |
|
51
|
|
|
* A user defined function to transform uploaded file names |
|
52
|
|
|
* |
|
53
|
|
|
* @var Closure |
|
54
|
|
|
*/ |
|
55
|
|
|
protected $cFileFilter = null; |
|
56
|
|
|
|
|
57
|
|
|
/** |
|
58
|
|
|
* Is the current request an HTTP upload |
|
59
|
|
|
* |
|
60
|
|
|
* @var boolean |
|
61
|
|
|
*/ |
|
62
|
|
|
protected $bRequestIsHttpUpload = false; |
|
63
|
|
|
|
|
64
|
|
|
/** |
|
65
|
|
|
* Read uploaded files info from the $_FILES global var |
|
66
|
|
|
*/ |
|
67
|
|
|
public function __construct() |
|
68
|
|
|
{ |
|
69
|
|
|
$this->sUploadSubdir = uniqid() . DIRECTORY_SEPARATOR; |
|
70
|
|
|
|
|
71
|
|
|
if(array_key_exists('jxnupl', $_POST)) |
|
72
|
|
|
{ |
|
73
|
|
|
$this->sTempFile = $_POST['jxnupl']; |
|
74
|
|
|
} |
|
75
|
|
|
elseif(array_key_exists('jxnupl', $_GET)) |
|
76
|
|
|
{ |
|
77
|
|
|
$this->sTempFile = $_GET['jxnupl']; |
|
78
|
|
|
} |
|
79
|
|
|
} |
|
80
|
|
|
|
|
81
|
|
|
/** |
|
82
|
|
|
* Filter uploaded file name |
|
83
|
|
|
* |
|
84
|
|
|
* @param Closure $cFileFilter The closure which filters filenames |
|
85
|
|
|
* |
|
86
|
|
|
* @return void |
|
87
|
|
|
*/ |
|
88
|
|
|
public function setFileFilter(Closure $cFileFilter) |
|
89
|
|
|
{ |
|
90
|
|
|
$this->cFileFilter = $cFileFilter; |
|
91
|
|
|
} |
|
92
|
|
|
|
|
93
|
|
|
/** |
|
94
|
|
|
* Filter uploaded file name |
|
95
|
|
|
* |
|
96
|
|
|
* @param string $sFilename The filename |
|
97
|
|
|
* @param string $sVarName The associated variable name |
|
98
|
|
|
* |
|
99
|
|
|
* @return string |
|
100
|
|
|
*/ |
|
101
|
|
|
protected function filterFilename($sFilename, $sVarName) |
|
102
|
|
|
{ |
|
103
|
|
|
if(($this->cFileFilter)) |
|
104
|
|
|
{ |
|
105
|
|
|
$cFileFilter = $this->cFileFilter; |
|
106
|
|
|
$sFilename = (string)$cFileFilter($sFilename, $sVarName); |
|
107
|
|
|
} |
|
108
|
|
|
return $sFilename; |
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
|
|
/** |
|
112
|
|
|
* Get the path to the upload dir |
|
113
|
|
|
* |
|
114
|
|
|
* @param string $sFieldId The filename |
|
115
|
|
|
* |
|
116
|
|
|
* @return string |
|
117
|
|
|
*/ |
|
118
|
|
|
protected function getUploadDir($sFieldId) |
|
119
|
|
|
{ |
|
120
|
|
|
// Default upload dir |
|
121
|
|
|
$sDefaultUploadDir = $this->getOption('upload.default.dir'); |
|
122
|
|
|
$sUploadDir = $this->getOption('upload.files.' . $sFieldId . '.dir', $sDefaultUploadDir); |
|
123
|
|
|
$sUploadDir = rtrim(trim($sUploadDir), '/\\') . DIRECTORY_SEPARATOR; |
|
124
|
|
|
// Verify that the upload dir exists and is writable |
|
125
|
|
|
if(!is_writable($sUploadDir)) |
|
126
|
|
|
{ |
|
127
|
|
|
throw new \Jaxon\Exception\Error($this->trans('errors.upload.access')); |
|
128
|
|
|
} |
|
129
|
|
|
$sUploadDir .= $this->sUploadSubdir; |
|
130
|
|
View Code Duplication |
if(!file_exists($sUploadDir) && !@mkdir($sUploadDir)) |
|
|
|
|
|
|
131
|
|
|
{ |
|
132
|
|
|
throw new \Jaxon\Exception\Error($this->trans('errors.upload.access')); |
|
133
|
|
|
} |
|
134
|
|
|
return $sUploadDir; |
|
135
|
|
|
} |
|
136
|
|
|
|
|
137
|
|
|
/** |
|
138
|
|
|
* Get the path to the upload temp dir |
|
139
|
|
|
* |
|
140
|
|
|
* @return string |
|
141
|
|
|
*/ |
|
142
|
|
|
protected function getUploadTempDir() |
|
143
|
|
|
{ |
|
144
|
|
|
// Default upload dir |
|
145
|
|
|
$sUploadDir = $this->getOption('upload.default.dir'); |
|
146
|
|
|
$sUploadDir = rtrim(trim($sUploadDir), '/\\') . DIRECTORY_SEPARATOR; |
|
147
|
|
|
// Verify that the upload dir exists and is writable |
|
148
|
|
|
if(!is_writable($sUploadDir)) |
|
149
|
|
|
{ |
|
150
|
|
|
throw new \Jaxon\Exception\Error($this->trans('errors.upload.access')); |
|
151
|
|
|
} |
|
152
|
|
|
$sUploadDir .= 'tmp' . DIRECTORY_SEPARATOR; |
|
153
|
|
View Code Duplication |
if(!file_exists($sUploadDir) && !@mkdir($sUploadDir)) |
|
|
|
|
|
|
154
|
|
|
{ |
|
155
|
|
|
throw new \Jaxon\Exception\Error($this->trans('errors.upload.access')); |
|
156
|
|
|
} |
|
157
|
|
|
return $sUploadDir; |
|
158
|
|
|
} |
|
159
|
|
|
|
|
160
|
|
|
/** |
|
161
|
|
|
* Get the path to the upload temp file |
|
162
|
|
|
* |
|
163
|
|
|
* @return string |
|
164
|
|
|
*/ |
|
165
|
|
|
protected function getUploadTempFile() |
|
166
|
|
|
{ |
|
167
|
|
|
$sUploadDir = $this->getOption('upload.default.dir'); |
|
168
|
|
|
$sUploadDir = rtrim(trim($sUploadDir), '/\\') . DIRECTORY_SEPARATOR; |
|
169
|
|
|
$sUploadDir .= 'tmp' . DIRECTORY_SEPARATOR; |
|
170
|
|
|
$sUploadTempFile = $sUploadDir . $this->sTempFile . '.json'; |
|
171
|
|
|
if(!is_readable($sUploadTempFile)) |
|
172
|
|
|
{ |
|
173
|
|
|
throw new \Jaxon\Exception\Error($this->trans('errors.upload.access')); |
|
174
|
|
|
} |
|
175
|
|
|
return $sUploadTempFile; |
|
176
|
|
|
} |
|
177
|
|
|
|
|
178
|
|
|
/** |
|
179
|
|
|
* Read uploaded files info from HTTP request data |
|
180
|
|
|
* |
|
181
|
|
|
* @return void |
|
182
|
|
|
*/ |
|
183
|
|
|
protected function readFromHttpData() |
|
184
|
|
|
{ |
|
185
|
|
|
// Check validity of the uploaded files |
|
186
|
|
|
$aTempFiles = []; |
|
187
|
|
|
foreach($_FILES as $sVarName => $aFile) |
|
188
|
|
|
{ |
|
189
|
|
|
if(is_array($aFile['name'])) |
|
190
|
|
|
{ |
|
191
|
|
|
$nFileCount = count($aFile['name']); |
|
192
|
|
|
for($i = 0; $i < $nFileCount; $i++) |
|
193
|
|
|
{ |
|
194
|
|
|
if(!$aFile['name'][$i]) |
|
195
|
|
|
{ |
|
196
|
|
|
continue; |
|
197
|
|
|
} |
|
198
|
|
|
if(!array_key_exists($sVarName, $aTempFiles)) |
|
199
|
|
|
{ |
|
200
|
|
|
$aTempFiles[$sVarName] = []; |
|
201
|
|
|
} |
|
202
|
|
|
// Filename without the extension |
|
203
|
|
|
$sFilename = $this->filterFilename(pathinfo($aFile['name'][$i], PATHINFO_FILENAME), $sVarName); |
|
204
|
|
|
// Copy the file data into the local array |
|
205
|
|
|
$aTempFiles[$sVarName][] = [ |
|
206
|
|
|
'name' => $aFile['name'][$i], |
|
207
|
|
|
'type' => $aFile['type'][$i], |
|
208
|
|
|
'tmp_name' => $aFile['tmp_name'][$i], |
|
209
|
|
|
'error' => $aFile['error'][$i], |
|
210
|
|
|
'size' => $aFile['size'][$i], |
|
211
|
|
|
'filename' => $sFilename, |
|
212
|
|
|
'extension' => pathinfo($aFile['name'][$i], PATHINFO_EXTENSION), |
|
213
|
|
|
]; |
|
214
|
|
|
} |
|
215
|
|
|
} |
|
216
|
|
|
else |
|
217
|
|
|
{ |
|
218
|
|
|
if(!$aFile['name']) |
|
219
|
|
|
{ |
|
220
|
|
|
continue; |
|
221
|
|
|
} |
|
222
|
|
|
if(!array_key_exists($sVarName, $aTempFiles)) |
|
223
|
|
|
{ |
|
224
|
|
|
$aTempFiles[$sVarName] = []; |
|
225
|
|
|
} |
|
226
|
|
|
// Filename without the extension |
|
227
|
|
|
$sFilename = $this->filterFilename(pathinfo($aFile['name'], PATHINFO_FILENAME), $sVarName); |
|
228
|
|
|
// Copy the file data into the local array |
|
229
|
|
|
$aTempFiles[$sVarName][] = [ |
|
230
|
|
|
'name' => $aFile['name'], |
|
231
|
|
|
'type' => $aFile['type'], |
|
232
|
|
|
'tmp_name' => $aFile['tmp_name'], |
|
233
|
|
|
'error' => $aFile['error'], |
|
234
|
|
|
'size' => $aFile['size'], |
|
235
|
|
|
'filename' => $sFilename, |
|
236
|
|
|
'extension' => pathinfo($aFile['name'], PATHINFO_EXTENSION), |
|
237
|
|
|
]; |
|
238
|
|
|
} |
|
239
|
|
|
} |
|
240
|
|
|
|
|
241
|
|
|
// Check uploaded files validity |
|
242
|
|
|
foreach($aTempFiles as $sVarName => $aFiles) |
|
243
|
|
|
{ |
|
244
|
|
|
foreach($aFiles as $aFile) |
|
245
|
|
|
{ |
|
246
|
|
|
// Verify upload result |
|
247
|
|
|
if($aFile['error'] != 0) |
|
248
|
|
|
{ |
|
249
|
|
|
throw new \Jaxon\Exception\Error($this->trans('errors.upload.failed', $aFile)); |
|
250
|
|
|
} |
|
251
|
|
|
// Verify file validity (format, size) |
|
252
|
|
|
if(!$this->validateUploadedFile($sVarName, $aFile)) |
|
253
|
|
|
{ |
|
254
|
|
|
throw new \Jaxon\Exception\Error($this->getValidatorMessage()); |
|
255
|
|
|
} |
|
256
|
|
|
// Get the path to the upload dir |
|
257
|
|
|
$this->getUploadDir($sVarName); |
|
258
|
|
|
} |
|
259
|
|
|
} |
|
260
|
|
|
|
|
261
|
|
|
// Copy the uploaded files from the temp dir to the user dir |
|
262
|
|
|
foreach($aTempFiles as $sVarName => $_aTempFiles) |
|
263
|
|
|
{ |
|
264
|
|
|
$this->aUserFiles[$sVarName] = []; |
|
265
|
|
|
foreach($_aTempFiles as $aFile) |
|
266
|
|
|
{ |
|
267
|
|
|
// Get the path to the upload dir |
|
268
|
|
|
$sUploadDir = $this->getUploadDir($sVarName); |
|
269
|
|
|
// Set the user file data |
|
270
|
|
|
$xUploadedFile = UploadedFile::fromHttpData($sUploadDir, $aFile); |
|
271
|
|
|
// All's right, move the file to the user dir. |
|
272
|
|
|
move_uploaded_file($aFile["tmp_name"], $xUploadedFile->path()); |
|
273
|
|
|
$this->aUserFiles[$sVarName][] = $xUploadedFile; |
|
274
|
|
|
} |
|
275
|
|
|
} |
|
276
|
|
|
} |
|
277
|
|
|
|
|
278
|
|
|
/** |
|
279
|
|
|
* Save uploaded files info to a temp file |
|
280
|
|
|
* |
|
281
|
|
|
* @return void |
|
282
|
|
|
*/ |
|
283
|
|
|
protected function saveToTempFile() |
|
284
|
|
|
{ |
|
285
|
|
|
// Convert uploaded file to an array |
|
286
|
|
|
$aFiles = []; |
|
287
|
|
|
foreach($this->aUserFiles as $sVarName => $aUserFiles) |
|
288
|
|
|
{ |
|
289
|
|
|
$aFiles[$sVarName] = []; |
|
290
|
|
|
foreach($aUserFiles as $aUserFile) |
|
291
|
|
|
{ |
|
292
|
|
|
$aFiles[$sVarName][] = $aUserFile->toTempData(); |
|
293
|
|
|
} |
|
294
|
|
|
} |
|
295
|
|
|
// Save upload data in a temp file |
|
296
|
|
|
$sUploadDir = $this->getUploadTempDir(); |
|
297
|
|
|
$this->sTempFile = uniqid(); |
|
298
|
|
|
file_put_contents($sUploadDir . $this->sTempFile . '.json', json_encode($aFiles)); |
|
299
|
|
|
} |
|
300
|
|
|
|
|
301
|
|
|
/** |
|
302
|
|
|
* Read uploaded files info from a temp file |
|
303
|
|
|
* |
|
304
|
|
|
* @return void |
|
305
|
|
|
*/ |
|
306
|
|
|
protected function readFromTempFile() |
|
307
|
|
|
{ |
|
308
|
|
|
// Upload temp file |
|
309
|
|
|
$sUploadTempFile = $this->getUploadTempFile(); |
|
310
|
|
|
$aFiles = json_decode(file_get_contents($sUploadTempFile), true); |
|
311
|
|
|
foreach($aFiles as $sVarName => $aUserFiles) |
|
312
|
|
|
{ |
|
313
|
|
|
$this->aUserFiles[$sVarName] = []; |
|
314
|
|
|
foreach($aUserFiles as $aUserFile) |
|
315
|
|
|
{ |
|
316
|
|
|
$this->aUserFiles[$sVarName][] = UploadedFile::fromTempData($aUserFile); |
|
317
|
|
|
} |
|
318
|
|
|
} |
|
319
|
|
|
// unlink($sUploadTempFile); |
|
320
|
|
|
} |
|
321
|
|
|
|
|
322
|
|
|
/** |
|
323
|
|
|
* Return the name of this plugin |
|
324
|
|
|
* |
|
325
|
|
|
* @return string |
|
326
|
|
|
*/ |
|
327
|
|
|
public function getName() |
|
328
|
|
|
{ |
|
329
|
|
|
return Jaxon::FILE_UPLOAD; |
|
330
|
|
|
} |
|
331
|
|
|
|
|
332
|
|
|
/** |
|
333
|
|
|
* Filter uploaded file name |
|
334
|
|
|
* |
|
335
|
|
|
* @param Closure $cFileFilter The closure which filters filenames |
|
336
|
|
|
* |
|
337
|
|
|
* @return void |
|
338
|
|
|
*/ |
|
339
|
|
|
public function filter(Closure $cFileFilter) |
|
340
|
|
|
{ |
|
341
|
|
|
$this->setFileFilter($cFileFilter); |
|
342
|
|
|
} |
|
343
|
|
|
|
|
344
|
|
|
/** |
|
345
|
|
|
* Get the uploaded files |
|
346
|
|
|
* |
|
347
|
|
|
* @return array |
|
348
|
|
|
*/ |
|
349
|
|
|
public function files() |
|
350
|
|
|
{ |
|
351
|
|
|
return $this->aUserFiles; |
|
352
|
|
|
} |
|
353
|
|
|
|
|
354
|
|
|
/** |
|
355
|
|
|
* Generate a hash for the registered browser events |
|
356
|
|
|
* |
|
357
|
|
|
* @return string |
|
358
|
|
|
*/ |
|
359
|
|
|
public function generateHash() |
|
360
|
|
|
{ |
|
361
|
|
|
return ''; |
|
362
|
|
|
} |
|
363
|
|
|
|
|
364
|
|
|
/** |
|
365
|
|
|
* Generate client side javascript code for the registered browser events |
|
366
|
|
|
* |
|
367
|
|
|
* @return string |
|
368
|
|
|
*/ |
|
369
|
|
|
public function getScript() |
|
370
|
|
|
{ |
|
371
|
|
|
return ''; |
|
372
|
|
|
} |
|
373
|
|
|
|
|
374
|
|
|
/** |
|
375
|
|
|
* Inform this plugin that other plugin can process the current request |
|
376
|
|
|
* |
|
377
|
|
|
* @return void |
|
378
|
|
|
*/ |
|
379
|
|
|
public function noRequestPluginFound() |
|
380
|
|
|
{ |
|
381
|
|
|
if(count($_FILES) > 0) |
|
382
|
|
|
{ |
|
383
|
|
|
$this->bRequestIsHttpUpload = true; |
|
384
|
|
|
} |
|
385
|
|
|
} |
|
386
|
|
|
|
|
387
|
|
|
/** |
|
388
|
|
|
* Check if this plugin can process the incoming Jaxon request |
|
389
|
|
|
* |
|
390
|
|
|
* @return boolean |
|
391
|
|
|
*/ |
|
392
|
|
|
public function canProcessRequest() |
|
393
|
|
|
{ |
|
394
|
|
|
return (count($_FILES) > 0 || ($this->sTempFile)); |
|
395
|
|
|
} |
|
396
|
|
|
|
|
397
|
|
|
/** |
|
398
|
|
|
* Process the uploaded files into the HTTP request |
|
399
|
|
|
* |
|
400
|
|
|
* @return boolean |
|
401
|
|
|
*/ |
|
402
|
|
|
public function processRequest() |
|
403
|
|
|
{ |
|
404
|
|
|
if(!$this->canProcessRequest()) |
|
405
|
|
|
{ |
|
406
|
|
|
return false; |
|
407
|
|
|
} |
|
408
|
|
|
|
|
409
|
|
|
if(count($_FILES) > 0) |
|
410
|
|
|
{ |
|
411
|
|
|
// Ajax request with upload |
|
412
|
|
|
$this->readFromHttpData(); |
|
413
|
|
|
|
|
414
|
|
|
if($this->bRequestIsHttpUpload) |
|
415
|
|
|
{ |
|
416
|
|
|
// Process an HTTP upload request |
|
417
|
|
|
// This requires to set the response to be returned. |
|
418
|
|
|
$xResponse = new UploadResponse(); |
|
419
|
|
|
try |
|
420
|
|
|
{ |
|
421
|
|
|
$this->saveToTempFile(); |
|
422
|
|
|
$xResponse->setUploadedFile($this->sTempFile); |
|
423
|
|
|
} |
|
424
|
|
|
catch(Exception $e) |
|
425
|
|
|
{ |
|
426
|
|
|
$xResponse->setErrorMessage($e->getMessage()); |
|
427
|
|
|
} |
|
428
|
|
|
jaxon()->di()->getResponseManager()->append($xResponse); |
|
429
|
|
|
} |
|
430
|
|
|
} |
|
431
|
|
|
elseif(($this->sTempFile)) |
|
432
|
|
|
{ |
|
433
|
|
|
// Ajax request following and HTTP upload |
|
434
|
|
|
$this->readFromTempFile(); |
|
435
|
|
|
} |
|
436
|
|
|
|
|
437
|
|
|
return true; |
|
438
|
|
|
} |
|
439
|
|
|
} |
|
440
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.