MediaWiki master
SpecialVersion.php
Go to the documentation of this file.
1<?php
23namespace MediaWiki\Specials;
24
25use Closure;
28use HtmlArmor;
29use Language;
44use Message;
45use ObjectCache;
46use Parser;
48use ParserOutput;
49use Symfony\Component\Yaml\Yaml;
50use Wikimedia\Parsoid\Core\SectionMetadata;
51use Wikimedia\Parsoid\Core\TOCData;
53
60
64 protected $firstExtOpened = false;
65
69 protected $coreId = '';
70
74 protected static $extensionTypes = false;
75
77 protected $tocData;
78
80 protected $tocIndex;
81
83 protected $tocSection;
84
86 protected $tocSubSection;
87
88 private ParserFactory $parserFactory;
89 private UrlUtils $urlUtils;
90 private IConnectionProvider $dbProvider;
91
97 public function __construct(
98 ParserFactory $parserFactory,
99 UrlUtils $urlUtils,
100 IConnectionProvider $dbProvider
101 ) {
102 parent::__construct( 'Version' );
103 $this->parserFactory = $parserFactory;
104 $this->urlUtils = $urlUtils;
105 $this->dbProvider = $dbProvider;
106 }
107
115 public static function getCredits( ExtensionRegistry $reg, Config $conf ): array {
116 $credits = $conf->get( MainConfigNames::ExtensionCredits );
117 foreach ( $reg->getAllThings() as $credit ) {
118 $credits[$credit['type']][] = $credit;
119 }
120 return $credits;
121 }
122
126 public function execute( $par ) {
127 $config = $this->getConfig();
128 $credits = self::getCredits( ExtensionRegistry::getInstance(), $config );
129
130 $this->setHeaders();
131 $this->outputHeader();
132 $out = $this->getOutput();
133 $out->setPreventClickjacking( false );
134
135 // Explode the sub page information into useful bits
136 $parts = explode( '/', (string)$par );
137 $extNode = null;
138 if ( isset( $parts[1] ) ) {
139 $extName = str_replace( '_', ' ', $parts[1] );
140 // Find it!
141 foreach ( $credits as $extensions ) {
142 foreach ( $extensions as $ext ) {
143 if ( isset( $ext['name'] ) && ( $ext['name'] === $extName ) ) {
144 $extNode = &$ext;
145 break 2;
146 }
147 }
148 }
149 if ( !$extNode ) {
150 $out->setStatusCode( 404 );
151 }
152 } else {
153 $extName = 'MediaWiki';
154 }
155
156 // Now figure out what to do
157 switch ( strtolower( $parts[0] ) ) {
158 case 'credits':
159 $out->addModuleStyles( 'mediawiki.special' );
160
161 $wikiText = '{{int:version-credits-not-found}}';
162 if ( $extName === 'MediaWiki' ) {
163 $wikiText = file_get_contents( MW_INSTALL_PATH . '/CREDITS' );
164 // Put the contributor list into columns
165 $wikiText = str_replace(
166 [ '<!-- BEGIN CONTRIBUTOR LIST -->', '<!-- END CONTRIBUTOR LIST -->' ],
167 [ '<div class="mw-version-credits">', '</div>' ],
168 $wikiText );
169 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
170 $file = ExtensionInfo::getAuthorsFileName( dirname( $extNode['path'] ) );
171 if ( $file ) {
172 $wikiText = file_get_contents( $file );
173 if ( str_ends_with( $file, '.txt' ) ) {
174 $wikiText = Html::element(
175 'pre',
176 [
177 'lang' => 'en',
178 'dir' => 'ltr',
179 ],
180 $wikiText
181 );
182 }
183 }
184 }
185
186 $out->setPageTitleMsg( $this->msg( 'version-credits-title' )->plaintextParams( $extName ) );
187 $out->addWikiTextAsInterface( $wikiText );
188 break;
189
190 case 'license':
191 $out->setPageTitleMsg( $this->msg( 'version-license-title' )->plaintextParams( $extName ) );
192
193 $licenseFound = false;
194
195 if ( $extName === 'MediaWiki' ) {
196 $out->addWikiTextAsInterface(
197 file_get_contents( MW_INSTALL_PATH . '/COPYING' )
198 );
199 $licenseFound = true;
200 } elseif ( ( $extNode !== null ) && isset( $extNode['path'] ) ) {
201 $files = ExtensionInfo::getLicenseFileNames( dirname( $extNode['path'] ) );
202 if ( $files ) {
203 $licenseFound = true;
204 foreach ( $files as $file ) {
205 $out->addWikiTextAsInterface(
206 Html::element(
207 'pre',
208 [
209 'lang' => 'en',
210 'dir' => 'ltr',
211 ],
212 file_get_contents( $file )
213 )
214 );
215 }
216 }
217 }
218 if ( !$licenseFound ) {
219 $out->addWikiTextAsInterface( '{{int:version-license-not-found}}' );
220 }
221 break;
222
223 default:
224 $out->addModuleStyles( 'mediawiki.special' );
225
226 $out->addHTML( $this->getMediaWikiCredits() );
227
228 $this->tocData = new TOCData();
229 $this->tocIndex = 0;
230 $this->tocSection = 0;
231 $this->tocSubSection = 0;
232
233 // Build the page contents (this also fills in TOCData)
234 $sections = [
235 $this->softwareInformation(),
236 $this->getEntryPointInfo(),
237 $this->getSkinCredits( $credits ),
238 $this->getExtensionCredits( $credits ),
239 $this->getExternalLibraries( $credits ),
240 $this->getClientSideLibraries(),
241 $this->getParserTags(),
242 $this->getParserFunctionHooks(),
243 $this->getHooks(),
244 $this->IPInfo(),
245 ];
246
247 // Insert TOC first
248 $pout = new ParserOutput;
249 $pout->setTOCData( $this->tocData );
250 $pout->setOutputFlag( ParserOutputFlags::SHOW_TOC );
251 $pout->setText( Parser::TOC_PLACEHOLDER );
252 $out->addParserOutput( $pout );
253
254 // Insert contents
255 foreach ( $sections as $content ) {
256 $out->addHTML( $content );
257 }
258
259 break;
260 }
261 }
262
270 private function addTocSection( $labelMsg, $id ) {
271 $this->tocIndex++;
272 $this->tocSection++;
273 $this->tocSubSection = 0;
274 $this->tocData->addSection( new SectionMetadata(
275 1,
276 2,
277 $this->msg( $labelMsg )->escaped(),
278 $this->getLanguage()->formatNum( $this->tocSection ),
279 (string)$this->tocIndex,
280 null,
281 null,
282 $id,
283 $id
284 ) );
285 }
286
294 private function addTocSubSection( $label, $id ) {
295 $this->tocIndex++;
296 $this->tocSubSection++;
297 $this->tocData->addSection( new SectionMetadata(
298 2,
299 3,
300 htmlspecialchars( $label ),
301 // See Parser::localizeTOC
302 $this->getLanguage()->formatNum( $this->tocSection ) . '.' .
303 $this->getLanguage()->formatNum( $this->tocSubSection ),
304 (string)$this->tocIndex,
305 null,
306 null,
307 $id,
308 $id
309 ) );
310 }
311
317 private function getMediaWikiCredits() {
318 // No TOC entry for this heading, we treat it like the lede section
319
320 $ret = Html::element(
321 'h2',
322 [ 'id' => 'mw-version-license' ],
323 $this->msg( 'version-license' )->text()
324 );
325
326 $ret .= Html::rawElement( 'div', [ 'class' => 'plainlinks' ],
327 $this->msg( new RawMessage( self::getCopyrightAndAuthorList() ) )->parseAsBlock() .
328 Html::rawElement( 'div', [ 'class' => 'mw-version-license-info' ],
329 $this->msg( 'version-license-info' )->parseAsBlock()
330 )
331 );
332
333 return $ret;
334 }
335
342 public static function getCopyrightAndAuthorList() {
343 if ( defined( 'MEDIAWIKI_INSTALL' ) ) {
344 $othersLink = '[https://www.mediawiki.org/wiki/Special:Version/Credits ' .
345 wfMessage( 'version-poweredby-others' )->plain() . ']';
346 } else {
347 $othersLink = '[[Special:Version/Credits|' .
348 wfMessage( 'version-poweredby-others' )->plain() . ']]';
349 }
350
351 $translatorsLink = '[https://translatewiki.net/wiki/Translating:MediaWiki/Credits ' .
352 wfMessage( 'version-poweredby-translators' )->plain() . ']';
353
354 $authorList = [
355 'Magnus Manske', 'Brion Vibber', 'Lee Daniel Crocker',
356 'Tim Starling', 'Erik Möller', 'Gabriel Wicke', 'Ævar Arnfjörð Bjarmason',
357 'Niklas Laxström', 'Domas Mituzas', 'Rob Church', 'Yuri Astrakhan',
358 'Aryeh Gregor', 'Aaron Schulz', 'Andrew Garrett', 'Raimond Spekking',
359 'Alexandre Emsenhuber', 'Siebrand Mazeland', 'Chad Horohoe',
360 'Roan Kattouw', 'Trevor Parscal', 'Bryan Tong Minh', 'Sam Reed',
361 'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
362 'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
363 'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
364 'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
365 'DannyS712', 'Ori Livneh', 'Max Semenik', 'Amir Sarabadani',
366 'Derk-Jan Hartman', 'Petr Pchelko',
367 $othersLink, $translatorsLink
368 ];
369
370 return wfMessage( 'version-poweredby-credits', MWTimestamp::getLocalInstance()->format( 'Y' ),
371 Message::listParam( $authorList ) )->plain();
372 }
373
379 private function getSoftwareInformation() {
380 $dbr = $this->dbProvider->getReplicaDatabase();
381
382 // Put the software in an array of form 'name' => 'version'. All messages should
383 // be loaded here, so feel free to use wfMessage in the 'name'. Wikitext
384 // can be used both in the name and value.
385 $software = [
386 '[https://www.mediawiki.org/ MediaWiki]' => self::getVersionLinked(),
387 '[https://php.net/ PHP]' => PHP_VERSION . " (" . PHP_SAPI . ")",
388 '[https://icu.unicode.org/ ICU]' => INTL_ICU_VERSION,
389 $dbr->getSoftwareLink() => $dbr->getServerInfo(),
390 ];
391
392 // T339915: If wikidiff2 is installed, show version
393 if ( phpversion( "wikidiff2" ) ) {
394 $software[ '[https://www.mediawiki.org/wiki/Wikidiff2 wikidiff2]' ] = phpversion( "wikidiff2" );
395 }
396
397 // Allow a hook to add/remove items.
398 $this->getHookRunner()->onSoftwareInfo( $software );
399
400 return $software;
401 }
402
408 private function softwareInformation() {
409 $this->addTocSection( 'version-software', 'mw-version-software' );
410
411 $out = Html::element(
412 'h2',
413 [ 'id' => 'mw-version-software' ],
414 $this->msg( 'version-software' )->text()
415 );
416
417 $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-software' ] );
418
419 $out .= Html::rawElement( 'tr', [],
420 Html::element( 'th', [], $this->msg( 'version-software-product' )->text() ) .
421 Html::element( 'th', [], $this->msg( 'version-software-version' )->text() )
422 );
423
424 foreach ( $this->getSoftwareInformation() as $name => $version ) {
425 $out .= Html::rawElement( 'tr', [],
426 Html::rawElement( 'td', [], $this->msg( new RawMessage( $name ) )->parse() ) .
427 Html::rawElement( 'td', [ 'dir' => 'ltr' ], $this->msg( new RawMessage( $version ) )->parse() )
428 );
429 }
430
431 $out .= Html::closeElement( 'table' );
432
433 return $out;
434 }
435
444 public static function getVersion( $flags = '', $lang = null ) {
445 $gitInfo = GitInfo::repo()->getHeadSHA1();
446 if ( !$gitInfo ) {
447 $version = MW_VERSION;
448 } elseif ( $flags === 'nodb' ) {
449 $shortSha1 = substr( $gitInfo, 0, 7 );
450 $version = MW_VERSION . " ($shortSha1)";
451 } else {
452 $shortSha1 = substr( $gitInfo, 0, 7 );
453 $msg = wfMessage( 'parentheses' );
454 if ( $lang !== null ) {
455 $msg->inLanguage( $lang );
456 }
457 $shortSha1 = $msg->params( $shortSha1 )->escaped();
458 $version = MW_VERSION . ' ' . $shortSha1;
459 }
460
461 return $version;
462 }
463
471 public static function getVersionLinked() {
472 $gitVersion = self::getVersionLinkedGit();
473 if ( $gitVersion ) {
474 $v = $gitVersion;
475 } else {
476 $v = MW_VERSION; // fallback
477 }
478
479 return $v;
480 }
481
485 private static function getMWVersionLinked() {
486 $versionUrl = "";
487 $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
488 if ( $hookRunner->onSpecialVersionVersionUrl( MW_VERSION, $versionUrl ) ) {
489 $versionParts = [];
490 preg_match( "/^(\d+\.\d+)/", MW_VERSION, $versionParts );
491 $versionUrl = "https://www.mediawiki.org/wiki/MediaWiki_{$versionParts[1]}";
492 }
493
494 return '[' . $versionUrl . ' ' . MW_VERSION . ']';
495 }
496
502 private static function getVersionLinkedGit() {
503 global $wgLang;
504
505 $gitInfo = new GitInfo( MW_INSTALL_PATH );
506 $headSHA1 = $gitInfo->getHeadSHA1();
507 if ( !$headSHA1 ) {
508 return false;
509 }
510
511 $shortSHA1 = '(' . substr( $headSHA1, 0, 7 ) . ')';
512
513 $gitHeadUrl = $gitInfo->getHeadViewUrl();
514 if ( $gitHeadUrl !== false ) {
515 $shortSHA1 = "[$gitHeadUrl $shortSHA1]";
516 }
517
518 $gitHeadCommitDate = $gitInfo->getHeadCommitDate();
519 if ( $gitHeadCommitDate ) {
520 $shortSHA1 .= Html::element( 'br' ) . $wgLang->timeanddate( (string)$gitHeadCommitDate, true );
521 }
522
523 return self::getMWVersionLinked() . " $shortSHA1";
524 }
525
535 public static function getExtensionTypes(): array {
536 if ( self::$extensionTypes === false ) {
537 self::$extensionTypes = [
538 'specialpage' => wfMessage( 'version-specialpages' )->text(),
539 'editor' => wfMessage( 'version-editors' )->text(),
540 'parserhook' => wfMessage( 'version-parserhooks' )->text(),
541 'variable' => wfMessage( 'version-variables' )->text(),
542 'media' => wfMessage( 'version-mediahandlers' )->text(),
543 'antispam' => wfMessage( 'version-antispam' )->text(),
544 'skin' => wfMessage( 'version-skins' )->text(),
545 'api' => wfMessage( 'version-api' )->text(),
546 'other' => wfMessage( 'version-other' )->text(),
547 ];
548
549 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
550 ->onExtensionTypes( self::$extensionTypes );
551 }
552
553 return self::$extensionTypes;
554 }
555
565 public static function getExtensionTypeName( $type ) {
566 $types = self::getExtensionTypes();
567
568 return $types[$type] ?? $types['other'];
569 }
570
577 private function getExtensionCredits( array $credits ) {
578 $extensionTypes = self::getExtensionTypes();
579
580 $this->addTocSection( 'version-extensions', 'mw-version-ext' );
581
582 $out = Html::element(
583 'h2',
584 [ 'id' => 'mw-version-ext' ],
585 $this->msg( 'version-extensions' )->text()
586 );
587
588 if (
589 !$credits ||
590 // Skins are displayed separately, see getSkinCredits()
591 ( count( $credits ) === 1 && isset( $credits['skin'] ) )
592 ) {
593 $out .= Html::element(
594 'p',
595 [],
596 $this->msg( 'version-extensions-no-ext' )->text()
597 );
598
599 return $out;
600 }
601
602 // Make sure the 'other' type is set to an array.
603 if ( !array_key_exists( 'other', $credits ) ) {
604 $credits['other'] = [];
605 }
606 $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-ext' ] );
607
608 // Find all extensions that do not have a valid type and give them the type 'other'.
609 foreach ( $credits as $type => $extensions ) {
610 if ( !array_key_exists( $type, $extensionTypes ) ) {
611 $credits['other'] = array_merge( $credits['other'], $extensions );
612 }
613 }
614
615 $this->firstExtOpened = false;
616 // Loop through the extension categories to display their extensions in the list.
617 foreach ( $extensionTypes as $type => $text ) {
618 // Skins have a separate section
619 if ( $type !== 'other' && $type !== 'skin' ) {
620 $out .= $this->getExtensionCategory( $type, $text, $credits[$type] ?? [] );
621 }
622 }
623
624 // We want the 'other' type to be last in the list.
625 $out .= $this->getExtensionCategory( 'other', $extensionTypes['other'], $credits['other'] );
626
627 $out .= Html::closeElement( 'table' );
628
629 return $out;
630 }
631
638 private function getSkinCredits( array $credits ) {
639 $this->addTocSection( 'version-skins', 'mw-version-skin' );
640
641 $out = Html::element(
642 'h2',
643 [ 'id' => 'mw-version-skin' ],
644 $this->msg( 'version-skins' )->text()
645 );
646
647 if ( !isset( $credits['skin'] ) || !$credits['skin'] ) {
648 $out .= Html::element(
649 'p',
650 [],
651 $this->msg( 'version-skins-no-skin' )->text()
652 );
653
654 return $out;
655 }
656 $out .= Html::openElement( 'table', [ 'class' => 'wikitable plainlinks', 'id' => 'sv-skin' ] );
657
658 $this->firstExtOpened = false;
659 $out .= $this->getExtensionCategory( 'skin', null, $credits['skin'] );
660
661 $out .= Html::closeElement( 'table' );
662
663 return $out;
664 }
665
672 protected function getExternalLibraries( array $credits ) {
673 $paths = [
674 MW_INSTALL_PATH . '/vendor/composer/installed.json'
675 ];
676
677 $extensionTypes = self::getExtensionTypes();
678 foreach ( $extensionTypes as $type => $message ) {
679 if ( !isset( $credits[$type] ) || $credits[$type] === [] ) {
680 continue;
681 }
682 foreach ( $credits[$type] as $extension ) {
683 if ( !isset( $extension['path'] ) ) {
684 continue;
685 }
686 $paths[] = dirname( $extension['path'] ) . '/vendor/composer/installed.json';
687 }
688 }
689
690 $dependencies = [];
691
692 foreach ( $paths as $path ) {
693 if ( !file_exists( $path ) ) {
694 continue;
695 }
696
697 $installed = new ComposerInstalled( $path );
698
699 $dependencies += $installed->getInstalledDependencies();
700 }
701
702 if ( $dependencies === [] ) {
703 return '';
704 }
705
706 ksort( $dependencies );
707
708 $this->addTocSection( 'version-libraries', 'mw-version-libraries' );
709
710 $out = Html::element(
711 'h2',
712 [ 'id' => 'mw-version-libraries' ],
713 $this->msg( 'version-libraries' )->text()
714 );
715 $out .= Html::openElement(
716 'table',
717 [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries' ]
718 );
719 $out .= Html::openElement( 'tr' )
720 . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
721 . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
722 . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
723 . Html::element( 'th', [], $this->msg( 'version-libraries-description' )->text() )
724 . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
725 . Html::closeElement( 'tr' );
726
727 foreach ( $dependencies as $name => $info ) {
728 if ( !is_array( $info ) || str_starts_with( $info['type'], 'mediawiki-' ) ) {
729 // Skip any extensions or skins since they'll be listed
730 // in their proper section
731 continue;
732 }
733 $authors = array_map( static function ( $arr ) {
734 return new HtmlArmor( isset( $arr['homepage'] ) ?
735 Html::element( 'a', [ 'href' => $arr['homepage'] ], $arr['name'] ) :
736 htmlspecialchars( $arr['name'] )
737 );
738 }, $info['authors'] );
739 $authors = $this->listAuthors( $authors, false, MW_INSTALL_PATH . "/vendor/$name" );
740
741 // We can safely assume that the libraries' names and descriptions
742 // are written in English and aren't going to be translated,
743 // so set appropriate lang and dir attributes
744 $out .= Html::openElement( 'tr', [
745 // Add an anchor so docs can link easily to the version of
746 // this specific library
747 'id' => Sanitizer::escapeIdForAttribute(
748 "mw-version-library-$name"
749 ) ] )
750 . Html::rawElement(
751 'td',
752 [],
753 Linker::makeExternalLink(
754 "https://packagist.org/packages/$name", $name,
755 true, '',
756 [ 'class' => 'mw-version-library-name' ]
757 )
758 )
759 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
760 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
761 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['licenses'] ) )
762 . Html::element( 'td', [ 'lang' => 'en', 'dir' => 'ltr' ], $info['description'] )
763 . Html::rawElement( 'td', [], $authors )
764 . Html::closeElement( 'tr' );
765 }
766 $out .= Html::closeElement( 'table' );
767
768 return $out;
769 }
770
776 private function getClientSideLibraries() {
777 $registryDirs = [ 'MediaWiki' => MW_INSTALL_PATH . '/resources/lib' ]
778 + ExtensionRegistry::getInstance()->getAttribute( 'ForeignResourcesDir' );
779
780 $modules = [];
781 foreach ( $registryDirs as $source => $registryDir ) {
782 $foreignResources = Yaml::parseFile( "$registryDir/foreign-resources.yaml" );
783 foreach ( $foreignResources as $name => $module ) {
784 $key = $name . $module['version'];
785 if ( isset( $modules[$key] ) ) {
786 $modules[$key]['source'][] = $source;
787 continue;
788 }
789 $modules[$key] = $module + [ 'name' => $name, 'source' => [ $source ] ];
790 }
791 }
792 ksort( $modules );
793
794 $this->addTocSection( 'version-libraries-client', 'mw-version-libraries-client' );
795
796 $out = Html::element(
797 'h2',
798 [ 'id' => 'mw-version-libraries-client' ],
799 $this->msg( 'version-libraries-client' )->text()
800 );
801 $out .= Html::openElement(
802 'table',
803 [ 'class' => 'wikitable plainlinks', 'id' => 'sv-libraries-client' ]
804 );
805 $out .= Html::openElement( 'tr' )
806 . Html::element( 'th', [], $this->msg( 'version-libraries-library' )->text() )
807 . Html::element( 'th', [], $this->msg( 'version-libraries-version' )->text() )
808 . Html::element( 'th', [], $this->msg( 'version-libraries-license' )->text() )
809 . Html::element( 'th', [], $this->msg( 'version-libraries-authors' )->text() )
810 . Html::element( 'th', [], $this->msg( 'version-libraries-source' )->text() )
811 . Html::closeElement( 'tr' );
812
813 foreach ( $modules as $name => $info ) {
814 // We can safely assume that the libraries' names and descriptions
815 // are written in English and aren't going to be translated,
816 // so set appropriate lang and dir attributes
817 $out .= Html::openElement( 'tr', [
818 // Add an anchor so docs can link easily to the version of
819 // this specific library
820 'id' => Sanitizer::escapeIdForAttribute(
821 "mw-version-library-$name"
822 ) ] )
823 . Html::rawElement(
824 'td',
825 [],
826 Linker::makeExternalLink(
827 $info['homepage'], $info['name'],
828 true, '',
829 [ 'class' => 'mw-version-library-name' ]
830 )
831 )
832 . Html::element( 'td', [ 'dir' => 'auto' ], $info['version'] )
833 . Html::element( 'td', [ 'dir' => 'auto' ], $info['license'] )
834 . Html::element( 'td', [ 'dir' => 'auto' ], $info['authors'] ?? '—' )
835 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
836 . Html::element( 'td', [ 'dir' => 'auto' ], $this->listToText( $info['source'] ) )
837 . Html::closeElement( 'tr' );
838 }
839 $out .= Html::closeElement( 'table' );
840
841 return $out;
842 }
843
849 protected function getParserTags() {
850 $tags = $this->parserFactory->getMainInstance()->getTags();
851 if ( !$tags ) {
852 return '';
853 }
854
855 $this->addTocSection( 'version-parser-extensiontags', 'mw-version-parser-extensiontags' );
856
857 $out = Html::rawElement(
858 'h2',
859 [ 'id' => 'mw-version-parser-extensiontags' ],
860 Html::rawElement(
861 'span',
862 [ 'class' => 'plainlinks' ],
863 Linker::makeExternalLink(
864 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Tag_extensions',
865 $this->msg( 'version-parser-extensiontags' )->text()
866 )
867 )
868 );
869
870 array_walk( $tags, static function ( &$value ) {
871 // Bidirectional isolation improves readability in RTL wikis
872 $value = Html::element(
873 'bdi',
874 // Prevent < and > from slipping to another line
875 [
876 'style' => 'white-space: nowrap;',
877 ],
878 "<$value>"
879 );
880 } );
881
882 $out .= $this->listToText( $tags );
883
884 return $out;
885 }
886
892 protected function getParserFunctionHooks() {
893 $funcHooks = $this->parserFactory->getMainInstance()->getFunctionHooks();
894 if ( !$funcHooks ) {
895 return '';
896 }
897
898 $this->addTocSection( 'version-parser-function-hooks', 'mw-version-parser-function-hooks' );
899
900 $out = Html::rawElement(
901 'h2',
902 [ 'id' => 'mw-version-parser-function-hooks' ],
903 Html::rawElement(
904 'span',
905 [ 'class' => 'plainlinks' ],
906 Linker::makeExternalLink(
907 'https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Parser_functions',
908 $this->msg( 'version-parser-function-hooks' )->text()
909 )
910 )
911 );
912
913 $out .= $this->listToText( $funcHooks );
914
915 return $out;
916 }
917
927 protected function getExtensionCategory( $type, ?string $text, array $creditsGroup ) {
928 $out = '';
929
930 if ( $creditsGroup ) {
931 $out .= $this->openExtType( $text, 'credits-' . $type );
932
933 usort( $creditsGroup, [ $this, 'compare' ] );
934
935 foreach ( $creditsGroup as $extension ) {
936 $out .= $this->getCreditsForExtension( $type, $extension );
937 }
938 }
939
940 return $out;
941 }
942
949 public function compare( $a, $b ) {
950 return $this->getLanguage()->lc( $a['name'] ) <=> $this->getLanguage()->lc( $b['name'] );
951 }
952
971 public function getCreditsForExtension( $type, array $extension ) {
972 $out = $this->getOutput();
973
974 // We must obtain the information for all the bits and pieces!
975 // ... such as extension names and links
976 if ( isset( $extension['namemsg'] ) ) {
977 // Localized name of extension
978 $extensionName = $this->msg( $extension['namemsg'] )->text();
979 } elseif ( isset( $extension['name'] ) ) {
980 // Non localized version
981 $extensionName = $extension['name'];
982 } else {
983 $extensionName = $this->msg( 'version-no-ext-name' )->text();
984 }
985
986 if ( isset( $extension['url'] ) ) {
987 $extensionNameLink = Linker::makeExternalLink(
988 $extension['url'],
989 $extensionName,
990 true,
991 '',
992 [ 'class' => 'mw-version-ext-name' ]
993 );
994 } else {
995 $extensionNameLink = htmlspecialchars( $extensionName );
996 }
997
998 // ... and the version information
999 // If the extension path is set we will check that directory for GIT
1000 // metadata in an attempt to extract date and vcs commit metadata.
1001 $canonicalVersion = '&ndash;';
1002 $extensionPath = null;
1003 $vcsVersion = null;
1004 $vcsLink = null;
1005 $vcsDate = null;
1006
1007 if ( isset( $extension['version'] ) ) {
1008 $canonicalVersion = $out->parseInlineAsInterface( $extension['version'] );
1009 }
1010
1011 if ( isset( $extension['path'] ) ) {
1012 $extensionPath = dirname( $extension['path'] );
1013 if ( $this->coreId == '' ) {
1014 wfDebug( 'Looking up core head id' );
1015 $coreHeadSHA1 = GitInfo::repo()->getHeadSHA1();
1016 if ( $coreHeadSHA1 ) {
1017 $this->coreId = $coreHeadSHA1;
1018 }
1019 }
1020 $cache = ObjectCache::getInstance( CACHE_ANYTHING );
1021 $memcKey = $cache->makeKey(
1022 'specialversion-ext-version-text', $extension['path'], $this->coreId
1023 );
1024 [ $vcsVersion, $vcsLink, $vcsDate ] = $cache->get( $memcKey );
1025
1026 if ( !$vcsVersion ) {
1027 wfDebug( "Getting VCS info for extension {$extension['name']}" );
1028 $gitInfo = new GitInfo( $extensionPath );
1029 $vcsVersion = $gitInfo->getHeadSHA1();
1030 if ( $vcsVersion !== false ) {
1031 $vcsVersion = substr( $vcsVersion, 0, 7 );
1032 $vcsLink = $gitInfo->getHeadViewUrl();
1033 $vcsDate = $gitInfo->getHeadCommitDate();
1034 }
1035 $cache->set( $memcKey, [ $vcsVersion, $vcsLink, $vcsDate ], 60 * 60 * 24 );
1036 } else {
1037 wfDebug( "Pulled VCS info for extension {$extension['name']} from cache" );
1038 }
1039 }
1040
1041 $versionString = Html::rawElement(
1042 'span',
1043 [ 'class' => 'mw-version-ext-version' ],
1044 $canonicalVersion
1045 );
1046
1047 if ( $vcsVersion ) {
1048 if ( $vcsLink ) {
1049 $vcsVerString = Linker::makeExternalLink(
1050 $vcsLink,
1051 $this->msg( 'version-version', $vcsVersion )->text(),
1052 true,
1053 '',
1054 [ 'class' => 'mw-version-ext-vcs-version' ]
1055 );
1056 } else {
1057 $vcsVerString = Html::element( 'span',
1058 [ 'class' => 'mw-version-ext-vcs-version' ],
1059 "({$vcsVersion})"
1060 );
1061 }
1062 $versionString .= " {$vcsVerString}";
1063
1064 if ( $vcsDate ) {
1065 $versionString .= ' ' . Html::element( 'span', [
1066 'class' => 'mw-version-ext-vcs-timestamp',
1067 'dir' => $this->getLanguage()->getDir(),
1068 ], $this->getLanguage()->timeanddate( $vcsDate, true ) );
1069 }
1070 $versionString = Html::rawElement( 'span',
1071 [ 'class' => 'mw-version-ext-meta-version' ],
1072 $versionString
1073 );
1074 }
1075
1076 // ... and license information; if a license file exists we
1077 // will link to it
1078 $licenseLink = '';
1079 if ( isset( $extension['name'] ) ) {
1080 $licenseName = null;
1081 if ( isset( $extension['license-name'] ) ) {
1082 $licenseName = new HtmlArmor( $out->parseInlineAsInterface( $extension['license-name'] ) );
1083 } elseif ( $extensionPath !== null && ExtensionInfo::getLicenseFileNames( $extensionPath ) ) {
1084 $licenseName = $this->msg( 'version-ext-license' )->text();
1085 }
1086 if ( $licenseName !== null ) {
1087 $licenseLink = $this->getLinkRenderer()->makeLink(
1088 $this->getPageTitle( 'License/' . $extension['name'] ),
1089 $licenseName,
1090 [
1091 'class' => 'mw-version-ext-license',
1092 'dir' => 'auto',
1093 ]
1094 );
1095 }
1096 }
1097
1098 // ... and generate the description; which can be a parameterized l10n message
1099 // in the form [ <msgname>, <parameter>, <parameter>... ] or just a straight
1100 // up string
1101 if ( isset( $extension['descriptionmsg'] ) ) {
1102 // Localized description of extension
1103 $descriptionMsg = $extension['descriptionmsg'];
1104
1105 if ( is_array( $descriptionMsg ) ) {
1106 $descriptionMsgKey = array_shift( $descriptionMsg );
1107 $descriptionMsg = array_map( 'htmlspecialchars', $descriptionMsg );
1108 $description = $this->msg( $descriptionMsgKey, ...$descriptionMsg )->text();
1109 } else {
1110 $description = $this->msg( $descriptionMsg )->text();
1111 }
1112 } elseif ( isset( $extension['description'] ) ) {
1113 // Non localized version
1114 $description = $extension['description'];
1115 } else {
1116 $description = '';
1117 }
1118 $description = $out->parseInlineAsInterface( $description );
1119
1120 // ... now get the authors for this extension
1121 $authors = $extension['author'] ?? [];
1122 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable path is set when there is a name
1123 $authors = $this->listAuthors( $authors, $extension['name'], $extensionPath );
1124
1125 // Finally! Create the table
1126 $html = Html::openElement( 'tr', [
1127 'class' => 'mw-version-ext',
1128 'id' => Sanitizer::escapeIdForAttribute( 'mw-version-ext-' . $type . '-' . $extension['name'] )
1129 ]
1130 );
1131
1132 $html .= Html::rawElement( 'td', [], $extensionNameLink );
1133 $html .= Html::rawElement( 'td', [], $versionString );
1134 $html .= Html::rawElement( 'td', [], $licenseLink );
1135 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-description' ], $description );
1136 $html .= Html::rawElement( 'td', [ 'class' => 'mw-version-ext-authors' ], $authors );
1137
1138 $html .= Html::closeElement( 'tr' );
1139
1140 return $html;
1141 }
1142
1148 private function getHooks() {
1149 if ( !$this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) ) {
1150 return '';
1151 }
1152
1153 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1154 $hookNames = $hookContainer->getHookNames();
1155
1156 if ( !$hookNames ) {
1157 return '';
1158 }
1159
1160 sort( $hookNames );
1161
1162 $ret = [];
1163 $this->addTocSection( 'version-hooks', 'mw-version-hooks' );
1164 $ret[] = Html::element(
1165 'h2',
1166 [ 'id' => 'mw-version-hooks' ],
1167 $this->msg( 'version-hooks' )->text()
1168 );
1169 $ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
1170 $ret[] = Html::openElement( 'tr' );
1171 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
1172 $ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
1173 $ret[] = Html::closeElement( 'tr' );
1174
1175 foreach ( $hookNames as $name ) {
1176 $handlers = $hookContainer->getHandlerDescriptions( $name );
1177
1178 $ret[] = Html::openElement( 'tr' );
1179 $ret[] = Html::element( 'td', [], $name );
1180 // @phan-suppress-next-line SecurityCheck-DoubleEscaped See FIXME in listToText
1181 $ret[] = Html::element( 'td', [], $this->listToText( $handlers ) );
1182 $ret[] = Html::closeElement( 'tr' );
1183 }
1184
1185 $ret[] = Html::closeElement( 'table' );
1186
1187 return implode( "\n", $ret );
1188 }
1189
1190 private function openExtType( string $text = null, string $name = null ) {
1191 $out = '';
1192
1193 $opt = [ 'colspan' => 5 ];
1194 if ( $this->firstExtOpened ) {
1195 // Insert a spacing line
1196 $out .= Html::rawElement( 'tr', [ 'class' => 'sv-space' ],
1197 Html::element( 'td', $opt )
1198 );
1199 }
1200 $this->firstExtOpened = true;
1201
1202 if ( $name ) {
1203 $opt['id'] = "sv-$name";
1204 }
1205
1206 if ( $text !== null ) {
1207 $out .= Html::rawElement( 'tr', [],
1208 Html::element( 'th', $opt, $text )
1209 );
1210 }
1211
1212 if ( $name && $text !== null ) {
1213 $this->addTocSubSection( $text, "sv-$name" );
1214 }
1215
1216 $firstHeadingMsg = ( $name === 'credits-skin' )
1217 ? 'version-skin-colheader-name'
1218 : 'version-ext-colheader-name';
1219 $out .= Html::openElement( 'tr' );
1220 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1221 $this->msg( $firstHeadingMsg )->text() );
1222 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1223 $this->msg( 'version-ext-colheader-version' )->text() );
1224 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1225 $this->msg( 'version-ext-colheader-license' )->text() );
1226 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1227 $this->msg( 'version-ext-colheader-description' )->text() );
1228 $out .= Html::element( 'th', [ 'class' => 'mw-version-ext-col-label' ],
1229 $this->msg( 'version-ext-colheader-credits' )->text() );
1230 $out .= Html::closeElement( 'tr' );
1231
1232 return $out;
1233 }
1234
1240 private function IPInfo() {
1241 $ip = str_replace( '--', ' - ', htmlspecialchars( $this->getRequest()->getIP() ) );
1242
1243 return "<!-- visited from $ip -->\n<span style='display:none'>visited from $ip</span>";
1244 }
1245
1266 public function listAuthors( $authors, $extName, $extDir ): string {
1267 $hasOthers = false;
1268 $linkRenderer = $this->getLinkRenderer();
1269
1270 $list = [];
1271 $authors = (array)$authors;
1272
1273 // Special case: if the authors array has only one item and it is "...",
1274 // it should not be rendered as the "version-poweredby-others" i18n msg,
1275 // but rather as "version-poweredby-various" i18n msg instead.
1276 if ( count( $authors ) === 1 && $authors[0] === '...' ) {
1277 // Link to the extension's or skin's AUTHORS or CREDITS file, if there is
1278 // such a file; otherwise just return the i18n msg as-is
1279 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1280 return $linkRenderer->makeLink(
1281 $this->getPageTitle( "Credits/$extName" ),
1282 $this->msg( 'version-poweredby-various' )->text()
1283 );
1284 } else {
1285 return $this->msg( 'version-poweredby-various' )->escaped();
1286 }
1287 }
1288
1289 // Otherwise, if we have an actual array that has more than one item,
1290 // process each array item as usual
1291 foreach ( $authors as $item ) {
1292 if ( $item instanceof HtmlArmor ) {
1293 $list[] = HtmlArmor::getHtml( $item );
1294 } elseif ( $item === '...' ) {
1295 $hasOthers = true;
1296
1297 if ( $extName && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1298 $text = $linkRenderer->makeLink(
1299 $this->getPageTitle( "Credits/$extName" ),
1300 $this->msg( 'version-poweredby-others' )->text()
1301 );
1302 } else {
1303 $text = $this->msg( 'version-poweredby-others' )->escaped();
1304 }
1305 $list[] = $text;
1306 } elseif ( str_ends_with( $item, ' ...]' ) ) {
1307 $hasOthers = true;
1308 $list[] = $this->getOutput()->parseInlineAsInterface(
1309 substr( $item, 0, -4 ) . $this->msg( 'version-poweredby-others' )->text() . "]"
1310 );
1311 } else {
1312 $list[] = $this->getOutput()->parseInlineAsInterface( $item );
1313 }
1314 }
1315
1316 if ( $extName && !$hasOthers && ExtensionInfo::getAuthorsFileName( $extDir ) ) {
1317 $list[] = $linkRenderer->makeLink(
1318 $this->getPageTitle( "Credits/$extName" ),
1319 $this->msg( 'version-poweredby-others' )->text()
1320 );
1321 }
1322
1323 return $this->listToText( $list, false );
1324 }
1325
1335 private function listToText( array $list, bool $sort = true ): string {
1336 if ( !$list ) {
1337 return '';
1338 }
1339 if ( $sort ) {
1340 sort( $list );
1341 }
1342
1343 return $this->getLanguage()
1344 ->listToText( array_map( [ __CLASS__, 'arrayToString' ], $list ) );
1345 }
1346
1356 public static function arrayToString( $list ) {
1357 if ( is_array( $list ) && count( $list ) == 1 ) {
1358 $list = $list[0];
1359 }
1360 if ( $list instanceof Closure ) {
1361 // Don't output stuff like "Closure$;1028376090#8$48499d94fe0147f7c633b365be39952b$"
1362 return 'Closure';
1363 } elseif ( is_object( $list ) ) {
1364 return wfMessage( 'parentheses' )->params( get_class( $list ) )->escaped();
1365 } elseif ( !is_array( $list ) ) {
1366 return $list;
1367 } else {
1368 if ( is_object( $list[0] ) ) {
1369 $class = get_class( $list[0] );
1370 } else {
1371 $class = $list[0];
1372 }
1373
1374 return wfMessage( 'parentheses' )->params( "$class, {$list[1]}" )->escaped();
1375 }
1376 }
1377
1383 public static function getGitHeadSha1( $dir ) {
1384 wfDeprecated( __METHOD__, '1.41' );
1385 return ( new GitInfo( $dir ) )->getHeadSHA1();
1386 }
1387
1392 public function getEntryPointInfo() {
1393 $config = $this->getConfig();
1394 $scriptPath = $config->get( MainConfigNames::ScriptPath ) ?: '/';
1395
1396 $entryPoints = [
1397 'version-entrypoints-articlepath' => $config->get( MainConfigNames::ArticlePath ),
1398 'version-entrypoints-scriptpath' => $scriptPath,
1399 'version-entrypoints-index-php' => wfScript( 'index' ),
1400 'version-entrypoints-api-php' => wfScript( 'api' ),
1401 'version-entrypoints-rest-php' => wfScript( 'rest' ),
1402 ];
1403
1404 $language = $this->getLanguage();
1405 $thAttributes = [
1406 'dir' => $language->getDir(),
1407 'lang' => $language->getHtmlCode()
1408 ];
1409
1410 $this->addTocSection( 'version-entrypoints', 'mw-version-entrypoints' );
1411
1412 $out = Html::element(
1413 'h2',
1414 [ 'id' => 'mw-version-entrypoints' ],
1415 $this->msg( 'version-entrypoints' )->text()
1416 ) .
1417 Html::openElement( 'table',
1418 [
1419 'class' => 'wikitable plainlinks',
1420 'id' => 'mw-version-entrypoints-table',
1421 'dir' => 'ltr',
1422 'lang' => 'en'
1423 ]
1424 ) .
1425 Html::openElement( 'tr' ) .
1426 Html::element(
1427 'th',
1428 $thAttributes,
1429 $this->msg( 'version-entrypoints-header-entrypoint' )->text()
1430 ) .
1431 Html::element(
1432 'th',
1433 $thAttributes,
1434 $this->msg( 'version-entrypoints-header-url' )->text()
1435 ) .
1436 Html::closeElement( 'tr' );
1437
1438 foreach ( $entryPoints as $message => $value ) {
1439 $url = $this->urlUtils->expand( $value, PROTO_RELATIVE );
1440 $out .= Html::openElement( 'tr' ) .
1441 Html::rawElement( 'td', [], $this->msg( $message )->parse() ) .
1442 Html::rawElement( 'td', [], Html::rawElement( 'code', [],
1443 $this->msg( new RawMessage( "[$url $value]" ) )->parse() ) ) .
1444 Html::closeElement( 'tr' );
1445 }
1446
1447 $out .= Html::closeElement( 'table' );
1448
1449 return $out;
1450 }
1451
1452 protected function getGroupName() {
1453 return 'wiki';
1454 }
1455}
1456
1461class_alias( SpecialVersion::class, 'SpecialVersion' );
getRequest()
const CACHE_ANYTHING
Definition Defines.php:85
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:36
const PROTO_RELATIVE
Definition Defines.php:193
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfScript( $script='index')
Get the URL path to a MediaWiki entry point.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
Definition Setup.php:536
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:88
Reads an installed.json file and provides accessors to get what is installed.
Load JSON files, and uses a Processor to extract information.
getAllThings()
Get credits information about all installed extensions and skins.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
Base class for language-specific code.
Definition Language.php:63
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
Variant of the Message class.
Some internal bits split of from Skin.php.
Definition Linker.php:65
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Parent class for all special pages.
Version information about MediaWiki (core, extensions, libs), PHP, and the database.
getParserFunctionHooks()
Obtains a list of installed parser function hooks and the associated H2 header.
string $coreId
The current rev id/SHA hash of MediaWiki core.
static arrayToString( $list)
Convert an array or object to a string for display.
static getVersion( $flags='', $lang=null)
Return a string of the MediaWiki version with Git revision if available.
listAuthors( $authors, $extName, $extDir)
Return a formatted unsorted list of authors.
static getExtensionTypeName( $type)
Returns the internationalized name for an extension type.
getCreditsForExtension( $type, array $extension)
Creates and formats a version line for a single extension.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
getExtensionCategory( $type, ?string $text, array $creditsGroup)
Creates and returns the HTML for a single extension category.
static getVersionLinked()
Return a wikitext-formatted string of the MediaWiki version with a link to the Git SHA1 of head if av...
static getExtensionTypes()
Returns an array with the base extension types.
compare( $a, $b)
Callback to sort extensions by type.
__construct(ParserFactory $parserFactory, UrlUtils $urlUtils, IConnectionProvider $dbProvider)
static getCopyrightAndAuthorList()
Get the "MediaWiki is copyright 2001-20xx by lots of cool folks" text.
getEntryPointInfo()
Get the list of entry points and their URLs.
static getCredits(ExtensionRegistry $reg, Config $conf)
static string[] false $extensionTypes
Lazy initialized key/value with message content.
getParserTags()
Obtains a list of installed parser tags and the associated H2 header.
getExternalLibraries(array $credits)
Generate an HTML table for external libraries that are installed.
Fetch status information from a local git repository.
Definition GitInfo.php:47
Library for creating and parsing MW-style timestamps.
A service to expand, parse, and otherwise manipulate URLs.
Definition UrlUtils.php:17
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
static listParam(array $list, $type='text')
Definition Message.php:1286
Functions to get cache objects.
Rendered output of a wiki page, as parsed from wikitext.
setTOCData(TOCData $tocData)
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:118
Interface for configuration instances.
Definition Config.php:32
Provide primary and replica IDatabase connections.
$source
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
$content
Definition router.php:76
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!is_readable( $file)) $ext
Definition router.php:48