Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
49.00% covered (danger)
49.00%
197 / 402
23.08% covered (danger)
23.08%
3 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialInterwiki
49.00% covered (danger)
49.00%
197 / 402
23.08% covered (danger)
23.08%
3 / 13
1019.71
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescription
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getSubpagesForPrefixSearch
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 canModify
45.45% covered (danger)
45.45%
5 / 11
0.00% covered (danger)
0.00%
0 / 1
11.84
 showForm
93.33% covered (success)
93.33%
98 / 105
0.00% covered (danger)
0.00%
0 / 1
15.07
 onSubmit
87.21% covered (warning)
87.21%
75 / 86
0.00% covered (danger)
0.00%
0 / 1
18.68
 isLanguagePrefix
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 showList
0.00% covered (danger)
0.00%
0 / 109
0.00% covered (danger)
0.00%
0 / 1
506
 makeTable
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
110
 error
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Specials;
4
5use LogicException;
6use ManualLogEntry;
7use MediaWiki\Html\Html;
8use MediaWiki\HTMLForm\HTMLForm;
9use MediaWiki\Interwiki\InterwikiLookup;
10use MediaWiki\Language\Language;
11use MediaWiki\Languages\LanguageNameUtils;
12use MediaWiki\MainConfigNames;
13use MediaWiki\Message\Message;
14use MediaWiki\Output\OutputPage;
15use MediaWiki\SpecialPage\SpecialPage;
16use MediaWiki\Status\Status;
17use MediaWiki\Title\Title;
18use MediaWiki\Utils\UrlUtils;
19use PermissionsError;
20use ReadOnlyError;
21use Wikimedia\Rdbms\IConnectionProvider;
22
23/**
24 * Implements Special:Interwiki
25 *
26 * @since 1.44
27 * @ingroup SpecialPage
28 */
29class SpecialInterwiki extends SpecialPage {
30    private Language $contLang;
31    private InterwikiLookup $interwikiLookup;
32    private LanguageNameUtils $languageNameUtils;
33    private UrlUtils $urlUtils;
34    private IConnectionProvider $dbProvider;
35    private array $virtualDomainsMapping;
36    private bool $interwikiMagic;
37
38    /**
39     * @param Language $contLang
40     * @param InterwikiLookup $interwikiLookup
41     * @param LanguageNameUtils $languageNameUtils
42     * @param UrlUtils $urlUtils
43     * @param IConnectionProvider $dbProvider
44     */
45    public function __construct(
46        Language $contLang,
47        InterwikiLookup $interwikiLookup,
48        LanguageNameUtils $languageNameUtils,
49        UrlUtils $urlUtils,
50        IConnectionProvider $dbProvider
51    ) {
52        parent::__construct( 'Interwiki' );
53
54        $this->contLang = $contLang;
55        $this->interwikiLookup = $interwikiLookup;
56        $this->languageNameUtils = $languageNameUtils;
57        $this->urlUtils = $urlUtils;
58        $this->dbProvider = $dbProvider;
59        $this->virtualDomainsMapping = $this->getConfig()->get( MainConfigNames::VirtualDomainsMapping ) ?? [];
60        $this->interwikiMagic = $this->getConfig()->get( MainConfigNames::InterwikiMagic ) ?? true;
61    }
62
63    public function doesWrites() {
64        return true;
65    }
66
67    /**
68     * Different description will be shown on Special:SpecialPage depending on
69     * whether the user can modify the data.
70     *
71     * @return Message
72     */
73    public function getDescription() {
74        return $this->msg( $this->canModify() ? 'interwiki' : 'interwiki-title-norights' );
75    }
76
77    public function getSubpagesForPrefixSearch() {
78        // delete, edit both require the prefix parameter.
79        return [ 'add' ];
80    }
81
82    /**
83     * Show the special page
84     *
85     * @param string|null $par parameter passed to the page or null
86     */
87    public function execute( $par ) {
88        $this->setHeaders();
89        $this->outputHeader();
90
91        $out = $this->getOutput();
92        $request = $this->getRequest();
93
94        $out->addModuleStyles( 'mediawiki.special.interwiki' );
95
96        $action = $par ?: $request->getRawVal( 'action', $par );
97
98        if ( in_array( $action, [ 'add', 'edit', 'delete' ] ) && $this->canModify( $out ) ) {
99            $this->showForm( $action );
100        } else {
101            $this->showList();
102        }
103    }
104
105    /**
106     * Returns boolean whether the user can modify the data.
107     *
108     * @param OutputPage|bool $out If an OutputPage object given, it adds the respective error message.
109     * @return bool
110     * @throws PermissionsError|ReadOnlyError
111     */
112    private function canModify( $out = false ) {
113        if ( !$this->getUser()->isAllowed( 'interwiki' ) ) {
114            // Check permissions
115            if ( $out ) {
116                throw new PermissionsError( 'interwiki' );
117            }
118
119            return false;
120        } elseif ( $this->getConfig()->get( MainConfigNames::InterwikiCache ) ) {
121            // Editing the interwiki cache is not supported
122            if ( $out ) {
123                $out->addWikiMsg( 'interwiki-cached' );
124            }
125
126            return false;
127        } else {
128            if ( $out ) {
129                $this->checkReadOnly();
130            }
131        }
132
133        return true;
134    }
135
136    /**
137     * @param string $action The action of the form
138     */
139    protected function showForm( $action ) {
140        $formDescriptor = [];
141        $hiddenFields = [
142            'action' => $action,
143        ];
144
145        $status = Status::newGood();
146        $request = $this->getRequest();
147        $prefix = $request->getText( 'prefix' );
148
149        switch ( $action ) {
150            case 'add':
151            case 'edit':
152                $formDescriptor = [
153                    'prefix' => [
154                        'type' => 'text',
155                        'label-message' => 'interwiki-prefix-label',
156                        'name' => 'prefix',
157                        'autofocus' => true,
158                    ],
159
160                    'local' => [
161                        'type' => 'check',
162                        'id' => 'mw-interwiki-local',
163                        'label-message' => 'interwiki-local-label',
164                        'name' => 'local',
165                    ],
166
167                    'trans' => [
168                        'type' => 'check',
169                        'id' => 'mw-interwiki-trans',
170                        'label-message' => 'interwiki-trans-label',
171                        'name' => 'trans',
172                    ],
173
174                    'url' => [
175                        'type' => 'url',
176                        'id' => 'mw-interwiki-url',
177                        'label-message' => 'interwiki-url-label',
178                        'maxlength' => 200,
179                        'name' => 'url',
180                        'size' => 60,
181                    ],
182
183                    'api' => [
184                        'type' => 'url',
185                        'id' => 'mw-interwiki-api',
186                        'label-message' => 'interwiki-api-label',
187                        'maxlength' => 200,
188                        'name' => 'api',
189                        'size' => 60,
190                    ],
191
192                    'reason' => [
193                        'type' => 'text',
194                        'id' => "mw-interwiki-{$action}reason",
195                        'label-message' => 'interwiki_reasonfield',
196                        'maxlength' => 200,
197                        'name' => 'reason',
198                        'size' => 60,
199                    ],
200                ];
201
202                break;
203            case 'delete':
204                $formDescriptor = [
205                    'prefix' => [
206                        'type' => 'hidden',
207                        'name' => 'prefix',
208                        'default' => $prefix,
209                    ],
210
211                    'reason' => [
212                        'type' => 'text',
213                        'name' => 'reason',
214                        'label-message' => 'interwiki_reasonfield',
215                    ],
216                ];
217
218                break;
219            default:
220                throw new LogicException( "Unexpected action: {$action}" );
221        }
222
223        if ( $action === 'edit' || $action === 'delete' ) {
224            $dbr = $this->dbProvider->getReplicaDatabase();
225            $row = $dbr->newSelectQueryBuilder()
226                ->select( '*' )
227                ->from( 'interwiki' )
228                ->where( [ 'iw_prefix' => $prefix ] )
229                ->caller( __METHOD__ )->fetchRow();
230
231            if ( $action === 'edit' ) {
232                if ( !$row ) {
233                    $status->fatal( 'interwiki_editerror', $prefix );
234                } else {
235                    $formDescriptor['prefix']['disabled'] = true;
236                    $formDescriptor['prefix']['default'] = $prefix;
237                    $hiddenFields['prefix'] = $prefix;
238                    $formDescriptor['url']['default'] = $row->iw_url;
239                    $formDescriptor['api']['default'] = $row->iw_api;
240                    $formDescriptor['trans']['default'] = $row->iw_trans;
241                    $formDescriptor['local']['default'] = $row->iw_local;
242                }
243            } else { // $action === 'delete'
244                if ( !$row ) {
245                    $status->fatal( 'interwiki_delfailed', $prefix );
246                }
247            }
248        }
249
250        if ( !$status->isOK() ) {
251            $formDescriptor = [];
252        }
253
254        $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
255        $htmlForm
256            ->addHiddenFields( $hiddenFields )
257            ->setSubmitCallback( [ $this, 'onSubmit' ] );
258
259        if ( $status->isOK() ) {
260            if ( $action === 'delete' ) {
261                $htmlForm->setSubmitDestructive();
262            }
263
264            $htmlForm->setSubmitTextMsg( $action !== 'add' ? $action : 'interwiki_addbutton' )
265                ->setPreHtml( $this->msg( $action !== 'delete' ? "interwiki_{$action}intro" :
266                    'interwiki_deleting', $prefix )->escaped() )
267                ->show();
268        } else {
269            $htmlForm->suppressDefaultSubmit()
270                ->prepareForm()
271                ->displayForm( $status );
272        }
273
274        $this->getOutput()->addBacklinkSubtitle( $this->getPageTitle() );
275    }
276
277    public function onSubmit( array $data ) {
278        $status = Status::newGood();
279        $request = $this->getRequest();
280        $config = $this->getConfig();
281        $prefix = $data['prefix'];
282        $do = $request->getRawVal( 'action' );
283        // Show an error if the prefix is invalid (only when adding one).
284        // Invalid characters for a title should also be invalid for a prefix.
285        // Whitespace, ':', '&' and '=' are invalid, too.
286        // (Bug 30599).
287        $validPrefixChars = preg_replace( '/[ :&=]/', '', Title::legalChars() );
288        if ( $do === 'add' && preg_match( "/\s|[^$validPrefixChars]/", $prefix ) ) {
289            $status->fatal( 'interwiki-badprefix', htmlspecialchars( $prefix ) );
290            return $status;
291        }
292        // Disallow adding local interlanguage definitions if using global
293        if (
294            $do === 'add'
295            && $this->isLanguagePrefix( $prefix )
296            && isset( $this->virtualDomainsMapping['virtual-interwiki-interlanguage'] )
297        ) {
298            $status->fatal( 'interwiki-cannotaddlocallanguage', htmlspecialchars( $prefix ) );
299            return $status;
300        }
301        $reason = $data['reason'];
302        $selfTitle = $this->getPageTitle();
303        $dbw = $this->dbProvider->getPrimaryDatabase();
304        switch ( $do ) {
305            case 'delete':
306                $dbw->newDeleteQueryBuilder()
307                    ->deleteFrom( 'interwiki' )
308                    ->where( [ 'iw_prefix' => $prefix ] )
309                    ->caller( __METHOD__ )->execute();
310
311                if ( $dbw->affectedRows() === 0 ) {
312                    $status->fatal( 'interwiki_delfailed', $prefix );
313                } else {
314                    $this->getOutput()->addWikiMsg( 'interwiki_deleted', $prefix );
315                    $log = new ManualLogEntry( 'interwiki', 'iw_delete' );
316                    $log->setTarget( $selfTitle );
317                    $log->setComment( $reason );
318                    $log->setParameters( [
319                        '4::prefix' => $prefix
320                    ] );
321                    $log->setPerformer( $this->getUser() );
322                    $log->insert();
323                    $this->interwikiLookup->invalidateCache( $prefix );
324                }
325                break;
326            /** @noinspection PhpMissingBreakStatementInspection */
327            case 'add':
328                $prefix = $this->contLang->lc( $prefix );
329                // Fall through
330            case 'edit':
331                // T374771: Trim the URL and API URLs to reduce confusion when
332                // the URLs are accidentally provided with extra whitespace
333                $theurl = trim( $data['url'] );
334                $api = trim( $data['api'] ?? '' );
335                $local = $data['local'] ? 1 : 0;
336                $trans = $data['trans'] ? 1 : 0;
337                $row = [
338                    'iw_prefix' => $prefix,
339                    'iw_url' => $theurl,
340                    'iw_api' => $api,
341                    'iw_wikiid' => '',
342                    'iw_local' => $local,
343                    'iw_trans' => $trans
344                ];
345
346                if ( $prefix === '' || $theurl === '' ) {
347                    $status->fatal( 'interwiki-submit-empty' );
348                    break;
349                }
350
351                // Simple URL validation: check that the protocol is one of
352                // the supported protocols for this wiki.
353                // (T32600)
354                if ( !$this->urlUtils->parse( $theurl ) ) {
355                    $status->fatal( 'interwiki-submit-invalidurl' );
356                    break;
357                }
358
359                if ( $do === 'add' ) {
360                    $dbw->newInsertQueryBuilder()
361                        ->insertInto( 'interwiki' )
362                        ->row( $row )
363                        ->ignore()
364                        ->caller( __METHOD__ )->execute();
365                } else { // $do === 'edit'
366                    $dbw->newUpdateQueryBuilder()
367                        ->update( 'interwiki' )
368                        ->set( $row )
369                        ->where( [ 'iw_prefix' => $prefix ] )
370                        ->ignore()
371                        ->caller( __METHOD__ )->execute();
372                }
373
374                // used here: interwiki_addfailed, interwiki_added, interwiki_edited
375                if ( $dbw->affectedRows() === 0 ) {
376                    $status->fatal( "interwiki_{$do}failed", $prefix );
377                } else {
378                    $this->getOutput()->addWikiMsg( "interwiki_{$do}ed", $prefix );
379                    $log = new ManualLogEntry( 'interwiki', 'iw_' . $do );
380                    $log->setTarget( $selfTitle );
381                    $log->setComment( $reason );
382                    $log->setParameters( [
383                        '4::prefix' => $prefix,
384                        '5::url' => $theurl,
385                        '6::trans' => $trans,
386                        '7::local' => $local
387                    ] );
388                    $log->setPerformer( $this->getUser() );
389                    $log->insert();
390                    $this->interwikiLookup->invalidateCache( $prefix );
391                }
392                break;
393            default:
394                throw new LogicException( "Unexpected action: {$do}" );
395        }
396
397        return $status;
398    }
399
400    /**
401     * Determine whether a prefix is a language link
402     *
403     * @param string $prefix
404     * @return bool
405     */
406    private function isLanguagePrefix( $prefix ) {
407        return $this->interwikiMagic
408            && $this->languageNameUtils->getLanguageName( $prefix );
409    }
410
411    protected function showList() {
412        $canModify = $this->canModify();
413
414        // Build lists
415        $iwPrefixes = $this->interwikiLookup->getAllPrefixes( null );
416        $iwGlobalPrefixes = [];
417        $iwGlobalLanguagePrefixes = [];
418        if ( isset( $this->virtualDomainsMapping['virtual-interwiki'] ) ) {
419            // Fetch list from global table
420            $dbrCentralDB = $this->dbProvider->getReplicaDatabase( 'virtual-interwiki' );
421            $res = $dbrCentralDB->newSelectQueryBuilder()
422                ->select( '*' )
423                ->from( 'interwiki' )
424                ->caller( __METHOD__ )->fetchResultSet();
425            $retval = [];
426            foreach ( $res as $row ) {
427                $row = (array)$row;
428                if ( !$this->isLanguagePrefix( $row['iw_prefix'] ) ) {
429                    $retval[] = $row;
430                }
431            }
432            $iwGlobalPrefixes = $retval;
433        }
434
435        // Almost the same loop as above, but for global inter*language* links, whereas the above is for
436        // global inter*wiki* links
437        $usingGlobalLanguages = isset( $this->virtualDomainsMapping['virtual-interwiki-interlanguage'] );
438        if ( $usingGlobalLanguages ) {
439            // Fetch list from global table
440            $dbrCentralLangDB = $this->dbProvider->getReplicaDatabase( 'virtual-interwiki-interlanguage' );
441            $res = $dbrCentralLangDB->newSelectQueryBuilder()
442                ->select( '*' )
443                ->from( 'interwiki' )
444                ->caller( __METHOD__ )->fetchResultSet();
445            $retval2 = [];
446            foreach ( $res as $row ) {
447                $row = (array)$row;
448                // Note that the above DB query explicitly *excludes* interlang ones
449                // (which makes sense), whereas here we _only_ care about interlang ones!
450                if ( $this->isLanguagePrefix( $row['iw_prefix'] ) ) {
451                    $retval2[] = $row;
452                }
453            }
454            $iwGlobalLanguagePrefixes = $retval2;
455        }
456
457        // Split out language links
458        $iwLocalPrefixes = [];
459        $iwLanguagePrefixes = [];
460        foreach ( $iwPrefixes as $iwPrefix ) {
461            if ( $this->isLanguagePrefix( $iwPrefix['iw_prefix'] ) ) {
462                $iwLanguagePrefixes[] = $iwPrefix;
463            } else {
464                $iwLocalPrefixes[] = $iwPrefix;
465            }
466        }
467
468        // If using global interlanguage links, just ditch the data coming from the
469        // local table and overwrite it with the global data
470        if ( $usingGlobalLanguages ) {
471            unset( $iwLanguagePrefixes );
472            $iwLanguagePrefixes = $iwGlobalLanguagePrefixes;
473        }
474
475        // Page intro content
476        $this->getOutput()->addWikiMsg( 'interwiki_intro' );
477
478        // Add 'view log' link
479        $logLink = $this->getLinkRenderer()->makeLink(
480            SpecialPage::getTitleFor( 'Log', 'interwiki' ),
481            $this->msg( 'interwiki-logtext' )->text()
482        );
483        $this->getOutput()->addHTML(
484            Html::rawElement( 'p', [ 'class' => 'mw-interwiki-log' ], $logLink )
485        );
486
487        // Add 'add' link
488        if ( $canModify ) {
489            if ( count( $iwGlobalPrefixes ) !== 0 ) {
490                if ( $usingGlobalLanguages ) {
491                    $addtext = 'interwiki-addtext-local-nolang';
492                } else {
493                    $addtext = 'interwiki-addtext-local';
494                }
495            } else {
496                if ( $usingGlobalLanguages ) {
497                    $addtext = 'interwiki-addtext-nolang';
498                } else {
499                    $addtext = 'interwiki_addtext';
500                }
501            }
502            $addtext = $this->msg( $addtext )->text();
503            $addlink = $this->getLinkRenderer()->makeKnownLink(
504                $this->getPageTitle( 'add' ), $addtext );
505            $this->getOutput()->addHTML(
506                Html::rawElement( 'p', [ 'class' => 'mw-interwiki-addlink' ], $addlink )
507            );
508        }
509
510        $this->getOutput()->addWikiMsg( 'interwiki-legend' );
511
512        if ( $iwPrefixes === [] && $iwGlobalPrefixes === [] ) {
513            // If the interwiki table(s) are empty, display an error message
514            $this->error( 'interwiki_error' );
515            return;
516        }
517
518        // Add the global table
519        if ( count( $iwGlobalPrefixes ) !== 0 ) {
520            $this->getOutput()->addHTML(
521                Html::rawElement(
522                    'h2',
523                    [ 'class' => 'interwikitable-global' ],
524                    $this->msg( 'interwiki-global-links' )->parse()
525                )
526            );
527            $this->getOutput()->addWikiMsg( 'interwiki-global-description' );
528
529            // $canModify is false here because this is just a display of remote data
530            $this->makeTable( false, $iwGlobalPrefixes );
531        }
532
533        // Add the local table
534        if ( count( $iwLocalPrefixes ) !== 0 ) {
535            if ( count( $iwGlobalPrefixes ) !== 0 ) {
536                $this->getOutput()->addHTML(
537                    Html::rawElement(
538                        'h2',
539                        [ 'class' => 'interwikitable-local' ],
540                        $this->msg( 'interwiki-local-links' )->parse()
541                    )
542                );
543                $this->getOutput()->addWikiMsg( 'interwiki-local-description' );
544            } else {
545                $this->getOutput()->addHTML(
546                    Html::rawElement(
547                        'h2',
548                        [ 'class' => 'interwikitable-local' ],
549                        $this->msg( 'interwiki-links' )->parse()
550                    )
551                );
552                $this->getOutput()->addWikiMsg( 'interwiki-description' );
553            }
554            $this->makeTable( $canModify, $iwLocalPrefixes );
555        }
556
557        // Add the language table
558        if ( count( $iwLanguagePrefixes ) !== 0 ) {
559            if ( $usingGlobalLanguages ) {
560                $header = 'interwiki-global-language-links';
561                $description = 'interwiki-global-language-description';
562            } else {
563                $header = 'interwiki-language-links';
564                $description = 'interwiki-language-description';
565            }
566
567            $this->getOutput()->addHTML(
568                Html::rawElement(
569                    'h2',
570                    [ 'class' => 'interwikitable-language' ],
571                    $this->msg( $header )->parse()
572                )
573            );
574            $this->getOutput()->addWikiMsg( $description );
575
576            // When using global interlanguage links, don't allow them to be modified
577            // except on the source wiki
578            $canModify = ( $usingGlobalLanguages ? false : $canModify );
579            $this->makeTable( $canModify, $iwLanguagePrefixes );
580        }
581    }
582
583    protected function makeTable( $canModify, $iwPrefixes ) {
584        // Output the existing Interwiki prefixes table header
585        $out = Html::openElement(
586            'table',
587            [ 'class' => 'mw-interwikitable wikitable sortable' ]
588        ) . "\n";
589        $out .= Html::openElement( 'thead' ) .
590            Html::openElement( 'tr', [ 'class' => 'interwikitable-header' ] ) .
591            Html::element( 'th', [], $this->msg( 'interwiki_prefix' )->text() ) .
592            Html::element( 'th', [], $this->msg( 'interwiki_url' )->text() ) .
593            Html::element( 'th', [], $this->msg( 'interwiki_local' )->text() ) .
594            Html::element( 'th', [], $this->msg( 'interwiki_trans' )->text() ) .
595            ( $canModify ?
596                Html::element(
597                    'th',
598                    [ 'class' => 'unsortable' ],
599                    $this->msg( 'interwiki_edit' )->text()
600                ) :
601                ''
602            );
603        $out .= Html::closeElement( 'tr' ) .
604            Html::closeElement( 'thead' ) . "\n" .
605            Html::openElement( 'tbody' );
606
607        $selfTitle = $this->getPageTitle();
608
609        // Output the existing Interwiki prefixes table rows
610        foreach ( $iwPrefixes as $iwPrefix ) {
611            $out .= Html::openElement( 'tr', [ 'class' => 'mw-interwikitable-row' ] );
612            $out .= Html::element( 'td', [ 'class' => 'mw-interwikitable-prefix' ],
613                $iwPrefix['iw_prefix'] );
614            $out .= Html::element(
615                'td',
616                [ 'class' => 'mw-interwikitable-url' ],
617                $iwPrefix['iw_url']
618            );
619            $attribs = [ 'class' => 'mw-interwikitable-local' ];
620            // Green background for cells with "yes".
621            if ( isset( $iwPrefix['iw_local'] ) && $iwPrefix['iw_local'] ) {
622                $attribs['class'] .= ' mw-interwikitable-local-yes';
623            }
624            // The messages interwiki_0 and interwiki_1 are used here.
625            $contents = isset( $iwPrefix['iw_local'] ) ?
626                $this->msg( 'interwiki_' . $iwPrefix['iw_local'] )->text() :
627                '-';
628            $out .= Html::element( 'td', $attribs, $contents );
629            $attribs = [ 'class' => 'mw-interwikitable-trans' ];
630            // Green background for cells with "yes".
631            if ( isset( $iwPrefix['iw_trans'] ) && $iwPrefix['iw_trans'] ) {
632                $attribs['class'] .= ' mw-interwikitable-trans-yes';
633            }
634            // The messages interwiki_0 and interwiki_1 are used here.
635            $contents = isset( $iwPrefix['iw_trans'] ) ?
636                $this->msg( 'interwiki_' . $iwPrefix['iw_trans'] )->text() :
637                '-';
638            $out .= Html::element( 'td', $attribs, $contents );
639
640            // Additional column when the interwiki table can be modified.
641            if ( $canModify ) {
642                $out .= Html::rawElement( 'td', [ 'class' => 'mw-interwikitable-modify' ],
643                    $this->getLinkRenderer()->makeKnownLink(
644                        $selfTitle,
645                        $this->msg( 'edit' )->text(),
646                        [],
647                        [ 'action' => 'edit', 'prefix' => $iwPrefix['iw_prefix'] ]
648                    ) .
649                    $this->msg( 'comma-separator' )->escaped() .
650                    $this->getLinkRenderer()->makeKnownLink(
651                        $selfTitle,
652                        $this->msg( 'delete' )->text(),
653                        [],
654                        [ 'action' => 'delete', 'prefix' => $iwPrefix['iw_prefix'] ]
655                    )
656                );
657            }
658            $out .= Html::closeElement( 'tr' ) . "\n";
659        }
660        $out .= Html::closeElement( 'tbody' ) .
661            Html::closeElement( 'table' );
662
663        $this->getOutput()->addHTML( $out );
664        $this->getOutput()->addModuleStyles( 'jquery.tablesorter.styles' );
665        $this->getOutput()->addModules( 'jquery.tablesorter' );
666    }
667
668    /**
669     * @param string ...$args
670     */
671    protected function error( ...$args ) {
672        $this->getOutput()->wrapWikiMsg( "<p class='error'>$1</p>", $args );
673    }
674
675    protected function getGroupName() {
676        return 'wiki';
677    }
678}