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