MediaWiki REL1_34
SpecialInterwiki.php
Go to the documentation of this file.
1<?php
2
4
13 public function __construct() {
14 parent::__construct( 'Interwiki' );
15 }
16
17 public function doesWrites() {
18 return true;
19 }
20
27 public function getDescription() {
28 return $this->msg( $this->canModify() ?
29 'interwiki' : 'interwiki-title-norights' )->plain();
30 }
31
32 public function getSubpagesForPrefixSearch() {
33 // delete, edit both require the prefix parameter.
34 return [ 'add' ];
35 }
36
42 public function execute( $par ) {
43 $this->setHeaders();
44 $this->outputHeader();
45
46 $out = $this->getOutput();
47 $request = $this->getRequest();
48
49 $out->addModuleStyles( 'ext.interwiki.specialpage' );
50
51 $action = $par ?: $request->getVal( 'action', $par );
52
53 if ( !in_array( $action, [ 'add', 'edit', 'delete' ] ) || !$this->canModify( $out ) ) {
54 $this->showList();
55 } else {
56 $this->showForm( $action );
57 }
58 }
59
66 public function canModify( $out = false ) {
67 global $wgInterwikiCache;
68 if ( !$this->getUser()->isAllowed( 'interwiki' ) ) {
69 // Check permissions
70 if ( $out ) {
71 throw new PermissionsError( 'interwiki' );
72 }
73
74 return false;
75 } elseif ( $wgInterwikiCache ) {
76 // Editing the interwiki cache is not supported
77 if ( $out ) {
78 $out->addWikiMsg( 'interwiki-cached' );
79 }
80
81 return false;
82 } elseif ( wfReadOnly() ) {
83 throw new ReadOnlyError;
84 }
85
86 return true;
87 }
88
92 protected function showForm( $action ) {
93 $formDescriptor = [];
94 $hiddenFields = [
95 'action' => $action,
96 ];
97
98 $status = Status::newGood();
99 $request = $this->getRequest();
100 $prefix = $request->getVal( 'prefix', $request->getVal( 'hiddenPrefix' ) );
101
102 switch ( $action ) {
103 case 'add':
104 case 'edit':
105 $formDescriptor = [
106 'prefix' => [
107 'type' => 'text',
108 'label-message' => 'interwiki-prefix-label',
109 'name' => 'prefix',
110 ],
111
112 'local' => [
113 'type' => 'check',
114 'id' => 'mw-interwiki-local',
115 'label-message' => 'interwiki-local-label',
116 'name' => 'local',
117 ],
118
119 'trans' => [
120 'type' => 'check',
121 'id' => 'mw-interwiki-trans',
122 'label-message' => 'interwiki-trans-label',
123 'name' => 'trans',
124 ],
125
126 'url' => [
127 'type' => 'url',
128 'id' => 'mw-interwiki-url',
129 'label-message' => 'interwiki-url-label',
130 'maxlength' => 200,
131 'name' => 'wpInterwikiURL',
132 'size' => 60,
133 'tabindex' => 1,
134 ],
135
136 'reason' => [
137 'type' => 'text',
138 'id' => "mw-interwiki-{$action}reason",
139 'label-message' => 'interwiki_reasonfield',
140 'maxlength' => 200,
141 'name' => 'wpInterwikiReason',
142 'size' => 60,
143 'tabindex' => 1,
144 ],
145 ];
146
147 break;
148 case 'delete':
149 $formDescriptor = [
150 'prefix' => [
151 'type' => 'hidden',
152 'name' => 'prefix',
153 'default' => $prefix,
154 ],
155
156 'reason' => [
157 'type' => 'text',
158 'name' => 'reason',
159 'label-message' => 'interwiki_reasonfield',
160 ],
161 ];
162
163 break;
164 }
165
166 $formDescriptor['hiddenPrefix'] = [
167 'type' => 'hidden',
168 'name' => 'hiddenPrefix',
169 'default' => $prefix,
170 ];
171
172 if ( $action === 'edit' ) {
174 $row = $dbr->selectRow( 'interwiki', '*', [ 'iw_prefix' => $prefix ], __METHOD__ );
175
176 $formDescriptor['prefix']['disabled'] = true;
177 $formDescriptor['prefix']['default'] = $prefix;
178 $hiddenFields['prefix'] = $prefix;
179
180 if ( !$row ) {
181 $status->fatal( 'interwiki_editerror', $prefix );
182 } else {
183 $formDescriptor['url']['default'] = $row->iw_url;
184 $formDescriptor['url']['trans'] = $row->iw_trans;
185 $formDescriptor['url']['local'] = $row->iw_local;
186 }
187 }
188
189 if ( !$status->isOK() ) {
190 $formDescriptor = [];
191 }
192
193 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
194 $htmlForm
195 ->addHiddenFields( $hiddenFields )
196 ->setSubmitCallback( [ $this, 'onSubmit' ] );
197
198 if ( $status->isOK() ) {
199 if ( $action === 'delete' ) {
200 $htmlForm->setSubmitDestructive();
201 }
202
203 $htmlForm->setSubmitTextMsg( $action !== 'add' ? $action : 'interwiki_addbutton' )
204 ->setIntro( $this->msg( $action !== 'delete' ? "interwiki_{$action}intro" :
205 'interwiki_deleting', $prefix ) )
206 ->show();
207 } else {
208 $htmlForm->suppressDefaultSubmit()
209 ->prepareForm()
210 ->displayForm( $status );
211 }
212
213 $this->getOutput()->addBacklinkSubtitle( $this->getPageTitle() );
214 }
215
216 public function onSubmit( array $data ) {
217 global $wgInterwikiCentralInterlanguageDB;
218
219 $status = Status::newGood();
220 $request = $this->getRequest();
221 $prefix = $this->getRequest()->getVal( 'prefix', '' );
222 $do = $request->getVal( 'action' );
223 // Show an error if the prefix is invalid (only when adding one).
224 // Invalid characters for a title should also be invalid for a prefix.
225 // Whitespace, ':', '&' and '=' are invalid, too.
226 // (Bug 30599).
227 global $wgLegalTitleChars;
228 $validPrefixChars = preg_replace( '/[ :&=]/', '', $wgLegalTitleChars );
229 if ( $do === 'add' && preg_match( "/\s|[^$validPrefixChars]/", $prefix ) ) {
230 $status->fatal( 'interwiki-badprefix', htmlspecialchars( $prefix ) );
231 return $status;
232 }
233 // Disallow adding local interlanguage definitions if using global
234 if (
235 $do === 'add' && Language::fetchLanguageName( $prefix )
236 && $wgInterwikiCentralInterlanguageDB !== wfWikiID()
237 && $wgInterwikiCentralInterlanguageDB !== null
238 ) {
239 $status->fatal( 'interwiki-cannotaddlocallanguage', htmlspecialchars( $prefix ) );
240 return $status;
241 }
242 $reason = $data['reason'];
243 $selfTitle = $this->getPageTitle();
244 $lookup = MediaWikiServices::getInstance()->getInterwikiLookup();
245 $dbw = wfGetDB( DB_MASTER );
246 switch ( $do ) {
247 case 'delete':
248 $dbw->delete( 'interwiki', [ 'iw_prefix' => $prefix ], __METHOD__ );
249
250 if ( $dbw->affectedRows() === 0 ) {
251 $status->fatal( 'interwiki_delfailed', $prefix );
252 } else {
253 $this->getOutput()->addWikiMsg( 'interwiki_deleted', $prefix );
254 $log = new LogPage( 'interwiki' );
255 $log->addEntry( 'iw_delete', $selfTitle, $reason, [ $prefix ] );
256 $lookup->invalidateCache( $prefix );
257 }
258 break;
260 case 'add':
261 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
262 $prefix = $contLang->lc( $prefix );
263 case 'edit':
264 $theurl = $data['url'];
265 $local = $data['local'] ? 1 : 0;
266 $trans = $data['trans'] ? 1 : 0;
267 $rows = [
268 'iw_prefix' => $prefix,
269 'iw_url' => $theurl,
270 'iw_local' => $local,
271 'iw_trans' => $trans
272 ];
273
274 if ( $prefix === '' || $theurl === '' ) {
275 $status->fatal( 'interwiki-submit-empty' );
276 break;
277 }
278
279 // Simple URL validation: check that the protocol is one of
280 // the supported protocols for this wiki.
281 // (bug 30600)
282 if ( !wfParseUrl( $theurl ) ) {
283 $status->fatal( 'interwiki-submit-invalidurl' );
284 break;
285 }
286
287 if ( $do === 'add' ) {
288 $dbw->insert( 'interwiki', $rows, __METHOD__, [ 'IGNORE' ] );
289 } else { // $do === 'edit'
290 $dbw->update( 'interwiki', $rows, [ 'iw_prefix' => $prefix ], __METHOD__, [ 'IGNORE' ] );
291 }
292
293 // used here: interwiki_addfailed, interwiki_added, interwiki_edited
294 if ( $dbw->affectedRows() === 0 ) {
295 $status->fatal( "interwiki_{$do}failed", $prefix );
296 } else {
297 $this->getOutput()->addWikiMsg( "interwiki_{$do}ed", $prefix );
298 $log = new LogPage( 'interwiki' );
299 $log->addEntry( 'iw_' . $do, $selfTitle, $reason, [ $prefix, $theurl, $trans, $local ] );
300 $lookup->invalidateCache( $prefix );
301 }
302 break;
303 }
304
305 return $status;
306 }
307
308 protected function showList() {
309 global $wgInterwikiCentralDB, $wgInterwikiCentralInterlanguageDB, $wgInterwikiViewOnly;
310
311 $canModify = $this->canModify();
312
313 // Build lists
314 $lookup = MediaWikiServices::getInstance()->getInterwikiLookup();
315 $iwPrefixes = $lookup->getAllPrefixes( null );
316 $iwGlobalPrefixes = [];
317 $iwGlobalLanguagePrefixes = [];
318 if ( $wgInterwikiCentralDB !== null && $wgInterwikiCentralDB !== wfWikiID() ) {
319 // Fetch list from global table
320 $dbrCentralDB = wfGetDB( DB_REPLICA, [], $wgInterwikiCentralDB );
321 $res = $dbrCentralDB->select( 'interwiki', '*', false, __METHOD__ );
322 $retval = [];
323 foreach ( $res as $row ) {
324 $row = (array)$row;
325 if ( !Language::fetchLanguageName( $row['iw_prefix'] ) ) {
326 $retval[] = $row;
327 }
328 }
329 $iwGlobalPrefixes = $retval;
330 }
331
332 // Almost the same loop as above, but for global inter*language* links, whereas the above is for
333 // global inter*wiki* links
334 $usingGlobalInterlangLinks = ( $wgInterwikiCentralInterlanguageDB !== null );
335 $isGlobalInterlanguageDB = ( $wgInterwikiCentralInterlanguageDB === wfWikiID() );
336 $usingGlobalLanguages = $usingGlobalInterlangLinks && !$isGlobalInterlanguageDB;
337 if ( $usingGlobalLanguages ) {
338 // Fetch list from global table
339 $dbrCentralLangDB = wfGetDB( DB_REPLICA, [], $wgInterwikiCentralInterlanguageDB );
340 $res = $dbrCentralLangDB->select( 'interwiki', '*', false, __METHOD__ );
341 $retval2 = [];
342 foreach ( $res as $row ) {
343 $row = (array)$row;
344 // Note that the above DB query explicitly *excludes* interlang ones
345 // (which makes sense), whereas here we _only_ care about interlang ones!
346 if ( Language::fetchLanguageName( $row['iw_prefix'] ) ) {
347 $retval2[] = $row;
348 }
349 }
350 $iwGlobalLanguagePrefixes = $retval2;
351 }
352
353 // Split out language links
354 $iwLocalPrefixes = [];
355 $iwLanguagePrefixes = [];
356 foreach ( $iwPrefixes as $iwPrefix ) {
357 if ( Language::fetchLanguageName( $iwPrefix['iw_prefix'] ) ) {
358 $iwLanguagePrefixes[] = $iwPrefix;
359 } else {
360 $iwLocalPrefixes[] = $iwPrefix;
361 }
362 }
363
364 // If using global interlanguage links, just ditch the data coming from the
365 // local table and overwrite it with the global data
366 if ( $usingGlobalInterlangLinks ) {
367 unset( $iwLanguagePrefixes );
368 $iwLanguagePrefixes = $iwGlobalLanguagePrefixes;
369 }
370
371 // Page intro content
372 $this->getOutput()->addWikiMsg( 'interwiki_intro' );
373
374 // Add 'view log' link when possible
375 if ( $wgInterwikiViewOnly === false ) {
376 $logLink = $this->getLinkRenderer()->makeLink(
377 SpecialPage::getTitleFor( 'Log', 'interwiki' ),
378 $this->msg( 'interwiki-logtext' )->text()
379 );
380 $this->getOutput()->addHTML( '<p class="mw-interwiki-log">' . $logLink . '</p>' );
381 }
382
383 // Add 'add' link
384 if ( $canModify ) {
385 if ( count( $iwGlobalPrefixes ) !== 0 ) {
386 if ( $usingGlobalLanguages ) {
387 $addtext = 'interwiki-addtext-local-nolang';
388 } else {
389 $addtext = 'interwiki-addtext-local';
390 }
391 } else {
392 if ( $usingGlobalLanguages ) {
393 $addtext = 'interwiki-addtext-nolang';
394 } else {
395 $addtext = 'interwiki_addtext';
396 }
397 }
398 $addtext = $this->msg( $addtext )->text();
399 $addlink = $this->getLinkRenderer()->makeKnownLink(
400 $this->getPageTitle( 'add' ), $addtext );
401 $this->getOutput()->addHTML(
402 '<p class="mw-interwiki-addlink">' . $addlink . '</p>' );
403 }
404
405 $this->getOutput()->addWikiMsg( 'interwiki-legend' );
406
407 if ( ( !is_array( $iwPrefixes ) || count( $iwPrefixes ) === 0 ) &&
408 ( !is_array( $iwGlobalPrefixes ) || count( $iwGlobalPrefixes ) === 0 )
409 ) {
410 // If the interwiki table(s) are empty, display an error message
411 $this->error( 'interwiki_error' );
412 return;
413 }
414
415 // Add the global table
416 if ( count( $iwGlobalPrefixes ) !== 0 ) {
417 $this->getOutput()->addHTML(
418 '<h2 id="interwikitable-global">' .
419 $this->msg( 'interwiki-global-links' )->parse() .
420 '</h2>'
421 );
422 $this->getOutput()->addWikiMsg( 'interwiki-global-description' );
423
424 // $canModify is false here because this is just a display of remote data
425 $this->makeTable( false, $iwGlobalPrefixes );
426 }
427
428 // Add the local table
429 if ( count( $iwLocalPrefixes ) !== 0 ) {
430 if ( count( $iwGlobalPrefixes ) !== 0 ) {
431 $this->getOutput()->addHTML(
432 '<h2 id="interwikitable-local">' .
433 $this->msg( 'interwiki-local-links' )->parse() .
434 '</h2>'
435 );
436 $this->getOutput()->addWikiMsg( 'interwiki-local-description' );
437 } else {
438 $this->getOutput()->addHTML(
439 '<h2 id="interwikitable-local">' .
440 $this->msg( 'interwiki-links' )->parse() .
441 '</h2>'
442 );
443 $this->getOutput()->addWikiMsg( 'interwiki-description' );
444 }
445 $this->makeTable( $canModify, $iwLocalPrefixes );
446 }
447
448 // Add the language table
449 if ( count( $iwLanguagePrefixes ) !== 0 ) {
450 if ( $usingGlobalLanguages ) {
451 $header = 'interwiki-global-language-links';
452 $description = 'interwiki-global-language-description';
453 } else {
454 $header = 'interwiki-language-links';
455 $description = 'interwiki-language-description';
456 }
457
458 $this->getOutput()->addHTML(
459 '<h2 id="interwikitable-language">' .
460 $this->msg( $header )->parse() .
461 '</h2>'
462 );
463 $this->getOutput()->addWikiMsg( $description );
464
465 // When using global interlanguage links, don't allow them to be modified
466 // except on the source wiki
467 $canModify = ( $usingGlobalLanguages ? false : $canModify );
468 $this->makeTable( $canModify, $iwLanguagePrefixes );
469 }
470 }
471
472 protected function makeTable( $canModify, $iwPrefixes ) {
473 // Output the existing Interwiki prefixes table header
474 $out = '';
475 $out .= Html::openElement(
476 'table',
477 [ 'class' => 'mw-interwikitable wikitable sortable body' ]
478 ) . "\n";
479 $out .= Html::openElement( 'thead' ) .
480 Html::openElement( 'tr', [ 'class' => 'interwikitable-header' ] ) .
481 Html::element( 'th', null, $this->msg( 'interwiki_prefix' )->text() ) .
482 Html::element( 'th', null, $this->msg( 'interwiki_url' )->text() ) .
483 Html::element( 'th', null, $this->msg( 'interwiki_local' )->text() ) .
484 Html::element( 'th', null, $this->msg( 'interwiki_trans' )->text() ) .
485 ( $canModify ?
486 Html::element(
487 'th',
488 [ 'class' => 'unsortable' ],
489 $this->msg( 'interwiki_edit' )->text()
490 ) :
491 ''
492 );
493 $out .= Html::closeElement( 'tr' ) .
494 Html::closeElement( 'thead' ) . "\n" .
495 Html::openElement( 'tbody' );
496
497 $selfTitle = $this->getPageTitle();
498
499 // Output the existing Interwiki prefixes table rows
500 foreach ( $iwPrefixes as $iwPrefix ) {
501 $out .= Html::openElement( 'tr', [ 'class' => 'mw-interwikitable-row' ] );
502 $out .= Html::element( 'td', [ 'class' => 'mw-interwikitable-prefix' ],
503 $iwPrefix['iw_prefix'] );
504 $out .= Html::element(
505 'td',
506 [ 'class' => 'mw-interwikitable-url' ],
507 $iwPrefix['iw_url']
508 );
509 $attribs = [ 'class' => 'mw-interwikitable-local' ];
510 // Green background for cells with "yes".
511 if ( isset( $iwPrefix['iw_local'] ) && $iwPrefix['iw_local'] ) {
512 $attribs['class'] .= ' mw-interwikitable-local-yes';
513 }
514 // The messages interwiki_0 and interwiki_1 are used here.
515 $contents = isset( $iwPrefix['iw_local'] ) ?
516 $this->msg( 'interwiki_' . $iwPrefix['iw_local'] )->text() :
517 '-';
518 $out .= Html::element( 'td', $attribs, $contents );
519 $attribs = [ 'class' => 'mw-interwikitable-trans' ];
520 // Green background for cells with "yes".
521 if ( isset( $iwPrefix['iw_trans'] ) && $iwPrefix['iw_trans'] ) {
522 $attribs['class'] .= ' mw-interwikitable-trans-yes';
523 }
524 // The messages interwiki_0 and interwiki_1 are used here.
525 $contents = isset( $iwPrefix['iw_trans'] ) ?
526 $this->msg( 'interwiki_' . $iwPrefix['iw_trans'] )->text() :
527 '-';
528 $out .= Html::element( 'td', $attribs, $contents );
529
530 // Additional column when the interwiki table can be modified.
531 if ( $canModify ) {
532 $out .= Html::rawElement( 'td', [ 'class' => 'mw-interwikitable-modify' ],
533 $this->getLinkRenderer()->makeKnownLink(
534 $selfTitle,
535 $this->msg( 'edit' )->text(),
536 [],
537 [ 'action' => 'edit', 'prefix' => $iwPrefix['iw_prefix'] ]
538 ) .
539 $this->msg( 'comma-separator' )->escaped() .
540 $this->getLinkRenderer()->makeKnownLink(
541 $selfTitle,
542 $this->msg( 'delete' )->text(),
543 [],
544 [ 'action' => 'delete', 'prefix' => $iwPrefix['iw_prefix'] ]
545 )
546 );
547 }
548 $out .= Html::closeElement( 'tr' ) . "\n";
549 }
550 $out .= Html::closeElement( 'tbody' ) .
551 Html::closeElement( 'table' );
552
553 $this->getOutput()->addHTML( $out );
554 $this->getOutput()->addModuleStyles( 'jquery.tablesorter.styles' );
555 $this->getOutput()->addModules( 'jquery.tablesorter' );
556 }
557
561 protected function error( ...$args ) {
562 $this->getOutput()->wrapWikiMsg( "<p class='error'>$1</p>", $args );
563 }
564
565 protected function getGroupName() {
566 return 'wiki';
567 }
568}
$wgLegalTitleChars
Allowed title characters – regex character class Don't change this unless you know what you're doing.
bool array string $wgInterwikiCache
Interwiki cache, either as an associative array or a path to a constant database (....
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
wfReadOnly()
Check whether the wiki is in read-only mode.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
if( $line===false) $args
Definition cdb.php:64
Class to simplify the use of log pages.
Definition LogPage.php:33
MediaWikiServices is the service locator for the application scope of MediaWiki.
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...
Implements Special:Interwiki.
canModify( $out=false)
Returns boolean whether the user can modify the data.
getDescription()
Different description will be shown on Special:SpecialPage depending on whether the user can modify t...
__construct()
Constructor - sets up the new special page.
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept for prefix searches.
doesWrites()
Indicates whether this special page may perform database writes.
execute( $par)
Show the special page.
makeTable( $canModify, $iwPrefixes)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Parent class for all special pages.
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.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26
$header