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