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\Model\Entity\Upload; |
25
|
|
|
|
26
|
|
|
class UploadsTable extends AppTable |
27
|
|
|
{ |
28
|
|
|
private const MAX_RESIZE = 800 * 1024; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* {@inheritDoc} |
32
|
|
|
*/ |
33
|
|
|
public function initialize(array $config) |
34
|
|
|
{ |
35
|
|
|
$this->addBehavior('Timestamp'); |
36
|
|
|
$this->setEntityClass(Upload::class); |
37
|
|
|
|
38
|
|
|
$this->belongsTo('Users', ['foreignKey' => 'user_id']); |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* {@inheritDoc} |
43
|
|
|
*/ |
44
|
|
|
public function validationDefault(Validator $validator) |
45
|
|
|
{ |
46
|
|
|
$validator |
|
|
|
|
47
|
|
|
->add('id', 'valid', ['rule' => 'numeric']) |
48
|
|
|
->allowEmpty('id', 'create') |
49
|
|
|
->notBlank('name') |
50
|
|
|
->notBlank('size') |
51
|
|
|
->notBlank('type') |
52
|
|
|
->notBlank('user_id') |
53
|
|
|
->requirePresence(['name', 'size', 'type', 'user_id'], 'create'); |
54
|
|
|
|
55
|
|
|
/** @var \ImageUploader\Lib\UploaderConfig */ |
56
|
|
|
$UploaderConfig = Configure::read('Saito.Settings.uploader'); |
57
|
|
|
|
58
|
|
|
$validator->add( |
59
|
|
|
'document', |
60
|
|
|
[ |
61
|
|
|
'mimeType' => [ |
62
|
|
|
'rule' => [ |
63
|
|
|
'mimeType', |
64
|
|
|
$UploaderConfig->getAllTypes(), |
65
|
|
|
], |
66
|
|
|
'message' => __d( |
67
|
|
|
'image_uploader', |
68
|
|
|
'validation.error.mimeType' |
69
|
|
|
) |
70
|
|
|
], |
71
|
|
|
'fileSize' => [ |
72
|
|
|
'rule' => [$this, 'validateFileSize'], |
73
|
|
|
], |
74
|
|
|
] |
75
|
|
|
); |
76
|
|
|
|
77
|
|
|
return $validator; |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* {@inheritDoc} |
82
|
|
|
*/ |
83
|
|
|
public function buildRules(RulesChecker $rules) |
84
|
|
|
{ |
85
|
|
|
/** @var \ImageUploader\Lib\UploaderConfig */ |
86
|
|
|
$UploaderConfig = Configure::read('Saito.Settings.uploader'); |
87
|
|
|
$nMax = $UploaderConfig->getMaxNumberOfUploadsPerUser(); |
88
|
|
|
$rules->add( |
89
|
|
|
function (Upload $entity, array $options) use ($nMax) { |
|
|
|
|
90
|
|
|
$count = $this->findByUserId($entity->get('user_id'))->count(); |
|
|
|
|
91
|
|
|
|
92
|
|
|
return $count < $nMax; |
93
|
|
|
}, |
94
|
|
|
'maxAllowedUploadsPerUser', |
95
|
|
|
[ |
96
|
|
|
'errorField' => 'user_id', |
97
|
|
|
'message' => __d('image_uploader', 'validation.error.maxNumberOfItems', $nMax) |
98
|
|
|
] |
99
|
|
|
); |
100
|
|
|
|
101
|
|
|
// check that user exists |
102
|
|
|
$rules->add($rules->existsIn('user_id', 'Users')); |
103
|
|
|
|
104
|
|
|
// check that same user can't have two items with the same name |
105
|
|
|
$rules->add( |
106
|
|
|
$rules->isUnique( |
107
|
|
|
['name', 'user_id'], |
108
|
|
|
__d('image_uploader', 'validation.error.fileExists') |
109
|
|
|
) |
110
|
|
|
); |
111
|
|
|
|
112
|
|
|
return $rules; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* {@inheritDoc} |
117
|
|
|
*/ |
118
|
|
|
public function beforeSave(Event $event, Upload $entity, \ArrayObject $options) |
|
|
|
|
119
|
|
|
{ |
120
|
|
|
if (!$entity->isDirty('name') && !$entity->isDirty('document')) { |
121
|
|
|
return true; |
122
|
|
|
} |
123
|
|
|
try { |
124
|
|
|
$this->moveUpload($entity); |
125
|
|
|
} catch (\Throwable $e) { |
126
|
|
|
return false; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
return true; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* {@inheritDoc} |
134
|
|
|
*/ |
135
|
|
|
public function beforeDelete(Event $event, Upload $entity, \ArrayObject $options) |
|
|
|
|
136
|
|
|
{ |
137
|
|
|
if ($entity->get('file')->exists()) { |
138
|
|
|
return $entity->get('file')->delete(); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return true; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Puts uploaded file into upload folder |
146
|
|
|
* |
147
|
|
|
* @param Upload $entity upload |
148
|
|
|
* @return void |
149
|
|
|
*/ |
150
|
|
|
private function moveUpload(Upload $entity): void |
151
|
|
|
{ |
152
|
|
|
/** @var File $file */ |
153
|
|
|
$file = $entity->get('file'); |
154
|
|
|
try { |
155
|
|
|
$tmpFile = new File($entity->get('document')['tmp_name']); |
156
|
|
|
if (!$tmpFile->exists()) { |
157
|
|
|
throw new \RuntimeException('Uploaded file not found.'); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
if (!$tmpFile->copy($file->path)) { |
161
|
|
|
throw new \RuntimeException('Uploaded file could not be moved'); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
$mime = $file->info()['mime']; |
165
|
|
|
switch ($mime) { |
166
|
|
|
case 'image/png': |
167
|
|
|
$file = $this->convertToJpeg($file); |
168
|
|
|
// fall through: png is further processed as jpeg |
169
|
|
|
case 'image/jpeg': |
170
|
|
|
$this->fixOrientation($file); |
171
|
|
|
$this->resize($file, self::MAX_RESIZE); |
172
|
|
|
$entity->set('size', $file->size()); |
173
|
|
|
break; |
174
|
|
|
default: |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
$entity->set('name', $file->name); |
178
|
|
|
$entity->set('type', $file->mime()); |
179
|
|
|
} catch (\Throwable $e) { |
180
|
|
|
if ($file->exists()) { |
181
|
|
|
$file->delete(); |
182
|
|
|
} |
183
|
|
|
throw new \RuntimeException('Moving uploaded file failed.'); |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Convert image file to jpeg |
189
|
|
|
* |
190
|
|
|
* @param File $file the non-jpeg image file handler |
191
|
|
|
* @return File handler to jpeg file |
192
|
|
|
*/ |
193
|
|
|
private function convertToJpeg(File $file): File |
194
|
|
|
{ |
195
|
|
|
$jpeg = new File($file->folder()->path . DS . $file->name() . '.jpg'); |
196
|
|
|
|
197
|
|
|
try { |
198
|
|
|
(new SimpleImage()) |
199
|
|
|
->fromFile($file->path) |
200
|
|
|
->toFile($jpeg->path, 'image/jpeg', 75); |
201
|
|
|
} catch (\Throwable $e) { |
202
|
|
|
if ($jpeg->exists()) { |
203
|
|
|
$jpeg->delete(); |
204
|
|
|
} |
205
|
|
|
throw new \RuntimeException('Converting file to jpeg failed.'); |
206
|
|
|
} finally { |
207
|
|
|
$file->delete(); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
return $jpeg; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Fix image orientation according to image exif data |
215
|
|
|
* |
216
|
|
|
* @param File $file file |
217
|
|
|
* @return File handle to fixed file |
218
|
|
|
*/ |
219
|
|
|
private function fixOrientation(File $file): File |
220
|
|
|
{ |
221
|
|
|
$new = new File($file->path); |
222
|
|
|
(new SimpleImage()) |
223
|
|
|
->fromFile($file->path) |
224
|
|
|
->autoOrient() |
225
|
|
|
->toFile($new->path, null, 75); |
226
|
|
|
|
227
|
|
|
return $new; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Resizes a file |
232
|
|
|
* |
233
|
|
|
* @param File $file file to resize |
234
|
|
|
* @param int $target size in bytes |
235
|
|
|
* @return void |
236
|
|
|
*/ |
237
|
|
|
private function resize(File $file, int $target): void |
238
|
|
|
{ |
239
|
|
|
$size = $file->size(); |
240
|
|
|
if ($size < $target) { |
241
|
|
|
return; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
$raw = $file->read(); |
245
|
|
|
|
246
|
|
|
list($width, $height) = getimagesizefromstring($raw); |
247
|
|
|
$ratio = $size / $target; |
248
|
|
|
$ratio = sqrt($ratio); |
249
|
|
|
|
250
|
|
|
$newwidth = (int)($width / $ratio); |
251
|
|
|
$newheight = (int)($height / $ratio); |
252
|
|
|
$destination = imagecreatetruecolor($newwidth, $newheight); |
253
|
|
|
|
254
|
|
|
$source = imagecreatefromstring($raw); |
255
|
|
|
imagecopyresized($destination, $source, 0, 0, 0, 0, $newwidth, $newheight, $width, $height); |
256
|
|
|
|
257
|
|
|
$raw = $destination; |
|
|
|
|
258
|
|
|
|
259
|
|
|
$type = $file->mime(); |
260
|
|
|
switch ($type) { |
261
|
|
|
case 'image/jpeg': |
262
|
|
|
imagejpeg($destination, $file->path); |
263
|
|
|
break; |
264
|
|
|
case 'image/png': |
265
|
|
|
imagepng($destination, $file->path); |
266
|
|
|
break; |
267
|
|
|
default: |
268
|
|
|
throw new \RuntimeException(); |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Validate file by size |
274
|
|
|
* |
275
|
|
|
* @param string $check value |
276
|
|
|
* @param array $context context |
277
|
|
|
* @return bool |
278
|
|
|
*/ |
279
|
|
|
public function validateFileSize($check, array $context) |
|
|
|
|
280
|
|
|
{ |
281
|
|
|
/** @var \ImageUploader\Lib\UploaderConfig */ |
282
|
|
|
$UploaderConfig = Configure::read('Saito.Settings.uploader'); |
283
|
|
|
$type = $check['type']; |
284
|
|
|
|
285
|
|
|
if (!$UploaderConfig->hasType($type)) { |
286
|
|
|
return __d( |
287
|
|
|
'image_uploader', |
288
|
|
|
'validation.error.mimeType', |
289
|
|
|
$type |
290
|
|
|
); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
$size = $UploaderConfig->getSize($check['type']); |
294
|
|
|
$result = Validation::fileSize($check, '<', $size); |
295
|
|
|
|
296
|
|
|
if ($result !== true) { |
297
|
|
|
return __d( |
298
|
|
|
'image_uploader', |
299
|
|
|
'validation.error.fileSize', |
300
|
|
|
Number::toReadableSize($size) |
301
|
|
|
); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
return true; |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.