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