Completed
Push — master ( 522199...324a18 )
by Dmitry
04:01
created

FileStorage::getTemporary()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 7
c 1
b 0
f 1
nc 3
nop 2
dl 0
loc 13
ccs 0
cts 11
cp 0
crap 12
rs 9.4285
1
<?php
2
3
/*
4
 * HiPanel core package
5
 *
6
 * @link      https://hipanel.com/
7
 * @package   hipanel-core
8
 * @license   BSD-3-Clause
9
 * @copyright Copyright (c) 2014-2016, HiQDev (http://hiqdev.com/)
10
 */
11
12
namespace hipanel\components;
13
14
use hipanel\helpers\FileHelper;
15
use hipanel\models\File;
16
use hiqdev\hiart\ErrorResponseException;
17
use Yii;
18
use yii\base\Component;
19
use yii\base\ErrorException;
20
use yii\base\Exception;
21
use yii\base\InvalidConfigException;
22
use yii\helpers\Url;
23
use yii\web\ForbiddenHttpException;
24
use yii\web\NotFoundHttpException;
25
use yii\web\UploadedFile;
26
27
/**
28
 * Class FileStorage provides interface to save uploaded files and view saved files
29
 * using integration with HiArt.
30
 *
31
 * @package hipanel\components
32
 */
