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