UploadsTable   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 310
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 135
c 1
b 0
f 1
dl 0
loc 310
rs 9.84
wmc 32

11 Methods

Rating   Name   Duplication   Size   Complexity  
A beforeSave() 0 12 4
B resize() 0 44 7
A validateFile() 0 21 3
A buildRules() 0 29 1
A beforeMarshal() 0 6 2
A convertToJpeg() 0 18 3
A beforeDelete() 0 7 2
B moveUpload() 0 35 7
A validationDefault() 0 31 1
A fixOrientation() 0 9 1
A initialize() 0 8 1
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
0 ignored issues
show
Deprecated Code introduced by
The function Cake\Validation\Validator::allowEmpty() has been deprecated: 3.7.0 Use allowEmptyString(), allowEmptyArray(), allowEmptyFile(), allowEmptyDate(), allowEmptyTime() or allowEmptyDateTime() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

68
        /** @scrutinizer ignore-deprecated */ $validator

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.

Loading history...
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) {
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

106
            function (Upload $entity, /** @scrutinizer ignore-unused */ array $options) use ($nMax) {

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

Loading history...
107
                $count = $this->findByUserId($entity->get('user_id'))->count();
0 ignored issues
show
Bug introduced by
The method findByUserId() does not exist on ImageUploader\Model\Table\UploadsTable. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

107
                $count = $this->/** @scrutinizer ignore-call */ findByUserId($entity->get('user_id'))->count();
Loading history...
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)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

136
    public function beforeMarshal(/** @scrutinizer ignore-unused */ Event $event, \ArrayObject $data)

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

Loading history...
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)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

148
    public function beforeSave(/** @scrutinizer ignore-unused */ Event $event, Upload $entity, \ArrayObject $options)

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

Loading history...
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

148
    public function beforeSave(Event $event, Upload $entity, /** @scrutinizer ignore-unused */ \ArrayObject $options)

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

Loading history...
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)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

165
    public function beforeDelete(/** @scrutinizer ignore-unused */ Event $event, Upload $entity, \ArrayObject $options)

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

Loading history...
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

165
    public function beforeDelete(Event $event, Upload $entity, /** @scrutinizer ignore-unused */ \ArrayObject $options)

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

Loading history...
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');
0 ignored issues
show
Bug introduced by
Are you sure $file->name() of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

226
        $jpeg = new File($file->folder()->path . DS . /** @scrutinizer ignore-type */ $file->name() . '.jpg');
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $raw can also be of type false; however, parameter $imagedata of getimagesizefromstring() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

278
        list($width, $height) = getimagesizefromstring(/** @scrutinizer ignore-type */ $raw);
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $raw can also be of type false; however, parameter $image of imagecreatefromstring() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

290
        $source = imagecreatefromstring(/** @scrutinizer ignore-type */ $raw);
Loading history...
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)
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

323
    public function validateFile($check, /** @scrutinizer ignore-unused */ array $context)

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

Loading history...
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