Completed
Push — develop ( 15b1f0...3fa33d )
by Schlaefer
02:30
created

UploadsTable::beforeMarshal()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 8
rs 10
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
        /** @var \ImageUploader\Lib\UploaderConfig */
71
        $UploaderConfig = Configure::read('Saito.Settings.uploader');
0 ignored issues
show
Unused Code introduced by
$UploaderConfig 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...
72
73
        $validator->add(
74
            'document',
75
            [
76
                'file' => [
77
                    'rule' => [$this, 'validateFile'],
78
                ],
79
            ]
80
        );
81
82
        $validator->add(
83
            'title',
84
            [
85
                'maxLength' => [
86
                    'rule' => ['maxLength', self::FILENAME_MAXLENGTH],
87
                    'message' => __('vld.uploads.title.maxlength', self::FILENAME_MAXLENGTH)
88
                ],
89
            ]
90
        );
91
92
        return $validator;
93
    }
94
95
    /**
96
     * {@inheritDoc}
97
     */
98
    public function buildRules(RulesChecker $rules)
99
    {
100
        /** @var \ImageUploader\Lib\UploaderConfig */
101
        $UploaderConfig = Configure::read('Saito.Settings.uploader');
102
        $nMax = $UploaderConfig->getMaxNumberOfUploadsPerUser();
103
        $rules->add(
104
            function (Upload $entity, array $options) use ($nMax) {
105
                $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...
106
107
                return $count < $nMax;
108
            },
109
            'maxAllowedUploadsPerUser',
110
            [
111
                'errorField' => 'user_id',
112
                'message' => __d('image_uploader', 'validation.error.maxNumberOfItems', $nMax)
113
            ]
114
        );
115
116
        // check that user exists
117
        $rules->add($rules->existsIn('user_id', 'Users'));
118
119
        // check that same user can't have two items with the same name
120
        $rules->add(
121
            $rules->isUnique(
122
                // Don't use a identifier like "name" which changes (jpg->png).
123
                ['title', 'user_id'],
124
                __d('image_uploader', 'validation.error.fileExists')
125
            )
126
        );
127
128
        return $rules;
129
    }
130
131
    /**
132
     * {@inheritDoc}
133
     */
134
    public function beforeMarshal(Event $event, \ArrayObject $data)
135
    {
136
        if (!empty($data['document'])) {
137
            /// Set mime/type by what is determined on the server about the file.
138
            $data['type'] = MimeType::get($data['document']['tmp_name'], $data['name']);
139
            $data['document']['type'] = $data['type'];
140
        }
141
    }
142
143
    /**
144
     * {@inheritDoc}
145
     */
146
    public function beforeSave(Event $event, Upload $entity, \ArrayObject $options)
147
    {
148
        if (!$entity->isDirty('name') && !$entity->isDirty('document')) {
149
            return true;
150
        }
151
        try {
152
            $this->moveUpload($entity);
153
        } catch (\Throwable $e) {
154
            return false;
155
        }
156
157
        return true;
158
    }
159
160
    /**
161
     * {@inheritDoc}
162
     */
163
    public function beforeDelete(Event $event, Upload $entity, \ArrayObject $options)
164
    {
165
        if ($entity->get('file')->exists()) {
166
            return $entity->get('file')->delete();
167
        }
168
169
        return true;
170
    }
171
172
    /**
173
     * Puts uploaded file into upload folder
174
     *
175
     * @param Upload $entity upload
176
     * @return void
177
     */
178
    private function moveUpload(Upload $entity): void
