1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* Saito - The Threaded Web Forum |
7
|
|
|
* |
8
|
|
|
* @copyright Copyright (c) the Saito Project Developers |
9
|
|
|
* @link https://github.com/Schlaefer/Saito |
10
|
|
|
* @license http://opensource.org/licenses/MIT |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace ImageUploader\Model\Table; |
14
|
|
|
|
15
|
|
|
use App\Lib\Model\Table\AppTable; |
16
|
|
|
use Cake\Core\Configure; |
17
|
|
|
use Cake\Event\Event; |
18
|
|
|
use Cake\Filesystem\File; |
19
|
|
|
use Cake\I18n\Number; |
20
|
|
|
use Cake\ORM\RulesChecker; |
21
|
|
|
use Cake\Validation\Validation; |
22
|
|
|
use Cake\Validation\Validator; |
23
|
|
|
use claviska\SimpleImage; |
24
|
|
|
use ImageUploader\Lib\MimeType; |
25
|
|
|
use ImageUploader\Model\Entity\Upload; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Uploads |
29
|
|
|
* |
30
|
|
|
* Indeces: |
31
|
|
|
* - user_id, title - Combined used for uniqueness test. User_id for user's |
32
|
|
|
* upload overview page. |
33
|
|
|
*/ |
34
|
|
|
class UploadsTable extends AppTable |
35
|
|
|
{ |
36
|
|
|
/** |
37
|
|
|
* Max filename length. |
38
|
|
|
* |
39
|
|
|
* Constrained to 191 due to InnoDB index max-length on MySQL 5.6. |
40
|
|
|
*/ |
41
|
|
|
public const FILENAME_MAXLENGTH = 191; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Uploader Configuration |
45
|
|
|
* |
46
|
|
|
* @var \ImageUploader\Lib\UploaderConfig |
47
|
|
|
*/ |
48
|
|
|
protected $UploaderConfig; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* {@inheritDoc} |
52
|
|
|
*/ |
53
|
|
|
public function initialize(array $config) |
54
|
|
|
{ |
55
|
|
|
$this->addBehavior('Timestamp'); |
56
|
|
|
$this->setEntityClass(Upload::class); |
57
|
|
|
|
58
|
|
|
$this->belongsTo('Users', ['foreignKey' => 'user_id']); |
59
|
|
|
|
60
|
|
|
$this->UploaderConfig = Configure::read('Saito.Settings.uploader'); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* {@inheritDoc} |
65
|
|
|
*/ |
66
|
|
|
public function validationDefault(Validator $validator) |
67
|
|
|
{ |
68
|
|
|
$validator |
|
|
|
|
69
|
|
|
->add('id', 'valid', ['rule' => 'numeric']) |
70
|
|
|
->allowEmpty('id', 'create') |
71
|
|
|
->notBlank('name') |
72
|
|
|
->notBlank('size') |
73
|
|
|
->notBlank('type') |
74
|
|
|
->notBlank('user_id') |
75
|
|
|
->requirePresence(['name', 'size', 'type', 'user_id'], 'create'); |
76
|
|
|
|
77
|
|
|
$validator->add( |
78
|
|
|
'document', |
79
|
|
|
[ |
80
|
|
|
'file' => [ |
81
|
|
|
'rule' => [$this, 'validateFile'], |
82
|
|
|
], |
83
|
|
|
] |
84
|
|
|
); |
85
|
|
|
|
86
|
|
|
$validator->add( |
87
|
|
|
'title', |
88
|
|
|
[ |
89
|
|
|
'maxLength' => [ |
90
|
|
|
'rule' => ['maxLength', self::FILENAME_MAXLENGTH], |
91
|
|
|
'message' => __('vld.uploads.title.maxlength', self::FILENAME_MAXLENGTH), |
92
|
|
|
], |
93
|
|
|
] |
94
|
|
|
); |
95
|
|
|
|
96
|
|
|
return $validator; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* {@inheritDoc} |
101
|
|
|
*/ |
102
|
|
|
public function buildRules(RulesChecker $rules) |
103
|
|
|
{ |
104
|
|
|
$nMax = $this->UploaderConfig->getMaxNumberOfUploadsPerUser(); |
105
|
|
|
$rules->add( |
106
|
|
|
function (Upload $entity, array $options) use ($nMax) { |
|
|
|
|
107
|
|
|
$count = $this->findByUserId($entity->get('user_id'))->count(); |
|
|
|
|
108
|
|
|
|
109
|
|
|
return $count < $nMax; |
110
|
|
|
}, |
111
|
|
|
'maxAllowedUploadsPerUser', |
112
|
|
|
[ |
113
|
|
|
'errorField' => 'user_id', |
114
|
|
|
'message' => __d('image_uploader', 'validation.error.maxNumberOfItems', $nMax), |
115
|
|
|
] |
116
|
|
|
); |
117
|
|
|
|
118
|
|
|
// check that user exists |
119
|
|
|
$rules->add($rules->existsIn('user_id', 'Users')); |
120
|
|
|
|
121
|
|
|
// check that same user can't have two items with the same name |
122
|
|
|
$rules->add( |
123
|
|
|
$rules->isUnique( |
124
|
|
|
// Don't use a identifier like "name" which changes (jpg->png). |
125
|
|
|
['title', 'user_id'], |
126
|
|
|
__d('image_uploader', 'validation.error.fileExists') |
127
|
|
|
) |
128
|
|
|
); |
129
|
|
|
|
130
|
|
|
return $rules; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* {@inheritDoc} |
135
|
|
|
*/ |
136
|
|
|
public function beforeMarshal(Event $event, \ArrayObject $data) |
|
|
|
|
137
|
|
|
{ |
138
|
|
|
if (!empty($data['document'])) { |
139
|
|
|
/// Set mime/type by what is determined on the server about the file. |
140
|
|
|
$data['type'] = MimeType::get($data['document']['tmp_name'], $data['name']); |
141
|
|
|
$data['document']['type'] = $data['type']; |
142
|
|
|
} |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* {@inheritDoc} |
147
|
|
|
*/ |
148
|
|
|
public function beforeSave(Event $event, Upload $entity, \ArrayObject $options) |
|
|
|
|
149
|
|
|
{ |
150
|
|
|
if (!$entity->isDirty('name') && !$entity->isDirty('document')) { |
151
|
|
|
return true; |
152
|
|
|
} |
153
|
|
|
try { |
154
|
|
|
$this->moveUpload($entity); |
155
|
|
|
} catch (\Throwable $e) { |
156
|
|
|
return false; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
return true; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* {@inheritDoc} |
164
|
|
|
*/ |
165
|
|
|
public function beforeDelete(Event $event, Upload $entity, \ArrayObject $options) |
|
|
|
|
166
|
|
|
{ |
167
|
|
|
if ($entity->get('file')->exists()) { |
168
|
|
|
return $entity->get('file')->delete(); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
return true; |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* Puts uploaded file into upload folder |
176
|
|
|
* |
177
|
|
|
* @param Upload $entity upload |
178
|
|
|
* @return void |
179
|
|
|
*/ |
180
|
|
|
private function moveUpload(Upload $entity): void |
181
|
|
|
{ |
182
|
|
|
/** @var File $file */ |
183
|
|
|
$file = $entity->get('file'); |
184
|
|
|
try { |
185
|
|
|
$tmpFile = new File($entity->get('document')['tmp_name']); |
186
|
|
|
if (!$tmpFile->exists()) { |
187
|
|
|
throw new \RuntimeException('Uploaded file not found.'); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
if (!$tmpFile->copy($file->path)) { |
191
|
|
|
throw new \RuntimeException('Uploaded file could not be moved'); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
$mime = $file->info()['mime']; |
195
|
|
|
switch ($mime) { |
196
|
|
|
case 'image/png': |
197
|
|
|
$file = $this->convertToJpeg($file); |
198
|
|
|
$entity->set('type', $file->mime()); |
199
|
|
|
// fall through: png is further processed as jpeg |
200
|
|
|
// no break |
201
|
|
|
case 'image/jpeg': |
202
|
|
|
$this->fixOrientation($file); |
203
|
|
|
$this->resize($file, $this->UploaderConfig->getMaxResize()); |
204
|
|
|
$entity->set('size', $file->size()); |
205
|
|
|
break; |
206
|
|
|
default: |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
$entity->set('name', $file->name); |
210
|
|
|
} catch (\Throwable $e) { |
211
|
|
|
if ($file->exists()) { |
212
|
|
|
$file->delete(); |
213
|
|
|
} |
214
|
|
|
throw new \RuntimeException('Moving uploaded file failed.'); |
215
|
|
|
} |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Convert image file to jpeg |
220
|
|
|
* |
221
|
|
|
* @param File $file the non-jpeg image file handler |
222
|
|
|
* @return File handler to jpeg file |
223
|
|
|
*/ |
224
|
|
|
private function convertToJpeg(File $file): File |
225
|
|
|
{ |
226
|
|
|
$jpeg = new File($file->folder()->path . DS . $file->name() . '.jpg'); |
|
|
|
|
227
|
|
|
|
228
|
|
|
try { |
229
|
|
|
(new SimpleImage()) |
230
|
|
|
->fromFile($file->path) |
231
|
|
|
->toFile($jpeg->path, 'image/jpeg', 100); |
232
|
|
|
} catch (\Throwable $e) { |
233
|
|
|
if ($jpeg->exists()) { |
234
|
|
|
$jpeg->delete(); |
235
|
|
|
} |
236
|
|
|
throw new \RuntimeException('Converting file to jpeg failed.'); |
237
|
|
|
} finally { |
238
|
|
|
$file->delete(); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
return $jpeg; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* Fix image orientation according to image exif data |
246
|
|
|
* |
247
|
|
|
* @param File $file file |
248
|
|
|
* @return File handle to fixed file |
249
|
|
|
*/ |
250
|
|
|
private function fixOrientation(File $file): File |
251
|
|
|
{ |
252
|
|
|
$new = new File($file->path); |
253
|
|
|
(new SimpleImage()) |
254
|
|
|
->fromFile($file->path) |
255
|
|
|
->autoOrient() |
256
|
|
|
->toFile($new->path, null, 100); |
257
|
|
|
|
258
|
|
|
return $new; |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* Resizes a file |
263
|
|
|
* |
264
|
|
|
* @param File $file file to resize |
265
|
|
|
* @param int $target size in bytes |
266
|
|
|
* @return void |
267
|
|
|
* @throws \RuntimeException on resizing error |
268
|
|
|
*/ |
269
|
|
|
private function resize(File $file, int $target): void |
270
|
|
|
{ |
271
|
|
|
$size = $file->size(); |
272
|
|
|
if ($size < $target) { |
273
|
|
|
return; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
$raw = $file->read(); |
277
|
|
|
|
278
|
|
|
list($width, $height) = getimagesizefromstring($raw); |
|
|
|
|
279
|
|
|
$ratio = $size / $target; |
280
|
|
|
$qualityImprovementFactor = 1.2; |
281
|
|
|
$ratio = sqrt($ratio) / $qualityImprovementFactor; |
282
|
|
|
|
283
|
|
|
$newwidth = (int)($width / $ratio); |
284
|
|
|
$newheight = (int)($height / $ratio); |
285
|
|
|
$destination = imagecreatetruecolor($newwidth, $newheight); |
286
|
|
|
if ($destination === false) { |
287
|
|
|
throw new \RuntimeException(); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
$source = imagecreatefromstring($raw); |
|
|
|
|
291
|
|
|
if ($source === false) { |
292
|
|
|
throw new \RuntimeException(); |
293
|
|
|
} |
294
|
|
|
$success = imagecopyresampled($destination, $source, 0, 0, 0, 0, $newwidth, $newheight, $width, $height); |
295
|
|
|
if ($success === false) { |
296
|
|
|
throw new \RuntimeException(); |
297
|
|
|
} |
298
|
|
|
|
299
|
|
|
$type = $file->mime(); |
300
|
|
|
switch ($type) { |
301
|
|
|
case 'image/jpeg': |
302
|
|
|
imagejpeg( |
303
|
|
|
$destination, |
304
|
|
|
$file->path, |
305
|
|
|
$this->UploaderConfig->getJpegCompressionFactor() |
306
|
|
|
); |
307
|
|
|
break; |
308
|
|
|
case 'image/png': |
309
|
|
|
imagepng($destination, $file->path); |
310
|
|
|
break; |
311
|
|
|
default: |
312
|
|
|
throw new \RuntimeException(); |
313
|
|
|
} |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* Validate file by size |
318
|
|
|
* |
319
|
|
|
* @param mixed $check value |
320
|
|
|
* @param array $context context |
321
|
|
|
* @return string|bool |
322
|
|
|
*/ |
323
|
|
|
public function validateFile($check, array $context) |
|
|
|
|
324
|
|
|
{ |
325
|
|
|
/** @var \ImageUploader\Lib\UploaderConfig */ |
326
|
|
|
$UploaderConfig = Configure::read('Saito.Settings.uploader'); |
327
|
|
|
|
328
|
|
|
/// Check file type |
329
|
|
|
if (!$UploaderConfig->hasType($check['type'])) { |
330
|
|
|
return __d('image_uploader', 'validation.error.mimeType', $check['type']); |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/// Check file size |
334
|
|
|
$size = $UploaderConfig->getSize($check['type']); |
335
|
|
|
if (!Validation::fileSize($check, '<', $size)) { |
336
|
|
|
return __d( |
337
|
|
|
'image_uploader', |
338
|
|
|
'validation.error.fileSize', |
339
|
|
|
Number::toReadableSize($size) |
340
|
|
|
); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
return true; |
344
|
|
|
} |
345
|
|
|
} |
346
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.