Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
17.68% |
58 / 328 |
|
15.00% |
3 / 20 |
CRAP | |
0.00% |
0 / 1 |
SpecialImportFile | |
17.68% |
58 / 328 |
|
15.00% |
3 / 20 |
3212.56 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
userCanExecute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
executeStandardChecks | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
90 | |||
getDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 61 |
|
0.00% |
0 / 1 |
306 | |||
handleAction | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
makeImportPlan | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
2 | |||
logErrorStats | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
doCodexImport | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
20 | |||
doImport | |
83.33% |
40 / 48 |
|
0.00% |
0 / 1 |
6.17 | |||
logActionStats | |
16.67% |
1 / 6 |
|
0.00% |
0 / 1 |
26.83 | |||
performPostImportActions | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getWarningMessage | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
5.67 | |||
showWarningMessage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
showImportPage | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getAutomatedCapabilities | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
90 | |||
showCodexImportPage | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
6 | |||
showLandingPage | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace FileImporter; |
4 | |
5 | use ErrorPageError; |
6 | use Exception; |
7 | use ExtensionRegistry; |
8 | use FileImporter\Data\ImportPlan; |
9 | use FileImporter\Data\ImportRequest; |
10 | use FileImporter\Exceptions\AbuseFilterWarningsException; |
11 | use FileImporter\Exceptions\CommunityPolicyException; |
12 | use FileImporter\Exceptions\DuplicateFilesException; |
13 | use FileImporter\Exceptions\HttpRequestException; |
14 | use FileImporter\Exceptions\ImportException; |
15 | use FileImporter\Exceptions\LocalizedImportException; |
16 | use FileImporter\Exceptions\RecoverableTitleException; |
17 | use FileImporter\Html\ChangeFileInfoForm; |
18 | use FileImporter\Html\ChangeFileNameForm; |
19 | use FileImporter\Html\DuplicateFilesErrorPage; |
20 | use FileImporter\Html\ErrorPage; |
21 | use FileImporter\Html\FileInfoDiffPage; |
22 | use FileImporter\Html\HelpBanner; |
23 | use FileImporter\Html\ImportPreviewPage; |
24 | use FileImporter\Html\ImportSuccessSnippet; |
25 | use FileImporter\Html\InfoPage; |
26 | use FileImporter\Html\InputFormPage; |
27 | use FileImporter\Html\RecoverableTitleExceptionPage; |
28 | use FileImporter\Html\SourceWikiCleanupSnippet; |
29 | use FileImporter\Services\Importer; |
30 | use FileImporter\Services\ImportPlanFactory; |
31 | use FileImporter\Services\SourceSiteLocator; |
32 | use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; |
33 | use MediaWiki\Config\Config; |
34 | use MediaWiki\Content\IContentHandlerFactory; |
35 | use MediaWiki\EditPage\EditPage; |
36 | use MediaWiki\Extension\GlobalBlocking\GlobalBlocking; |
37 | use MediaWiki\Html\Html; |
38 | use MediaWiki\Logger\LoggerFactory; |
39 | use MediaWiki\MediaWikiServices; |
40 | use MediaWiki\Request\WebRequest; |
41 | use MediaWiki\SpecialPage\SpecialPage; |
42 | use MediaWiki\Status\Status; |
43 | use MediaWiki\User\Options\UserOptionsManager; |
44 | use MediaWiki\User\User; |
45 | use OOUI\HtmlSnippet; |
46 | use OOUI\MessageWidget; |
47 | use PermissionsError; |
48 | use Psr\Log\LoggerInterface; |
49 | use StatusValue; |
50 | use UploadBase; |
51 | use UserBlockedError; |
52 | |
53 | /** |
54 | * @license GPL-2.0-or-later |
55 | * @author Addshore |
56 | */ |
57 | class SpecialImportFile extends SpecialPage { |
58 | |
59 | private const ERROR_UPLOAD_DISABLED = 'uploadDisabled'; |
60 | private const ERROR_USER_PERMISSIONS = 'userPermissionsError'; |
61 | private const ERROR_LOCAL_BLOCK = 'userBlocked'; |
62 | private const ERROR_GLOBAL_BLOCK = 'userGloballyBlocked'; |
63 | |
64 | private SourceSiteLocator $sourceSiteLocator; |
65 | private Importer $importer; |
66 | private ImportPlanFactory $importPlanFactory; |
67 | private IContentHandlerFactory $contentHandlerFactory; |
68 | private StatsdDataFactoryInterface $stats; |
69 | private UserOptionsManager $userOptionsManager; |
70 | private LoggerInterface $logger; |
71 | |
72 | public function __construct( |
73 | SourceSiteLocator $sourceSiteLocator, |
74 | Importer $importer, |
75 | ImportPlanFactory $importPlanFactory, |
76 | IContentHandlerFactory $contentHandlerFactory, |
77 | StatsdDataFactoryInterface $statsdDataFactory, |
78 | UserOptionsManager $userOptionsManager, |
79 | Config $config |
80 | ) { |
81 | parent::__construct( |
82 | 'ImportFile', |
83 | $config->get( 'FileImporterRequiredRight' ), |
84 | $config->get( 'FileImporterShowInputScreen' ) |
85 | ); |
86 | |
87 | $this->sourceSiteLocator = $sourceSiteLocator; |
88 | $this->importer = $importer; |
89 | $this->importPlanFactory = $importPlanFactory; |
90 | $this->contentHandlerFactory = $contentHandlerFactory; |
91 | $this->stats = $statsdDataFactory; |
92 | $this->userOptionsManager = $userOptionsManager; |
93 | $this->logger = LoggerFactory::getInstance( 'FileImporter' ); |
94 | } |
95 | |
96 | public function doesWrites() { |
97 | return true; |
98 | } |
99 | |
100 | /** |
101 | * @inheritDoc |
102 | */ |
103 | public function getGroupName() { |
104 | return 'media'; |
105 | } |
106 | |
107 | /** |
108 | * @inheritDoc |
109 | */ |
110 | public function userCanExecute( User $user ) { |
111 | return UploadBase::isEnabled() && parent::userCanExecute( $user ); |
112 | } |
113 | |
114 | /** |
115 | * Checks based on those in EditPage and SpecialUpload |
116 | * |
117 | * @throws ErrorPageError when one of the checks failed |
118 | */ |
119 | private function executeStandardChecks() { |
120 | $unicodeCheck = $this->getRequest()->getText( 'wpUnicodeCheck' ); |
121 | if ( $unicodeCheck && $unicodeCheck !== EditPage::UNICODE_CHECK ) { |
122 | throw new ErrorPageError( 'errorpagetitle', 'unicode-support-fail' ); |
123 | } |
124 | |
125 | # Check uploading enabled |
126 | if ( !UploadBase::isEnabled() ) { |
127 | $this->logErrorStats( self::ERROR_UPLOAD_DISABLED, false ); |
128 | throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' ); |
129 | } |
130 | |
131 | $user = $this->getUser(); |
132 | |
133 | // Check if the user does have all the rights required via $wgFileImporterRequiredRight (set |
134 | // to "upload" by default), as well as "upload" and "edit" in case …RequiredRight is more |
135 | // relaxed. Note special pages must call userCanExecute() manually when parent::execute() |
136 | // isn't called, {@see SpecialPage::__construct}. |
137 | $missingPermission = parent::userCanExecute( $user ) |
138 | ? UploadBase::isAllowed( $user ) |
139 | : $this->getRestriction(); |
140 | if ( is_string( $missingPermission ) ) { |
141 | $this->logErrorStats( self::ERROR_USER_PERMISSIONS, false ); |
142 | throw new PermissionsError( $missingPermission ); |
143 | } |
144 | |
145 | # Check blocks |
146 | $localBlock = $user->getBlock(); |
147 | if ( $localBlock ) { |
148 | $this->logErrorStats( self::ERROR_LOCAL_BLOCK, false ); |
149 | throw new UserBlockedError( $localBlock ); |
150 | } |
151 | |
152 | // Global blocks |
153 | if ( ExtensionRegistry::getInstance()->isLoaded( 'GlobalBlocking' ) ) { |
154 | $globalBlock = GlobalBlocking::getUserBlock( $user, $this->getRequest()->getIP() ); |
155 | if ( $globalBlock ) { |
156 | $this->logErrorStats( self::ERROR_GLOBAL_BLOCK, false ); |
157 | throw new UserBlockedError( $globalBlock ); |
158 | } |
159 | } |
160 | |
161 | # Check whether we actually want to allow changing stuff |
162 | $this->checkReadOnly(); |
163 | } |
164 | |
165 | /** @inheritDoc */ |
166 | public function getDescription() { |
167 | return $this->msg( 'fileimporter-specialpage' ); |
168 | } |
169 | |
170 | /** |
171 | * @param string|null $subPage |
172 | */ |
173 | public function execute( $subPage ): void { |
174 | $webRequest = $this->getRequest(); |
175 | $clientUrl = $webRequest->getVal( 'clientUrl', '' ); |
176 | $action = $webRequest->getRawVal( ImportPreviewPage::ACTION_BUTTON ); |
177 | if ( $action ) { |
178 | $this->logger->info( "Performing $action on ImportPlan for URL: $clientUrl" ); |
179 | } |
180 | |
181 | $isCodex = $webRequest->getBool( 'codex' ) && |
182 | MediaWikiServices::getInstance()->getMainConfig()->get( 'FileImporterCodexMode' ); |
183 | $isCodexSubmit = $isCodex && $this->getRequest()->wasPosted() && $action === 'submit'; |
184 | |
185 | if ( !$isCodexSubmit ) { |
186 | $this->setHeaders(); |
187 | $this->getOutput()->enableOOUI(); |
188 | } |
189 | $this->executeStandardChecks(); |
190 | |
191 | if ( !$isCodex ) { |
192 | $this->getOutput()->addModuleStyles( 'ext.FileImporter.SpecialCss' ); |
193 | $this->getOutput()->addModuleStyles( 'ext.FileImporter.Images' ); |
194 | $this->getOutput()->addModules( 'ext.FileImporter.SpecialJs' ); |
195 | } |
196 | |
197 | // Note: executions by users that don't have the rights to view the page etc will not be |
198 | // shown in this metric as executeStandardChecks will have already kicked them out, |
199 | $this->stats->increment( 'FileImporter.specialPage.execute.total' ); |
200 | // The importSource url parameter is added to requests from the FileExporter extension. |
201 | if ( $webRequest->getRawVal( 'importSource' ) === 'FileExporter' ) { |
202 | $this->stats->increment( 'FileImporter.specialPage.execute.fromFileExporter' ); |
203 | } |
204 | |
205 | if ( $clientUrl === '' ) { |
206 | $this->stats->increment( 'FileImporter.specialPage.execute.noClientUrl' ); |
207 | $this->showLandingPage(); |
208 | return; |
209 | } |
210 | |
211 | if ( $webRequest->getBool( HelpBanner::HIDE_HELP_BANNER_CHECK_BOX ) && |
212 | $this->getUser()->isNamed() |
213 | ) { |
214 | $this->userOptionsManager->setOption( |
215 | $this->getUser(), |
216 | HelpBanner::HIDE_HELP_BANNER_PREFERENCE, |
217 | '1' |
218 | ); |
219 | $this->userOptionsManager->saveOptions( $this->getUser() ); |
220 | } |
221 | |
222 | try { |
223 | $this->logger->info( 'Getting ImportPlan for URL: ' . $clientUrl ); |
224 | $importPlan = $this->makeImportPlan( $webRequest ); |
225 | |
226 | if ( $isCodexSubmit ) { |
227 | // disable all default output of the special page, like headers, title, navigation |
228 | $this->getOutput()->disable(); |
229 | header( 'Content-type: application/json; charset=utf-8' ); |
230 | $this->doCodexImport( $importPlan ); |
231 | } elseif ( $isCodex ) { |
232 | $this->getOutput()->addModules( 'ext.FileImporter.SpecialCodexJs' ); |
233 | $this->showCodexImportPage( $importPlan ); |
234 | } else { |
235 | $this->handleAction( $action, $importPlan ); |
236 | } |
237 | } catch ( ImportException $exception ) { |
238 | $this->logger->info( 'ImportException: ' . $exception->getMessage() ); |
239 | $this->logErrorStats( |
240 | (string)$exception->getCode(), |
241 | $exception instanceof RecoverableTitleException |
242 | ); |
243 | |
244 | if ( $exception instanceof DuplicateFilesException ) { |
245 | $html = ( new DuplicateFilesErrorPage( $this ) )->getHtml( |
246 | $exception->getFiles(), |
247 | $clientUrl |
248 | ); |
249 | } elseif ( $exception instanceof RecoverableTitleException ) { |
250 | $html = ( new RecoverableTitleExceptionPage( $this ) )->getHtml( $exception ); |
251 | } else { |
252 | $html = ( new ErrorPage( $this ) )->getHtml( |
253 | $this->getWarningMessage( $exception ), |
254 | $clientUrl, |
255 | $exception instanceof CommunityPolicyException ? 'warning' : 'error' |
256 | ); |
257 | } |
258 | $this->getOutput()->enableOOUI(); |
259 | $this->getOutput()->addHTML( $html ); |
260 | } |
261 | } |
262 | |
263 | private function handleAction( ?string $action, ImportPlan $importPlan ): void { |
264 | switch ( $action ) { |
265 | case ImportPreviewPage::ACTION_SUBMIT: |
266 | $this->doImport( $importPlan ); |
267 | break; |
268 | case ImportPreviewPage::ACTION_EDIT_TITLE: |
269 | $importPlan->setActionIsPerformed( ImportPreviewPage::ACTION_EDIT_TITLE ); |
270 | $this->getOutput()->addHTML( |
271 | ( new ChangeFileNameForm( $this ) )->getHtml( $importPlan ) |
272 | ); |
273 | break; |
274 | case ImportPreviewPage::ACTION_EDIT_INFO: |
275 | $importPlan->setActionIsPerformed( ImportPreviewPage::ACTION_EDIT_INFO ); |
276 | $this->getOutput()->addHTML( |
277 | ( new ChangeFileInfoForm( $this ) )->getHtml( $importPlan ) |
278 | ); |
279 | break; |
280 | case ImportPreviewPage::ACTION_VIEW_DIFF: |
281 | $contentHandler = $this->contentHandlerFactory->getContentHandler( CONTENT_MODEL_WIKITEXT ); |
282 | $this->getOutput()->addHTML( |
283 | ( new FileInfoDiffPage( $this ) )->getHtml( $importPlan, $contentHandler ) |
284 | ); |
285 | break; |
286 | default: |
287 | $this->showImportPage( $importPlan ); |
288 | } |
289 | } |
290 | |
291 | /** |
292 | * @throws ImportException |
293 | */ |
294 | private function makeImportPlan( WebRequest $webRequest ): ImportPlan { |
295 | $importRequest = new ImportRequest( |
296 | $webRequest->getVal( 'clientUrl' ), |
297 | $webRequest->getVal( 'intendedFileName' ), |
298 | $webRequest->getVal( 'intendedWikitext' ), |
299 | $webRequest->getVal( 'intendedRevisionSummary' ), |
300 | $webRequest->getRawVal( 'importDetailsHash', '' ) |
301 | ); |
302 | |
303 | $url = $importRequest->getUrl(); |
304 | $sourceSite = $this->sourceSiteLocator->getSourceSite( $url ); |
305 | $importDetails = $sourceSite->retrieveImportDetails( $url ); |
306 | |
307 | $importPlan = $this->importPlanFactory->newPlan( |
308 | $importRequest, |
309 | $importDetails, |
310 | $this->getUser() |
311 | ); |
312 | $importPlan->setActionStats( |
313 | json_decode( $webRequest->getVal( 'actionStats', '[]' ), true ) |
314 | ); |
315 | $importPlan->setValidationWarnings( |
316 | json_decode( $webRequest->getVal( 'validationWarnings', '[]' ), true ) |
317 | ); |
318 | $importPlan->setAutomateSourceWikiCleanUp( |
319 | $webRequest->getBool( 'automateSourceWikiCleanup' ) |
320 | ); |
321 | $importPlan->setAutomateSourceWikiDelete( |
322 | $webRequest->getBool( 'automateSourceWikiDelete' ) |
323 | ); |
324 | |
325 | return $importPlan; |
326 | } |
327 | |
328 | private function logErrorStats( string $type, bool $isRecoverable ): void { |
329 | $this->stats->increment( 'FileImporter.error.byRecoverable.' |
330 | . wfBoolToStr( $isRecoverable ) . '.byType.' . $type ); |
331 | } |
332 | |
333 | private function doCodexImport( ImportPlan $importPlan ): void { |
334 | // TODO handle error cases and echo JSON to allow Codex to visualize the errors |
335 | try { |
336 | $this->importer->import( |
337 | $this->getUser(), |
338 | $importPlan |
339 | ); |
340 | $this->stats->increment( 'FileImporter.import.result.success' ); |
341 | $this->logActionStats( $importPlan ); |
342 | |
343 | $postImportResult = $this->performPostImportActions( $importPlan ); |
344 | $successRedirectUrl = ( new ImportSuccessSnippet() )->getRedirectWithNotice( |
345 | $importPlan->getTitle(), |
346 | $this->getUser(), |
347 | $postImportResult |
348 | ); |
349 | |
350 | echo json_encode( [ |
351 | 'success' => true, |
352 | 'redirect' => $successRedirectUrl, |
353 | ] ); |
354 | } catch ( ImportException $exception ) { |
355 | if ( $exception instanceof AbuseFilterWarningsException ) { |
356 | $warningMessages = []; |
357 | $warningMessages[] = [ |
358 | 'type' => 'warning', |
359 | 'message' => $this->getWarningMessage( $exception ) |
360 | ]; |
361 | |
362 | foreach ( $exception->getMessages() as $msg ) { |
363 | $warningMessages[] = [ |
364 | 'type' => 'warning', |
365 | 'message' => $this->msg( $msg )->parse() |
366 | ]; |
367 | } |
368 | |
369 | echo json_encode( [ |
370 | 'error' => true, |
371 | 'warningMessages' => $warningMessages, |
372 | 'validationWarnings' => $importPlan->getValidationWarnings() |
373 | ] ); |
374 | } else { |
375 | // TODO: More graceful error handling |
376 | echo json_encode( [ |
377 | 'error' => true, |
378 | 'output' => $exception->getTrace(), |
379 | ] ); |
380 | } |
381 | } |
382 | } |
383 | |
384 | private function doImport( ImportPlan $importPlan ): bool { |
385 | $out = $this->getOutput(); |
386 | $importDetails = $importPlan->getDetails(); |
387 | |
388 | $importDetailsHash = $out->getRequest()->getRawVal( 'importDetailsHash', '' ); |
389 | $token = $out->getRequest()->getRawVal( 'token', '' ); |
390 | |
391 | if ( !$this->getContext()->getCsrfTokenSet()->matchToken( $token ) ) { |
392 | $this->showWarningMessage( $this->msg( 'fileimporter-badtoken' )->parse() ); |
393 | $this->logErrorStats( 'badToken', true ); |
394 | return false; |
395 | } |
396 | |
397 | if ( $importDetails->getOriginalHash() !== $importDetailsHash ) { |
398 | $this->showWarningMessage( $this->msg( 'fileimporter-badimporthash' )->parse() ); |
399 | $this->logErrorStats( 'badImportHash', true ); |
400 | return false; |
401 | } |
402 | |
403 | try { |
404 | $this->importer->import( |
405 | $this->getUser(), |
406 | $importPlan |
407 | ); |
408 | $this->stats->increment( 'FileImporter.import.result.success' ); |
409 | // TODO: inline at site of action |
410 | $this->logActionStats( $importPlan ); |
411 | |
412 | $postImportResult = $this->performPostImportActions( $importPlan ); |
413 | |
414 | $out->redirect( |
415 | ( new ImportSuccessSnippet() )->getRedirectWithNotice( |
416 | $importPlan->getTitle(), |
417 | $this->getUser(), |
418 | $postImportResult |
419 | ) |
420 | ); |
421 | |
422 | return true; |
423 | } catch ( ImportException $exception ) { |
424 | $this->logErrorStats( |
425 | (string)$exception->getCode(), |
426 | $exception instanceof RecoverableTitleException |
427 | ); |
428 | |
429 | if ( $exception instanceof AbuseFilterWarningsException ) { |
430 | $this->showWarningMessage( $this->getWarningMessage( $exception ), 'warning' ); |
431 | |
432 | foreach ( $exception->getMessages() as $msg ) { |
433 | $this->showWarningMessage( |
434 | $this->msg( $msg )->parse(), |
435 | 'warning', |
436 | true |
437 | ); |
438 | } |
439 | |
440 | $this->showImportPage( $importPlan ); |
441 | } else { |
442 | $this->showWarningMessage( |
443 | Html::rawElement( 'strong', [], $this->msg( 'fileimporter-importfailed' )->parse() ) . |
444 | '<br>' . |
445 | $this->getWarningMessage( $exception ), |
446 | 'error' |
447 | ); |
448 | } |
449 | return false; |
450 | } |
451 | } |
452 | |
453 | private function logActionStats( ImportPlan $importPlan ): void { |
454 | foreach ( $importPlan->getActionStats() as $key => $_ ) { |
455 | if ( |
456 | $key === ImportPreviewPage::ACTION_EDIT_TITLE || |
457 | $key === ImportPreviewPage::ACTION_EDIT_INFO || |
458 | $key === SourceWikiCleanupSnippet::ACTION_OFFERED_SOURCE_DELETE || |
459 | $key === SourceWikiCleanupSnippet::ACTION_OFFERED_SOURCE_EDIT |
460 | ) { |
461 | $this->stats->increment( 'FileImporter.specialPage.action.' . $key ); |
462 | } |
463 | } |
464 | } |
465 | |
466 | private function performPostImportActions( ImportPlan $importPlan ): StatusValue { |
467 | $sourceSite = $importPlan->getRequest()->getUrl(); |
468 | $postImportHandler = $this->sourceSiteLocator->getSourceSite( $sourceSite ) |
469 | ->getPostImportHandler(); |
470 | |
471 | return $postImportHandler->execute( $importPlan, $this->getUser() ); |
472 | } |
473 | |
474 | /** |
475 | * @return string HTML |
476 | */ |
477 | private function getWarningMessage( Exception $ex ): string { |
478 | if ( $ex instanceof LocalizedImportException ) { |
479 | return $ex->getMessageObject()->inLanguage( $this->getLanguage() )->parse(); |
480 | } |
481 | if ( $ex instanceof HttpRequestException ) { |
482 | return Status::wrap( $ex->getStatusValue() )->getHTML( false, false, |
483 | $this->getLanguage() ); |
484 | } |
485 | |
486 | return htmlspecialchars( $ex->getMessage() ); |
487 | } |
488 | |
489 | /** |
490 | * @param string $html |
491 | * @param string $type Set to "notice" for a gray box, defaults to "error" (red) |
492 | * @param bool $inline |
493 | */ |
494 | private function showWarningMessage( string $html, string $type = 'error', bool $inline = false ): void { |
495 | $this->getOutput()->enableOOUI(); |
496 | $this->getOutput()->addHTML( |
497 | new MessageWidget( [ |
498 | 'label' => new HtmlSnippet( $html ), |
499 | 'type' => $type, |
500 | 'inline' => $inline, |
501 | ] ) . |
502 | '<br>' |
503 | ); |
504 | } |
505 | |
506 | private function showImportPage( ImportPlan $importPlan ): void { |
507 | $this->getOutput()->addHTML( |
508 | ( new ImportPreviewPage( $this ) )->getHtml( $importPlan ) |
509 | ); |
510 | } |
511 | |
512 | /** |
513 | * @return array of automation features and whether they are available |
514 | */ |
515 | private function getAutomatedCapabilities( ImportPlan $importPlan ) { |
516 | $capabilities = []; |
517 | |
518 | $config = $this->getContext()->getConfig(); |
519 | $isCentralAuthEnabled = ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ); |
520 | $lookup = MediaWikiServices::getInstance()->getService( |
521 | 'FileImporterTemplateLookup' ); |
522 | $remoteActionApi = MediaWikiServices::getInstance()->getService( |
523 | 'FileImporterMediaWikiRemoteApiActionExecutor' ); |
524 | $sourceUrl = $importPlan->getRequest()->getUrl(); |
525 | |
526 | $capabilities['canAutomateEdit'] = |
527 | $isCentralAuthEnabled && |
528 | $config->get( 'FileImporterSourceWikiTemplating' ) && |
529 | $lookup->fetchNowCommonsLocalTitle( $sourceUrl ) && |
530 | $remoteActionApi->executeTestEditActionQuery( |
531 | $sourceUrl, |
532 | $this->getUser(), |
533 | $importPlan->getTitle() |
534 | )->isGood(); |
535 | $capabilities['canAutomateDelete'] = |
536 | $isCentralAuthEnabled && |
537 | $config->get( 'FileImporterSourceWikiDeletion' ) && |
538 | $remoteActionApi->executeUserRightsQuery( $sourceUrl, $this->getUser() )->isGood(); |
539 | |
540 | if ( $capabilities['canAutomateDelete'] ) { |
541 | $capabilities['automateDeleteSelected'] = $importPlan->getAutomateSourceWikiDelete(); |
542 | $this->stats->increment( 'FileImporter.specialPage.action.offeredSourceDelete' ); |
543 | } elseif ( $capabilities['canAutomateEdit'] ) { |
544 | $capabilities['automateEditSelected'] = |
545 | $importPlan->getAutomateSourceWikiCleanUp() || |
546 | $importPlan->getRequest()->getImportDetailsHash() === ''; |
547 | $capabilities['cleanupTitle'] = |
548 | $lookup->fetchNowCommonsLocalTitle( $sourceUrl ); |
549 | $this->stats->increment( 'FileImporter.specialPage.action.offeredSourceEdit' ); |
550 | } |
551 | |
552 | return $capabilities; |
553 | } |
554 | |
555 | private function showCodexImportPage( ImportPlan $importPlan ): void { |
556 | $this->getOutput()->addHTML( |
557 | Html::rawElement( 'noscript', [], $this->msg( 'fileimporter-no-script-warning' ) ) |
558 | ); |
559 | |
560 | $this->getOutput()->addHTML( |
561 | Html::rawElement( 'div', [ 'id' => 'ext-fileimporter-vue-root' ] ) |
562 | ); |
563 | |
564 | $showHelpBanner = !MediaWikiServices::getInstance()->getService( 'UserOptionsLookup' ) |
565 | ->getBoolOption( $this->getUser(), 'userjs-fileimporter-hide-help-banner' ); |
566 | |
567 | $this->getOutput()->addJsConfigVars( [ |
568 | 'wgFileImporterAutomatedCapabilities' => $this->getAutomatedCapabilities( $importPlan ), |
569 | 'wgFileImporterClientUrl' => $importPlan->getRequest()->getUrl()->getUrl(), |
570 | 'wgFileImporterEditToken' => $this->getUser()->getEditToken(), |
571 | 'wgFileImporterFileRevisionsCount' => |
572 | count( $importPlan->getDetails()->getFileRevisions()->toArray() ), |
573 | 'wgFileImporterHelpBannerContentHtml' => $showHelpBanner ? |
574 | FileImporterUtils::addTargetBlankToLinks( |
575 | $this->msg( 'fileimporter-help-banner-text' )->parse() |
576 | ) : null, |
577 | 'wgFileImporterTextRevisionsCount' => |
578 | count( $importPlan->getDetails()->getTextRevisions()->toArray() ), |
579 | 'wgFileImporterTitle' => $importPlan->getFileName(), |
580 | 'wgFileImporterFileExtension' => $importPlan->getFileExtension(), |
581 | 'wgFileImporterPrefixedTitle' => $importPlan->getTitle()->getPrefixedText(), |
582 | 'wgFileImporterImageUrl' => $importPlan->getDetails()->getImageDisplayUrl(), |
583 | 'wgFileImporterInitialFileInfoWikitext' => $importPlan->getInitialFileInfoText(), |
584 | 'wgFileImporterFileInfoWikitext' => |
585 | // FIXME: can assume the edit field is persistent |
586 | $importPlan->getRequest()->getIntendedText() ?? $importPlan->getFileInfoText(), |
587 | 'wgFileImporterEditSummary' => $importPlan->getRequest()->getIntendedSummary(), |
588 | 'wgFileImporterDetailsHash' => $importPlan->getDetails()->getOriginalHash(), |
589 | 'wgFileImporterTemplateReplacementCount' => $importPlan->getNumberOfTemplateReplacements(), |
590 | ] ); |
591 | } |
592 | |
593 | private function showLandingPage(): void { |
594 | $page = $this->getConfig()->get( 'FileImporterShowInputScreen' ) |
595 | ? new InputFormPage( $this ) |
596 | : new InfoPage( $this ); |
597 | |
598 | $this->getOutput()->addHTML( $page->getHtml() ); |
599 | } |
600 | |
601 | } |