Completed
Push — master ( a0b650...225222 )
by Schlaefer
06:04 queued 03:08
created

UploadsTable::validateFile()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 22
rs 9.568
c 0
b 0
f 0
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
    private const MAX_RESIZE = 800 * 1024;
44
45
    /**
46
     * {@inheritDoc}
47
     */
48
    public function initialize(array $config)
49
    {
50
        $this->addBehavior('Timestamp');
51
        $this->setEntityClass(Upload::class);
52
53
        $this->belongsTo('Users', ['foreignKey' => 'user_id']);
54
    }
55
56
    /**
57
     * {@inheritDoc}
58
     */
59
    public function validationDefault(Validator $validator)
60
    {
61
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::allowEmpty() has been deprecated with message: 3.7.0 Use allowEmptyString(), allowEmptyArray(), allowEmptyFile(), allowEmptyDate(), allowEmptyTime() or allowEmptyDateTime() instead.

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.

Loading history...
62
            ->add('id', 'valid', ['rule' => 'numeric'])
63
            ->allowEmpty('id', 'create')
64
            ->notBlank('name')
65
            ->notBlank('size')
66
            ->notBlank('type')
67
            ->notBlank('user_id')
68
            ->requirePresence(['name', 'size', 'type', 'user_id'], 'create');
69
70
        $validator->add(
71
            'document',
72
            [
73
                'file' => [
74
                    'rule' => [$this, 'validateFile'],
75
                ],
76
            ]
77
        );
78
79
        $validator->add(
80
            'title',
81
            [
82
                'maxLength' => [
83
                    'rule' => ['maxLength', self::FILENAME_MAXLENGTH],
84
                    'message' => __('vld.uploads.title.maxlength', self::FILENAME_MAXLENGTH)
85
                ],
86
            ]
87
        );
88
89
        return $validator;
90
    }
91
92
    /**
93
     * {@inheritDoc}
94
     */
95
    public function buildRules(RulesChecker $rules)
96
    {
97
        /** @var \ImageUploader\Lib\UploaderConfig */
98
        $UploaderConfig = Configure::read('Saito.Settings.uploader');
99
        $nMax = $UploaderConfig->getMaxNumberOfUploadsPerUser();
100
        $rules->add(
101
            function (Upload $entity, array $options) use ($nMax) {
102
                $count = $this->findByUserId($entity->get('user_id'))->count();
0 ignored issues
show
Documentation Bug introduced by
The method findByUserId does not exist on object<ImageUploader\Model\Table\UploadsTable>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
103
104
                return $count < $nMax;
105
            },
106
            'maxAllowedUploadsPerUser',
107
            [
108
                'errorField' => 'user_id',
109
                'message' => __d('image_uploader', 'validation.error.maxNumberOfItems', $nMax)
110
            ]
111
        );
112
113
        // check that user exists
114
        $rules->add($rules->existsIn('user_id', 'Users'));
115
116
        // check that same user can't have two items with the same name
117
        $rules->add(
118
            $rules->isUnique(
119
                // Don't use a identifier like "name" which changes (jpg->png).
120
                ['title', 'user_id'],
121
                __d('image_uploader', 'validation.error.fileExists')
122
            )
123
        );
124
125
        return $rules;
126
    }
127
128
    /**
129
     * {@inheritDoc}
130
     */
131
    public function beforeMarshal(Event $event, \ArrayObject $data)
