Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
18.07% |
58 / 321 |
|
15.00% |
3 / 20 |
CRAP | |
0.00% |
0 / 1 |
SpecialImportFile | |
18.07% |
58 / 321 |
|
15.00% |
3 / 20 |
3003.88 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 14 |
|
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 / 18 |
|
0.00% |
0 / 1 |
56 | |||
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 / 28 |
|
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 FileImporter\Data\ImportPlan; |
8 | use FileImporter\Data\ImportRequest; |
9 | use FileImporter\Exceptions\AbuseFilterWarningsException; |
10 | use FileImporter\Exceptions\CommunityPolicyException; |
11 | use FileImporter\Exceptions\DuplicateFilesException; |
12 | use FileImporter\Exceptions\HttpRequestException; |
13 | use FileImporter\Exceptions\ImportException; |
14 | use FileImporter\Exceptions\LocalizedImportException; |
15 | use FileImporter\Exceptions\RecoverableTitleException; |
16 | use FileImporter\Html\ChangeFileInfoForm; |
17 | use FileImporter\Html\ChangeFileNameForm; |
18 | use FileImporter\Html\DuplicateFilesErrorPage; |
19 | use FileImporter\Html\ErrorPage; |
20 | use FileImporter\Html\FileInfoDiffPage; |
21 | use FileImporter\Html\HelpBanner; |
22 | use FileImporter\Html\ImportPreviewPage; |
23 | use FileImporter\Html\ImportSuccessSnippet; |
24 | use FileImporter\Html\InfoPage; |
25 | use FileImporter\Html\InputFormPage; |
26 | use FileImporter\Html\RecoverableTitleExceptionPage; |
27 | use FileImporter\Html\SourceWikiCleanupSnippet; |
28 | use FileImporter\Remote\MediaWiki\RemoteApiActionExecutor; |
29 | use FileImporter\Services\Importer; |
30 | use FileImporter\Services\ImportPlanFactory; |
31 | use FileImporter\Services\SourceSiteLocator; |
32 | use FileImporter\Services\WikidataTemplateLookup; |
33 | use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; |
34 | use MediaWiki\Config\Config; |
35 | use MediaWiki\Content\IContentHandlerFactory; |
36 | use MediaWiki\EditPage\EditPage; |
37 | use MediaWiki\Html\Html; |
38 | use MediaWiki\Logger\LoggerFactory; |
39 | use MediaWiki\Registration\ExtensionRegistry; |
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 | |
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 StatsdDataFactoryInterface $stats; |
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 | StatsdDataFactoryInterface $statsdDataFactory, |
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->stats = $statsdDataFactory; |
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 | $this->stats->increment( 'FileImporter.specialPage.execute.total' ); |
196 | // The importSource url parameter is added to requests from the FileExporter extension. |
197 | if ( $webRequest->getRawVal( 'importSource' ) === 'FileExporter' ) { |
198 | $this->stats->increment( 'FileImporter.specialPage.execute.fromFileExporter' ); |
199 | } |
200 | |
201 | if ( $clientUrl === '' ) { |
202 | $this->stats->increment( 'FileImporter.specialPage.execute.noClientUrl' ); |
203 | $this->showLandingPage(); |
204 | return; |
205 | } |
206 | |
207 | if ( $webRequest->getBool( HelpBanner::HIDE_HELP_BANNER_CHECK_BOX ) && |
208 | $this->getUser()->isNamed() |
209 | ) { |
210 | $this->userOptionsManager->setOption( |
211 | $this->getUser(), |
212 | HelpBanner::HIDE_HELP_BANNER_PREFERENCE, |
213 | '1' |
214 | ); |
215 | $this->userOptionsManager->saveOptions( $this->getUser() ); |
216 | } |
217 | |
218 | try { |
219 | $this->logger->info( 'Getting ImportPlan for URL: ' . $clientUrl ); |
220 | $importPlan = $this->makeImportPlan( $webRequest ); |
221 | |
222 | if ( $isCodexSubmit ) { |
223 | // disable all default output of the special page, like headers, title, navigation |
224 | $this->getOutput()->disable(); |
225 | header( 'Content-type: application/json; charset=utf-8' ); |
226 | $this->doCodexImport( $importPlan ); |
227 | } elseif ( $isCodex ) { |
228 | $this->getOutput()->addModules( 'ext.FileImporter.SpecialCodexJs' ); |
229 | $this->showCodexImportPage( $importPlan ); |
230 | } else { |
231 | $this->handleAction( $action, $importPlan ); |
232 | } |
233 | } catch ( ImportException $exception ) { |
234 | $this->logger->info( 'ImportException: ' . $exception->getMessage() ); |
235 | $this->logErrorStats( |
236 | (string)$exception->getCode(), |
237 | $exception instanceof RecoverableTitleException |
238 | ); |
239 | |
240 | if ( $exception instanceof DuplicateFilesException ) { |
241 | $html = ( new DuplicateFilesErrorPage( $this ) )->getHtml( |
242 | $exception->getFiles(), |
243 | $clientUrl |
244 | ); |
245 | } elseif ( $exception instanceof RecoverableTitleException ) { |
246 | $html = ( new RecoverableTitleExceptionPage( $this ) )->getHtml( $exception ); |
247 | } else { |
248 | $html = ( new ErrorPage( $this ) )->getHtml( |
249 | $this->getWarningMessage( $exception ), |
250 | $clientUrl, |
251 | $exception instanceof CommunityPolicyException ? 'warning' : 'error' |
252 | ); |
253 | } |
254 | $this->getOutput()->enableOOUI(); |
255 | $this->getOutput()->addHTML( $html ); |
256 | } |
257 | } |
258 | |
259 | private function handleAction( ?string $action, ImportPlan $importPlan ): void { |
260 | switch ( $action ) { |
261 | case ImportPreviewPage::ACTION_SUBMIT: |
262 | $this->doImport( $importPlan ); |
263 | break; |
264 | case ImportPreviewPage::ACTION_EDIT_TITLE: |
265 | $importPlan->setActionIsPerformed( ImportPreviewPage::ACTION_EDIT_TITLE ); |
266 | $this->getOutput()->addHTML( |
267 | ( new ChangeFileNameForm( $this ) )->getHtml( $importPlan ) |
268 | ); |
269 | break; |
270 | case ImportPreviewPage::ACTION_EDIT_INFO: |
271 | $importPlan->setActionIsPerformed( ImportPreviewPage::ACTION_EDIT_INFO ); |
272 | $this->getOutput()->addHTML( |
273 | ( new ChangeFileInfoForm( $this ) )->getHtml( $importPlan ) |
274 | ); |
275 | break; |
276 | case ImportPreviewPage::ACTION_VIEW_DIFF: |
277 | $contentHandler = $this->contentHandlerFactory->getContentHandler( CONTENT_MODEL_WIKITEXT ); |
278 | $this->getOutput()->addHTML( |
279 | ( new FileInfoDiffPage( $this ) )->getHtml( $importPlan, $contentHandler ) |
280 | ); |
281 | break; |
282 | default: |
283 | $this->showImportPage( $importPlan ); |
284 | } |
285 | } |
286 | |
287 | /** |
288 | * @throws ImportException |
289 | */ |
290 | private function makeImportPlan( WebRequest $webRequest ): ImportPlan { |
291 | $importRequest = new ImportRequest( |
292 | $webRequest->getVal( 'clientUrl' ), |
293 | $webRequest->getVal( 'intendedFileName' ), |
294 | $webRequest->getVal( 'intendedWikitext' ), |
295 | $webRequest->getVal( 'intendedRevisionSummary' ), |
296 | $webRequest->getRawVal( 'importDetailsHash' ) ?? '' |
297 | ); |
298 | |
299 | $url = $importRequest->getUrl(); |
300 | $sourceSite = $this->sourceSiteLocator->getSourceSite( $url ); |
301 | $importDetails = $sourceSite->retrieveImportDetails( $url ); |
302 | |
303 | $importPlan = $this->importPlanFactory->newPlan( |
304 | $importRequest, |
305 | $importDetails, |
306 | $this->getUser() |
307 | ); |
308 | $importPlan->setActionStats( |
309 | json_decode( $webRequest->getVal( 'actionStats', '[]' ), true ) |
310 | ); |
311 | $importPlan->setValidationWarnings( |
312 | json_decode( $webRequest->getVal( 'validationWarnings', '[]' ), true ) |
313 | ); |
314 | $importPlan->setAutomateSourceWikiCleanUp( |
315 | $webRequest->getBool( 'automateSourceWikiCleanup' ) |
316 | ); |
317 | $importPlan->setAutomateSourceWikiDelete( |
318 | $webRequest->getBool( 'automateSourceWikiDelete' ) |
319 | ); |
320 | |
321 | return $importPlan; |
322 | } |
323 | |
324 | private function logErrorStats( string $type, bool $isRecoverable ): void { |
325 | $this->stats->increment( 'FileImporter.error.byRecoverable.' |
326 | . wfBoolToStr( $isRecoverable ) . '.byType.' . $type ); |
327 | } |
328 | |
329 | private function doCodexImport( ImportPlan $importPlan ): void { |
330 | // TODO handle error cases and echo JSON to allow Codex to visualize the errors |
331 | try { |
332 | $this->importer->import( |
333 | $this->getUser(), |
334 | $importPlan |
335 | ); |
336 | $this->stats->increment( 'FileImporter.import.result.success' ); |
337 | $this->logActionStats( $importPlan ); |
338 | |
339 | $postImportResult = $this->performPostImportActions( $importPlan ); |
340 | $successRedirectUrl = ( new ImportSuccessSnippet() )->getRedirectWithNotice( |
341 | $importPlan->getTitle(), |
342 | $this->getUser(), |
343 | $postImportResult |
344 | ); |
345 | |
346 | echo json_encode( [ |
347 | 'success' => true, |
348 | 'redirect' => $successRedirectUrl, |
349 | ] ); |
350 | } catch ( ImportException $exception ) { |
351 | if ( $exception instanceof AbuseFilterWarningsException ) { |
352 | $warningMessages = []; |
353 | $warningMessages[] = [ |
354 | 'type' => 'warning', |
355 | 'message' => $this->getWarningMessage( $exception ) |
356 | ]; |
357 | |
358 | foreach ( $exception->getMessages() as $msg ) { |
359 | $warningMessages[] = [ |
360 | 'type' => 'warning', |
361 | 'message' => $this->msg( $msg )->parse() |
362 | ]; |
363 | } |
364 | |
365 | echo json_encode( [ |
366 | 'error' => true, |
367 | 'warningMessages' => $warningMessages, |
368 | 'validationWarnings' => $importPlan->getValidationWarnings() |
369 | ] ); |
370 | } else { |
371 | // TODO: More graceful error handling |
372 | echo json_encode( [ |
373 | 'error' => true, |
374 | 'output' => $exception->getTrace(), |
375 | ] ); |
376 | } |
377 | } |
378 | } |
379 | |
380 | private function doImport( ImportPlan $importPlan ): bool { |
381 | $out = $this->getOutput(); |
382 | $importDetails = $importPlan->getDetails(); |
383 | |
384 | $importDetailsHash = $out->getRequest()->getRawVal( 'importDetailsHash' ) ?? ''; |
385 | $token = $out->getRequest()->getRawVal( 'token' ) ?? ''; |
386 | |
387 | if ( !$this->getContext()->getCsrfTokenSet()->matchToken( $token ) ) { |
388 | $this->showWarningMessage( $this->msg( 'fileimporter-badtoken' )->parse() ); |
389 | $this->logErrorStats( 'badToken', true ); |
390 | return false; |
391 | } |
392 | |
393 | if ( $importDetails->getOriginalHash() !== $importDetailsHash ) { |
394 | $this->showWarningMessage( $this->msg( 'fileimporter-badimporthash' )->parse() ); |
395 | $this->logErrorStats( 'badImportHash', true ); |
396 | return false; |
397 | } |
398 | |
399 | try { |
400 | $this->importer->import( |
401 | $this->getUser(), |
402 | $importPlan |
403 | ); |
404 | $this->stats->increment( 'FileImporter.import.result.success' ); |
405 | // TODO: inline at site of action |
406 | $this->logActionStats( $importPlan ); |
407 | |
408 | $postImportResult = $this->performPostImportActions( $importPlan ); |
409 | |
410 | $out->redirect( |
411 | ( new ImportSuccessSnippet() )->getRedirectWithNotice( |
412 | $importPlan->getTitle(), |
413 | $this->getUser(), |
414 | $postImportResult |
415 | ) |
416 | ); |
417 | |
418 | return true; |
419 | } catch ( ImportException $exception ) { |
420 | $this->logErrorStats( |
421 | (string)$exception->getCode(), |
422 | $exception instanceof RecoverableTitleException |
423 | ); |
424 | |
425 | if ( $exception instanceof AbuseFilterWarningsException ) { |
426 | $this->showWarningMessage( $this->getWarningMessage( $exception ), 'warning' ); |
427 | |
428 | foreach ( $exception->getMessages() as $msg ) { |
429 | $this->showWarningMessage( |
430 | $this->msg( $msg )->parse(), |
431 | 'warning', |
432 | true |
433 | ); |
434 | } |
435 | |
436 | $this->showImportPage( $importPlan ); |
437 | } else { |
438 | $this->showWarningMessage( |
439 | Html::rawElement( 'strong', [], $this->msg( 'fileimporter-importfailed' )->parse() ) . |
440 | '<br>' . |
441 | $this->getWarningMessage( $exception ), |
442 | 'error' |
443 | ); |
444 | } |
445 | return false; |
446 | } |
447 | } |
448 | |
449 | private function logActionStats( ImportPlan $importPlan ): void { |
450 | foreach ( $importPlan->getActionStats() as $key => $_ ) { |
451 | if ( |
452 | $key === ImportPreviewPage::ACTION_EDIT_TITLE || |
453 | $key === ImportPreviewPage::ACTION_EDIT_INFO || |
454 | $key === SourceWikiCleanupSnippet::ACTION_OFFERED_SOURCE_DELETE || |
455 | $key === SourceWikiCleanupSnippet::ACTION_OFFERED_SOURCE_EDIT |
456 | ) { |
457 | $this->stats->increment( 'FileImporter.specialPage.action.' . $key ); |
458 | } |
459 | } |
460 | } |
461 | |
462 | private function performPostImportActions( ImportPlan $importPlan ): StatusValue { |
463 | $sourceSite = $importPlan->getRequest()->getUrl(); |
464 | $postImportHandler = $this->sourceSiteLocator->getSourceSite( $sourceSite ) |
465 | ->getPostImportHandler(); |
466 | |
467 | return $postImportHandler->execute( $importPlan, $this->getUser() ); |
468 | } |
469 | |
470 | /** |
471 | * @return string HTML |
472 | */ |
473 | private function getWarningMessage( Exception $ex ): string { |
474 | if ( $ex instanceof LocalizedImportException ) { |
475 | return $ex->getMessageObject()->inLanguage( $this->getLanguage() )->parse(); |
476 | } |
477 | if ( $ex instanceof HttpRequestException ) { |
478 | return Status::wrap( $ex->getStatusValue() )->getHTML( false, false, |
479 | $this->getLanguage() ); |
480 | } |
481 | |
482 | return htmlspecialchars( $ex->getMessage() ); |
483 | } |
484 | |
485 | /** |
486 | * @param string $html |
487 | * @param string $type Set to "notice" for a gray box, defaults to "error" (red) |
488 | * @param bool $inline |
489 | */ |
490 | private function showWarningMessage( string $html, string $type = 'error', bool $inline = false ): void { |
491 | $this->getOutput()->enableOOUI(); |
492 | $this->getOutput()->addHTML( |
493 | new MessageWidget( [ |
494 | 'label' => new HtmlSnippet( $html ), |
495 | 'type' => $type, |
496 | 'inline' => $inline, |
497 | ] ) . |
498 | '<br>' |
499 | ); |
500 | } |
501 | |
502 | private function showImportPage( ImportPlan $importPlan ): void { |
503 | $this->getOutput()->addHTML( |
504 | ( new ImportPreviewPage( $this ) )->getHtml( $importPlan ) |
505 | ); |
506 | } |
507 | |
508 | /** |
509 | * @return array of automation features and whether they are available |
510 | */ |
511 | private function getAutomatedCapabilities( ImportPlan $importPlan ) { |
512 | $capabilities = []; |
513 | |
514 | $config = $this->getConfig(); |
515 | $isCentralAuthEnabled = ExtensionRegistry::getInstance()->isLoaded( 'CentralAuth' ); |
516 | $sourceUrl = $importPlan->getRequest()->getUrl(); |
517 | |
518 | $capabilities['canAutomateEdit'] = |
519 | $isCentralAuthEnabled && |
520 | $config->get( 'FileImporterSourceWikiTemplating' ) && |
521 | $this->templateLookup->fetchNowCommonsLocalTitle( $sourceUrl ) && |
522 | $this->remoteActionApi->executeTestEditActionQuery( |
523 | $sourceUrl, |
524 | $this->getUser(), |
525 | $importPlan->getTitle() |
526 | )->isGood(); |
527 | $capabilities['canAutomateDelete'] = |
528 | $isCentralAuthEnabled && |
529 | $config->get( 'FileImporterSourceWikiDeletion' ) && |
530 | $this->remoteActionApi->executeUserRightsQuery( $sourceUrl, $this->getUser() )->isGood(); |
531 | |
532 | if ( $capabilities['canAutomateDelete'] ) { |
533 | $capabilities['automateDeleteSelected'] = $importPlan->getAutomateSourceWikiDelete(); |
534 | $this->stats->increment( 'FileImporter.specialPage.action.offeredSourceDelete' ); |
535 | } elseif ( $capabilities['canAutomateEdit'] ) { |
536 | $capabilities['automateEditSelected'] = |
537 | $importPlan->getAutomateSourceWikiCleanUp() || |
538 | $importPlan->getRequest()->getImportDetailsHash() === ''; |
539 | $capabilities['cleanupTitle'] = |
540 | $this->templateLookup->fetchNowCommonsLocalTitle( $sourceUrl ); |
541 | $this->stats->increment( 'FileImporter.specialPage.action.offeredSourceEdit' ); |
542 | } |
543 | |
544 | return $capabilities; |
545 | } |
546 | |
547 | private function showCodexImportPage( ImportPlan $importPlan ): void { |
548 | $this->getOutput()->addHTML( |
549 | Html::rawElement( 'noscript', [], $this->msg( 'fileimporter-no-script-warning' ) ) |
550 | ); |
551 | |
552 | $this->getOutput()->addHTML( |
553 | Html::rawElement( 'div', [ 'id' => 'ext-fileimporter-vue-root' ] ) |
554 | ); |
555 | |
556 | $showHelpBanner = !$this->userOptionsManager |
557 | ->getBoolOption( $this->getUser(), 'userjs-fileimporter-hide-help-banner' ); |
558 | |
559 | $this->getOutput()->addJsConfigVars( [ |
560 | 'wgFileImporterAutomatedCapabilities' => $this->getAutomatedCapabilities( $importPlan ), |
561 | 'wgFileImporterClientUrl' => $importPlan->getRequest()->getUrl()->getUrl(), |
562 | 'wgFileImporterEditToken' => $this->getUser()->getEditToken(), |
563 | 'wgFileImporterFileRevisionsCount' => |
564 | count( $importPlan->getDetails()->getFileRevisions()->toArray() ), |
565 | 'wgFileImporterHelpBannerContentHtml' => $showHelpBanner ? |
566 | FileImporterUtils::addTargetBlankToLinks( |
567 | $this->msg( 'fileimporter-help-banner-text' )->parse() |
568 | ) : null, |
569 | 'wgFileImporterTextRevisionsCount' => |
570 | count( $importPlan->getDetails()->getTextRevisions()->toArray() ), |
571 | 'wgFileImporterTitle' => $importPlan->getFileName(), |
572 | 'wgFileImporterFileExtension' => $importPlan->getFileExtension(), |
573 | 'wgFileImporterPrefixedTitle' => $importPlan->getTitle()->getPrefixedText(), |
574 | 'wgFileImporterImageUrl' => $importPlan->getDetails()->getImageDisplayUrl(), |
575 | 'wgFileImporterInitialFileInfoWikitext' => $importPlan->getInitialFileInfoText(), |
576 | 'wgFileImporterFileInfoWikitext' => |
577 | // FIXME: can assume the edit field is persistent |
578 | $importPlan->getRequest()->getIntendedText() ?? $importPlan->getFileInfoText(), |
579 | 'wgFileImporterEditSummary' => $importPlan->getRequest()->getIntendedSummary(), |
580 | 'wgFileImporterDetailsHash' => $importPlan->getDetails()->getOriginalHash(), |
581 | 'wgFileImporterTemplateReplacementCount' => $importPlan->getNumberOfTemplateReplacements(), |
582 | ] ); |
583 | } |
584 | |
585 | private function showLandingPage(): void { |
586 | $page = $this->getConfig()->get( 'FileImporterShowInputScreen' ) |
587 | ? new InputFormPage( $this ) |
588 | : new InfoPage( $this ); |
589 | |
590 | $this->getOutput()->addHTML( $page->getHtml() ); |
591 | } |
592 | |
593 | } |