MediaWiki master
SpecialImport.php
Go to the documentation of this file.
1<?php
27namespace MediaWiki\Specials;
28
29use Exception;
39use UnexpectedValueException;
41
49 private $importSources;
50
51 private PermissionManager $permManager;
52 private WikiImporterFactory $wikiImporterFactory;
53
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
78 public function execute( $par ) {
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
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' ) ) {
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' );
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
483class_alias( SpecialImport::class, 'SpecialImport' );
wfMergeErrorArrays(... $args)
Merge arrays in the style of PermissionManager::getPermissionErrors, with duplicate removal e....
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:206
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 service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
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.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
MediaWiki page data importer.
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.
__construct(PermissionManager $permManager, WikiImporterFactory $wikiImporterFactory)
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.
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
Factory service for WikiImporter instances.
$source
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...