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