Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 358
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialImport
0.00% covered (danger)
0.00%
0 / 358
0.00% covered (danger)
0.00%
0 / 10
3906
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRestriction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 handleRetrievedData
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 execute
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
110
 doImport
0.00% covered (danger)
0.00%
0 / 143
0.00% covered (danger)
0.00%
0 / 1
870
 getMappingFormPart
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
12
 showForm
0.00% covered (danger)
0.00%
0 / 140
0.00% covered (danger)
0.00%
0 / 1
182
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2003,2005 Brooke Vibber <bvibber@wikimedia.org>
4 * https://www.mediawiki.org/
5 *
6 * @license GPL-2.0-or-later
7 * @file
8 */
9
10namespace MediaWiki\Specials;
11
12use Exception;
13use MediaWiki\EditPage\DataStashTrait;
14use MediaWiki\Exception\PermissionsError;
15use MediaWiki\Html\Html;
16use MediaWiki\HTMLForm\HTMLForm;
17use MediaWiki\Import\ImportStreamSource;
18use MediaWiki\Import\WikiImporterFactory;
19use MediaWiki\MainConfigNames;
20use MediaWiki\Permissions\PermissionManager;
21use MediaWiki\Permissions\PermissionStatus;
22use MediaWiki\SpecialPage\SpecialPage;
23use MediaWiki\Specials\Helpers\ImportReporter;
24use MediaWiki\Status\Status;
25use MediaWiki\Title\Title;
26use UnexpectedValueException;
27use Wikimedia\Rdbms\DBError;
28use Wikimedia\RequestTimeout\TimeoutException;
29
30/**
31 * MediaWiki page data importer
32 *
33 * @ingroup SpecialPage
34 */
35class SpecialImport extends SpecialPage {
36    use DataStashTrait;
37
38    /** @var array */
39    private $importSources;
40
41    public function __construct(
42        private readonly WikiImporterFactory $wikiImporterFactory,
43        private readonly PermissionManager $permissionManager,
44    ) {
45        parent::__construct( 'Import' );
46    }
47
48    /** @inheritDoc */
49    public function getRestriction(): string {
50        return 'import';
51    }
52
53    /** @inheritDoc */
54    public function doesWrites() {
55        return true;
56    }
57
58    /** DataStashTrait abstracts / helpers */
59    protected function getTitle(): Title {
60        return $this->getPageTitle();
61    }
62
63    protected function handleRetrievedData( array $data ): void {
64        foreach ( $data as $k => $v ) {
65            $this->getRequest()->setVal( $k, $v );
66        }
67    }
68
69    /**
70     * Execute
71     * @param string|null $par
72     */
73    public function execute( $par ) {
74        $this->useTransactionalTimeLimit();
75
76        $this->setHeaders();
77        $this->outputHeader();
78
79        $this->importSources = $this->getConfig()->get( MainConfigNames::ImportSources );
80        // Avoid phan error by checking the type
81        if ( !is_array( $this->importSources ) ) {
82            throw new UnexpectedValueException( '$wgImportSources must be an array' );
83        }
84        $this->getHookRunner()->onImportSources( $this->importSources );
85
86        $authority = $this->getAuthority();
87        $statusImport = PermissionStatus::newEmpty();
88        $authority->isDefinitelyAllowed( 'import', $statusImport );
89        $statusImportUpload = PermissionStatus::newEmpty();
90        $authority->isDefinitelyAllowed( 'importupload', $statusImportUpload );
91        // Only show an error here if the user can't import using either method.
92        // If they can use at least one of the methods, allow access, and checks elsewhere
93        // will ensure that we only show the form(s) they can use.
94        $out = $this->getOutput();
95        if ( !$statusImport->isGood() && !$statusImportUpload->isGood() ) {
96            // Show separate messages for each check. There isn't a good way to merge them into a single
97            // message if the checks failed for different reasons.
98            $out->prepareErrorPage();
99            $out->setPageTitleMsg( $this->msg( 'permissionserrors' ) );
100            $out->addWikiTextAsInterface(
101                $out->formatPermissionStatus( $statusImport, 'import' )
102            );
103            $out->addWikiTextAsInterface(
104                $out->formatPermissionStatus( $statusImportUpload, 'importupload' )
105            );
106            return;
107        }
108
109        $out->addModules( 'mediawiki.misc-authed-ooui' );
110        $out->addModuleStyles( 'mediawiki.special.import.styles.ooui' );
111
112        $this->checkReadOnly();
113
114        $request = $this->getRequest();
115        $this->setStashKey( 'import:' . $this->getUser()->getName() );
116        $returningFromReauth = $this->retrieveStashedData();
117
118        if ( $returningFromReauth
119            && $request->getVal( 'source' ) === 'upload'
120            && !$request->getUpload( 'xmlimport' )->exists()
121        ) {
122            $this->getOutput()->addHTML( Html::warningBox(
123                $this->msg( 'import-upload-reauth-reupload' )->parse()
124            ) );
125            $this->showForm();
126            return;
127        }
128
129        if ( ( $request->wasPosted() && $request->getRawVal( 'action' ) === 'submit' )
130            || $returningFromReauth
131        ) {
132            $this->doImport();
133        }
134        $this->showForm();
135    }
136
137    /**
138     * Do the actual import
139     */
140    private function doImport() {
141        $isUpload = false;
142        $request = $this->getRequest();
143        $sourceName = $request->getVal( 'source' );
144        $assignKnownUsers = $request->getCheck( 'assignKnownUsers' );
145
146        $logcomment = $request->getText( 'log-comment' );
147        $pageLinkDepth = $this->getConfig()->get( MainConfigNames::ExportMaxLinkDepth ) == 0
148            ? 0
149            : $request->getIntOrNull( 'pagelink-depth' );
150
151        $rootpage = '';
152        $frompage = '';
153        $history = false;
154        $includeTemplates = false;
155        $mapping = $request->getVal( 'mapping' );
156        $namespace = $this->getConfig()->get( MainConfigNames::ImportTargetNamespace );
157        if ( $mapping === 'namespace' ) {
158            $namespace = $request->getIntOrNull( 'namespace' );
159        } elseif ( $mapping === 'subpage' ) {
160            $rootpage = $request->getText( 'rootpage' );
161        }
162
163        $user = $this->getUser();
164        $authority = $this->getAuthority();
165        $status = PermissionStatus::newEmpty();
166
167        $fullInterwikiPrefix = null;
168        if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
169            $source = Status::newFatal( 'import-token-mismatch' );
170        } elseif ( $sourceName === 'upload' ) {
171            $isUpload = true;
172            $fullInterwikiPrefix = $request->getVal( 'usernamePrefix' );
173            if ( $authority->authorizeAction( 'importupload', $status ) ) {
174                $source = ImportStreamSource::newFromUpload( "xmlimport" );
175            } else {
176                throw new PermissionsError( 'importupload', $status );
177            }
178        } elseif ( $sourceName === 'interwiki' ) {
179            if ( !$authority->authorizeAction( 'import', $status ) ) {
180                throw new PermissionsError( 'import', $status );
181            }
182            $interwiki = $fullInterwikiPrefix = $request->getVal( 'interwiki' );
183            // does this interwiki have subprojects?
184            $hasSubprojects = array_key_exists( $interwiki, $this->importSources );
185            if ( !$hasSubprojects && !in_array( $interwiki, $this->importSources ) ) {
186                $source = Status::newFatal( "import-invalid-interwiki" );
187            } else {
188                $subproject = null;
189                if ( $hasSubprojects ) {
190                    $subproject = $request->getVal( 'subproject' );
191                    // Trim "project::" prefix added for JS
192                    if ( str_starts_with( $subproject, $interwiki . '::' ) ) {
193                        $subproject = substr( $subproject, strlen( $interwiki . '::' ) );
194                    }
195                    $fullInterwikiPrefix .= ':' . $subproject;
196                }
197                if ( $hasSubprojects &&
198                    !in_array( $subproject, $this->importSources[$interwiki] )
199                ) {
200                    $source = Status::newFatal( 'import-invalid-interwiki' );
201                } else {
202                    $history = $request->getCheck( 'interwikiHistory' );
203                    $frompage = $request->getText( 'frompage' );
204                    $includeTemplates = $request->getCheck( 'interwikiTemplates' );
205                    $source = ImportStreamSource::newFromInterwiki(
206                        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
207                        $fullInterwikiPrefix,
208                        $frompage,
209                        $history,
210                        $includeTemplates,
211                        $pageLinkDepth );
212                }
213            }
214        } else {
215            $source = Status::newFatal( "importunknownsource" );
216        }
217
218        if ( (string)$fullInterwikiPrefix === '' ) {
219            $source->fatal( 'importnoprefix' );
220        }
221
222        $out = $this->getOutput();
223        if ( !$source->isGood() ) {
224            $out->wrapWikiMsg(
225                Html::errorBox( '$1' ),
226                [
227                    'importfailed',
228                    $source->getWikiText( false, false, $this->getLanguage() ),
229                    count( $source->getMessages() )
230                ]
231            );
232        } else {
233            $importer = $this->wikiImporterFactory->getWikiImporter( $source->value, $this->getAuthority() );
234            if ( $namespace !== null ) {
235                $importer->setTargetNamespace( $namespace );
236            } elseif ( $rootpage !== null ) {
237                $statusRootPage = $importer->setTargetRootPage( $rootpage );
238                if ( !$statusRootPage->isGood() ) {
239                    $out->wrapWikiMsg(
240                        Html::errorBox( '$1' ),
241                        [
242                            'import-options-wrong',
243                            $statusRootPage->getWikiText( false, false, $this->getLanguage() ),
244                            count( $statusRootPage->getMessages() )
245                        ]
246                    );
247
248                    return;
249                }
250            }
251            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
252            $importer->setUsernamePrefix( $fullInterwikiPrefix, $assignKnownUsers );
253
254            // T432713#12228661 - temporarily add noratelimit for imports
255            $scopedNoRateLimit = $this->permissionManager->addTemporaryUserRights( $user, 'noratelimit' );
256
257            // detect if reauth is needed
258            $importer->setAuthorizeOnly( true );
259            $importer->doImport();
260
261            if ( $importer->getReauthOperation() !== null ) {
262                $queryParams = $this->stashDataOnPost();
263                $status = PermissionStatus::newEmpty();
264                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
265                $status->setReauthOperation( $importer->getReauthOperation() );
266                $this->doReauthRedirect( $status, $queryParams );
267                return;
268            }
269
270            // Re-open the source; the pre-pass consumed the stream
271            $source = $isUpload
272                ? ImportStreamSource::newFromUpload( 'xmlimport' )
273                : ImportStreamSource::newFromInterwiki(
274                    // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
275                    $fullInterwikiPrefix,
276                    $frompage,
277                    $history,
278                    $includeTemplates,
279                    $pageLinkDepth
280                );
281            if ( !$source->isGood() ) {
282                return;
283            }
284            $importer = $this->wikiImporterFactory->getWikiImporter( $source->value, $this->getAuthority() );
285
286            if ( $namespace !== null ) {
287                $importer->setTargetNamespace( $namespace );
288            } elseif ( $rootpage !== null ) {
289                $importer->setTargetRootPage( $rootpage );
290            }
291            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
292            $importer->setUsernamePrefix( $fullInterwikiPrefix, $assignKnownUsers );
293            $importer->setAuthorizeOnly( false );
294
295            $out->addWikiMsg( "importstart" );
296
297            $reporter = new ImportReporter(
298                $importer,
299                $isUpload,
300                $fullInterwikiPrefix,
301                $logcomment,
302                $this->getContext()
303            );
304            $exception = false;
305
306            $reporter->open();
307            try {
308                $importer->doImport();
309            } catch ( DBError | TimeoutException $e ) {
310                // Re-throw exceptions which are not safe to catch (T383933).
311                throw $e;
312            } catch ( Exception $e ) {
313                $exception = $e;
314            } finally {
315                $result = $reporter->close();
316            }
317
318            if ( $exception ) {
319                # No source or XML parse error
320                $out->wrapWikiMsg(
321                    Html::errorBox( '$1' ),
322                    [ 'importfailed', wfEscapeWikiText( $exception->getMessage() ), 1 ]
323                );
324            } elseif ( !$result->isGood() ) {
325                # Zero revisions
326                $out->wrapWikiMsg(
327                    Html::errorBox( '$1' ),
328                    [
329                        'importfailed',
330                        $result->getWikiText( false, false, $this->getLanguage() ),
331                        count( $result->getMessages() )
332                    ]
333                );
334            } else {
335                # Success!
336                $this->destroyStashedData();
337                $out->addWikiMsg( 'importsuccess' );
338            }
339        }
340    }
341
342    private function getMappingFormPart( string $sourceName ): array {
343        $defaultNamespace = $this->getConfig()->get( MainConfigNames::ImportTargetNamespace );
344        return [
345            'mapping' => [
346                'type' => 'radio',
347                'name' => 'mapping',
348                // IDs: mw-import-mapping-interwiki, mw-import-mapping-upload
349                'id' => "mw-import-mapping-$sourceName",
350                'options-messages' => [
351                    'import-mapping-default' => 'default',
352                    'import-mapping-namespace' => 'namespace',
353                    'import-mapping-subpage' => 'subpage'
354                ],
355                'default' => $defaultNamespace !== null ? 'namespace' : 'default'
356            ],
357            'namespace' => [
358                'type' => 'namespaceselect',
359                'name' => 'namespace',
360                // IDs: mw-import-namespace-interwiki, mw-import-namespace-upload
361                'id' => "mw-import-namespace-$sourceName",
362                'default' => $defaultNamespace ?: '',
363                'all' => null,
364                'disable-if' => [ '!==', 'mapping', 'namespace' ],
365            ],
366            'rootpage' => [
367                'type' => 'text',
368                'name' => 'rootpage',
369                // Should be "mw-import-...", but we keep the inaccurate ID for compat
370                // IDs: mw-interwiki-rootpage-interwiki, mw-interwiki-rootpage-upload
371                'id' => "mw-interwiki-rootpage-$sourceName",
372                'disable-if' => [ '!==', 'mapping', 'subpage' ],
373            ],
374        ];
375    }
376
377    private function showForm() {
378        $action = $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] );
379        $authority = $this->getAuthority();
380        $out = $this->getOutput();
381        $this->addHelpLink( 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Import', true );
382
383        $interwikiFormDescriptor = [];
384        $uploadFormDescriptor = [];
385
386        if ( $authority->isDefinitelyAllowed( 'importupload' ) ) {
387            $mappingSelection = $this->getMappingFormPart( 'upload' );
388            $uploadFormDescriptor += [
389                'intro' => [
390                    'type' => 'info',
391                    'raw' => true,
392                    'default' => $this->msg( 'importtext' )->parseAsBlock()
393                ],
394                'xmlimport' => [
395                    'type' => 'file',
396                    'name' => 'xmlimport',
397                    'accept' => [ 'application/xml', 'text/xml' ],
398                    'label-message' => 'import-upload-filename',
399                    'required' => true,
400                ],
401                'usernamePrefix' => [
402                    'type' => 'text',
403                    'name' => 'usernamePrefix',
404                    'label-message' => 'import-upload-username-prefix',
405                    'required' => true,
406                ],
407                'assignKnownUsers' => [
408                    'type' => 'check',
409                    'name' => 'assignKnownUsers',
410                    'label-message' => 'import-assign-known-users'
411                ],
412                'log-comment' => [
413                    'type' => 'text',
414                    'name' => 'log-comment',
415                    'label-message' => 'import-comment'
416                ],
417                'source' => [
418                    'type' => 'hidden',
419                    'name' => 'source',
420                    'default' => 'upload',
421                    'id' => '',
422                ],
423            ];
424
425            $uploadFormDescriptor += $mappingSelection;
426
427            $htmlForm = HTMLForm::factory( 'ooui', $uploadFormDescriptor, $this->getContext() );
428            $htmlForm->setAction( $action );
429            $htmlForm->setId( 'mw-import-upload-form' );
430            $htmlForm->setWrapperLegendMsg( 'import-upload' );
431            $htmlForm->setSubmitTextMsg( 'uploadbtn' );
432            $htmlForm->prepareForm()->displayForm( false );
433
434        } elseif ( !$this->importSources ) {
435            $out->addWikiMsg( 'importnosources' );
436        }
437
438        if ( $authority->isDefinitelyAllowed( 'import' ) && $this->importSources ) {
439
440            $projects = [];
441            $needSubprojectField = false;
442            foreach ( $this->importSources as $key => $value ) {
443                if ( is_int( $key ) ) {
444                    $key = $value;
445                } elseif ( $value !== $key ) {
446                    $needSubprojectField = true;
447                }
448
449                $projects[ $key ] = $key;
450            }
451
452            $interwikiFormDescriptor += [
453                'intro' => [
454                    'type' => 'info',
455                    'raw' => true,
456                    'default' => $this->msg( 'import-interwiki-text' )->parseAsBlock()
457                ],
458                'interwiki' => [
459                    'type' => 'select',
460                    'name' => 'interwiki',
461                    'label-message' => 'import-interwiki-sourcewiki',
462                    'options' => $projects
463                ],
464            ];
465
466            if ( $needSubprojectField ) {
467                $subprojects = [];
468                foreach ( $this->importSources as $key => $value ) {
469                    if ( is_array( $value ) ) {
470                        foreach ( $value as $subproject ) {
471                            $subprojects[ $key . ':' . $subproject ] = $key . '::' . $subproject;
472                        }
473                    }
474                }
475
476                $interwikiFormDescriptor += [
477                    'subproject' => [
478                        'type' => 'select',
479                        'name' => 'subproject',
480                        'options' => $subprojects
481                    ]
482                ];
483            }
484
485            $interwikiFormDescriptor += [
486                'frompage' => [
487                    'type' => 'text',
488                    'name' => 'frompage',
489                    'label-message' => 'import-interwiki-sourcepage'
490                ],
491                'interwikiHistory' => [
492                    'type' => 'check',
493                    'name' => 'interwikiHistory',
494                    'label-message' => 'import-interwiki-history'
495                ],
496                'interwikiTemplates' => [
497                    'type' => 'check',
498                    'name' => 'interwikiTemplates',
499                    'label-message' => 'import-interwiki-templates'
500                ],
501                'assignKnownUsers' => [
502                    'type' => 'check',
503                    'name' => 'assignKnownUsers',
504                    'label-message' => 'import-assign-known-users'
505                ],
506            ];
507
508            if ( $this->getConfig()->get( MainConfigNames::ExportMaxLinkDepth ) > 0 ) {
509                $interwikiFormDescriptor += [
510                    'pagelink-depth' => [
511                        'type' => 'int',
512                        'name' => 'pagelink-depth',
513                        'label-message' => 'export-pagelinks',
514                        'default' => 0
515                    ]
516                ];
517            }
518
519            $interwikiFormDescriptor += [
520                'log-comment' => [
521                    'type' => 'text',
522                    'name' => 'log-comment',
523                    'label-message' => 'import-comment'
524                ],
525                'source' => [
526                    'type' => 'hidden',
527                    'name' => 'source',
528                    'default' => 'interwiki',
529                    'id' => '',
530                ],
531            ];
532            $mappingSelection = $this->getMappingFormPart( 'interwiki' );
533
534            $interwikiFormDescriptor += $mappingSelection;
535
536            $htmlForm = HTMLForm::factory( 'ooui', $interwikiFormDescriptor, $this->getContext() );
537            $htmlForm->setAction( $action );
538            $htmlForm->setId( 'mw-import-interwiki-form' );
539            $htmlForm->setWrapperLegendMsg( 'importinterwiki' );
540            $htmlForm->setSubmitTextMsg( 'import-interwiki-submit' );
541            $htmlForm->prepareForm()->displayForm( false );
542        }
543    }
544
545    /** @inheritDoc */
546    protected function getGroupName() {
547        return 'pagetools';
548    }
549}
550
551// @codeCoverageIgnoreStart
552/** @deprecated class alias since 1.41 */
553class_alias( SpecialImport::class, 'SpecialImport' );
554// @codeCoverageIgnoreEnd