Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4148014
SiteConfig.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
50 KB
Referenced Files
None
Subscribers
None
SiteConfig.php
View Options
<?php
declare
(
strict_types
=
1
);
namespace
Wikimedia\Parsoid\Config
;
use
Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface
;
use
Monolog\Formatter\LineFormatter
;
use
Monolog\Handler\ErrorLogHandler
;
use
Monolog\Handler\StreamHandler
;
use
Monolog\Logger
;
use
Psr\Container\ContainerInterface
;
use
Psr\Container\NotFoundExceptionInterface
;
use
Psr\Log\LoggerInterface
;
use
Psr\Log\LogLevel
;
use
Psr\Log\NullLogger
;
use
Wikimedia\Assert\Assert
;
use
Wikimedia\Bcp47Code\Bcp47Code
;
use
Wikimedia\ObjectFactory\ObjectFactory
;
use
Wikimedia\Parsoid\Core\ContentMetadataCollector
;
use
Wikimedia\Parsoid\Core\ContentModelHandler
;
use
Wikimedia\Parsoid\Core\LinkTarget
;
use
Wikimedia\Parsoid\DOM\Document
;
use
Wikimedia\Parsoid\Ext\AnnotationStripper
;
use
Wikimedia\Parsoid\Ext\ExtensionModule
;
use
Wikimedia\Parsoid\Ext\ExtensionTagHandler
;
use
Wikimedia\Parsoid\Ext\Gallery\Gallery
;
use
Wikimedia\Parsoid\Ext\Indicator\Indicator
;
use
Wikimedia\Parsoid\Ext\JSON\JSON
;
use
Wikimedia\Parsoid\Ext\LST\LST
;
use
Wikimedia\Parsoid\Ext\Nowiki\Nowiki
;
use
Wikimedia\Parsoid\Ext\Pre\Pre
;
use
Wikimedia\Parsoid\Utils\DOMUtils
;
use
Wikimedia\Parsoid\Utils\PHPUtils
;
use
Wikimedia\Parsoid\Utils\Utils
;
use
Wikimedia\Parsoid\Wikitext\Consts
;
/**
* Site-level configuration interface for Parsoid
*
* This includes both global configuration and wiki-level configuration.
*/
abstract
class
SiteConfig
{
/**
* FIXME: not private so that ParserTests can reset these variables
* since they reuse site config and other objects between tests for
* efficiency reasons.
*
* @var array|null
*/
protected
$mwAliases
;
/** @var array|null */
private
$behaviorSwitches
;
/** @var array|null */
private
$variables
;
/** @var array|null */
private
$mediaOptions
;
/** @var array|null */
protected
$functionSynonyms
;
/** @var string[] */
private
$protocolsRegexes
=
[];
/**
* FIXME: not private so that ParserTests can reset these variables
* since they reuse site config and other objects between tests for
* efficiency reasons.
* @var array|null
*/
protected
$interwikiMapNoNamespaces
;
/**
* FIXME: not private so that ParserTests can reset these variables
* since they reuse site config and other objects between tests for
* efficiency reasons.
* @var string|null|bool
*/
protected
$linkTrailRegex
=
false
;
/**
* These extension modules provide "core" functionality
* and their implementations live in the Parsoid repo.
*
* @var class-string<ExtensionModule>[]
*/
private
static
$coreExtModules
=
[
// content modules
JSON
::
class
,
// extension tags
Nowiki
::
class
,
Pre
::
class
,
Gallery
::
class
,
Indicator
::
class
,
// The following implementations will move to their own repositories
// soon, but for now are implemented in the Parsoid repo.
LST
::
class
];
/**
* Array specifying fully qualified class name for Parsoid-compatible extensions
* @var ?array<int,ExtensionModule>
*/
private
$extModules
=
null
;
/**
* Private counter to assign IDs to $extModules
* @var int
*/
private
$extModuleNextId
=
0
;
// phpcs:disable Generic.Files.LineLength.TooLong
/**
* Register a Parsoid extension module.
* @param string|array{name:string}|array{factory:callable}|array{class:class-string<ExtensionModule>} $configOrSpec
* Either an object factory specification for an ExtensionModule object,
* or else the configuration array which ExtensionModule::getConfig()
* would return. (The latter is preferred, but our internal extensions
* use the former.)
* @return int An integer identifier that can be passed to
* ::unregisterExtensionModule to remove this extension (
*/
final
public
function
registerExtensionModule
(
$configOrSpec
):
int
{
$this
->
getExtensionModules
();
// ensure it's initialized w/ core modules
if
(
is_string
(
$configOrSpec
)
||
isset
(
$configOrSpec
[
'class'
]
)
||
isset
(
$configOrSpec
[
'factory'
]
)
)
{
// Treat this as an object factory spec for an ExtensionModule
// ObjectFactory::createObject accepts an array, not just a callable (phan bug)
// @phan-suppress-next-line PhanTypeInvalidCallableArraySize
$module
=
$this
->
getObjectFactory
()->
createObject
(
$configOrSpec
,
[
'allowClassName'
=>
true
,
'assertClass'
=>
ExtensionModule
::
class
,
]
);
}
else
{
// Treat this as a configuration array, create a new anonymous
// ExtensionModule object for it.
$module
=
new
class
(
$configOrSpec
)
implements
ExtensionModule
{
private
$config
;
/** @param array $config */
public
function
__construct
(
$config
)
{
$this
->
config
=
$config
;
}
/** @inheritDoc */
public
function
getConfig
():
array
{
return
$this
->
config
;
}
};
}
$extId
=
$this
->
extModuleNextId
++;
$this
->
extModules
[
$extId
]
=
$module
;
// remove cached extConfig to ensure this registration is picked up
$this
->
extConfig
=
null
;
return
$extId
;
}
// phpcs:enable Generic.Files.LineLength.TooLong
/**
* Unregister a Parsoid extension module. This is typically used
* only for testing purposes in order to reset a shared SiteConfig
* to its original configuration.
* @param int $extId The value returned by the call to
* ::registerExtensionModule()
*/
final
public
function
unregisterExtensionModule
(
int
$extId
):
void
{
unset
(
$this
->
extModules
[
$extId
]
);
$this
->
extConfig
=
null
;
// remove cached extConfig
}
/**
* Return the set of Parsoid extension modules associated with this
* SiteConfig.
*
* @return ExtensionModule[]
*/
final
public
function
getExtensionModules
()
{
if
(
$this
->
extModules
===
null
)
{
$this
->
extModules
=
[];
foreach
(
self
::
$coreExtModules
as
$m
)
{
$this
->
extModules
[
$this
->
extModuleNextId
++]
=
new
$m
();
}
}
return
array_values
(
$this
->
extModules
);
}
/** @var LoggerInterface|null */
protected
$logger
=
null
;
/** @var int */
protected
$iwMatcherBatchSize
=
4096
;
/** @var array|null */
protected
$iwMatcher
=
null
;
/** @var bool */
protected
$addHTMLTemplateParameters
=
false
;
/** @var bool */
protected
$scrubBidiChars
=
false
;
/** @var bool */
protected
$linterEnabled
=
false
;
/** @var ?array */
protected
$extConfig
=
null
;
/**
* Tag handlers for some extensions currently explicit call unstripNowiki
* first thing in their handlers. They do this to strip <nowiki>..</nowiki>
* wrappers around args when encountered in the {{#tag:...}} parser function.
* However, this strategy won't work for Parsoid which calls the preprocessor
* to get expanded wikitext. In this mode, <nowiki> wrappers won't be stripped
* and this leads to functional differences in parsing and output.
*
* See T203293 and T299103 for more details.
*
* To get around this, T299103 proposes that extensions that require this support
* set a config flag in their Parsoid extension config. On the Parsoid end, we
* then let the legacy parser know of these tags. When such extension tags are
* encountered in the {{#tag:...}} parser function handler (see tagObj function
* in CoreParserFunctions.php), that handler can than automatically strip these
* nowiki wrappers on behalf of the extension.
*
* This serves two purposes. For one, it lets Parsoid support these extensions
* in this nowiki use edge case. For another, extensions that register handlers
* with Parsoid can get rid of explicit calls to unstripNowiki() in the
* tag handlers for the legacy parser.
*
* This property maintains an array of tags that need this support.
*
* @var array an associative array of tag names
*/
private
$t299103Tags
=
[];
/**
* Base constructor.
*
* This constructor is public because it is used to create mock objects
* in our test suite.
*/
public
function
__construct
()
{
}
/************************************************************************//**
* @name Global config
* @{
*/
/**
* General log channel
* @return LoggerInterface
*/
public
function
getLogger
():
LoggerInterface
{
if
(
$this
->
logger
===
null
)
{
$this
->
logger
=
new
NullLogger
;
}
return
$this
->
logger
;
}
/**
* Set the log channel, for debugging
* @param ?LoggerInterface $logger
*/
public
function
setLogger
(
?
LoggerInterface
$logger
):
void
{
$this
->
logger
=
$logger
;
}
/**
* Default gallery options for this wiki.
* @return array<string,string|int|bool>
*/
public
function
galleryOptions
():
array
{
return
[
'imagesPerRow'
=>
0
,
'imageWidth'
=>
120
,
'imageHeight'
=>
120
,
'captionLength'
=>
true
,
'showBytes'
=>
true
,
'showDimensions'
=>
true
,
'mode'
=>
'traditional'
,
];
}
/**
* When processing template parameters, parse them to HTML and add it to the
* template parameters data.
* @return bool
*/
public
function
addHTMLTemplateParameters
():
bool
{
return
$this
->
addHTMLTemplateParameters
;
}
/**
* Statistics aggregator, for counting and timing.
*
* @return StatsdDataFactoryInterface|null
*/
public
function
metrics
():
?
StatsdDataFactoryInterface
{
return
null
;
}
/**
* Increment a counter metric
* @param string $name
* @param array $labels
* @param float $amount
* @return void
*/
abstract
public
function
incrementCounter
(
string
$name
,
array
$labels
,
float
$amount
=
1
);
/**
* Record a timing metric.
*
* Note that the value should be provided in *milliseconds* even though
* the name of the metric may end (by convention) in `_seconds`. The
* metrics infrastructure will make the appropriate conversion.
*
* @param string $name
* @param float $value A timing value *in milliseconds*
* @param array $labels
* @return void
*/
abstract
public
function
observeTiming
(
string
$name
,
float
$value
,
array
$labels
);
/**
* If enabled, bidi chars adjacent to category links will be stripped
* in the html -> wt serialization pass.
* @return bool
*/
public
function
scrubBidiChars
():
bool
{
return
$this
->
scrubBidiChars
;
}
/** @} */
/************************************************************************//**
* @name Wiki config
* @{
*/
/**
* Allowed external image URL prefixes.
*
* @return string[] The empty array matches no URLs. The empty string matches
* all URLs.
*/
abstract
public
function
allowedExternalImagePrefixes
():
array
;
/**
* Site base URI
*
* This would be the URI found in `<base href="..." />`.
*
* @return string
*/
abstract
public
function
baseURI
():
string
;
/**
* Prefix for relative links
*
* Prefix to prepend to a page title to link to that page.
* Intended to be relative to the URI returned by baseURI().
*
* If possible, keep the default "./" so clients need not know this value
* to extract titles from link hrefs.
*
* @return string
*/
public
function
relativeLinkPrefix
():
string
{
return
'./'
;
}
/**
* Regex matching all double-underscore magic words
* @return string
*/
public
function
bswPagePropRegexp
():
string
{
static
$bswPagePropRegexp
=
null
;
if
(
$bswPagePropRegexp
===
null
)
{
$bswRegexp
=
$this
->
bswRegexp
();
$bswPagePropRegexp
=
'@(?:^|
\\
s)mw:PageProp/(?:'
.
PHPUtils
::
reStrip
(
$bswRegexp
,
'@'
)
.
')(?=$|
\\
s)@uDS'
;
}
return
$bswPagePropRegexp
;
}
/**
* Map a canonical namespace name to its index
*
* @note This replaces canonicalNamespaces
* @param string $name all-lowercase and with underscores rather than spaces.
* @return int|null
*/
abstract
public
function
canonicalNamespaceId
(
string
$name
):
?
int
;
/**
* Map a namespace name to its index
*
* @note This replaces canonicalNamespaces
* @param string $name all-lowercase and with underscores rather than spaces.
* @return int|null
*/
abstract
public
function
namespaceId
(
string
$name
):
?
int
;
/**
* Map a namespace index to its preferred name
* (with spaces, not underscores).
*
* @note This replaces namespaceNames
* @param int $ns
* @return string|null
*/
abstract
public
function
namespaceName
(
int
$ns
):
?
string
;
/**
* Test if a namespace has subpages
*
* @note This replaces namespacesWithSubpages
* @param int $ns
* @return bool
*/
abstract
public
function
namespaceHasSubpages
(
int
$ns
):
bool
;
/**
* Return namespace case setting
* @param int $ns
* @return string 'first-letter' or 'case-sensitive'
*/
abstract
public
function
namespaceCase
(
int
$ns
):
string
;
/**
* Test if a namespace is a talk namespace
*
* @note This replaces title.getNamespace().isATalkNamespace()
* @param int $ns
* @return bool
*/
public
function
namespaceIsTalk
(
int
$ns
):
bool
{
return
$ns
>
0
&&
$ns
%
2
;
}
/**
* Uppercasing method for titles
* @param string $str
* @return string
*/
public
function
ucfirst
(
string
$str
):
string
{
$o
=
ord
(
$str
);
if
(
$o
<
96
)
{
// if already uppercase...
return
$str
;
}
elseif
(
$o
<
128
)
{
if
(
$str
[
0
]
===
'i'
&&
in_array
(
$this
->
langBcp47
()->
toBcp47Code
(),
[
'az'
,
'tr'
,
'kaa'
,
'kk'
],
true
)
)
{
return
'İ'
.
mb_substr
(
$str
,
1
);
}
return
ucfirst
(
$str
);
// use PHP's ucfirst()
}
else
{
// fall back to more complex logic in case of multibyte strings
$char
=
mb_substr
(
$str
,
0
,
1
);
return
mb_strtoupper
(
$char
)
.
mb_substr
(
$str
,
1
);
}
}
/**
* Get the default local name for a special page
* @param string $alias Special page alias
* @return string|null
*/
abstract
public
function
specialPageLocalName
(
string
$alias
):
?
string
;
/**
* Treat language links as magic connectors, not inline links
* @return bool
*/
abstract
public
function
interwikiMagic
():
bool
;
/**
* Return true if the specified magic link syntax is enabled on this
* wiki.
* @param string $which One of "ISBN", "PMID", or "RFC"
* @return true if the specified magic link type is enabled on this wiki
*/
public
function
magicLinkEnabled
(
string
$which
):
bool
{
// This should be an abstract method, but in order to provide
// graceful upgrades, start by defaulting to true for all link types
return
true
;
}
/**
* Interwiki link data.
*
* Note that the order of the keys in this array is significant: if more
* than one prefix matches a given URL during html2wt conversion, the
* *first* match is used. If you want `wikitech` to be used instead of
* `labsconsole`, for example, the `'wikitech'=>[....]` key needs to
* enumerate first.
*
* @return array<string,array> Keys are interwiki prefixes, values are arrays with the following keys:
* - prefix: (string) The interwiki prefix, same as the key.
* - url: (string) Target URL, containing a '$1' to be replaced by the interwiki target.
* - protorel: (bool, optional) Whether the url may be accessed by both http:// and https://.
* - local: (bool, optional) Whether the interwiki link is considered local (to the wikifarm).
* - localinterwiki: (bool, optional) Whether the interwiki link points to the current wiki.
* - language: (bool, optional) Whether the interwiki link is a language link.
* - extralanglink: (bool, optional) Whether the interwiki link is an "extra language link".
* - linktext: (string, optional) For "extra language links", the link text.
* (booleans marked "optional" must be omitted if false)
*/
abstract
public
function
interwikiMap
():
array
;
/**
* Interwiki link data, after removing items that conflict with namespace names.
* (In case of such conflict, namespace wins, interwiki is ignored.)
* @return array<string,array> See interwikiMap()
*/
public
function
interwikiMapNoNamespaces
():
array
{
if
(
$this
->
interwikiMapNoNamespaces
===
null
)
{
$this
->
interwikiMapNoNamespaces
=
[];
foreach
(
$this
->
interwikiMap
()
as
$key
=>
$value
)
{
if
(
$this
->
namespaceId
(
(
string
)
$key
)
===
null
)
{
$this
->
interwikiMapNoNamespaces
[
$key
]
=
$value
;
}
}
}
return
$this
->
interwikiMapNoNamespaces
;
}
/**
* Match interwiki URLs
* @param string $href Link to match against
* @return string[]|null Two values [ string $key, string $target ] on success, null on no match.
*/
public
function
interwikiMatcher
(
string
$href
):
?
array
{
if
(
$this
->
iwMatcher
===
null
)
{
$keys
=
[
[],
[]
];
$patterns
=
[
[],
[]
];
foreach
(
$this
->
interwikiMapNoNamespaces
()
as
$key
=>
$iw
)
{
$key
=
(
string
)
$key
;
$lang
=
(
int
)(
!
empty
(
$iw
[
'language'
]
)
);
$url
=
$iw
[
'url'
];
$protocolRelative
=
substr
(
$url
,
0
,
2
)
===
'//'
;
if
(
!
empty
(
$iw
[
'protorel'
]
)
)
{
$url
=
preg_replace
(
'/^https?:/'
,
''
,
$url
);
$protocolRelative
=
true
;
}
// full-url match pattern
$keys
[
$lang
][]
=
$key
;
$patterns
[
$lang
][]
=
// Support protocol-relative URLs
(
$protocolRelative
?
'(?:https?:)?'
:
''
)
// Convert placeholder to group match
.
strtr
(
preg_quote
(
$url
,
'/'
),
[
'
\\
$1'
=>
'(.*?)'
]
);
if
(
!
empty
(
$iw
[
'local'
]
)
)
{
// ./$interwikiPrefix:$title and
// $interwikiPrefix%3A$title shortcuts
// are recognized and the local wiki forwards
// these shortcuts to the remote wiki
$keys
[
$lang
][]
=
$key
;
$patterns
[
$lang
][]
=
'^
\\
.
\\
/'
.
$iw
[
'prefix'
]
.
':(.*?)'
;
$keys
[
$lang
][]
=
$key
;
$patterns
[
$lang
][]
=
'^'
.
$iw
[
'prefix'
]
.
'%3A(.*?)'
;
}
}
// Prefer language matches over non-language matches
$numLangs
=
count
(
$keys
[
1
]
);
$keys
=
array_merge
(
$keys
[
1
],
$keys
[
0
]
);
$patterns
=
array_merge
(
$patterns
[
1
],
$patterns
[
0
]
);
// Chunk patterns into reasonably sized regexes
$this
->
iwMatcher
=
[];
$batchStart
=
0
;
$batchLen
=
0
;
foreach
(
$patterns
as
$i
=>
$pat
)
{
$len
=
strlen
(
$pat
);
if
(
$i
!==
$batchStart
&&
$batchLen
+
$len
>
$this
->
iwMatcherBatchSize
)
{
$this
->
iwMatcher
[]
=
[
array_slice
(
$keys
,
$batchStart
,
$i
-
$batchStart
),
'/^(?:'
.
implode
(
'|'
,
array_slice
(
$patterns
,
$batchStart
,
$i
-
$batchStart
)
)
.
')$/Di'
,
$numLangs
-
$batchStart
,
];
$batchStart
=
$i
;
$batchLen
=
$len
;
}
else
{
$batchLen
+=
$len
;
}
}
$i
=
count
(
$patterns
);
if
(
$i
>
$batchStart
)
{
$this
->
iwMatcher
[]
=
[
array_slice
(
$keys
,
$batchStart
,
$i
-
$batchStart
),
'/^(?:'
.
implode
(
'|'
,
array_slice
(
$patterns
,
$batchStart
,
$i
-
$batchStart
)
)
.
')$/Di'
,
$numLangs
-
$batchStart
,
];
}
}
foreach
(
$this
->
iwMatcher
as
[
$keys
,
$regex
,
$numLangs
]
)
{
if
(
preg_match
(
$regex
,
$href
,
$m
,
PREG_UNMATCHED_AS_NULL
)
)
{
foreach
(
$keys
as
$i
=>
$key
)
{
if
(
isset
(
$m
[
$i
+
1
]
)
)
{
if
(
$i
<
$numLangs
)
{
// Escape language interwikis with a colon
$key
=
':'
.
$key
;
}
return
[
$key
,
$m
[
$i
+
1
]
];
}
}
}
}
return
null
;
}
/**
* Wiki identifier, for cache keys.
* Should match a key in mwApiMap()?
* @return string
*/
abstract
public
function
iwp
():
string
;
/**
* Legal title characters
*
* Regex is intended to match bytes, not Unicode characters.
*
* @return string Regex character class (i.e. the bit that goes inside `[]`)
*/
abstract
public
function
legalTitleChars
():
string
;
/**
* Link prefix regular expression.
* @return string|null
*/
abstract
public
function
linkPrefixRegex
():
?
string
;
/**
* Return raw link trail regexp from config
* @return string
*/
abstract
protected
function
linkTrail
():
string
;
/**
* Link trail regular expression.
* @return string|null
*/
public
function
linkTrailRegex
():
?
string
{
if
(
$this
->
linkTrailRegex
===
false
)
{
$trail
=
$this
->
linkTrail
();
$trail
=
str_replace
(
'(.*)$'
,
''
,
$trail
);
if
(
strpos
(
$trail
,
'()'
)
!==
false
)
{
// Empty regex from zh-hans
$this
->
linkTrailRegex
=
null
;
}
else
{
$this
->
linkTrailRegex
=
$trail
;
}
}
return
$this
->
linkTrailRegex
;
}
/**
* Wiki language code.
* @return Bcp47Code BCP-47 language code
*/
abstract
public
function
langBcp47
():
Bcp47Code
;
/**
* Main page title, as LinkTarget
* @return LinkTarget
*/
abstract
public
function
mainPageLinkTarget
():
LinkTarget
;
/**
* Lookup config
* @param string $key
* @return mixed|null config value for $key, if present or null, if not.
* @deprecated This very broad interface is no longer needed.
*/
abstract
public
function
getMWConfigValue
(
string
$key
);
/**
* Whether the wiki language is right-to-left
* @return bool
*/
abstract
public
function
rtl
():
bool
;
/**
* Whether language converter is enabled for the specified language
* @param Bcp47Code $lang
* @return bool
*/
abstract
public
function
langConverterEnabledBcp47
(
Bcp47Code
$lang
):
bool
;
/**
* The URL path to index.php.
* @return string
*/
abstract
public
function
script
():
string
;
/**
* FIXME: This is only used to compute the modules path below
* and maybe shouldn't be exposed.
*
* The base wiki path
* @return string
*/
abstract
public
function
scriptpath
():
string
;
/**
* The base URL of the server.
* @return string
*/
abstract
public
function
server
():
string
;
/**
* Export content metadata via meta tags (and via a stylesheet
* for now to aid some clients).
*
* @param Document $document
* @param ContentMetadataCollector $metadata
* @param string $defaultTitle The default title to display, as an
* unescaped string
* @param Bcp47Code $lang a BCP-47 language code
*/
abstract
public
function
exportMetadataToHeadBcp47
(
Document
$document
,
ContentMetadataCollector
$metadata
,
string
$defaultTitle
,
Bcp47Code
$lang
):
void
;
/**
* Helper function to create <head> elements from metadata.
* @param Document $document
* @param string $modulesLoadURI
* @param string[] $modules
* @param string[] $moduleStyles
* @param array<string,mixed> $jsConfigVars
* @param string $htmlTitle The display title, as escaped HTML
* @param Bcp47Code $lang a Bcp47Code object
*/
protected
function
exportMetadataHelper
(
Document
$document
,
string
$modulesLoadURI
,
array
$modules
,
array
$moduleStyles
,
array
$jsConfigVars
,
string
$htmlTitle
,
Bcp47Code
$lang
):
void
{
// $htmlTitle contains the DISPLAYTITLE but it corresponds to the
// value of the ParserOutput *not* the ultimate value which would
// be used in the <h1> tag *nor* the plaintext value which would
// be used for the page <title>. OutputPage does additional
// validation/stripping on the displaytitle value before using it.
// As such we're going to just ignore $htmlTitle for now rather
// than report an incorrect value in the <head> (T324431).
// JsConfigVars
$content
=
null
;
try
{
if
(
$jsConfigVars
)
{
$content
=
PHPUtils
::
jsonEncode
(
$jsConfigVars
);
}
}
catch
(
\Exception
$e
)
{
// Similar to ResourceLoader::makeConfigSetScript. See T289358
$this
->
getLogger
()->
log
(
LogLevel
::
WARNING
,
'JSON serialization of config data failed. '
.
'This usually means the config data is not valid UTF-8.'
);
}
if
(
$content
)
{
DOMUtils
::
appendToHead
(
$document
,
'meta'
,
[
'property'
=>
'mw:jsConfigVars'
,
'content'
=>
$content
,
]
);
}
// Styles from modules returned from preprocessor / parse requests
if
(
$modules
)
{
// mw:generalModules can be processed via JS (and async) and are usually (but
// not always) JS scripts.
DOMUtils
::
appendToHead
(
$document
,
'meta'
,
[
'property'
=>
'mw:generalModules'
,
'content'
=>
implode
(
'|'
,
array_unique
(
$modules
)
)
]
);
}
// Styles from modules returned from preprocessor / parse requests
if
(
$moduleStyles
)
{
// mw:moduleStyles are CSS modules that are render-blocking.
DOMUtils
::
appendToHead
(
$document
,
'meta'
,
[
'property'
=>
'mw:moduleStyles'
,
'content'
=>
implode
(
'|'
,
array_unique
(
$moduleStyles
)
)
]
);
}
/*
* While unnecessary for Wikimedia clients, a stylesheet url in
* the <head> is useful for clients like Kiwix and others who
* might not want to process the meta tags to construct the
* resourceloader url.
*
* Given that these clients will be consuming Parsoid HTML outside
* a MediaWiki skin, the clients are effectively responsible for
* their own "skin". But, once again, as a courtesy, we are
* hardcoding the vector skin modules for them. But, note that
* this may cause page elements to render differently than how
* they render on Wikimedia sites with the vector skin since this
* is probably missing a number of other modules.
*
* All that said, note that JS-generated parts of the page will
* still require them to have more intimate knowledge of how to
* process the JS modules. Except for <graph>s, page content
* doesn't require JS modules at this point. So, where these
* clients want to invest in the necessary logic to construct a
* better resourceloader url, they could simply delete / ignore
* this stylesheet.
*/
$moreStyles
=
array_merge
(
$moduleStyles
,
[
'mediawiki.skinning.content.parsoid'
,
// Use the base styles that API output and fallback skin use.
'mediawiki.skinning.interface'
,
// Make sure to include contents of user generated styles
// e.g. MediaWiki:Common.css / MediaWiki:Mobile.css
'site.styles'
]
);
# need to use MW-internal language code for constructing resource
# loader path.
$langMw
=
Utils
::
bcp47ToMwCode
(
$lang
);
$styleURI
=
$modulesLoadURI
.
'?lang='
.
$langMw
.
'&modules='
.
PHPUtils
::
encodeURIComponent
(
implode
(
'|'
,
array_unique
(
$moreStyles
)
)
)
.
'&only=styles&skin=vector'
;
DOMUtils
::
appendToHead
(
$document
,
'link'
,
[
'rel'
=>
'stylesheet'
,
'href'
=>
$styleURI
]
);
}
/**
* A regexp matching the localized 'REDIRECT' marker for this wiki.
* The regexp should be delimited, but should not have boundary anchors
* or capture groups.
* @return string
*/
abstract
public
function
redirectRegexp
():
string
;
/**
* A regexp matching the localized 'Category' prefix for this wiki.
* The regexp should be delimited, but should not have boundary anchors
* or capture groups.
* @return string
*/
abstract
public
function
categoryRegexp
():
string
;
/**
* A regexp matching localized behavior switches for this wiki.
* The regexp should be delimited, but should not have boundary anchors
* or capture groups.
* @return string
*/
abstract
public
function
bswRegexp
():
string
;
/**
* A regex matching a line containing just whitespace, comments, and
* sol transparent links and behavior switches.
* @return string
*/
public
function
solTransparentWikitextRegexp
():
string
{
// cscott sadly says: Note that this depends on the precise
// localization of the magic words of this particular wiki.
static
$solTransparentWikitextRegexp
=
null
;
if
(
$solTransparentWikitextRegexp
===
null
)
{
$redirect
=
PHPUtils
::
reStrip
(
$this
->
redirectRegexp
(),
'@'
);
$category
=
PHPUtils
::
reStrip
(
$this
->
categoryRegexp
(),
'@'
);
$bswRegexp
=
PHPUtils
::
reStrip
(
$this
->
bswRegexp
(),
'@'
);
$comment
=
PHPUtils
::
reStrip
(
Utils
::
COMMENT_REGEXP
,
'@'
);
$solTransparentWikitextRegexp
=
'@'
.
'^[
\t\n\r\0\x
0b]*'
.
'(?:'
.
'(?:'
.
$redirect
.
')'
.
'[
\t\n\r\x
0c]*(?::[
\t\n\r\x
0c]*)?
\[\[
[^
\]
]+
\]\]
'
.
')?'
.
'(?:'
.
'
\[\[
'
.
$category
.
'
\:
[^
\]
]*?
\]\]
|'
.
'__(?:'
.
$bswRegexp
.
')__|'
.
$comment
.
'|'
.
'[
\t\n\r\0\x
0b]'
.
')*$@'
;
}
return
$solTransparentWikitextRegexp
;
}
/**
* A regex matching a line containing just comments and
* sol transparent links and behavior switches.
*
* @param bool $addIncludes
* @return string
*/
public
function
solTransparentWikitextNoWsRegexp
(
bool
$addIncludes
=
false
):
string
{
// cscott sadly says: Note that this depends on the precise
// localization of the magic words of this particular wiki.
static
$solTransparentWikitextNoWsRegexp
=
null
;
if
(
$solTransparentWikitextNoWsRegexp
===
null
)
{
$redirect
=
PHPUtils
::
reStrip
(
$this
->
redirectRegexp
(),
'@'
);
$category
=
PHPUtils
::
reStrip
(
$this
->
categoryRegexp
(),
'@'
);
$bswRegexp
=
PHPUtils
::
reStrip
(
$this
->
bswRegexp
(),
'@'
);
$comment
=
PHPUtils
::
reStrip
(
Utils
::
COMMENT_REGEXP
,
'@'
);
$solTransparentWikitextNoWsRegexp
=
'@'
.
'((?:'
.
'(?:'
.
$redirect
.
')'
.
'[
\t\n\r\x
0c]*(?::[
\t\n\r\x
0c]*)?
\[\[
[^
\]
]+
\]\]
'
.
')?'
.
'(?:'
.
'
\[\[
'
.
$category
.
'
\:
[^
\]
]*?
\]\]
|'
.
'__(?:'
.
$bswRegexp
.
')__|'
.
$comment
.
// FIXME(SSS): What about onlyinclude and noinclude?
(
$addIncludes
?
'|<includeonly>[
\S\s
]*?</includeonly>'
:
''
)
.
')*)@'
;
}
return
$solTransparentWikitextNoWsRegexp
;
}
/**
* The wiki's time zone offset
* @return int Minutes east of UTC
*/
abstract
public
function
timezoneOffset
():
int
;
/**
* Language variant information for the given language (or null if
* unknown).
* @param Bcp47Code $lang The language for which you want variant information
* @return ?array{base:Bcp47Code,fallbacks:Bcp47Code[]} an array with
* two fields:
* - base: (Bcp47Code) Base BCP-47 language code (e.g. "zh")
* - fallbacks: (Bcp47Code[]) Fallback variants, as BCP-47 codes
*/
abstract
public
function
variantsFor
(
Bcp47Code
$lang
):
?
array
;
/**
* Default thumbnail width
*/
abstract
public
function
widthOption
():
int
;
abstract
protected
function
getVariableIDs
():
array
;
abstract
protected
function
getMagicWords
():
array
;
/**
* Does the SiteConfig provide precomputed function synonyms?
* If no, the SiteConfig is expected to provide an implementation
* for updateFunctionSynonym.
*/
protected
function
haveComputedFunctionSynonyms
():
bool
{
return
true
;
}
/**
* Get a list of precomputed function synonyms
*/
protected
function
getFunctionSynonyms
():
array
{
return
[];
}
protected
function
updateFunctionSynonym
(
string
$func
,
string
$magicword
,
bool
$caseSensitive
):
void
{
throw
new
\RuntimeException
(
"Unexpected code path!"
);
}
private
function
populateMagicWords
()
{
if
(
!
empty
(
$this
->
mwAliases
)
)
{
return
;
}
$this
->
mwAliases
=
$this
->
behaviorSwitches
=
$this
->
variables
=
$this
->
mediaOptions
=
[];
$variablesMap
=
PHPUtils
::
makeSet
(
$this
->
getVariableIDs
()
);
$this
->
functionSynonyms
=
$this
->
getFunctionSynonyms
();
$haveSynonyms
=
$this
->
haveComputedFunctionSynonyms
();
foreach
(
$this
->
getMagicWords
()
as
$magicword
=>
$aliases
)
{
$caseSensitive
=
array_shift
(
$aliases
);
$isVariable
=
isset
(
$variablesMap
[
$magicword
]
);
$isMediaOption
=
preg_match
(
'/^(img|timedmedia)_/'
,
$magicword
);
foreach
(
$aliases
as
$alias
)
{
$this
->
mwAliases
[
$magicword
][]
=
$alias
;
if
(
!
$caseSensitive
)
{
$alias
=
mb_strtolower
(
$alias
);
$this
->
mwAliases
[
$magicword
][]
=
$alias
;
}
if
(
substr
(
$alias
,
0
,
2
)
===
'__'
)
{
$this
->
behaviorSwitches
[
$alias
]
=
[
$caseSensitive
,
$magicword
];
}
if
(
$isVariable
)
{
$this
->
variables
[
$alias
]
=
$magicword
;
}
if
(
$isMediaOption
)
{
$this
->
mediaOptions
[
$alias
]
=
[
$caseSensitive
,
$magicword
];
}
if
(
!
$haveSynonyms
)
{
$this
->
updateFunctionSynonym
(
$alias
,
$magicword
,
(
bool
)
$caseSensitive
);
}
}
}
}
/**
* List all magic words by canonical name
* @return string[][] Keys are canonical names, values are arrays of aliases.
*/
public
function
mwAliases
():
array
{
$this
->
populateMagicWords
();
return
$this
->
mwAliases
;
}
/**
* Return canonical magic word for a function hook
* @param string $str
* @return string|null
*/
public
function
getMagicWordForFunctionHook
(
string
$str
):
?
string
{
$this
->
populateMagicWords
();
return
$this
->
functionSynonyms
[
1
][
$str
]
??
# Case insensitive functions
$this
->
functionSynonyms
[
0
][
mb_strtolower
(
$str
)]
??
null
;
}
/**
* Return canonical magic word for a variable
* @param string $str
* @return string|null
*/
public
function
getMagicWordForVariable
(
string
$str
):
?
string
{
$this
->
populateMagicWords
();
return
$this
->
variables
[
$str
]
??
null
;
}
private
static
function
getMagicWordCanonicalName
(
array
$mws
,
string
$word
):
?
string
{
if
(
isset
(
$mws
[
$word
]
)
)
{
return
$mws
[
$word
][
1
];
}
$mw
=
$mws
[
mb_strtolower
(
$word
)]
??
null
;
return
(
$mw
&&
!
$mw
[
0
]
)
?
$mw
[
1
]
:
null
;
}
/**
* Return canonical magic word for a media option
* @param string $word
* @return string|null
*/
public
function
getMagicWordForMediaOption
(
string
$word
):
?
string
{
$this
->
populateMagicWords
();
return
self
::
getMagicWordCanonicalName
(
$this
->
mediaOptions
,
$word
);
}
/**
* Return canonical magic word for a behavior switch
* @param string $word
* @return string|null
*/
public
function
getMagicWordForBehaviorSwitch
(
string
$word
):
?
string
{
$this
->
populateMagicWords
();
return
self
::
getMagicWordCanonicalName
(
$this
->
behaviorSwitches
,
$word
);
}
/**
* Check if a string is a recognized behavior switch.
*
* @param string $word
* @return bool
*/
public
function
isBehaviorSwitch
(
string
$word
):
bool
{
return
$this
->
getMagicWordForBehaviorSwitch
(
$word
)
!==
null
;
}
/**
* Convert the internal canonical magic word name to the wikitext alias.
* @param string $word Canonical magic word name
* @param string $suggest Suggested alias (used as fallback and preferred choice)
* @return string
*/
public
function
getMagicWordWT
(
string
$word
,
string
$suggest
):
string
{
$aliases
=
$this
->
mwAliases
()[
$word
]
??
null
;
if
(
!
$aliases
)
{
return
$suggest
;
}
$ind
=
0
;
if
(
$suggest
)
{
$ind
=
array_search
(
$suggest
,
$aliases
,
true
);
}
return
$aliases
[
$ind
?:
0
];
}
/**
* Get a regexp matching a localized magic word, given its id.
*
* FIXME: misleading function name
*
* @param string $id
* @return string
*/
abstract
public
function
getMagicWordMatcher
(
string
$id
):
string
;
/**
* Get a matcher function for fetching values out of interpolated magic words,
* ie those with `$1` in their aliases.
*
* The matcher takes a string and returns null if it doesn't match any of
* the words, or an associative array if it did match:
* - k: The magic word that matched
* - v: The value of $1 that was matched
* (the JS also returned 'a' with the specific alias that matched, but that
* seems to be unused and so is omitted here)
*
* @param string[] $words Magic words to match
* @return callable
*/
abstract
protected
function
getParameterizedAliasMatcher
(
array
$words
):
callable
;
/**
* Get a matcher function for fetching values out of interpolated magic words
* which are media prefix options.
*
* The matcher takes a string and returns null if it doesn't match any of
* the words, or an associative array if it did match:
* - k: The magic word that matched
* - v: The value of $1 that was matched
* (the JS also returned 'a' with the specific alias that matched, but that
* seems to be unused and so is omitted here)
*
* @return callable
*/
final
public
function
getMediaPrefixParameterizedAliasMatcher
():
callable
{
// PORT-FIXME: this shouldn't be a constant, we should fetch these
// from the SiteConfig. Further, we probably need a hook here so
// Parsoid can handle media options defined in extensions... in
// particular timedmedia_* magic words from Extension:TimedMediaHandler
$mws
=
array_keys
(
Consts
::
$Media
[
'PrefixOptions'
]
);
return
$this
->
getParameterizedAliasMatcher
(
$mws
);
}
/**
* Get the maximum template depth
*
* @return int
*/
abstract
public
function
getMaxTemplateDepth
():
int
;
/**
* Return name spaces aliases for the NS_SPECIAL namespace
* @return array
*/
abstract
protected
function
getSpecialNSAliases
():
array
;
/**
* Return Special Page aliases for a special page name
* @param string $specialPage
* @return array
*/
abstract
protected
function
getSpecialPageAliases
(
string
$specialPage
):
array
;
/**
* Quote a title regex
*
* Assumes '/' as the delimiter, and replaces spaces or underscores with
* `[ _]` so either will be matched.
*
* @param string $s
* @param string $delimiter Defaults to '/'
* @return string
*/
protected
static
function
quoteTitleRe
(
string
$s
,
string
$delimiter
=
'/'
):
string
{
$s
=
preg_quote
(
$s
,
$delimiter
);
$s
=
strtr
(
$s
,
[
' '
=>
'[ _]'
,
'_'
=>
'[ _]'
,
]
);
return
$s
;
}
/**
* Matcher for ISBN/RFC/PMID URL patterns, returning the type and number.
*
* The match method takes a string and returns false on no match or a tuple
* like this on match: [ 'RFC', '12345' ]
*
* @return callable
*/
public
function
getExtResourceURLPatternMatcher
():
callable
{
$nsAliases
=
implode
(
'|'
,
array_unique
(
$this
->
getSpecialNSAliases
()
)
);
$pageAliases
=
implode
(
'|'
,
array_map
(
[
$this
,
'quoteTitleRe'
],
$this
->
getSpecialPageAliases
(
'Booksources'
)
)
);
$pats
=
[
'ISBN'
=>
'(?:
\.\.
?/)*(?i:'
.
$nsAliases
.
')(?:%3[Aa]|:)'
.
'(?i:'
.
$pageAliases
.
')(?:%2[Ff]|/)(?P<ISBN>
\d
+[Xx]?)'
,
'RFC'
=>
'[^/]*//tools
\.
ietf
\.
org/html/rfc(?P<RFC>
\w
+)'
,
'PMID'
=>
'[^/]*//www
\.
ncbi
\.
nlm
\.
nih
\.
gov/pubmed/(?P<PMID>
\w
+)
\?
dopt=Abstract'
,
];
// T145590: remove patterns for disabled magic links
foreach
(
array_keys
(
$pats
)
as
$v
)
{
if
(
!
$this
->
magicLinkEnabled
(
$v
)
)
{
unset
(
$pats
[
$v
]
);
}
}
$regex
=
'!^(?:'
.
implode
(
'|'
,
$pats
)
.
')$!'
;
return
static
function
(
$text
)
use
(
$pats
,
$regex
)
{
if
(
preg_match
(
$regex
,
$text
,
$m
)
)
{
foreach
(
$pats
as
$k
=>
$re
)
{
if
(
isset
(
$m
[
$k
]
)
&&
$m
[
$k
]
!==
''
)
{
return
[
$k
,
$m
[
$k
]
];
}
}
}
return
false
;
};
}
/**
* @return bool
*/
public
function
linterEnabled
():
bool
{
return
$this
->
linterEnabled
;
}
/**
* Return the desired linter configuration. These are heuristic values
* which have hardcoded defaults but could be overridden on a per-wiki
* basis.
* @return array{enabled?:string[],disabled?:string[],maxTableColumnHeuristic?:int,maxTableRowsToCheck?:int}
*/
public
function
getLinterSiteConfig
():
array
{
return
[
// Allow list for specific lint types.
// Takes precedence over block list.
'enabled'
=>
null
,
// Block list for specific lint types.
// Not used if an allow list is set.
'disabled'
=>
null
,
// The maximum columns in a table before the table is considered
// large
'maxTableColumnHeuristic'
=>
5
,
// The maximum rows (header or data) to be checked for the large
// table lint
// - If we consider the first N rows to be representative of the
// table, and the table is well-formed and uniform, it is
// sufficent to check the first N rows to check if the table is
// "large".
// - This heuristic is used together with the
// 'maxTableColumnHeuristic' to identify "large tables".
'maxTableRowsToCheck'
=>
10
,
// Max length of content covered by 'white-space:nowrap' CSS
// that we consider "safe" when Tidy is replaced. Beyond that,
// wikitext will have to be fixed up to manually insert whitespace
// at the right places. Length in bytes.
'tidyWhitespaceBugMaxLength'
=>
100
,
];
}
/**
* Serialize ISBN/RFC/PMID URL patterns
*
* @param string[] $match As returned by the getExtResourceURLPatternMatcher() matcher
* @param string $href Fallback link target, if $match is invalid.
* @param string $content Link text
* @return string
*/
public
function
makeExtResourceURL
(
array
$match
,
string
$href
,
string
$content
):
string
{
$normalized
=
preg_replace
(
'/[
\x
{00A0}
\x
{1680}
\x
{2000}-
\x
{200A}
\x
{202F}
\x
{205F}
\x
{3000}]+/u'
,
' '
,
Utils
::
decodeWtEntities
(
$content
)
);
// TODO: T145590 ("Update Parsoid to be compatible with magic links being disabled")
switch
(
$match
[
0
]
)
{
case
'ISBN'
:
$normalized
=
strtoupper
(
preg_replace
(
'/[
\-
\t
]/'
,
''
,
$normalized
)
);
// validate ISBN length and format, so as not to produce magic links
// which aren't actually magic
$valid
=
preg_match
(
'/^ISBN(97[89])?
\d
{9}(
\d
|X)$/D'
,
$normalized
);
if
(
implode
(
''
,
$match
)
===
$normalized
&&
$valid
)
{
return
$content
;
}
// strip "./" prefix. TODO: Use relativeLinkPrefix() instead?
$href
=
PHPUtils
::
stripPrefix
(
$href
,
'./'
);
return
"[[$href|$content]]"
;
case
'RFC'
:
case
'PMID'
:
$normalized
=
preg_replace
(
'/[
\t
]/'
,
''
,
$normalized
);
return
implode
(
''
,
$match
)
===
$normalized
?
$content
:
"[$href $content]"
;
default
:
throw
new
\InvalidArgumentException
(
"Invalid match type '{$match[0]}'"
);
}
}
/**
* Get the list of valid protocols
* @return array
*/
abstract
protected
function
getProtocols
():
array
;
/**
* Get a regex fragment matching URL protocols, quoted for an exclamation
* mark delimiter. The case-insensitive option should be used.
*
* @param bool $excludeProtRel Whether to exclude protocol-relative URLs
* @return string
*/
public
function
getProtocolsRegex
(
bool
$excludeProtRel
=
false
)
{
$excludeProtRel
=
(
int
)
$excludeProtRel
;
if
(
!
isset
(
$this
->
protocolsRegexes
[
$excludeProtRel
]
)
)
{
$parts
=
[];
foreach
(
$this
->
getProtocols
()
as
$protocol
)
{
if
(
!
$excludeProtRel
||
$protocol
!==
'//'
)
{
$parts
[]
=
preg_quote
(
$protocol
,
'!'
);
}
}
$this
->
protocolsRegexes
[
$excludeProtRel
]
=
implode
(
'|'
,
$parts
);
}
return
$this
->
protocolsRegexes
[
$excludeProtRel
];
}
/**
* Matcher for valid protocols, must be anchored at start of string.
* @param string $potentialLink
* @return bool Whether $potentialLink begins with a valid protocol
*/
public
function
hasValidProtocol
(
string
$potentialLink
):
bool
{
$re
=
'!^(?:'
.
$this
->
getProtocolsRegex
()
.
')!i'
;
return
(
bool
)
preg_match
(
$re
,
$potentialLink
);
}
/**
* Matcher for valid protocols, may occur at any point within string.
* @param string $potentialLink
* @return bool Whether $potentialLink contains a valid protocol
*/
public
function
findValidProtocol
(
string
$potentialLink
):
bool
{
$re
=
'!(?:
\W
|^)(?:'
.
$this
->
getProtocolsRegex
()
.
')!i'
;
return
(
bool
)
preg_match
(
$re
,
$potentialLink
);
}
/** @} */
/**
* Fake timestamp, for unit tests.
* @return int|null Unix timestamp, or null to not fake it
*/
public
function
fakeTimestamp
():
?
int
{
return
null
;
}
/**
* Get an array of defined extension tags, with the lower case name in the
* key, the value arbitrary. This is the set of extension tags that are
* configured in M/W core. $coreExtModules may already be part of it,
* but eventually this distinction will disappear since all extension tags
* have to be defined against the Parsoid's extension API.
*
* @return array
*/
abstract
protected
function
getNonNativeExtensionTags
():
array
;
/**
* Return an object factory to use when instantiating extensions.
* (This is assumed to be plumbed up to an appropriate service container.)
* @return ObjectFactory The object factory to use for extensions
*/
public
function
getObjectFactory
():
ObjectFactory
{
// Default implementation returns an object factory with an
// empty service container.
return
new
ObjectFactory
(
new
class
()
implements
ContainerInterface
{
/**
* @param string $id
* @return never
*/
public
function
get
(
$id
)
{
throw
new
class
(
"Empty service container"
)
extends
\Error
implements
NotFoundExceptionInterface
{
};
}
/**
* @param string $id
* @return false
*/
public
function
has
(
$id
):
bool
{
return
false
;
}
}
);
}
/**
* FIXME: might benefit from T250230 (caching) but see T270307 --
* currently SiteConfig::unregisterExtensionModule() is called
* during testing, which requires invalidating $this->extConfig.
* (See also SiteConfig::fakeTimestamp() etc.) We'd probably need
* to more fully separate/mock the "testing SiteConfig" as well
* as provide a way for parser options to en/disable individual
* registered modules before this class can be considered immutable
* and cached.
*/
private
function
constructExtConfig
()
{
$this
->
extConfig
=
[
'allTags'
=>
[],
'parsoidExtTags'
=>
[],
'annotationTags'
=>
[],
'domProcessors'
=>
[],
'annotationStrippers'
=>
[],
'contentModels'
=>
[],
];
// There may be some tags defined by the parent wiki which have no
// associated parsoid modules; for now we handle these by invoking
// the legacy parser.
$this
->
extConfig
[
'allTags'
]
=
$this
->
getNonNativeExtensionTags
();
foreach
(
$this
->
getExtensionModules
()
as
$module
)
{
$this
->
processExtensionModule
(
$module
);
}
}
/**
* @param string $lowerTagName
* @return bool
*/
public
function
tagNeedsNowikiStrippedInTagPF
(
string
$lowerTagName
):
bool
{
return
isset
(
$this
->
t299103Tags
[
$lowerTagName
]
);
}
/**
* Register a Parsoid-compatible extension
* @param ExtensionModule $ext
*/
protected
function
processExtensionModule
(
ExtensionModule
$ext
):
void
{
Assert
::
invariant
(
$this
->
extConfig
!==
null
,
"not yet inited!"
);
$extConfig
=
$ext
->
getConfig
();
Assert
::
invariant
(
isset
(
$extConfig
[
'name'
]
),
"Every extension module must have a name."
);
$name
=
$extConfig
[
'name'
];
// These are extension tag handlers. They have
// wt2html (sourceToDom), html2wt (domToWikitext), and
// linter functionality.
foreach
(
$extConfig
[
'tags'
]
??
[]
as
$tagConfig
)
{
$lowerTagName
=
mb_strtolower
(
$tagConfig
[
'name'
]
);
$this
->
extConfig
[
'allTags'
][
$lowerTagName
]
=
true
;
$this
->
extConfig
[
'parsoidExtTags'
][
$lowerTagName
]
=
$tagConfig
;
// Deal with b/c nowiki stripping support needed by some extensions.
// This register this tag with the legacy parser for
// implicit nowiki stripping in {{#tag:..}} args for this tag.
if
(
isset
(
$tagConfig
[
'options'
][
'stripNowiki'
]
)
)
{
$this
->
t299103Tags
[
$lowerTagName
]
=
true
;
}
}
if
(
isset
(
$extConfig
[
'annotations'
]
)
)
{
$annotationConfig
=
$extConfig
[
'annotations'
];
$annotationTags
=
$annotationConfig
[
'tagNames'
]
??
$annotationConfig
;
foreach
(
$annotationTags
??
[]
as
$aTag
)
{
$lowerTagName
=
mb_strtolower
(
$aTag
);
$this
->
extConfig
[
'allTags'
][
$lowerTagName
]
=
true
;
$this
->
extConfig
[
'annotationTags'
][
$lowerTagName
]
=
true
;
}
if
(
isset
(
$annotationConfig
[
'annotationStripper'
]
)
)
{
$obj
=
$this
->
getObjectFactory
()->
createObject
(
$annotationConfig
[
'annotationStripper'
],
[
'allowClassName'
=>
true
,
'assertClass'
=>
AnnotationStripper
::
class
,
]
);
$this
->
extConfig
[
'annotationStrippers'
][
$name
]
=
$obj
;
}
}
// Extension modules may also register dom processors.
// This is for wt2htmlPostProcessor and html2wtPreProcessor
// functionality.
if
(
isset
(
$extConfig
[
'domProcessors'
]
)
)
{
$this
->
extConfig
[
'domProcessors'
][
$name
]
=
$extConfig
[
'domProcessors'
];
}
foreach
(
$extConfig
[
'contentModels'
]
??
[]
as
$cm
=>
$spec
)
{
// For compatibility with mediawiki core, the first
// registered extension wins.
if
(
isset
(
$this
->
extConfig
[
'contentModels'
][
$cm
]
)
)
{
continue
;
}
$handler
=
$this
->
getObjectFactory
()->
createObject
(
$spec
,
[
'allowClassName'
=>
true
,
'assertClass'
=>
ContentModelHandler
::
class
,
]
);
$this
->
extConfig
[
'contentModels'
][
$cm
]
=
$handler
;
}
}
protected
function
getExtConfig
():
array
{
if
(
!
$this
->
extConfig
)
{
$this
->
constructExtConfig
();
}
return
$this
->
extConfig
;
}
/**
* Return a ContentModelHandler for the specified $contentmodel, if one is registered.
* If null is returned, will use the default wikitext content model handler.
*
* @param string $contentmodel
* @return ContentModelHandler|null
*/
public
function
getContentModelHandler
(
string
$contentmodel
):
?
ContentModelHandler
{
return
(
$this
->
getExtConfig
()
)[
'contentModels'
][
$contentmodel
]
??
null
;
}
/**
* Returns all the annotationStrippers that are defined as annotation configuration
* @return array<AnnotationStripper>
*/
public
function
getAnnotationStrippers
():
array
{
$res
=
$this
->
getExtConfig
()[
'annotationStrippers'
]
??
[];
// ensures stability of the method list order
ksort
(
$res
);
return
array_values
(
$res
);
}
/**
* Determine whether a given name, which must have already been converted
* to lower case, is a valid extension tag name.
*
* @param string $name
* @return bool
*/
public
function
isExtensionTag
(
string
$name
):
bool
{
return
isset
(
$this
->
getExtensionTagNameMap
()[
$name
]
);
}
/**
* @param string $tagName is $tagName an annotation tag?
* @return bool
*/
public
function
isAnnotationTag
(
string
$tagName
):
bool
{
return
$this
->
getExtConfig
()[
'annotationTags'
][
mb_strtolower
(
$tagName
)]
??
false
;
}
/**
* Get an array of defined annotation tags in lower case
* @return array
*/
public
function
getAnnotationTags
():
array
{
$extConfig
=
$this
->
getExtConfig
();
return
array_keys
(
$extConfig
[
'annotationTags'
]
);
}
/**
* Get an array of defined extension tags, with the lower case name
* in the key, and the value being arbitrary.
*
* @return array
*/
public
function
getExtensionTagNameMap
():
array
{
$extConfig
=
$this
->
getExtConfig
();
return
$extConfig
[
'allTags'
];
}
/**
* @param string $tagName Extension tag name
* @return array|null
*/
public
function
getExtTagConfig
(
string
$tagName
):
?
array
{
$extConfig
=
$this
->
getExtConfig
();
return
$extConfig
[
'parsoidExtTags'
][
mb_strtolower
(
$tagName
)]
??
null
;
}
private
$tagHandlerCache
=
[];
/**
* @param string $tagName Extension tag name
* @return ExtensionTagHandler|null
* Returns the implementation of the named extension, if there is one.
*/
public
function
getExtTagImpl
(
string
$tagName
):
?
ExtensionTagHandler
{
if
(
!
array_key_exists
(
$tagName
,
$this
->
tagHandlerCache
)
)
{
$tagConfig
=
$this
->
getExtTagConfig
(
$tagName
);
$this
->
tagHandlerCache
[
$tagName
]
=
isset
(
$tagConfig
[
'handler'
]
)
?
$this
->
getObjectFactory
()->
createObject
(
$tagConfig
[
'handler'
],
[
'allowClassName'
=>
true
,
'assertClass'
=>
ExtensionTagHandler
::
class
,
]
)
:
null
;
}
return
$this
->
tagHandlerCache
[
$tagName
];
}
/**
* Return an array mapping extension name to an array of object factory
* specs for Ext\DOMProcessor objects
* @return array
*/
public
function
getExtDOMProcessors
():
array
{
$extConfig
=
$this
->
getExtConfig
();
return
$extConfig
[
'domProcessors'
];
}
/** @var array<string,int> */
protected
$wt2htmlLimits
=
[
// We won't handle pages beyond this size
'wikitextSize'
=>
2048
*
1024
,
// ParserOptions::maxIncludeSize
// Max list items per page
'listItem'
=>
30000
,
// Max table cells per page
'tableCell'
=>
30000
,
// Max transclusions per page
'transclusion'
=>
10000
,
// DISABLED for now
// Max images per page
'image'
=>
1000
,
// Max top-level token size
'token'
=>
1000000
,
// 1M
];
/**
* @return array<string,int>
*/
public
function
getWt2HtmlLimits
():
array
{
return
$this
->
wt2htmlLimits
;
}
/** @var array<string,int> */
protected
$html2wtLimits
=
[
// We refuse to serialize HTML strings bigger than this
'htmlSize'
=>
10000000
,
// 10M
];
/**
* @return array<string,int>
*/
public
function
getHtml2WtLimits
():
array
{
return
$this
->
html2wtLimits
;
}
/**
* @param ?string $filePath File to log to (if null, logs to console)
* @return Logger
*/
public
static
function
createLogger
(
?
string
$filePath
=
null
):
Logger
{
// Use Monolog's PHP console handler
$logger
=
new
Logger
(
"Parsoid CLI"
);
$format
=
'%message%'
;
if
(
$filePath
)
{
$handler
=
new
StreamHandler
(
$filePath
);
$format
.=
"
\n
"
;
}
else
{
$handler
=
new
ErrorLogHandler
();
}
// Don't suppress inline newlines
$handler
->
setFormatter
(
new
LineFormatter
(
$format
,
null
,
true
)
);
$logger
->
pushHandler
(
$handler
);
if
(
$filePath
)
{
// Separator between logs since StreamHandler appends
$logger
->
log
(
Logger
::
INFO
,
"-------------- starting fresh log --------------"
);
}
return
$logger
;
}
abstract
public
function
getNoFollowConfig
():
array
;
/** @return string|false */
abstract
public
function
getExternalLinkTarget
();
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Aug 19 2026, 18:37 (6 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
ec/de/56a479cae87cf049ce3a711b2216
Default Alt Text
SiteConfig.php (50 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment