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 Exception;
6use FileImporter\Data\ImportPlan;
7use FileImporter\Data\ImportRequest;
8use FileImporter\Exceptions\AbuseFilterWarningsException;
9use FileImporter\Exceptions\CommunityPolicyException;
10use FileImporter\Exceptions\DuplicateFilesException;
11use FileImporter\Exceptions\HttpRequestException;
12use FileImporter\Exceptions\ImportException;
13use FileImporter\Exceptions\LocalizedImportException;
14use FileImporter\Exceptions\RecoverableTitleException;
15use FileImporter\Html\ChangeFileInfoForm;
16use FileImporter\Html\ChangeFileNameForm;
17use FileImporter\Html\DuplicateFilesErrorPage;
18use FileImporter\Html\ErrorPage;
19use FileImporter\Html\FileInfoDiffPage;
20use FileImporter\Html\HelpBanner;
21use FileImporter\Html\ImportPreviewPage;
22use FileImporter\Html\ImportSuccessSnippet;
23use FileImporter\Html\InfoPage;
24use FileImporter\Html\InputFormPage;
25use FileImporter\Html\RecoverableTitleExceptionPage;
26use FileImporter\Html\SourceWikiCleanupSnippet;
27use FileImporter\Remote\MediaWiki\RemoteApiActionExecutor;
28use FileImporter\Services\Importer;
29use FileImporter\Services\ImportPlanFactory;
30use FileImporter\Services\SourceSiteLocator;
31use FileImporter\Services\WikidataTemplateLookup;
32use MediaWiki\Config\Config;
33use MediaWiki\Content\IContentHandlerFactory;
34use MediaWiki\EditPage\EditPage;
35use MediaWiki\Exception\ErrorPageError;
36use MediaWiki\Exception\PermissionsError;
37use MediaWiki\Exception\UserBlockedError;
38use MediaWiki\Html\Html;
39use MediaWiki\Logger\LoggerFactory;
40use MediaWiki\Registration\ExtensionRegistry;
41use MediaWiki\Request\WebRequest;
42use MediaWiki\SpecialPage\SpecialPage;
43use MediaWiki\Status\Status;
44use MediaWiki\User\Options\UserOptionsManager;
45use MediaWiki\User\User;
46use OOUI\HtmlSnippet;
47use OOUI\MessageWidget;
48use Psr\Log\LoggerInterface;
49use StatusValue;
50use UploadBase;
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    /** @inheritDoc */
102    public function doesWrites() {
103        return true;
104    }
105
106    /**
107     * @inheritDoc
108     */
109    public function getGroupName() {
110        return 'media';
111    }
112
113    /**
114     * @inheritDoc
115     */
116    public function userCanExecute( User $user ) {
117        return UploadBase::isEnabled() && parent::userCanExecute( $user );
118    }
119
120    /**
121     * Checks based on those in EditPage and SpecialUpload
122     *
123     * @throws ErrorPageError when one of the checks failed
124     */
125    private function executeStandardChecks(): void {
126        $unicodeCheck = $this->getRequest()->getText( 'wpUnicodeCheck' );
127        if ( $unicodeCheck && $unicodeCheck !== EditPage::UNICODE_CHECK ) {
128            throw new ErrorPageError( 'errorpagetitle', 'unicode-support-fail' );
129        }
130
131        # Check uploading enabled
132        if ( !UploadBase::isEnabled() ) {
133            $this->logErrorStats( self::ERROR_UPLOAD_DISABLED, false );
134            throw new ErrorPageError( 'uploaddisabled', 'uploaddisabledtext' );
135        }
136
137        $user = $this->getUser();
138
139        // Check if the user does have all the rights required via $wgFileImporterRequiredRight (set
140        // to "upload" by default), as well as "upload" and "edit" in case â€¦RequiredRight is more
141        // relaxed. Note special pages must call userCanExecute() manually when parent::execute()
142        // isn't called, {@see SpecialPage::__construct}.
143        $missingPermission = parent::userCanExecute( $user )
144            ? UploadBase::isAllowed( $user )
145            : $this->getRestriction();
146        if ( is_string( $missingPermission ) ) {
147            $this->logErrorStats( self::ERROR_USER_PERMISSIONS, false );
148            throw new PermissionsError( $missingPermission );
149        }
150
151        # Check blocks
152        $localBlock = $user->getBlock();
153        if ( $localBlock ) {
154            $this->logErrorStats( self::ERROR_LOCAL_BLOCK, false );
155            throw new UserBlockedError( $localBlock );
156        }
157
158        # Check whether we actually want to allow changing stuff
159        $this->checkReadOnly();
160    }
161
162    /** @inheritDoc */
163    public function getDescription() {
164        return $this->msg( 'fileimporter-specialpage' );
165    }
166
167    /**
168     * @param string|null $subPage
169     */
170    public function execute( $subPage ): void {
171        $webRequest = $this->getRequest();
172        $clientUrl = $webRequest->getVal( 'clientUrl', '' );
173        $action = $webRequest->getRawVal( ImportPreviewPage::ACTION_BUTTON );
174        if ( $action ) {
175            $this->logger->info( "Performing $action on ImportPlan for URL: $clientUrl" );
176        }
177
178        $isCodex = $webRequest->getBool( 'codex' ) &&
179            $this->getConfig()->get( 'FileImporterCodexMode' );
180        $isCodexSubmit = $isCodex && $this->getRequest()->wasPosted() && $action === 'submit';
181
182        if ( !$isCodexSubmit ) {
183            $this->setHeaders();
184            $this->getOutput()->enableOOUI();
185        }
186        $this->executeStandardChecks();
187
188        if ( !$isCodex ) {
189            $this->getOutput()->addModuleStyles( 'ext.FileImporter.SpecialCss' );
190            $this->getOutput()->addModuleStyles( 'ext.FileImporter.Images' );
191            $this->getOutput()->addModules( 'ext.FileImporter.SpecialJs' );
192        }
193
194        // Note: executions by users that don't have the rights to view the page etc will not be
195        // shown in this metric as executeStandardChecks will have already kicked them out,
196        $execTotalMetric = $this->statsFactory->getCounter( 'specialPage_executions_total' )
197            ->setLabel( 'parameter', 'none' );
198        // The importSource url parameter is added to requests from the FileExporter extension.
199        if ( $webRequest->getRawVal( 'importSource' ) === 'FileExporter' ) {
200            $execTotalMetric->setLabel( 'parameter', 'fromFileExporter' )
201                ->copyToStatsdAt( 'FileImporter.specialPage.execute.fromFileExporter' );
202        }
203
204        if ( $clientUrl === '' ) {
205            $execTotalMetric->setLabel( 'parameter', 'noClientUrl' )
206                ->copyToStatsdAt( '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->statsFactory->getCounter( 'errors_total' )
330            ->setLabel( 'recoverable', wfBoolToStr( $isRecoverable ) )
331            ->setLabel( 'type', $type )
332            ->copyToStatsdAt( 'FileImporter.error.byRecoverable.' . wfBoolToStr( $isRecoverable ) . '.byType.' . $type )
333            ->increment();
334    }
335
336    private function doCodexImport( ImportPlan $importPlan ): void {
337        // TODO handle error cases and echo JSON to allow Codex to visualize the errors
338        try {
339            $this->importer->import(
340                $this->getUser(),
341                $importPlan
342            );
343            $this->statsFactory->getCounter( 'imports_total' )
344                ->setLabel( 'result', 'success' )
345                ->copyToStatsdAt( 'FileImporter.import.result.success' )
346                ->increment();
347            $this->logActionStats( $importPlan );
348
349            $postImportResult = $this->performPostImportActions( $importPlan );
350            $successRedirectUrl = ( new ImportSuccessSnippet() )->getRedirectWithNotice(
351                $importPlan->getTitle(),
352                $this->getUser(),
353                $postImportResult
354            );
355
356            echo json_encode( [
357                'success' => true,
358                'redirect' => $successRedirectUrl,
359            ] );
360        } catch ( ImportException $exception ) {
361            if ( $exception instanceof AbuseFilterWarningsException ) {
362                $warningMessages = [];
363                $warningMessages[] = [
364                    'type' => 'warning',
365                    'message' => $this->getWarningMessage( $exception )
366                ];
367
368                foreach ( $exception->getMessages() as $msg ) {
369                    $warningMessages[] = [
370                        'type' => 'warning',
371                        'message' => $this->msg( $msg )->parse()
372                    ];
373                }
374
375                echo json_encode( [
376                    'error' => true,
377                    'warningMessages' => $warningMessages,
378                    'validationWarnings' => $importPlan->getValidationWarnings()
379                ] );
380            } else {
381                // TODO: More graceful error handling
382                echo json_encode( [
383                    'error' => true,
384                    'output' => $exception->getTrace(),
385                ] );
386            }
387        }
388    }
389
390    private function doImport( ImportPlan $importPlan ): bool {
391        $out = $this->getOutput();
392        $importDetails = $importPlan->getDetails();
393
394        $importDetailsHash = $out->getRequest()->getRawVal( 'importDetailsHash' ) ?? '';
395        $token = $out->getRequest()->getRawVal( 'token' ) ?? '';
396
397        if ( !$this->getContext()->getCsrfTokenSet()->matchToken( $token ) ) {
398            $this->showWarningMessage( $this->msg( 'fileimporter-badtoken' )->parse() );
399            $this->logErrorStats( 'badToken', true );
400            return false;
401        }
402
403        if ( $importDetails->getOriginalHash() !== $importDetailsHash ) {
404            $this->showWarningMessage( $this->msg( 'fileimporter-badimporthash' )->parse() );
405            $this->logErrorStats( 'badImportHash', true );
406            return false;
407        }
408
409        try {
410            $this->importer->import(
411                $this->getUser(),
412                $importPlan
413            );
414            $this->statsFactory->getCounter( 'imports_total' )
415                ->setLabel( 'result', 'success' )
416                ->copyToStatsdAt( 'FileImporter.import.result.success' )
417                ->increment();
418            // TODO: inline at site of action
419            $this->logActionStats( $importPlan );
420
421            $postImportResult = $this->performPostImportActions( $importPlan );
422
423            $out->redirect(
424                ( new ImportSuccessSnippet() )->getRedirectWithNotice(
425                    $importPlan->getTitle(),
426                    $this->getUser(),
427                    $postImportResult
428                )
429            );
430
431            return true;
432        } catch ( ImportException $exception ) {
433            $this->logErrorStats(
434                (string)$exception->getCode(),
435                $exception instanceof RecoverableTitleException
436            );
437
438            if ( $exception instanceof AbuseFilterWarningsException ) {
439                $this->showWarningMessage( $this->getWarningMessage( $exception ), 'warning' );
440
441                foreach ( $exception->getMessages() as $msg ) {
442                    $this->showWarningMessage(
443                        $this->msg( $msg )->parse(),
444                        'warning',
445                        true
446                    );
447                }
448
449                $this->showImportPage( $importPlan );
450            } else {
451                $this->showWarningMessage(
452                    Html::rawElement( 'strong', [], $this->msg( 'fileimporter-importfailed' )->parse() ) .
453                    '<br>' .
454                    $this->getWarningMessage( $exception ),
455                    'error'
456                );
457            }
458            return false;
459        }
460    }
461
462    private function logActionStats( ImportPlan $importPlan ): void {
463        foreach ( $importPlan->getActionStats() as $key => $_ ) {
464            if (
465                $key === ImportPreviewPage::ACTION_EDIT_TITLE ||
466                $key === ImportPreviewPage::ACTION_EDIT_INFO ||
467                $key === SourceWikiCleanupSnippet::ACTION_OFFERED_SOURCE_DELETE ||
468                $key === SourceWikiCleanupSnippet::ACTION_OFFERED_SOURCE_EDIT
469            ) {
470                $this->statsFactory->getCounter( 'specialPage_actions_total' )
471                    ->setLabel( 'action', $key )
472                    ->copyToStatsdAt( 'FileImporter.specialPage.action.' . $key )
473                    ->increment();
474            }
475        }
476    }
477
478    private function performPostImportActions( ImportPlan $importPlan ): StatusValue {
479        $sourceSite = $importPlan->getRequest()->getUrl();
480        $postImportHandler = $this->sourceSiteLocator->getSourceSite( $sourceSite )
481            ->getPostImportHandler();
482
483        return $postImportHandler->execute( $importPlan, $this->getUser() );
484    }
485
486    /**
487     * @return string HTML
488     */
489    private function getWarningMessage( Exception $ex ): string {
490        if ( $ex instanceof LocalizedImportException ) {
491            return $ex->getMessageObject()->inLanguage( $this->getLanguage() )->parse();
492        }
493        if ( $ex instanceof HttpRequestException ) {
494            return Status::wrap( $ex->getStatusValue() )->getHTML( false, false,
495                $this->getLanguage() );
496        }
497
498        return htmlspecialchars( $ex->getMessage() );
499    }
500
501    /**
502     * @param string $html
503     * @param string $type Set to "notice" for a gray box, defaults to "error" (red)
504     * @param bool $inline
505     */
506    private function showWarningMessage( string $html, string $type = 'error', bool $inline = false ): void {
507        $this->getOutput()->enableOOUI();
508        $this->getOutput()->addHTML(
509            new MessageWidget( [
510                'label' => new HtmlSnippet( $html ),
511                'type' => $type,
512                'inline' => $inline,
513            ] ) .
514            '<br>'
515        );
516    }
517
518    private function showImportPage( ImportPlan $importPlan ): void {
519        $this->getOutput()->addHTML(
520            ( new ImportPreviewPage( $this ) )->getHtml( $importPlan )
521        );
522    }
523
524    /**
525     * @return array of automation features and whether they are available
526     */
527    private function getAutomatedCapabilities( ImportPlan $importPlan ) {
528        $capabilities = [];
529
530        $config = $this->getConfig();
531        $isCentralAuthEnabled = ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' );
532        $sourceUrl = $importPlan->getRequest()->getUrl();
533
534        $capabilities['canAutomateEdit'] =
535            $isCentralAuthEnabled &&
536            $config->get( 'FileImporterSourceWikiTemplating' ) &&
537            $this->templateLookup->fetchNowCommonsLocalTitle( $sourceUrl ) &&
538            $this->remoteActionApi->executeTestEditActionQuery(
539                $sourceUrl,
540                $this->getUser(),
541                $importPlan->getTitle()
542            )->isGood();
543        $capabilities['canAutomateDelete'] =
544            $isCentralAuthEnabled &&
545            $config->get( 'FileImporterSourceWikiDeletion' ) &&
546            $this->remoteActionApi->executeUserRightsQuery( $sourceUrl, $this->getUser() )->isGood();
547
548        if ( $capabilities['canAutomateDelete'] ) {
549            $capabilities['automateDeleteSelected'] = $importPlan->getAutomateSourceWikiDelete();
550
551            $this->statsFactory->getCounter( 'specialPage_actions_total' )
552                ->setLabel( 'action', 'offeredSourceDelete' )
553                ->copyToStatsdAt( 'FileImporter.specialPage.action.offeredSourceDelete' )
554                ->increment();
555        } elseif ( $capabilities['canAutomateEdit'] ) {
556            $capabilities['automateEditSelected'] =
557                $importPlan->getAutomateSourceWikiCleanUp() ||
558                $importPlan->getRequest()->getImportDetailsHash() === '';
559            $capabilities['cleanupTitle'] =
560                $this->templateLookup->fetchNowCommonsLocalTitle( $sourceUrl );
561            $this->statsFactory->getCounter( 'specialPage_actions_total' )
562                ->setLabel( 'action', 'offeredSourceEdit' )
563                ->copyToStatsdAt( 'FileImporter.specialPage.action.offeredSourceEdit' )
564                ->increment();
565        }
566
567        return $capabilities;
568    }
569
570    private function showCodexImportPage( ImportPlan $importPlan ): void {
571        $this->getOutput()->addHTML(
572            Html::rawElement( 'noscript', [], $this->msg( 'fileimporter-no-script-warning' ) )
573        );
574
575        $this->getOutput()->addHTML(
576            Html::rawElement( 'div', [ 'id' => 'ext-fileimporter-vue-root' ] )
577        );
578
579        $showHelpBanner = !$this->userOptionsManager
580            ->getBoolOption( $this->getUser(), 'userjs-fileimporter-hide-help-banner' );
581
582        $this->getOutput()->addJsConfigVars( [
583            'wgFileImporterAutomatedCapabilities' => $this->getAutomatedCapabilities( $importPlan ),
584            'wgFileImporterClientUrl' => $importPlan->getRequest()->getUrl()->getUrl(),
585            'wgFileImporterEditToken' => $this->getUser()->getEditToken(),
586            'wgFileImporterFileRevisionsCount' =>
587                count( $importPlan->getDetails()->getFileRevisions()->toArray() ),
588            'wgFileImporterHelpBannerContentHtml' => $showHelpBanner ?
589                FileImporterUtils::addTargetBlankToLinks(
590                    $this->msg( 'fileimporter-help-banner-text' )->parse()
591                ) : null,
592            'wgFileImporterTextRevisionsCount' =>
593                count( $importPlan->getDetails()->getTextRevisions()->toArray() ),
594            'wgFileImporterTitle' => $importPlan->getFileName(),
595            'wgFileImporterFileExtension' => $importPlan->getFileExtension(),
596            'wgFileImporterPrefixedTitle' => $importPlan->getTitle()->getPrefixedText(),
597            'wgFileImporterImageUrl' => $importPlan->getDetails()->getImageDisplayUrl(),
598            'wgFileImporterInitialFileInfoWikitext' => $importPlan->getInitialFileInfoText(),
599            'wgFileImporterFileInfoWikitext' =>
600                // FIXME: can assume the edit field is persistent
601                $importPlan->getRequest()->getIntendedText() ?? $importPlan->getFileInfoText(),
602            'wgFileImporterEditSummary' => $importPlan->getRequest()->getIntendedSummary(),
603            'wgFileImporterDetailsHash' => $importPlan->getDetails()->getOriginalHash(),
604            'wgFileImporterTemplateReplacementCount' => $importPlan->getNumberOfTemplateReplacements(),
605        ] );
606    }
607
608    private function showLandingPage(): void {
609        $page = $this->getConfig()->get( 'FileImporterShowInputScreen' )
610            ? new InputFormPage( $this )
611            : new InfoPage( $this );
612
613        $this->getOutput()->addHTML( $page->getHtml() );
614    }
615
616}