Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
18.71% |
64 / 342 |
|
15.00% |
3 / 20 |
CRAP | |
0.00% |
0 / 1 |
SpecialImportFile | |
18.71% |
64 / 342 |
|
15.00% |
3 / 20 |
2935.21 | |
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 / 64 |
|
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% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
doCodexImport | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
20 | |||
doImport | |
84.31% |
43 / 51 |
|
0.00% |
0 / 1 |
6.14 | |||
logActionStats | |
11.11% |
1 / 9 |
|
0.00% |
0 / 1 |
31.28 | |||
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 / 34 |
|
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 Exception; |
6 | use FileImporter\Data\ImportPlan; |
7 | use FileImporter\Data\ImportRequest; |
8 | use FileImporter\Exceptions\AbuseFilterWarningsException; |
9 | use FileImporter\Exceptions\CommunityPolicyException; |
10 | use FileImporter\Exceptions\DuplicateFilesException; |
11 | use FileImporter\Exceptions\HttpRequestException; |
12 | use FileImporter\Exceptions\ImportException; |
13 | use FileImporter\Exceptions\LocalizedImportException; |
14 | use FileImporter\Exceptions\RecoverableTitleException; |
15 | use FileImporter\Html\ChangeFileInfoForm; |
16 | use FileImporter\Html\ChangeFileNameForm; |
17 | use FileImporter\Html\DuplicateFilesErrorPage; |
18 | use FileImporter\Html\ErrorPage; |
19 | use FileImporter\Html\FileInfoDiffPage; |
20 | use FileImporter\Html\HelpBanner; |
21 | use FileImporter\Html\ImportPreviewPage; |
22 | use FileImporter\Html\ImportSuccessSnippet; |
23 | use FileImporter\Html\InfoPage; |
24 | use FileImporter\Html\InputFormPage; |
25 | use FileImporter\Html\RecoverableTitleExceptionPage; |
26 | use FileImporter\Html\SourceWikiCleanupSnippet; |
27 | use FileImporter\Remote\MediaWiki\RemoteApiActionExecutor; |
28 | use FileImporter\Services\Importer; |
29 | use FileImporter\Services\ImportPlanFactory; |
30 | use FileImporter\Services\SourceSiteLocator; |
31 | use FileImporter\Services\WikidataTemplateLookup; |
32 | use MediaWiki\Config\Config; |
33 | use MediaWiki\Content\IContentHandlerFactory; |
34 | use MediaWiki\EditPage\EditPage; |
35 | use MediaWiki\Exception\ErrorPageError; |
36 | use MediaWiki\Exception\PermissionsError; |
37 | use MediaWiki\Exception\UserBlockedError; |
38 | use MediaWiki\Html\Html; |
39 | use MediaWiki\Logger\LoggerFactory; |
40 | use MediaWiki\Registration\ExtensionRegistry; |
41 | use MediaWiki\Request\WebRequest; |
42 | use MediaWiki\SpecialPage\SpecialPage; |
43 | use MediaWiki\Status\Status; |
44 | use MediaWiki\User\Options\UserOptionsManager; |
45 | use MediaWiki\User\User; |
46 | use OOUI\HtmlSnippet; |
47 | use OOUI\MessageWidget; |
48 | use Psr\Log\LoggerInterface; |
49 | use StatusValue; |
50 | use UploadBase; |
51 | use Wikimedia\Stats\StatsFactory; |
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 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 | } |