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