Completed
Push — master ( 6fdefe...ec2cdb )
by Jeff
04:37 queued 01:52
created

Media::shouldDeleteFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 12
c 1
b 0
f 0
rs 9.4285
cc 2
eloc 8
nc 2
nop 0
1
<?php
2
3
namespace app\models\types;
4
5
use Yii;
6
use Mhor\MediaInfo\MediaInfo;
7
use yii\helpers\FileHelper;
8
use yii\helpers\Url;
9
use yii\web\UploadedFile;
10
use app\models\ContentType;
11
12
/**
13
 * This is the model class for Media content type.
14
 *
15
 * @property \FileInstance $upload
16
 * @property  int $size
17
 */
18
abstract class Media extends ContentType
19
{
20
    const BASE_PATH = 'uploads/';
21
    const TYPE_PATH = 'tmp/';
22
    const BASE_URI = '@web/';
23
    public $usable = false;
24
    public $canPreview = true;
25
26
    public $upload;
27
    public $_size;
28
    public $_filename;
29
30
    /**
31
     * Take a file instance and upload it to FS, also save in DB.
32
     *
33
     * @param \FileInstance $fileInstance
34
     *
35
     * @return bool success
36
     */
37
    public function upload($fileInstance)
38
    {
39
        if ($fileInstance === null) {
40
            $this->addError('load', Yii::t('app', 'There\'s no file'));
41
42
            return false;
43
        }
44
45
        $this->upload = $fileInstance;
46
47
        $filename = $this->upload->baseName.'.'.$this->upload->extension;
48
        $tmpFilepath = tempnam(sys_get_temp_dir(), 'LCDS_');
49
50
        if ($this->upload->saveAs($tmpFilepath)) {
51 View Code Duplication
            if (static::validateFile($tmpFilepath)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
52
                return ['filename' => $filename, 'tmppath' => $tmpFilepath, 'duration' => static::getDuration($tmpFilepath)];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array('filename' ...uration($tmpFilepath)); (array<string,string|double|null>) is incompatible with the return type of the parent method app\models\ContentType::upload of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
53
            }
54
            $this->addError('load', Yii::t('app', 'Invalid file'));
55
            unlink($tmpFilepath);
56
57
            return false;
58
        }
59
        $this->addError('load', Yii::t('app', 'Cannot save file'));
60
61
        return false;
62
    }
63
64
    /**
65
     * Custom file validation based on mediainfo description.
66
     *
67
     * @param string $realFilepath filesystem path
68
     *
69
     * @return bool is file valid
70
     */
71
    public static function validateFile($realFilepath)
72
    {
73
        return false;
74
    }
75
76
    /**
77
     * Validate an url based on PHP filter_var.
78
     *
79
     * @param string $url
80
     *
81
     * @return bool valid
82
     */
83
    public static function validateUrl($url)
84
    {
85
        return $url && filter_var($url, FILTER_VALIDATE_URL);
86
    }
87
88
    /**
89
     * Take an url and download it, also save it in DB.
90
     *
91
     * @param string $url
92
     *
93
     * @return bool|string[] error or json success string
94
     */
95
    public function sideload($url)
96
    {
97
        if (!self::validateUrl($url)) {
98
            $this->addError('load', Yii::t('app', 'Empty or incorrect URL'));
99
100
            return false;
101
        }
102
103
        $curl = curl_init($url);
104
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
105
        curl_setopt($curl, CURLOPT_PROXY, Yii::$app->params['proxy']);
106
        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
107
        curl_setopt($curl, CURLOPT_HEADERFUNCTION, [&$this, 'readHeaderFilename']);
108
        $fileContent = curl_exec($curl);
109
        $error = curl_error($curl);
110
        curl_close($curl);
111
112
        if ($error) {
113
            $this->addError('load', Yii::t('app', $error));
114
115
            return false;
116
        }
117
118
        $filename = $this->_filename;
119
        if (!$filename) {
120
            $urlSplit = explode('/', $url);
121
            $filename = $urlSplit[count($urlSplit) - 1];
122
        }
123
124
        $tmpFilepath = tempnam(sys_get_temp_dir(), 'LCDS_');
125
126
        $file = fopen($tmpFilepath, 'w+');
127
        fputs($file, $fileContent);
128
        fclose($file);
129
130
        $fileInstance = new UploadedFile();
131
        $fileInstance->name = $filename;
132
        $fileInstance->tempName = $tmpFilepath;
133
        $fileInstance->type = FileHelper::getMimeType($fileInstance->tempName);
134
        $fileInstance->size = $this->_size;
135
        $this->upload = $fileInstance;
136
137 View Code Duplication
        if (static::validateFile($fileInstance->tempName)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
138
            return ['filename' => $filename, 'tmppath' => $tmpFilepath, 'duration' => static::getDuration($tmpFilepath)];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array('filename' ...uration($tmpFilepath)); (array) is incompatible with the return type of the parent method app\models\ContentType::sideload of type boolean|string[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
139
        }
140
141
        $this->addError('load', Yii::t('app', 'Invalid file'));
142
        unlink($fileInstance->tempName);
143
144
        return false;
145
    }
146
147
    /**
148
     * Header parsing method used to get size & filename.
149
     *
150
     * @param mixed  $curl   curl handler
151
     * @param string $header
152
     *
153
     * @return int header length
154
     */
155
    public function readHeaderFilename($curl, $header)
156
    {
157
        if (strpos($header, 'Content-Length:') === 0 && preg_match('/(\d+)/', $header, $matches)) {
158
            $this->_size = intval(trim($matches[1]));
159
        } elseif (strpos($header, 'Content-Disposition:') === 0 && preg_match('/filename=(.*)$/', $header, $matches)) {
160
            $this->_filename = trim(str_replace('"', '', $matches[1]));
161
        }
162
163
        return strlen($header);
164
    }
165
166
    /**
167
     * Custom error getter for upload/sideload temp file.
168
     *
169
     * @return string error
170
     */
171
    public function getLoadError()
172
    {
173
        $errors = $this->getErrors();
174
        if (array_key_exists('load', $errors) && count($errors['load'])) {
175
            return implode(' - ', $errors['load']);
176
        }
177
178
        return Yii::t('app', 'Incorrect file');
179
    }
180
181
    /**
182
     * Get storage path from web root.
183
     *
184
     * @return string path
185
     */
186
    public static function getPath()
187
    {
188
        return self::BASE_PATH.static::TYPE_PATH;
189
    }
190
191
    /**
192
     * Get Yii aliased storage path.
193
     *
194
     * @return string path
195
     */
196
    public static function getWebPath()
197
    {
198
        return self::BASE_URI.self::getPath();
199
    }
200
201
    /**
202
     * Get filesystem storage path.
203
     *
204
     * @return string path
205
     */
206
    public static function getRealPath()
207
    {
208
        return \Yii::getAlias('@app/').'web/'.self::getPath();
209
    }
210
211
    /**
212
     * Try to get media info for this media.
213
     *
214
     * @return \Mhor\MediaInfo\Container\MediaInfoContainer|null media info
215
     */
216
    protected static function getMediaInfo($realFilepath)
217
    {
218
        try {
219
            return (new MediaInfo())->getInfo($realFilepath);
220
        } catch (\RuntimeException $e) {
221
            return;
222
        }
223
    }
224
225
    /**
226
     * Use mediainfo to parse media duration.
227
     *
228
     * @param string $realFilepath
229
     *
230
     * @return int|null media duration
231
     */
232
    public static function getDuration($realFilepath)
233
    {
234
        $mediainfo = static::getMediaInfo($realFilepath);
235
        if ($mediainfo) {
236
            $general = $mediainfo->getGeneral();
237
            if ($general && $general->get('duration')) {
238
                return ceil($general->get('duration')->getMilliseconds() / 1000);
239
            }
240
        }
241
    }
242
243
    /**
244
     * Before save event
245
     * Handles file movement from tmp directory to proper media storage
246
     * Makes sure there is no overwrite by appending to filename.
247
     *
248
     * @param bool $insert is inserted
249
     *
250
     * @return bool success
251
     */
252
    public function beforeSaveContent($insert, $data)
253
    {
254
        if ($insert) {
255
            list($path, $filename) = explode('§', $data);
256
            $tmppath = FileHelper::normalizePath($path);
257
258
            $parts = explode(DIRECTORY_SEPARATOR, $tmppath);
259
            array_pop($parts); // Remove filename
260
            if (implode(DIRECTORY_SEPARATOR, $parts) == sys_get_temp_dir() && strpos(DIRECTORY_SEPARATOR, $filename) === false && file_exists($tmppath)) {
261
                $filename = static::getUniqFilename(static::getRealPath(), $filename);
262
                $data = static::getWebPath().$filename;
263
                $realFilepath = static::getRealPath().$filename;
264
265
                if (rename($tmppath, $realFilepath)) {
266
                    return $data;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $data; (string) is incompatible with the return type of the parent method app\models\ContentType::beforeSaveContent of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
267
                }
268
            }
269
270
            return false;
271
        }
272
273
        return true;
274
    }
275
276
    /**
277
     * Create unique filename by checking for existence and appending to filename.
278
     *
279
     * @param string $path     filepath
280
     * @param string $filename
281
     *
282
     * @return string unique filename
283
     */
284
    protected static function getUniqFilename($path, $filename)
285
    {
286
        if (!file_exists($path.$filename)) {
287
            return $filename;
288
        }
289
290
        $parts = explode('.', $filename);
291
        if (count($parts) > 1) {
292
            $ext = array_pop($parts);
293
        } else {
294
            $ext = null;
295
        }
296
        $name = implode('.', $parts);
297
298
        $i = 1;
299
        $filename = $name.$i.($ext ? '.'.$ext : '');
300
        while (file_exists($path.$filename)) {
301
            $filename = $name.++$i.($ext ? '.'.$ext : '');
302
        }
303
304
        return $filename;
305
    }
306
307
    /**
308
     * {@inheritdoc}
309
     */
310
    public function processData($data)
311
    {
312
        return Url::to($data);
313
    }
314
}
315