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