Issues (8)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/UploadBehavior.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace mohorev\file;
4
5
use Closure;
6
use Yii;
7
use yii\base\Behavior;
8
use yii\base\InvalidArgumentException;
9
use yii\base\InvalidConfigException;
10
use yii\db\BaseActiveRecord;
11
use yii\helpers\ArrayHelper;
12
use yii\helpers\FileHelper;
13
use yii\httpclient\Client;
14
use yii\web\UploadedFile;
15
16
/**
17
 * UploadBehavior automatically uploads file and fills the specified attribute
18
 * with a value of the name of the uploaded file.
19
 *
20
 * To use UploadBehavior, insert the following code to your ActiveRecord class:
21
 *
22
 * ```php
23
 * use mohorev\file\UploadBehavior;
24
 *
25
 * function behaviors()
26
 * {
27
 *     return [
28
 *         [
29
 *             'class' => UploadBehavior::class,
30
 *             'attribute' => 'file',
31
 *             'scenarios' => ['insert', 'update'],
32
 *             'path' => '@webroot/upload/{id}',
33
 *             'url' => '@web/upload/{id}',
34
 *         ],
35
 *     ];
36
 * }
37
 * ```
38
 *
39
 * @author Alexander Mohorev <[email protected]>
40
 * @author Alexey Samoylov <[email protected]>
41
 */
