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); |
|
|
|
|
72
|
|
|
FileHelper::createDirectory($this->tempDirectory); |
|
|
|
|
73
|
|
|
|
74
|
|
|
$this->directory = Yii::getAlias($this->directory); |
|
|
|
|
75
|
|
|
FileHelper::createDirectory($this->directory); |
|
|
|
|
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)); |
|
|
|
|
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(); |
|
|
|
|
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
|
|
|
|
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 theid
property of an instance of theAccount
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.