132
    {
133
        if (!empty($data['document'])) {
134
            /// Set mime/type by what is determined on the server about the file.
135
            $data['type'] = MimeType::get($data['document']['tmp_name'], $data['name']);
136
            $data['document']['type'] = $data['type'];
137
        }
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143
    public function beforeSave(Event $event, Upload $entity, \ArrayObject $options)
144
    {
145
        if (!$entity->isDirty('name') && !$entity->isDirty('document')) {
146
            return true;
147
        }
148
        try {
149
            $this->moveUpload($entity);
150
        } catch (\Throwable $e) {
151
            return false;
152
        }
153
154
        return true;
155
    }
156
157
    /**
158
     * {@inheritDoc}
159
     */
160
    public function beforeDelete(Event $event, Upload $entity, \ArrayObject $options)
161
    {
162
        if ($entity->get('file')->exists()) {
163
            return $entity->get('file')->delete();
164
        }
165
166
        return true;
167
    }
168
169
    /**
170
     * Puts uploaded file into upload folder
171
     *
172
     * @param Upload $entity upload
173
     * @return void
174
     */
175
    private function moveUpload(Upload $entity): void
176
    {
177
        /** @var File $file */
178
        $file = $entity->get('file');
179
        try {
180
            $tmpFile = new File($entity->get('document')['tmp_name']);
181
            if (!$tmpFile->exists()) {
182
                throw new \RuntimeException('Uploaded file not found.');
183
            }
184
185
            if (!$tmpFile->copy($file->path)) {
186
                throw new \RuntimeException('Uploaded file could not be moved');
187
            }
188
189
            $mime = $file->info()['mime'];
190
            switch ($mime) {
191
                case 'image/png':
192
                    $file = $this->convertToJpeg($file);
193
                    $entity->set('type', $file->mime());
194
                    // fall through: png is further processed as jpeg
195
                    // no break
196
                case 'image/jpeg':
197
                    $this->fixOrientation($file);
198
                    $this->resize($file, self::MAX_RESIZE);
199
                    $entity->set('size', $file->size());
200
                    break;
201
                default:
202
            }
203
204
            $entity->set('name', $file->name);
205
        } catch (\Throwable $e) {
206
            if ($file->exists()) {
207
                $file->delete();
208
            }
209
            throw new \RuntimeException('Moving uploaded file failed.');
210
        }
211
    }
212
213
    /**
214
     * Convert image file to jpeg
215
     *
216
     * @param File $file the non-jpeg image file handler
217
     * @return File handler to jpeg file
218
     */
219
    private function convertToJpeg(File $file): File
220
    {
221
        $jpeg = new File($file->folder()->path . DS . $file->name() . '.jpg');
222
223
        try {
224
            (new SimpleImage())
225
                ->fromFile($file->path)
226
                ->toFile($jpeg->path, 'image/jpeg', 75);
227
        } catch (\Throwable $e) {
228
            if ($jpeg->exists()) {
229
                $jpeg->delete();
230
            }
231
            throw new \RuntimeException('Converting file to jpeg failed.');
232
        } finally {
233
            $file->delete();
234
        }
235
236
        return $jpeg;
237
    }
238
239
    /**
240
     * Fix image orientation according to image exif data
241
     *
242
     * @param File $file file
243
     * @return File handle to fixed file
244
     */
245
    private function fixOrientation(File $file): File
246
    {
247
        $new = new File($file->path);
248
        (new SimpleImage())
249
            ->fromFile($file->path)
250
            ->autoOrient()
251
            ->toFile($new->path, null, 75);
252
253
        return $new;
254
    }
255
256
    /**
257
     * Resizes a file
258
     *
259
     * @param File $file file to resize
260
     * @param int $target size in bytes
261
     * @return void
262
     */
263
    private function resize(File $file, int $target): void
264
    {
265
        $size = $file->size();
266
        if ($size < $target) {
267
            return;
268
        }
269
270
        $raw = $file->read();
271
272
        list($width, $height) = getimagesizefromstring($raw);
273
        $ratio = $size / $target;
274
        $ratio = sqrt($ratio);
275
276
        $newwidth = (int)($width / $ratio);
277
        $newheight = (int)($height / $ratio);
278
        $destination = imagecreatetruecolor($newwidth, $newheight);
279
280
        $source = imagecreatefromstring($raw);
281
        imagecopyresized($destination, $source, 0, 0, 0, 0, $newwidth, $newheight, $width, $height);
282
283
        $raw = $destination;
0 ignored issues
show
Unused Code introduced by
$raw is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
284
285
        $type = $file->mime();
286
        switch ($type) {
287
            case 'image/jpeg':
288
                imagejpeg($destination, $file->path);
289
                break;
290
            case 'image/png':
291
                imagepng($destination, $file->path);
292
                break;
293
            default:
294
                throw new \RuntimeException();
295
        }
296
    }
297
298
    /**
299
     * Validate file by size
300
     *
301
     * @param mixed $check value
302
     * @param array $context context
303
     * @return string|bool
304
     */
305
    public function validateFile($check, array $context)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
306
    {
307
        /** @var \ImageUploader\Lib\UploaderConfig */
308
        $UploaderConfig = Configure::read('Saito.Settings.uploader');
309
310
        /// Check file type
311
        if (!$UploaderConfig->hasType($check['type'])) {
312
            return __d('image_uploader', 'validation.error.mimeType', $check['type']);
313
        }
314
315
        /// Check file size
316
        $size = $UploaderConfig->getSize($check['type']);
317
        if (!Validation::fileSize($check, '<', $size)) {
318
            return __d(
319
                'image_uploader',
320
                'validation.error.fileSize',
321
                Number::toReadableSize($size)
322
            );
323
        }
324
325
        return true;
326
    }
327
}
328