FileStorage::getTemporaryViewUrl()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
/**
3
 * HiPanel core package
4
 *
5
 * @link      https://hipanel.com/
6
 * @package   hipanel-core
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2014-2019, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hipanel\components;
12
13
use hipanel\helpers\FileHelper;
14
use hipanel\models\File;
15
use hiqdev\hiart\ResponseErrorException;
16
use Yii;
17
use yii\base\Component;
18
use yii\base\ErrorException;
19
use yii\base\Exception;
20
use yii\base\InvalidConfigException;
21
use yii\helpers\Url;
22
use yii\web\ForbiddenHttpException;
23
use yii\web\NotFoundHttpException;
24
use yii\web\UploadedFile;
25
26
/**
27
 * Class FileStorage provides interface to save uploaded files and view saved files
28
 * using integration with HiArt.
29
 */
30
class FileStorage extends Component
31
{
32
    /**
33
     * @var string Secret string used to create hashes of files.
34
     * Required property, must be configured in config
35
     */
36
    public $secret;
37
38
    /**
39
     * @var string Path to the directory for temporary files storage.
40
     * Used to save file after upload until API downloads it.
41
     * Defaults to `@runtime/tmp`
42
     */
43
    public $tempDirectory = '@runtime/tmp';
44
45
    /**
46
     * @var string Path to the directory for files permanent storing.
47
     * Defaults to `@runtime/upload`.
48
     */
49
    public $directory = '@runtime/upload';
50
51
    /**
52
     * @var string The route that will be passed to API in order to download uploaded file.
53
     * The action must accept 2 GET parameters: `filename` and `key`, then
54
     * call [[FileStorage::readTemporary($filename, $key)]] and send file contents in its body.
55
     * Defaults to `@file/temp-view`.
56
     */
57
    public $temporaryViewRoute = '@file/temp-view';
58
59
    /**
60
     * @var string Namespace of the class that represents File.
61
     * Defaults to [[File]].
62
     */
63
    public $fileModelClass = File::class;
64
65
    /** {@inheritdoc} */
66
    public function init()
67
    {
68
        $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...
69
        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...
70
71
        $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...
72
        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...
73
74
        if ($this->secret === null) {
75
            throw new InvalidConfigException('Please, set the "secret" property for the FileStorage component');
76
        }
77
    }
78
79
    /**
80
     * Saves uploaded file under the [[tempDirectory]] with random file name.
81
     *
82
     * @param UploadedFile $file
83
     * @throws ErrorException when file is not saved
84
     * @return string randomly generated file name
85
     */
86
    public function saveUploadedFile(UploadedFile $file)
87
    {
88
        do {
89
            $filename = Yii::$app->security->generateRandomString(16) . '.' . $file->getExtension();
90
            $path = $this->getTemporaryPath($filename);
91
        } while (is_file($path));
92
93
        if (!$file->saveAs($path)) {
94
            throw new ErrorException('Failed to save uploaded file');
95
        }
96
97
        return $filename;
98
    }
99
100
    /**
101
     * Builds path to the temporary location of $filename under the [[tempDirectory]].
102
     *
103
     * @param string $filename
104
     * @return string full path to the temporary file
105
     */
106
    protected function getTemporaryPath($filename = '')
107
    {
108
        return $this->tempDirectory . DIRECTORY_SEPARATOR . $filename;
109
    }
110
111
    /**
112
     * Puts file $filename to the API.
113
     *
114
     * File must be previously saved to the [[tempDirectory]] using [[saveUploadedFile]] method,
115
     * otherwise exception will be thrown.
116
     *
117
     * @param string $filename The temporary file name
118
     * @param string $originalName Original (as file was uploaded) file name. Optional, defaults to $filename
119
     * @throws Exception when file $filename does not exist
120
     * @return File The file model
121
     */
122
    public function put($filename, $originalName = null)
123
    {
124
        if (!is_file($this->getTemporaryPath($filename))) {
125
            throw new Exception('File you are trying to upload does not exist');
126
        }
127
128
        if ($originalName === null) {
129
            $originalName = basename($filename);
130
        }
131
132
        $model = Yii::createObject([
133
            'class' => $this->fileModelClass,
134
            'scenario' => 'put',
135
            'filename' => $originalName,
136
            'url' => $this->getTemporaryViewUrl($filename),
137
        ]);
138
139
        $model->save();
140
141
        unlink($this->getTemporaryPath($filename));
142
143
        return $model;
144
    }
145
146
    /**
147
     * Builds key identifying the [[File]] model to be cached.
148
     * @param integer $fileId
149
     * @return array
150
     * @see get
151
     * @see getFileModel
152
     */
153
    protected function buildCacheKey($fileId)
154
    {
155
        return [static::class, 'file', $fileId, Yii::$app->user->isGuest ? true : Yii::$app->user->id];
156
    }
157
158
    /**
159
     * Gets the path of the file with $id.
160
     *
161
     * Method downloads the requested file from the API and saves it to the local machine.
162
     * Method respects authentication and access rules.
163
     *
164
     * @param integer|File $file the ID of the file, or the [[File]] model.
165
     * When model is passed, no additional query will be performed.
166
     * @param bool $overrideCache whether the cache must be invalidated
167
     * @throws Exception when fails to save file locally
168
     * @throws ForbiddenHttpException when file is not available to client due to policies
169
     * @return string full path to the file. File is located under the [[directory]]
170
     */
171
    public function get($file, $overrideCache = false)
172
    {
173
        if (!$file instanceof $this->fileModelClass) {
174
            $file = $this->getFileModel($file, $overrideCache);
0 ignored issues
show
Bug introduced by
It seems like $file defined by $this->getFileModel($file, $overrideCache) on line 174 can also be of type object<hipanel\models\File>; however, hipanel\components\FileStorage::getFileModel() does only seem to accept integer, 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...
175
        }
176
177
        $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...
178
        if (!is_file($path) || $overrideCache) {
179
            $content = $file->perform('get', ['id' => $file->id]);
0 ignored issues
show
Documentation introduced by
The property id 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...
180
181
            if (!FileHelper::createDirectory(dirname($path))) {
182
                throw new Exception('Failed to create directory');
183
            }
184
185
            if (file_put_contents($path, $content) === false) {
186
                throw new Exception('Failed to create local file');
187
            }
188
        }
189
190
        $cache = $this->getCache();
191
        $key = $this->buildCacheKey($file->id);
0 ignored issues
show
Documentation introduced by
The property id 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...
192
        $cache->set($key, $file);
193
194
        return $path;
195
    }
196
197
    /**
198
     * @return \yii\caching\Cache
199
     */
200
    protected function getCache()
201
    {
202
        return Yii::$app->cache;
203
    }
204
205
    /**
206
     * Retrieves [[File]] model for the $id.
207
     * Uses cache and can get model from it.
208
     *
209
     * @param integer $id the ID of the file
210
     * @param bool $overrideCache whether the cache must be invalidated
211
     * @param bool $throwException whether ForbiddenHttpException will be thrown
212
     * when file is not available due to policies
213
     * @throws ForbiddenHttpException when file is not available to client due
214
     * to policies and $throwException parameter is set to `true`
215
     * @return File
216
     */
217
    public function getFileModel($id, $overrideCache = false, $throwException = true)
218
    {
219
        $cache = $this->getCache();
220
        $key = $this->buildCacheKey($id);
221
222
        /** @var File $file */
223
        $file = $cache->get($key);
224
225
        if ($file !== false && !$overrideCache) {
226
            return $file;
227
        }
228
229
        /** @var File $model */
230
        $model = $this->fileModelClass;
231
        try {
232
            $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 244 which is incompatible with the return type documented by hipanel\components\FileStorage::getFileModel of type hipanel\models\File|null.
Loading history...
233
        } catch (ResponseErrorException $e) {
234
            if ($throwException) {
235
                throw new ForbiddenHttpException($e->getMessage());
236
            }
237
            $file = null;
238
        }
239
240
        if ($file === null && $throwException) {
241
            throw new ForbiddenHttpException('The requested file is not available');
242
        }
243
244
        return $file;
245
    }
246
247
    /**
248
     * Return URL to the route that provides access to the temporary file.
249
     * @param string $filename the file name
250
     * @return string URL
251
     * @see temporaryViewRoute
252
     */
253
    protected function getTemporaryViewUrl($filename)
254
    {
255
        return Url::to([$this->temporaryViewRoute, 'filename' => $filename, 'key' => $this->buildHash($filename)], true);
256
    }
257
258
    /**
259
     * Builds MD5 hash using [[secret]] and $sting.
260
     *
261
     * @param $string
262
     * @return string MD5 hash
263
     */
264
    protected function buildHash($string)
265
    {
266
        return md5(sha1($this->secret) . $string);
267
    }
268
269
    /**
270
     * Gets path to the temporary file $filename located under the [[tempDirectory]].
271
     *
272
     * @param string $filename the file name
273
     * @param string $key secret key that was previously generated by [[buildHash]] method unauthorized access
274
     * @throws ForbiddenHttpException when failed to verify secret $key
275
     * @throws NotFoundHttpException when the requested files does not exist
276
     * @return string path to the temporary file
277
     */
278
    public function getTemporary($filename, $key)
279
    {
280
        if (!Yii::$app->security->compareString($this->buildHash($filename), $key)) {
281
            throw new ForbiddenHttpException('The provided key is invalid');
282
        }
283
284
        $path = $this->getTemporaryPath($filename);
285
        if (!is_file($path)) {
286
            throw new NotFoundHttpException('The requested file does not exist');
287
        }
288
289
        return $path;
290
    }
291
}
292