Completed
Push — master ( 2192f5...189b4a )
by Lorenzo
04:54
created

Uploadable::checkOrCreateUploadBasePath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
3
namespace Padosoft\Uploadable;
4
5
use DB;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Http\UploadedFile;
8
use Illuminate\Support\Facades\Log;
9
use Illuminate\Support\Facades\URL;
10
use Padosoft\Io\DirHelper;
11
use Padosoft\Io\FileHelper;
12
use Padosoft\Laravel\Request\RequestHelper;
13
use Padosoft\Laravel\Request\UploadedFileHelper;
14
15
/**
16
 * Class Uploadable
17
 * Auto upload and save files on save/create/delete model.
18
 * @package Padosoft\Uploadable
19
 */
20
trait Uploadable
21
{
22
    /** @var UploadOptions */
23
    protected $uploadOptions;
24
25
    /**
26
     * Boot the trait.
27
     */
28
    public static function bootUploadable()
29
    {
30
        self::bindCreateEvents();
31
        self::bindSaveEvents();
32
        self::bindUpdateEvents();
33
        self::bindDeleteEvents();
34
    }
35
36
    /**
37
     * Bind create model events.
38
     */
39
    protected static function bindCreateEvents()
40
    {
41
        static::creating(function ($model) {
42
            $model->uploadOptions = $model->getUploadOptionsOrDefault();
43
            $model->guardAgainstInvalidUploadOptions();
44
        });
45
    }
46
47
    /**
48
     * Bind save model events.
49
     */
50
    protected static function bindSaveEvents()
51
    {
52
        static::saving(function (Model $model) {
53
            $model->generateAllNewUploadFileNameAndSetAttribute();
54
        });
55
        static::saved(function (Model $model) {
56
            $model->uploadFiles();
57
        });
58
    }
59
60
    /**
61
     * Bind update model events.
62
     */
63
    protected static function bindUpdateEvents()
64
    {
65
        static::updating(function (Model $model) {
66
            $model->generateAllNewUploadFileNameAndSetAttribute();
67
        });
68
        static::updated(function (Model $model) {
0 ignored issues
show
Bug introduced by
The method updated() does not exist on Padosoft\Uploadable\Uploadable. Did you maybe mean updateDb()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
69
            $model->uploadFiles();
70
        });
71
    }
72
73
    /**
74
     * Bind delete model events.
75
     */
76
    protected static function bindDeleteEvents()
77
    {
78
        static::deleting(function (Model $model) {
79
            $model->uploadOptions = $model->getUploadOptionsOrDefault();
80
            $model->guardAgainstInvalidUploadOptions();
81
        });
82
83
        static::deleted(function (Model $model) {
84
            $model->deleteUploadedFiles();
85
        });
86
    }
87
88
    /**
89
     * Retrive a specifice UploadOptions for this model, or return default UploadOptions
90
     * @return UploadOptions
91
     */
92
    public function getUploadOptionsOrDefault() : UploadOptions
93
    {
94
        if ($this->uploadOptions) {
95
            return $this->uploadOptions;
96
        }
97
98
        if (method_exists($this, 'getUploadOptions')) {
99
            $method = 'getUploadOptions';
100
            $this->uploadOptions = $this->{$method}();
101
        } else {
102
            $this->uploadOptions = UploadOptions::create()->getUploadOptionsDefault()
103
                ->setUploadBasePath(public_path('upload/' . $this->getTable()));
0 ignored issues
show
Bug introduced by
It seems like getTable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
104
        }
105
106
        return $this->uploadOptions;
107
    }
108
109
    /**
110
     * This function will throw an exception when any of the options is missing or invalid.
111
     * @throws InvalidOption
112
     */
113
    public function guardAgainstInvalidUploadOptions()
114
    {
115
        if (!count($this->uploadOptions->uploads)) {
116
            throw InvalidOption::missingUploadFields();
117
        }
118
        if ($this->uploadOptions->uploadBasePath === null || $this->uploadOptions->uploadBasePath == '') {
119
            throw InvalidOption::missingUploadBasePath();
120
        }
121
    }
122
123
    /**
124
     * Handle file upload.
125
     */
126
    public function uploadFiles()
127
    {
128
        //invalid model
129
        if ($this->id < 1) {
0 ignored issues
show
Bug introduced by
The property id does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
130
            return;
131
        }
132
133
        //loop for every upload model attributes and do upload if has a file in request
134
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
135
            $this->uploadFile($uploadField);
136
        }
137
    }
138
139
    /**
140
     * Upload a file releted to a passed attribute name
141
     * @param string $uploadField
142
     */
143
    public function uploadFile(string $uploadField)
