MediaWiki master
SpecialImport.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Specials;
25
26use Exception;
36use UnexpectedValueException;
39use Wikimedia\RequestTimeout\TimeoutException;
40
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
68 public function execute( $par ) {
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
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 ( 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
488class_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")
Show an error when a user tries to do something they do not have the necessary permissions for.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:209
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
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.
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...
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 POST requests to this special page require write access to the wiki.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Factory service for WikiImporter instances.
Database error base class.
Definition DBError.php:36
$source