Completed
Push — master ( 5b5792...2b84bd )
by Schlaefer
03:33 queued 11s
created

UploadsTable::buildRules()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 31
rs 9.424
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\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
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...
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) {
1 ignored issue
show
Unused Code introduced by
The parameter $options 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...
90
                $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...
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)
2 ignored issues
show
Unused Code introduced by
The parameter $event 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...
Unused Code introduced by
The parameter $options 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...
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)
2 ignored issues
show
Unused Code introduced by
The parameter $event 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...
Unused Code introduced by
The parameter $options 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...
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;
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...
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)
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...
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