33
class FileStorage extends Component
34
{
35
    /**
36
     * @var string Secret string used to create hashes of files.
37
     * Required property, must be configured in config
38
     */
39
    public $secret;
40
41
    /**
42
     * @var string Path to the directory for temporary files storage.
43
     * Used to save file after upload until API downloads it.
44
     * Defaults to `@runtime/tmp`
45
     */
46
    public $tempDirectory = '@runtime/tmp';
47
48
    /**
49
     * @var string Path to the directory for files permanent storing.
50
     * Defaults to `@runtime/upload`.
51
     */
52
    public $directory = '@runtime/upload';
53
54
    /**
55
     * @var string The route that will be passed to API in order to download uploaded file.
56
     * The action must accept 2 GET parameters: `filename` and `key`, then
57
     * call [[FileStorage::readTemporary($filename, $key)]] and send file contents in its body.
58
     * Defaults to `@file/temp-view`.
59
     */
60
    public $temporaryViewRoute = '@file/temp-view';
61
62
    /**
63
     * @var string Namespace of the class that represents File.
64
     * Defaults to [[File]].
65
     */
66
    public $fileModelClass = File::class;
67
68
    /** @inheritdoc */
69
    public function init()
70
    {
71
        $this->tempDirectory = Yii::getAlias($this->tempDirectory);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Yii::getAlias($this->tempDirectory) can also be of type boolean. However, the property $tempDirectory is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
72
        FileHelper::createDirectory($this->tempDirectory);
0 ignored issues
show
Bug introduced by
It seems like $this->tempDirectory can also be of type boolean; however, yii\helpers\BaseFileHelper::createDirectory() 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...
73
74
        $this->directory = Yii::getAlias($this->directory);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Yii::getAlias($this->directory) can also be of type boolean. However, the property $directory is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
75
        FileHelper::createDirectory($this->directory);
0 ignored issues
show
Bug introduced by
It seems like $this->directory can also be of type boolean; however, yii\helpers\BaseFileHelper::createDirectory() 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...
76
77
        if ($this->secret === null) {
78
            throw new InvalidConfigException("Please, set the \"secret\" property for the FileStorage component");
79
        }
80
81
    }
82
83
    /**
84
     * Saves uploaded file under the [[tempDirectory]] with random file name
85
     *
86
     * @param UploadedFile $file
87
     * @return string randomly generated file name
88
     * @throws ErrorException when file is not saved
89
     */
90
    public function saveUploadedFile(UploadedFile $file)
91
    {
92
        do {
93
            $filename = Yii::$app->security->generateRandomString(16) . '.' . $file->getExtension();
94
            $path = $this->getTemporaryPath($filename);
95
        } while (is_file($path));
96
97
        if (!$file->saveAs($path)) {
98
            throw new ErrorException('Failed to save uploaded file');
99
        }
100
101
        return $filename;
102
    }
103
104
    /**
105
     * Builds path to the temporary location of $filename under the [[tempDirectory]]
106
     *
107
     * @param string $filename
108
     * @return string full path to the temporary file
109
     */
110
    protected function getTemporaryPath($filename = '')
111
    {
112
        return $this->tempDirectory . DIRECTORY_SEPARATOR . $filename;
113
    }
114
115
    /**
116
     * Puts file $filename to the API.
117
     *
118
     * File must be previously saved to the [[tempDirectory]] using [[saveUploadedFile]] method,
119
     * otherwise exception will be thrown.
120
     *
121
     * @param string $filename The temporary file name
122
     * @param string $originalName Original (as file was uploaded) file name. Optional, defaults to $filename
123
     * @return File The file model
124
     * @throws Exception when file $filename does not exist
125
     */
126
    public function put($filename, $originalName = null)
127
    {
128
        if (!is_file($this->getTemporaryPath($filename))) {
129
            throw new Exception('File you are trying to upload does not exist');
130
        }
131
132
        if ($originalName === null) {
133
            $originalName = basename($filename);
134
        }
135
136
        $model = Yii::createObject([
137
            'class' => $this->fileModelClass,
138
            'scenario' => 'put',
139
            'filename' => $originalName,
140
            'url' => $this->getTemporaryViewUrl($filename)
141
        ]);
142
143
        $model->save();
144
145
        unlink($this->getTemporaryPath($filename));
146
147
        return $model;
148
    }
149
150
    /**
151
     * Builds key identifying the [[File]] model to be cached.
152
     * @param integer $fileId
153
     * @return array
154
     * @see get
155
     * @see getFileModel
156
     */
157
    protected function buildCacheKey($fileId)
158
    {
159
        return [static::class, 'file', $fileId, Yii::$app->user->isGuest ? true : Yii::$app->user->id];
160
    }
161
162
    /**
163
     * Gets the path of the file with $id
164
     *
165
     * Method downloads the requested file from the API and saves it to the local machine.
166
     * Method respects authentication and access rules.
167
     *
168
     * @param integer $id the ID of the file
169
     * @param bool $overrideCache whether the cache must be invalidated
170
     * @return string full path to the file. File is located under the [[directory]]
171
     * @throws Exception when fails to save file locally
172
     * @throws ForbiddenHttpException when file is not available to client due to policies
173
     */
174
    public function get($id, $overrideCache = false)
175
    {
176
        $file = $this->getFileModel($id, $overrideCache);
177
178
        $path = FileHelper::getPrefixedPath($this->directory, static::buildHash($file->md5));
0 ignored issues
show
Documentation introduced by
The property md5 does not exist on object<hipanel\models\File>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
179
        if (!is_file($path) || $overrideCache) {
180
            $content = $file::perform('Get', ['id' => $id]);
181
182
            if (!FileHelper::createDirectory(dirname($path))) {
183
                throw new \yii\base\Exception("Failed to create directory");
184
            }
185
186
            if (!file_put_contents($path, $content)) {
187
                throw new \yii\base\Exception("Failed to create local file");
188
            }
189
        }
190
191
        $cache = $this->getCache();
192
        $key = $this->buildCacheKey($id);
193
        $cache->set($key, $file, 0);
194
195
        return $path;
196
    }
197
198
    /**
199
     * @return \yii\caching\Cache
200
     */
201
    protected function getCache()
202
    {
203
        return Yii::$app->cache;
204
    }
205
206
    /**
207
     * Retrieves [[File]] model for the $id.
208
     * Uses cache and can get model from it.
209
     *
210
     * @param integer $id the ID of the file
211
     * @param bool $overrideCache whether the cache must be invalidated
212
     * @return File
213
     * @throws ForbiddenHttpException when file is not available to client due to policies
214
     */
215
    public function getFileModel($id, $overrideCache = false)
216
    {
217
        $cache = $this->getCache();
218
        $key = $this->buildCacheKey($id);
219
220
        /** @var File $file */
221
        $file = $cache->get($key);
222
223
        if ($file !== false && !$overrideCache) {
224
            return $file;
225
        }
226
227
        /** @var File $model */
228
        $model = $this->fileModelClass;
229
        try {
230
            $file = $model::find()->where(['id' => $id])->one();
0 ignored issues
show
Bug Compatibility introduced by
The expression $model::find()->where(array('id' => $id))->one(); of type hiqdev\hiart\ActiveRecord|array|null adds the type array to the return on line 235 which is incompatible with the return type documented by hipanel\components\FileStorage::getFileModel of type hipanel\models\File|null.
Loading history...
231
        } catch (ErrorResponseException $e) {
232
            throw new ForbiddenHttpException($e->getMessage());
233
        }
234
235
        return $file;
236
    }
237
238
    /**
239
     * Return URL to the route that provides access to the temporary file.
240
     * @param string $filename the file name
241
     * @return string URL
242
     * @see temporaryViewRoute
243
     */
244
    protected function getTemporaryViewUrl($filename)
245
    {
246
        return Url::to([$this->temporaryViewRoute, 'filename' => $filename, 'key' => $this->buildHash($filename)], true);
247
    }
248
249
    /**
250
     * Builds MD5 hash using [[secret]] and $sting
251
     *
252
     * @param $string
253
     * @return string MD5 hash
254
     */
255
    protected function buildHash($string)
256
    {
257
        return md5(sha1($this->secret) . $string);
258
    }
259
260
    /**
261
     * Gets path to the temporary file $filename located under the [[tempDirectory]]
262
     *
263
     * @param string $filename the file name
264
     * @param string $key secret key that was previously generated by [[buildHash]] method unauthorized access
265
     * @return string path to the temporary file
266
     * @throws ForbiddenHttpException when failed to verify secret $key
267
     * @throws NotFoundHttpException when the requested files does not exist
268
     */
269
    public function getTemporary($filename, $key)
270
    {
271
        if (!Yii::$app->security->compareString($this->buildHash($filename), $key)) {
272
            throw new ForbiddenHttpException('The provided key is invalid');
273
        }
274
275
        $path = $this->getTemporaryPath($filename);
276
        if (!is_file($path)) {
277
            throw new NotFoundHttpException('The requested file does not exist');
278
        }
279
280
        return $path;
281
    }
282
}
283