144
    {
145
        //check if there is a valid file in request for current attribute
146
        if (!RequestHelper::isValidCurrentRequestUploadFile($uploadField,
147
            $this->getUploadOptionsOrDefault()->uploadsMimeType)
148
        ) {
149
            return;
150
        }
151
152
        //retrive the uploaded file
153
        $uploadedFile = RequestHelper::getCurrentRequestFileSafe($uploadField);
154
        if ($uploadedFile === null) {
155
            return;
156
        }
157
158
        //do the work
159
        $newName = $this->doUpload($uploadedFile, $uploadField);
160
161
        //save on db (not call model save because invoke event and entering in loop)
162
        $this->updateDb($uploadField, $newName);
163
    }
164
165
    /**
166
     * Get an UploadedFile, generate new name, and save it in destination path.
167
     * Return empty string if it fails, otherwise return the saved file name.
168
     * @param UploadedFile $uploadedFile
169
     * @param string $uploadAttribute
170
     * @return string
171
     */
172
    protected function doUpload(UploadedFile $uploadedFile, $uploadAttribute) : string
173
    {
174
        if (!$uploadedFile || !$uploadAttribute) {
175
            return '';
176
        }
177
178
        //get file name by attribute
179
        $newName = $this->{$uploadAttribute};
180
181
        //get upload path to store (method create dir if not exists and return '' if it failed)
182
        $pathToStore = $this->getUploadFileBasePath($uploadAttribute);
183
184
        //delete if file already exists
185
        FileHelper::unlinkSafe($pathToStore . '/' . $newName);
186
187
        //move file to destination folder
188
        try {
189
            $targetFile = $uploadedFile->move($pathToStore, $newName);
190
        } catch (\Symfony\Component\HttpFoundation\File\Exception\FileException $e) {
191
            $targetFile = null;
192
            Log::warning('Error in doUpload() when try to move ' . $newName . ' to folder: ' . $pathToStore . PHP_EOL . $e->getMessage() . PHP_EOL . $e->getTraceAsString());
193
        }
194
195
        return $targetFile ? $newName : '';
196
    }
197
198
    /**
199
     * Check request for valid files and server for correct paths defined in model
200
     * @return bool
201
     */
202
    public function requestHasValidFilesAndCorrectPaths() : bool
203
    {
204
        //current request has not uploaded files
205
        if (!RequestHelper::currentRequestHasFiles()) {
206
            return false;
207
        }
208
209
        //ensure that all upload path are ok or create it.
210
        if (!$this->checkOrCreateAllUploadBasePaths()) {
211
            return false;
212
        }
213
214
        return true;
215
    }
216
217
    /**
218
     * Generate a new file name for uploaded file.
219
     * Return empty string if uploadedFile is null, otherwise return the new file name..
220
     * @param UploadedFile $uploadedFile
221
     * @param string $uploadField
222
     * @return string
223
     */
224
    public function generateNewUploadFileName(UploadedFile $uploadedFile, string $uploadField) : string
0 ignored issues
show
Unused Code introduced by
The parameter $uploadField 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...
225
    {
226
        if (!$uploadedFile) {
227
            return '';
228
        }
229
       if (!$this->id && $this->getUploadOptionsOrDefault()->appendModelIdSuffixInUploadedFileName) {
230
            return '';
231
        }
232
233
        //check if file need a new name
234
        $newName = $this->calcolateNewUploadFileName($uploadedFile);
235
        if ($newName != '') {
236
            return $newName;
237
        }
238
239
        //no new file name, return original file name
240
        return $uploadedFile->getFilename();
241
    }
242
243
    /**
244
     * Check if file need a new name and return it, otherwise return empty string.
245
     * @param UploadedFile $uploadedFile
246
     * @return string
247
     */
248
    public function calcolateNewUploadFileName(UploadedFile $uploadedFile) : string
249
    {
250
        if (!$this->getUploadOptionsOrDefault()->appendModelIdSuffixInUploadedFileName) {
251
            return '';
252
        }
253
254
        //retrive original file name and extension
255
        $filenameWithoutExtension = UploadedFileHelper::getFilenameWithoutExtension($uploadedFile);
256
        $ext = $uploadedFile->getClientOriginalExtension();
257
258
        $newName = $filenameWithoutExtension . $this->getUploadOptionsOrDefault()->uploadFileNameSuffixSeparator . $this->id . '.' . $ext;
259
        return sanitize_filename($newName);
260
    }
261
262
    /**
263
     * delete all Uploaded Files
264
     */
265
    public function deleteUploadedFiles()
266
    {
267
        //loop for every upload model attributes
268
        foreach ($this->getUploadOptionsOrDefault()->uploads as $uploadField) {
269
            $this->deleteUploadedFile($uploadField);
270
        }
271
    }
272
273
    /**
274
     * Delete upload file related to passed attribute name
275
     * @param string $uploadField
276
     */
277
    public function deleteUploadedFile(string $uploadField)
278
    {
279
        if (!$uploadField) {
280
            return;
281
        }
282
283
        if (!$this->{$uploadField}) {
284
            return;
285
        }
286
287
        //retrive correct upload storage path for current attribute
288
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
289
290
        //unlink file
291
        $path = sprintf("%s/%s", $uploadFieldPath, $this->{$uploadField});
292
        FileHelper::unlinkSafe($path);
293
294
        //reset model attribute and update db field
295
        $this->setBlanckAttributeAndDB($uploadField);
296
    }
297
298
    /**
299
     * Reset model attribute and update db field
300
     * @param string $uploadField
301
     */
302
    public function setBlanckAttributeAndDB(string $uploadField)
303
    {
304
        if (!$uploadField) {
305
            return;
306
        }
307
308
        //set to black attribute
309
        $this->{$uploadField} = '';
310
311
        //save on db (not call model save because invoke event and entering in loop)
312
        $this->updateDb($uploadField, '');
313
    }
314
315
    /**
316
     * Return true If All Upload atrributes Are Empty or
317
     * if the uploads array is not set.
318
     * @return bool
319
     */
320
    public function checkIfAllUploadFieldsAreEmpty() : bool
321
    {
322
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
323
            //for performance if one attribute has value exit false
324
            if ($uploadField && $this->{$uploadField}) {
325
                return false;
326
            }
327
        }
328
329
        return true;
330
    }
331
332
    /**
333
     * Check all attributes upload path, and try to create dir if not already exists.
334
     * Return false if it fails to create all founded dirs.
335
     * @return bool
336
     */
337
    public function checkOrCreateAllUploadBasePaths() : bool
338
    {
339
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
340
            if (!$this->checkOrCreateUploadBasePath($uploadField)) {
341
                return false;
342
            }
343
        }
344
345
        return true;
346
    }
347
348
    /**
349
     * Check uploads property and return a uploads class field
350
     * or empty array if somethings wrong.
351
     * @return array
352
     */
353
    public function getUploadsAttributesSafe() : array
354
    {
355
        if (!is_array($this->getUploadOptionsOrDefault()->uploads)) {
356
            return [];
357
        }
358
359
        return $this->getUploadOptionsOrDefault()->uploads;
360
    }
361
362
    /**
363
     * Check attribute upload path, and try to create dir if not already exists.
364
     * Return false if it fails to create the dir.
365
     * @param string $uploadField
366
     * @return bool
367
     */
368
    public function checkOrCreateUploadBasePath(string $uploadField) : bool
369
    {
370
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
371
372
        return DirHelper::checkDirExistOrCreate($uploadFieldPath,
373
            $this->getUploadOptionsOrDefault()->uploadCreateDirModeMask);
374
    }
375
376
    /**
377
     * Return the upload path for the passed attribute and try to create it if not exists.
378
     * Returns empty string if dir if not exists and fails to create it.
379
     * @param string $uploadField
380
     * @return string
381
     */
382
    public function getUploadFileBasePath(string $uploadField) : string
383
    {
384
        //default model upload path
385
        $uploadFieldPath = DirHelper::canonicalize($this->getUploadOptionsOrDefault()->uploadBasePath);
386
387
        //overwrite if there is specific path for the field
388
        $specificPath = $this->getUploadFileBasePathSpecific($uploadField);
389
        if ($specificPath != '') {
390
            $uploadFieldPath = $specificPath;
391
        }
392
393
        //check if exists or try to create dir
394
        if (!DirHelper::checkDirExistOrCreate($uploadFieldPath,
395
            $this->getUploadOptionsOrDefault()->uploadCreateDirModeMask)
396
        ) {
397
            return '';
398
        }
399
400
        return $uploadFieldPath;
401
    }
402
403
    /**
404
     * Return the specific upload path (by uploadPaths prop) for the passed attribute if exists,
405
     * otherwise return empty string.
406
     * If uploadPaths for this field is relative, a public_path was appended.
407
     * @param string $uploadField
408
     * @return string
409
     */
410
    public function getUploadFileBasePathSpecific(string $uploadField) : string
411
    {
412
        //check if there is a specified upload path
413
        if (empty($this->getUploadOptionsOrDefault()->uploadPaths) || count($this->getUploadOptionsOrDefault()->uploadPaths) < 1
414
            || !array_key_exists($uploadField, $this->getUploadOptionsOrDefault()->uploadPaths)
415
        ) {
416
            return '';
417
        }
418
        $path = $this->getUploadOptionsOrDefault()->uploadPaths[$uploadField];
419
        return DirHelper::isAbsolute($path) ? DirHelper::canonicalize($path) : DirHelper::canonicalize(public_path($path));
420
    }