179
    {
180
        /** @var File $file */
181
        $file = $entity->get('file');
182
        try {
183
            $tmpFile = new File($entity->get('document')['tmp_name']);
184
            if (!$tmpFile->exists()) {
185
                throw new \RuntimeException('Uploaded file not found.');
186
            }
187
188
            if (!$tmpFile->copy($file->path)) {
189
                throw new \RuntimeException('Uploaded file could not be moved');
190
            }
191
192
            $mime = $file->info()['mime'];
193
            switch ($mime) {
194
                case 'image/png':
195
                    $file = $this->convertToJpeg($file);
196
                    $entity->set('type', $file->mime());
197
                    // fall through: png is further processed as jpeg
198
                    // no break
199
                case 'image/jpeg':
200
                    $this->fixOrientation($file);
201
                    $this->resize($file, self::MAX_RESIZE);
202
                    $entity->set('size', $file->size());
203
                    break;
204
                default:
205
            }
206
207
            $entity->set('name', $file->name);
208
        } catch (\Throwable $e) {
209
            if ($file->exists()) {
210
                $file->delete();
211
            }
212
            throw new \RuntimeException('Moving uploaded file failed.');
213
        }
214
    }
215
216
    /**
217
     * Convert image file to jpeg
218
     *
219
     * @param File $file the non-jpeg image file handler
220
     * @return File handler to jpeg file
221
     */
222
    private function convertToJpeg(File $file): File
223
    {
224
        $jpeg = new File($file->folder()->path . DS . $file->name() . '.jpg');
225
226
        try {
227
            (new SimpleImage())
228
                ->fromFile($file->path)
229
                ->toFile($jpeg->path, 'image/jpeg', 75);
230
        } catch (\Throwable $e) {
231
            if ($jpeg->exists()) {
232
                $jpeg->delete();
233
            }
234
            throw new \RuntimeException('Converting file to jpeg failed.');
235
        } finally {
236
            $file->delete();
237
        }
238
239
        return $jpeg;
240
    }
241
242
    /**
243
     * Fix image orientation according to image exif data
244
     *
245
     * @param File $file file
246
     * @return File handle to fixed file
247
     */
248
    private function fixOrientation(File $file): File
249
    {
250
        $new = new File($file->path);
251
        (new SimpleImage())
252
            ->fromFile($file->path)
253
            ->autoOrient()
254
            ->toFile($new->path, null, 75);
255
256
        return $new;
257
    }
258
259
    /**
260
     * Resizes a file
261
     *
262
     * @param File $file file to resize
263
     * @param int $target size in bytes
264
     * @return void
265
     */
266
    private function resize(File $file, int $target): void
267
    {
268
        $size = $file->size();
269
        if ($size < $target) {
270
            return;
271
        }
272
273
        $raw = $file->read();
274
275
        list($width, $height) = getimagesizefromstring($raw);
276
        $ratio = $size / $target;
277
        $ratio = sqrt($ratio);
278
279
        $newwidth = (int)($width / $ratio);
280
        $newheight = (int)($height / $ratio);
281
        $destination = imagecreatetruecolor($newwidth, $newheight);
282
283
        $source = imagecreatefromstring($raw);
284
        imagecopyresized($destination, $source, 0, 0, 0, 0, $newwidth, $newheight, $width, $height);
285
286
        $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...
287
288
        $type = $file->mime();
289
        switch ($type) {
290
            case 'image/jpeg':
291
                imagejpeg($destination, $file->path);
292
                break;
293
            case 'image/png':
294
                imagepng($destination, $file->path);
295
                break;
296
            default:
297
                throw new \RuntimeException();
298
        }
299
    }
300
301
    /**
302
     * Validate file by size
303
     *
304
     * @param mixed $check value
305
     * @param array $context context
306
     * @return string|bool
307
     */
308
    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...
309
    {
310
        /** @var \ImageUploader\Lib\UploaderConfig */
311
        $UploaderConfig = Configure::read('Saito.Settings.uploader');
312
313
        /// Check file type
314
        if (!$UploaderConfig->hasType($check['type'])) {
315
            return __d('image_uploader', 'validation.error.mimeType', $check['type']);
316
        }
317
318
        /// Check file size
319
        $size = $UploaderConfig->getSize($check['type']);
320
        if (!Validation::fileSize($check, '<', $size)) {
321
            return __d(
322
                'image_uploader',
323
                'validation.error.fileSize',
324
                Number::toReadableSize($size)
325
            );
326
        }
327
328
        return true;
329
    }
330
}
331