Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
3.46% |
19 / 549 |
|
0.00% |
0 / 25 |
CRAP | |
0.00% |
0 / 1 |
SpecialUpload | |
3.47% |
19 / 548 |
|
0.00% |
0 / 25 |
22900.53 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
loadRequest | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
132 | |||
isAsyncUpload | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
userCanExecute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
execute | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
306 | |||
showUploadStatus | |
0.00% |
0 / 64 |
|
0.00% |
0 / 1 |
342 | |||
showUploadProgress | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
42 | |||
showUploadForm | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getUploadForm | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
72 | |||
showRecoverableUploadError | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
showUploadWarning | |
0.00% |
0 / 75 |
|
0.00% |
0 / 1 |
380 | |||
showUploadError | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
performUploadChecks | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
72 | |||
getPageTextAndTags | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
processUpload | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
20 | |||
processAsyncUpload | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
20 | |||
getInitialPageText | |
79.17% |
19 / 24 |
|
0.00% |
0 / 1 |
8.58 | |||
getWatchCheck | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
56 | |||
processVerificationError | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
210 | |||
unsaveUploadedFile | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getExistsWarning | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
90 | |||
getDupeWarning | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
rotationEnabled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Specials; |
22 | |
23 | use BitmapHandler; |
24 | use ChangeTags; |
25 | use ErrorPageError; |
26 | use ImageGalleryBase; |
27 | use JobQueueGroup; |
28 | use LocalFile; |
29 | use LocalRepo; |
30 | use LogEventsList; |
31 | use MediaWiki\Config\Config; |
32 | use MediaWiki\HookContainer\HookRunner; |
33 | use MediaWiki\Html\Html; |
34 | use MediaWiki\HTMLForm\HTMLForm; |
35 | use MediaWiki\Logger\LoggerFactory; |
36 | use MediaWiki\MainConfigNames; |
37 | use MediaWiki\MediaWikiServices; |
38 | use MediaWiki\Message\Message; |
39 | use MediaWiki\Request\FauxRequest; |
40 | use MediaWiki\Request\WebRequest; |
41 | use MediaWiki\SpecialPage\SpecialPage; |
42 | use MediaWiki\Status\Status; |
43 | use MediaWiki\Title\NamespaceInfo; |
44 | use MediaWiki\Title\Title; |
45 | use MediaWiki\User\Options\UserOptionsLookup; |
46 | use MediaWiki\User\User; |
47 | use MediaWiki\Watchlist\WatchlistManager; |
48 | use PermissionsError; |
49 | use Psr\Log\LoggerInterface; |
50 | use RepoGroup; |
51 | use UnexpectedValueException; |
52 | use UploadBase; |
53 | use UploadForm; |
54 | use UploadFromStash; |
55 | use UserBlockedError; |
56 | use WikiFilePage; |
57 | |
58 | /** |
59 | * Form for uploading media files. |
60 | * |
61 | * @ingroup SpecialPage |
62 | * @ingroup Upload |
63 | */ |
64 | class SpecialUpload extends SpecialPage { |
65 | |
66 | private LocalRepo $localRepo; |
67 | private UserOptionsLookup $userOptionsLookup; |
68 | private NamespaceInfo $nsInfo; |
69 | private WatchlistManager $watchlistManager; |
70 | /** @var bool wether uploads by url should be asynchronous or not */ |
71 | public bool $allowAsync; |
72 | private JobQueueGroup $jobQueueGroup; |
73 | private LoggerInterface $log; |
74 | |
75 | /** |
76 | * @param RepoGroup|null $repoGroup |
77 | * @param UserOptionsLookup|null $userOptionsLookup |
78 | * @param NamespaceInfo|null $nsInfo |
79 | * @param WatchlistManager|null $watchlistManager |
80 | */ |
81 | public function __construct( |
82 | ?RepoGroup $repoGroup = null, |
83 | ?UserOptionsLookup $userOptionsLookup = null, |
84 | ?NamespaceInfo $nsInfo = null, |
85 | ?WatchlistManager $watchlistManager = null |
86 | ) { |
87 | parent::__construct( 'Upload', 'upload' ); |
88 | // This class is extended and therefor fallback to global state - T265300 |
89 | $services = MediaWikiServices::getInstance(); |
90 | $this->jobQueueGroup = $services->getJobQueueGroup(); |
91 | $repoGroup ??= $services->getRepoGroup(); |
92 | $this->localRepo = $repoGroup->getLocalRepo(); |
93 | $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup(); |
94 | $this->nsInfo = $nsInfo ?? $services->getNamespaceInfo(); |
95 | $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager(); |
96 | $this->allowAsync = ( |
97 | $this->getConfig()->get( MainConfigNames::EnableAsyncUploads ) && |
98 | $this->getConfig()->get( MainConfigNames::EnableAsyncUploadsByURL ) |
99 | ); |
100 | $this->log = LoggerFactory::getInstance( 'SpecialUpload' ); |
101 | } |
102 | |
103 | public function doesWrites() { |
104 | return true; |
105 | } |
106 | |
107 | // Misc variables |
108 | |
109 | /** @var WebRequest|FauxRequest The request this form is supposed to handle */ |
110 | public $mRequest; |
111 | /** @var string */ |
112 | public $mSourceType; |
113 | |
114 | /** @var string The cache key to use to retreive the status of your async upload */ |
115 | public $mCacheKey; |
116 | |
117 | /** @var UploadBase */ |
118 | public $mUpload; |
119 | |
120 | /** @var LocalFile */ |
121 | public $mLocalFile; |
122 | /** @var bool */ |
123 | public $mUploadClicked; |
124 | |
125 | // User input variables from the "description" section |
126 | |
127 | /** @var string The requested target file name */ |
128 | public $mDesiredDestName; |
129 | /** @var string */ |
130 | public $mComment; |
131 | /** @var string */ |
132 | public $mLicense; |
133 | |
134 | // User input variables from the root section |
135 | |
136 | /** @var bool */ |
137 | public $mIgnoreWarning; |
138 | /** @var bool */ |
139 | public $mWatchthis; |
140 | /** @var string */ |
141 | public $mCopyrightStatus; |
142 | /** @var string */ |
143 | public $mCopyrightSource; |
144 | |
145 | // Hidden variables |
146 | |
147 | /** @var string */ |
148 | public $mDestWarningAck; |
149 | |
150 | /** @var bool The user followed an "overwrite this file" link */ |
151 | public $mForReUpload; |
152 | |
153 | /** @var bool The user clicked "Cancel and return to upload form" button */ |
154 | public $mCancelUpload; |
155 | /** @var bool */ |
156 | public $mTokenOk; |
157 | |
158 | /** @var bool Subclasses can use this to determine whether a file was uploaded */ |
159 | public $mUploadSuccessful = false; |
160 | |
161 | /** @var string Raw html injection point for hooks not using HTMLForm */ |
162 | public $uploadFormTextTop; |
163 | /** @var string Raw html injection point for hooks not using HTMLForm */ |
164 | public $uploadFormTextAfterSummary; |
165 | |
166 | /** |
167 | * Initialize instance variables from request and create an Upload handler |
168 | */ |
169 | protected function loadRequest() { |
170 | $this->mRequest = $request = $this->getRequest(); |
171 | $this->mSourceType = $request->getVal( 'wpSourceType', 'file' ); |
172 | $this->mUpload = UploadBase::createFromRequest( $request ); |
173 | $this->mUploadClicked = $request->wasPosted() |
174 | && ( $request->getCheck( 'wpUpload' ) |
175 | || $request->getCheck( 'wpUploadIgnoreWarning' ) ); |
176 | |
177 | // Guess the desired name from the filename if not provided |
178 | $this->mDesiredDestName = $request->getText( 'wpDestFile' ); |
179 | if ( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null ) { |
180 | $this->mDesiredDestName = $request->getFileName( 'wpUploadFile' ); |
181 | } |
182 | $this->mLicense = $request->getText( 'wpLicense' ); |
183 | |
184 | $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' ); |
185 | $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ) |
186 | || $request->getCheck( 'wpUploadIgnoreWarning' ); |
187 | $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $this->getUser()->isRegistered(); |
188 | $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' ); |
189 | $this->mCopyrightSource = $request->getText( 'wpUploadSource' ); |
190 | |
191 | $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file |
192 | |
193 | $commentDefault = ''; |
194 | $commentMsg = $this->msg( 'upload-default-description' )->inContentLanguage(); |
195 | if ( !$this->mForReUpload && !$commentMsg->isDisabled() ) { |
196 | $commentDefault = $commentMsg->plain(); |
197 | } |
198 | $this->mComment = $request->getText( 'wpUploadDescription', $commentDefault ); |
199 | |
200 | $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' ) |
201 | || $request->getCheck( 'wpReUpload' ); // b/w compat |
202 | |
203 | // If it was posted check for the token (no remote POST'ing with user credentials) |
204 | $token = $request->getVal( 'wpEditToken' ); |
205 | $this->mTokenOk = $this->getUser()->matchEditToken( $token ); |
206 | |
207 | // If this is an upload from Url and we're allowing async processing, |
208 | // check for the presence of the cache key parameter, or compute it. Else, it should be empty. |
209 | if ( $this->isAsyncUpload() ) { |
210 | $this->mCacheKey = \UploadFromUrl::getCacheKeyFromRequest( $request ); |
211 | } else { |
212 | $this->mCacheKey = ''; |
213 | } |
214 | |
215 | $this->uploadFormTextTop = ''; |
216 | $this->uploadFormTextAfterSummary = ''; |
217 | } |
218 | |
219 | /** |
220 | * Check if the current request is an async upload by url request |
221 | * |
222 | * @return bool |
223 | */ |
224 | protected function isAsyncUpload() { |
225 | return ( $this->mSourceType === 'url' && $this->allowAsync ); |
226 | } |
227 | |
228 | /** |
229 | * This page can be shown if uploading is enabled. |
230 | * Handle permission checking elsewhere in order to be able to show |
231 | * custom error messages. |
232 | * |
233 | * @param User $user |
234 | * @return bool |
235 | */ |
236 | public function userCanExecute( User $user ) { |
237 | return UploadBase::isEnabled() && parent::userCanExecute( $user ); |
238 | } |
239 | |
240 | /** |
241 | * @param string|null $par |
242 | */ |
243 | public function execute( $par ) { |
244 | $this->useTransactionalTimeLimit(); |
245 | |
246 | $this->setHeaders(); |
247 | $this->outputHeader(); |
248 | |
249 | # Check uploading enabled |
250 | if ( !UploadBase::isEnabled() ) { |
251 | throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' ); |
252 | } |
253 | |
254 | $this->addHelpLink( 'Help:Managing files' ); |
255 | |
256 | # Check permissions |
257 | $user = $this->getUser(); |
258 | $permissionRequired = UploadBase::isAllowed( $user ); |
259 | if ( $permissionRequired !== true ) { |
260 | throw new PermissionsError( $permissionRequired ); |
261 | } |
262 | |
263 | # Check blocks |
264 | if ( $user->isBlockedFromUpload() ) { |
265 | throw new UserBlockedError( |
266 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null |
267 | $user->getBlock(), |
268 | $user, |
269 | $this->getLanguage(), |
270 | $this->getRequest()->getIP() |
271 | ); |
272 | } |
273 | |
274 | # Check whether we actually want to allow changing stuff |
275 | $this->checkReadOnly(); |
276 | |
277 | $this->loadRequest(); |
278 | |
279 | # Unsave the temporary file in case this was a cancelled upload |
280 | if ( $this->mCancelUpload && !$this->unsaveUploadedFile() ) { |
281 | # Something went wrong, so unsaveUploadedFile showed a warning |
282 | return; |
283 | } |
284 | |
285 | # If we have a cache key, show the upload status. |
286 | if ( $this->mTokenOk && $this->mCacheKey !== '' ) { |
287 | if ( $this->mUpload && $this->mUploadClicked && !$this->mCancelUpload ) { |
288 | # If the user clicked the upload button, we need to process the upload |
289 | $this->processAsyncUpload(); |
290 | } else { |
291 | # Show the upload status |
292 | $this->showUploadStatus( $user ); |
293 | } |
294 | } elseif ( |
295 | # Process upload or show a form |
296 | $this->mTokenOk && !$this->mCancelUpload && |
297 | ( $this->mUpload && $this->mUploadClicked ) |
298 | ) { |
299 | $this->processUpload(); |
300 | } else { |
301 | # Backwards compatibility hook |
302 | if ( !$this->getHookRunner()->onUploadForm_initial( $this ) ) { |
303 | wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" ); |
304 | |
305 | return; |
306 | } |
307 | $this->showUploadForm( $this->getUploadForm() ); |
308 | } |
309 | |
310 | # Cleanup |
311 | if ( $this->mUpload ) { |
312 | $this->mUpload->cleanupTempFile(); |
313 | } |
314 | } |
315 | |
316 | /** |
317 | * Show the upload status |
318 | * |
319 | * @param User $user The owner of the upload |
320 | */ |
321 | protected function showUploadStatus( $user ) { |
322 | // first, let's fetch the status from the main stash |
323 | $progress = UploadBase::getSessionStatus( $user, $this->mCacheKey ); |
324 | if ( !$progress ) { |
325 | $progress = [ 'status' => Status::newFatal( 'invalid-cache-key' ) ]; |
326 | } |
327 | $this->log->debug( 'Upload status: stage {stage}, result {result}', $progress ); |
328 | |
329 | $status = $progress['status'] ?? Status::newFatal( 'invalid-cache-key' ); |
330 | $stage = $progress['stage'] ?? 'unknown'; |
331 | $result = $progress['result'] ?? 'unknown'; |
332 | switch ( $stage ) { |
333 | case 'publish': |
334 | switch ( $result ) { |
335 | case 'Success': |
336 | // The upload is done. Check the result and either show the form with the error |
337 | // occurred, or redirect to the file itself |
338 | // Success, redirect to description page |
339 | $this->mUploadSuccessful = true; |
340 | $this->getHookRunner()->onSpecialUploadComplete( $this ); |
341 | // Redirect to the destination URL, but purge the cache of the file description page first |
342 | // TODO: understand why this is needed |
343 | $title = Title::makeTitleSafe( NS_FILE, $this->mRequest->getText( 'wpDestFile' ) ); |
344 | if ( $title ) { |
345 | $this->log->debug( 'Purging page', [ 'title' => $title->getText() ] ); |
346 | $page = new WikiFilePage( $title ); |
347 | $page->doPurge(); |
348 | } |
349 | $this->getOutput()->redirect( $this->mRequest->getText( 'wpDestUrl' ) ); |
350 | break; |
351 | case 'Warning': |
352 | $this->showUploadWarning( UploadBase::unserializeWarnings( $progress['warnings'] ) ); |
353 | break; |
354 | case 'Failure': |
355 | $details = $status->getValue(); |
356 | // Verification failed. |
357 | if ( is_array( $details ) && isset( $details['verification'] ) ) { |
358 | $this->processVerificationError( $details['verification'] ); |
359 | } else { |
360 | $this->showUploadError( $this->getOutput()->parseAsInterface( |
361 | $status->getWikiText( false, false, $this->getLanguage() ) ) |
362 | ); |
363 | } |
364 | break; |
365 | case 'Poll': |
366 | $this->showUploadProgress( |
367 | [ 'active' => true, 'msg' => 'upload-progress-processing' ] |
368 | ); |
369 | break; |
370 | default: |
371 | // unknown result, just show a generic error |
372 | $this->showUploadError( $this->getOutput()->parseAsInterface( |
373 | $status->getWikiText( false, false, $this->getLanguage() ) ) |
374 | ); |
375 | break; |
376 | } |
377 | break; |
378 | case 'queued': |
379 | // show stalled progress bar |
380 | $this->showUploadProgress( [ 'active' => false, 'msg' => 'upload-progress-queued' ] ); |
381 | break; |
382 | case 'fetching': |
383 | switch ( $result ) { |
384 | case 'Success': |
385 | // The file is being downloaded from a URL |
386 | // TODO: show active progress bar saying we're downloading the file |
387 | $this->showUploadProgress( [ 'active' => true, 'msg' => 'upload-progress-downloading' ] ); |
388 | break; |
389 | case 'Failure': |
390 | // downloading failed |
391 | $this->showUploadError( $this->getOutput()->parseAsInterface( |
392 | $status->getWikiText( false, false, $this->getLanguage() ) ) |
393 | ); |
394 | break; |
395 | default: |
396 | // unknown result, just show a generic error |
397 | $this->showUploadError( $this->getOutput()->parseAsInterface( |
398 | $status->getWikiText( false, false, $this->getLanguage() ) ) |
399 | ); |
400 | break; |
401 | } |
402 | break; |
403 | default: |
404 | // unknown status, just show a generic error |
405 | if ( $status->isOK() ) { |
406 | $status = Status::newFatal( 'upload-progress-unknown' ); |
407 | } |
408 | $statusmsg = $this->getOutput()->parseAsInterface( |
409 | $status->getWikiText( false, false, $this->getLanguage() ) |
410 | ); |
411 | $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . '</h2>' . HTML::errorBox( $statusmsg ); |
412 | $this->showUploadForm( $this->getUploadForm( $message ) ); |
413 | break; |
414 | } |
415 | } |
416 | |
417 | /** |
418 | * Show the upload progress in a form, with a refresh button |
419 | * |
420 | * This is used when the upload is being processed asynchronously. We're |
421 | * forced to use a refresh button because we need to poll the primary mainstash. |
422 | * See UploadBase::getSessionStatus for more information. |
423 | * |
424 | * @param array $options |
425 | * @return void |
426 | */ |
427 | private function showUploadProgress( $options ) { |
428 | // $isActive = $options['active'] ?? false; |
429 | //$progressBarProperty = $isActive ? '' : 'disabled'; |
430 | $message = $this->msg( $options['msg'] )->escaped(); |
431 | $destUrl = $this->mRequest->getText( 'wpDestUrl', '' ); |
432 | if ( !$destUrl && $this->mUpload ) { |
433 | if ( !$this->mLocalFile ) { |
434 | $this->mLocalFile = $this->mUpload->getLocalFile(); |
435 | } |
436 | // This probably means the title is bad, so we can't get the URL |
437 | // but we need to wait for the job to execute. |
438 | if ( $this->mLocalFile === null ) { |
439 | $destUrl = ''; |
440 | } else { |
441 | $destUrl = $this->mLocalFile->getTitle()->getFullURL(); |
442 | } |
443 | } |
444 | |
445 | $destName = $this->mDesiredDestName; |
446 | if ( !$destName ) { |
447 | $destName = $this->mRequest->getText( 'wpDestFile' ); |
448 | } |
449 | |
450 | // Needed if we have warnings to show |
451 | $sourceURL = $this->mRequest->getText( 'wpUploadFileURL' ); |
452 | |
453 | $form = new HTMLForm( [ |
454 | 'CacheKey' => [ |
455 | 'type' => 'hidden', |
456 | 'default' => $this->mCacheKey, |
457 | ], |
458 | 'SourceType' => [ |
459 | 'type' => 'hidden', |
460 | 'default' => $this->mSourceType, |
461 | ], |
462 | 'DestUrl' => [ |
463 | 'type' => 'hidden', |
464 | 'default' => $destUrl, |
465 | ], |
466 | 'DestFile' => [ |
467 | 'type' => 'hidden', |
468 | 'default' => $destName, |
469 | ], |
470 | 'UploadFileURL' => [ |
471 | 'type' => 'hidden', |
472 | 'default' => $sourceURL, |
473 | ], |
474 | ], $this->getContext(), 'uploadProgress' ); |
475 | $form->setSubmitText( $this->msg( 'upload-refresh' )->escaped() ); |
476 | // TODO: use codex, add a progress bar |
477 | //$preHtml = "<cdx-progress-bar aria--label='upload progressbar' $progressBarProperty />"; |
478 | $preHtml = "<div id='upload-progress-message'>$message</div>"; |
479 | $form->addPreHtml( $preHtml ); |
480 | $form->setSubmitCallback( |
481 | static function ( $formData ) { |
482 | return true; |
483 | } |
484 | ); |
485 | $form->prepareForm(); |
486 | $this->getOutput()->addHTML( $form->getHTML( false ) ); |
487 | } |
488 | |
489 | /** |
490 | * Show the main upload form |
491 | * |
492 | * @param HTMLForm|string $form An HTMLForm instance or HTML string to show |
493 | */ |
494 | protected function showUploadForm( $form ) { |
495 | if ( $form instanceof HTMLForm ) { |
496 | $form->show(); |
497 | } else { |
498 | $this->getOutput()->addHTML( $form ); |
499 | } |
500 | } |
501 | |
502 | /** |
503 | * Get an UploadForm instance with title and text properly set. |
504 | * |
505 | * @param string $message HTML string to add to the form |
506 | * @param string|null $sessionKey Session key in case this is a stashed upload |
507 | * @param bool $hideIgnoreWarning Whether to hide "ignore warning" check box |
508 | * @return UploadForm |
509 | */ |
510 | protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) { |
511 | # Initialize form |
512 | $form = new UploadForm( |
513 | [ |
514 | 'watch' => $this->getWatchCheck(), |
515 | 'forreupload' => $this->mForReUpload, |
516 | 'sessionkey' => $sessionKey, |
517 | 'hideignorewarning' => $hideIgnoreWarning, |
518 | 'destwarningack' => (bool)$this->mDestWarningAck, |
519 | |
520 | 'description' => $this->mComment, |
521 | 'texttop' => $this->uploadFormTextTop, |
522 | 'textaftersummary' => $this->uploadFormTextAfterSummary, |
523 | 'destfile' => $this->mDesiredDestName, |
524 | ], |
525 | $this->getContext(), |
526 | $this->getLinkRenderer(), |
527 | $this->localRepo, |
528 | $this->getContentLanguage(), |
529 | $this->nsInfo, |
530 | $this->getHookContainer() |
531 | ); |
532 | $form->setTitle( $this->getPageTitle() ); // Remove subpage |
533 | |
534 | # Check the token, but only if necessary |
535 | if ( |
536 | !$this->mTokenOk && !$this->mCancelUpload && |
537 | ( $this->mUpload && $this->mUploadClicked ) |
538 | ) { |
539 | $form->addPreHtml( $this->msg( 'session_fail_preview' )->parse() ); |
540 | } |
541 | |
542 | # Give a notice if the user is uploading a file that has been deleted or moved |
543 | # Note that this is independent from the message 'filewasdeleted' |
544 | $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); |
545 | $delNotice = ''; // empty by default |
546 | if ( $desiredTitleObj instanceof Title && !$desiredTitleObj->exists() ) { |
547 | LogEventsList::showLogExtract( $delNotice, [ 'delete', 'move' ], |
548 | $desiredTitleObj, |
549 | '', [ 'lim' => 10, |
550 | 'conds' => [ $this->localRepo->getReplicaDB()->expr( 'log_action', '!=', 'revision' ) ], |
551 | 'showIfEmpty' => false, |
552 | 'msgKey' => [ 'upload-recreate-warning' ] ] |
553 | ); |
554 | } |
555 | $form->addPreHtml( $delNotice ); |
556 | |
557 | # Add text to form |
558 | $form->addPreHtml( '<div id="uploadtext">' . |
559 | $this->msg( 'uploadtext', [ $this->mDesiredDestName ] )->parseAsBlock() . |
560 | '</div>' ); |
561 | # Add upload error message |
562 | $form->addPreHtml( $message ); |
563 | |
564 | # Add footer to form |
565 | $uploadFooter = $this->msg( 'uploadfooter' ); |
566 | if ( !$uploadFooter->isDisabled() ) { |
567 | $form->addPostHtml( '<div id="mw-upload-footer-message">' |
568 | . $uploadFooter->parseAsBlock() . "</div>\n" ); |
569 | } |
570 | |
571 | return $form; |
572 | } |
573 | |
574 | /** |
575 | * Stashes the upload and shows the main upload form. |
576 | * |
577 | * Note: only errors that can be handled by changing the name or |
578 | * description should be redirected here. It should be assumed that the |
579 | * file itself is sensible and has passed UploadBase::verifyFile. This |
580 | * essentially means that UploadBase::VERIFICATION_ERROR and |
581 | * UploadBase::EMPTY_FILE should not be passed here. |
582 | * |
583 | * @param string $message HTML message to be passed to mainUploadForm |
584 | */ |
585 | protected function showRecoverableUploadError( $message ) { |
586 | $stashStatus = $this->mUpload->tryStashFile( $this->getUser() ); |
587 | if ( $stashStatus->isGood() ) { |
588 | $sessionKey = $stashStatus->getValue()->getFileKey(); |
589 | $uploadWarning = 'upload-tryagain'; |
590 | } else { |
591 | $sessionKey = null; |
592 | $uploadWarning = 'upload-tryagain-nostash'; |
593 | } |
594 | $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . '</h2>' . |
595 | Html::errorBox( $message ); |
596 | |
597 | $form = $this->getUploadForm( $message, $sessionKey ); |
598 | $form->setSubmitText( $this->msg( $uploadWarning )->escaped() ); |
599 | $this->showUploadForm( $form ); |
600 | } |
601 | |
602 | /** |
603 | * Stashes the upload, shows the main form, but adds a "continue anyway button". |
604 | * Also checks whether there are actually warnings to display. |
605 | * |
606 | * @param array $warnings |
607 | * @return bool True if warnings were displayed, false if there are no |
608 | * warnings and it should continue processing |
609 | */ |
610 | protected function showUploadWarning( $warnings ) { |
611 | # If there are no warnings, or warnings we can ignore, return early. |
612 | # mDestWarningAck is set when some javascript has shown the warning |
613 | # to the user. mForReUpload is set when the user clicks the "upload a |
614 | # new version" link. |
615 | if ( !$warnings || ( count( $warnings ) == 1 |
616 | && isset( $warnings['exists'] ) |
617 | && ( $this->mDestWarningAck || $this->mForReUpload ) ) |
618 | ) { |
619 | return false; |
620 | } |
621 | |
622 | if ( $this->mUpload ) { |
623 | $stashStatus = $this->mUpload->tryStashFile( $this->getUser() ); |
624 | if ( $stashStatus->isGood() ) { |
625 | $sessionKey = $stashStatus->getValue()->getFileKey(); |
626 | $uploadWarning = 'uploadwarning-text'; |
627 | } else { |
628 | $sessionKey = null; |
629 | $uploadWarning = 'uploadwarning-text-nostash'; |
630 | } |
631 | } else { |
632 | $sessionKey = null; |
633 | $uploadWarning = 'uploadwarning-text-nostash'; |
634 | } |
635 | |
636 | // Add styles for the warning, reused from the live preview |
637 | $this->getOutput()->addModuleStyles( 'mediawiki.special' ); |
638 | |
639 | $linkRenderer = $this->getLinkRenderer(); |
640 | $warningHtml = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . "</h2>\n" |
641 | . '<div class="mw-destfile-warning"><ul>'; |
642 | foreach ( $warnings as $warning => $args ) { |
643 | if ( $warning == 'badfilename' ) { |
644 | $this->mDesiredDestName = Title::makeTitle( NS_FILE, $args )->getText(); |
645 | } |
646 | if ( $warning == 'exists' ) { |
647 | $msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n"; |
648 | } elseif ( $warning == 'no-change' ) { |
649 | $file = $args; |
650 | $filename = $file->getTitle()->getPrefixedText(); |
651 | $msg = "\t<li>" . $this->msg( 'fileexists-no-change', $filename )->parse() . "</li>\n"; |
652 | } elseif ( $warning == 'duplicate-version' ) { |
653 | $file = $args[0]; |
654 | $count = count( $args ); |
655 | $filename = $file->getTitle()->getPrefixedText(); |
656 | $message = $this->msg( 'fileexists-duplicate-version' ) |
657 | ->params( $filename ) |
658 | ->numParams( $count ); |
659 | $msg = "\t<li>" . $message->parse() . "</li>\n"; |
660 | } elseif ( $warning == 'was-deleted' ) { |
661 | # If the file existed before and was deleted, warn the user of this |
662 | $ltitle = SpecialPage::getTitleFor( 'Log' ); |
663 | $llink = $linkRenderer->makeKnownLink( |
664 | $ltitle, |
665 | $this->msg( 'deletionlog' )->text(), |
666 | [], |
667 | [ |
668 | 'type' => 'delete', |
669 | 'page' => Title::makeTitle( NS_FILE, $args )->getPrefixedText(), |
670 | ] |
671 | ); |
672 | $msg = "\t<li>" . $this->msg( 'filewasdeleted' )->rawParams( $llink )->parse() . "</li>\n"; |
673 | } elseif ( $warning == 'duplicate' ) { |
674 | $msg = $this->getDupeWarning( $args ); |
675 | } elseif ( $warning == 'duplicate-archive' ) { |
676 | if ( $args === '' ) { |
677 | $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate-notitle' )->parse() |
678 | . "</li>\n"; |
679 | } else { |
680 | $msg = "\t<li>" . $this->msg( 'file-deleted-duplicate', |
681 | Title::makeTitle( NS_FILE, $args )->getPrefixedText() )->parse() |
682 | . "</li>\n"; |
683 | } |
684 | } else { |
685 | if ( $args === true ) { |
686 | $args = []; |
687 | } elseif ( !is_array( $args ) ) { |
688 | $args = [ $args ]; |
689 | } |
690 | $msg = "\t<li>" . $this->msg( $warning, $args )->parse() . "</li>\n"; |
691 | } |
692 | $warningHtml .= $msg; |
693 | } |
694 | $warningHtml .= "</ul></div>\n"; |
695 | $warningHtml .= $this->msg( $uploadWarning )->parseAsBlock(); |
696 | |
697 | $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true ); |
698 | $form->setSubmitTextMsg( 'upload-tryagain' ); |
699 | $form->addButton( [ |
700 | 'name' => 'wpUploadIgnoreWarning', |
701 | 'value' => $this->msg( 'ignorewarning' )->text() |
702 | ] ); |
703 | $form->addButton( [ |
704 | 'name' => 'wpCancelUpload', |
705 | 'value' => $this->msg( 'reuploaddesc' )->text() |
706 | ] ); |
707 | |
708 | $this->showUploadForm( $form ); |
709 | |
710 | # Indicate that we showed a form |
711 | return true; |
712 | } |
713 | |
714 | /** |
715 | * Show the upload form with error message, but do not stash the file. |
716 | * |
717 | * @param string $message HTML string |
718 | */ |
719 | protected function showUploadError( $message ) { |
720 | $message = '<h2>' . $this->msg( 'uploadwarning' )->escaped() . '</h2>' . |
721 | Html::errorBox( $message ); |
722 | $this->showUploadForm( $this->getUploadForm( $message ) ); |
723 | } |
724 | |
725 | /** |
726 | * Common steps for processing uploads |
727 | * |
728 | * @param Status $fetchFileStatus |
729 | * @return bool |
730 | */ |
731 | protected function performUploadChecks( $fetchFileStatus ): bool { |
732 | if ( !$fetchFileStatus->isOK() ) { |
733 | $this->showUploadError( $this->getOutput()->parseAsInterface( |
734 | $fetchFileStatus->getWikiText( false, false, $this->getLanguage() ) |
735 | ) ); |
736 | |
737 | return false; |
738 | } |
739 | if ( !$this->getHookRunner()->onUploadForm_BeforeProcessing( $this ) ) { |
740 | wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." ); |
741 | // This code path is deprecated. If you want to break upload processing |
742 | // do so by hooking into the appropriate hooks in UploadBase::verifyUpload |
743 | // and UploadBase::verifyFile. |
744 | // If you use this hook to break uploading, the user will be returned |
745 | // an empty form with no error message whatsoever. |
746 | return false; |
747 | } |
748 | |
749 | // Upload verification |
750 | // If this is an asynchronous upload-by-url, skip the verification |
751 | if ( $this->isAsyncUpload() ) { |
752 | return true; |
753 | } |
754 | $details = $this->mUpload->verifyUpload(); |
755 | if ( $details['status'] != UploadBase::OK ) { |
756 | $this->processVerificationError( $details ); |
757 | |
758 | return false; |
759 | } |
760 | |
761 | // Verify permissions for this title |
762 | $user = $this->getUser(); |
763 | $permErrors = $this->mUpload->verifyTitlePermissions( $user ); |
764 | if ( $permErrors !== true ) { |
765 | $code = array_shift( $permErrors[0] ); |
766 | $this->showRecoverableUploadError( $this->msg( $code, $permErrors[0] )->parse() ); |
767 | |
768 | return false; |
769 | } |
770 | |
771 | $this->mLocalFile = $this->mUpload->getLocalFile(); |
772 | |
773 | // Check warnings if necessary |
774 | if ( !$this->mIgnoreWarning ) { |
775 | $warnings = $this->mUpload->checkWarnings( $user ); |
776 | if ( $this->showUploadWarning( $warnings ) ) { |
777 | return false; |
778 | } |
779 | } |
780 | |
781 | return true; |
782 | } |
783 | |
784 | /** |
785 | * Get the page text and tags for the upload |
786 | * |
787 | * @return array|null |
788 | */ |
789 | protected function getPageTextAndTags() { |
790 | // Get the page text if this is not a reupload |
791 | if ( !$this->mForReUpload ) { |
792 | $pageText = self::getInitialPageText( $this->mComment, $this->mLicense, |
793 | $this->mCopyrightStatus, $this->mCopyrightSource, |
794 | $this->getConfig() ); |
795 | } else { |
796 | $pageText = false; |
797 | } |
798 | $changeTags = $this->getRequest()->getVal( 'wpChangeTags' ); |
799 | if ( $changeTags === null || $changeTags === '' ) { |
800 | $changeTags = []; |
801 | } else { |
802 | $changeTags = array_filter( array_map( 'trim', explode( ',', $changeTags ) ) ); |
803 | } |
804 | if ( $changeTags ) { |
805 | $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange( |
806 | $changeTags, $this->getUser() ); |
807 | if ( !$changeTagsStatus->isOK() ) { |
808 | $this->showUploadError( $this->getOutput()->parseAsInterface( |
809 | $changeTagsStatus->getWikiText( false, false, $this->getLanguage() ) |
810 | ) ); |
811 | |
812 | return null; |
813 | } |
814 | } |
815 | return [ $pageText, $changeTags ]; |
816 | } |
817 | |
818 | /** |
819 | * Do the upload. |
820 | * Checks are made in SpecialUpload::execute() |
821 | */ |
822 | protected function processUpload() { |
823 | // Fetch the file if required |
824 | $status = $this->mUpload->fetchFile(); |
825 | if ( !$this->performUploadChecks( $status ) ) { |
826 | return; |
827 | } |
828 | $user = $this->getUser(); |
829 | $pageAndTags = $this->getPageTextAndTags(); |
830 | if ( $pageAndTags === null ) { |
831 | return; |
832 | } |
833 | [ $pageText, $changeTags ] = $pageAndTags; |
834 | |
835 | $status = $this->mUpload->performUpload( |
836 | $this->mComment, |
837 | $pageText, |
838 | $this->mWatchthis, |
839 | $user, |
840 | $changeTags |
841 | ); |
842 | |
843 | if ( !$status->isGood() ) { |
844 | $this->showRecoverableUploadError( |
845 | $this->getOutput()->parseAsInterface( |
846 | $status->getWikiText( false, false, $this->getLanguage() ) |
847 | ) |
848 | ); |
849 | |
850 | return; |
851 | } |
852 | |
853 | // Success, redirect to description page |
854 | $this->mUploadSuccessful = true; |
855 | $this->getHookRunner()->onSpecialUploadComplete( $this ); |
856 | $this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() ); |
857 | } |
858 | |
859 | /** |
860 | * Process an asynchronous upload |
861 | */ |
862 | protected function processAsyncUpload() { |
863 | // Ensure the upload we're dealing with is an UploadFromUrl |
864 | if ( !$this->mUpload instanceof \UploadFromUrl ) { |
865 | $this->showUploadError( $this->msg( 'uploaderror' )->escaped() ); |
866 | |
867 | return; |
868 | } |
869 | // check we can fetch the file |
870 | $status = $this->mUpload->canFetchFile(); |
871 | if ( !$this->performUploadChecks( $status ) ) { |
872 | $this->log->debug( 'Upload failed verification: {error}', [ 'error' => $status ] ); |
873 | return; |
874 | } |
875 | |
876 | $pageAndTags = $this->getPageTextAndTags(); |
877 | if ( $pageAndTags === null ) { |
878 | return; |
879 | } |
880 | [ $pageText, $changeTags ] = $pageAndTags; |
881 | |
882 | // Create a new job to process the upload from url |
883 | $job = new \UploadFromUrlJob( |
884 | [ |
885 | 'filename' => $this->mUpload->getDesiredDestName(), |
886 | 'url' => $this->mUpload->getUrl(), |
887 | 'comment' => $this->mComment, |
888 | 'tags' => $changeTags, |
889 | 'text' => $pageText, |
890 | 'watch' => $this->mWatchthis, |
891 | 'watchlistexpiry' => null, |
892 | 'session' => $this->getContext()->exportSession(), |
893 | 'reupload' => $this->mForReUpload, |
894 | 'ignorewarnings' => $this->mIgnoreWarning, |
895 | ] |
896 | ); |
897 | // Save the session status |
898 | $cacheKey = $job->getCacheKey(); |
899 | UploadBase::setSessionStatus( $this->getUser(), $cacheKey, [ |
900 | 'status' => Status::newGood(), |
901 | 'stage' => 'queued', |
902 | 'result' => 'Poll' |
903 | ] ); |
904 | $this->log->info( "Submitting UploadFromUrlJob for {filename}", |
905 | [ 'filename' => $this->mUpload->getDesiredDestName() ] |
906 | ); |
907 | // Submit the job |
908 | $this->jobQueueGroup->push( $job ); |
909 | // Show the upload status |
910 | $this->showUploadStatus( $this->getUser() ); |
911 | } |
912 | |
913 | /** |
914 | * Get the initial image page text based on a comment and optional file status information |
915 | * @param string $comment |
916 | * @param string $license |
917 | * @param string $copyStatus |
918 | * @param string $source |
919 | * @param Config|null $config Configuration object to load data from |
920 | * @return string |
921 | */ |
922 | public static function getInitialPageText( $comment = '', $license = '', |
923 | $copyStatus = '', $source = '', ?Config $config = null |
924 | ) { |
925 | if ( $config === null ) { |
926 | wfDebug( __METHOD__ . ' called without a Config instance passed to it' ); |
927 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
928 | } |
929 | |
930 | $msg = []; |
931 | $forceUIMsgAsContentMsg = (array)$config->get( MainConfigNames::ForceUIMsgAsContentMsg ); |
932 | /* These messages are transcluded into the actual text of the description page. |
933 | * Thus, forcing them as content messages makes the upload to produce an int: template |
934 | * instead of hardcoding it there in the uploader language. |
935 | */ |
936 | foreach ( [ 'license-header', 'filedesc', 'filestatus', 'filesource' ] as $msgName ) { |
937 | if ( in_array( $msgName, $forceUIMsgAsContentMsg ) ) { |
938 | $msg[$msgName] = "{{int:$msgName}}"; |
939 | } else { |
940 | $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text(); |
941 | } |
942 | } |
943 | |
944 | $licenseText = ''; |
945 | if ( $license !== '' ) { |
946 | $licenseText = '== ' . $msg['license-header'] . " ==\n{{" . $license . "}}\n"; |
947 | } |
948 | |
949 | $pageText = $comment . "\n"; |
950 | $headerText = '== ' . $msg['filedesc'] . ' =='; |
951 | if ( $comment !== '' && !str_contains( $comment, $headerText ) ) { |
952 | // prepend header to page text unless it's already there (or there is no content) |
953 | $pageText = $headerText . "\n" . $pageText; |
954 | } |
955 | |
956 | if ( $config->get( MainConfigNames::UseCopyrightUpload ) ) { |
957 | $pageText .= '== ' . $msg['filestatus'] . " ==\n" . $copyStatus . "\n"; |
958 | $pageText .= $licenseText; |
959 | $pageText .= '== ' . $msg['filesource'] . " ==\n" . $source; |
960 | } else { |
961 | $pageText .= $licenseText; |
962 | } |
963 | |
964 | // allow extensions to modify the content |
965 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
966 | ->onUploadForm_getInitialPageText( $pageText, $msg, $config ); |
967 | |
968 | return $pageText; |
969 | } |
970 | |
971 | /** |
972 | * See if we should check the 'watch this page' checkbox on the form |
973 | * based on the user's preferences and whether we're being asked |
974 | * to create a new file or update an existing one. |
975 | * |
976 | * In the case where 'watch edits' is off but 'watch creations' is on, |
977 | * we'll leave the box unchecked. |
978 | * |
979 | * Note that the page target can be changed *on the form*, so our check |
980 | * state can get out of sync. |
981 | * @return bool |
982 | */ |
983 | protected function getWatchCheck() { |
984 | $user = $this->getUser(); |
985 | if ( $this->userOptionsLookup->getBoolOption( $user, 'watchdefault' ) ) { |
986 | // Watch all edits! |
987 | return true; |
988 | } |
989 | |
990 | $desiredTitleObj = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName ); |
991 | if ( $desiredTitleObj instanceof Title && |
992 | $this->watchlistManager->isWatched( $user, $desiredTitleObj ) ) { |
993 | // Already watched, don't change that |
994 | return true; |
995 | } |
996 | |
997 | $local = $this->localRepo->newFile( $this->mDesiredDestName ); |
998 | if ( $local && $local->exists() ) { |
999 | // We're uploading a new version of an existing file. |
1000 | // No creation, so don't watch it if we're not already. |
1001 | return false; |
1002 | } else { |
1003 | // New page should get watched if that's our option. |
1004 | return $this->userOptionsLookup->getBoolOption( $user, 'watchcreations' ) || |
1005 | $this->userOptionsLookup->getBoolOption( $user, 'watchuploads' ); |
1006 | } |
1007 | } |
1008 | |
1009 | /** |
1010 | * Provides output to the user for a result of UploadBase::verifyUpload |
1011 | * |
1012 | * @param array $details Result of UploadBase::verifyUpload |
1013 | */ |
1014 | protected function processVerificationError( $details ) { |
1015 | switch ( $details['status'] ) { |
1016 | /** Statuses that only require name changing */ |
1017 | case UploadBase::MIN_LENGTH_PARTNAME: |
1018 | $this->showRecoverableUploadError( $this->msg( 'minlength1' )->escaped() ); |
1019 | break; |
1020 | case UploadBase::ILLEGAL_FILENAME: |
1021 | $this->showRecoverableUploadError( $this->msg( 'illegalfilename', |
1022 | $details['filtered'] )->parse() ); |
1023 | break; |
1024 | case UploadBase::FILENAME_TOO_LONG: |
1025 | $this->showRecoverableUploadError( $this->msg( 'filename-toolong' )->escaped() ); |
1026 | break; |
1027 | case UploadBase::FILETYPE_MISSING: |
1028 | $this->showRecoverableUploadError( $this->msg( 'filetype-missing' )->parse() ); |
1029 | break; |
1030 | case UploadBase::WINDOWS_NONASCII_FILENAME: |
1031 | $this->showRecoverableUploadError( $this->msg( 'windows-nonascii-filename' )->parse() ); |
1032 | break; |
1033 | |
1034 | /** Statuses that require reuploading */ |
1035 | case UploadBase::EMPTY_FILE: |
1036 | $this->showUploadError( $this->msg( 'emptyfile' )->escaped() ); |
1037 | break; |
1038 | case UploadBase::FILE_TOO_LARGE: |
1039 | $this->showUploadError( $this->msg( 'largefileserver' )->escaped() ); |
1040 | break; |
1041 | case UploadBase::FILETYPE_BADTYPE: |
1042 | $msg = $this->msg( 'filetype-banned-type' ); |
1043 | if ( isset( $details['blacklistedExt'] ) ) { |
1044 | $msg->params( $this->getLanguage()->commaList( $details['blacklistedExt'] ) ); |
1045 | } else { |
1046 | $msg->params( $details['finalExt'] ); |
1047 | } |
1048 | $extensions = |
1049 | array_unique( $this->getConfig()->get( MainConfigNames::FileExtensions ) ); |
1050 | $msg->params( $this->getLanguage()->commaList( $extensions ), |
1051 | count( $extensions ) ); |
1052 | |
1053 | // Add PLURAL support for the first parameter. This results |
1054 | // in a bit unlogical parameter sequence, but does not break |
1055 | // old translations |
1056 | if ( isset( $details['blacklistedExt'] ) ) { |
1057 | $msg->params( count( $details['blacklistedExt'] ) ); |
1058 | } else { |
1059 | $msg->params( 1 ); |
1060 | } |
1061 | |
1062 | $this->showUploadError( $msg->parse() ); |
1063 | break; |
1064 | case UploadBase::VERIFICATION_ERROR: |
1065 | unset( $details['status'] ); |
1066 | $code = array_shift( $details['details'] ); |
1067 | $this->showUploadError( $this->msg( $code, $details['details'] )->parse() ); |
1068 | break; |
1069 | case UploadBase::HOOK_ABORTED: |
1070 | # allow hooks to return error details in an array, or as a single string key |
1071 | $msg = Message::newFromSpecifier( $details['error'] ); |
1072 | $this->showUploadError( $this->msg( $msg )->parse() ); |
1073 | break; |
1074 | default: |
1075 | throw new UnexpectedValueException( __METHOD__ . ": Unknown value `{$details['status']}`" ); |
1076 | } |
1077 | } |
1078 | |
1079 | /** |
1080 | * Remove a temporarily kept file stashed by saveTempUploadedFile(). |
1081 | * |
1082 | * @return bool Success |
1083 | */ |
1084 | protected function unsaveUploadedFile() { |
1085 | if ( !( $this->mUpload instanceof UploadFromStash ) ) { |
1086 | return true; |
1087 | } |
1088 | $success = $this->mUpload->unsaveUploadedFile(); |
1089 | if ( !$success ) { |
1090 | $this->getOutput()->showErrorPage( |
1091 | 'internalerror', |
1092 | 'filedeleteerror', |
1093 | [ $this->mUpload->getTempPath() ] |
1094 | ); |
1095 | |
1096 | return false; |
1097 | } else { |
1098 | return true; |
1099 | } |
1100 | } |
1101 | |
1102 | /** Functions for formatting warnings */ |
1103 | |
1104 | /** |
1105 | * Formats a result of UploadBase::getExistsWarning as HTML |
1106 | * This check is static and can be done pre-upload via AJAX |
1107 | * |
1108 | * @param array $exists The result of UploadBase::getExistsWarning |
1109 | * @return string Empty string if there is no warning or an HTML fragment |
1110 | */ |
1111 | public static function getExistsWarning( $exists ) { |
1112 | if ( !$exists ) { |
1113 | return ''; |
1114 | } |
1115 | |
1116 | $file = $exists['file']; |
1117 | $filename = $file->getTitle()->getPrefixedText(); |
1118 | $warnMsg = null; |
1119 | |
1120 | if ( $exists['warning'] == 'exists' ) { |
1121 | // Exact match |
1122 | $warnMsg = wfMessage( 'fileexists', $filename ); |
1123 | } elseif ( $exists['warning'] == 'page-exists' ) { |
1124 | // Page exists but file does not |
1125 | $warnMsg = wfMessage( 'filepageexists', $filename ); |
1126 | } elseif ( $exists['warning'] == 'exists-normalized' ) { |
1127 | $warnMsg = wfMessage( 'fileexists-extension', $filename, |
1128 | $exists['normalizedFile']->getTitle()->getPrefixedText() ); |
1129 | } elseif ( $exists['warning'] == 'thumb' ) { |
1130 | // Swapped argument order compared with other messages for backwards compatibility |
1131 | $warnMsg = wfMessage( 'fileexists-thumbnail-yes', |
1132 | $exists['thumbFile']->getTitle()->getPrefixedText(), $filename ); |
1133 | } elseif ( $exists['warning'] == 'thumb-name' ) { |
1134 | // Image w/o '180px-' does not exists, but we do not like these filenames |
1135 | $name = $file->getName(); |
1136 | $badPart = substr( $name, 0, strpos( $name, '-' ) + 1 ); |
1137 | $warnMsg = wfMessage( 'file-thumbnail-no', $badPart ); |
1138 | } elseif ( $exists['warning'] == 'bad-prefix' ) { |
1139 | $warnMsg = wfMessage( 'filename-bad-prefix', $exists['prefix'] ); |
1140 | } |
1141 | |
1142 | return $warnMsg ? $warnMsg->page( $file->getTitle() )->parse() : ''; |
1143 | } |
1144 | |
1145 | /** |
1146 | * Construct a warning and a gallery from an array of duplicate files. |
1147 | * @param array $dupes |
1148 | * @return string |
1149 | */ |
1150 | public function getDupeWarning( $dupes ) { |
1151 | if ( !$dupes ) { |
1152 | return ''; |
1153 | } |
1154 | |
1155 | $gallery = ImageGalleryBase::factory( false, $this->getContext() ); |
1156 | $gallery->setShowBytes( false ); |
1157 | $gallery->setShowDimensions( false ); |
1158 | foreach ( $dupes as $file ) { |
1159 | $gallery->add( $file->getTitle() ); |
1160 | } |
1161 | |
1162 | return '<li>' . |
1163 | $this->msg( 'file-exists-duplicate' )->numParams( count( $dupes ) )->parse() . |
1164 | $gallery->toHTML() . "</li>\n"; |
1165 | } |
1166 | |
1167 | protected function getGroupName() { |
1168 | return 'media'; |
1169 | } |
1170 | |
1171 | /** |
1172 | * Should we rotate images in the preview on Special:Upload. |
1173 | * |
1174 | * This controls js: mw.config.get( 'wgFileCanRotate' ) |
1175 | * |
1176 | * @todo What about non-BitmapHandler handled files? |
1177 | * @return bool |
1178 | */ |
1179 | public static function rotationEnabled() { |
1180 | $bitmapHandler = new BitmapHandler(); |
1181 | return $bitmapHandler->autoRotateEnabled(); |
1182 | } |
1183 | } |
1184 | |
1185 | /** |
1186 | * Retain the old class name for backwards compatibility. |
1187 | * @deprecated since 1.41 |
1188 | */ |
1189 | class_alias( SpecialUpload::class, 'SpecialUpload' ); |