421
422
    /**
423
     * Return the full (path+filename) upload abs path for the passed attribute.
424
     * Returns empty string if dir if not exists.
425
     * @param string $uploadField
426
     * @return string
427
     */
428
    public function getUploadFileFullPath(string $uploadField) : string
429
    {
430
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
431
        $uploadFieldPath = DirHelper::addFinalSlash($uploadFieldPath) . $this->{$uploadField};
432
433
        if ($this->isSlashOrEmptyDir($uploadFieldPath)) {
434
            return '';
435
        }
436
437
        return $uploadFieldPath;
438
    }
439
440
    /**
441
     * Return the full url (base url + filename) for the passed attribute.
442
     * Returns empty string if dir if not exists.
443
     * Ex.:  http://localhost/laravel/public/upload/news/pippo.jpg
444
     * @param string $uploadField
445
     * @return string
446
     */
447 View Code Duplication
    public function getUploadFileUrl(string $uploadField) : string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
448
    {
449
        $Url = $this->getUploadFileFullPath($uploadField);
450
451
        $uploadFieldPath = DirHelper::canonicalize($this->removePublicPath($Url));
452
453
        return $uploadFieldPath == '' ? '' : URL::to($uploadFieldPath);
454
    }
455
456
    /**
457
     * get a path and remove public_path.
458
     * @param string $path
459
     * @return string
460
     */
461
    public function removePublicPath(string $path) : string
462
    {
463
        if ($path == '') {
464
            return '';
465
        }
466
        $path = str_replace(DirHelper::canonicalize(public_path()), '', DirHelper::canonicalize($path));
467
        if ($this->isSlashOrEmptyDir($path)) {
468
            return '';
469
        }
470
471
        return $path;
472
    }
473
474
    /**
475
     * Return the base url (without filename) for the passed attribute.
476
     * Returns empty string if dir if not exists.
477
     * Ex.:  http://localhost/laravel/public/upload/
478
     * @param string $uploadField
479
     * @return string
480
     */
481 View Code Duplication
    public function getUploadFileBaseUrl(string $uploadField) : string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
482
    {
483
        $uploadFieldPath = $this->getUploadFileBasePath($uploadField);
484
485
        $uploadFieldPath = DirHelper::canonicalize(DirHelper::addFinalSlash($this->removePublicPath($uploadFieldPath)));
486
487
        if ($this->isSlashOrEmptyDir($uploadFieldPath)) {
488
            return '';
489
        }
490
491
        return URL::to($uploadFieldPath);
492
    }
493
494
    /**
495
     * Calcolate the new name for ALL uploaded files and set relative upload attributes
496
     */
497
    public function generateAllNewUploadFileNameAndSetAttribute()
498
    {
499
        foreach ($this->getUploadsAttributesSafe() as $uploadField) {
500
            $this->generateNewUploadFileNameAndSetAttribute($uploadField);
501
        }
502
    }
503
504
    /**
505
     * Calcolate the new name for uploaded file relative to passed attribute name and set the upload attribute
506
     * @param string $uploadField
507
     */
508
    public function generateNewUploadFileNameAndSetAttribute(string $uploadField)
509
    {
510
        if (!trim($uploadField)) {
511
            return;
512
        }
513
514
        //generate new file name
515
        $uploadedFile = RequestHelper::getCurrentRequestFileSafe($uploadField);
516
        if ($uploadedFile === null) {
517
            return;
518
        }
519
        $newName = $this->generateNewUploadFileName($uploadedFile, $uploadField);
0 ignored issues
show
Bug introduced by
It seems like $uploadedFile defined by \Padosoft\Laravel\Reques...tFileSafe($uploadField) on line 515 can also be of type array; however, Padosoft\Uploadable\Uplo...rateNewUploadFileName() does only seem to accept object<Illuminate\Http\UploadedFile>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
520
        if ($newName == '') {
521
            return;
522
        }
523
524
        //set attribute
525
        $this->{$uploadField} = $newName;
526
    }
527
528
    /**
529
     * @param string $uploadField
530
     * @param $newName
531
     */
532
    public function updateDb(string $uploadField, $newName)
533
    {
534
        if ($this->id < 1) {
535
            return;
536
        }
537
        DB::table($this->getTable())
0 ignored issues
show
Bug introduced by
It seems like getTable() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
538
            ->where('id', $this->id)
539
            ->update([$uploadField => $newName]);
540
    }
541
542
    /**
543
     * @param string $path
544
     * @return bool
545
     */
546
    public function isSlashOrEmptyDir(string $path):bool
547
    {
548
        return $path === null || $path == '' || $path == '\/' || $path == DIRECTORY_SEPARATOR;
549
    }
550
}
551