Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
70.77% |
494 / 698 |
|
36.36% |
32 / 88 |
CRAP | |
0.00% |
0 / 1 |
CoreParserFunctions | |
70.88% |
494 / 697 |
|
36.36% |
32 / 88 |
2321.95 | |
0.00% |
0 / 1 |
register | |
90.74% |
49 / 54 |
|
0.00% |
0 / 1 |
4.01 | |||
intFunction | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
formatDate | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
ns | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 | |||
nse | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
urlencode | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
6 | |||
lcfirst | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
ucfirst | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
lc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
uc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
localurl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
localurle | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
fullurl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fullurle | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
canonicalurl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canonicalurle | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
urlFunction | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
4.59 | |||
formatnum | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getLegacyFormatNum | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
grammar | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
gender | |
77.78% |
14 / 18 |
|
0.00% |
0 / 1 |
8.70 | |||
plural | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
formal | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
bidi | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
displaytitle | |
81.25% |
39 / 48 |
|
0.00% |
0 / 1 |
15.29 | |||
matchAgainstMagicword | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
formatRaw | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
12.41 | |||
numberofpages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
numberofusers | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
numberofactiveusers | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
numberofarticles | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
numberoffiles | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
numberofadmins | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
numberofedits | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pagesinnamespace | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
numberingroup | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
makeTitle | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
namespace | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
namespacee | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
namespacenumber | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
talkspace | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
talkspacee | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
subjectspace | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
subjectspacee | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
pagename | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
pagenamee | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
fullpagename | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
fullpagenamee | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
subpagename | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
subpagenamee | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
rootpagename | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
rootpagenamee | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
basepagename | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
basepagenamee | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
talkpagename | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
talkpagenamee | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
subjectpagename | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
subjectpagenamee | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
pagesincategory | |
44.74% |
17 / 38 |
|
0.00% |
0 / 1 |
15.27 | |||
pagesize | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
5.05 | |||
protectionlevel | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
protectionexpiry | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
language | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
dir | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
bcp47 | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
pad | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
5.01 | |||
padleft | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
padright | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
anchorencode | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
special | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
speciale | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
defaultsort | |
55.00% |
11 / 20 |
|
0.00% |
0 / 1 |
13.83 | |||
filepath | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
56 | |||
tagObj | |
62.50% |
20 / 32 |
|
0.00% |
0 / 1 |
15.27 | |||
getCachedRevisionObject | |
83.33% |
25 / 30 |
|
0.00% |
0 / 1 |
16.04 | |||
pageid | |
43.48% |
10 / 23 |
|
0.00% |
0 / 1 |
23.63 | |||
revisionid | |
61.54% |
16 / 26 |
|
0.00% |
0 / 1 |
30.57 | |||
getRevisionTimestampSubstring | |
77.78% |
14 / 18 |
|
0.00% |
0 / 1 |
6.40 | |||
revisionday | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
revisionday2 | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
revisionmonth | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
revisionmonth1 | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
revisionyear | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
revisiontimestamp | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
revisionuser | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
cascadingsources | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
interwikilink | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
4.00 | |||
interlanguagelink | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
5.01 |
1 | <?php |
2 | /** |
3 | * Parser functions provided by MediaWiki core |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup Parser |
22 | */ |
23 | |
24 | namespace MediaWiki\Parser; |
25 | |
26 | use InvalidArgumentException; |
27 | use MediaWiki\Category\Category; |
28 | use MediaWiki\Config\ServiceOptions; |
29 | use MediaWiki\Language\Language; |
30 | use MediaWiki\Language\LanguageCode; |
31 | use MediaWiki\Languages\LanguageNameUtils; |
32 | use MediaWiki\Linker\Linker; |
33 | use MediaWiki\MainConfigNames; |
34 | use MediaWiki\MediaWikiServices; |
35 | use MediaWiki\Message\Message; |
36 | use MediaWiki\Revision\RevisionAccessException; |
37 | use MediaWiki\Revision\RevisionRecord; |
38 | use MediaWiki\SiteStats\SiteStats; |
39 | use MediaWiki\SpecialPage\SpecialPage; |
40 | use MediaWiki\Title\Title; |
41 | use MediaWiki\Title\TitleValue; |
42 | use MediaWiki\User\User; |
43 | use Wikimedia\Bcp47Code\Bcp47CodeValue; |
44 | use Wikimedia\RemexHtml\Tokenizer\Attributes; |
45 | use Wikimedia\RemexHtml\Tokenizer\PlainAttributes; |
46 | |
47 | /** |
48 | * Various core parser functions, registered in every Parser |
49 | * @ingroup Parser |
50 | */ |
51 | class CoreParserFunctions { |
52 | /** @var int Assume that no output will later be saved this many seconds after parsing */ |
53 | private const MAX_TTS = 900; |
54 | |
55 | /** |
56 | * @internal |
57 | */ |
58 | public const REGISTER_OPTIONS = [ |
59 | // See documentation for the corresponding config options |
60 | MainConfigNames::AllowDisplayTitle, |
61 | MainConfigNames::AllowSlowParserFunctions, |
62 | ]; |
63 | |
64 | /** |
65 | * @param Parser $parser |
66 | * @param ServiceOptions $options |
67 | * |
68 | * @return void |
69 | * @internal |
70 | */ |
71 | public static function register( Parser $parser, ServiceOptions $options ) { |
72 | $options->assertRequiredOptions( self::REGISTER_OPTIONS ); |
73 | $allowDisplayTitle = $options->get( MainConfigNames::AllowDisplayTitle ); |
74 | $allowSlowParserFunctions = $options->get( MainConfigNames::AllowSlowParserFunctions ); |
75 | |
76 | # Syntax for arguments (see Parser::setFunctionHook): |
77 | # "name for lookup in localized magic words array", |
78 | # function callback, |
79 | # optional Parser::SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}} |
80 | # instead of {{#int:...}}) |
81 | $noHashFunctions = [ |
82 | 'ns', 'nse', 'urlencode', 'lcfirst', 'ucfirst', 'lc', 'uc', |
83 | 'localurl', 'localurle', 'fullurl', 'fullurle', 'canonicalurl', |
84 | 'canonicalurle', 'formatnum', 'grammar', 'gender', 'plural', 'formal', |
85 | 'bidi', 'numberingroup', 'language', |
86 | 'padleft', 'padright', 'anchorencode', 'defaultsort', 'filepath', |
87 | 'pagesincategory', 'pagesize', 'protectionlevel', 'protectionexpiry', |
88 | # The following are the "parser function" forms of magic |
89 | # variables defined in CoreMagicVariables. The no-args form will |
90 | # go through the magic variable code path (and be cached); the |
91 | # presence of arguments will cause the parser function form to |
92 | # be invoked. (Note that the actual implementation will pass |
93 | # a Parser object as first argument, in addition to the |
94 | # parser function parameters.) |
95 | |
96 | # For this group, the first parameter to the parser function is |
97 | # "page title", and the no-args form (and the magic variable) |
98 | # defaults to "current page title". |
99 | 'pagename', 'pagenamee', |
100 | 'fullpagename', 'fullpagenamee', |
101 | 'subpagename', 'subpagenamee', |
102 | 'rootpagename', 'rootpagenamee', |
103 | 'basepagename', 'basepagenamee', |
104 | 'talkpagename', 'talkpagenamee', |
105 | 'subjectpagename', 'subjectpagenamee', |
106 | 'pageid', 'revisionid', 'revisionday', |
107 | 'revisionday2', 'revisionmonth', 'revisionmonth1', 'revisionyear', |
108 | 'revisiontimestamp', |
109 | 'revisionuser', |
110 | 'cascadingsources', |
111 | 'namespace', 'namespacee', 'namespacenumber', 'talkspace', 'talkspacee', |
112 | 'subjectspace', 'subjectspacee', |
113 | |
114 | # More parser functions corresponding to CoreMagicVariables. |
115 | # For this group, the first parameter to the parser function is |
116 | # "raw" (uses the 'raw' format if present) and the no-args form |
117 | # (and the magic variable) defaults to 'not raw'. |
118 | 'numberofarticles', 'numberoffiles', |
119 | 'numberofusers', |
120 | 'numberofactiveusers', |
121 | 'numberofpages', |
122 | 'numberofadmins', |
123 | 'numberofedits', |
124 | |
125 | # These magic words already contain the hash, and the no-args form |
126 | # is the same as passing an empty first argument |
127 | 'bcp47', |
128 | 'dir', |
129 | 'interwikilink', |
130 | 'interlanguagelink', |
131 | ]; |
132 | foreach ( $noHashFunctions as $func ) { |
133 | $parser->setFunctionHook( $func, [ __CLASS__, $func ], Parser::SFH_NO_HASH ); |
134 | } |
135 | |
136 | $parser->setFunctionHook( 'int', [ __CLASS__, 'intFunction' ], Parser::SFH_NO_HASH ); |
137 | $parser->setFunctionHook( 'special', [ __CLASS__, 'special' ] ); |
138 | $parser->setFunctionHook( 'speciale', [ __CLASS__, 'speciale' ] ); |
139 | $parser->setFunctionHook( 'tag', [ __CLASS__, 'tagObj' ], Parser::SFH_OBJECT_ARGS ); |
140 | $parser->setFunctionHook( 'formatdate', [ __CLASS__, 'formatDate' ] ); |
141 | |
142 | if ( $allowDisplayTitle ) { |
143 | $parser->setFunctionHook( |
144 | 'displaytitle', |
145 | [ __CLASS__, 'displaytitle' ], |
146 | Parser::SFH_NO_HASH |
147 | ); |
148 | } |
149 | if ( $allowSlowParserFunctions ) { |
150 | $parser->setFunctionHook( |
151 | 'pagesinnamespace', |
152 | [ __CLASS__, 'pagesinnamespace' ], |
153 | Parser::SFH_NO_HASH |
154 | ); |
155 | } |
156 | } |
157 | |
158 | /** |
159 | * @param Parser $parser |
160 | * @param string $part1 Message key |
161 | * @param string ...$params To pass to wfMessage() |
162 | * @return array |
163 | */ |
164 | public static function intFunction( $parser, $part1 = '', ...$params ) { |
165 | if ( strval( $part1 ) !== '' ) { |
166 | $message = wfMessage( $part1, $params ) |
167 | ->inLanguage( $parser->getOptions()->getUserLangObj() ); |
168 | return [ $message->plain(), 'noparse' => false ]; |
169 | } else { |
170 | return [ 'found' => false ]; |
171 | } |
172 | } |
173 | |
174 | /** |
175 | * @param Parser $parser |
176 | * @param string $date |
177 | * @param string|null $defaultPref |
178 | * |
179 | * @return string |
180 | */ |
181 | public static function formatDate( $parser, $date, $defaultPref = null ) { |
182 | $lang = $parser->getTargetLanguage(); |
183 | $df = MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang ); |
184 | |
185 | $date = trim( $date ); |
186 | |
187 | $pref = $parser->getOptions()->getDateFormat(); |
188 | |
189 | // Specify a different default date format other than the normal default |
190 | // if the user has 'default' for their setting |
191 | if ( $pref == 'default' && $defaultPref ) { |
192 | $pref = $defaultPref; |
193 | } |
194 | |
195 | $date = $df->reformat( $pref, $date, [ 'match-whole' ] ); |
196 | return $date; |
197 | } |
198 | |
199 | public static function ns( $parser, $part1 = '' ) { |
200 | if ( intval( $part1 ) || $part1 == "0" ) { |
201 | $index = intval( $part1 ); |
202 | } else { |
203 | $index = $parser->getContentLanguage()->getNsIndex( str_replace( ' ', '_', $part1 ) ); |
204 | } |
205 | if ( $index !== false ) { |
206 | return $parser->getContentLanguage()->getFormattedNsText( $index ); |
207 | } else { |
208 | return [ 'found' => false ]; |
209 | } |
210 | } |
211 | |
212 | public static function nse( $parser, $part1 = '' ) { |
213 | $ret = self::ns( $parser, $part1 ); |
214 | if ( is_string( $ret ) ) { |
215 | $ret = wfUrlencode( str_replace( ' ', '_', $ret ) ); |
216 | } |
217 | return $ret; |
218 | } |
219 | |
220 | /** |
221 | * urlencodes a string according to one of three patterns: (T24474) |
222 | * |
223 | * By default (for HTTP "query" strings), spaces are encoded as '+'. |
224 | * Or to encode a value for the HTTP "path", spaces are encoded as '%20'. |
225 | * For links to "wiki"s, or similar software, spaces are encoded as '_', |
226 | * |
227 | * @param Parser $parser |
228 | * @param string $s The text to encode. |
229 | * @param string|null $arg (optional): The type of encoding. |
230 | * @return string |
231 | */ |
232 | public static function urlencode( $parser, $s = '', $arg = null ) { |
233 | static $magicWords = null; |
234 | if ( $magicWords === null ) { |
235 | $magicWords = |
236 | $parser->getMagicWordFactory()->newArray( [ 'url_path', 'url_query', 'url_wiki' ] ); |
237 | } |
238 | switch ( $magicWords->matchStartToEnd( $arg ?? '' ) ) { |
239 | // Encode as though it's a wiki page, '_' for ' '. |
240 | case 'url_wiki': |
241 | $func = 'wfUrlencode'; |
242 | $s = str_replace( ' ', '_', $s ); |
243 | break; |
244 | |
245 | // Encode for an HTTP Path, '%20' for ' '. |
246 | case 'url_path': |
247 | $func = 'rawurlencode'; |
248 | break; |
249 | |
250 | // Encode for HTTP query, '+' for ' '. |
251 | case 'url_query': |
252 | default: |
253 | $func = 'urlencode'; |
254 | } |
255 | // See T105242, where the choice to kill markers and various |
256 | // other options were discussed. |
257 | return $func( $parser->killMarkers( $s ) ); |
258 | } |
259 | |
260 | public static function lcfirst( $parser, $s = '' ) { |
261 | return $parser->getContentLanguage()->lcfirst( $s ); |
262 | } |
263 | |
264 | public static function ucfirst( $parser, $s = '' ) { |
265 | return $parser->getContentLanguage()->ucfirst( $s ); |
266 | } |
267 | |
268 | /** |
269 | * @param Parser $parser |
270 | * @param string $s |
271 | * @return string |
272 | */ |
273 | public static function lc( $parser, $s = '' ) { |
274 | return $parser->markerSkipCallback( $s, [ $parser->getContentLanguage(), 'lc' ] ); |
275 | } |
276 | |
277 | /** |
278 | * @param Parser $parser |
279 | * @param string $s |
280 | * @return string |
281 | */ |
282 | public static function uc( $parser, $s = '' ) { |
283 | return $parser->markerSkipCallback( $s, [ $parser->getContentLanguage(), 'uc' ] ); |
284 | } |
285 | |
286 | public static function localurl( $parser, $s = '', $arg = null ) { |
287 | return self::urlFunction( 'getLocalURL', $s, $arg ); |
288 | } |
289 | |
290 | public static function localurle( $parser, $s = '', $arg = null ) { |
291 | $temp = self::urlFunction( 'getLocalURL', $s, $arg ); |
292 | if ( !is_string( $temp ) ) { |
293 | return $temp; |
294 | } else { |
295 | return htmlspecialchars( $temp, ENT_COMPAT ); |
296 | } |
297 | } |
298 | |
299 | public static function fullurl( $parser, $s = '', $arg = null ) { |
300 | return self::urlFunction( 'getFullURL', $s, $arg ); |
301 | } |
302 | |
303 | public static function fullurle( $parser, $s = '', $arg = null ) { |
304 | $temp = self::urlFunction( 'getFullURL', $s, $arg ); |
305 | if ( !is_string( $temp ) ) { |
306 | return $temp; |
307 | } else { |
308 | return htmlspecialchars( $temp, ENT_COMPAT ); |
309 | } |
310 | } |
311 | |
312 | public static function canonicalurl( $parser, $s = '', $arg = null ) { |
313 | return self::urlFunction( 'getCanonicalURL', $s, $arg ); |
314 | } |
315 | |
316 | public static function canonicalurle( $parser, $s = '', $arg = null ) { |
317 | $temp = self::urlFunction( 'getCanonicalURL', $s, $arg ); |
318 | if ( !is_string( $temp ) ) { |
319 | return $temp; |
320 | } else { |
321 | return htmlspecialchars( $temp, ENT_COMPAT ); |
322 | } |
323 | } |
324 | |
325 | public static function urlFunction( $func, $s = '', $arg = null ) { |
326 | # Due to order of execution of a lot of bits, the values might be encoded |
327 | # before arriving here; if that's true, then the title can't be created |
328 | # and the variable will fail. If we can't get a decent title from the first |
329 | # attempt, url-decode and try for a second. |
330 | $title = Title::newFromText( $s ) ?? Title::newFromURL( urldecode( $s ) ); |
331 | if ( $title !== null ) { |
332 | # Convert NS_MEDIA -> NS_FILE |
333 | if ( $title->inNamespace( NS_MEDIA ) ) { |
334 | $title = Title::makeTitle( NS_FILE, $title->getDBkey() ); |
335 | } |
336 | if ( $arg !== null ) { |
337 | $text = $title->$func( $arg ); |
338 | } else { |
339 | $text = $title->$func(); |
340 | } |
341 | return $text; |
342 | } else { |
343 | return [ 'found' => false ]; |
344 | } |
345 | } |
346 | |
347 | /** |
348 | * @param Parser $parser |
349 | * @param string $num |
350 | * @param string|null $arg |
351 | * @return string |
352 | */ |
353 | public static function formatnum( $parser, $num = '', $arg = null ) { |
354 | if ( self::matchAgainstMagicword( $parser->getMagicWordFactory(), 'rawsuffix', $arg ) ) { |
355 | $func = [ $parser->getTargetLanguage(), 'parseFormattedNumber' ]; |
356 | } elseif ( |
357 | self::matchAgainstMagicword( $parser->getMagicWordFactory(), 'nocommafysuffix', $arg ) |
358 | ) { |
359 | $func = [ $parser->getTargetLanguage(), 'formatNumNoSeparators' ]; |
360 | $func = self::getLegacyFormatNum( $parser, $func ); |
361 | } else { |
362 | $func = [ $parser->getTargetLanguage(), 'formatNum' ]; |
363 | $func = self::getLegacyFormatNum( $parser, $func ); |
364 | } |
365 | return $parser->markerSkipCallback( $num, $func ); |
366 | } |
367 | |
368 | /** |
369 | * @param Parser $parser |
370 | * @param callable $callback |
371 | * |
372 | * @return callable |
373 | */ |
374 | private static function getLegacyFormatNum( $parser, $callback ) { |
375 | // For historic reasons, the formatNum parser function will |
376 | // take arguments which are not actually formatted numbers, |
377 | // which then trigger deprecation warnings in Language::formatNum*. |
378 | // Instead emit a tracking category instead to allow linting. |
379 | return static function ( $number ) use ( $parser, $callback ) { |
380 | $validNumberRe = '(-(?=[\d\.]))?(\d+|(?=\.\d))(\.\d*)?([Ee][-+]?\d+)?'; |
381 | if ( |
382 | !is_numeric( $number ) && |
383 | $number !== (string)NAN && |
384 | $number !== (string)INF && |
385 | $number !== (string)-INF |
386 | ) { |
387 | $parser->addTrackingCategory( 'nonnumeric-formatnum' ); |
388 | // Don't split on NAN/INF in the legacy case since they are |
389 | // likely to be found embedded inside non-numeric text. |
390 | return preg_replace_callback( "/{$validNumberRe}/", static function ( $m ) use ( $callback ) { |
391 | return call_user_func( $callback, $m[0] ); |
392 | }, $number ); |
393 | } |
394 | return call_user_func( $callback, $number ); |
395 | }; |
396 | } |
397 | |
398 | /** |
399 | * @param Parser $parser |
400 | * @param string $case |
401 | * @param string $word |
402 | * @return string |
403 | */ |
404 | public static function grammar( $parser, $case = '', $word = '' ) { |
405 | $word = $parser->killMarkers( $word ); |
406 | return $parser->getTargetLanguage()->convertGrammar( $word, $case ); |
407 | } |
408 | |
409 | /** |
410 | * @param Parser $parser |
411 | * @param string $username |
412 | * @param string ...$forms What to output for each gender |
413 | * @return string |
414 | */ |
415 | public static function gender( $parser, $username, ...$forms ) { |
416 | // Some shortcuts to avoid loading user data unnecessarily |
417 | if ( count( $forms ) === 0 ) { |
418 | return ''; |
419 | } elseif ( count( $forms ) === 1 ) { |
420 | return $forms[0]; |
421 | } |
422 | |
423 | $username = trim( $username ); |
424 | |
425 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
426 | $gender = $userOptionsLookup->getDefaultOption( 'gender' ); |
427 | |
428 | // allow prefix and normalize (e.g. "*foo" -> "*foo" ). |
429 | $title = Title::newFromText( $username, NS_USER ); |
430 | |
431 | if ( $title && $title->inNamespace( NS_USER ) ) { |
432 | $username = $title->getText(); |
433 | } |
434 | |
435 | // check parameter, or use the ParserOptions if in interface message |
436 | $user = User::newFromName( $username ); |
437 | $genderCache = MediaWikiServices::getInstance()->getGenderCache(); |
438 | if ( $user ) { |
439 | $gender = $genderCache->getGenderOf( $user, __METHOD__ ); |
440 | } elseif ( $username === '' && $parser->getOptions()->getInterfaceMessage() ) { |
441 | $gender = $genderCache->getGenderOf( $parser->getOptions()->getUserIdentity(), __METHOD__ ); |
442 | } |
443 | $ret = $parser->getTargetLanguage()->gender( $gender, $forms ); |
444 | return $ret; |
445 | } |
446 | |
447 | /** |
448 | * @param Parser $parser |
449 | * @param string $text |
450 | * @param string ...$forms What to output for each number (singular, dual, plural, etc.) |
451 | * @return string |
452 | */ |
453 | public static function plural( $parser, $text = '', ...$forms ) { |
454 | $text = $parser->getTargetLanguage()->parseFormattedNumber( $text ); |
455 | settype( $text, ctype_digit( $text ) ? 'int' : 'float' ); |
456 | // @phan-suppress-next-line PhanTypeMismatchArgument Phan does not handle settype |
457 | return $parser->getTargetLanguage()->convertPlural( $text, $forms ); |
458 | } |
459 | |
460 | public static function formal( Parser $parser, string ...$forms ): string { |
461 | $index = $parser->getTargetLanguage()->getFormalityIndex(); |
462 | return $forms[$index] ?? $forms[0]; |
463 | } |
464 | |
465 | /** |
466 | * @param Parser $parser |
467 | * @param string $text |
468 | * @return string |
469 | */ |
470 | public static function bidi( $parser, $text = '' ) { |
471 | return $parser->getTargetLanguage()->embedBidi( $text ); |
472 | } |
473 | |
474 | /** |
475 | * Override the title of the page when viewed, provided we've been given a |
476 | * title which will normalise to the canonical title |
477 | * |
478 | * @param Parser $parser Parent parser |
479 | * @param string $text Desired title text |
480 | * @param string $uarg |
481 | * @return string |
482 | */ |
483 | public static function displaytitle( $parser, $text = '', $uarg = '' ) { |
484 | $restrictDisplayTitle = MediaWikiServices::getInstance()->getMainConfig() |
485 | ->get( MainConfigNames::RestrictDisplayTitle ); |
486 | |
487 | static $magicWords = null; |
488 | if ( $magicWords === null ) { |
489 | $magicWords = $parser->getMagicWordFactory()->newArray( |
490 | [ 'displaytitle_noerror', 'displaytitle_noreplace' ] ); |
491 | } |
492 | $arg = $magicWords->matchStartToEnd( $uarg ); |
493 | |
494 | // parse a limited subset of wiki markup (just the single quote items) |
495 | $text = $parser->doQuotes( $text ); |
496 | |
497 | // remove stripped text (e.g. the UNIQ-QINU stuff) that was generated by tag extensions/whatever |
498 | $text = $parser->killMarkers( $text ); |
499 | |
500 | // See T28547 for rationale for this processing. |
501 | // list of disallowed tags for DISPLAYTITLE |
502 | // these will be escaped even though they are allowed in normal wiki text |
503 | $bad = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'blockquote', 'ol', 'ul', 'li', 'hr', |
504 | 'table', 'tr', 'th', 'td', 'dl', 'dd', 'caption', 'p', 'ruby', 'rb', 'rt', 'rtc', 'rp', 'br' ]; |
505 | |
506 | // disallow some styles that could be used to bypass $wgRestrictDisplayTitle |
507 | if ( $restrictDisplayTitle ) { |
508 | // This code is tested with the cases marked T28547 in |
509 | // parserTests.txt |
510 | $htmlTagsCallback = static function ( Attributes $attr ): Attributes { |
511 | $decoded = $attr->getValues(); |
512 | |
513 | if ( isset( $decoded['style'] ) ) { |
514 | // this is called later anyway, but we need it right now for the regexes below to be safe |
515 | // calling it twice doesn't hurt |
516 | $decoded['style'] = Sanitizer::checkCss( $decoded['style'] ); |
517 | |
518 | if ( preg_match( '/(display|user-select|visibility)\s*:/i', $decoded['style'] ) ) { |
519 | $decoded['style'] = '/* attempt to bypass $wgRestrictDisplayTitle */'; |
520 | } |
521 | } |
522 | |
523 | return new PlainAttributes( $decoded ); |
524 | }; |
525 | } else { |
526 | $htmlTagsCallback = null; |
527 | } |
528 | |
529 | // only requested titles that normalize to the actual title are allowed through |
530 | // if $wgRestrictDisplayTitle is true (it is by default) |
531 | // mimic the escaping process that occurs in OutputPage::setPageTitle |
532 | $text = Sanitizer::removeSomeTags( $text, [ |
533 | 'attrCallback' => $htmlTagsCallback, |
534 | 'removeTags' => $bad, |
535 | ] ); |
536 | $title = Title::newFromText( Sanitizer::stripAllTags( $text ) ); |
537 | // Decode entities in $text the same way that Title::newFromText does |
538 | $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text ); |
539 | |
540 | if ( !$restrictDisplayTitle || |
541 | ( $title instanceof Title |
542 | && !$title->hasFragment() |
543 | && $title->equals( $parser->getTitle() ) ) |
544 | ) { |
545 | $old = $parser->getOutput()->getPageProperty( 'displaytitle' ); |
546 | if ( $old === null || $arg !== 'displaytitle_noreplace' ) { |
547 | $parser->getOutput()->setDisplayTitle( $text ); |
548 | } |
549 | if ( $old !== null && $old !== $text && !$arg ) { |
550 | |
551 | $converter = $parser->getTargetLanguageConverter(); |
552 | return '<span class="error">' . |
553 | $parser->msg( 'duplicate-displaytitle', |
554 | // Message should be parsed, but these params should only be escaped. |
555 | $converter->markNoConversion( wfEscapeWikiText( $old ) ), |
556 | $converter->markNoConversion( wfEscapeWikiText( $filteredText ) ) |
557 | )->text() . |
558 | '</span>'; |
559 | } else { |
560 | return ''; |
561 | } |
562 | } else { |
563 | $parser->getOutput()->addWarningMsg( |
564 | 'restricted-displaytitle', |
565 | // Message should be parsed, but this param should only be escaped. |
566 | Message::plaintextParam( $filteredText ) |
567 | ); |
568 | $parser->addTrackingCategory( 'restricted-displaytitle-ignored' ); |
569 | } |
570 | } |
571 | |
572 | /** |
573 | * Matches the given value against the value of given magic word |
574 | * |
575 | * @param MagicWordFactory $magicWordFactory A factory to get the word from, e.g., from |
576 | * $parser->getMagicWordFactory() |
577 | * @param string $magicword Magic word key |
578 | * @param string $value Value to match |
579 | * @return bool True on successful match |
580 | */ |
581 | private static function matchAgainstMagicword( |
582 | MagicWordFactory $magicWordFactory, $magicword, $value |
583 | ) { |
584 | $value = trim( strval( $value ) ); |
585 | if ( $value === '' ) { |
586 | return false; |
587 | } |
588 | $mwObject = $magicWordFactory->get( $magicword ); |
589 | return $mwObject->matchStartToEnd( $value ); |
590 | } |
591 | |
592 | /** |
593 | * Formats a number according to a language. |
594 | * |
595 | * @param int|float $num |
596 | * @param ?string $raw |
597 | * @param Language $language |
598 | * @param MagicWordFactory|null $magicWordFactory To evaluate $raw |
599 | * @return string |
600 | */ |
601 | public static function formatRaw( |
602 | $num, $raw, $language, ?MagicWordFactory $magicWordFactory = null |
603 | ) { |
604 | if ( $raw !== null && $raw !== '' ) { |
605 | if ( !$magicWordFactory ) { |
606 | $magicWordFactory = MediaWikiServices::getInstance()->getMagicWordFactory(); |
607 | } |
608 | if ( self::matchAgainstMagicword( $magicWordFactory, 'rawsuffix', $raw ) ) { |
609 | return (string)$num; |
610 | } |
611 | } |
612 | return $language->formatNum( $num ); |
613 | } |
614 | |
615 | public static function numberofpages( $parser, $raw = null ) { |
616 | return self::formatRaw( SiteStats::pages(), $raw, $parser->getTargetLanguage() ); |
617 | } |
618 | |
619 | public static function numberofusers( $parser, $raw = null ) { |
620 | return self::formatRaw( SiteStats::users(), $raw, $parser->getTargetLanguage() ); |
621 | } |
622 | |
623 | public static function numberofactiveusers( $parser, $raw = null ) { |
624 | return self::formatRaw( SiteStats::activeUsers(), $raw, $parser->getTargetLanguage() ); |
625 | } |
626 | |
627 | public static function numberofarticles( $parser, $raw = null ) { |
628 | return self::formatRaw( SiteStats::articles(), $raw, $parser->getTargetLanguage() ); |
629 | } |
630 | |
631 | public static function numberoffiles( $parser, $raw = null ) { |
632 | return self::formatRaw( SiteStats::images(), $raw, $parser->getTargetLanguage() ); |
633 | } |
634 | |
635 | public static function numberofadmins( $parser, $raw = null ) { |
636 | return self::formatRaw( |
637 | SiteStats::numberingroup( 'sysop' ), |
638 | $raw, |
639 | $parser->getTargetLanguage() |
640 | ); |
641 | } |
642 | |
643 | public static function numberofedits( $parser, $raw = null ) { |
644 | return self::formatRaw( SiteStats::edits(), $raw, $parser->getTargetLanguage() ); |
645 | } |
646 | |
647 | public static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) { |
648 | return self::formatRaw( |
649 | SiteStats::pagesInNs( intval( $namespace ) ), |
650 | $raw, |
651 | $parser->getTargetLanguage() |
652 | ); |
653 | } |
654 | |
655 | public static function numberingroup( $parser, $name = '', $raw = null ) { |
656 | return self::formatRaw( |
657 | SiteStats::numberingroup( strtolower( $name ) ), |
658 | $raw, |
659 | $parser->getTargetLanguage() |
660 | ); |
661 | } |
662 | |
663 | /** |
664 | * Helper function for preprocessing an optional argument which represents |
665 | * a title. |
666 | * @param Parser $parser |
667 | * @param string|null $t if null, returns the Parser's Title |
668 | * for consistency with magic variable forms |
669 | * @return ?Title |
670 | */ |
671 | private static function makeTitle( Parser $parser, ?string $t ) { |
672 | if ( $t === null ) { |
673 | // For consistency with magic variable forms |
674 | $title = $parser->getTitle(); |
675 | } else { |
676 | $title = Title::newFromText( $t ); |
677 | } |
678 | return $title; |
679 | } |
680 | |
681 | /** |
682 | * Given a title, return the namespace name that would be given by the |
683 | * corresponding magic word |
684 | * @param Parser $parser |
685 | * @param string|null $title |
686 | * @return mixed|string |
687 | * @since 1.39 |
688 | */ |
689 | public static function namespace( $parser, $title = null ) { |
690 | $t = self::makeTitle( $parser, $title ); |
691 | if ( $t === null ) { |
692 | return ''; |
693 | } |
694 | return str_replace( '_', ' ', $t->getNsText() ); |
695 | } |
696 | |
697 | public static function namespacee( $parser, $title = null ) { |
698 | $t = self::makeTitle( $parser, $title ); |
699 | if ( $t === null ) { |
700 | return ''; |
701 | } |
702 | return wfUrlencode( $t->getNsText() ); |
703 | } |
704 | |
705 | public static function namespacenumber( $parser, $title = null ) { |
706 | $t = self::makeTitle( $parser, $title ); |
707 | if ( $t === null ) { |
708 | return ''; |
709 | } |
710 | return (string)$t->getNamespace(); |
711 | } |
712 | |
713 | public static function talkspace( $parser, $title = null ) { |
714 | $t = self::makeTitle( $parser, $title ); |
715 | if ( $t === null || !$t->canHaveTalkPage() ) { |
716 | return ''; |
717 | } |
718 | return str_replace( '_', ' ', $t->getTalkNsText() ); |
719 | } |
720 | |
721 | public static function talkspacee( $parser, $title = null ) { |
722 | $t = self::makeTitle( $parser, $title ); |
723 | if ( $t === null || !$t->canHaveTalkPage() ) { |
724 | return ''; |
725 | } |
726 | return wfUrlencode( $t->getTalkNsText() ); |
727 | } |
728 | |
729 | public static function subjectspace( $parser, $title = null ) { |
730 | $t = self::makeTitle( $parser, $title ); |
731 | if ( $t === null ) { |
732 | return ''; |
733 | } |
734 | return str_replace( '_', ' ', $t->getSubjectNsText() ); |
735 | } |
736 | |
737 | public static function subjectspacee( $parser, $title = null ) { |
738 | $t = self::makeTitle( $parser, $title ); |
739 | if ( $t === null ) { |
740 | return ''; |
741 | } |
742 | return wfUrlencode( $t->getSubjectNsText() ); |
743 | } |
744 | |
745 | /** |
746 | * Functions to get and normalize pagenames, corresponding to the magic words |
747 | * of the same names |
748 | * @param Parser $parser |
749 | * @param string|null $title |
750 | * @return string |
751 | */ |
752 | public static function pagename( $parser, $title = null ) { |
753 | $t = self::makeTitle( $parser, $title ); |
754 | if ( $t === null ) { |
755 | return ''; |
756 | } |
757 | return wfEscapeWikiText( $t->getText() ); |
758 | } |
759 | |
760 | public static function pagenamee( $parser, $title = null ) { |
761 | $t = self::makeTitle( $parser, $title ); |
762 | if ( $t === null ) { |
763 | return ''; |
764 | } |
765 | return wfEscapeWikiText( $t->getPartialURL() ); |
766 | } |
767 | |
768 | public static function fullpagename( $parser, $title = null ) { |
769 | $t = self::makeTitle( $parser, $title ); |
770 | if ( $t === null ) { |
771 | return ''; |
772 | } |
773 | return wfEscapeWikiText( $t->getPrefixedText() ); |
774 | } |
775 | |
776 | public static function fullpagenamee( $parser, $title = null ) { |
777 | $t = self::makeTitle( $parser, $title ); |
778 | if ( $t === null ) { |
779 | return ''; |
780 | } |
781 | return wfEscapeWikiText( $t->getPrefixedURL() ); |
782 | } |
783 | |
784 | public static function subpagename( $parser, $title = null ) { |
785 | $t = self::makeTitle( $parser, $title ); |
786 | if ( $t === null ) { |
787 | return ''; |
788 | } |
789 | return wfEscapeWikiText( $t->getSubpageText() ); |
790 | } |
791 | |
792 | public static function subpagenamee( $parser, $title = null ) { |
793 | $t = self::makeTitle( $parser, $title ); |
794 | if ( $t === null ) { |
795 | return ''; |
796 | } |
797 | return wfEscapeWikiText( $t->getSubpageUrlForm() ); |
798 | } |
799 | |
800 | public static function rootpagename( $parser, $title = null ) { |
801 | $t = self::makeTitle( $parser, $title ); |
802 | if ( $t === null ) { |
803 | return ''; |
804 | } |
805 | return wfEscapeWikiText( $t->getRootText() ); |
806 | } |
807 | |
808 | public static function rootpagenamee( $parser, $title = null ) { |
809 | $t = self::makeTitle( $parser, $title ); |
810 | if ( $t === null ) { |
811 | return ''; |
812 | } |
813 | return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getRootText() ) ) ); |
814 | } |
815 | |
816 | public static function basepagename( $parser, $title = null ) { |
817 | $t = self::makeTitle( $parser, $title ); |
818 | if ( $t === null ) { |
819 | return ''; |
820 | } |
821 | return wfEscapeWikiText( $t->getBaseText() ); |
822 | } |
823 | |
824 | public static function basepagenamee( $parser, $title = null ) { |
825 | $t = self::makeTitle( $parser, $title ); |
826 | if ( $t === null ) { |
827 | return ''; |
828 | } |
829 | return wfEscapeWikiText( wfUrlencode( str_replace( ' ', '_', $t->getBaseText() ) ) ); |
830 | } |
831 | |
832 | public static function talkpagename( $parser, $title = null ) { |
833 | $t = self::makeTitle( $parser, $title ); |
834 | if ( $t === null || !$t->canHaveTalkPage() ) { |
835 | return ''; |
836 | } |
837 | return wfEscapeWikiText( $t->getTalkPage()->getPrefixedText() ); |
838 | } |
839 | |
840 | public static function talkpagenamee( $parser, $title = null ) { |
841 | $t = self::makeTitle( $parser, $title ); |
842 | if ( $t === null || !$t->canHaveTalkPage() ) { |
843 | return ''; |
844 | } |
845 | return wfEscapeWikiText( $t->getTalkPage()->getPrefixedURL() ); |
846 | } |
847 | |
848 | public static function subjectpagename( $parser, $title = null ) { |
849 | $t = self::makeTitle( $parser, $title ); |
850 | if ( $t === null ) { |
851 | return ''; |
852 | } |
853 | return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedText() ); |
854 | } |
855 | |
856 | public static function subjectpagenamee( $parser, $title = null ) { |
857 | $t = self::makeTitle( $parser, $title ); |
858 | if ( $t === null ) { |
859 | return ''; |
860 | } |
861 | return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedURL() ); |
862 | } |
863 | |
864 | /** |
865 | * Return the number of pages, files or subcats in the given category, |
866 | * or 0 if it's nonexistent. This is an expensive parser function and |
867 | * can't be called too many times per page. |
868 | * @param Parser $parser |
869 | * @param string $name |
870 | * @param string $arg1 |
871 | * @param string $arg2 |
872 | * @return string |
873 | */ |
874 | public static function pagesincategory( $parser, $name = '', $arg1 = '', $arg2 = '' ) { |
875 | static $magicWords = null; |
876 | if ( $magicWords === null ) { |
877 | $magicWords = $parser->getMagicWordFactory()->newArray( [ |
878 | 'pagesincategory_all', |
879 | 'pagesincategory_pages', |
880 | 'pagesincategory_subcats', |
881 | 'pagesincategory_files' |
882 | ] ); |
883 | } |
884 | static $cache = []; |
885 | |
886 | // split the given option to its variable |
887 | if ( self::matchAgainstMagicword( $parser->getMagicWordFactory(), 'rawsuffix', $arg1 ) ) { |
888 | // {{pagesincategory:|raw[|type]}} |
889 | $raw = $arg1; |
890 | $type = $magicWords->matchStartToEnd( $arg2 ); |
891 | } else { |
892 | // {{pagesincategory:[|type[|raw]]}} |
893 | $type = $magicWords->matchStartToEnd( $arg1 ); |
894 | $raw = $arg2; |
895 | } |
896 | if ( !$type ) { // backward compatibility |
897 | $type = 'pagesincategory_all'; |
898 | } |
899 | |
900 | $title = Title::makeTitleSafe( NS_CATEGORY, $name ); |
901 | if ( !$title ) { # invalid title |
902 | return self::formatRaw( 0, $raw, $parser->getTargetLanguage() ); |
903 | } |
904 | $languageConverter = MediaWikiServices::getInstance() |
905 | ->getLanguageConverterFactory() |
906 | ->getLanguageConverter( $parser->getContentLanguage() ); |
907 | $languageConverter->findVariantLink( $name, $title, true ); |
908 | |
909 | // Normalize name for cache |
910 | $name = $title->getDBkey(); |
911 | |
912 | if ( !isset( $cache[$name] ) ) { |
913 | $category = Category::newFromTitle( $title ); |
914 | |
915 | $allCount = $subcatCount = $fileCount = $pageCount = 0; |
916 | if ( $parser->incrementExpensiveFunctionCount() ) { |
917 | $allCount = $category->getMemberCount(); |
918 | $subcatCount = $category->getSubcatCount(); |
919 | $fileCount = $category->getFileCount(); |
920 | $pageCount = $category->getPageCount( Category::COUNT_CONTENT_PAGES ); |
921 | } |
922 | $cache[$name]['pagesincategory_all'] = $allCount; |
923 | $cache[$name]['pagesincategory_pages'] = $pageCount; |
924 | $cache[$name]['pagesincategory_subcats'] = $subcatCount; |
925 | $cache[$name]['pagesincategory_files'] = $fileCount; |
926 | } |
927 | |
928 | $count = $cache[$name][$type]; |
929 | return self::formatRaw( $count, $raw, $parser->getTargetLanguage() ); |
930 | } |
931 | |
932 | /** |
933 | * Return the size of the given page, or 0 if it's nonexistent. This is an |
934 | * expensive parser function and can't be called too many times per page. |
935 | * |
936 | * @param Parser $parser |
937 | * @param string $page Name of page to check (Default: empty string) |
938 | * @param string|null $raw Should number be human readable with commas or just number |
939 | * @return string |
940 | */ |
941 | public static function pagesize( $parser, $page = '', $raw = null ) { |
942 | $title = Title::newFromText( $page ); |
943 | |
944 | if ( !is_object( $title ) || $title->isExternal() ) { |
945 | return self::formatRaw( 0, $raw, $parser->getTargetLanguage() ); |
946 | } |
947 | |
948 | // fetch revision from cache/database and return the value |
949 | $rev = self::getCachedRevisionObject( $parser, $title, ParserOutputFlags::VARY_REVISION_SHA1 ); |
950 | $length = $rev ? $rev->getSize() : 0; |
951 | if ( $length === null ) { |
952 | // We've had bugs where rev_len was not being recorded for empty pages, see T135414 |
953 | $length = 0; |
954 | } |
955 | return self::formatRaw( $length, $raw, $parser->getTargetLanguage() ); |
956 | } |
957 | |
958 | /** |
959 | * Returns the requested protection level for the current page. This |
960 | * is an expensive parser function and can't be called too many times |
961 | * per page, unless the protection levels/expiries for the given title |
962 | * have already been retrieved |
963 | * |
964 | * @param Parser $parser |
965 | * @param string $type |
966 | * @param string $title |
967 | * |
968 | * @return string |
969 | */ |
970 | public static function protectionlevel( $parser, $type = '', $title = '' ) { |
971 | $titleObject = Title::newFromText( $title ) ?? $parser->getTitle(); |
972 | $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore(); |
973 | if ( $restrictionStore->areRestrictionsLoaded( $titleObject ) || $parser->incrementExpensiveFunctionCount() ) { |
974 | $restrictions = $restrictionStore->getRestrictions( $titleObject, strtolower( $type ) ); |
975 | # RestrictionStore::getRestrictions returns an array, its possible it may have |
976 | # multiple values in the future |
977 | return implode( ',', $restrictions ); |
978 | } |
979 | return ''; |
980 | } |
981 | |
982 | /** |
983 | * Returns the requested protection expiry for the current page. This |
984 | * is an expensive parser function and can't be called too many times |
985 | * per page, unless the protection levels/expiries for the given title |
986 | * have already been retrieved |
987 | * |
988 | * @param Parser $parser |
989 | * @param string $type |
990 | * @param string $title |
991 | * |
992 | * @return string |
993 | */ |
994 | public static function protectionexpiry( $parser, $type = '', $title = '' ) { |
995 | $titleObject = Title::newFromText( $title ) ?? $parser->getTitle(); |
996 | $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore(); |
997 | if ( $restrictionStore->areRestrictionsLoaded( $titleObject ) || $parser->incrementExpensiveFunctionCount() ) { |
998 | // getRestrictionExpiry() returns null on invalid type; trying to |
999 | // match protectionlevel() function that returns empty string instead |
1000 | return $restrictionStore->getRestrictionExpiry( $titleObject, strtolower( $type ) ) ?? ''; |
1001 | } |
1002 | return ''; |
1003 | } |
1004 | |
1005 | /** |
1006 | * Gives language names. |
1007 | * @param Parser $parser |
1008 | * @param string $code Language code (of which to get name) |
1009 | * @param string $inLanguage Language code (in which to get name); |
1010 | * if missing or empty, the language's autonym will be returned. |
1011 | * @return string |
1012 | */ |
1013 | public static function language( $parser, $code = '', $inLanguage = '' ) { |
1014 | if ( $code === '' ) { |
1015 | $code = $parser->getTargetLanguage()->getCode(); |
1016 | } |
1017 | if ( $inLanguage === '' ) { |
1018 | $inLanguage = LanguageNameUtils::AUTONYMS; |
1019 | } |
1020 | $lang = MediaWikiServices::getInstance() |
1021 | ->getLanguageNameUtils() |
1022 | ->getLanguageName( $code, $inLanguage ); |
1023 | return $lang !== '' ? $lang : LanguageCode::bcp47( $code ); |
1024 | } |
1025 | |
1026 | /** |
1027 | * Gives direction of script of a language given a language code. |
1028 | * @param Parser $parser |
1029 | * @param string $code a language code. If missing, the parser target |
1030 | * language will be used. |
1031 | * @param string $arg If this optional argument matches the |
1032 | * `language_option_bcp47` magic word, the language code will be treated |
1033 | * as a BCP-47 code. |
1034 | * @return string 'rtl' if the language code is recognized as |
1035 | * right-to-left, otherwise returns 'ltr' |
1036 | */ |
1037 | public static function dir( Parser $parser, string $code = '', string $arg = '' ): string { |
1038 | static $magicWords = null; |
1039 | $languageFactory = MediaWikiServices::getInstance()->getLanguageFactory(); |
1040 | |
1041 | if ( $code === '' ) { |
1042 | $lang = $parser->getTargetLanguage(); |
1043 | } else { |
1044 | if ( $arg !== '' ) { |
1045 | if ( $magicWords === null ) { |
1046 | $magicWords = $parser->getMagicWordFactory()->newArray( [ 'language_option_bcp47' ] ); |
1047 | } |
1048 | if ( $magicWords->matchStartToEnd( $arg ) === 'language_option_bcp47' ) { |
1049 | // Prefer the BCP-47 interpretation of this code. |
1050 | $code = new Bcp47CodeValue( $code ); |
1051 | } |
1052 | } |
1053 | try { |
1054 | $lang = $languageFactory->getLanguage( $code ); |
1055 | } catch ( InvalidArgumentException $ex ) { |
1056 | $parser->addTrackingCategory( 'bad-language-code-category' ); |
1057 | return 'ltr'; |
1058 | } |
1059 | } |
1060 | return $lang->getDir(); |
1061 | } |
1062 | |
1063 | /** |
1064 | * Gives the BCP-47 code for a language given the mediawiki internal |
1065 | * language code. |
1066 | * @param Parser $parser |
1067 | * @param string $code a language code. If missing, the parser target |
1068 | * language will be used. |
1069 | * @return string the corresponding BCP-47 code |
1070 | */ |
1071 | public static function bcp47( Parser $parser, string $code = '' ): string { |
1072 | if ( $code === '' ) { |
1073 | return $parser->getTargetLanguage()->toBcp47Code(); |
1074 | } else { |
1075 | return LanguageCode::bcp47( $code ); |
1076 | } |
1077 | } |
1078 | |
1079 | /** |
1080 | * Unicode-safe str_pad with the restriction that $length is forced to be <= 500 |
1081 | * @param Parser $parser |
1082 | * @param string $string |
1083 | * @param string $length |
1084 | * @param string $padding |
1085 | * @param int $direction |
1086 | * @return string |
1087 | */ |
1088 | public static function pad( |
1089 | $parser, $string, $length, $padding = '0', $direction = STR_PAD_RIGHT |
1090 | ) { |
1091 | $padding = $parser->killMarkers( $padding ); |
1092 | $lengthOfPadding = mb_strlen( $padding ); |
1093 | if ( $lengthOfPadding == 0 ) { |
1094 | return $string; |
1095 | } |
1096 | |
1097 | # The remaining length to add counts down to 0 as padding is added |
1098 | $length = min( (int)$length, 500 ) - mb_strlen( $string ); |
1099 | if ( $length <= 0 ) { |
1100 | // Nothing to add |
1101 | return $string; |
1102 | } |
1103 | |
1104 | # $finalPadding is just $padding repeated enough times so that |
1105 | # mb_strlen( $string ) + mb_strlen( $finalPadding ) == $length |
1106 | $finalPadding = ''; |
1107 | while ( $length > 0 ) { |
1108 | # If $length < $lengthofPadding, truncate $padding so we get the |
1109 | # exact length desired. |
1110 | $finalPadding .= mb_substr( $padding, 0, $length ); |
1111 | $length -= $lengthOfPadding; |
1112 | } |
1113 | |
1114 | if ( $direction == STR_PAD_LEFT ) { |
1115 | return $finalPadding . $string; |
1116 | } else { |
1117 | return $string . $finalPadding; |
1118 | } |
1119 | } |
1120 | |
1121 | public static function padleft( $parser, $string = '', $length = 0, $padding = '0' ) { |
1122 | return self::pad( $parser, $string, $length, $padding, STR_PAD_LEFT ); |
1123 | } |
1124 | |
1125 | public static function padright( $parser, $string = '', $length = 0, $padding = '0' ) { |
1126 | return self::pad( $parser, $string, $length, $padding ); |
1127 | } |
1128 | |
1129 | /** |
1130 | * @param Parser $parser |
1131 | * @param string $text |
1132 | * @return string |
1133 | */ |
1134 | public static function anchorencode( $parser, $text ) { |
1135 | $text = $parser->killMarkers( $text ); |
1136 | $section = (string)substr( $parser->guessSectionNameFromWikiText( $text ), 1 ); |
1137 | return Sanitizer::safeEncodeAttribute( $section ); |
1138 | } |
1139 | |
1140 | public static function special( $parser, $text ) { |
1141 | [ $page, $subpage ] = MediaWikiServices::getInstance()->getSpecialPageFactory()-> |
1142 | resolveAlias( $text ); |
1143 | if ( $page ) { |
1144 | $title = SpecialPage::getTitleFor( $page, $subpage ); |
1145 | return $title->getPrefixedText(); |
1146 | } else { |
1147 | // unknown special page, just use the given text as its title, if at all possible |
1148 | $title = Title::makeTitleSafe( NS_SPECIAL, $text ); |
1149 | return $title ? $title->getPrefixedText() : self::special( $parser, 'Badtitle' ); |
1150 | } |
1151 | } |
1152 | |
1153 | public static function speciale( $parser, $text ) { |
1154 | return wfUrlencode( str_replace( ' ', '_', self::special( $parser, $text ) ) ); |
1155 | } |
1156 | |
1157 | /** |
1158 | * @param Parser $parser |
1159 | * @param string $text The sortkey to use |
1160 | * @param string $uarg Either "noreplace" or "noerror" (in en) |
1161 | * both suppress errors, and noreplace does nothing if |
1162 | * a default sortkey already exists. |
1163 | * @return string |
1164 | */ |
1165 | public static function defaultsort( $parser, $text, $uarg = '' ) { |
1166 | static $magicWords = null; |
1167 | if ( $magicWords === null ) { |
1168 | $magicWords = $parser->getMagicWordFactory()->newArray( |
1169 | [ 'defaultsort_noerror', 'defaultsort_noreplace' ] ); |
1170 | } |
1171 | $arg = $magicWords->matchStartToEnd( $uarg ); |
1172 | |
1173 | $text = trim( $text ); |
1174 | if ( strlen( $text ) == 0 ) { |
1175 | return ''; |
1176 | } |
1177 | $old = $parser->getOutput()->getPageProperty( 'defaultsort' ); |
1178 | if ( $old === null || $arg !== 'defaultsort_noreplace' ) { |
1179 | $parser->getOutput()->setPageProperty( 'defaultsort', $text ); |
1180 | } |
1181 | |
1182 | if ( $old === null || $old == $text || $arg ) { |
1183 | return ''; |
1184 | } else { |
1185 | $converter = $parser->getTargetLanguageConverter(); |
1186 | return '<span class="error">' . |
1187 | $parser->msg( 'duplicate-defaultsort', |
1188 | // Message should be parsed, but these params should only be escaped. |
1189 | $converter->markNoConversion( wfEscapeWikiText( $old ) ), |
1190 | $converter->markNoConversion( wfEscapeWikiText( $text ) ) |
1191 | )->text() . |
1192 | '</span>'; |
1193 | } |
1194 | } |
1195 | |
1196 | /** |
1197 | * Usage {{filepath|300}}, {{filepath|nowiki}}, {{filepath|nowiki|300}} |
1198 | * or {{filepath|300|nowiki}} or {{filepath|300px}}, {{filepath|200x300px}}, |
1199 | * {{filepath|nowiki|200x300px}}, {{filepath|200x300px|nowiki}}. |
1200 | * |
1201 | * @param Parser $parser |
1202 | * @param string $name |
1203 | * @param string $argA |
1204 | * @param string $argB |
1205 | * @return array|string |
1206 | */ |
1207 | public static function filepath( $parser, $name = '', $argA = '', $argB = '' ) { |
1208 | $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $name ); |
1209 | |
1210 | if ( $argA == 'nowiki' ) { |
1211 | // {{filepath: | option [| size] }} |
1212 | $isNowiki = true; |
1213 | $parsedWidthParam = $parser->parseWidthParam( $argB ); |
1214 | } else { |
1215 | // {{filepath: [| size [|option]] }} |
1216 | $parsedWidthParam = $parser->parseWidthParam( $argA ); |
1217 | $isNowiki = ( $argB == 'nowiki' ); |
1218 | } |
1219 | |
1220 | if ( $file ) { |
1221 | $url = $file->getFullUrl(); |
1222 | |
1223 | // If a size is requested... |
1224 | if ( count( $parsedWidthParam ) ) { |
1225 | $mto = $file->transform( $parsedWidthParam ); |
1226 | // ... and we can |
1227 | if ( $mto && !$mto->isError() ) { |
1228 | // ... change the URL to point to a thumbnail. |
1229 | $urlUtils = MediaWikiServices::getInstance()->getUrlUtils(); |
1230 | $url = $urlUtils->expand( $mto->getUrl(), PROTO_RELATIVE ) ?? false; |
1231 | } |
1232 | } |
1233 | if ( $isNowiki ) { |
1234 | return [ $url, 'nowiki' => true ]; |
1235 | } |
1236 | return $url; |
1237 | } else { |
1238 | return ''; |
1239 | } |
1240 | } |
1241 | |
1242 | /** |
1243 | * Parser function to extension tag adaptor |
1244 | * @param Parser $parser |
1245 | * @param PPFrame $frame |
1246 | * @param PPNode[] $args |
1247 | * @return string |
1248 | */ |
1249 | public static function tagObj( $parser, $frame, $args ) { |
1250 | if ( !count( $args ) ) { |
1251 | return ''; |
1252 | } |
1253 | $tagName = strtolower( trim( $frame->expand( array_shift( $args ) ) ) ); |
1254 | $processNowiki = $parser->tagNeedsNowikiStrippedInTagPF( $tagName ) ? PPFrame::PROCESS_NOWIKI : 0; |
1255 | |
1256 | if ( count( $args ) ) { |
1257 | $inner = $frame->expand( array_shift( $args ), $processNowiki ); |
1258 | } else { |
1259 | $inner = null; |
1260 | } |
1261 | |
1262 | $attributes = []; |
1263 | foreach ( $args as $arg ) { |
1264 | $bits = $arg->splitArg(); |
1265 | if ( strval( $bits['index'] ) === '' ) { |
1266 | $name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) ); |
1267 | $value = trim( $frame->expand( $bits['value'] ) ); |
1268 | if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) { |
1269 | $value = $m[1] ?? ''; |
1270 | } |
1271 | $attributes[$name] = $value; |
1272 | } |
1273 | } |
1274 | |
1275 | $stripList = $parser->getStripList(); |
1276 | if ( !in_array( $tagName, $stripList ) ) { |
1277 | // we can't handle this tag (at least not now), so just re-emit it as an ordinary tag |
1278 | $attrText = ''; |
1279 | foreach ( $attributes as $name => $value ) { |
1280 | $attrText .= ' ' . htmlspecialchars( $name ) . |
1281 | '="' . htmlspecialchars( $value, ENT_COMPAT ) . '"'; |
1282 | } |
1283 | if ( $inner === null ) { |
1284 | return "<$tagName$attrText/>"; |
1285 | } |
1286 | return "<$tagName$attrText>$inner</$tagName>"; |
1287 | } |
1288 | |
1289 | $params = [ |
1290 | 'name' => $tagName, |
1291 | 'inner' => $inner, |
1292 | 'attributes' => $attributes, |
1293 | 'close' => "</$tagName>", |
1294 | ]; |
1295 | return $parser->extensionSubstitution( $params, $frame ); |
1296 | } |
1297 | |
1298 | /** |
1299 | * Fetched the current revision of the given title and return this. |
1300 | * Will increment the expensive function count and |
1301 | * add a template link to get the value refreshed on changes. |
1302 | * For a given title, which is equal to the current parser title, |
1303 | * the RevisionRecord object from the parser is used, when that is the current one |
1304 | * |
1305 | * @param Parser $parser |
1306 | * @param Title $title |
1307 | * @param string $vary ParserOutput vary-* flag |
1308 | * @return RevisionRecord|null |
1309 | * @since 1.23 |
1310 | */ |
1311 | private static function getCachedRevisionObject( $parser, $title, $vary ) { |
1312 | if ( !$title ) { |
1313 | return null; |
1314 | } |
1315 | |
1316 | $revisionRecord = null; |
1317 | |
1318 | $isSelfReferential = $title->equals( $parser->getTitle() ); |
1319 | if ( $isSelfReferential ) { |
1320 | // Revision is for the same title that is currently being parsed. Only use the last |
1321 | // saved revision, regardless of Parser::getRevisionId() or fake revision injection |
1322 | // callbacks against the current title. |
1323 | |
1324 | // FIXME (T318278): the above is the intention, but doesn't |
1325 | // describe the actual current behavior of this code, since |
1326 | // ->isCurrent() for the last saved revision will return |
1327 | // false so we're going to fall through and end up calling |
1328 | // ->getCurrentRevisionRecordOfTitle(). |
1329 | $parserRevisionRecord = $parser->getRevisionRecordObject(); |
1330 | if ( $parserRevisionRecord && $parserRevisionRecord->isCurrent() ) { |
1331 | $revisionRecord = $parserRevisionRecord; |
1332 | } |
1333 | } |
1334 | |
1335 | $parserOutput = $parser->getOutput(); |
1336 | if ( !$revisionRecord ) { |
1337 | if ( |
1338 | !$parser->isCurrentRevisionOfTitleCached( $title ) && |
1339 | !$parser->incrementExpensiveFunctionCount() |
1340 | ) { |
1341 | return null; // not allowed |
1342 | } |
1343 | // Get the current revision, ignoring Parser::getRevisionId() being null/old |
1344 | $revisionRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title ); |
1345 | if ( !$revisionRecord ) { |
1346 | // Convert `false` error return to `null` |
1347 | $revisionRecord = null; |
1348 | } |
1349 | // Register dependency in templatelinks |
1350 | $parserOutput->addTemplate( |
1351 | $title, |
1352 | $revisionRecord ? $revisionRecord->getPageId() : 0, |
1353 | $revisionRecord ? $revisionRecord->getId() : 0 |
1354 | ); |
1355 | } |
1356 | |
1357 | if ( $isSelfReferential ) { |
1358 | wfDebug( __METHOD__ . ": used current revision, setting $vary" ); |
1359 | // Upon page save, the result of the parser function using this might change |
1360 | $parserOutput->setOutputFlag( $vary ); |
1361 | if ( $vary === ParserOutputFlags::VARY_REVISION_SHA1 && $revisionRecord ) { |
1362 | try { |
1363 | $sha1 = $revisionRecord->getSha1(); |
1364 | } catch ( RevisionAccessException $e ) { |
1365 | $sha1 = null; |
1366 | } |
1367 | $parserOutput->setRevisionUsedSha1Base36( $sha1 ); |
1368 | } |
1369 | } |
1370 | |
1371 | return $revisionRecord; |
1372 | } |
1373 | |
1374 | /** |
1375 | * Get the pageid of a specified page |
1376 | * @param Parser $parser |
1377 | * @param string|null $title Title to get the pageid from |
1378 | * @return int|null|string |
1379 | * @since 1.23 |
1380 | */ |
1381 | public static function pageid( $parser, $title = null ) { |
1382 | $t = self::makeTitle( $parser, $title ); |
1383 | if ( !$t ) { |
1384 | return ''; |
1385 | } elseif ( !$t->canExist() || $t->isExternal() ) { |
1386 | return 0; // e.g. special page or interwiki link |
1387 | } |
1388 | |
1389 | $parserOutput = $parser->getOutput(); |
1390 | |
1391 | if ( $t->equals( $parser->getTitle() ) ) { |
1392 | // Revision is for the same title that is currently being parsed. |
1393 | // Use the title from Parser in case a new page ID was injected into it. |
1394 | $parserOutput->setOutputFlag( ParserOutputFlags::VARY_PAGE_ID ); |
1395 | $id = $parser->getTitle()->getArticleID(); |
1396 | if ( $id ) { |
1397 | $parserOutput->setSpeculativePageIdUsed( $id ); |
1398 | } |
1399 | |
1400 | return $id; |
1401 | } |
1402 | |
1403 | // Check the link cache for the title |
1404 | $linkCache = MediaWikiServices::getInstance()->getLinkCache(); |
1405 | $pdbk = $t->getPrefixedDBkey(); |
1406 | $id = $linkCache->getGoodLinkID( $pdbk ); |
1407 | if ( $id != 0 || $linkCache->isBadLink( $pdbk ) ) { |
1408 | $parserOutput->addLink( $t, $id ); |
1409 | |
1410 | return $id; |
1411 | } |
1412 | |
1413 | // We need to load it from the DB, so mark expensive |
1414 | if ( $parser->incrementExpensiveFunctionCount() ) { |
1415 | $id = $t->getArticleID(); |
1416 | $parserOutput->addLink( $t, $id ); |
1417 | |
1418 | return $id; |
1419 | } |
1420 | |
1421 | return null; |
1422 | } |
1423 | |
1424 | /** |
1425 | * Get the id from the last revision of a specified page. |
1426 | * @param Parser $parser |
1427 | * @param string|null $title Title to get the id from |
1428 | * @return int|null|string |
1429 | * @since 1.23 |
1430 | */ |
1431 | public static function revisionid( $parser, $title = null ) { |
1432 | $t = self::makeTitle( $parser, $title ); |
1433 | if ( $t === null || $t->isExternal() ) { |
1434 | return ''; |
1435 | } |
1436 | |
1437 | $services = MediaWikiServices::getInstance(); |
1438 | if ( |
1439 | $t->equals( $parser->getTitle() ) && |
1440 | $services->getMainConfig()->get( MainConfigNames::MiserMode ) && |
1441 | !$parser->getOptions()->getInterfaceMessage() && |
1442 | // @TODO: disallow this word on all namespaces (T235957) |
1443 | $services->getNamespaceInfo()->isSubject( $t->getNamespace() ) |
1444 | ) { |
1445 | // Use a stub result instead of the actual revision ID in order to avoid |
1446 | // double parses on page save but still allow preview detection (T137900) |
1447 | if ( $parser->getRevisionId() || $parser->getOptions()->getSpeculativeRevId() ) { |
1448 | return '-'; |
1449 | } else { |
1450 | $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_EXISTS ); |
1451 | return ''; |
1452 | } |
1453 | } |
1454 | // Fetch revision from cache/database and return the value. |
1455 | // Inform the edit saving system that getting the canonical output |
1456 | // after revision insertion requires a parse that used that exact |
1457 | // revision ID. |
1458 | if ( $t->equals( $parser->getTitle() ) && $title === null ) { |
1459 | // special handling for no-arg case: use speculative rev id |
1460 | // for current page. |
1461 | $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_ID ); |
1462 | $id = $parser->getRevisionId(); |
1463 | if ( $id === 0 ) { |
1464 | $rev = $parser->getRevisionRecordObject(); |
1465 | if ( $rev ) { |
1466 | $id = $rev->getId(); |
1467 | } |
1468 | } |
1469 | if ( !$id ) { |
1470 | $id = $parser->getOptions()->getSpeculativeRevId(); |
1471 | if ( $id ) { |
1472 | $parser->getOutput()->setSpeculativeRevIdUsed( $id ); |
1473 | } |
1474 | } |
1475 | return (string)$id; |
1476 | } |
1477 | $rev = self::getCachedRevisionObject( $parser, $t, ParserOutputFlags::VARY_REVISION_ID ); |
1478 | return $rev ? $rev->getId() : ''; |
1479 | } |
1480 | |
1481 | private static function getRevisionTimestampSubstring( |
1482 | Parser $parser, |
1483 | Title $title, |
1484 | int $start, |
1485 | int $len, |
1486 | int $mtts |
1487 | ): string { |
1488 | // If fetching the revision timestamp of the current page, substitute the |
1489 | // speculative timestamp to be used when this revision is saved. This |
1490 | // avoids having to invalidate the cache immediately by assuming the "last |
1491 | // saved revision" will in fact be this one. |
1492 | // Don't do this for interface messages (eg, edit notices) however; in that |
1493 | // case fall through and use the actual timestamp of the last saved revision. |
1494 | if ( $title->equals( $parser->getTitle() ) && !$parser->getOptions()->getInterfaceMessage() ) { |
1495 | // Get the timezone-adjusted timestamp to be used for this revision |
1496 | $resNow = substr( $parser->getRevisionTimestamp(), $start, $len ); |
1497 | // Possibly set vary-revision if there is not yet an associated revision |
1498 | if ( !$parser->getRevisionRecordObject() ) { |
1499 | // Get the timezone-adjusted timestamp $mtts seconds in the future. |
1500 | // This future is relative to the current time and not that of the |
1501 | // parser options. The rendered timestamp can be compared to that |
1502 | // of the timestamp specified by the parser options. |
1503 | $resThen = substr( |
1504 | $parser->getContentLanguage()->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ), |
1505 | $start, |
1506 | $len |
1507 | ); |
1508 | |
1509 | if ( $resNow !== $resThen ) { |
1510 | // Inform the edit saving system that getting the canonical output after |
1511 | // revision insertion requires a parse that used an actual revision timestamp |
1512 | $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_REVISION_TIMESTAMP ); |
1513 | } |
1514 | } |
1515 | |
1516 | return $resNow; |
1517 | } else { |
1518 | $rev = self::getCachedRevisionObject( $parser, $title, ParserOutputFlags::VARY_REVISION_TIMESTAMP ); |
1519 | if ( !$rev ) { |
1520 | return ''; |
1521 | } |
1522 | $resNow = substr( |
1523 | $parser->getContentLanguage()->userAdjust( $rev->getTimestamp(), '' ), $start, $len |
1524 | ); |
1525 | return $resNow; |
1526 | } |
1527 | } |
1528 | |
1529 | /** |
1530 | * Get the day from the last revision of a specified page. |
1531 | * @param Parser $parser |
1532 | * @param string|null $title Title to get the day from |
1533 | * @return string |
1534 | * @since 1.23 |
1535 | */ |
1536 | public static function revisionday( $parser, $title = null ) { |
1537 | $t = self::makeTitle( $parser, $title ); |
1538 | if ( $t === null || $t->isExternal() ) { |
1539 | return ''; |
1540 | } |
1541 | return strval( (int)self::getRevisionTimestampSubstring( |
1542 | $parser, $t, 6, 2, self::MAX_TTS |
1543 | ) ); |
1544 | } |
1545 | |
1546 | /** |
1547 | * Get the day with leading zeros from the last revision of a specified page. |
1548 | * @param Parser $parser |
1549 | * @param string|null $title Title to get the day from |
1550 | * @return string |
1551 | * @since 1.23 |
1552 | */ |
1553 | public static function revisionday2( $parser, $title = null ) { |
1554 | $t = self::makeTitle( $parser, $title ); |
1555 | if ( $t === null || $t->isExternal() ) { |
1556 | return ''; |
1557 | } |
1558 | return self::getRevisionTimestampSubstring( |
1559 | $parser, $t, 6, 2, self::MAX_TTS |
1560 | ); |
1561 | } |
1562 | |
1563 | /** |
1564 | * Get the month with leading zeros from the last revision of a specified page. |
1565 | * @param Parser $parser |
1566 | * @param string|null $title Title to get the month from |
1567 | * @return string |
1568 | * @since 1.23 |
1569 | */ |
1570 | public static function revisionmonth( $parser, $title = null ) { |
1571 | $t = self::makeTitle( $parser, $title ); |
1572 | if ( $t === null || $t->isExternal() ) { |
1573 | return ''; |
1574 | } |
1575 | return self::getRevisionTimestampSubstring( |
1576 | $parser, $t, 4, 2, self::MAX_TTS |
1577 | ); |
1578 | } |
1579 | |
1580 | /** |
1581 | * Get the month from the last revision of a specified page. |
1582 | * @param Parser $parser |
1583 | * @param string|null $title Title to get the month from |
1584 | * @return string |
1585 | * @since 1.23 |
1586 | */ |
1587 | public static function revisionmonth1( $parser, $title = null ) { |
1588 | $t = self::makeTitle( $parser, $title ); |
1589 | if ( $t === null || $t->isExternal() ) { |
1590 | return ''; |
1591 | } |
1592 | return strval( (int)self::getRevisionTimestampSubstring( |
1593 | $parser, $t, 4, 2, self::MAX_TTS |
1594 | ) ); |
1595 | } |
1596 | |
1597 | /** |
1598 | * Get the year from the last revision of a specified page. |
1599 | * @param Parser $parser |
1600 | * @param string|null $title Title to get the year from |
1601 | * @return string |
1602 | * @since 1.23 |
1603 | */ |
1604 | public static function revisionyear( $parser, $title = null ) { |
1605 | $t = self::makeTitle( $parser, $title ); |
1606 | if ( $t === null || $t->isExternal() ) { |
1607 | return ''; |
1608 | } |
1609 | return self::getRevisionTimestampSubstring( |
1610 | $parser, $t, 0, 4, self::MAX_TTS |
1611 | ); |
1612 | } |
1613 | |
1614 | /** |
1615 | * Get the timestamp from the last revision of a specified page. |
1616 | * @param Parser $parser |
1617 | * @param string|null $title Title to get the timestamp from |
1618 | * @return string |
1619 | * @since 1.23 |
1620 | */ |
1621 | public static function revisiontimestamp( $parser, $title = null ) { |
1622 | $t = self::makeTitle( $parser, $title ); |
1623 | if ( $t === null || $t->isExternal() ) { |
1624 | return ''; |
1625 | } |
1626 | return self::getRevisionTimestampSubstring( |
1627 | $parser, $t, 0, 14, self::MAX_TTS |
1628 | ); |
1629 | } |
1630 | |
1631 | /** |
1632 | * Get the user from the last revision of a specified page. |
1633 | * @param Parser $parser |
1634 | * @param string|null $title Title to get the user from |
1635 | * @return string |
1636 | * @since 1.23 |
1637 | */ |
1638 | public static function revisionuser( $parser, $title = null ) { |
1639 | $t = self::makeTitle( $parser, $title ); |
1640 | if ( $t === null || $t->isExternal() ) { |
1641 | return ''; |
1642 | } |
1643 | // VARY_USER informs the edit saving system that getting the canonical |
1644 | // output after revision insertion requires a parse that used the |
1645 | // actual user ID. |
1646 | if ( $t->equals( $parser->getTitle() ) ) { |
1647 | // Fall back to Parser's "revision user" for the current title |
1648 | $parser->getOutput()->setOutputFlag( ParserOutputFlags::VARY_USER ); |
1649 | // Note that getRevisionUser() can return null; we need to |
1650 | // be sure to cast this to (an empty) string, since returning |
1651 | // null means "magic variable not handled". |
1652 | return (string)$parser->getRevisionUser(); |
1653 | } |
1654 | // Fetch revision from cache/database and return the value. |
1655 | $rev = self::getCachedRevisionObject( $parser, $t, ParserOutputFlags::VARY_USER ); |
1656 | $user = ( $rev !== null ) ? $rev->getUser() : null; |
1657 | return $user ? $user->getName() : ''; |
1658 | } |
1659 | |
1660 | /** |
1661 | * Returns the sources of any cascading protection acting on a specified page. |
1662 | * Pages will not return their own title unless they transclude themselves. |
1663 | * This is an expensive parser function and can't be called too many times per page, |
1664 | * unless cascading protection sources for the page have already been loaded. |
1665 | * |
1666 | * @param Parser $parser |
1667 | * @param ?string $title |
1668 | * |
1669 | * @return string |
1670 | * @since 1.23 |
1671 | */ |
1672 | public static function cascadingsources( $parser, $title = '' ) { |
1673 | $titleObject = Title::newFromText( $title ) ?? $parser->getTitle(); |
1674 | $restrictionStore = MediaWikiServices::getInstance()->getRestrictionStore(); |
1675 | if ( $restrictionStore->areCascadeProtectionSourcesLoaded( $titleObject ) |
1676 | || $parser->incrementExpensiveFunctionCount() |
1677 | ) { |
1678 | $names = []; |
1679 | $sources = $restrictionStore->getCascadeProtectionSources( $titleObject ); |
1680 | $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter(); |
1681 | foreach ( $sources[0] as $sourcePageIdentity ) { |
1682 | $names[] = $titleFormatter->getPrefixedText( $sourcePageIdentity ); |
1683 | } |
1684 | return implode( '|', $names ); |
1685 | } |
1686 | return ''; |
1687 | } |
1688 | |
1689 | public static function interwikilink( $parser, $prefix = '', $title = '', $linkText = null ) { |
1690 | $services = MediaWikiServices::getInstance(); |
1691 | if ( |
1692 | $prefix !== '' && |
1693 | $services->getInterwikiLookup()->isValidInterwiki( $prefix ) |
1694 | ) { |
1695 | if ( $linkText !== null ) { |
1696 | $linkText = Parser::stripOuterParagraph( |
1697 | # FIXME T382287: when using Parsoid this may leave |
1698 | # strip markers behind for embedded extension tags. |
1699 | $parser->recursiveTagParseFully( $linkText ) |
1700 | ); |
1701 | } |
1702 | [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' ); |
1703 | $target = new TitleValue( NS_MAIN, $title, $frag, $prefix ); |
1704 | $parser->getOutput()->addInterwikiLink( $target ); |
1705 | return [ |
1706 | 'text' => Linker::link( $target, $linkText ), |
1707 | 'isHTML' => true, |
1708 | ]; |
1709 | } |
1710 | // Invalid interwiki link, render as plain text |
1711 | return [ 'found' => false ]; |
1712 | } |
1713 | |
1714 | public static function interlanguagelink( $parser, $prefix = '', $title = '', $linkText = null ) { |
1715 | $services = MediaWikiServices::getInstance(); |
1716 | $extraInterlanguageLinkPrefixes = $services->getMainConfig()->get( |
1717 | MainConfigNames::ExtraInterlanguageLinkPrefixes |
1718 | ); |
1719 | if ( |
1720 | $prefix !== '' && |
1721 | $services->getInterwikiLookup()->isValidInterwiki( $prefix ) && |
1722 | ( |
1723 | $services->getLanguageNameUtils()->getLanguageName( |
1724 | $prefix, LanguageNameUtils::AUTONYMS, LanguageNameUtils::DEFINED |
1725 | ) || in_array( $prefix, $extraInterlanguageLinkPrefixes, true ) |
1726 | ) |
1727 | ) { |
1728 | // $linkText is ignored for language links, but fragment is kept |
1729 | [ $title, $frag ] = array_pad( explode( '#', $title, 2 ), 2, '' ); |
1730 | $parser->getOutput()->addLanguageLink( |
1731 | new TitleValue( |
1732 | NS_MAIN, $title, $frag, $prefix |
1733 | ) |
1734 | ); |
1735 | return ''; |
1736 | } |
1737 | // Invalid language link, render as plain text |
1738 | return [ 'found' => false ]; |
1739 | } |
1740 | } |
1741 | |
1742 | /** @deprecated class alias since 1.43 */ |
1743 | class_alias( CoreParserFunctions::class, 'CoreParserFunctions' ); |