These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Backend for uploading files from chunks. |
||
4 | * |
||
5 | * This program is free software; you can redistribute it and/or modify |
||
6 | * it under the terms of the GNU General Public License as published by |
||
7 | * the Free Software Foundation; either version 2 of the License, or |
||
8 | * (at your option) any later version. |
||
9 | * |
||
10 | * This program is distributed in the hope that it will be useful, |
||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
13 | * GNU General Public License for more details. |
||
14 | * |
||
15 | * You should have received a copy of the GNU General Public License along |
||
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
18 | * http://www.gnu.org/copyleft/gpl.html |
||
19 | * |
||
20 | * @file |
||
21 | * @ingroup Upload |
||
22 | */ |
||
23 | |||
24 | /** |
||
25 | * Implements uploading from chunks |
||
26 | * |
||
27 | * @ingroup Upload |
||
28 | * @author Michael Dale |
||
29 | */ |
||
30 | class UploadFromChunks extends UploadFromFile { |
||
31 | protected $mOffset; |
||
32 | protected $mChunkIndex; |
||
33 | protected $mFileKey; |
||
34 | protected $mVirtualTempPath; |
||
35 | /** @var LocalRepo */ |
||
36 | private $repo; |
||
37 | |||
38 | /** |
||
39 | * Setup local pointers to stash, repo and user (similar to UploadFromStash) |
||
40 | * |
||
41 | * @param User $user |
||
42 | * @param UploadStash|bool $stash Default: false |
||
43 | * @param FileRepo|bool $repo Default: false |
||
44 | */ |
||
45 | View Code Duplication | public function __construct( User $user, $stash = false, $repo = false ) { |
|
46 | $this->user = $user; |
||
0 ignored issues
–
show
|
|||
47 | |||
48 | if ( $repo ) { |
||
49 | $this->repo = $repo; |
||
0 ignored issues
–
show
It seems like
$repo of type object<FileRepo> or boolean is incompatible with the declared type object<LocalRepo> of property $repo .
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property. Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.. ![]() |
|||
50 | } else { |
||
51 | $this->repo = RepoGroup::singleton()->getLocalRepo(); |
||
52 | } |
||
53 | |||
54 | if ( $stash ) { |
||
55 | $this->stash = $stash; |
||
0 ignored issues
–
show
The property
stash does not exist. Did you maybe forget to declare it?
In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code: class MyClass { }
$x = new MyClass();
$x->foo = true;
Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion: class MyClass {
public $foo;
}
$x = new MyClass();
$x->foo = true;
![]() |
|||
56 | } else { |
||
57 | if ( $user ) { |
||
58 | wfDebug( __METHOD__ . " creating new UploadFromChunks instance for " . $user->getId() . "\n" ); |
||
59 | } else { |
||
60 | wfDebug( __METHOD__ . " creating new UploadFromChunks instance with no user\n" ); |
||
61 | } |
||
62 | $this->stash = new UploadStash( $this->repo, $this->user ); |
||
0 ignored issues
–
show
It seems like
$this->repo can also be of type boolean ; however, UploadStash::__construct() does only seem to accept object<FileRepo> , 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. ![]() |
|||
63 | } |
||
64 | } |
||
65 | |||
66 | /** |
||
67 | * Calls the parent doStashFile and updates the uploadsession table to handle "chunks" |
||
68 | * |
||
69 | * @param User|null $user |
||
70 | * @return UploadStashFile Stashed file |
||
71 | */ |
||
72 | protected function doStashFile( User $user = null ) { |
||
73 | // Stash file is the called on creating a new chunk session: |
||
74 | $this->mChunkIndex = 0; |
||
75 | $this->mOffset = 0; |
||
76 | |||
77 | $this->verifyChunk(); |
||
78 | // Create a local stash target |
||
79 | $this->mStashFile = parent::doStashFile( $user ); |
||
80 | // Update the initial file offset (based on file size) |
||
81 | $this->mOffset = $this->mStashFile->getSize(); |
||
82 | $this->mFileKey = $this->mStashFile->getFileKey(); |
||
83 | |||
84 | // Output a copy of this first to chunk 0 location: |
||
85 | $this->outputChunk( $this->mStashFile->getPath() ); |
||
86 | |||
87 | // Update db table to reflect initial "chunk" state |
||
88 | $this->updateChunkStatus(); |
||
89 | |||
90 | return $this->mStashFile; |
||
91 | } |
||
92 | |||
93 | /** |
||
94 | * Continue chunk uploading |
||
95 | * |
||
96 | * @param string $name |
||
97 | * @param string $key |
||
98 | * @param WebRequestUpload $webRequestUpload |
||
99 | */ |
||
100 | public function continueChunks( $name, $key, $webRequestUpload ) { |
||
101 | $this->mFileKey = $key; |
||
102 | $this->mUpload = $webRequestUpload; |
||
103 | // Get the chunk status form the db: |
||
104 | $this->getChunkStatus(); |
||
105 | |||
106 | $metadata = $this->stash->getMetadata( $key ); |
||
107 | $this->initializePathInfo( $name, |
||
108 | $this->getRealPath( $metadata['us_path'] ), |
||
109 | $metadata['us_size'], |
||
110 | false |
||
111 | ); |
||
112 | } |
||
113 | |||
114 | /** |
||
115 | * Append the final chunk and ready file for parent::performUpload() |
||
116 | * @return FileRepoStatus |
||
117 | */ |
||
118 | public function concatenateChunks() { |
||
119 | $chunkIndex = $this->getChunkIndex(); |
||
120 | wfDebug( __METHOD__ . " concatenate {$this->mChunkIndex} chunks:" . |
||
121 | $this->getOffset() . ' inx:' . $chunkIndex . "\n" ); |
||
122 | |||
123 | // Concatenate all the chunks to mVirtualTempPath |
||
124 | $fileList = []; |
||
125 | // The first chunk is stored at the mVirtualTempPath path so we start on "chunk 1" |
||
126 | for ( $i = 0; $i <= $chunkIndex; $i++ ) { |
||
127 | $fileList[] = $this->getVirtualChunkLocation( $i ); |
||
128 | } |
||
129 | |||
130 | // Get the file extension from the last chunk |
||
131 | $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath ); |
||
132 | // Get a 0-byte temp file to perform the concatenation at |
||
133 | $tmpFile = TempFSFile::factory( 'chunkedupload_', $ext, wfTempDir() ); |
||
134 | $tmpPath = false; // fail in concatenate() |
||
135 | if ( $tmpFile ) { |
||
136 | // keep alive with $this |
||
137 | $tmpPath = $tmpFile->bind( $this )->getPath(); |
||
138 | } |
||
139 | |||
140 | // Concatenate the chunks at the temp file |
||
141 | $tStart = microtime( true ); |
||
142 | $status = $this->repo->concatenate( $fileList, $tmpPath, FileRepo::DELETE_SOURCE ); |
||
143 | $tAmount = microtime( true ) - $tStart; |
||
144 | if ( !$status->isOK() ) { |
||
145 | return $status; |
||
146 | } |
||
147 | |||
148 | wfDebugLog( 'fileconcatenate', "Combined $i chunks in $tAmount seconds." ); |
||
149 | |||
150 | // File system path of the actual full temp file |
||
151 | $this->setTempFile( $tmpPath ); |
||
152 | |||
153 | $ret = $this->verifyUpload(); |
||
154 | if ( $ret['status'] !== UploadBase::OK ) { |
||
155 | wfDebugLog( 'fileconcatenate', "Verification failed for chunked upload" ); |
||
156 | $status->fatal( $this->getVerificationErrorCode( $ret['status'] ) ); |
||
157 | |||
158 | return $status; |
||
159 | } |
||
160 | |||
161 | // Update the mTempPath and mStashFile |
||
162 | // (for FileUpload or normal Stash to take over) |
||
163 | $tStart = microtime( true ); |
||
164 | // This is a re-implementation of UploadBase::tryStashFile(), we can't call it because we |
||
165 | // override doStashFile() with completely different functionality in this class... |
||
166 | $error = $this->runUploadStashFileHook( $this->user ); |
||
167 | if ( $error ) { |
||
168 | call_user_func_array( [ $status, 'fatal' ], $error ); |
||
169 | return $status; |
||
170 | } |
||
171 | try { |
||
172 | $this->mStashFile = parent::doStashFile( $this->user ); |
||
173 | } catch ( UploadStashException $e ) { |
||
174 | $status->fatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() ); |
||
175 | return $status; |
||
176 | } |
||
177 | |||
178 | $tAmount = microtime( true ) - $tStart; |
||
179 | $this->mStashFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo()) |
||
180 | wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds." ); |
||
181 | |||
182 | return $status; |
||
183 | } |
||
184 | |||
185 | /** |
||
186 | * Returns the virtual chunk location: |
||
187 | * @param int $index |
||
188 | * @return string |
||
189 | */ |
||
190 | function getVirtualChunkLocation( $index ) { |
||
191 | return $this->repo->getVirtualUrl( 'temp' ) . |
||
192 | '/' . |
||
193 | $this->repo->getHashPath( |
||
194 | $this->getChunkFileKey( $index ) |
||
195 | ) . |
||
196 | $this->getChunkFileKey( $index ); |
||
197 | } |
||
198 | |||
199 | /** |
||
200 | * Add a chunk to the temporary directory |
||
201 | * |
||
202 | * @param string $chunkPath Path to temporary chunk file |
||
203 | * @param int $chunkSize Size of the current chunk |
||
204 | * @param int $offset Offset of current chunk ( mutch match database chunk offset ) |
||
205 | * @return Status |
||
206 | */ |
||
207 | public function addChunk( $chunkPath, $chunkSize, $offset ) { |
||
208 | // Get the offset before we add the chunk to the file system |
||
209 | $preAppendOffset = $this->getOffset(); |
||
210 | |||
211 | if ( $preAppendOffset + $chunkSize > $this->getMaxUploadSize() ) { |
||
212 | $status = Status::newFatal( 'file-too-large' ); |
||
213 | } else { |
||
214 | // Make sure the client is uploading the correct chunk with a matching offset. |
||
215 | if ( $preAppendOffset == $offset ) { |
||
216 | // Update local chunk index for the current chunk |
||
217 | $this->mChunkIndex++; |
||
218 | try { |
||
219 | # For some reason mTempPath is set to first part |
||
220 | $oldTemp = $this->mTempPath; |
||
221 | $this->mTempPath = $chunkPath; |
||
222 | $this->verifyChunk(); |
||
223 | $this->mTempPath = $oldTemp; |
||
224 | } catch ( UploadChunkVerificationException $e ) { |
||
225 | return Status::newFatal( $e->getMessage() ); |
||
226 | } |
||
227 | $status = $this->outputChunk( $chunkPath ); |
||
228 | if ( $status->isGood() ) { |
||
229 | // Update local offset: |
||
230 | $this->mOffset = $preAppendOffset + $chunkSize; |
||
231 | // Update chunk table status db |
||
232 | $this->updateChunkStatus(); |
||
233 | } |
||
234 | } else { |
||
235 | $status = Status::newFatal( 'invalid-chunk-offset' ); |
||
236 | } |
||
237 | } |
||
238 | |||
239 | return $status; |
||
240 | } |
||
241 | |||
242 | /** |
||
243 | * Update the chunk db table with the current status: |
||
244 | */ |
||
245 | private function updateChunkStatus() { |
||
246 | wfDebug( __METHOD__ . " update chunk status for {$this->mFileKey} offset:" . |
||
247 | $this->getOffset() . ' inx:' . $this->getChunkIndex() . "\n" ); |
||
248 | |||
249 | $dbw = $this->repo->getMasterDB(); |
||
250 | $dbw->update( |
||
251 | 'uploadstash', |
||
252 | [ |
||
253 | 'us_status' => 'chunks', |
||
254 | 'us_chunk_inx' => $this->getChunkIndex(), |
||
255 | 'us_size' => $this->getOffset() |
||
256 | ], |
||
257 | [ 'us_key' => $this->mFileKey ], |
||
258 | __METHOD__ |
||
259 | ); |
||
260 | } |
||
261 | |||
262 | /** |
||
263 | * Get the chunk db state and populate update relevant local values |
||
264 | */ |
||
265 | private function getChunkStatus() { |
||
266 | // get Master db to avoid race conditions. |
||
267 | // Otherwise, if chunk upload time < replag there will be spurious errors |
||
268 | $dbw = $this->repo->getMasterDB(); |
||
269 | $row = $dbw->selectRow( |
||
270 | 'uploadstash', |
||
271 | [ |
||
272 | 'us_chunk_inx', |
||
273 | 'us_size', |
||
274 | 'us_path', |
||
275 | ], |
||
276 | [ 'us_key' => $this->mFileKey ], |
||
277 | __METHOD__ |
||
278 | ); |
||
279 | // Handle result: |
||
280 | if ( $row ) { |
||
281 | $this->mChunkIndex = $row->us_chunk_inx; |
||
282 | $this->mOffset = $row->us_size; |
||
283 | $this->mVirtualTempPath = $row->us_path; |
||
284 | } |
||
285 | } |
||
286 | |||
287 | /** |
||
288 | * Get the current Chunk index |
||
289 | * @return int Index of the current chunk |
||
290 | */ |
||
291 | private function getChunkIndex() { |
||
292 | if ( $this->mChunkIndex !== null ) { |
||
293 | return $this->mChunkIndex; |
||
294 | } |
||
295 | |||
296 | return 0; |
||
297 | } |
||
298 | |||
299 | /** |
||
300 | * Get the offset at which the next uploaded chunk will be appended to |
||
301 | * @return int Current byte offset of the chunk file set |
||
302 | */ |
||
303 | public function getOffset() { |
||
304 | if ( $this->mOffset !== null ) { |
||
305 | return $this->mOffset; |
||
306 | } |
||
307 | |||
308 | return 0; |
||
309 | } |
||
310 | |||
311 | /** |
||
312 | * Output the chunk to disk |
||
313 | * |
||
314 | * @param string $chunkPath |
||
315 | * @throws UploadChunkFileException |
||
316 | * @return FileRepoStatus |
||
317 | */ |
||
318 | private function outputChunk( $chunkPath ) { |
||
319 | // Key is fileKey + chunk index |
||
320 | $fileKey = $this->getChunkFileKey(); |
||
321 | |||
322 | // Store the chunk per its indexed fileKey: |
||
323 | $hashPath = $this->repo->getHashPath( $fileKey ); |
||
324 | $storeStatus = $this->repo->quickImport( $chunkPath, |
||
325 | $this->repo->getZonePath( 'temp' ) . "/{$hashPath}{$fileKey}" ); |
||
326 | |||
327 | // Check for error in stashing the chunk: |
||
328 | if ( !$storeStatus->isOK() ) { |
||
329 | $error = $storeStatus->getErrorsArray(); |
||
330 | $error = reset( $error ); |
||
331 | View Code Duplication | if ( !count( $error ) ) { |
|
332 | $error = $storeStatus->getWarningsArray(); |
||
333 | $error = reset( $error ); |
||
334 | if ( !count( $error ) ) { |
||
335 | $error = [ 'unknown', 'no error recorded' ]; |
||
336 | } |
||
337 | } |
||
338 | throw new UploadChunkFileException( "Error storing file in '$chunkPath': " . |
||
339 | implode( '; ', $error ) ); |
||
340 | } |
||
341 | |||
342 | return $storeStatus; |
||
343 | } |
||
344 | |||
345 | private function getChunkFileKey( $index = null ) { |
||
346 | if ( $index === null ) { |
||
347 | $index = $this->getChunkIndex(); |
||
348 | } |
||
349 | |||
350 | return $this->mFileKey . '.' . $index; |
||
351 | } |
||
352 | |||
353 | /** |
||
354 | * Verify that the chunk isn't really an evil html file |
||
355 | * |
||
356 | * @throws UploadChunkVerificationException |
||
357 | */ |
||
358 | private function verifyChunk() { |
||
359 | // Rest mDesiredDestName here so we verify the name as if it were mFileKey |
||
360 | $oldDesiredDestName = $this->mDesiredDestName; |
||
361 | $this->mDesiredDestName = $this->mFileKey; |
||
362 | $this->mTitle = false; |
||
363 | $res = $this->verifyPartialFile(); |
||
364 | $this->mDesiredDestName = $oldDesiredDestName; |
||
365 | $this->mTitle = false; |
||
366 | if ( is_array( $res ) ) { |
||
367 | throw new UploadChunkVerificationException( $res[0] ); |
||
368 | } |
||
369 | } |
||
370 | } |
||
371 | |||
372 | class UploadChunkZeroLengthFileException extends MWException { |
||
373 | } |
||
374 | |||
375 | class UploadChunkFileException extends MWException { |
||
376 | } |
||
377 | |||
378 | class UploadChunkVerificationException extends MWException { |
||
379 | } |
||
380 |
In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:
Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion: