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