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 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 MediaWiki\Config\Config; |
34 | use MediaWiki\Content\IContentHandlerFactory; |
35 | use MediaWiki\EditPage\EditPage; |
36 | use MediaWiki\Html\Html; |
37 | use MediaWiki\Logger\LoggerFactory; |
38 | use MediaWiki\Registration\ExtensionRegistry; |
39 | use MediaWiki\Request\WebRequest; |
40 | use MediaWiki\SpecialPage\SpecialPage; |
41 | use MediaWiki\Status\Status; |
42 | use MediaWiki\User\Options\UserOptionsManager; |
43 | use MediaWiki\User\User; |
44 | use OOUI\HtmlSnippet; |
45 | use OOUI\MessageWidget; |
46 | use PermissionsError; |
47 | use Psr\Log\LoggerInterface; |
48 | use StatusValue; |
49 | use UploadBase; |
50 | use UserBlockedError; |
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 | 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 | } |