Page MenuHomeWickedGov Phorge

SiteConfig.php
No OneTemporary

Size
22 KB
Referenced Files
None
Subscribers
None

SiteConfig.php

<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Config\Api;
use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
use Wikimedia\Bcp47Code\Bcp47Code;
use Wikimedia\Parsoid\Config\SiteConfig as ISiteConfig;
use Wikimedia\Parsoid\Config\StubMetadataCollector;
use Wikimedia\Parsoid\Core\ContentMetadataCollector;
use Wikimedia\Parsoid\DOM\Document;
use Wikimedia\Parsoid\Mocks\MockMetrics;
use Wikimedia\Parsoid\Utils\ConfigUtils;
use Wikimedia\Parsoid\Utils\PHPUtils;
use Wikimedia\Parsoid\Utils\Title;
use Wikimedia\Parsoid\Utils\UrlUtils;
use Wikimedia\Parsoid\Utils\Utils;
/**
* SiteConfig via MediaWiki's Action API
*
* Note this is intended for testing, not performance.
*/
class SiteConfig extends ISiteConfig {
/** @var ApiHelper */
private $api;
/** @var array|null */
private $siteData;
/** @var array|null */
private $protocols;
/** @var string|null */
private $baseUri;
/** @var string|null */
private $relativeLinkPrefix;
/** @var string */
private $savedCategoryRegexp;
/** @var string */
private $savedRedirectRegexp;
/** @var string */
private $savedBswRegexp;
/** @var array<int,string> */
protected $nsNames = [];
/** @var array<int,string> */
protected $nsCase = [];
/** @var array<string,int> */
protected $nsIds = [];
/** @var array<string,int> */
protected $nsCanon = [];
/** @var array<int,bool> */
protected $nsWithSubpages = [];
/** @var array<string,string> */
private $specialPageNames = [];
/** @var array */
private $specialPageAliases = [];
/** @var array|null */
private $interwikiMap;
/** @var array<string,array>|null Keys are stored as lowercased BCP-47 code strings */
private $variants;
/** @var array<string,bool>|null Keys are stored as lowercased BCP-47 code strings */
private $langConverterEnabled;
/** @var array|null */
private $apiMagicWords;
/** @var array|null */
private $paramMWs;
/** @var array|null */
private $apiVariables;
/** @var array|null */
private $apiFunctionHooks;
/** @var array|null */
private $allMWs;
/** @var array|null */
private $extensionTags;
/** @var int|null */
private $widthOption;
/** @var int */
private $maxDepth = 40;
private $featureDetectionDone = false;
private $hasVideoInfo = false;
/** @var string[] Base parameters for a siteinfo query */
public const SITE_CONFIG_QUERY_PARAMS = [
'action' => 'query',
'meta' => 'siteinfo',
'siprop' => 'general|protocols|namespaces|namespacealiases|magicwords|interwikimap|'
. 'languagevariants|defaultoptions|specialpagealiases|extensiontags|'
. 'functionhooks|variables',
];
public function __construct( ApiHelper $api, array $opts ) {
parent::__construct();
$this->api = $api;
$this->linterEnabled = (bool)( $opts['linting'] ?? false );
$this->addHTMLTemplateParameters = (bool)( $opts['addHTMLTemplateParameters'] ?? false );
if ( isset( $opts['maxDepth'] ) ) {
$this->maxDepth = (int)$opts['maxDepth'];
}
$this->setLogger( $opts['logger'] ?? self::createLogger() );
if ( isset( $opts['wt2htmlLimits'] ) ) {
$this->wt2htmlLimits = array_merge(
$this->wt2htmlLimits, $opts['wt2htmlLimits']
);
}
if ( isset( $opts['html2wtLimits'] ) ) {
$this->html2wtLimits = array_merge(
$this->html2wtLimits, $opts['html2wtLimits']
);
}
}
protected function reset() {
$this->siteData = null;
$this->baseUri = null;
$this->relativeLinkPrefix = null;
// Superclass value reset since parsertests reuse SiteConfig objects
$this->linkTrailRegex = false;
$this->mwAliases = null;
$this->interwikiMapNoNamespaces = null;
$this->iwMatcher = null;
}
/**
* Combine sets of regex fragments
* @param string[][] $res
* - $regexes[0] are case-insensitive regex fragments. Must not be empty.
* - $regexes[1] are case-sensitive regex fragments. Must not be empty.
* @return string Combined regex fragment. May be an alternation. Assumes
* the outer environment is case-sensitive.
*/
private function combineRegexArrays( array $res ): string {
if ( $res ) {
if ( isset( $res[0] ) ) {
$res[0] = '(?i:' . implode( '|', $res[0] ) . ')';
}
if ( isset( $res[1] ) ) {
$res[1] = '(?:' . implode( '|', $res[1] ) . ')';
}
return implode( '|', $res );
}
// None? Return a failing regex
return '(?!)';
}
/**
* Add a new namespace to the config
*
* Protected access to let mocks and parser tests versions
* add new namespaces as required.
*
* @param array $ns Namespace info
*/
protected function addNamespace( array $ns ): void {
$id = (int)$ns['id'];
$this->nsNames[$id] = $ns['name'];
$this->nsIds[Utils::normalizeNamespaceName( $ns['name'] )] = $id;
$this->nsCanon[Utils::normalizeNamespaceName( $ns['canonical'] ?? $ns['name'] )] = $id;
if ( $ns['subpages'] ) {
$this->nsWithSubpages[$id] = true;
}
$this->nsCase[$id] = (string)$ns['case'];
}
private function detectFeatures(): void {
if ( !$this->featureDetectionDone ) {
$this->featureDetectionDone = true;
$data = $this->api->makeRequest( [ 'action' => 'paraminfo', 'modules' => 'query' ] );
$props = $data["paraminfo"]["modules"][0]["parameters"]["0"]["type"] ?? [];
$this->hasVideoInfo = in_array( 'videoinfo', $props, true );
}
}
public function hasVideoInfo(): bool {
$this->detectFeatures();
return $this->hasVideoInfo;
}
/**
* Load site data from the Action API, if necessary
*/
private function loadSiteData(): void {
if ( $this->siteData !== null ) {
return;
}
$data = $this->api->makeRequest( self::SITE_CONFIG_QUERY_PARAMS )['query'];
$this->siteData = $data['general'];
$this->widthOption = $data['general']['thumblimits'][$data['defaultoptions']['thumbsize']];
$this->protocols = $data['protocols'];
$this->apiVariables = $data['variables'];
$this->apiFunctionHooks = PHPUtils::makeSet( $data['functionhooks'] );
// Process namespace data from API
$this->nsNames = [];
$this->nsCase = [];
$this->nsIds = [];
$this->nsCanon = [];
$this->nsWithSubpages = [];
foreach ( $data['namespaces'] as $ns ) {
$this->addNamespace( $ns );
}
foreach ( $data['namespacealiases'] as $ns ) {
$this->nsIds[Utils::normalizeNamespaceName( $ns['alias'] )] = $ns['id'];
}
// Process magic word data from API
$bsws = [];
$this->paramMWs = [];
$this->allMWs = [];
// Recast the API results in the format that core MediaWiki returns internally
// This enables us to use the Production SiteConfig without changes and add the
// extra overhead to this developer API usage.
$this->apiMagicWords = [];
foreach ( $data['magicwords'] as $mw ) {
$cs = (int)$mw['case-sensitive'];
$mwName = $mw['name'];
$this->apiMagicWords[$mwName][] = $cs;
$pmws = [];
$allMWs = [];
foreach ( $mw['aliases'] as $alias ) {
$this->apiMagicWords[$mwName][] = $alias;
// Aliases for double underscore mws include the underscores
if ( substr( $alias, 0, 2 ) === '__' && substr( $alias, -2 ) === '__' ) {
$bsws[$cs][] = preg_quote( substr( $alias, 2, -2 ), '@' );
}
if ( strpos( $alias, '$1' ) !== false ) {
$pmws[$cs][] = strtr( preg_quote( $alias, '/' ), [ '\\$1' => "(.*?)" ] );
}
$allMWs[$cs][] = preg_quote( $alias, '/' );
}
if ( $pmws ) {
$this->paramMWs[$mwName] = '/^(?:' . $this->combineRegexArrays( $pmws ) . ')$/uDS';
}
$this->allMWs[$mwName] = '/^(?:' . $this->combineRegexArrays( $allMWs ) . ')$/D';
}
$bswRegexp = $this->combineRegexArrays( $bsws );
// Parse interwiki map data from the API
$this->interwikiMap = ConfigUtils::computeInterwikiMap( $data['interwikimap'] );
// Parse variant data from the API
# T320662: API should return these in BCP-47 forms
$this->langConverterEnabled = [];
$this->variants = [];
foreach ( $data['languagevariants'] as $base => $variants ) {
$baseBcp47 = Utils::mwCodeToBcp47( $base );
if ( $this->siteData['langconversion'] ) {
$baseKey = strtolower( $baseBcp47->toBcp47Code() );
$this->langConverterEnabled[$baseKey] = true;
foreach ( $variants as $code => $vdata ) {
$variantKey = strtolower( Utils::mwCodeToBcp47( $code )->toBcp47Code() );
$this->variants[$variantKey] = [
'base' => $baseBcp47,
'fallbacks' => array_map(
[ Utils::class, 'mwCodeToBcp47' ],
$vdata['fallbacks']
),
];
}
}
}
// Parse extension tag data from the API
$this->extensionTags = [];
foreach ( $data['extensiontags'] as $tag ) {
$tag = preg_replace( '/^<|>$/D', '', $tag );
$this->ensureExtensionTag( $tag );
}
$this->specialPageAliases = $data['specialpagealiases'];
$this->specialPageNames = [];
foreach ( $this->specialPageAliases as $special ) {
$alias = strtr( mb_strtoupper( $special['realname'] ), ' ', '_' );
$this->specialPageNames[$alias] = $special['aliases'][0];
foreach ( $special['aliases'] as $alias ) {
$alias = strtr( mb_strtoupper( $alias ), ' ', '_' );
$this->specialPageNames[$alias] = $special['aliases'][0];
}
}
$redirect = '(?i:\#REDIRECT)';
$quote = static function ( $s ) {
$q = preg_quote( $s, '@' );
# Note that PHP < 7.3 doesn't escape # in preg_quote. That means
# that the $redirect regexp will fail if used with the `x` flag.
# Manually hack around this for PHP 7.2; can remove this workaround
# once minimum PHP version >= 7.3
if ( preg_quote( '#' ) === '#' ) {
$q = str_replace( '#', '\\#', $q );
}
return $q;
};
foreach ( $data['magicwords'] as $mw ) {
if ( $mw['name'] === 'redirect' ) {
$redirect = implode( '|', array_map( $quote, $mw['aliases'] ) );
if ( !$mw['case-sensitive'] ) {
$redirect = '(?i:' . $redirect . ')';
}
break;
}
}
// `$this->nsNames[14]` is set earlier by the calls to `$this->addNamespace( $ns )`
// @phan-suppress-next-line PhanCoalescingAlwaysNull
$category = $this->quoteTitleRe( $this->nsNames[14] ?? 'Category', '@' );
if ( $category !== 'Category' ) {
$category = "(?:$category|Category)";
}
$this->savedCategoryRegexp = "@{$category}@";
$this->savedRedirectRegexp = "@{$redirect}@";
$this->savedBswRegexp = "@{$bswRegexp}@";
}
public function galleryOptions(): array {
$this->loadSiteData();
return $this->siteData['galleryoptions'];
}
public function allowedExternalImagePrefixes(): array {
$this->loadSiteData();
return $this->siteData['externalimages'] ?? [];
}
/**
* Determine the article base URI and relative prefix
*/
private function determineArticlePath(): void {
$this->loadSiteData();
$url = $this->siteData['server'] . $this->siteData['articlepath'];
if ( substr( $url, -2 ) !== '$1' ) {
throw new \UnexpectedValueException( "Article path '$url' does not have '$1' at the end" );
}
$url = substr( $url, 0, -2 );
$bits = UrlUtils::parseUrl( $url );
if ( !$bits ) {
throw new \UnexpectedValueException( "Failed to parse article path '$url'" );
}
if ( empty( $bits['path'] ) ) {
$path = '/';
} else {
$path = UrlUtils::removeDotSegments( $bits['path'] );
}
$relParts = [ 'query' => true, 'fragment' => true ];
$base = array_diff_key( $bits, $relParts );
$rel = array_intersect_key( $bits, $relParts );
$i = strrpos( $path, '/' );
$base['path'] = substr( $path, 0, $i + 1 );
$rel['path'] = '.' . substr( $path, $i );
$this->baseUri = UrlUtils::assembleUrl( $base );
$this->relativeLinkPrefix = UrlUtils::assembleUrl( $rel );
}
public function baseURI(): string {
if ( $this->baseUri === null ) {
$this->determineArticlePath();
}
return $this->baseUri;
}
public function relativeLinkPrefix(): string {
if ( $this->relativeLinkPrefix === null ) {
$this->determineArticlePath();
}
return $this->relativeLinkPrefix;
}
/** @inheritDoc */
public function canonicalNamespaceId( string $name ): ?int {
$this->loadSiteData();
return $this->nsCanon[Utils::normalizeNamespaceName( $name )] ?? null;
}
/** @inheritDoc */
public function namespaceId( string $name ): ?int {
$this->loadSiteData();
$name = Utils::normalizeNamespaceName( $name );
return $this->nsCanon[$name] ?? $this->nsIds[$name] ?? null;
}
/** @inheritDoc */
public function namespaceName( int $ns ): ?string {
$this->loadSiteData();
return $this->nsNames[$ns] ?? null;
}
/** @inheritDoc */
public function namespaceHasSubpages( int $ns ): bool {
$this->loadSiteData();
return $this->nsWithSubpages[$ns] ?? false;
}
/** @inheritDoc */
public function namespaceCase( int $ns ): string {
$this->loadSiteData();
return $this->nsCase[$ns] ?? 'first-letter';
}
/** @inheritDoc */
public function specialPageLocalName( string $alias ): ?string {
$this->loadSiteData();
$alias = strtr( mb_strtoupper( $alias ), ' ', '_' );
return $this->specialPageNames[$alias] ?? null;
}
/** @inheritDoc */
public function magicLinkEnabled( string $which ): bool {
$this->loadSiteData();
$magic = $this->siteData['magiclinks'] ?? [];
// Default to true, as wikis too old to export the 'magiclinks'
// property always had magic links enabled.
return $magic[$which] ?? true;
}
public function interwikiMagic(): bool {
$this->loadSiteData();
return $this->siteData['interwikimagic'];
}
public function interwikiMap(): array {
$this->loadSiteData();
return $this->interwikiMap;
}
public function iwp(): string {
$this->loadSiteData();
return $this->siteData['wikiid'];
}
public function legalTitleChars(): string {
$this->loadSiteData();
return $this->siteData['legaltitlechars'];
}
public function linkPrefixRegex(): ?string {
$this->loadSiteData();
if ( !empty( $this->siteData['linkprefixcharset'] ) ) {
return '/[' . $this->siteData['linkprefixcharset'] . ']+$/Du';
} else {
// We don't care about super-old MediaWiki, so don't try to parse 'linkprefix'.
return null;
}
}
/** @inheritDoc */
protected function linkTrail(): string {
$this->loadSiteData();
return $this->siteData['linktrail'];
}
public function langBcp47(): Bcp47Code {
$this->loadSiteData();
return Utils::mwCodeToBcp47( $this->siteData['lang'] );
}
public function mainpage(): string {
$this->loadSiteData();
return $this->siteData['mainpage'];
}
public function mainPageLinkTarget(): Title {
$this->loadSiteData();
return Title::newFromText( $this->siteData['mainpage'], $this );
}
/** @inheritDoc */
public function getMWConfigValue( string $key ) {
$this->loadSiteData();
switch ( $key ) {
// Hardcoded values for these 2 keys
case 'CiteResponsiveReferences':
return $this->siteData['citeresponsivereferences'] ?? false;
case 'CiteResponsiveReferencesThreshold':
return 10;
// We can add more hardcoded keys based on testing needs
// but null is the default for keys unsupported in this mode.
default:
return null;
}
}
public function rtl(): bool {
$this->loadSiteData();
return $this->siteData['rtl'];
}
/** @inheritDoc */
public function langConverterEnabledBcp47( Bcp47Code $lang ): bool {
$this->loadSiteData();
return $this->langConverterEnabled[strtolower( $lang->toBcp47Code() )] ?? false;
}
public function script(): string {
$this->loadSiteData();
return $this->siteData['script'];
}
public function scriptpath(): string {
$this->loadSiteData();
return $this->siteData['scriptpath'];
}
public function server(): string {
$this->loadSiteData();
return $this->siteData['server'];
}
/**
* @inheritDoc
*/
public function exportMetadataToHeadBcp47(
Document $document,
ContentMetadataCollector $metadata,
string $defaultTitle,
Bcp47Code $lang
): void {
'@phan-var StubMetadataCollector $metadata'; // @var StubMetadataCollector $metadata
$moduleLoadURI = $this->server() . $this->scriptpath() . '/load.php';
// Parsoid/JS always made this protocol-relative, so match
// that (for now at least)
$moduleLoadURI = preg_replace( '#^https?://#', '//', $moduleLoadURI );
// Look for a displaytitle.
$displayTitle = $metadata->getPageProperty( 'displaytitle' ) ??
// Use the default title, properly escaped
Utils::escapeHtml( $defaultTitle );
$this->exportMetadataHelper(
$document,
$moduleLoadURI,
$metadata->getModules(),
$metadata->getModuleStyles(),
$metadata->getJsConfigVars(),
$displayTitle,
$lang
);
}
public function redirectRegexp(): string {
$this->loadSiteData();
return $this->savedRedirectRegexp;
}
public function categoryRegexp(): string {
$this->loadSiteData();
return $this->savedCategoryRegexp;
}
public function bswRegexp(): string {
$this->loadSiteData();
return $this->savedBswRegexp;
}
public function timezoneOffset(): int {
$this->loadSiteData();
return $this->siteData['timeoffset'];
}
/** @inheritDoc */
public function variantsFor( Bcp47Code $lang ): ?array {
$this->loadSiteData();
return $this->variants[strtolower( $lang->toBcp47Code() )] ?? null;
}
public function widthOption(): int {
$this->loadSiteData();
return $this->widthOption;
}
/** @inheritDoc */
protected function getVariableIDs(): array {
$this->loadSiteData();
return $this->apiVariables;
}
/** @inheritDoc */
protected function haveComputedFunctionSynonyms(): bool {
return false;
}
private static $noHashFunctions = null;
/** @inheritDoc */
protected function updateFunctionSynonym( string $func, string $magicword, bool $caseSensitive ): void {
if ( !$this->apiFunctionHooks ) {
$this->loadSiteData();
}
if ( isset( $this->apiFunctionHooks[$magicword] ) ) {
if ( !self::$noHashFunctions ) {
// FIXME: This is an approximation only computed in non-integrated mode for
// commandline and developer testing. This set is probably not up to date
// and also doesn't reflect no-hash functions registered by extensions
// via setFunctionHook calls. As such, you might run into GOTCHAs during
// debugging of production issues in standalone / API config mode.
self::$noHashFunctions = PHPUtils::makeSet( [
'ns', 'nse', 'urlencode', 'lcfirst', 'ucfirst', 'lc', 'uc',
'localurl', 'localurle', 'fullurl', 'fullurle', 'canonicalurl',
'canonicalurle', 'formatnum', 'grammar', 'gender', 'plural', 'bidi',
'numberofpages', 'numberofusers', 'numberofactiveusers',
'numberofarticles', 'numberoffiles', 'numberofadmins',
'numberingroup', 'numberofedits', 'language',
'padleft', 'padright', 'anchorencode', 'defaultsort', 'filepath',
'pagesincategory', 'pagesize', 'protectionlevel', 'protectionexpiry',
'namespacee', 'namespacenumber', 'talkspace', 'talkspacee',
'subjectspace', 'subjectspacee', 'pagename', 'pagenamee',
'fullpagename', 'fullpagenamee', 'rootpagename', 'rootpagenamee',
'basepagename', 'basepagenamee', 'subpagename', 'subpagenamee',
'talkpagename', 'talkpagenamee', 'subjectpagename',
'subjectpagenamee', 'pageid', 'revisionid', 'revisionday',
'revisionday2', 'revisionmonth', 'revisionmonth1', 'revisionyear',
'revisiontimestamp', 'revisionuser', 'cascadingsources',
// Special callbacks in core
'namespace', 'int', 'displaytitle', 'pagesinnamespace',
] );
}
$syn = $func;
if ( substr( $syn, -1 ) === ':' ) {
$syn = substr( $syn, 0, -1 );
}
if ( !isset( self::$noHashFunctions[$magicword] ) ) {
$syn = '#' . $syn;
}
$this->functionSynonyms[intval( $caseSensitive )][$syn] = $magicword;
}
}
/** @inheritDoc */
protected function getMagicWords(): array {
$this->loadSiteData();
return $this->apiMagicWords;
}
/** @inheritDoc */
public function getMagicWordMatcher( string $id ): string {
$this->loadSiteData();
return $this->allMWs[$id] ?? '/^(?!)$/';
}
/** @inheritDoc */
public function getParameterizedAliasMatcher( array $words ): callable {
$this->loadSiteData();
$regexes = array_intersect_key( $this->paramMWs, array_flip( $words ) );
return static function ( $text ) use ( $regexes ) {
/**
* $name is the canonical magic word name
* $re has patterns for matching aliases
*/
foreach ( $regexes as $name => $re ) {
if ( preg_match( $re, $text, $m ) ) {
unset( $m[0] );
// Ex. regexp here is, /^(?:(?:|vinculo\=(.*?)|enlace\=(.*?)|link\=(.*?)))$/uS
// Check all the capture groups for a value, if not, it's safe to return an
// empty string since we did get a match.
foreach ( $m as $v ) {
if ( $v !== '' ) {
return [ 'k' => $name, 'v' => $v ];
}
}
return [ 'k' => $name, 'v' => '' ];
}
}
return null;
};
}
/**
* This function is public so it can be used to synchronize env for
* hybrid parserTests. The parserTests setup includes the definition
* of a number of non-standard extension tags, whose names are passed
* over from the JS side in hybrid testing.
* @param string $tag Name of an extension tag assumed to be present
*/
public function ensureExtensionTag( string $tag ): void {
$this->loadSiteData();
$this->extensionTags[mb_strtolower( $tag )] = true;
}
/** @inheritDoc */
protected function getNonNativeExtensionTags(): array {
$this->loadSiteData();
return $this->extensionTags;
}
/** @inheritDoc */
public function getMaxTemplateDepth(): int {
// Not in the API result
return $this->maxDepth;
}
/** @inheritDoc */
protected function getSpecialNSAliases(): array {
$nsAliases = [
'Special',
];
foreach ( $this->nsIds as $name => $id ) {
if ( $id === -1 ) {
$nsAliases[] = $this->quoteTitleRe( $name, '!' );
}
}
return $nsAliases;
}
/** @inheritDoc */
protected function getSpecialPageAliases( string $specialPage ): array {
$spAliases = [ $specialPage ];
foreach ( $this->specialPageAliases as $special ) {
if ( $special['realname'] === $specialPage ) {
$spAliases = array_merge( $spAliases, $special['aliases'] );
break;
}
}
return $spAliases;
}
/** @inheritDoc */
protected function getProtocols(): array {
$this->loadSiteData();
return $this->protocols;
}
/** @var ?MockMetrics */
private $metrics;
/** @inheritDoc */
public function metrics(): ?StatsdDataFactoryInterface {
if ( $this->metrics === null ) {
$this->metrics = new MockMetrics();
}
return $this->metrics;
}
/**
* Increment a counter metric
* @param string $name
* @param array $labels
* @param float $amount
* @return void
*/
public function incrementCounter( string $name, array $labels, float $amount = 1 ): void {
// We don't use the labels for now, using MockMetrics instead
$this->metrics->increment( $name );
}
/**
* Record a timing metric
* @param string $name
* @param float $value
* @param array $labels
* @return void
*/
public function observeTiming( string $name, float $value, array $labels ): void {
// We don't use the labels for now, using MockMetrics instead
$this->metrics->timing( $name, $value );
}
/** @inheritDoc */
public function getNoFollowConfig(): array {
$this->loadSiteData();
return [
'nofollow' => $this->siteData['nofollowlinks'] ?? true,
'nsexceptions' => $this->siteData['nofollownsexceptions'] ?? [],
'domainexceptions' => $this->siteData['nofollowdomainexceptions'] ?? [ 'mediawiki.org' ]
];
}
/** @inheritDoc */
public function getExternalLinkTarget() {
$this->loadSiteData();
return $this->siteData['externallinktarget'] ?? false;
}
}

File Metadata

Mime Type
text/x-php
Expires
Sat, May 16, 19:48 (4 h, 3 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
73/15/30b827f033b658a7444cf4a9ae07
Default Alt Text
SiteConfig.php (22 KB)

Event Timeline