1<?php 2 3use MediaWiki\MediaWikiServices; 4 5/** 6 * Implements Special:Interwiki 7 * @ingroup SpecialPage 8 */ 9class SpecialInterwiki extends SpecialPage { 10 /** 11 * Constructor - sets up the new special page 12 */ 13 public function __construct() { 14 parent::__construct( 'Interwiki' ); 15 } 16 17 public function doesWrites() { 18 return true; 19 } 20 21 /** 22 * Different description will be shown on Special:SpecialPage depending on 23 * whether the user can modify the data. 24 * 25 * @return string 26 */ 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 37 /** 38 * Show the special page 39 * 40 * @param string|null $par parameter passed to the page or null 41 */ 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 60 /** 61 * Returns boolean whether the user can modify the data. 62 * @param OutputPage|bool $out If $wgOut object given, it adds the respective error message. 63 * @return bool 64 * @throws PermissionsError|ReadOnlyError 65 */ 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 } else { 83 $this->checkReadOnly(); 84 } 85 86 return true; 87 } 88 89 /** 90 * @param string $action The action of the form 91 */ 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' ) { 173 $dbr = wfGetDB( DB_REPLICA ); 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 )->escaped() ) 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( 256 'iw_delete', 257 $selfTitle, 258 $reason, 259 [ $prefix ], 260 $this->getUser() 261 ); 262 $lookup->invalidateCache( $prefix ); 263 } 264 break; 265 /** @noinspection PhpMissingBreakStatementInspection */ 266 case 'add': 267 $contLang = MediaWikiServices::getInstance()->getContentLanguage(); 268 $prefix = $contLang->lc( $prefix ); 269 case 'edit': 270 $theurl = $data['url']; 271 $local = $data['local'] ? 1 : 0; 272 $trans = $data['trans'] ? 1 : 0; 273 $rows = [ 274 'iw_prefix' => $prefix, 275 'iw_url' => $theurl, 276 'iw_local' => $local, 277 'iw_trans' => $trans 278 ]; 279 280 if ( $prefix === '' || $theurl === '' ) { 281 $status->fatal( 'interwiki-submit-empty' ); 282 break; 283 } 284 285 // Simple URL validation: check that the protocol is one of 286 // the supported protocols for this wiki. 287 // (bug 30600) 288 if ( !wfParseUrl( $theurl ) ) { 289 $status->fatal( 'interwiki-submit-invalidurl' ); 290 break; 291 } 292 293 if ( $do === 'add' ) { 294 $dbw->insert( 'interwiki', $rows, __METHOD__, [ 'IGNORE' ] ); 295 } else { // $do === 'edit' 296 $dbw->update( 'interwiki', $rows, [ 'iw_prefix' => $prefix ], __METHOD__, [ 'IGNORE' ] ); 297 } 298 299 // used here: interwiki_addfailed, interwiki_added, interwiki_edited 300 if ( $dbw->affectedRows() === 0 ) { 301 $status->fatal( "interwiki_{$do}failed", $prefix ); 302 } else { 303 $this->getOutput()->addWikiMsg( "interwiki_{$do}ed", $prefix ); 304 $log = new LogPage( 'interwiki' ); 305 $log->addEntry( 306 'iw_' . $do, 307 $selfTitle, 308 $reason, 309 [ $prefix, $theurl, $trans, $local ], 310 $this->getUser() 311 ); 312 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable 313 $lookup->invalidateCache( $prefix ); 314 } 315 break; 316 } 317 318 return $status; 319 } 320 321 protected function showList() { 322 global $wgInterwikiCentralDB, $wgInterwikiCentralInterlanguageDB, $wgInterwikiViewOnly; 323 324 $canModify = $this->canModify(); 325 326 // Build lists 327 $lookup = MediaWikiServices::getInstance()->getInterwikiLookup(); 328 $iwPrefixes = $lookup->getAllPrefixes( null ); 329 $iwGlobalPrefixes = []; 330 $iwGlobalLanguagePrefixes = []; 331 if ( $wgInterwikiCentralDB !== null && $wgInterwikiCentralDB !== wfWikiID() ) { 332 // Fetch list from global table 333 $dbrCentralDB = wfGetDB( DB_REPLICA, [], $wgInterwikiCentralDB ); 334 $res = $dbrCentralDB->select( 'interwiki', '*', [], __METHOD__ ); 335 $retval = []; 336 foreach ( $res as $row ) { 337 $row = (array)$row; 338 if ( !Language::fetchLanguageName( $row['iw_prefix'] ) ) { 339 $retval[] = $row; 340 } 341 } 342 $iwGlobalPrefixes = $retval; 343 } 344 345 // Almost the same loop as above, but for global inter*language* links, whereas the above is for 346 // global inter*wiki* links 347 $usingGlobalInterlangLinks = ( $wgInterwikiCentralInterlanguageDB !== null ); 348 $isGlobalInterlanguageDB = ( $wgInterwikiCentralInterlanguageDB === wfWikiID() ); 349 $usingGlobalLanguages = $usingGlobalInterlangLinks && !$isGlobalInterlanguageDB; 350 if ( $usingGlobalLanguages ) { 351 // Fetch list from global table 352 $dbrCentralLangDB = wfGetDB( DB_REPLICA, [], $wgInterwikiCentralInterlanguageDB ); 353 $res = $dbrCentralLangDB->select( 'interwiki', '*', [], __METHOD__ ); 354 $retval2 = []; 355 foreach ( $res as $row ) { 356 $row = (array)$row; 357 // Note that the above DB query explicitly *excludes* interlang ones 358 // (which makes sense), whereas here we _only_ care about interlang ones! 359 if ( Language::fetchLanguageName( $row['iw_prefix'] ) ) { 360 $retval2[] = $row; 361 } 362 } 363 $iwGlobalLanguagePrefixes = $retval2; 364 } 365 366 // Split out language links 367 $iwLocalPrefixes = []; 368 $iwLanguagePrefixes = []; 369 foreach ( $iwPrefixes as $iwPrefix ) { 370 if ( Language::fetchLanguageName( $iwPrefix['iw_prefix'] ) ) { 371 $iwLanguagePrefixes[] = $iwPrefix; 372 } else { 373 $iwLocalPrefixes[] = $iwPrefix; 374 } 375 } 376 377 // If using global interlanguage links, just ditch the data coming from the 378 // local table and overwrite it with the global data 379 if ( $usingGlobalInterlangLinks ) { 380 unset( $iwLanguagePrefixes ); 381 $iwLanguagePrefixes = $iwGlobalLanguagePrefixes; 382 } 383 384 // Page intro content 385 $this->getOutput()->addWikiMsg( 'interwiki_intro' ); 386 387 // Add 'view log' link when possible 388 if ( $wgInterwikiViewOnly === false ) { 389 $logLink = $this->getLinkRenderer()->makeLink( 390 SpecialPage::getTitleFor( 'Log', 'interwiki' ), 391 $this->msg( 'interwiki-logtext' )->text() 392 ); 393 $this->getOutput()->addHTML( '<p class="mw-interwiki-log">' . $logLink . '</p>' ); 394 } 395 396 // Add 'add' link 397 if ( $canModify ) { 398 if ( count( $iwGlobalPrefixes ) !== 0 ) { 399 if ( $usingGlobalLanguages ) { 400 $addtext = 'interwiki-addtext-local-nolang'; 401 } else { 402 $addtext = 'interwiki-addtext-local'; 403 } 404 } else { 405 if ( $usingGlobalLanguages ) { 406 $addtext = 'interwiki-addtext-nolang'; 407 } else { 408 $addtext = 'interwiki_addtext'; 409 } 410 } 411 $addtext = $this->msg( $addtext )->text(); 412 $addlink = $this->getLinkRenderer()->makeKnownLink( 413 $this->getPageTitle( 'add' ), $addtext ); 414 $this->getOutput()->addHTML( 415 '<p class="mw-interwiki-addlink">' . $addlink . '</p>' ); 416 } 417 418 $this->getOutput()->addWikiMsg( 'interwiki-legend' ); 419 420 if ( $iwPrefixes === [] && $iwGlobalPrefixes === [] ) { 421 // If the interwiki table(s) are empty, display an error message 422 $this->error( 'interwiki_error' ); 423 return; 424 } 425 426 // Add the global table 427 if ( count( $iwGlobalPrefixes ) !== 0 ) { 428 $this->getOutput()->addHTML( 429 '<h2 id="interwikitable-global">' . 430 $this->msg( 'interwiki-global-links' )->parse() . 431 '</h2>' 432 ); 433 $this->getOutput()->addWikiMsg( 'interwiki-global-description' ); 434 435 // $canModify is false here because this is just a display of remote data 436 $this->makeTable( false, $iwGlobalPrefixes ); 437 } 438 439 // Add the local table 440 if ( count( $iwLocalPrefixes ) !== 0 ) { 441 if ( count( $iwGlobalPrefixes ) !== 0 ) { 442 $this->getOutput()->addHTML( 443 '<h2 id="interwikitable-local">' . 444 $this->msg( 'interwiki-local-links' )->parse() . 445 '</h2>' 446 ); 447 $this->getOutput()->addWikiMsg( 'interwiki-local-description' ); 448 } else { 449 $this->getOutput()->addHTML( 450 '<h2 id="interwikitable-local">' . 451 $this->msg( 'interwiki-links' )->parse() . 452 '</h2>' 453 ); 454 $this->getOutput()->addWikiMsg( 'interwiki-description' ); 455 } 456 $this->makeTable( $canModify, $iwLocalPrefixes ); 457 } 458 459 // Add the language table 460 if ( count( $iwLanguagePrefixes ) !== 0 ) { 461 if ( $usingGlobalLanguages ) { 462 $header = 'interwiki-global-language-links'; 463 $description = 'interwiki-global-language-description'; 464 } else { 465 $header = 'interwiki-language-links'; 466 $description = 'interwiki-language-description'; 467 } 468 469 $this->getOutput()->addHTML( 470 '<h2 id="interwikitable-language">' . 471 $this->msg( $header )->parse() . 472 '</h2>' 473 ); 474 $this->getOutput()->addWikiMsg( $description ); 475 476 // When using global interlanguage links, don't allow them to be modified 477 // except on the source wiki 478 $canModify = ( $usingGlobalLanguages ? false : $canModify ); 479 $this->makeTable( $canModify, $iwLanguagePrefixes ); 480 } 481 } 482 483 protected function makeTable( $canModify, $iwPrefixes ) { 484 // Output the existing Interwiki prefixes table header 485 $out = ''; 486 $out .= Html::openElement( 487 'table', 488 [ 'class' => 'mw-interwikitable wikitable sortable body' ] 489 ) . "\n"; 490 $out .= Html::openElement( 'thead' ) . 491 Html::openElement( 'tr', [ 'class' => 'interwikitable-header' ] ) . 492 Html::element( 'th', [], $this->msg( 'interwiki_prefix' )->text() ) . 493 Html::element( 'th', [], $this->msg( 'interwiki_url' )->text() ) . 494 Html::element( 'th', [], $this->msg( 'interwiki_local' )->text() ) . 495 Html::element( 'th', [], $this->msg( 'interwiki_trans' )->text() ) . 496 ( $canModify ? 497 Html::element( 498 'th', 499 [ 'class' => 'unsortable' ], 500 $this->msg( 'interwiki_edit' )->text() 501 ) : 502 '' 503 ); 504 $out .= Html::closeElement( 'tr' ) . 505 Html::closeElement( 'thead' ) . "\n" . 506 Html::openElement( 'tbody' ); 507 508 $selfTitle = $this->getPageTitle(); 509 510 // Output the existing Interwiki prefixes table rows 511 foreach ( $iwPrefixes as $iwPrefix ) { 512 $out .= Html::openElement( 'tr', [ 'class' => 'mw-interwikitable-row' ] ); 513 $out .= Html::element( 'td', [ 'class' => 'mw-interwikitable-prefix' ], 514 $iwPrefix['iw_prefix'] ); 515 $out .= Html::element( 516 'td', 517 [ 'class' => 'mw-interwikitable-url' ], 518 $iwPrefix['iw_url'] 519 ); 520 $attribs = [ 'class' => 'mw-interwikitable-local' ]; 521 // Green background for cells with "yes". 522 if ( isset( $iwPrefix['iw_local'] ) && $iwPrefix['iw_local'] ) { 523 $attribs['class'] .= ' mw-interwikitable-local-yes'; 524 } 525 // The messages interwiki_0 and interwiki_1 are used here. 526 $contents = isset( $iwPrefix['iw_local'] ) ? 527 $this->msg( 'interwiki_' . $iwPrefix['iw_local'] )->text() : 528 '-'; 529 $out .= Html::element( 'td', $attribs, $contents ); 530 $attribs = [ 'class' => 'mw-interwikitable-trans' ]; 531 // Green background for cells with "yes". 532 if ( isset( $iwPrefix['iw_trans'] ) && $iwPrefix['iw_trans'] ) { 533 $attribs['class'] .= ' mw-interwikitable-trans-yes'; 534 } 535 // The messages interwiki_0 and interwiki_1 are used here. 536 $contents = isset( $iwPrefix['iw_trans'] ) ? 537 $this->msg( 'interwiki_' . $iwPrefix['iw_trans'] )->text() : 538 '-'; 539 $out .= Html::element( 'td', $attribs, $contents ); 540 541 // Additional column when the interwiki table can be modified. 542 if ( $canModify ) { 543 $out .= Html::rawElement( 'td', [ 'class' => 'mw-interwikitable-modify' ], 544 $this->getLinkRenderer()->makeKnownLink( 545 $selfTitle, 546 $this->msg( 'edit' )->text(), 547 [], 548 [ 'action' => 'edit', 'prefix' => $iwPrefix['iw_prefix'] ] 549 ) . 550 $this->msg( 'comma-separator' )->escaped() . 551 $this->getLinkRenderer()->makeKnownLink( 552 $selfTitle, 553 $this->msg( 'delete' )->text(), 554 [], 555 [ 'action' => 'delete', 'prefix' => $iwPrefix['iw_prefix'] ] 556 ) 557 ); 558 } 559 $out .= Html::closeElement( 'tr' ) . "\n"; 560 } 561 $out .= Html::closeElement( 'tbody' ) . 562 Html::closeElement( 'table' ); 563 564 $this->getOutput()->addHTML( $out ); 565 $this->getOutput()->addModuleStyles( 'jquery.tablesorter.styles' ); 566 $this->getOutput()->addModules( 'jquery.tablesorter' ); 567 } 568 569 /** 570 * @param string ...$args 571 */ 572 protected function error( ...$args ) { 573 $this->getOutput()->wrapWikiMsg( "<p class='error'>$1</p>", $args ); 574 } 575 576 protected function getGroupName() { 577 return 'wiki'; 578 } 579} 580