MediaWiki master
SpecialImport.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Specials;
25
26use Exception;
36use UnexpectedValueException;
38
46 private $importSources;
47
48 private WikiImporterFactory $wikiImporterFactory;
49
53 public function __construct(
54 WikiImporterFactory $wikiImporterFactory
55 ) {
56 parent::__construct( 'Import', 'import' );
57
58 $this->wikiImporterFactory = $wikiImporterFactory;
59 }
60
61 public function doesWrites() {
62 return true;
63 }
64
69 public function execute( $par ) {
71
72 $this->setHeaders();
73 $this->outputHeader();
74
75 $this->importSources = $this->getConfig()->get( MainConfigNames::ImportSources );
76 // Avoid phan error by checking the type
77 if ( !is_array( $this->importSources ) ) {
78 throw new UnexpectedValueException( '$wgImportSources must be an array' );
79 }
80 $this->getHookRunner()->onImportSources( $this->importSources );
81
82 $authority = $this->getAuthority();
83 $statusImport = PermissionStatus::newEmpty();
84 $authority->isDefinitelyAllowed( 'import', $statusImport );
85 $statusImportUpload = PermissionStatus::newEmpty();
86 $authority->isDefinitelyAllowed( 'importupload', $statusImportUpload );
87 // Only show an error here if the user can't import using either method.
88 // If they can use at least one of the methods, allow access, and checks elsewhere
89 // will ensure that we only show the form(s) they can use.
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
94 $this->getOutput()->prepareErrorPage();
95 $this->getOutput()->setPageTitleMsg( $this->msg( 'permissionserrors' ) );
96 $this->getOutput()->addWikiTextAsInterface( Html::errorBox(
97 $this->getOutput()->formatPermissionStatus( $statusImport, 'import' )
98 ) );
99 $this->getOutput()->addWikiTextAsInterface( Html::errorBox(
100 $this->getOutput()->formatPermissionStatus( $statusImportUpload, 'importupload' )
101 ) );
102 return;
103 }
104
105 $this->getOutput()->addModules( 'mediawiki.misc-authed-ooui' );
106 $this->getOutput()->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
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 ) ) {
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' );
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 ( Exception $e ) {
247 $exception = $e;
248 }
249 $result = $reporter->close();
250
251 if ( $exception ) {
252 # No source or XML parse error
253 $out->wrapWikiMsg(
254 Html::errorBox( '$1' ),
255 [ 'importfailed', wfEscapeWikiText( $exception->getMessage() ), 1 ]
256 );
257 } elseif ( !$result->isGood() ) {
258 # Zero revisions
259 $out->wrapWikiMsg(
260 Html::errorBox( '$1' ),
261 [
262 'importfailed',
263 $result->getWikiText( false, false, $this->getLanguage() ),
264 count( $result->getMessages() )
265 ]
266 );
267 } else {
268 # Success!
269 $out->addWikiMsg( 'importsuccess' );
270 }
271 $out->addHTML( '<hr />' );
272 }
273 }
274
275 private function getMappingFormPart( $sourceName ) {
276 $defaultNamespace = $this->getConfig()->get( MainConfigNames::ImportTargetNamespace );
277 return [
278 'mapping' => [
279 'type' => 'radio',
280 'name' => 'mapping',
281 // IDs: mw-import-mapping-interwiki, mw-import-mapping-upload
282 'id' => "mw-import-mapping-$sourceName",
283 'options-messages' => [
284 'import-mapping-default' => 'default',
285 'import-mapping-namespace' => 'namespace',
286 'import-mapping-subpage' => 'subpage'
287 ],
288 'default' => $defaultNamespace !== null ? 'namespace' : 'default'
289 ],
290 'namespace' => [
291 'type' => 'namespaceselect',
292 'name' => 'namespace',
293 // IDs: mw-import-namespace-interwiki, mw-import-namespace-upload
294 'id' => "mw-import-namespace-$sourceName",
295 'default' => $defaultNamespace ?: '',
296 'all' => null,
297 'disable-if' => [ '!==', 'mapping', 'namespace' ],
298 ],
299 'rootpage' => [
300 'type' => 'text',
301 'name' => 'rootpage',
302 // Should be "mw-import-...", but we keep the inaccurate ID for compat
303 // IDs: mw-interwiki-rootpage-interwiki, mw-interwiki-rootpage-upload
304 'id' => "mw-interwiki-rootpage-$sourceName",
305 'disable-if' => [ '!==', 'mapping', 'subpage' ],
306 ],
307 ];
308 }
309
310 private function showForm() {
311 $action = $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] );
312 $authority = $this->getAuthority();
313 $out = $this->getOutput();
314 $this->addHelpLink( 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Import', true );
315
316 $interwikiFormDescriptor = [];
317 $uploadFormDescriptor = [];
318
319 if ( $authority->isDefinitelyAllowed( 'importupload' ) ) {
320 $mappingSelection = $this->getMappingFormPart( 'upload' );
321 $uploadFormDescriptor += [
322 'intro' => [
323 'type' => 'info',
324 'raw' => true,
325 'default' => $this->msg( 'importtext' )->parseAsBlock()
326 ],
327 'xmlimport' => [
328 'type' => 'file',
329 'name' => 'xmlimport',
330 'accept' => [ 'application/xml', 'text/xml' ],
331 'label-message' => 'import-upload-filename',
332 'required' => true,
333 ],
334 'usernamePrefix' => [
335 'type' => 'text',
336 'name' => 'usernamePrefix',
337 'label-message' => 'import-upload-username-prefix',
338 'required' => true,
339 ],
340 'assignKnownUsers' => [
341 'type' => 'check',
342 'name' => 'assignKnownUsers',
343 'label-message' => 'import-assign-known-users'
344 ],
345 'log-comment' => [
346 'type' => 'text',
347 'name' => 'log-comment',
348 'label-message' => 'import-comment'
349 ],
350 'source' => [
351 'type' => 'hidden',
352 'name' => 'source',
353 'default' => 'upload',
354 'id' => '',
355 ],
356 ];
357
358 $uploadFormDescriptor += $mappingSelection;
359
360 $htmlForm = HTMLForm::factory( 'ooui', $uploadFormDescriptor, $this->getContext() );
361 $htmlForm->setAction( $action );
362 $htmlForm->setId( 'mw-import-upload-form' );
363 $htmlForm->setWrapperLegendMsg( 'import-upload' );
364 $htmlForm->setSubmitTextMsg( 'uploadbtn' );
365 $htmlForm->prepareForm()->displayForm( false );
366
367 } elseif ( !$this->importSources ) {
368 $out->addWikiMsg( 'importnosources' );
369 }
370
371 if ( $authority->isDefinitelyAllowed( 'import' ) && $this->importSources ) {
372
373 $projects = [];
374 $needSubprojectField = false;
375 foreach ( $this->importSources as $key => $value ) {
376 if ( is_int( $key ) ) {
377 $key = $value;
378 } elseif ( $value !== $key ) {
379 $needSubprojectField = true;
380 }
381
382 $projects[ $key ] = $key;
383 }
384
385 $interwikiFormDescriptor += [
386 'intro' => [
387 'type' => 'info',
388 'raw' => true,
389 'default' => $this->msg( 'import-interwiki-text' )->parseAsBlock()
390 ],
391 'interwiki' => [
392 'type' => 'select',
393 'name' => 'interwiki',
394 'label-message' => 'import-interwiki-sourcewiki',
395 'options' => $projects
396 ],
397 ];
398
399 if ( $needSubprojectField ) {
400 $subprojects = [];
401 foreach ( $this->importSources as $key => $value ) {
402 if ( is_array( $value ) ) {
403 foreach ( $value as $subproject ) {
404 $subprojects[ $subproject ] = $key . '::' . $subproject;
405 }
406 }
407 }
408
409 $interwikiFormDescriptor += [
410 'subproject' => [
411 'type' => 'select',
412 'name' => 'subproject',
413 'options' => $subprojects
414 ]
415 ];
416 }
417
418 $interwikiFormDescriptor += [
419 'frompage' => [
420 'type' => 'text',
421 'name' => 'frompage',
422 'label-message' => 'import-interwiki-sourcepage'
423 ],
424 'interwikiHistory' => [
425 'type' => 'check',
426 'name' => 'interwikiHistory',
427 'label-message' => 'import-interwiki-history'
428 ],
429 'interwikiTemplates' => [
430 'type' => 'check',
431 'name' => 'interwikiTemplates',
432 'label-message' => 'import-interwiki-templates'
433 ],
434 'assignKnownUsers' => [
435 'type' => 'check',
436 'name' => 'assignKnownUsers',
437 'label-message' => 'import-assign-known-users'
438 ],
439 ];
440
441 if ( $this->getConfig()->get( MainConfigNames::ExportMaxLinkDepth ) > 0 ) {
442 $interwikiFormDescriptor += [
443 'pagelink-depth' => [
444 'type' => 'int',
445 'name' => 'pagelink-depth',
446 'label-message' => 'export-pagelinks',
447 'default' => 0
448 ]
449 ];
450 }
451
452 $interwikiFormDescriptor += [
453 'log-comment' => [
454 'type' => 'text',
455 'name' => 'log-comment',
456 'label-message' => 'import-comment'
457 ],
458 'source' => [
459 'type' => 'hidden',
460 'name' => 'source',
461 'default' => 'interwiki',
462 'id' => '',
463 ],
464 ];
465 $mappingSelection = $this->getMappingFormPart( 'interwiki' );
466
467 $interwikiFormDescriptor += $mappingSelection;
468
469 $htmlForm = HTMLForm::factory( 'ooui', $interwikiFormDescriptor, $this->getContext() );
470 $htmlForm->setAction( $action );
471 $htmlForm->setId( 'mw-import-interwiki-form' );
472 $htmlForm->setWrapperLegendMsg( 'importinterwiki' );
473 $htmlForm->setSubmitTextMsg( 'import-interwiki-submit' );
474 $htmlForm->prepareForm()->displayForm( false );
475 }
476 }
477
478 protected function getGroupName() {
479 return 'pagetools';
480 }
481}
482
484class_alias( SpecialImport::class, 'SpecialImport' );
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Reporting callback.
Imports a XML dump from a file (either from file upload, files on disk, or HTTP)
static newFromInterwiki( $interwiki, $page, $history=false, $templates=false, $pageLinkDepth=0)
static newFromUpload( $fieldname="xmlimport")
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:208
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
A class containing constants representing the names of configuration variables.
const ExportMaxLinkDepth
Name constant for the ExportMaxLinkDepth setting, for use with Config::get()
const ImportSources
Name constant for the ImportSources setting, for use with Config::get()
const ImportTargetNamespace
Name constant for the ImportTargetNamespace setting, for use with Config::get()
A StatusValue for permission errors.
Parent class for all special pages.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getUser()
Shortcut to get the User executing this instance.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
getPageTitle( $subpage=false)
Get a self-referential title object.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages By default the message key is the canonical name of...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
MediaWiki page data importer.
__construct(WikiImporterFactory $wikiImporterFactory)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
doesWrites()
Indicates whether this special page may perform database writes.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Show an error when a user tries to do something they do not have the necessary permissions for.
Factory service for WikiImporter instances.
$source