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