MediaWiki  master
SpecialImport.php
Go to the documentation of this file.
1 <?php
29 
35 class SpecialImport extends SpecialPage {
37  private $importSources;
38 
40  private $permManager;
41 
43  private $wikiImporterFactory;
44 
49  public function __construct(
50  PermissionManager $permManager,
51  WikiImporterFactory $wikiImporterFactory
52  ) {
53  parent::__construct( 'Import', 'import' );
54 
55  $this->permManager = $permManager;
56  $this->wikiImporterFactory = $wikiImporterFactory;
57  }
58 
59  public function doesWrites() {
60  return true;
61  }
62 
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  $user = $this->getUser();
83  if ( !$this->permManager->userHasAnyRight( $user, 'import', 'importupload' ) ) {
84  throw new PermissionsError( 'import' );
85  }
86 
87  # @todo Allow PermissionManager::getPermissionErrors() to take an array
88  $errors = wfMergeErrorArrays(
89  $this->permManager->getPermissionErrors(
90  'import', $user, $this->getPageTitle(),
91  PermissionManager::RIGOR_FULL,
92  [ 'ns-specialprotected', 'badaccess-group0', 'badaccess-groups' ]
93  ),
94  $this->permManager->getPermissionErrors(
95  'importupload', $user, $this->getPageTitle(),
96  PermissionManager::RIGOR_FULL,
97  [ 'ns-specialprotected', 'badaccess-group0', 'badaccess-groups' ]
98  )
99  );
100 
101  if ( $errors ) {
102  throw new PermissionsError( 'import', $errors );
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 
142  $fullInterwikiPrefix = null;
143  if ( !$user->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
144  $source = Status::newFatal( 'import-token-mismatch' );
145  } elseif ( $sourceName === 'upload' ) {
146  $isUpload = true;
147  $fullInterwikiPrefix = $request->getVal( 'usernamePrefix' );
148  if ( $this->permManager->userHasRight( $user, 'importupload' ) ) {
150  } else {
151  throw new PermissionsError( 'importupload' );
152  }
153  } elseif ( $sourceName === 'interwiki' ) {
154  if ( !$this->permManager->userHasRight( $user, 'import' ) ) {
155  throw new PermissionsError( 'import' );
156  }
157  $interwiki = $fullInterwikiPrefix = $request->getVal( 'interwiki' );
158  // does this interwiki have subprojects?
159  $hasSubprojects = array_key_exists( $interwiki, $this->importSources );
160  if ( !$hasSubprojects && !in_array( $interwiki, $this->importSources ) ) {
161  $source = Status::newFatal( "import-invalid-interwiki" );
162  } else {
163  $subproject = null;
164  if ( $hasSubprojects ) {
165  $subproject = $request->getVal( 'subproject' );
166  // Trim "project::" prefix added for JS
167  if ( strpos( $subproject, $interwiki . '::' ) === 0 ) {
168  $subproject = substr( $subproject, strlen( $interwiki . '::' ) );
169  }
170  $fullInterwikiPrefix .= ':' . $subproject;
171  }
172  if ( $hasSubprojects &&
173  !in_array( $subproject, $this->importSources[$interwiki] )
174  ) {
175  $source = Status::newFatal( 'import-invalid-interwiki' );
176  } else {
177  $history = $request->getCheck( 'interwikiHistory' );
178  $frompage = $request->getText( 'frompage' );
179  $includeTemplates = $request->getCheck( 'interwikiTemplates' );
181  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
182  $fullInterwikiPrefix,
183  $frompage,
184  $history,
185  $includeTemplates,
186  $pageLinkDepth );
187  }
188  }
189  } else {
190  $source = Status::newFatal( "importunknownsource" );
191  }
192 
193  if ( (string)$fullInterwikiPrefix === '' ) {
194  $source->fatal( 'importnoprefix' );
195  }
196 
197  $out = $this->getOutput();
198  if ( !$source->isGood() ) {
199  $out->wrapWikiTextAsInterface( 'error',
200  $this->msg( 'importfailed', $source->getWikiText( false, false, $this->getLanguage() ) )
201  ->plain()
202  );
203  } else {
204  $importer = $this->wikiImporterFactory->getWikiImporter( $source->value );
205  if ( $namespace !== null ) {
206  $importer->setTargetNamespace( $namespace );
207  } elseif ( $rootpage !== null ) {
208  $statusRootPage = $importer->setTargetRootPage( $rootpage );
209  if ( !$statusRootPage->isGood() ) {
210  $out->wrapWikiMsg(
211  "<div class=\"error\">\n$1\n</div>",
212  [
213  'import-options-wrong',
214  $statusRootPage->getWikiText( false, false, $this->getLanguage() ),
215  count( $statusRootPage->getErrorsArray() )
216  ]
217  );
218 
219  return;
220  }
221  }
222  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
223  $importer->setUsernamePrefix( $fullInterwikiPrefix, $assignKnownUsers );
224 
225  $out->addWikiMsg( "importstart" );
226 
227  $reporter = new ImportReporter(
228  $importer,
229  $isUpload,
230  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
231  $fullInterwikiPrefix,
232  $logcomment
233  );
234  $reporter->setContext( $this->getContext() );
235  $exception = false;
236 
237  $reporter->open();
238  try {
239  $importer->doImport();
240  } catch ( Exception $e ) {
241  $exception = $e;
242  }
243  $result = $reporter->close();
244 
245  if ( $exception ) {
246  # No source or XML parse error
247  $out->wrapWikiMsg(
248  "<div class=\"error\">\n$1\n</div>",
249  [ 'importfailed', $exception->getMessage() ]
250  );
251  } elseif ( !$result->isGood() ) {
252  # Zero revisions
253  $out->wrapWikiMsg(
254  "<div class=\"error\">\n$1\n</div>",
255  [ 'importfailed', $result->getWikiText( false, false, $this->getLanguage() ) ]
256  );
257  } else {
258  # Success!
259  $out->addWikiMsg( 'importsuccess' );
260  }
261  $out->addHTML( '<hr />' );
262  }
263  }
264 
265  private function getMappingFormPart( $sourceName ) {
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  $user = $this->getUser();
303  $out = $this->getOutput();
304  $this->addHelpLink( 'https://meta.wikimedia.org/wiki/Special:MyLanguage/Help:Import', true );
305 
306  $interwikiFormDescriptor = [];
307  $uploadFormDescriptor = [];
308 
309  if ( $this->permManager->userHasRight( $user, '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  // TODO: Is this field required?
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 ( empty( $this->importSources ) ) {
358  $out->addWikiMsg( 'importnosources' );
359  }
360 
361  if ( $this->permManager->userHasRight( $user, 'import' ) && !empty( $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 
468  protected function getGroupName() {
469  return 'pagetools';
470  }
471 }
wfMergeErrorArrays(... $args)
Merge arrays in the style of PermissionManager::getPermissionErrors, with duplicate removal e....
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:349
Reporting callback.
static newFromInterwiki( $interwiki, $page, $history=false, $templates=false, $pageLinkDepth=0)
static newFromUpload( $fieldname="xmlimport")
A class containing constants representing the names of configuration variables.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Show an error when a user tries to do something they do not have the necessary permissions for.
MediaWiki page data importer.
doesWrites()
Indicates whether this special page may perform database writes.
execute( $par)
Execute.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
__construct(PermissionManager $permManager, WikiImporterFactory $wikiImporterFactory)
Parent class for all special pages.
Definition: SpecialPage.php:44
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getPageTitle( $subpage=false)
Get a self-referential title object.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
Factory service for WikiImporter instances.
$source