42
class UploadBehavior extends Behavior
43
{
44
    /**
45
     * @event Event an event that is triggered after a file is uploaded.
46
     */
47
    const EVENT_AFTER_UPLOAD = 'afterUpload';
48
49
    /**
50
     * @var string the attribute which holds the attachment.
51
     */
52
    public $attribute;
53
    /**
54
     * @var array the scenarios in which the behavior will be triggered
55
     */
56
    public $scenarios = [];
57
    /**
58
     * @var string|callable|array Base path or path alias to the directory in which to save files,
59
     * or callable for setting up your custom path generation logic.
60
     * If $path is callable, callback signature should be as follow and return a string:
61
     *
62
     * ```php
63
     * function (\yii\db\ActiveRecord $model)
64
     * {
65
     *     // do something...
66
     *     return $string;
67
     * }
68
     * ```
69
     * If this property is set up as array, it should be, for example, like as follow ['\app\models\UserProfile', 'buildAvatarPath'],
70
     * where first element is class name, while second is its static method that should be called for path generation.
71
     *
72
     * Example:
73
     * ```php
74
     * public static function buildAvatarPath(\yii\db\ActiveRecord $model)
75
     * {
76
     *      $basePath = '@webroot/upload/avatars/';
77
     *      $suffix = implode('/', array_slice(str_split(md5($model->id), 2), 0, 2));
78
     *      return $basePath . $suffix;
79
     * }
80
     * ```
81
     */
82
    public $path;
83
    /**
84
     * @var string|callable|array Base URL or path alias for this file,
85
     * or callable for setting up your custom URL generation logic.
86
     * If $url is callable, callback signature should be as follow and return a string:
87
     *
88
     * ```php
89
     * function (\yii\db\ActiveRecord $model)
90
     * {
91
     *     // do something...
92
     *     return $string;
93
     * }
94
     * ```
95
     * If this property is set up as array, it should be, for example, like as follow ['\app\models\UserProfile', 'buildAvatarUrl'],
96
     * where first element is class name, while second is its static method that should be called for URL generation.
97
     *
98
     * Example:
99
     * ```php
100
     * public static function buildAvatarUrl(\yii\db\ActiveRecord $model)
101
     * {
102
     *      $baseUrl = '@web/upload/avatars/';
103
     *      $suffix = implode('/', array_slice(str_split(md5($model->id), 2), 0, 2));
104
     *      return $baseUrl . $suffix;
105
     * }
106
     * ```
107
     */
108
    public $url;
109
    /**
110
     * @var bool Getting file instance by name
111
     */
112
    public $instanceByName = false;
113
    /**
114
     * @var boolean|callable generate a new unique name for the file
115
     * set true or anonymous function takes the old filename and returns a new name.
116
     * @see self::generateFileName()
117
     */
118
    public $generateNewName = true;
119
    /**
120
     * @var boolean If `true` current attribute file will be deleted
121
     */
122
    public $unlinkOnSave = true;
123
    /**
124
     * @var boolean If `true` current attribute file will be deleted after model deletion.
125
     */
126
    public $unlinkOnDelete = true;
127
    /**
128
     * @var boolean $deleteTempFile whether to delete the temporary file after saving.
129
     */
130
    public $deleteTempFile = true;
131
    /**
132
     * @var boolean $deleteEmptyDir whether to delete the empty directory after model deletion.
133
     */
134
    public $deleteEmptyDir = true;
135
    /**
136
     * @var bool restore old value after fail attribute validation
137
     */
138
    public $restoreValueAfterFailValidation = true;
139
    /**
140
     * @var string temporary folder name
141
     */
142
    public $tempFolder = '@runtime';
143
    /**
144
     * @var UploadedFile the uploaded file instance.
145
     */
146
    private $_file;
147
    /**
148
     * @var boolean flag for not generate new filename on import
149
     */
150
    private $_import;
151
    /**
152
     * @var string filename
153
     */
154
    private $_temp_file_path;
155
156
157
    /**
158
     * @inheritdoc
159
     */
160 20
    public function init()
161
    {
162 20
        parent::init();
163
164 20
        if ($this->attribute === null) {
165
            throw new InvalidConfigException('The "attribute" property must be set.');
166
        }
167 20
        if ($this->path === null) {
168
            throw new InvalidConfigException('The "path" property must be set.');
169
        }
170 20
        if ($this->url === null) {
171
            throw new InvalidConfigException('The "url" property must be set.');
172
        }
173 20
    }
174
175
    /**
176
     * @inheritdoc
177
     */
178 20
    public function events()
179
    {
180
        return [
181 20
            BaseActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
182
            BaseActiveRecord::EVENT_AFTER_VALIDATE => 'afterValidate',
183
            BaseActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave',
184
            BaseActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave',
185
            BaseActiveRecord::EVENT_AFTER_INSERT => 'afterSave',
186
            BaseActiveRecord::EVENT_AFTER_UPDATE => 'afterSave',
187
            BaseActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
188
        ];
189
    }
190
191
    /**
192
     * This method is invoked before validation starts.
193
     */
194 12
    public function beforeValidate()
195
    {
196
        /** @var BaseActiveRecord $model */
197 12
        $model = $this->owner;
198 12
        if (in_array($model->scenario, $this->scenarios)) {
199 12
            if (($file = $model->getAttribute($this->attribute)) instanceof UploadedFile) {
200 5
                $this->_file = $file;
201
            } else {
202 8
                if ($this->instanceByName === true) {
203
                    $this->_file = UploadedFile::getInstanceByName($this->attribute);
204
                } else {
205 8
                    $this->_file = UploadedFile::getInstance($model, $this->attribute);
206
                }
207
            }
208 12
            if ($this->_file instanceof UploadedFile) {
209 12
                $this->_file->name = $this->getFileName($this->_file);
210 12
                $model->setAttribute($this->attribute, $this->_file);
211
            }
212
        }
213 12
    }
214
215
    /**
216
     * @param UploadedFile $file
217
     * @return string
218
     */
219 12
    protected function getFileName($file)
220
    {
221 12
        if ($this->generateNewName && !$this->_import) {
222 1
            return $this->generateNewName instanceof Closure
223
                ? call_user_func($this->generateNewName, $file)
224 1
                : $this->generateFileName($file);
225
        } else {
226 11
            return $this->sanitize($file->name);
227
        }
228
    }
229
230
    /**
231
     * Generates random filename.
232
     * @param UploadedFile $file
233
     * @return string
234
     */
235 1
    protected function generateFileName($file)
236
    {
237 1
        return uniqid() . '.' . $file->extension;
238
    }
239
240
    /**
241
     * Replaces characters in strings that are illegal/unsafe for filename.
242
     *
243
     * #my*  unsaf<e>&file:name?".png
244
     *
245
     * @param string $filename the source filename to be "sanitized"
246
     * @return boolean string the sanitized filename
247
     */
248 12
    public static function sanitize($filename)
249
    {
250 12
        return str_replace([' ', '"', '\'', '&', '/', '\\', '?', '#'], '-', $filename);
251
    }
252
253
    /**
254
     * This method is called at the beginning of inserting or updating a record.
255
     */
256 11
    public function beforeSave()
257
    {
258
        /** @var BaseActiveRecord $model */
259 11
        $model = $this->owner;
260 11
        if (in_array($model->scenario, $this->scenarios)) {
261 11
            if ($this->_file instanceof UploadedFile) {
262 10
                if (!$model->getIsNewRecord() && $model->isAttributeChanged($this->attribute)) {
263 5
                    if ($this->unlinkOnSave === true) {
264 5
                        $this->delete($this->attribute, true);
265
                    }
266
                }
267 10
                $model->setAttribute($this->attribute, $this->_file->name);
268 3
            } elseif (!$this->_import) {
269
                // Protect attribute
270 11
                unset($model->{$this->attribute});
271
            }
272
        } else {
273
            if (!$model->getIsNewRecord() && $model->isAttributeChanged($this->attribute)) {
274
                if ($this->unlinkOnSave === true) {
275
                    $this->delete($this->attribute, true);
276
                }
277
            }
278
        }
279 11
    }
280
281
    /**
282
     * Deletes old file.
283
     * @param string $attribute
284
     * @param boolean $old
285
     */
286 6
    protected function delete($attribute, $old = false)
287
    {
288 6
        $path = $this->getUploadPath($attribute, $old);
289
290 6
        $this->deleteFile($path);
0 ignored issues
show
It seems like $path defined by $this->getUploadPath($attribute, $old) on line 288 can also be of type boolean or null; however, mohorev\file\UploadBehavior::deleteFile() does only seem to accept string, 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...
291
292 6
        if ($this->deleteEmptyDir) {
293 6
            $dir = dirname($path);
294 6
            if (is_dir($dir) && count(scandir($dir)) == 2) {
295 1
                rmdir($dir);
296
            }
297
        }
298 6
    }
299
300
    /**
301
     * Returns file path for the attribute.
302
     *
303
     * @param string $attribute
304
     * @param boolean $old
305
     * @return string|null the file path.
306
     * @throws \yii\base\InvalidConfigException
307
     */
308 10
    public function getUploadPath($attribute, $old = false)
309
    {
310
        /** @var BaseActiveRecord $model */
311 10
        $model = $this->owner;
312 10
        $path = $this->resolvePath($this->path);
313 10
        $fileName = ($old === true) ? $model->getOldAttribute($attribute) : $model->$attribute;
314
315 10
        return $fileName ? Yii::getAlias($path . '/' . $fileName) : null;
316
    }
317
318
    /**
319
     * Replaces all placeholders in path variable with corresponding values.
320
     */
321 11
    protected function resolvePath($path)
322
    {
323
        /** @var BaseActiveRecord $model */
324 11
        $model = $this->owner;
325 11
        if (is_string($path)) {
326 11
            return preg_replace_callback('/{([^}]+)}/', function ($matches) use ($model) {
327 11
                $name = $matches[1];
328 11
                $attribute = ArrayHelper::getValue($model, $name);
329 11
                if (is_string($attribute) || is_numeric($attribute)) {
330 11
                    return $attribute;
331
                } else {
332
                    return $matches[0];
333
                }
334 11
            }, $path);
335
        } elseif (is_callable($path) || is_array($path)) {
336
            return call_user_func($path, $model);
337
        } else {
338
            throw new InvalidArgumentException(
339
                '$path argument must be a string, array or callable: ' . gettype($path) . ' given.'
340
            );
341
        }
342
    }
343
344
    /**
345
     * Delete file from path
346
     * @param string $path
347
     */
348 7
    protected function deleteFile($path)
349
    {
350 7
        if (is_file($path)) {
351 4
            unlink($path);
352
        }
353 7
        return;
354
    }
355
356
    /**
357
     * This method is called at the end of inserting or updating a record.
358
     * @throws \yii\base\Exception
359
     */
360 11
    public function afterSave()
361
    {
362 11
        if ($this->_file instanceof UploadedFile) {
363 10
            $path = $this->getUploadPath($this->attribute);
364 10
            if (is_string($path) && FileHelper::createDirectory(dirname($path))) {
365 10
                $this->save($this->_file, $path);
366 10
                $this->deleteTempFile();
367 10
                $this->afterUpload();
368
            } else {
369
                throw new InvalidArgumentException(
370
                    "Directory specified in 'path' attribute doesn't exist or cannot be created."
371
                );
372
            }
373
        }
374 11
    }
375
376
    /**
377
     * Saves the uploaded file.
378
     * @param UploadedFile $file the uploaded file instance
379
     * @param string $path the file path used to save the uploaded file
380
     * @return boolean true whether the file is saved successfully
381
     */
382 10
    protected function save($file, $path)
383
    {
384 10
        return $file->saveAs($path, $this->deleteTempFile);
385
    }
386
387
    /**
388
     * Remove temp file if exist
389
     */
390 11
    protected function deleteTempFile()
391
    {
392 11
        if ($this->_temp_file_path !== null) {
393 3
            $this->deleteFile($this->_temp_file_path);
394 3
            $this->_temp_file_path = null;
395
        }
396 11
    }
397
398
    /**
399
     * This method is invoked after uploading a file.
400
     * The default implementation raises the [[EVENT_AFTER_UPLOAD]] event.
401
     * You may override this method to do postprocessing after the file is uploaded.
402
     * Make sure you call the parent implementation so that the event is raised properly.
403
     */
404 11
    protected function afterUpload()
405
    {
406 11
        $this->owner->trigger(self::EVENT_AFTER_UPLOAD);
407 11
    }
408
409
    /**
410
     * This method is invoked after deleting a record.
411
     */
412 1
    public function afterDelete()
413
    {
414 1
        $attribute = $this->attribute;
415 1
        if ($this->unlinkOnDelete && $attribute) {
416 1
            $this->delete($attribute);
417
        }
418 1
    }
419
420
    /**
421
     * Returns file url for the attribute.
422
     *
423
     * @param string $attribute
424
     * @return string|null
425
     * @throws \yii\base\InvalidConfigException
426
     */
427 1
    public function getUploadUrl($attribute)
428
    {
429
        /** @var BaseActiveRecord $model */
430 1
        $model = $this->owner;
431 1
        $url = $this->resolvePath($this->url);
432 1
        $fileName = $model->getOldAttribute($attribute);
433
434 1
        return $fileName ? Yii::getAlias($url . '/' . $fileName) : null;
435
    }
436
437
    /**
438
     * Set old attribute value if has attribute validation error
439
     */
440 12
    public function afterValidate()
441
    {
442
        /** @var BaseActiveRecord $model */
443 12
        $model = $this->owner;
444
445 12
        if ($this->restoreValueAfterFailValidation && $model->hasErrors($this->attribute)) {
446 2
            $model->setAttribute($this->attribute, $model->getOldAttribute($this->attribute));
447
        }
448
449 12
        return;
450
    }
451
452
    /**
453
     * Set attribute by filename or file content with auto set file extension and validation by mime type
454
     *
455
     * @param string $attribute
456
     * @param string $filePath
457
     * @param string $fileContent
458
     * @throws InvalidConfigException
459
     * @throws \yii\base\Exception
460
     */
461 3
    protected function setAttributeByImportFile($attribute, $filePath, $fileContent = null)
462
    {
463
        /** @var BaseActiveRecord $model */
464 3
        $model = $this->owner;
465
466 3
        $old_value = $model->getAttribute($attribute);
467
468 3
        $temp_filename = uniqid();
469 3
        $temp_file_path = \Yii::getAlias($this->tempFolder) . '/' . $temp_filename;
470
471
        try {
472 3
            if ($fileContent === null) {
473 2
                copy($filePath, $temp_file_path);
474
            } else {
475 1
                file_put_contents($temp_file_path, $fileContent);
476
            }
477
478 3
            $this->_temp_file_path = $temp_file_path;
479
480 3
            $pathinfo = pathinfo($filePath);
481
482
            //check extension by mime type
483 3
            $mime = FileHelper::getMimeType($temp_file_path);
484 3
            $extension = FileHelper::getExtensionsByMimeType($mime);
485
486
            //compare with pathinfo values
487 3
            if (in_array($pathinfo['extension'], $extension)) {
488 3
                $extension = $pathinfo['extension'];
489
            } else {
490
                $extension = $extension[0];
491
            }
492
493
            //get full filename
494 3
            if ($this->generateNewName) {
495
                $full_filename = basename($temp_filename) . '.' . $extension;
496
            } else {
497 3
                $full_filename = $pathinfo['filename'] . '.' . $extension;
498
            }
499
500
            //for validation
501 3
            $upload = new UploadedFile();
502 3
            $upload->tempName = $temp_file_path;
503 3
            $upload->name = basename($full_filename);
504
505 3
            $model->setAttribute($attribute, $upload);
506
            //check validation rules in model
507 3
            if ($model->validate($attribute)) {
508 2
                $this->_file = $upload;
509
510 2
                $file_path = $this->getUploadPath($attribute);
511
                //copy file to uploadpath folder
512 2
                if (is_string($file_path) && FileHelper::createDirectory(dirname($file_path))) {
513 2
                    copy($temp_file_path, $file_path);
514
                }
515
            } else {
516 1
                $model->setAttribute($attribute, $old_value);
517 3
                $this->deleteTempFile();
518
            }
519
        } catch (\Exception $e) {
520
            $this->deleteTempFile();
521
        }
522 3
    }
523
524
    /**
525
     * Upload file from url
526
     *
527
     * @param $attribute string name of attribute with attached UploadBehavior
528
     * @param $url string
529
     * @throws InvalidConfigException
530
     * @throws \yii\base\Exception
531
     * @throws \yii\httpclient\Exception
532
     */
533 2
    public function uploadFromUrl($attribute, $url)
534
    {
535 2
        $this->_import = true;
536
537 2
        $client = new Client();
538
539 2
        $response = $client->createRequest()
540 2
            ->setUrl($url)
541 2
            ->setMethod('GET')
542 2
            ->send();
543
544 2
        if ($response->isOk) {
545 1
            $fileContent = $response->content;
546 1
            $this->setAttributeByImportFile($attribute, $url, $fileContent);
547 1
            $this->afterUpload();
548
        } else {
549 1
            throw new InvalidArgumentException('url $url not valid');
550
        }
551 1
    }
552
553
    /**
554
     * Import file from local storage
555
     *
556
     * @param $attribute string name of attribute with attached UploadBehavior
557
     * @param $filename
558
     * @throws InvalidConfigException
559
     * @throws \yii\base\Exception
560
     */
561 3
    public function uploadFromFile($attribute, $filename)
562
    {
563 3
        $this->_import = true;
564
565 3
        $file_path = \Yii::getAlias($filename);
566
567 3
        if (file_exists($file_path)) {
568 2
            $this->setAttributeByImportFile($attribute, $file_path);
0 ignored issues
show
It seems like $file_path defined by \Yii::getAlias($filename) on line 565 can also be of type boolean; however, mohorev\file\UploadBehav...AttributeByImportFile() does only seem to accept string, 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...
569 2
            $this->afterUpload();
570
        } else {
571 1
            throw new InvalidArgumentException('file $filename not exist');
572
        }
573 2
    }
574
}
575