MediaWiki master
SpecialInterwiki.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Specials;
4
5use LogicException;
22
30 public function __construct(
31 private readonly Language $contLang,
32 private readonly InterwikiLookup $interwikiLookup,
33 private readonly LanguageNameUtils $languageNameUtils,
34 private readonly UrlUtils $urlUtils,
35 private readonly IConnectionProvider $dbProvider,
36 ) {
37 parent::__construct( 'Interwiki' );
38 }
39
41 public function doesWrites() {
42 return true;
43 }
44
51 public function getDescription() {
52 return $this->msg( $this->canModify() ? 'interwiki' : 'interwiki-title-norights' );
53 }
54
56 public function getSubpagesForPrefixSearch() {
57 // delete, edit both require the prefix parameter.
58 return [ 'add' ];
59 }
60
66 public function execute( $par ) {
67 $this->setHeaders();
68 $this->outputHeader();
69
70 $out = $this->getOutput();
71 $request = $this->getRequest();
72
73 $out->addModuleStyles( 'mediawiki.special.interwiki' );
74
75 $action = $par ?? $request->getRawVal( 'action' );
76
77 if ( in_array( $action, [ 'add', 'edit', 'delete' ] ) && $this->canModify( $out ) ) {
78 $this->showForm( $action );
79 } else {
80 $this->showList();
81 }
82 }
83
91 private function canModify( $out = false ) {
92 if ( !$this->getUser()->isAllowed( 'interwiki' ) ) {
93 // Check permissions
94 if ( $out ) {
95 throw new PermissionsError( 'interwiki' );
96 }
97
98 return false;
99 } elseif ( $this->getConfig()->get( MainConfigNames::InterwikiCache ) ) {
100 // Editing the interwiki cache is not supported
101 if ( $out ) {
102 $out->addWikiMsg( 'interwiki-cached' );
103 }
104
105 return false;
106 } else {
107 if ( $out ) {
108 $this->checkReadOnly();
109 }
110 }
111
112 return true;
113 }
114
118 protected function showForm( $action ) {
119 $formDescriptor = [];
120 $hiddenFields = [
121 'action' => $action,
122 ];
123
124 $status = Status::newGood();
125 $request = $this->getRequest();
126 $prefix = $request->getText( 'prefix' );
127
128 switch ( $action ) {
129 case 'add':
130 case 'edit':
131 $formDescriptor = [
132 'prefix' => [
133 'type' => 'text',
134 'label-message' => 'interwiki-prefix-label',
135 'name' => 'prefix',
136 'autofocus' => true,
137 ],
138
139 'local' => [
140 'type' => 'check',
141 'id' => 'mw-interwiki-local',
142 'label-message' => 'interwiki-local-label',
143 'name' => 'local',
144 ],
145
146 'trans' => [
147 'type' => 'check',
148 'id' => 'mw-interwiki-trans',
149 'label-message' => 'interwiki-trans-label',
150 'name' => 'trans',
151 ],
152
153 'url' => [
154 'type' => 'url',
155 'id' => 'mw-interwiki-url',
156 'label-message' => 'interwiki-url-label',
157 'maxlength' => 200,
158 'name' => 'url',
159 'size' => 60,
160 ],
161
162 'api' => [
163 'type' => 'url',
164 'id' => 'mw-interwiki-api',
165 'label-message' => 'interwiki-api-label',
166 'maxlength' => 200,
167 'name' => 'api',
168 'size' => 60,
169 ],
170
171 'reason' => [
172 'type' => 'text',
173 'id' => "mw-interwiki-{$action}reason",
174 'label-message' => 'interwiki_reasonfield',
175 'maxlength' => 200,
176 'name' => 'reason',
177 'size' => 60,
178 ],
179 ];
180
181 break;
182 case 'delete':
183 $formDescriptor = [
184 'prefix' => [
185 'type' => 'hidden',
186 'name' => 'prefix',
187 'default' => $prefix,
188 ],
189
190 'reason' => [
191 'type' => 'text',
192 'name' => 'reason',
193 'label-message' => 'interwiki_reasonfield',
194 ],
195 ];
196
197 break;
198 default:
199 throw new LogicException( "Unexpected action: {$action}" );
200 }
201
202 if ( $action === 'edit' || $action === 'delete' ) {
203 $dbr = $this->dbProvider->getReplicaDatabase();
204 $row = $dbr->newSelectQueryBuilder()
205 ->select( '*' )
206 ->from( 'interwiki' )
207 ->where( [ 'iw_prefix' => $prefix ] )
208 ->caller( __METHOD__ )->fetchRow();
209
210 if ( $action === 'edit' ) {
211 if ( !$row ) {
212 $status->fatal( 'interwiki_editerror', $prefix );
213 } else {
214 $formDescriptor['prefix']['disabled'] = true;
215 $formDescriptor['prefix']['default'] = $prefix;
216 $hiddenFields['prefix'] = $prefix;
217 $formDescriptor['url']['default'] = $row->iw_url;
218 $formDescriptor['api']['default'] = $row->iw_api;
219 $formDescriptor['trans']['default'] = $row->iw_trans;
220 $formDescriptor['local']['default'] = $row->iw_local;
221 }
222 } else { // $action === 'delete'
223 if ( !$row ) {
224 $status->fatal( 'interwiki_delfailed', $prefix );
225 }
226 }
227 }
228
229 if ( !$status->isOK() ) {
230 $formDescriptor = [];
231 }
232
233 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
234 $htmlForm
235 ->addHiddenFields( $hiddenFields )
236 ->setSubmitCallback( $this->onSubmit( ... ) );
237
238 if ( $status->isOK() ) {
239 if ( $action === 'delete' ) {
240 $htmlForm->setSubmitDestructive();
241 }
242
243 $htmlForm->setSubmitTextMsg( $action !== 'add' ? $action : 'interwiki_addbutton' )
244 ->setPreHtml( $this->msg( $action !== 'delete' ? "interwiki_{$action}intro" :
245 'interwiki_deleting', $prefix )->escaped() )
246 ->show();
247 } else {
248 $htmlForm->suppressDefaultSubmit()
249 ->prepareForm()
250 ->displayForm( $status );
251 }
252
253 $this->getOutput()->addBacklinkSubtitle( $this->getPageTitle() );
254 }
255
260 private function onSubmit( array $data ) {
261 $status = Status::newGood();
262 $request = $this->getRequest();
263 $config = $this->getConfig();
264 $prefix = $data['prefix'];
265 $do = $request->getRawVal( 'action' );
266 // Show an error if the prefix is invalid (only when adding one).
267 // Invalid characters for a title should also be invalid for a prefix.
268 // Whitespace, ':', '&' and '=' are invalid, too.
269 // (Bug 30599).
270 $validPrefixChars = preg_replace( '/[ :&=]/', '', Title::legalChars() );
271 if ( $do === 'add' && preg_match( "/\s|[^$validPrefixChars]/", $prefix ) ) {
272 $status->fatal( 'interwiki-badprefix', htmlspecialchars( $prefix ) );
273 return $status;
274 }
275 $virtualDomainsMapping = $this->getConfig()->get( MainConfigNames::VirtualDomainsMapping ) ?? [];
276 // Disallow adding local interlanguage definitions if using global
277 if (
278 $do === 'add'
279 && $this->isLanguagePrefix( $prefix )
280 && isset( $virtualDomainsMapping['virtual-interwiki-interlanguage'] )
281 ) {
282 $status->fatal( 'interwiki-cannotaddlocallanguage', htmlspecialchars( $prefix ) );
283 return $status;
284 }
285 $reason = $data['reason'];
286 $selfTitle = $this->getPageTitle();
287 $dbw = $this->dbProvider->getPrimaryDatabase();
288 switch ( $do ) {
289 case 'delete':
290 $dbw->newDeleteQueryBuilder()
291 ->deleteFrom( 'interwiki' )
292 ->where( [ 'iw_prefix' => $prefix ] )
293 ->caller( __METHOD__ )->execute();
294
295 if ( $dbw->affectedRows() === 0 ) {
296 $status->fatal( 'interwiki_delfailed', $prefix );
297 } else {
298 $this->getOutput()->addWikiMsg( 'interwiki_deleted', $prefix );
299 $log = new ManualLogEntry( 'interwiki', 'iw_delete' );
300 $log->setTarget( $selfTitle );
301 $log->setComment( $reason );
302 $log->setParameters( [
303 '4::prefix' => $prefix
304 ] );
305 $log->setPerformer( $this->getUser() );
306 $log->insert();
307 $this->interwikiLookup->invalidateCache( $prefix );
308 }
309 break;
311 case 'add':
312 $prefix = $this->contLang->lc( $prefix );
313 // Fall through
314 case 'edit':
315 // T374771: Trim the URL and API URLs to reduce confusion when
316 // the URLs are accidentally provided with extra whitespace
317 $theurl = trim( $data['url'] );
318 $api = trim( $data['api'] ?? '' );
319 $local = $data['local'] ? 1 : 0;
320 $trans = $data['trans'] ? 1 : 0;
321 $row = [
322 'iw_prefix' => $prefix,
323 'iw_url' => $theurl,
324 'iw_api' => $api,
325 'iw_wikiid' => '',
326 'iw_local' => $local,
327 'iw_trans' => $trans
328 ];
329
330 if ( $prefix === '' || $theurl === '' ) {
331 $status->fatal( 'interwiki-submit-empty' );
332 break;
333 }
334
335 // Simple URL validation: check that the protocol is one of
336 // the supported protocols for this wiki.
337 // (T32600)
338 if ( !$this->urlUtils->parse( $theurl ) ) {
339 $status->fatal( 'interwiki-submit-invalidurl' );
340 break;
341 }
342
343 if ( $do === 'add' ) {
344 $dbw->newInsertQueryBuilder()
345 ->insertInto( 'interwiki' )
346 ->row( $row )
347 ->ignore()
348 ->caller( __METHOD__ )->execute();
349 } else { // $do === 'edit'
350 $dbw->newUpdateQueryBuilder()
351 ->update( 'interwiki' )
352 ->set( $row )
353 ->where( [ 'iw_prefix' => $prefix ] )
354 ->ignore()
355 ->caller( __METHOD__ )->execute();
356 }
357
358 // used here: interwiki_addfailed, interwiki_added, interwiki_edited
359 if ( $dbw->affectedRows() === 0 ) {
360 $status->fatal( "interwiki_{$do}failed", $prefix );
361 } else {
362 $this->getOutput()->addWikiMsg( "interwiki_{$do}ed", $prefix );
363 $log = new ManualLogEntry( 'interwiki', 'iw_' . $do );
364 $log->setTarget( $selfTitle );
365 $log->setComment( $reason );
366 $log->setParameters( [
367 '4::prefix' => $prefix,
368 '5::url' => $theurl,
369 '6::trans' => $trans,
370 '7::local' => $local
371 ] );
372 $log->setPerformer( $this->getUser() );
373 $log->insert();
374 $this->interwikiLookup->invalidateCache( $prefix );
375 }
376 break;
377 default:
378 throw new LogicException( "Unexpected action: {$do}" );
379 }
380
381 return $status;
382 }
383
390 private function isLanguagePrefix( $prefix ) {
391 return $this->getConfig()->get( MainConfigNames::InterwikiMagic )
392 && $this->languageNameUtils->getLanguageName( $prefix );
393 }
394
395 protected function showList() {
396 $canModify = $this->canModify();
397
398 // Build lists
399 $iwPrefixes = $this->interwikiLookup->getAllPrefixes( null );
400 $iwGlobalPrefixes = [];
401 $iwGlobalLanguagePrefixes = [];
402 $virtualDomainsMapping = $this->getConfig()->get( MainConfigNames::VirtualDomainsMapping ) ?? [];
403 if ( isset( $virtualDomainsMapping['virtual-interwiki'] ) ) {
404 // Fetch list from global table
405 $dbrCentralDB = $this->dbProvider->getReplicaDatabase( 'virtual-interwiki' );
406 $res = $dbrCentralDB->newSelectQueryBuilder()
407 ->select( '*' )
408 ->from( 'interwiki' )
409 ->caller( __METHOD__ )->fetchResultSet();
410 $retval = [];
411 foreach ( $res as $row ) {
412 $row = (array)$row;
413 if ( !$this->isLanguagePrefix( $row['iw_prefix'] ) ) {
414 $retval[] = $row;
415 }
416 }
417 $iwGlobalPrefixes = $retval;
418 }
419
420 // Almost the same loop as above, but for global inter*language* links, whereas the above is for
421 // global inter*wiki* links
422 $usingGlobalLanguages = isset( $virtualDomainsMapping['virtual-interwiki-interlanguage'] );
423 if ( $usingGlobalLanguages ) {
424 // Fetch list from global table
425 $dbrCentralLangDB = $this->dbProvider->getReplicaDatabase( 'virtual-interwiki-interlanguage' );
426 $res = $dbrCentralLangDB->newSelectQueryBuilder()
427 ->select( '*' )
428 ->from( 'interwiki' )
429 ->caller( __METHOD__ )->fetchResultSet();
430 $retval2 = [];
431 foreach ( $res as $row ) {
432 $row = (array)$row;
433 // Note that the above DB query explicitly *excludes* interlang ones
434 // (which makes sense), whereas here we _only_ care about interlang ones!
435 if ( $this->isLanguagePrefix( $row['iw_prefix'] ) ) {
436 $retval2[] = $row;
437 }
438 }
439 $iwGlobalLanguagePrefixes = $retval2;
440 }
441
442 // Split out language links
443 $iwLocalPrefixes = [];
444 $iwLanguagePrefixes = [];
445 foreach ( $iwPrefixes as $iwPrefix ) {
446 if ( $this->isLanguagePrefix( $iwPrefix['iw_prefix'] ) ) {
447 $iwLanguagePrefixes[] = $iwPrefix;
448 } else {
449 $iwLocalPrefixes[] = $iwPrefix;
450 }
451 }
452
453 // If using global interlanguage links, just ditch the data coming from the
454 // local table and overwrite it with the global data
455 if ( $usingGlobalLanguages ) {
456 unset( $iwLanguagePrefixes );
457 $iwLanguagePrefixes = $iwGlobalLanguagePrefixes;
458 }
459
460 // Page intro content
461 $this->getOutput()->addWikiMsg( 'interwiki_intro' );
462
463 // Add 'view log' link
464 $logLink = $this->getLinkRenderer()->makeLink(
465 SpecialPage::getTitleFor( 'Log', 'interwiki' ),
466 $this->msg( 'interwiki-logtext' )->text()
467 );
468 $this->getOutput()->addHTML(
469 Html::rawElement( 'p', [ 'class' => 'mw-interwiki-log' ], $logLink )
470 );
471
472 // Add 'add' link
473 if ( $canModify ) {
474 if ( count( $iwGlobalPrefixes ) !== 0 ) {
475 if ( $usingGlobalLanguages ) {
476 $addtext = 'interwiki-addtext-local-nolang';
477 } else {
478 $addtext = 'interwiki-addtext-local';
479 }
480 } else {
481 if ( $usingGlobalLanguages ) {
482 $addtext = 'interwiki-addtext-nolang';
483 } else {
484 $addtext = 'interwiki_addtext';
485 }
486 }
487 $addtext = $this->msg( $addtext )->text();
488 $addlink = $this->getLinkRenderer()->makeKnownLink(
489 $this->getPageTitle( 'add' ), $addtext );
490 $this->getOutput()->addHTML(
491 Html::rawElement( 'p', [ 'class' => 'mw-interwiki-addlink' ], $addlink )
492 );
493 }
494
495 $this->getOutput()->addWikiMsg( 'interwiki-legend' );
496
497 if ( $iwPrefixes === [] && $iwGlobalPrefixes === [] ) {
498 // If the interwiki table(s) are empty, display an error message
499 $this->error( 'interwiki_error' );
500 return;
501 }
502
503 // Add the global table
504 if ( count( $iwGlobalPrefixes ) !== 0 ) {
505 $this->getOutput()->addHTML(
506 Html::rawElement(
507 'h2',
508 [ 'class' => 'interwikitable-global' ],
509 $this->msg( 'interwiki-global-links' )->parse()
510 )
511 );
512 $this->getOutput()->addWikiMsg( 'interwiki-global-description' );
513
514 // $canModify is false here because this is just a display of remote data
515 $this->makeTable( false, $iwGlobalPrefixes );
516 }
517
518 // Add the local table
519 if ( count( $iwLocalPrefixes ) !== 0 ) {
520 if ( count( $iwGlobalPrefixes ) !== 0 ) {
521 $this->getOutput()->addHTML(
522 Html::rawElement(
523 'h2',
524 [ 'class' => 'interwikitable-local' ],
525 $this->msg( 'interwiki-local-links' )->parse()
526 )
527 );
528 $this->getOutput()->addWikiMsg( 'interwiki-local-description' );
529 } else {
530 $this->getOutput()->addHTML(
531 Html::rawElement(
532 'h2',
533 [ 'class' => 'interwikitable-local' ],
534 $this->msg( 'interwiki-links' )->parse()
535 )
536 );
537 $this->getOutput()->addWikiMsg( 'interwiki-description' );
538 }
539 $this->makeTable( $canModify, $iwLocalPrefixes );
540 }
541
542 // Add the language table
543 if ( count( $iwLanguagePrefixes ) !== 0 ) {
544 if ( $usingGlobalLanguages ) {
545 $header = 'interwiki-global-language-links';
546 $description = 'interwiki-global-language-description';
547 } else {
548 $header = 'interwiki-language-links';
549 $description = 'interwiki-language-description';
550 }
551
552 $this->getOutput()->addHTML(
553 Html::rawElement(
554 'h2',
555 [ 'class' => 'interwikitable-language' ],
556 $this->msg( $header )->parse()
557 )
558 );
559 $this->getOutput()->addWikiMsg( $description );
560
561 // When using global interlanguage links, don't allow them to be modified
562 // except on the source wiki
563 $canModify = ( $usingGlobalLanguages ? false : $canModify );
564 $this->makeTable( $canModify, $iwLanguagePrefixes );
565 }
566 }
567
568 protected function makeTable( bool $canModify, array $iwPrefixes ) {
569 // Output the existing Interwiki prefixes table header
570 $out = Html::openElement(
571 'table',
572 [ 'class' => 'mw-interwikitable wikitable sortable' ]
573 ) . "\n";
574 $out .= Html::openElement( 'thead' ) .
575 Html::openElement( 'tr', [ 'class' => 'interwikitable-header' ] ) .
576 Html::element( 'th', [], $this->msg( 'interwiki_prefix' )->text() ) .
577 Html::element( 'th', [], $this->msg( 'interwiki_url' )->text() ) .
578 Html::element( 'th', [], $this->msg( 'interwiki_local' )->text() ) .
579 Html::element( 'th', [], $this->msg( 'interwiki_trans' )->text() ) .
580 ( $canModify ?
582 'th',
583 [ 'class' => 'unsortable' ],
584 $this->msg( 'interwiki_edit' )->text()
585 ) :
586 ''
587 );
588 $out .= Html::closeElement( 'tr' ) .
589 Html::closeElement( 'thead' ) . "\n" .
590 Html::openElement( 'tbody' );
591
592 $selfTitle = $this->getPageTitle();
593
594 // Output the existing Interwiki prefixes table rows
595 foreach ( $iwPrefixes as $iwPrefix ) {
596 $out .= Html::openElement( 'tr', [ 'class' => 'mw-interwikitable-row' ] );
597 $out .= Html::element( 'td', [ 'class' => 'mw-interwikitable-prefix' ],
598 $iwPrefix['iw_prefix'] );
599 $out .= Html::element(
600 'td',
601 [ 'class' => 'mw-interwikitable-url' ],
602 $iwPrefix['iw_url']
603 );
604 $attribs = [ 'class' => 'mw-interwikitable-local' ];
605 // Green background for cells with "yes".
606 if ( isset( $iwPrefix['iw_local'] ) && $iwPrefix['iw_local'] ) {
607 $attribs['class'] .= ' mw-interwikitable-local-yes';
608 }
609 // The messages interwiki_0 and interwiki_1 are used here.
610 $contents = isset( $iwPrefix['iw_local'] ) ?
611 $this->msg( 'interwiki_' . $iwPrefix['iw_local'] )->text() :
612 '-';
613 $out .= Html::element( 'td', $attribs, $contents );
614 $attribs = [ 'class' => 'mw-interwikitable-trans' ];
615 // Green background for cells with "yes".
616 if ( isset( $iwPrefix['iw_trans'] ) && $iwPrefix['iw_trans'] ) {
617 $attribs['class'] .= ' mw-interwikitable-trans-yes';
618 }
619 // The messages interwiki_0 and interwiki_1 are used here.
620 $contents = isset( $iwPrefix['iw_trans'] ) ?
621 $this->msg( 'interwiki_' . $iwPrefix['iw_trans'] )->text() :
622 '-';
623 $out .= Html::element( 'td', $attribs, $contents );
624
625 // Additional column when the interwiki table can be modified.
626 if ( $canModify ) {
627 $out .= Html::rawElement( 'td', [ 'class' => 'mw-interwikitable-modify' ],
628 $this->getLinkRenderer()->makeKnownLink(
629 $selfTitle,
630 $this->msg( 'edit' )->text(),
631 [],
632 [ 'action' => 'edit', 'prefix' => $iwPrefix['iw_prefix'] ]
633 ) .
634 $this->msg( 'comma-separator' )->escaped() .
635 $this->getLinkRenderer()->makeKnownLink(
636 $selfTitle,
637 $this->msg( 'delete' )->text(),
638 [],
639 [ 'action' => 'delete', 'prefix' => $iwPrefix['iw_prefix'] ]
640 )
641 );
642 }
643 $out .= Html::closeElement( 'tr' ) . "\n";
644 }
645 $out .= Html::closeElement( 'tbody' ) .
646 Html::closeElement( 'table' );
647
648 $this->getOutput()->addHTML( $out );
649 $this->getOutput()->addModuleStyles( 'jquery.tablesorter.styles' );
650 $this->getOutput()->addModules( 'jquery.tablesorter' );
651 }
652
656 protected function error( ...$args ) {
657 $this->getOutput()->wrapWikiMsg( "<p class='error'>$1</p>", $args );
658 }
659
661 protected function getGroupName() {
662 return 'wiki';
663 }
664}
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...
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:207
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
A service that provides utilities to do with language names and codes.
Base class for language-specific code.
Definition Language.php:65
Class for creating new log entries and inserting them into the database.
A class containing constants representing the names of configuration variables.
const InterwikiCache
Name constant for the InterwikiCache setting, for use with Config::get()
const InterwikiMagic
Name constant for the InterwikiMagic setting, for use with Config::get()
const VirtualDomainsMapping
Name constant for the VirtualDomainsMapping setting, for use with Config::get()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
This is one of the Core classes and should be read at least once by any new developers.
Parent class for all special pages.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
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,...
getUser()
Shortcut to get the User executing this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages By default the message key is the canonical name of...
Implements Special:Interwiki.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
__construct(private readonly Language $contLang, private readonly InterwikiLookup $interwikiLookup, private readonly LanguageNameUtils $languageNameUtils, private readonly UrlUtils $urlUtils, private readonly IConnectionProvider $dbProvider,)
makeTable(bool $canModify, array $iwPrefixes)
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept for prefix searches....
getDescription()
Different description will be shown on Special:SpecialPage depending on whether the user can modify t...
execute( $par)
Show the special page.
doesWrites()
Indicates whether POST requests to this special page require write access to the wiki....
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Represents a title within MediaWiki.
Definition Title.php:69
A service to expand, parse, and otherwise manipulate URLs.
Definition UrlUtils.php:16
Service interface for looking up Interwiki records.
Provide primary and replica IDatabase connections.
element(SerializerNode $parent, SerializerNode $node, $contents)