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