Completed
Pull Request — master (#31)
by
unknown
06:02
created

UploadBehavior::sanitize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
namespace mongosoft\file;
4
5
use Closure;
6
use Yii;
7
use yii\base\Behavior;
8
use yii\base\InvalidConfigException;
9
use yii\base\InvalidParamException;
10
use yii\db\BaseActiveRecord;
11
use yii\helpers\ArrayHelper;
12
use yii\helpers\FileHelper;
13
use yii\web\UploadedFile;
14
15
/**
16
 * UploadBehavior automatically uploads file and fills the specified attribute
17
 * with a value of the name of the uploaded file.
18
 *
19
 * To use UploadBehavior, insert the following code to your ActiveRecord class:
20
 *
21
 * ```php
22
 * use mongosoft\file\UploadBehavior;
23
 *
24
 * function behaviors()
25
 * {
26
 *     return [
27
 *         [
28
 *             'class' => UploadBehavior::className(),
29
 *             'attribute' => 'file',
30
 *             'scenarios' => ['insert', 'update'],
31
 *             'path' => '@webroot/upload/{id}',
32
 *             'url' => '@web/upload/{id}',
33
 *         ],
34
 *     ];
35
 * }
36
 * ```
37
 *
38
 * @author Alexander Mohorev <[email protected]>
39
 * @author Alexey Samoylov <[email protected]>
40
 */
41
class UploadBehavior extends Behavior
42
{
43
    /**
44
     * @event Event an event that is triggered after a file is uploaded.
45
     */
46
    const EVENT_AFTER_UPLOAD = 'afterUpload';
47
48
    /**
49
     * @var string the attribute which holds the attachment.
50
     */
51
    public $attribute;
52
    /**
53
     * @var array the scenarios in which the behavior will be triggered
54
     */
55
    public $scenarios = [];
56
    /**
57
     * @var string the base path or path alias to the directory in which to save files.
58
     */
59
    public $path;
60
    /**
61
     * @var string the base URL or path alias for this file
62
     */
63
    public $url;
64
    /**
65
     * @var bool Getting file instance by name
66
     */
67
    public $instanceByName = false;
68
    /**
69
     * @var boolean|callable generate a new unique name for the file
70
     * set true or anonymous function takes the old filename and returns a new name.
71
     * @see self::generateFileName()
72
     */
73
    public $generateNewName = true;
74
    /**
75
     * @var boolean If `true` current attribute file will be deleted
76
     */
77
    public $unlinkOnSave = true;
78
    /**
79
     * @var boolean If `true` current attribute file will be deleted after model deletion.
80
     */
81
    public $unlinkOnDelete = true;
82
    /**
83
     * @var boolean $deleteTempFile whether to delete the temporary file after saving.
84
     */
85
    public $deleteTempFile = true;
86
87
    /**
88
     * @var UploadedFile the uploaded file instance.
89
     */
90
    private $_file;
91
92
93
    /**
94
     * @inheritdoc
95
     */
96 7
    public function init()
97
    {
98 7
        parent::init();
99
100 7
        if ($this->attribute === null) {
101
            throw new InvalidConfigException('The "attribute" property must be set.');
102
        }
103 7
        if ($this->path === null) {
104
            throw new InvalidConfigException('The "path" property must be set.');
105
        }
106 7
        if ($this->url === null) {
107
            throw new InvalidConfigException('The "url" property must be set.');
108
        }
109 7
    }
110
111
    /**
112
     * @inheritdoc
113
     */
114 7
    public function events()
115
    {
116
        return [
117 7
            BaseActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
118 7
            BaseActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave',
119 7
            BaseActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave',
120 7
            BaseActiveRecord::EVENT_AFTER_INSERT => 'afterSave',
121 7
            BaseActiveRecord::EVENT_AFTER_UPDATE => 'afterSave',
122 7
            BaseActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
123 7
        ];
124
    }
125
126
    /**
127
     * This method is invoked before validation starts.
128
     */
129 4
    public function beforeValidate()
130
    {
131
        /** @var BaseActiveRecord $model */
132 4
        $model = $this->owner;
133 4
        if (in_array($model->scenario, $this->scenarios)) {
134 4
            if (($file = $model->getAttribute($this->attribute)) instanceof UploadedFile) {
135 1
                $this->_file = $file;
136 1
            } else {
137 3
                if ($this->instanceByName === true) {
138
                    $this->_file = UploadedFile::getInstanceByName($this->attribute);
139
                } else {
140 3
                    $this->_file = UploadedFile::getInstance($model, $this->attribute);
141
                }
142
            }
143 4
            if ($this->_file instanceof UploadedFile) {
144 4
                $this->_file->name = $this->getFileName($this->_file);
145 4
                $model->setAttribute($this->attribute, $this->_file);
146 4
            }
147 4
        }
148 4
    }
149
150
    /**
151
     * This method is called at the beginning of inserting or updating a record.
152
     */
153 4
    public function beforeSave()
154
    {
155
        /** @var BaseActiveRecord $model */
156 4
        $model = $this->owner;
157 4
        if (in_array($model->scenario, $this->scenarios)) {
158 4
            if ($this->_file instanceof UploadedFile) {
159 4
                if (!$model->getIsNewRecord() && $model->isAttributeChanged($this->attribute)) {
160 1
                    if ($this->unlinkOnSave === true) {
161 1
                        $this->delete($this->attribute, true);
162 1
                    }
163 1
                }
164 4
                $model->setAttribute($this->attribute, $this->_file->name);
165 4
            } else {
166
                // Protect attribute
167
                unset($model->{$this->attribute});
168
            }
169 4
        } else {
170
            if (!$model->getIsNewRecord() && $model->isAttributeChanged($this->attribute)) {
171
                if ($this->unlinkOnSave === true) {
172
                    $this->delete($this->attribute, true);
173
                }
174
            }
175
        }
176 4
    }
177
178
    /**
179
     * This method is called at the end of inserting or updating a record.
180
     * @throws \yii\base\InvalidParamException
181
     */
182 4
    public function afterSave()
183
    {
184 4
        if ($this->_file instanceof UploadedFile) {
185 4
            $path = $this->getUploadPath($this->attribute);
186 4
            if (is_string($path) && FileHelper::createDirectory(dirname($path))) {
187 4
                $this->save($this->_file, $path);
188 4
                $this->afterUpload();
189 4
            } else {
190
                throw new InvalidParamException("Directory specified in 'path' attribute doesn't exist or cannot be created.");
191
            }
192 4
        }
193 4
    }
194
195
    /**
196
     * This method is invoked after deleting a record.
197
     */
198 4
    public function afterDelete()
199
    {
200
        $attribute = $this->attribute;
201
        if ($this->unlinkOnDelete && $attribute) {
202 4
            $this->delete($attribute);
203
        }
204
    }
205
206
    /**
207
     * Returns file path for the attribute.
208
     * @param string $attribute
209
     * @param boolean $old
210
     * @return string|null the file path.
211
     */
212 4
    public function getUploadPath($attribute, $old = false)
213
    {
214
        /** @var BaseActiveRecord $model */
215 4
        $model = $this->owner;
216 4
        $path = $this->resolvePath($this->path);
217 4
        $fileName = ($old === true) ? $model->getOldAttribute($attribute) : $model->$attribute;
218
219 4
        return $fileName ? Yii::getAlias($path . '/' . $fileName) : null;
220
    }
221
222
    /**
223
     * Returns file url for the attribute.
224
     * @param string $attribute
225
     * @return string|null
226
     */
227
    public function getUploadUrl($attribute)
228
    {
229
        /** @var BaseActiveRecord $model */
230
        $model = $this->owner;
231
        $url = $this->resolvePath($this->url);
232
        $fileName = $model->getOldAttribute($attribute);
233
234
        return $fileName ? Yii::getAlias($url . '/' . $fileName) : null;
235
    }
236
237
    /**
238
     * Replaces all placeholders in path variable with corresponding values.
239
     */
240 4
    protected function resolvePath($path)
241
    {
242
        /** @var BaseActiveRecord $model */
243 4
        $model = $this->owner;
244 4
        return preg_replace_callback('/{([^}]+)}/', function ($matches) use ($model) {
245 4
            $name = $matches[1];
246 4
            $attribute = ArrayHelper::getValue($model, $name);
247 4
            if (is_string($attribute) || is_numeric($attribute)) {
248 4
                return $attribute;
249
            } else {
250
                return $matches[0];
251
            }
252 4
        }, $path);
253
    }
254
255
    /**
256
     * Saves the uploaded file.
257
     * @param UploadedFile $file the uploaded file instance
258
     * @param string $path the file path used to save the uploaded file
259
     * @return boolean true whether the file is saved successfully
260
     */
261 4
    protected function save($file, $path)
262
    {
263 4
        return $file->saveAs($path, $this->deleteTempFile);
264
    }
265
266
    /**
267
     * Deletes old file.
268
     * @param string $attribute
269
     * @param boolean $old
270
     */
271 1
    protected function delete($attribute, $old = false)
272
    {
273 1
        $path = $this->getUploadPath($attribute, $old);
274 1
        if (is_file($path)) {
275
            unlink($path);
276
        }
277 1
    }
278
279
    /**
280
     * @param UploadedFile $file
281
     * @return string
282
     */
283 4
    protected function getFileName($file)
284
    {
285 4
        if ($this->generateNewName) {
286 4
            return $this->generateNewName instanceof Closure
287 4
                ? call_user_func($this->generateNewName, $file)
288 4
                : $this->generateFileName($file);
289
        } else {
290
            return $this->sanitize($file->name);
291
        }
292
    }
293
294
    /**
295
     * Replaces characters in strings that are illegal/unsafe for filename.
296
     *
297
     * #my*  unsaf<e>&file:name?".png
298
     *
299
     * @param string $filename the source filename to be "sanitized"
300
     * @return boolean string the sanitized filename
301
     */
302 1
    public static function sanitize($filename)
303
    {
304 1
        return str_replace([' ', '"', '\'', '&', '/', '\\', '?', '#'], '-', $filename);
305
    }
306
307
    /**
308
     * Generates random filename.
309
     * @param UploadedFile $file
310
     * @return string
311
     */
312 4
    protected function generateFileName($file)
313
    {
314 4
        return uniqid() . '.' . $file->extension;
315
    }
316
317
    /**
318
     * This method is invoked after uploading a file.
319
     * The default implementation raises the [[EVENT_AFTER_UPLOAD]] event.
320
     * You may override this method to do postprocessing after the file is uploaded.
321
     * Make sure you call the parent implementation so that the event is raised properly.
322
     */
323 4
    protected function afterUpload()
324
    {
325 4
        $this->owner->trigger(self::EVENT_AFTER_UPLOAD);
326 4
    }
327
}
328