|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* PHP parser that converts wiki markup to HTML. |
|
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
|
|
|
use MediaWiki\Linker\LinkRenderer; |
|
24
|
|
|
use MediaWiki\MediaWikiServices; |
|
25
|
|
|
|
|
26
|
|
|
/** |
|
27
|
|
|
* @defgroup Parser Parser |
|
28
|
|
|
*/ |
|
29
|
|
|
|
|
30
|
|
|
/** |
|
31
|
|
|
* PHP Parser - Processes wiki markup (which uses a more user-friendly |
|
32
|
|
|
* syntax, such as "[[link]]" for making links), and provides a one-way |
|
33
|
|
|
* transformation of that wiki markup it into (X)HTML output / markup |
|
34
|
|
|
* (which in turn the browser understands, and can display). |
|
35
|
|
|
* |
|
36
|
|
|
* There are seven main entry points into the Parser class: |
|
37
|
|
|
* |
|
38
|
|
|
* - Parser::parse() |
|
39
|
|
|
* produces HTML output |
|
40
|
|
|
* - Parser::preSaveTransform() |
|
41
|
|
|
* produces altered wiki markup |
|
42
|
|
|
* - Parser::preprocess() |
|
43
|
|
|
* removes HTML comments and expands templates |
|
44
|
|
|
* - Parser::cleanSig() and Parser::cleanSigInSig() |
|
45
|
|
|
* cleans a signature before saving it to preferences |
|
46
|
|
|
* - Parser::getSection() |
|
47
|
|
|
* return the content of a section from an article for section editing |
|
48
|
|
|
* - Parser::replaceSection() |
|
49
|
|
|
* replaces a section by number inside an article |
|
50
|
|
|
* - Parser::getPreloadText() |
|
51
|
|
|
* removes <noinclude> sections and <includeonly> tags |
|
52
|
|
|
* |
|
53
|
|
|
* Globals used: |
|
54
|
|
|
* object: $wgContLang |
|
55
|
|
|
* |
|
56
|
|
|
* @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away! |
|
57
|
|
|
* |
|
58
|
|
|
* @par Settings: |
|
59
|
|
|
* $wgNamespacesWithSubpages |
|
60
|
|
|
* |
|
61
|
|
|
* @par Settings only within ParserOptions: |
|
62
|
|
|
* $wgAllowExternalImages |
|
63
|
|
|
* $wgAllowSpecialInclusion |
|
64
|
|
|
* $wgInterwikiMagic |
|
65
|
|
|
* $wgMaxArticleSize |
|
66
|
|
|
* |
|
67
|
|
|
* @ingroup Parser |
|
68
|
|
|
*/ |
|
69
|
|
|
class Parser { |
|
70
|
|
|
/** |
|
71
|
|
|
* Update this version number when the ParserOutput format |
|
72
|
|
|
* changes in an incompatible way, so the parser cache |
|
73
|
|
|
* can automatically discard old data. |
|
74
|
|
|
*/ |
|
75
|
|
|
const VERSION = '1.6.4'; |
|
76
|
|
|
|
|
77
|
|
|
/** |
|
78
|
|
|
* Update this version number when the output of serialiseHalfParsedText() |
|
79
|
|
|
* changes in an incompatible way |
|
80
|
|
|
*/ |
|
81
|
|
|
const HALF_PARSED_VERSION = 2; |
|
82
|
|
|
|
|
83
|
|
|
# Flags for Parser::setFunctionHook |
|
84
|
|
|
const SFH_NO_HASH = 1; |
|
85
|
|
|
const SFH_OBJECT_ARGS = 2; |
|
86
|
|
|
|
|
87
|
|
|
# Constants needed for external link processing |
|
88
|
|
|
# Everything except bracket, space, or control characters |
|
89
|
|
|
# \p{Zs} is unicode 'separator, space' category. It covers the space 0x20 |
|
90
|
|
|
# as well as U+3000 is IDEOGRAPHIC SPACE for bug 19052 |
|
91
|
|
|
const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F\p{Zs}]'; |
|
92
|
|
|
# Simplified expression to match an IPv4 or IPv6 address, or |
|
93
|
|
|
# at least one character of a host name (embeds EXT_LINK_URL_CLASS) |
|
94
|
|
|
const EXT_LINK_ADDR = '(?:[0-9.]+|\\[(?i:[0-9a-f:.]+)\\]|[^][<>"\\x00-\\x20\\x7F\p{Zs}])'; |
|
95
|
|
|
# RegExp to make image URLs (embeds IPv6 part of EXT_LINK_ADDR) |
|
96
|
|
|
// @codingStandardsIgnoreStart Generic.Files.LineLength |
|
97
|
|
|
const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)((?:\\[(?i:[0-9a-f:.]+)\\])?[^][<>"\\x00-\\x20\\x7F\p{Zs}]+) |
|
98
|
|
|
\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sxu'; |
|
99
|
|
|
// @codingStandardsIgnoreEnd |
|
100
|
|
|
|
|
101
|
|
|
# Regular expression for a non-newline space |
|
102
|
|
|
const SPACE_NOT_NL = '(?:\t| |&\#0*160;|&\#[Xx]0*[Aa]0;|\p{Zs})'; |
|
103
|
|
|
|
|
104
|
|
|
# Flags for preprocessToDom |
|
105
|
|
|
const PTD_FOR_INCLUSION = 1; |
|
106
|
|
|
|
|
107
|
|
|
# Allowed values for $this->mOutputType |
|
108
|
|
|
# Parameter to startExternalParse(). |
|
109
|
|
|
const OT_HTML = 1; # like parse() |
|
110
|
|
|
const OT_WIKI = 2; # like preSaveTransform() |
|
111
|
|
|
const OT_PREPROCESS = 3; # like preprocess() |
|
112
|
|
|
const OT_MSG = 3; |
|
113
|
|
|
const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged. |
|
114
|
|
|
|
|
115
|
|
|
/** |
|
116
|
|
|
* @var string Prefix and suffix for temporary replacement strings |
|
117
|
|
|
* for the multipass parser. |
|
118
|
|
|
* |
|
119
|
|
|
* \x7f should never appear in input as it's disallowed in XML. |
|
120
|
|
|
* Using it at the front also gives us a little extra robustness |
|
121
|
|
|
* since it shouldn't match when butted up against identifier-like |
|
122
|
|
|
* string constructs. |
|
123
|
|
|
* |
|
124
|
|
|
* Must not consist of all title characters, or else it will change |
|
125
|
|
|
* the behavior of <nowiki> in a link. |
|
126
|
|
|
* |
|
127
|
|
|
* Must have a character that needs escaping in attributes, otherwise |
|
128
|
|
|
* someone could put a strip marker in an attribute, to get around |
|
129
|
|
|
* escaping quote marks, and break out of the attribute. Thus we add |
|
130
|
|
|
* `'". |
|
131
|
|
|
*/ |
|
132
|
|
|
const MARKER_SUFFIX = "-QINU`\"'\x7f"; |
|
133
|
|
|
const MARKER_PREFIX = "\x7f'\"`UNIQ-"; |
|
134
|
|
|
|
|
135
|
|
|
# Markers used for wrapping the table of contents |
|
136
|
|
|
const TOC_START = '<mw:toc>'; |
|
137
|
|
|
const TOC_END = '</mw:toc>'; |
|
138
|
|
|
|
|
139
|
|
|
# Persistent: |
|
140
|
|
|
public $mTagHooks = []; |
|
141
|
|
|
public $mTransparentTagHooks = []; |
|
142
|
|
|
public $mFunctionHooks = []; |
|
143
|
|
|
public $mFunctionSynonyms = [ 0 => [], 1 => [] ]; |
|
144
|
|
|
public $mFunctionTagHooks = []; |
|
145
|
|
|
public $mStripList = []; |
|
146
|
|
|
public $mDefaultStripList = []; |
|
147
|
|
|
public $mVarCache = []; |
|
148
|
|
|
public $mImageParams = []; |
|
149
|
|
|
public $mImageParamsMagicArray = []; |
|
150
|
|
|
public $mMarkerIndex = 0; |
|
151
|
|
|
public $mFirstCall = true; |
|
152
|
|
|
|
|
153
|
|
|
# Initialised by initialiseVariables() |
|
154
|
|
|
|
|
155
|
|
|
/** |
|
156
|
|
|
* @var MagicWordArray |
|
157
|
|
|
*/ |
|
158
|
|
|
public $mVariables; |
|
159
|
|
|
|
|
160
|
|
|
/** |
|
161
|
|
|
* @var MagicWordArray |
|
162
|
|
|
*/ |
|
163
|
|
|
public $mSubstWords; |
|
164
|
|
|
# Initialised in constructor |
|
165
|
|
|
public $mConf, $mExtLinkBracketedRegex, $mUrlProtocols; |
|
|
|
|
|
|
166
|
|
|
|
|
167
|
|
|
# Initialized in getPreprocessor() |
|
168
|
|
|
/** @var Preprocessor */ |
|
169
|
|
|
public $mPreprocessor; |
|
170
|
|
|
|
|
171
|
|
|
# Cleared with clearState(): |
|
172
|
|
|
/** |
|
173
|
|
|
* @var ParserOutput |
|
174
|
|
|
*/ |
|
175
|
|
|
public $mOutput; |
|
176
|
|
|
public $mAutonumber; |
|
177
|
|
|
|
|
178
|
|
|
/** |
|
179
|
|
|
* @var StripState |
|
180
|
|
|
*/ |
|
181
|
|
|
public $mStripState; |
|
182
|
|
|
|
|
183
|
|
|
public $mIncludeCount; |
|
184
|
|
|
/** |
|
185
|
|
|
* @var LinkHolderArray |
|
186
|
|
|
*/ |
|
187
|
|
|
public $mLinkHolders; |
|
188
|
|
|
|
|
189
|
|
|
public $mLinkID; |
|
190
|
|
|
public $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth; |
|
|
|
|
|
|
191
|
|
|
public $mDefaultSort; |
|
192
|
|
|
public $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores; |
|
|
|
|
|
|
193
|
|
|
public $mExpensiveFunctionCount; # number of expensive parser function calls |
|
194
|
|
|
public $mShowToc, $mForceTocPosition; |
|
|
|
|
|
|
195
|
|
|
|
|
196
|
|
|
/** |
|
197
|
|
|
* @var User |
|
198
|
|
|
*/ |
|
199
|
|
|
public $mUser; # User object; only used when doing pre-save transform |
|
200
|
|
|
|
|
201
|
|
|
# Temporary |
|
202
|
|
|
# These are variables reset at least once per parse regardless of $clearState |
|
203
|
|
|
|
|
204
|
|
|
/** |
|
205
|
|
|
* @var ParserOptions |
|
206
|
|
|
*/ |
|
207
|
|
|
public $mOptions; |
|
208
|
|
|
|
|
209
|
|
|
/** |
|
210
|
|
|
* @var Title |
|
211
|
|
|
*/ |
|
212
|
|
|
public $mTitle; # Title context, used for self-link rendering and similar things |
|
213
|
|
|
public $mOutputType; # Output type, one of the OT_xxx constants |
|
214
|
|
|
public $ot; # Shortcut alias, see setOutputType() |
|
215
|
|
|
public $mRevisionObject; # The revision object of the specified revision ID |
|
216
|
|
|
public $mRevisionId; # ID to display in {{REVISIONID}} tags |
|
217
|
|
|
public $mRevisionTimestamp; # The timestamp of the specified revision ID |
|
218
|
|
|
public $mRevisionUser; # User to display in {{REVISIONUSER}} tag |
|
219
|
|
|
public $mRevisionSize; # Size to display in {{REVISIONSIZE}} variable |
|
220
|
|
|
public $mRevIdForTs; # The revision ID which was used to fetch the timestamp |
|
221
|
|
|
public $mInputSize = false; # For {{PAGESIZE}} on current page. |
|
222
|
|
|
|
|
223
|
|
|
/** |
|
224
|
|
|
* @var string Deprecated accessor for the strip marker prefix. |
|
225
|
|
|
* @deprecated since 1.26; use Parser::MARKER_PREFIX instead. |
|
226
|
|
|
**/ |
|
227
|
|
|
public $mUniqPrefix = Parser::MARKER_PREFIX; |
|
228
|
|
|
|
|
229
|
|
|
/** |
|
230
|
|
|
* @var array Array with the language name of each language link (i.e. the |
|
231
|
|
|
* interwiki prefix) in the key, value arbitrary. Used to avoid sending |
|
232
|
|
|
* duplicate language links to the ParserOutput. |
|
233
|
|
|
*/ |
|
234
|
|
|
public $mLangLinkLanguages; |
|
235
|
|
|
|
|
236
|
|
|
/** |
|
237
|
|
|
* @var MapCacheLRU|null |
|
238
|
|
|
* @since 1.24 |
|
239
|
|
|
* |
|
240
|
|
|
* A cache of the current revisions of titles. Keys are $title->getPrefixedDbKey() |
|
241
|
|
|
*/ |
|
242
|
|
|
public $currentRevisionCache; |
|
243
|
|
|
|
|
244
|
|
|
/** |
|
245
|
|
|
* @var bool Recursive call protection. |
|
246
|
|
|
* This variable should be treated as if it were private. |
|
247
|
|
|
*/ |
|
248
|
|
|
public $mInParse = false; |
|
249
|
|
|
|
|
250
|
|
|
/** @var SectionProfiler */ |
|
251
|
|
|
protected $mProfiler; |
|
252
|
|
|
|
|
253
|
|
|
/** |
|
254
|
|
|
* @var LinkRenderer |
|
255
|
|
|
*/ |
|
256
|
|
|
protected $mLinkRenderer; |
|
257
|
|
|
|
|
258
|
|
|
/** |
|
259
|
|
|
* @param array $conf |
|
260
|
|
|
*/ |
|
261
|
|
|
public function __construct( $conf = [] ) { |
|
262
|
|
|
$this->mConf = $conf; |
|
263
|
|
|
$this->mUrlProtocols = wfUrlProtocols(); |
|
264
|
|
|
$this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' . |
|
265
|
|
|
self::EXT_LINK_ADDR . |
|
266
|
|
|
self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/Su'; |
|
267
|
|
|
if ( isset( $conf['preprocessorClass'] ) ) { |
|
268
|
|
|
$this->mPreprocessorClass = $conf['preprocessorClass']; |
|
|
|
|
|
|
269
|
|
|
} elseif ( defined( 'HPHP_VERSION' ) ) { |
|
270
|
|
|
# Preprocessor_Hash is much faster than Preprocessor_DOM under HipHop |
|
271
|
|
|
$this->mPreprocessorClass = 'Preprocessor_Hash'; |
|
|
|
|
|
|
272
|
|
|
} elseif ( extension_loaded( 'domxml' ) ) { |
|
273
|
|
|
# PECL extension that conflicts with the core DOM extension (bug 13770) |
|
274
|
|
|
wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" ); |
|
275
|
|
|
$this->mPreprocessorClass = 'Preprocessor_Hash'; |
|
|
|
|
|
|
276
|
|
|
} elseif ( extension_loaded( 'dom' ) ) { |
|
277
|
|
|
$this->mPreprocessorClass = 'Preprocessor_DOM'; |
|
|
|
|
|
|
278
|
|
|
} else { |
|
279
|
|
|
$this->mPreprocessorClass = 'Preprocessor_Hash'; |
|
|
|
|
|
|
280
|
|
|
} |
|
281
|
|
|
wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" ); |
|
|
|
|
|
|
282
|
|
|
} |
|
283
|
|
|
|
|
284
|
|
|
/** |
|
285
|
|
|
* Reduce memory usage to reduce the impact of circular references |
|
286
|
|
|
*/ |
|
287
|
|
|
public function __destruct() { |
|
288
|
|
|
if ( isset( $this->mLinkHolders ) ) { |
|
289
|
|
|
unset( $this->mLinkHolders ); |
|
290
|
|
|
} |
|
291
|
|
|
foreach ( $this as $name => $value ) { |
|
|
|
|
|
|
292
|
|
|
unset( $this->$name ); |
|
293
|
|
|
} |
|
294
|
|
|
} |
|
295
|
|
|
|
|
296
|
|
|
/** |
|
297
|
|
|
* Allow extensions to clean up when the parser is cloned |
|
298
|
|
|
*/ |
|
299
|
|
|
public function __clone() { |
|
300
|
|
|
$this->mInParse = false; |
|
301
|
|
|
|
|
302
|
|
|
// Bug 56226: When you create a reference "to" an object field, that |
|
303
|
|
|
// makes the object field itself be a reference too (until the other |
|
304
|
|
|
// reference goes out of scope). When cloning, any field that's a |
|
305
|
|
|
// reference is copied as a reference in the new object. Both of these |
|
306
|
|
|
// are defined PHP5 behaviors, as inconvenient as it is for us when old |
|
307
|
|
|
// hooks from PHP4 days are passing fields by reference. |
|
308
|
|
|
foreach ( [ 'mStripState', 'mVarCache' ] as $k ) { |
|
309
|
|
|
// Make a non-reference copy of the field, then rebind the field to |
|
310
|
|
|
// reference the new copy. |
|
311
|
|
|
$tmp = $this->$k; |
|
312
|
|
|
$this->$k =& $tmp; |
|
313
|
|
|
unset( $tmp ); |
|
314
|
|
|
} |
|
315
|
|
|
|
|
316
|
|
|
Hooks::run( 'ParserCloned', [ $this ] ); |
|
317
|
|
|
} |
|
318
|
|
|
|
|
319
|
|
|
/** |
|
320
|
|
|
* Do various kinds of initialisation on the first call of the parser |
|
321
|
|
|
*/ |
|
322
|
|
|
public function firstCallInit() { |
|
323
|
|
|
if ( !$this->mFirstCall ) { |
|
324
|
|
|
return; |
|
325
|
|
|
} |
|
326
|
|
|
$this->mFirstCall = false; |
|
327
|
|
|
|
|
328
|
|
|
CoreParserFunctions::register( $this ); |
|
329
|
|
|
CoreTagHooks::register( $this ); |
|
330
|
|
|
$this->initialiseVariables(); |
|
331
|
|
|
|
|
332
|
|
|
Hooks::run( 'ParserFirstCallInit', [ &$this ] ); |
|
333
|
|
|
} |
|
334
|
|
|
|
|
335
|
|
|
/** |
|
336
|
|
|
* Clear Parser state |
|
337
|
|
|
* |
|
338
|
|
|
* @private |
|
339
|
|
|
*/ |
|
340
|
|
|
public function clearState() { |
|
341
|
|
|
if ( $this->mFirstCall ) { |
|
342
|
|
|
$this->firstCallInit(); |
|
343
|
|
|
} |
|
344
|
|
|
$this->mOutput = new ParserOutput; |
|
345
|
|
|
$this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] ); |
|
346
|
|
|
$this->mAutonumber = 0; |
|
347
|
|
|
$this->mIncludeCount = []; |
|
348
|
|
|
$this->mLinkHolders = new LinkHolderArray( $this ); |
|
349
|
|
|
$this->mLinkID = 0; |
|
350
|
|
|
$this->mRevisionObject = $this->mRevisionTimestamp = |
|
351
|
|
|
$this->mRevisionId = $this->mRevisionUser = $this->mRevisionSize = null; |
|
352
|
|
|
$this->mVarCache = []; |
|
353
|
|
|
$this->mUser = null; |
|
354
|
|
|
$this->mLangLinkLanguages = []; |
|
355
|
|
|
$this->currentRevisionCache = null; |
|
356
|
|
|
|
|
357
|
|
|
$this->mStripState = new StripState; |
|
358
|
|
|
|
|
359
|
|
|
# Clear these on every parse, bug 4549 |
|
360
|
|
|
$this->mTplRedirCache = $this->mTplDomCache = []; |
|
361
|
|
|
|
|
362
|
|
|
$this->mShowToc = true; |
|
363
|
|
|
$this->mForceTocPosition = false; |
|
364
|
|
|
$this->mIncludeSizes = [ |
|
365
|
|
|
'post-expand' => 0, |
|
366
|
|
|
'arg' => 0, |
|
367
|
|
|
]; |
|
368
|
|
|
$this->mPPNodeCount = 0; |
|
369
|
|
|
$this->mGeneratedPPNodeCount = 0; |
|
370
|
|
|
$this->mHighestExpansionDepth = 0; |
|
371
|
|
|
$this->mDefaultSort = false; |
|
372
|
|
|
$this->mHeadings = []; |
|
373
|
|
|
$this->mDoubleUnderscores = []; |
|
374
|
|
|
$this->mExpensiveFunctionCount = 0; |
|
375
|
|
|
|
|
376
|
|
|
# Fix cloning |
|
377
|
|
|
if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) { |
|
|
|
|
|
|
378
|
|
|
$this->mPreprocessor = null; |
|
379
|
|
|
} |
|
380
|
|
|
|
|
381
|
|
|
$this->mProfiler = new SectionProfiler(); |
|
382
|
|
|
|
|
383
|
|
|
Hooks::run( 'ParserClearState', [ &$this ] ); |
|
384
|
|
|
} |
|
385
|
|
|
|
|
386
|
|
|
/** |
|
387
|
|
|
* Convert wikitext to HTML |
|
388
|
|
|
* Do not call this function recursively. |
|
389
|
|
|
* |
|
390
|
|
|
* @param string $text Text we want to parse |
|
391
|
|
|
* @param Title $title |
|
392
|
|
|
* @param ParserOptions $options |
|
393
|
|
|
* @param bool $linestart |
|
394
|
|
|
* @param bool $clearState |
|
395
|
|
|
* @param int $revid Number to pass in {{REVISIONID}} |
|
396
|
|
|
* @return ParserOutput A ParserOutput |
|
397
|
|
|
*/ |
|
398
|
|
|
public function parse( $text, Title $title, ParserOptions $options, |
|
399
|
|
|
$linestart = true, $clearState = true, $revid = null |
|
400
|
|
|
) { |
|
401
|
|
|
/** |
|
402
|
|
|
* First pass--just handle <nowiki> sections, pass the rest off |
|
403
|
|
|
* to internalParse() which does all the real work. |
|
404
|
|
|
*/ |
|
405
|
|
|
|
|
406
|
|
|
global $wgShowHostnames; |
|
407
|
|
|
|
|
408
|
|
|
if ( $clearState ) { |
|
409
|
|
|
// We use U+007F DELETE to construct strip markers, so we have to make |
|
410
|
|
|
// sure that this character does not occur in the input text. |
|
411
|
|
|
$text = strtr( $text, "\x7f", "?" ); |
|
412
|
|
|
$magicScopeVariable = $this->lock(); |
|
|
|
|
|
|
413
|
|
|
} |
|
414
|
|
|
|
|
415
|
|
|
$this->startParse( $title, $options, self::OT_HTML, $clearState ); |
|
416
|
|
|
|
|
417
|
|
|
$this->currentRevisionCache = null; |
|
418
|
|
|
$this->mInputSize = strlen( $text ); |
|
|
|
|
|
|
419
|
|
|
if ( $this->mOptions->getEnableLimitReport() ) { |
|
420
|
|
|
$this->mOutput->resetParseStartTime(); |
|
421
|
|
|
} |
|
422
|
|
|
|
|
423
|
|
|
$oldRevisionId = $this->mRevisionId; |
|
424
|
|
|
$oldRevisionObject = $this->mRevisionObject; |
|
425
|
|
|
$oldRevisionTimestamp = $this->mRevisionTimestamp; |
|
426
|
|
|
$oldRevisionUser = $this->mRevisionUser; |
|
427
|
|
|
$oldRevisionSize = $this->mRevisionSize; |
|
428
|
|
|
if ( $revid !== null ) { |
|
429
|
|
|
$this->mRevisionId = $revid; |
|
430
|
|
|
$this->mRevisionObject = null; |
|
431
|
|
|
$this->mRevisionTimestamp = null; |
|
432
|
|
|
$this->mRevisionUser = null; |
|
433
|
|
|
$this->mRevisionSize = null; |
|
434
|
|
|
} |
|
435
|
|
|
|
|
436
|
|
|
Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] ); |
|
437
|
|
|
# No more strip! |
|
438
|
|
|
Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] ); |
|
439
|
|
|
$text = $this->internalParse( $text ); |
|
440
|
|
|
Hooks::run( 'ParserAfterParse', [ &$this, &$text, &$this->mStripState ] ); |
|
441
|
|
|
|
|
442
|
|
|
$text = $this->internalParseHalfParsed( $text, true, $linestart ); |
|
443
|
|
|
|
|
444
|
|
|
/** |
|
445
|
|
|
* A converted title will be provided in the output object if title and |
|
446
|
|
|
* content conversion are enabled, the article text does not contain |
|
447
|
|
|
* a conversion-suppressing double-underscore tag, and no |
|
448
|
|
|
* {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over |
|
449
|
|
|
* automatic link conversion. |
|
450
|
|
|
*/ |
|
451
|
|
|
if ( !( $options->getDisableTitleConversion() |
|
452
|
|
|
|| isset( $this->mDoubleUnderscores['nocontentconvert'] ) |
|
453
|
|
|
|| isset( $this->mDoubleUnderscores['notitleconvert'] ) |
|
454
|
|
|
|| $this->mOutput->getDisplayTitle() !== false ) |
|
455
|
|
|
) { |
|
456
|
|
|
$convruletitle = $this->getConverterLanguage()->getConvRuleTitle(); |
|
457
|
|
|
if ( $convruletitle ) { |
|
458
|
|
|
$this->mOutput->setTitleText( $convruletitle ); |
|
459
|
|
|
} else { |
|
460
|
|
|
$titleText = $this->getConverterLanguage()->convertTitle( $title ); |
|
461
|
|
|
$this->mOutput->setTitleText( $titleText ); |
|
462
|
|
|
} |
|
463
|
|
|
} |
|
464
|
|
|
|
|
465
|
|
|
if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) { |
|
466
|
|
|
$this->limitationWarn( 'expensive-parserfunction', |
|
467
|
|
|
$this->mExpensiveFunctionCount, |
|
468
|
|
|
$this->mOptions->getExpensiveParserFunctionLimit() |
|
469
|
|
|
); |
|
470
|
|
|
} |
|
471
|
|
|
|
|
472
|
|
|
# Information on include size limits, for the benefit of users who try to skirt them |
|
473
|
|
|
if ( $this->mOptions->getEnableLimitReport() ) { |
|
474
|
|
|
$max = $this->mOptions->getMaxIncludeSize(); |
|
475
|
|
|
|
|
476
|
|
|
$cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' ); |
|
477
|
|
|
if ( $cpuTime !== null ) { |
|
478
|
|
|
$this->mOutput->setLimitReportData( 'limitreport-cputime', |
|
479
|
|
|
sprintf( "%.3f", $cpuTime ) |
|
480
|
|
|
); |
|
481
|
|
|
} |
|
482
|
|
|
|
|
483
|
|
|
$wallTime = $this->mOutput->getTimeSinceStart( 'wall' ); |
|
484
|
|
|
$this->mOutput->setLimitReportData( 'limitreport-walltime', |
|
485
|
|
|
sprintf( "%.3f", $wallTime ) |
|
486
|
|
|
); |
|
487
|
|
|
|
|
488
|
|
|
$this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes', |
|
489
|
|
|
[ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ] |
|
490
|
|
|
); |
|
491
|
|
|
$this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes', |
|
492
|
|
|
[ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ] |
|
493
|
|
|
); |
|
494
|
|
|
$this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize', |
|
495
|
|
|
[ $this->mIncludeSizes['post-expand'], $max ] |
|
496
|
|
|
); |
|
497
|
|
|
$this->mOutput->setLimitReportData( 'limitreport-templateargumentsize', |
|
498
|
|
|
[ $this->mIncludeSizes['arg'], $max ] |
|
499
|
|
|
); |
|
500
|
|
|
$this->mOutput->setLimitReportData( 'limitreport-expansiondepth', |
|
501
|
|
|
[ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ] |
|
502
|
|
|
); |
|
503
|
|
|
$this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount', |
|
504
|
|
|
[ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ] |
|
505
|
|
|
); |
|
506
|
|
|
Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] ); |
|
507
|
|
|
|
|
508
|
|
|
$limitReport = "NewPP limit report\n"; |
|
509
|
|
|
if ( $wgShowHostnames ) { |
|
510
|
|
|
$limitReport .= 'Parsed by ' . wfHostname() . "\n"; |
|
511
|
|
|
} |
|
512
|
|
|
$limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n"; |
|
513
|
|
|
$limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n"; |
|
514
|
|
|
$limitReport .= 'Dynamic content: ' . |
|
515
|
|
|
( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) . |
|
516
|
|
|
"\n"; |
|
517
|
|
|
|
|
518
|
|
|
foreach ( $this->mOutput->getLimitReportData() as $key => $value ) { |
|
519
|
|
|
if ( Hooks::run( 'ParserLimitReportFormat', |
|
520
|
|
|
[ $key, &$value, &$limitReport, false, false ] |
|
521
|
|
|
) ) { |
|
522
|
|
|
$keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false ); |
|
523
|
|
|
$valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] ) |
|
524
|
|
|
->inLanguage( 'en' )->useDatabase( false ); |
|
525
|
|
|
if ( !$valueMsg->exists() ) { |
|
526
|
|
|
$valueMsg = new RawMessage( '$1' ); |
|
527
|
|
|
} |
|
528
|
|
|
if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) { |
|
529
|
|
|
$valueMsg->params( $value ); |
|
530
|
|
|
$limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n"; |
|
531
|
|
|
} |
|
532
|
|
|
} |
|
533
|
|
|
} |
|
534
|
|
|
// Since we're not really outputting HTML, decode the entities and |
|
535
|
|
|
// then re-encode the things that need hiding inside HTML comments. |
|
536
|
|
|
$limitReport = htmlspecialchars_decode( $limitReport ); |
|
537
|
|
|
Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ] ); |
|
538
|
|
|
|
|
539
|
|
|
// Sanitize for comment. Note '‐' in the replacement is U+2010, |
|
540
|
|
|
// which looks much like the problematic '-'. |
|
541
|
|
|
$limitReport = str_replace( [ '-', '&' ], [ '‐', '&' ], $limitReport ); |
|
542
|
|
|
$text .= "\n<!-- \n$limitReport-->\n"; |
|
543
|
|
|
|
|
544
|
|
|
// Add on template profiling data |
|
545
|
|
|
$dataByFunc = $this->mProfiler->getFunctionStats(); |
|
546
|
|
|
uasort( $dataByFunc, function ( $a, $b ) { |
|
547
|
|
|
return $a['real'] < $b['real']; // descending order |
|
548
|
|
|
} ); |
|
549
|
|
|
$profileReport = "Transclusion expansion time report (%,ms,calls,template)\n"; |
|
550
|
|
|
foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) { |
|
551
|
|
|
$profileReport .= sprintf( "%6.2f%% %8.3f %6d - %s\n", |
|
552
|
|
|
$item['%real'], $item['real'], $item['calls'], |
|
553
|
|
|
htmlspecialchars( $item['name'] ) ); |
|
554
|
|
|
} |
|
555
|
|
|
$text .= "\n<!-- \n$profileReport-->\n"; |
|
556
|
|
|
|
|
557
|
|
|
if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) { |
|
558
|
|
|
wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' . |
|
559
|
|
|
$this->mTitle->getPrefixedDBkey() ); |
|
560
|
|
|
} |
|
561
|
|
|
} |
|
562
|
|
|
$this->mOutput->setText( $text ); |
|
563
|
|
|
|
|
564
|
|
|
$this->mRevisionId = $oldRevisionId; |
|
565
|
|
|
$this->mRevisionObject = $oldRevisionObject; |
|
566
|
|
|
$this->mRevisionTimestamp = $oldRevisionTimestamp; |
|
567
|
|
|
$this->mRevisionUser = $oldRevisionUser; |
|
568
|
|
|
$this->mRevisionSize = $oldRevisionSize; |
|
569
|
|
|
$this->mInputSize = false; |
|
570
|
|
|
$this->currentRevisionCache = null; |
|
571
|
|
|
|
|
572
|
|
|
return $this->mOutput; |
|
573
|
|
|
} |
|
574
|
|
|
|
|
575
|
|
|
/** |
|
576
|
|
|
* Half-parse wikitext to half-parsed HTML. This recursive parser entry point |
|
577
|
|
|
* can be called from an extension tag hook. |
|
578
|
|
|
* |
|
579
|
|
|
* The output of this function IS NOT SAFE PARSED HTML; it is "half-parsed" |
|
580
|
|
|
* instead, which means that lists and links have not been fully parsed yet, |
|
581
|
|
|
* and strip markers are still present. |
|
582
|
|
|
* |
|
583
|
|
|
* Use recursiveTagParseFully() to fully parse wikitext to output-safe HTML. |
|
584
|
|
|
* |
|
585
|
|
|
* Use this function if you're a parser tag hook and you want to parse |
|
586
|
|
|
* wikitext before or after applying additional transformations, and you |
|
587
|
|
|
* intend to *return the result as hook output*, which will cause it to go |
|
588
|
|
|
* through the rest of parsing process automatically. |
|
589
|
|
|
* |
|
590
|
|
|
* If $frame is not provided, then template variables (e.g., {{{1}}}) within |
|
591
|
|
|
* $text are not expanded |
|
592
|
|
|
* |
|
593
|
|
|
* @param string $text Text extension wants to have parsed |
|
594
|
|
|
* @param bool|PPFrame $frame The frame to use for expanding any template variables |
|
595
|
|
|
* @return string UNSAFE half-parsed HTML |
|
596
|
|
|
*/ |
|
597
|
|
|
public function recursiveTagParse( $text, $frame = false ) { |
|
598
|
|
|
Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] ); |
|
599
|
|
|
Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] ); |
|
600
|
|
|
$text = $this->internalParse( $text, false, $frame ); |
|
601
|
|
|
return $text; |
|
602
|
|
|
} |
|
603
|
|
|
|
|
604
|
|
|
/** |
|
605
|
|
|
* Fully parse wikitext to fully parsed HTML. This recursive parser entry |
|
606
|
|
|
* point can be called from an extension tag hook. |
|
607
|
|
|
* |
|
608
|
|
|
* The output of this function is fully-parsed HTML that is safe for output. |
|
609
|
|
|
* If you're a parser tag hook, you might want to use recursiveTagParse() |
|
610
|
|
|
* instead. |
|
611
|
|
|
* |
|
612
|
|
|
* If $frame is not provided, then template variables (e.g., {{{1}}}) within |
|
613
|
|
|
* $text are not expanded |
|
614
|
|
|
* |
|
615
|
|
|
* @since 1.25 |
|
616
|
|
|
* |
|
617
|
|
|
* @param string $text Text extension wants to have parsed |
|
618
|
|
|
* @param bool|PPFrame $frame The frame to use for expanding any template variables |
|
619
|
|
|
* @return string Fully parsed HTML |
|
620
|
|
|
*/ |
|
621
|
|
|
public function recursiveTagParseFully( $text, $frame = false ) { |
|
622
|
|
|
$text = $this->recursiveTagParse( $text, $frame ); |
|
623
|
|
|
$text = $this->internalParseHalfParsed( $text, false ); |
|
624
|
|
|
return $text; |
|
625
|
|
|
} |
|
626
|
|
|
|
|
627
|
|
|
/** |
|
628
|
|
|
* Expand templates and variables in the text, producing valid, static wikitext. |
|
629
|
|
|
* Also removes comments. |
|
630
|
|
|
* Do not call this function recursively. |
|
631
|
|
|
* @param string $text |
|
632
|
|
|
* @param Title $title |
|
633
|
|
|
* @param ParserOptions $options |
|
634
|
|
|
* @param int|null $revid |
|
635
|
|
|
* @param bool|PPFrame $frame |
|
636
|
|
|
* @return mixed|string |
|
637
|
|
|
*/ |
|
638
|
|
|
public function preprocess( $text, Title $title = null, |
|
639
|
|
|
ParserOptions $options, $revid = null, $frame = false |
|
640
|
|
|
) { |
|
641
|
|
|
$magicScopeVariable = $this->lock(); |
|
|
|
|
|
|
642
|
|
|
$this->startParse( $title, $options, self::OT_PREPROCESS, true ); |
|
643
|
|
|
if ( $revid !== null ) { |
|
644
|
|
|
$this->mRevisionId = $revid; |
|
645
|
|
|
} |
|
646
|
|
|
Hooks::run( 'ParserBeforeStrip', [ &$this, &$text, &$this->mStripState ] ); |
|
647
|
|
|
Hooks::run( 'ParserAfterStrip', [ &$this, &$text, &$this->mStripState ] ); |
|
648
|
|
|
$text = $this->replaceVariables( $text, $frame ); |
|
649
|
|
|
$text = $this->mStripState->unstripBoth( $text ); |
|
650
|
|
|
return $text; |
|
651
|
|
|
} |
|
652
|
|
|
|
|
653
|
|
|
/** |
|
654
|
|
|
* Recursive parser entry point that can be called from an extension tag |
|
655
|
|
|
* hook. |
|
656
|
|
|
* |
|
657
|
|
|
* @param string $text Text to be expanded |
|
658
|
|
|
* @param bool|PPFrame $frame The frame to use for expanding any template variables |
|
659
|
|
|
* @return string |
|
660
|
|
|
* @since 1.19 |
|
661
|
|
|
*/ |
|
662
|
|
|
public function recursivePreprocess( $text, $frame = false ) { |
|
663
|
|
|
$text = $this->replaceVariables( $text, $frame ); |
|
664
|
|
|
$text = $this->mStripState->unstripBoth( $text ); |
|
665
|
|
|
return $text; |
|
666
|
|
|
} |
|
667
|
|
|
|
|
668
|
|
|
/** |
|
669
|
|
|
* Process the wikitext for the "?preload=" feature. (bug 5210) |
|
670
|
|
|
* |
|
671
|
|
|
* "<noinclude>", "<includeonly>" etc. are parsed as for template |
|
672
|
|
|
* transclusion, comments, templates, arguments, tags hooks and parser |
|
673
|
|
|
* functions are untouched. |
|
674
|
|
|
* |
|
675
|
|
|
* @param string $text |
|
676
|
|
|
* @param Title $title |
|
677
|
|
|
* @param ParserOptions $options |
|
678
|
|
|
* @param array $params |
|
679
|
|
|
* @return string |
|
680
|
|
|
*/ |
|
681
|
|
|
public function getPreloadText( $text, Title $title, ParserOptions $options, $params = [] ) { |
|
682
|
|
|
$msg = new RawMessage( $text ); |
|
683
|
|
|
$text = $msg->params( $params )->plain(); |
|
684
|
|
|
|
|
685
|
|
|
# Parser (re)initialisation |
|
686
|
|
|
$magicScopeVariable = $this->lock(); |
|
|
|
|
|
|
687
|
|
|
$this->startParse( $title, $options, self::OT_PLAIN, true ); |
|
688
|
|
|
|
|
689
|
|
|
$flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES; |
|
690
|
|
|
$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION ); |
|
691
|
|
|
$text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags ); |
|
692
|
|
|
$text = $this->mStripState->unstripBoth( $text ); |
|
693
|
|
|
return $text; |
|
694
|
|
|
} |
|
695
|
|
|
|
|
696
|
|
|
/** |
|
697
|
|
|
* Get a random string |
|
698
|
|
|
* |
|
699
|
|
|
* @return string |
|
700
|
|
|
* @deprecated since 1.26; use wfRandomString() instead. |
|
701
|
|
|
*/ |
|
702
|
|
|
public static function getRandomString() { |
|
703
|
|
|
wfDeprecated( __METHOD__, '1.26' ); |
|
704
|
|
|
return wfRandomString( 16 ); |
|
705
|
|
|
} |
|
706
|
|
|
|
|
707
|
|
|
/** |
|
708
|
|
|
* Set the current user. |
|
709
|
|
|
* Should only be used when doing pre-save transform. |
|
710
|
|
|
* |
|
711
|
|
|
* @param User|null $user User object or null (to reset) |
|
712
|
|
|
*/ |
|
713
|
|
|
public function setUser( $user ) { |
|
714
|
|
|
$this->mUser = $user; |
|
715
|
|
|
} |
|
716
|
|
|
|
|
717
|
|
|
/** |
|
718
|
|
|
* Accessor for mUniqPrefix. |
|
719
|
|
|
* |
|
720
|
|
|
* @return string |
|
721
|
|
|
* @deprecated since 1.26; use Parser::MARKER_PREFIX instead. |
|
722
|
|
|
*/ |
|
723
|
|
|
public function uniqPrefix() { |
|
724
|
|
|
wfDeprecated( __METHOD__, '1.26' ); |
|
725
|
|
|
return self::MARKER_PREFIX; |
|
726
|
|
|
} |
|
727
|
|
|
|
|
728
|
|
|
/** |
|
729
|
|
|
* Set the context title |
|
730
|
|
|
* |
|
731
|
|
|
* @param Title $t |
|
732
|
|
|
*/ |
|
733
|
|
|
public function setTitle( $t ) { |
|
734
|
|
|
if ( !$t ) { |
|
735
|
|
|
$t = Title::newFromText( 'NO TITLE' ); |
|
736
|
|
|
} |
|
737
|
|
|
|
|
738
|
|
|
if ( $t->hasFragment() ) { |
|
739
|
|
|
# Strip the fragment to avoid various odd effects |
|
740
|
|
|
$this->mTitle = $t->createFragmentTarget( '' ); |
|
|
|
|
|
|
741
|
|
|
} else { |
|
742
|
|
|
$this->mTitle = $t; |
|
743
|
|
|
} |
|
744
|
|
|
} |
|
745
|
|
|
|
|
746
|
|
|
/** |
|
747
|
|
|
* Accessor for the Title object |
|
748
|
|
|
* |
|
749
|
|
|
* @return Title |
|
750
|
|
|
*/ |
|
751
|
|
|
public function getTitle() { |
|
752
|
|
|
return $this->mTitle; |
|
753
|
|
|
} |
|
754
|
|
|
|
|
755
|
|
|
/** |
|
756
|
|
|
* Accessor/mutator for the Title object |
|
757
|
|
|
* |
|
758
|
|
|
* @param Title $x Title object or null to just get the current one |
|
759
|
|
|
* @return Title |
|
760
|
|
|
*/ |
|
761
|
|
|
public function Title( $x = null ) { |
|
762
|
|
|
return wfSetVar( $this->mTitle, $x ); |
|
763
|
|
|
} |
|
764
|
|
|
|
|
765
|
|
|
/** |
|
766
|
|
|
* Set the output type |
|
767
|
|
|
* |
|
768
|
|
|
* @param int $ot New value |
|
769
|
|
|
*/ |
|
770
|
|
|
public function setOutputType( $ot ) { |
|
771
|
|
|
$this->mOutputType = $ot; |
|
772
|
|
|
# Shortcut alias |
|
773
|
|
|
$this->ot = [ |
|
774
|
|
|
'html' => $ot == self::OT_HTML, |
|
775
|
|
|
'wiki' => $ot == self::OT_WIKI, |
|
776
|
|
|
'pre' => $ot == self::OT_PREPROCESS, |
|
777
|
|
|
'plain' => $ot == self::OT_PLAIN, |
|
778
|
|
|
]; |
|
779
|
|
|
} |
|
780
|
|
|
|
|
781
|
|
|
/** |
|
782
|
|
|
* Accessor/mutator for the output type |
|
783
|
|
|
* |
|
784
|
|
|
* @param int|null $x New value or null to just get the current one |
|
785
|
|
|
* @return int |
|
786
|
|
|
*/ |
|
787
|
|
|
public function OutputType( $x = null ) { |
|
788
|
|
|
return wfSetVar( $this->mOutputType, $x ); |
|
789
|
|
|
} |
|
790
|
|
|
|
|
791
|
|
|
/** |
|
792
|
|
|
* Get the ParserOutput object |
|
793
|
|
|
* |
|
794
|
|
|
* @return ParserOutput |
|
795
|
|
|
*/ |
|
796
|
|
|
public function getOutput() { |
|
797
|
|
|
return $this->mOutput; |
|
798
|
|
|
} |
|
799
|
|
|
|
|
800
|
|
|
/** |
|
801
|
|
|
* Get the ParserOptions object |
|
802
|
|
|
* |
|
803
|
|
|
* @return ParserOptions |
|
804
|
|
|
*/ |
|
805
|
|
|
public function getOptions() { |
|
806
|
|
|
return $this->mOptions; |
|
807
|
|
|
} |
|
808
|
|
|
|
|
809
|
|
|
/** |
|
810
|
|
|
* Accessor/mutator for the ParserOptions object |
|
811
|
|
|
* |
|
812
|
|
|
* @param ParserOptions $x New value or null to just get the current one |
|
813
|
|
|
* @return ParserOptions Current ParserOptions object |
|
814
|
|
|
*/ |
|
815
|
|
|
public function Options( $x = null ) { |
|
816
|
|
|
return wfSetVar( $this->mOptions, $x ); |
|
817
|
|
|
} |
|
818
|
|
|
|
|
819
|
|
|
/** |
|
820
|
|
|
* @return int |
|
821
|
|
|
*/ |
|
822
|
|
|
public function nextLinkID() { |
|
823
|
|
|
return $this->mLinkID++; |
|
824
|
|
|
} |
|
825
|
|
|
|
|
826
|
|
|
/** |
|
827
|
|
|
* @param int $id |
|
828
|
|
|
*/ |
|
829
|
|
|
public function setLinkID( $id ) { |
|
830
|
|
|
$this->mLinkID = $id; |
|
831
|
|
|
} |
|
832
|
|
|
|
|
833
|
|
|
/** |
|
834
|
|
|
* Get a language object for use in parser functions such as {{FORMATNUM:}} |
|
835
|
|
|
* @return Language |
|
836
|
|
|
*/ |
|
837
|
|
|
public function getFunctionLang() { |
|
838
|
|
|
return $this->getTargetLanguage(); |
|
839
|
|
|
} |
|
840
|
|
|
|
|
841
|
|
|
/** |
|
842
|
|
|
* Get the target language for the content being parsed. This is usually the |
|
843
|
|
|
* language that the content is in. |
|
844
|
|
|
* |
|
845
|
|
|
* @since 1.19 |
|
846
|
|
|
* |
|
847
|
|
|
* @throws MWException |
|
848
|
|
|
* @return Language |
|
849
|
|
|
*/ |
|
850
|
|
|
public function getTargetLanguage() { |
|
851
|
|
|
$target = $this->mOptions->getTargetLanguage(); |
|
852
|
|
|
|
|
853
|
|
|
if ( $target !== null ) { |
|
854
|
|
|
return $target; |
|
855
|
|
|
} elseif ( $this->mOptions->getInterfaceMessage() ) { |
|
856
|
|
|
return $this->mOptions->getUserLangObj(); |
|
857
|
|
|
} elseif ( is_null( $this->mTitle ) ) { |
|
858
|
|
|
throw new MWException( __METHOD__ . ': $this->mTitle is null' ); |
|
859
|
|
|
} |
|
860
|
|
|
|
|
861
|
|
|
return $this->mTitle->getPageLanguage(); |
|
862
|
|
|
} |
|
863
|
|
|
|
|
864
|
|
|
/** |
|
865
|
|
|
* Get the language object for language conversion |
|
866
|
|
|
* @return Language|null |
|
867
|
|
|
*/ |
|
868
|
|
|
public function getConverterLanguage() { |
|
869
|
|
|
return $this->getTargetLanguage(); |
|
870
|
|
|
} |
|
871
|
|
|
|
|
872
|
|
|
/** |
|
873
|
|
|
* Get a User object either from $this->mUser, if set, or from the |
|
874
|
|
|
* ParserOptions object otherwise |
|
875
|
|
|
* |
|
876
|
|
|
* @return User |
|
877
|
|
|
*/ |
|
878
|
|
|
public function getUser() { |
|
879
|
|
|
if ( !is_null( $this->mUser ) ) { |
|
880
|
|
|
return $this->mUser; |
|
881
|
|
|
} |
|
882
|
|
|
return $this->mOptions->getUser(); |
|
883
|
|
|
} |
|
884
|
|
|
|
|
885
|
|
|
/** |
|
886
|
|
|
* Get a preprocessor object |
|
887
|
|
|
* |
|
888
|
|
|
* @return Preprocessor |
|
889
|
|
|
*/ |
|
890
|
|
|
public function getPreprocessor() { |
|
891
|
|
|
if ( !isset( $this->mPreprocessor ) ) { |
|
892
|
|
|
$class = $this->mPreprocessorClass; |
|
|
|
|
|
|
893
|
|
|
$this->mPreprocessor = new $class( $this ); |
|
894
|
|
|
} |
|
895
|
|
|
return $this->mPreprocessor; |
|
896
|
|
|
} |
|
897
|
|
|
|
|
898
|
|
|
/** |
|
899
|
|
|
* Get a LinkRenderer instance to make links with |
|
900
|
|
|
* |
|
901
|
|
|
* @since 1.28 |
|
902
|
|
|
* @return LinkRenderer |
|
903
|
|
|
*/ |
|
904
|
|
|
public function getLinkRenderer() { |
|
905
|
|
|
if ( !$this->mLinkRenderer ) { |
|
906
|
|
|
$this->mLinkRenderer = MediaWikiServices::getInstance() |
|
907
|
|
|
->getLinkRendererFactory()->create(); |
|
908
|
|
|
$this->mLinkRenderer->setStubThreshold( |
|
909
|
|
|
$this->getOptions()->getStubThreshold() |
|
910
|
|
|
); |
|
911
|
|
|
} |
|
912
|
|
|
|
|
913
|
|
|
return $this->mLinkRenderer; |
|
914
|
|
|
} |
|
915
|
|
|
|
|
916
|
|
|
/** |
|
917
|
|
|
* Replaces all occurrences of HTML-style comments and the given tags |
|
918
|
|
|
* in the text with a random marker and returns the next text. The output |
|
919
|
|
|
* parameter $matches will be an associative array filled with data in |
|
920
|
|
|
* the form: |
|
921
|
|
|
* |
|
922
|
|
|
* @code |
|
923
|
|
|
* 'UNIQ-xxxxx' => array( |
|
924
|
|
|
* 'element', |
|
925
|
|
|
* 'tag content', |
|
926
|
|
|
* array( 'param' => 'x' ), |
|
927
|
|
|
* '<element param="x">tag content</element>' ) ) |
|
928
|
|
|
* @endcode |
|
929
|
|
|
* |
|
930
|
|
|
* @param array $elements List of element names. Comments are always extracted. |
|
931
|
|
|
* @param string $text Source text string. |
|
932
|
|
|
* @param array $matches Out parameter, Array: extracted tags |
|
933
|
|
|
* @param string|null $uniq_prefix |
|
934
|
|
|
* @return string Stripped text |
|
935
|
|
|
* @since 1.26 The uniq_prefix argument is deprecated. |
|
936
|
|
|
*/ |
|
937
|
|
|
public static function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = null ) { |
|
938
|
|
|
if ( $uniq_prefix !== null ) { |
|
939
|
|
|
wfDeprecated( __METHOD__ . ' called with $prefix argument', '1.26' ); |
|
940
|
|
|
} |
|
941
|
|
|
static $n = 1; |
|
942
|
|
|
$stripped = ''; |
|
943
|
|
|
$matches = []; |
|
944
|
|
|
|
|
945
|
|
|
$taglist = implode( '|', $elements ); |
|
946
|
|
|
$start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i"; |
|
947
|
|
|
|
|
948
|
|
|
while ( $text != '' ) { |
|
949
|
|
|
$p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE ); |
|
950
|
|
|
$stripped .= $p[0]; |
|
951
|
|
|
if ( count( $p ) < 5 ) { |
|
952
|
|
|
break; |
|
953
|
|
|
} |
|
954
|
|
|
if ( count( $p ) > 5 ) { |
|
955
|
|
|
# comment |
|
956
|
|
|
$element = $p[4]; |
|
957
|
|
|
$attributes = ''; |
|
958
|
|
|
$close = ''; |
|
959
|
|
|
$inside = $p[5]; |
|
960
|
|
|
} else { |
|
961
|
|
|
# tag |
|
962
|
|
|
$element = $p[1]; |
|
963
|
|
|
$attributes = $p[2]; |
|
964
|
|
|
$close = $p[3]; |
|
965
|
|
|
$inside = $p[4]; |
|
966
|
|
|
} |
|
967
|
|
|
|
|
968
|
|
|
$marker = self::MARKER_PREFIX . "-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX; |
|
969
|
|
|
$stripped .= $marker; |
|
970
|
|
|
|
|
971
|
|
|
if ( $close === '/>' ) { |
|
972
|
|
|
# Empty element tag, <tag /> |
|
973
|
|
|
$content = null; |
|
974
|
|
|
$text = $inside; |
|
975
|
|
|
$tail = null; |
|
976
|
|
|
} else { |
|
977
|
|
|
if ( $element === '!--' ) { |
|
978
|
|
|
$end = '/(-->)/'; |
|
979
|
|
|
} else { |
|
980
|
|
|
$end = "/(<\\/$element\\s*>)/i"; |
|
981
|
|
|
} |
|
982
|
|
|
$q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE ); |
|
983
|
|
|
$content = $q[0]; |
|
984
|
|
|
if ( count( $q ) < 3 ) { |
|
985
|
|
|
# No end tag -- let it run out to the end of the text. |
|
986
|
|
|
$tail = ''; |
|
987
|
|
|
$text = ''; |
|
988
|
|
|
} else { |
|
989
|
|
|
$tail = $q[1]; |
|
990
|
|
|
$text = $q[2]; |
|
991
|
|
|
} |
|
992
|
|
|
} |
|
993
|
|
|
|
|
994
|
|
|
$matches[$marker] = [ $element, |
|
995
|
|
|
$content, |
|
996
|
|
|
Sanitizer::decodeTagAttributes( $attributes ), |
|
997
|
|
|
"<$element$attributes$close$content$tail" ]; |
|
998
|
|
|
} |
|
999
|
|
|
return $stripped; |
|
1000
|
|
|
} |
|
1001
|
|
|
|
|
1002
|
|
|
/** |
|
1003
|
|
|
* Get a list of strippable XML-like elements |
|
1004
|
|
|
* |
|
1005
|
|
|
* @return array |
|
1006
|
|
|
*/ |
|
1007
|
|
|
public function getStripList() { |
|
1008
|
|
|
return $this->mStripList; |
|
1009
|
|
|
} |
|
1010
|
|
|
|
|
1011
|
|
|
/** |
|
1012
|
|
|
* Add an item to the strip state |
|
1013
|
|
|
* Returns the unique tag which must be inserted into the stripped text |
|
1014
|
|
|
* The tag will be replaced with the original text in unstrip() |
|
1015
|
|
|
* |
|
1016
|
|
|
* @param string $text |
|
1017
|
|
|
* |
|
1018
|
|
|
* @return string |
|
1019
|
|
|
*/ |
|
1020
|
|
|
public function insertStripItem( $text ) { |
|
1021
|
|
|
$marker = self::MARKER_PREFIX . "-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX; |
|
1022
|
|
|
$this->mMarkerIndex++; |
|
1023
|
|
|
$this->mStripState->addGeneral( $marker, $text ); |
|
1024
|
|
|
return $marker; |
|
1025
|
|
|
} |
|
1026
|
|
|
|
|
1027
|
|
|
/** |
|
1028
|
|
|
* parse the wiki syntax used to render tables |
|
1029
|
|
|
* |
|
1030
|
|
|
* @private |
|
1031
|
|
|
* @param string $text |
|
1032
|
|
|
* @return string |
|
1033
|
|
|
*/ |
|
1034
|
|
|
public function doTableStuff( $text ) { |
|
1035
|
|
|
|
|
1036
|
|
|
$lines = StringUtils::explode( "\n", $text ); |
|
1037
|
|
|
$out = ''; |
|
1038
|
|
|
$td_history = []; # Is currently a td tag open? |
|
1039
|
|
|
$last_tag_history = []; # Save history of last lag activated (td, th or caption) |
|
1040
|
|
|
$tr_history = []; # Is currently a tr tag open? |
|
1041
|
|
|
$tr_attributes = []; # history of tr attributes |
|
1042
|
|
|
$has_opened_tr = []; # Did this table open a <tr> element? |
|
1043
|
|
|
$indent_level = 0; # indent level of the table |
|
1044
|
|
|
|
|
1045
|
|
|
foreach ( $lines as $outLine ) { |
|
1046
|
|
|
$line = trim( $outLine ); |
|
1047
|
|
|
|
|
1048
|
|
|
if ( $line === '' ) { # empty line, go to next line |
|
1049
|
|
|
$out .= $outLine . "\n"; |
|
1050
|
|
|
continue; |
|
1051
|
|
|
} |
|
1052
|
|
|
|
|
1053
|
|
|
$first_character = $line[0]; |
|
1054
|
|
|
$first_two = substr( $line, 0, 2 ); |
|
1055
|
|
|
$matches = []; |
|
1056
|
|
|
|
|
1057
|
|
|
if ( preg_match( '/^(:*)\s*\{\|(.*)$/', $line, $matches ) ) { |
|
1058
|
|
|
# First check if we are starting a new table |
|
1059
|
|
|
$indent_level = strlen( $matches[1] ); |
|
1060
|
|
|
|
|
1061
|
|
|
$attributes = $this->mStripState->unstripBoth( $matches[2] ); |
|
1062
|
|
|
$attributes = Sanitizer::fixTagAttributes( $attributes, 'table' ); |
|
1063
|
|
|
|
|
1064
|
|
|
$outLine = str_repeat( '<dl><dd>', $indent_level ) . "<table{$attributes}>"; |
|
1065
|
|
|
array_push( $td_history, false ); |
|
1066
|
|
|
array_push( $last_tag_history, '' ); |
|
1067
|
|
|
array_push( $tr_history, false ); |
|
1068
|
|
|
array_push( $tr_attributes, '' ); |
|
1069
|
|
|
array_push( $has_opened_tr, false ); |
|
1070
|
|
|
} elseif ( count( $td_history ) == 0 ) { |
|
1071
|
|
|
# Don't do any of the following |
|
1072
|
|
|
$out .= $outLine . "\n"; |
|
1073
|
|
|
continue; |
|
1074
|
|
|
} elseif ( $first_two === '|}' ) { |
|
1075
|
|
|
# We are ending a table |
|
1076
|
|
|
$line = '</table>' . substr( $line, 2 ); |
|
1077
|
|
|
$last_tag = array_pop( $last_tag_history ); |
|
1078
|
|
|
|
|
1079
|
|
|
if ( !array_pop( $has_opened_tr ) ) { |
|
1080
|
|
|
$line = "<tr><td></td></tr>{$line}"; |
|
1081
|
|
|
} |
|
1082
|
|
|
|
|
1083
|
|
|
if ( array_pop( $tr_history ) ) { |
|
1084
|
|
|
$line = "</tr>{$line}"; |
|
1085
|
|
|
} |
|
1086
|
|
|
|
|
1087
|
|
|
if ( array_pop( $td_history ) ) { |
|
1088
|
|
|
$line = "</{$last_tag}>{$line}"; |
|
1089
|
|
|
} |
|
1090
|
|
|
array_pop( $tr_attributes ); |
|
1091
|
|
|
$outLine = $line . str_repeat( '</dd></dl>', $indent_level ); |
|
1092
|
|
|
} elseif ( $first_two === '|-' ) { |
|
1093
|
|
|
# Now we have a table row |
|
1094
|
|
|
$line = preg_replace( '#^\|-+#', '', $line ); |
|
1095
|
|
|
|
|
1096
|
|
|
# Whats after the tag is now only attributes |
|
1097
|
|
|
$attributes = $this->mStripState->unstripBoth( $line ); |
|
1098
|
|
|
$attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' ); |
|
1099
|
|
|
array_pop( $tr_attributes ); |
|
1100
|
|
|
array_push( $tr_attributes, $attributes ); |
|
1101
|
|
|
|
|
1102
|
|
|
$line = ''; |
|
1103
|
|
|
$last_tag = array_pop( $last_tag_history ); |
|
1104
|
|
|
array_pop( $has_opened_tr ); |
|
1105
|
|
|
array_push( $has_opened_tr, true ); |
|
1106
|
|
|
|
|
1107
|
|
|
if ( array_pop( $tr_history ) ) { |
|
1108
|
|
|
$line = '</tr>'; |
|
1109
|
|
|
} |
|
1110
|
|
|
|
|
1111
|
|
|
if ( array_pop( $td_history ) ) { |
|
1112
|
|
|
$line = "</{$last_tag}>{$line}"; |
|
1113
|
|
|
} |
|
1114
|
|
|
|
|
1115
|
|
|
$outLine = $line; |
|
1116
|
|
|
array_push( $tr_history, false ); |
|
1117
|
|
|
array_push( $td_history, false ); |
|
1118
|
|
|
array_push( $last_tag_history, '' ); |
|
1119
|
|
|
} elseif ( $first_character === '|' |
|
1120
|
|
|
|| $first_character === '!' |
|
1121
|
|
|
|| $first_two === '|+' |
|
1122
|
|
|
) { |
|
1123
|
|
|
# This might be cell elements, td, th or captions |
|
1124
|
|
|
if ( $first_two === '|+' ) { |
|
1125
|
|
|
$first_character = '+'; |
|
1126
|
|
|
$line = substr( $line, 2 ); |
|
1127
|
|
|
} else { |
|
1128
|
|
|
$line = substr( $line, 1 ); |
|
1129
|
|
|
} |
|
1130
|
|
|
|
|
1131
|
|
|
// Implies both are valid for table headings. |
|
1132
|
|
|
if ( $first_character === '!' ) { |
|
1133
|
|
|
$line = StringUtils::replaceMarkup( '!!', '||', $line ); |
|
1134
|
|
|
} |
|
1135
|
|
|
|
|
1136
|
|
|
# Split up multiple cells on the same line. |
|
1137
|
|
|
# FIXME : This can result in improper nesting of tags processed |
|
1138
|
|
|
# by earlier parser steps. |
|
1139
|
|
|
$cells = explode( '||', $line ); |
|
1140
|
|
|
|
|
1141
|
|
|
$outLine = ''; |
|
1142
|
|
|
|
|
1143
|
|
|
# Loop through each table cell |
|
1144
|
|
|
foreach ( $cells as $cell ) { |
|
1145
|
|
|
$previous = ''; |
|
1146
|
|
|
if ( $first_character !== '+' ) { |
|
1147
|
|
|
$tr_after = array_pop( $tr_attributes ); |
|
1148
|
|
|
if ( !array_pop( $tr_history ) ) { |
|
1149
|
|
|
$previous = "<tr{$tr_after}>\n"; |
|
1150
|
|
|
} |
|
1151
|
|
|
array_push( $tr_history, true ); |
|
1152
|
|
|
array_push( $tr_attributes, '' ); |
|
1153
|
|
|
array_pop( $has_opened_tr ); |
|
1154
|
|
|
array_push( $has_opened_tr, true ); |
|
1155
|
|
|
} |
|
1156
|
|
|
|
|
1157
|
|
|
$last_tag = array_pop( $last_tag_history ); |
|
1158
|
|
|
|
|
1159
|
|
|
if ( array_pop( $td_history ) ) { |
|
1160
|
|
|
$previous = "</{$last_tag}>\n{$previous}"; |
|
1161
|
|
|
} |
|
1162
|
|
|
|
|
1163
|
|
|
if ( $first_character === '|' ) { |
|
1164
|
|
|
$last_tag = 'td'; |
|
1165
|
|
|
} elseif ( $first_character === '!' ) { |
|
1166
|
|
|
$last_tag = 'th'; |
|
1167
|
|
|
} elseif ( $first_character === '+' ) { |
|
1168
|
|
|
$last_tag = 'caption'; |
|
1169
|
|
|
} else { |
|
1170
|
|
|
$last_tag = ''; |
|
1171
|
|
|
} |
|
1172
|
|
|
|
|
1173
|
|
|
array_push( $last_tag_history, $last_tag ); |
|
1174
|
|
|
|
|
1175
|
|
|
# A cell could contain both parameters and data |
|
1176
|
|
|
$cell_data = explode( '|', $cell, 2 ); |
|
1177
|
|
|
|
|
1178
|
|
|
# Bug 553: Note that a '|' inside an invalid link should not |
|
1179
|
|
|
# be mistaken as delimiting cell parameters |
|
1180
|
|
|
if ( strpos( $cell_data[0], '[[' ) !== false ) { |
|
1181
|
|
|
$cell = "{$previous}<{$last_tag}>{$cell}"; |
|
1182
|
|
|
} elseif ( count( $cell_data ) == 1 ) { |
|
1183
|
|
|
$cell = "{$previous}<{$last_tag}>{$cell_data[0]}"; |
|
1184
|
|
|
} else { |
|
1185
|
|
|
$attributes = $this->mStripState->unstripBoth( $cell_data[0] ); |
|
1186
|
|
|
$attributes = Sanitizer::fixTagAttributes( $attributes, $last_tag ); |
|
1187
|
|
|
$cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}"; |
|
1188
|
|
|
} |
|
1189
|
|
|
|
|
1190
|
|
|
$outLine .= $cell; |
|
1191
|
|
|
array_push( $td_history, true ); |
|
1192
|
|
|
} |
|
1193
|
|
|
} |
|
1194
|
|
|
$out .= $outLine . "\n"; |
|
1195
|
|
|
} |
|
1196
|
|
|
|
|
1197
|
|
|
# Closing open td, tr && table |
|
1198
|
|
|
while ( count( $td_history ) > 0 ) { |
|
1199
|
|
|
if ( array_pop( $td_history ) ) { |
|
1200
|
|
|
$out .= "</td>\n"; |
|
1201
|
|
|
} |
|
1202
|
|
|
if ( array_pop( $tr_history ) ) { |
|
1203
|
|
|
$out .= "</tr>\n"; |
|
1204
|
|
|
} |
|
1205
|
|
|
if ( !array_pop( $has_opened_tr ) ) { |
|
1206
|
|
|
$out .= "<tr><td></td></tr>\n"; |
|
1207
|
|
|
} |
|
1208
|
|
|
|
|
1209
|
|
|
$out .= "</table>\n"; |
|
1210
|
|
|
} |
|
1211
|
|
|
|
|
1212
|
|
|
# Remove trailing line-ending (b/c) |
|
1213
|
|
View Code Duplication |
if ( substr( $out, -1 ) === "\n" ) { |
|
1214
|
|
|
$out = substr( $out, 0, -1 ); |
|
1215
|
|
|
} |
|
1216
|
|
|
|
|
1217
|
|
|
# special case: don't return empty table |
|
1218
|
|
|
if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) { |
|
1219
|
|
|
$out = ''; |
|
1220
|
|
|
} |
|
1221
|
|
|
|
|
1222
|
|
|
return $out; |
|
1223
|
|
|
} |
|
1224
|
|
|
|
|
1225
|
|
|
/** |
|
1226
|
|
|
* Helper function for parse() that transforms wiki markup into half-parsed |
|
1227
|
|
|
* HTML. Only called for $mOutputType == self::OT_HTML. |
|
1228
|
|
|
* |
|
1229
|
|
|
* @private |
|
1230
|
|
|
* |
|
1231
|
|
|
* @param string $text The text to parse |
|
1232
|
|
|
* @param bool $isMain Whether this is being called from the main parse() function |
|
1233
|
|
|
* @param PPFrame|bool $frame A pre-processor frame |
|
1234
|
|
|
* |
|
1235
|
|
|
* @return string |
|
1236
|
|
|
*/ |
|
1237
|
|
|
public function internalParse( $text, $isMain = true, $frame = false ) { |
|
1238
|
|
|
|
|
1239
|
|
|
$origText = $text; |
|
1240
|
|
|
|
|
1241
|
|
|
# Hook to suspend the parser in this state |
|
1242
|
|
|
if ( !Hooks::run( 'ParserBeforeInternalParse', [ &$this, &$text, &$this->mStripState ] ) ) { |
|
1243
|
|
|
return $text; |
|
1244
|
|
|
} |
|
1245
|
|
|
|
|
1246
|
|
|
# if $frame is provided, then use $frame for replacing any variables |
|
1247
|
|
|
if ( $frame ) { |
|
1248
|
|
|
# use frame depth to infer how include/noinclude tags should be handled |
|
1249
|
|
|
# depth=0 means this is the top-level document; otherwise it's an included document |
|
1250
|
|
|
if ( !$frame->depth ) { |
|
1251
|
|
|
$flag = 0; |
|
1252
|
|
|
} else { |
|
1253
|
|
|
$flag = Parser::PTD_FOR_INCLUSION; |
|
1254
|
|
|
} |
|
1255
|
|
|
$dom = $this->preprocessToDom( $text, $flag ); |
|
1256
|
|
|
$text = $frame->expand( $dom ); |
|
|
|
|
|
|
1257
|
|
|
} else { |
|
1258
|
|
|
# if $frame is not provided, then use old-style replaceVariables |
|
1259
|
|
|
$text = $this->replaceVariables( $text ); |
|
1260
|
|
|
} |
|
1261
|
|
|
|
|
1262
|
|
|
Hooks::run( 'InternalParseBeforeSanitize', [ &$this, &$text, &$this->mStripState ] ); |
|
1263
|
|
|
$text = Sanitizer::removeHTMLtags( |
|
1264
|
|
|
$text, |
|
1265
|
|
|
[ &$this, 'attributeStripCallback' ], |
|
1266
|
|
|
false, |
|
1267
|
|
|
array_keys( $this->mTransparentTagHooks ) |
|
1268
|
|
|
); |
|
1269
|
|
|
Hooks::run( 'InternalParseBeforeLinks', [ &$this, &$text, &$this->mStripState ] ); |
|
1270
|
|
|
|
|
1271
|
|
|
# Tables need to come after variable replacement for things to work |
|
1272
|
|
|
# properly; putting them before other transformations should keep |
|
1273
|
|
|
# exciting things like link expansions from showing up in surprising |
|
1274
|
|
|
# places. |
|
1275
|
|
|
$text = $this->doTableStuff( $text ); |
|
1276
|
|
|
|
|
1277
|
|
|
$text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text ); |
|
1278
|
|
|
|
|
1279
|
|
|
$text = $this->doDoubleUnderscore( $text ); |
|
1280
|
|
|
|
|
1281
|
|
|
$text = $this->doHeadings( $text ); |
|
1282
|
|
|
$text = $this->replaceInternalLinks( $text ); |
|
1283
|
|
|
$text = $this->doAllQuotes( $text ); |
|
1284
|
|
|
$text = $this->replaceExternalLinks( $text ); |
|
1285
|
|
|
|
|
1286
|
|
|
# replaceInternalLinks may sometimes leave behind |
|
1287
|
|
|
# absolute URLs, which have to be masked to hide them from replaceExternalLinks |
|
1288
|
|
|
$text = str_replace( self::MARKER_PREFIX . 'NOPARSE', '', $text ); |
|
1289
|
|
|
|
|
1290
|
|
|
$text = $this->doMagicLinks( $text ); |
|
1291
|
|
|
$text = $this->formatHeadings( $text, $origText, $isMain ); |
|
1292
|
|
|
|
|
1293
|
|
|
return $text; |
|
1294
|
|
|
} |
|
1295
|
|
|
|
|
1296
|
|
|
/** |
|
1297
|
|
|
* Helper function for parse() that transforms half-parsed HTML into fully |
|
1298
|
|
|
* parsed HTML. |
|
1299
|
|
|
* |
|
1300
|
|
|
* @param string $text |
|
1301
|
|
|
* @param bool $isMain |
|
1302
|
|
|
* @param bool $linestart |
|
1303
|
|
|
* @return string |
|
1304
|
|
|
*/ |
|
1305
|
|
|
private function internalParseHalfParsed( $text, $isMain = true, $linestart = true ) { |
|
1306
|
|
|
$text = $this->mStripState->unstripGeneral( $text ); |
|
1307
|
|
|
|
|
1308
|
|
|
if ( $isMain ) { |
|
1309
|
|
|
Hooks::run( 'ParserAfterUnstrip', [ &$this, &$text ] ); |
|
1310
|
|
|
} |
|
1311
|
|
|
|
|
1312
|
|
|
# Clean up special characters, only run once, next-to-last before doBlockLevels |
|
1313
|
|
|
$fixtags = [ |
|
1314
|
|
|
# french spaces, last one Guillemet-left |
|
1315
|
|
|
# only if there is something before the space |
|
1316
|
|
|
'/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1 ', |
|
1317
|
|
|
# french spaces, Guillemet-right |
|
1318
|
|
|
'/(\\302\\253) /' => '\\1 ', |
|
1319
|
|
|
'/ (!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874. |
|
1320
|
|
|
]; |
|
1321
|
|
|
$text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text ); |
|
1322
|
|
|
|
|
1323
|
|
|
$text = $this->doBlockLevels( $text, $linestart ); |
|
1324
|
|
|
|
|
1325
|
|
|
$this->replaceLinkHolders( $text ); |
|
1326
|
|
|
|
|
1327
|
|
|
/** |
|
1328
|
|
|
* The input doesn't get language converted if |
|
1329
|
|
|
* a) It's disabled |
|
1330
|
|
|
* b) Content isn't converted |
|
1331
|
|
|
* c) It's a conversion table |
|
1332
|
|
|
* d) it is an interface message (which is in the user language) |
|
1333
|
|
|
*/ |
|
1334
|
|
|
if ( !( $this->mOptions->getDisableContentConversion() |
|
1335
|
|
|
|| isset( $this->mDoubleUnderscores['nocontentconvert'] ) ) |
|
1336
|
|
|
) { |
|
1337
|
|
|
if ( !$this->mOptions->getInterfaceMessage() ) { |
|
1338
|
|
|
# The position of the convert() call should not be changed. it |
|
1339
|
|
|
# assumes that the links are all replaced and the only thing left |
|
1340
|
|
|
# is the <nowiki> mark. |
|
1341
|
|
|
$text = $this->getConverterLanguage()->convert( $text ); |
|
1342
|
|
|
} |
|
1343
|
|
|
} |
|
1344
|
|
|
|
|
1345
|
|
|
$text = $this->mStripState->unstripNoWiki( $text ); |
|
1346
|
|
|
|
|
1347
|
|
|
if ( $isMain ) { |
|
1348
|
|
|
Hooks::run( 'ParserBeforeTidy', [ &$this, &$text ] ); |
|
1349
|
|
|
} |
|
1350
|
|
|
|
|
1351
|
|
|
$text = $this->replaceTransparentTags( $text ); |
|
1352
|
|
|
$text = $this->mStripState->unstripGeneral( $text ); |
|
1353
|
|
|
|
|
1354
|
|
|
$text = Sanitizer::normalizeCharReferences( $text ); |
|
1355
|
|
|
|
|
1356
|
|
|
if ( MWTidy::isEnabled() && $this->mOptions->getTidy() ) { |
|
1357
|
|
|
$text = MWTidy::tidy( $text ); |
|
1358
|
|
|
$this->mOutput->addModuleStyles( MWTidy::getModuleStyles() ); |
|
1359
|
|
|
} else { |
|
1360
|
|
|
# attempt to sanitize at least some nesting problems |
|
1361
|
|
|
# (bug #2702 and quite a few others) |
|
1362
|
|
|
$tidyregs = [ |
|
1363
|
|
|
# ''Something [http://www.cool.com cool''] --> |
|
1364
|
|
|
# <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a> |
|
1365
|
|
|
'/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' => |
|
1366
|
|
|
'\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9', |
|
1367
|
|
|
# fix up an anchor inside another anchor, only |
|
1368
|
|
|
# at least for a single single nested link (bug 3695) |
|
1369
|
|
|
'/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' => |
|
1370
|
|
|
'\\1\\2</a>\\3</a>\\1\\4</a>', |
|
1371
|
|
|
# fix div inside inline elements- doBlockLevels won't wrap a line which |
|
1372
|
|
|
# contains a div, so fix it up here; replace |
|
1373
|
|
|
# div with escaped text |
|
1374
|
|
|
'/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' => |
|
1375
|
|
|
'\\1\\3<div\\5>\\6</div>\\8\\9', |
|
1376
|
|
|
# remove empty italic or bold tag pairs, some |
|
1377
|
|
|
# introduced by rules above |
|
1378
|
|
|
'/<([bi])><\/\\1>/' => '', |
|
1379
|
|
|
]; |
|
1380
|
|
|
|
|
1381
|
|
|
$text = preg_replace( |
|
1382
|
|
|
array_keys( $tidyregs ), |
|
1383
|
|
|
array_values( $tidyregs ), |
|
1384
|
|
|
$text ); |
|
1385
|
|
|
} |
|
1386
|
|
|
|
|
1387
|
|
|
if ( $isMain ) { |
|
1388
|
|
|
Hooks::run( 'ParserAfterTidy', [ &$this, &$text ] ); |
|
1389
|
|
|
} |
|
1390
|
|
|
|
|
1391
|
|
|
return $text; |
|
1392
|
|
|
} |
|
1393
|
|
|
|
|
1394
|
|
|
/** |
|
1395
|
|
|
* Replace special strings like "ISBN xxx" and "RFC xxx" with |
|
1396
|
|
|
* magic external links. |
|
1397
|
|
|
* |
|
1398
|
|
|
* DML |
|
1399
|
|
|
* @private |
|
1400
|
|
|
* |
|
1401
|
|
|
* @param string $text |
|
1402
|
|
|
* |
|
1403
|
|
|
* @return string |
|
1404
|
|
|
*/ |
|
1405
|
|
|
public function doMagicLinks( $text ) { |
|
1406
|
|
|
$prots = wfUrlProtocolsWithoutProtRel(); |
|
1407
|
|
|
$urlChar = self::EXT_LINK_URL_CLASS; |
|
1408
|
|
|
$addr = self::EXT_LINK_ADDR; |
|
1409
|
|
|
$space = self::SPACE_NOT_NL; # non-newline space |
|
1410
|
|
|
$spdash = "(?:-|$space)"; # a dash or a non-newline space |
|
1411
|
|
|
$spaces = "$space++"; # possessive match of 1 or more spaces |
|
1412
|
|
|
$text = preg_replace_callback( |
|
1413
|
|
|
'!(?: # Start cases |
|
1414
|
|
|
(<a[ \t\r\n>].*?</a>) | # m[1]: Skip link text |
|
1415
|
|
|
(<.*?>) | # m[2]: Skip stuff inside |
|
1416
|
|
|
# HTML elements' . " |
|
1417
|
|
|
(\b(?i:$prots)($addr$urlChar*)) | # m[3]: Free external links |
|
1418
|
|
|
# m[4]: Post-protocol path |
|
1419
|
|
|
\b(?:RFC|PMID) $spaces # m[5]: RFC or PMID, capture number |
|
1420
|
|
|
([0-9]+)\b | |
|
1421
|
|
|
\bISBN $spaces ( # m[6]: ISBN, capture number |
|
1422
|
|
|
(?: 97[89] $spdash? )? # optional 13-digit ISBN prefix |
|
1423
|
|
|
(?: [0-9] $spdash? ){9} # 9 digits with opt. delimiters |
|
1424
|
|
|
[0-9Xx] # check digit |
|
1425
|
|
|
)\b |
|
1426
|
|
|
)!xu", [ &$this, 'magicLinkCallback' ], $text ); |
|
1427
|
|
|
return $text; |
|
1428
|
|
|
} |
|
1429
|
|
|
|
|
1430
|
|
|
/** |
|
1431
|
|
|
* @throws MWException |
|
1432
|
|
|
* @param array $m |
|
1433
|
|
|
* @return HTML|string |
|
1434
|
|
|
*/ |
|
1435
|
|
|
public function magicLinkCallback( $m ) { |
|
1436
|
|
|
if ( isset( $m[1] ) && $m[1] !== '' ) { |
|
1437
|
|
|
# Skip anchor |
|
1438
|
|
|
return $m[0]; |
|
1439
|
|
|
} elseif ( isset( $m[2] ) && $m[2] !== '' ) { |
|
1440
|
|
|
# Skip HTML element |
|
1441
|
|
|
return $m[0]; |
|
1442
|
|
|
} elseif ( isset( $m[3] ) && $m[3] !== '' ) { |
|
1443
|
|
|
# Free external link |
|
1444
|
|
|
return $this->makeFreeExternalLink( $m[0], strlen( $m[4] ) ); |
|
1445
|
|
|
} elseif ( isset( $m[5] ) && $m[5] !== '' ) { |
|
1446
|
|
|
# RFC or PMID |
|
1447
|
|
|
if ( substr( $m[0], 0, 3 ) === 'RFC' ) { |
|
1448
|
|
|
$keyword = 'RFC'; |
|
1449
|
|
|
$urlmsg = 'rfcurl'; |
|
1450
|
|
|
$cssClass = 'mw-magiclink-rfc'; |
|
1451
|
|
|
$id = $m[5]; |
|
1452
|
|
|
} elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) { |
|
1453
|
|
|
$keyword = 'PMID'; |
|
1454
|
|
|
$urlmsg = 'pubmedurl'; |
|
1455
|
|
|
$cssClass = 'mw-magiclink-pmid'; |
|
1456
|
|
|
$id = $m[5]; |
|
1457
|
|
|
} else { |
|
1458
|
|
|
throw new MWException( __METHOD__ . ': unrecognised match type "' . |
|
1459
|
|
|
substr( $m[0], 0, 20 ) . '"' ); |
|
1460
|
|
|
} |
|
1461
|
|
|
$url = wfMessage( $urlmsg, $id )->inContentLanguage()->text(); |
|
1462
|
|
|
return Linker::makeExternalLink( $url, "{$keyword} {$id}", true, $cssClass, [], $this->mTitle ); |
|
1463
|
|
|
} elseif ( isset( $m[6] ) && $m[6] !== '' ) { |
|
1464
|
|
|
# ISBN |
|
1465
|
|
|
$isbn = $m[6]; |
|
1466
|
|
|
$space = self::SPACE_NOT_NL; # non-newline space |
|
1467
|
|
|
$isbn = preg_replace( "/$space/", ' ', $isbn ); |
|
1468
|
|
|
$num = strtr( $isbn, [ |
|
1469
|
|
|
'-' => '', |
|
1470
|
|
|
' ' => '', |
|
1471
|
|
|
'x' => 'X', |
|
1472
|
|
|
] ); |
|
1473
|
|
|
$titleObj = SpecialPage::getTitleFor( 'Booksources', $num ); |
|
1474
|
|
|
return '<a href="' . |
|
1475
|
|
|
htmlspecialchars( $titleObj->getLocalURL() ) . |
|
1476
|
|
|
"\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>"; |
|
1477
|
|
|
} else { |
|
1478
|
|
|
return $m[0]; |
|
1479
|
|
|
} |
|
1480
|
|
|
} |
|
1481
|
|
|
|
|
1482
|
|
|
/** |
|
1483
|
|
|
* Make a free external link, given a user-supplied URL |
|
1484
|
|
|
* |
|
1485
|
|
|
* @param string $url |
|
1486
|
|
|
* @param int $numPostProto |
|
1487
|
|
|
* The number of characters after the protocol. |
|
1488
|
|
|
* @return string HTML |
|
1489
|
|
|
* @private |
|
1490
|
|
|
*/ |
|
1491
|
|
|
public function makeFreeExternalLink( $url, $numPostProto ) { |
|
1492
|
|
|
$trail = ''; |
|
1493
|
|
|
|
|
1494
|
|
|
# The characters '<' and '>' (which were escaped by |
|
1495
|
|
|
# removeHTMLtags()) should not be included in |
|
1496
|
|
|
# URLs, per RFC 2396. |
|
1497
|
|
|
# Make terminate a URL as well (bug T84937) |
|
1498
|
|
|
$m2 = []; |
|
1499
|
|
View Code Duplication |
if ( preg_match( |
|
1500
|
|
|
'/&(lt|gt|nbsp|#x0*(3[CcEe]|[Aa]0)|#0*(60|62|160));/', |
|
1501
|
|
|
$url, |
|
1502
|
|
|
$m2, |
|
1503
|
|
|
PREG_OFFSET_CAPTURE |
|
1504
|
|
|
) ) { |
|
1505
|
|
|
$trail = substr( $url, $m2[0][1] ) . $trail; |
|
1506
|
|
|
$url = substr( $url, 0, $m2[0][1] ); |
|
1507
|
|
|
} |
|
1508
|
|
|
|
|
1509
|
|
|
# Move trailing punctuation to $trail |
|
1510
|
|
|
$sep = ',;\.:!?'; |
|
1511
|
|
|
# If there is no left bracket, then consider right brackets fair game too |
|
1512
|
|
|
if ( strpos( $url, '(' ) === false ) { |
|
1513
|
|
|
$sep .= ')'; |
|
1514
|
|
|
} |
|
1515
|
|
|
|
|
1516
|
|
|
$urlRev = strrev( $url ); |
|
1517
|
|
|
$numSepChars = strspn( $urlRev, $sep ); |
|
1518
|
|
|
# Don't break a trailing HTML entity by moving the ; into $trail |
|
1519
|
|
|
# This is in hot code, so use substr_compare to avoid having to |
|
1520
|
|
|
# create a new string object for the comparison |
|
1521
|
|
|
if ( $numSepChars && substr_compare( $url, ";", -$numSepChars, 1 ) === 0 ) { |
|
1522
|
|
|
# more optimization: instead of running preg_match with a $ |
|
1523
|
|
|
# anchor, which can be slow, do the match on the reversed |
|
1524
|
|
|
# string starting at the desired offset. |
|
1525
|
|
|
# un-reversed regexp is: /&([a-z]+|#x[\da-f]+|#\d+)$/i |
|
1526
|
|
|
if ( preg_match( '/\G([a-z]+|[\da-f]+x#|\d+#)&/i', $urlRev, $m2, 0, $numSepChars ) ) { |
|
1527
|
|
|
$numSepChars--; |
|
1528
|
|
|
} |
|
1529
|
|
|
} |
|
1530
|
|
|
if ( $numSepChars ) { |
|
1531
|
|
|
$trail = substr( $url, -$numSepChars ) . $trail; |
|
1532
|
|
|
$url = substr( $url, 0, -$numSepChars ); |
|
1533
|
|
|
} |
|
1534
|
|
|
|
|
1535
|
|
|
# Verify that we still have a real URL after trail removal, and |
|
1536
|
|
|
# not just lone protocol |
|
1537
|
|
|
if ( strlen( $trail ) >= $numPostProto ) { |
|
1538
|
|
|
return $url . $trail; |
|
1539
|
|
|
} |
|
1540
|
|
|
|
|
1541
|
|
|
$url = Sanitizer::cleanUrl( $url ); |
|
1542
|
|
|
|
|
1543
|
|
|
# Is this an external image? |
|
1544
|
|
|
$text = $this->maybeMakeExternalImage( $url ); |
|
1545
|
|
|
if ( $text === false ) { |
|
1546
|
|
|
# Not an image, make a link |
|
1547
|
|
|
$text = Linker::makeExternalLink( $url, |
|
1548
|
|
|
$this->getConverterLanguage()->markNoConversion( $url, true ), |
|
1549
|
|
|
true, 'free', |
|
1550
|
|
|
$this->getExternalLinkAttribs( $url ), $this->mTitle ); |
|
1551
|
|
|
# Register it in the output object... |
|
1552
|
|
|
# Replace unnecessary URL escape codes with their equivalent characters |
|
1553
|
|
|
$pasteurized = self::normalizeLinkUrl( $url ); |
|
1554
|
|
|
$this->mOutput->addExternalLink( $pasteurized ); |
|
1555
|
|
|
} |
|
1556
|
|
|
return $text . $trail; |
|
1557
|
|
|
} |
|
1558
|
|
|
|
|
1559
|
|
|
/** |
|
1560
|
|
|
* Parse headers and return html |
|
1561
|
|
|
* |
|
1562
|
|
|
* @private |
|
1563
|
|
|
* |
|
1564
|
|
|
* @param string $text |
|
1565
|
|
|
* |
|
1566
|
|
|
* @return string |
|
1567
|
|
|
*/ |
|
1568
|
|
|
public function doHeadings( $text ) { |
|
1569
|
|
|
for ( $i = 6; $i >= 1; --$i ) { |
|
1570
|
|
|
$h = str_repeat( '=', $i ); |
|
1571
|
|
|
$text = preg_replace( "/^$h(.+)$h\\s*$/m", "<h$i>\\1</h$i>", $text ); |
|
1572
|
|
|
} |
|
1573
|
|
|
return $text; |
|
1574
|
|
|
} |
|
1575
|
|
|
|
|
1576
|
|
|
/** |
|
1577
|
|
|
* Replace single quotes with HTML markup |
|
1578
|
|
|
* @private |
|
1579
|
|
|
* |
|
1580
|
|
|
* @param string $text |
|
1581
|
|
|
* |
|
1582
|
|
|
* @return string The altered text |
|
1583
|
|
|
*/ |
|
1584
|
|
|
public function doAllQuotes( $text ) { |
|
1585
|
|
|
$outtext = ''; |
|
1586
|
|
|
$lines = StringUtils::explode( "\n", $text ); |
|
1587
|
|
|
foreach ( $lines as $line ) { |
|
1588
|
|
|
$outtext .= $this->doQuotes( $line ) . "\n"; |
|
1589
|
|
|
} |
|
1590
|
|
|
$outtext = substr( $outtext, 0, -1 ); |
|
1591
|
|
|
return $outtext; |
|
1592
|
|
|
} |
|
1593
|
|
|
|
|
1594
|
|
|
/** |
|
1595
|
|
|
* Helper function for doAllQuotes() |
|
1596
|
|
|
* |
|
1597
|
|
|
* @param string $text |
|
1598
|
|
|
* |
|
1599
|
|
|
* @return string |
|
1600
|
|
|
*/ |
|
1601
|
|
|
public function doQuotes( $text ) { |
|
1602
|
|
|
$arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE ); |
|
1603
|
|
|
$countarr = count( $arr ); |
|
1604
|
|
|
if ( $countarr == 1 ) { |
|
1605
|
|
|
return $text; |
|
1606
|
|
|
} |
|
1607
|
|
|
|
|
1608
|
|
|
// First, do some preliminary work. This may shift some apostrophes from |
|
1609
|
|
|
// being mark-up to being text. It also counts the number of occurrences |
|
1610
|
|
|
// of bold and italics mark-ups. |
|
1611
|
|
|
$numbold = 0; |
|
1612
|
|
|
$numitalics = 0; |
|
1613
|
|
|
for ( $i = 1; $i < $countarr; $i += 2 ) { |
|
1614
|
|
|
$thislen = strlen( $arr[$i] ); |
|
1615
|
|
|
// If there are ever four apostrophes, assume the first is supposed to |
|
1616
|
|
|
// be text, and the remaining three constitute mark-up for bold text. |
|
1617
|
|
|
// (bug 13227: ''''foo'''' turns into ' ''' foo ' ''') |
|
1618
|
|
|
if ( $thislen == 4 ) { |
|
1619
|
|
|
$arr[$i - 1] .= "'"; |
|
1620
|
|
|
$arr[$i] = "'''"; |
|
1621
|
|
|
$thislen = 3; |
|
1622
|
|
|
} elseif ( $thislen > 5 ) { |
|
1623
|
|
|
// If there are more than 5 apostrophes in a row, assume they're all |
|
1624
|
|
|
// text except for the last 5. |
|
1625
|
|
|
// (bug 13227: ''''''foo'''''' turns into ' ''''' foo ' ''''') |
|
1626
|
|
|
$arr[$i - 1] .= str_repeat( "'", $thislen - 5 ); |
|
1627
|
|
|
$arr[$i] = "'''''"; |
|
1628
|
|
|
$thislen = 5; |
|
1629
|
|
|
} |
|
1630
|
|
|
// Count the number of occurrences of bold and italics mark-ups. |
|
1631
|
|
|
if ( $thislen == 2 ) { |
|
1632
|
|
|
$numitalics++; |
|
1633
|
|
|
} elseif ( $thislen == 3 ) { |
|
1634
|
|
|
$numbold++; |
|
1635
|
|
|
} elseif ( $thislen == 5 ) { |
|
1636
|
|
|
$numitalics++; |
|
1637
|
|
|
$numbold++; |
|
1638
|
|
|
} |
|
1639
|
|
|
} |
|
1640
|
|
|
|
|
1641
|
|
|
// If there is an odd number of both bold and italics, it is likely |
|
1642
|
|
|
// that one of the bold ones was meant to be an apostrophe followed |
|
1643
|
|
|
// by italics. Which one we cannot know for certain, but it is more |
|
1644
|
|
|
// likely to be one that has a single-letter word before it. |
|
1645
|
|
|
if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) { |
|
1646
|
|
|
$firstsingleletterword = -1; |
|
1647
|
|
|
$firstmultiletterword = -1; |
|
1648
|
|
|
$firstspace = -1; |
|
1649
|
|
|
for ( $i = 1; $i < $countarr; $i += 2 ) { |
|
1650
|
|
|
if ( strlen( $arr[$i] ) == 3 ) { |
|
1651
|
|
|
$x1 = substr( $arr[$i - 1], -1 ); |
|
1652
|
|
|
$x2 = substr( $arr[$i - 1], -2, 1 ); |
|
1653
|
|
|
if ( $x1 === ' ' ) { |
|
1654
|
|
|
if ( $firstspace == -1 ) { |
|
1655
|
|
|
$firstspace = $i; |
|
1656
|
|
|
} |
|
1657
|
|
|
} elseif ( $x2 === ' ' ) { |
|
1658
|
|
|
$firstsingleletterword = $i; |
|
1659
|
|
|
// if $firstsingleletterword is set, we don't |
|
1660
|
|
|
// look at the other options, so we can bail early. |
|
1661
|
|
|
break; |
|
1662
|
|
|
} else { |
|
1663
|
|
|
if ( $firstmultiletterword == -1 ) { |
|
1664
|
|
|
$firstmultiletterword = $i; |
|
1665
|
|
|
} |
|
1666
|
|
|
} |
|
1667
|
|
|
} |
|
1668
|
|
|
} |
|
1669
|
|
|
|
|
1670
|
|
|
// If there is a single-letter word, use it! |
|
1671
|
|
|
if ( $firstsingleletterword > -1 ) { |
|
1672
|
|
|
$arr[$firstsingleletterword] = "''"; |
|
1673
|
|
|
$arr[$firstsingleletterword - 1] .= "'"; |
|
1674
|
|
|
} elseif ( $firstmultiletterword > -1 ) { |
|
1675
|
|
|
// If not, but there's a multi-letter word, use that one. |
|
1676
|
|
|
$arr[$firstmultiletterword] = "''"; |
|
1677
|
|
|
$arr[$firstmultiletterword - 1] .= "'"; |
|
1678
|
|
|
} elseif ( $firstspace > -1 ) { |
|
1679
|
|
|
// ... otherwise use the first one that has neither. |
|
1680
|
|
|
// (notice that it is possible for all three to be -1 if, for example, |
|
1681
|
|
|
// there is only one pentuple-apostrophe in the line) |
|
1682
|
|
|
$arr[$firstspace] = "''"; |
|
1683
|
|
|
$arr[$firstspace - 1] .= "'"; |
|
1684
|
|
|
} |
|
1685
|
|
|
} |
|
1686
|
|
|
|
|
1687
|
|
|
// Now let's actually convert our apostrophic mush to HTML! |
|
1688
|
|
|
$output = ''; |
|
1689
|
|
|
$buffer = ''; |
|
1690
|
|
|
$state = ''; |
|
1691
|
|
|
$i = 0; |
|
1692
|
|
|
foreach ( $arr as $r ) { |
|
1693
|
|
|
if ( ( $i % 2 ) == 0 ) { |
|
1694
|
|
|
if ( $state === 'both' ) { |
|
1695
|
|
|
$buffer .= $r; |
|
1696
|
|
|
} else { |
|
1697
|
|
|
$output .= $r; |
|
1698
|
|
|
} |
|
1699
|
|
|
} else { |
|
1700
|
|
|
$thislen = strlen( $r ); |
|
1701
|
|
|
if ( $thislen == 2 ) { |
|
1702
|
|
View Code Duplication |
if ( $state === 'i' ) { |
|
1703
|
|
|
$output .= '</i>'; |
|
1704
|
|
|
$state = ''; |
|
1705
|
|
|
} elseif ( $state === 'bi' ) { |
|
1706
|
|
|
$output .= '</i>'; |
|
1707
|
|
|
$state = 'b'; |
|
1708
|
|
|
} elseif ( $state === 'ib' ) { |
|
1709
|
|
|
$output .= '</b></i><b>'; |
|
1710
|
|
|
$state = 'b'; |
|
1711
|
|
|
} elseif ( $state === 'both' ) { |
|
1712
|
|
|
$output .= '<b><i>' . $buffer . '</i>'; |
|
1713
|
|
|
$state = 'b'; |
|
1714
|
|
|
} else { // $state can be 'b' or '' |
|
1715
|
|
|
$output .= '<i>'; |
|
1716
|
|
|
$state .= 'i'; |
|
1717
|
|
|
} |
|
1718
|
|
View Code Duplication |
} elseif ( $thislen == 3 ) { |
|
1719
|
|
|
if ( $state === 'b' ) { |
|
1720
|
|
|
$output .= '</b>'; |
|
1721
|
|
|
$state = ''; |
|
1722
|
|
|
} elseif ( $state === 'bi' ) { |
|
1723
|
|
|
$output .= '</i></b><i>'; |
|
1724
|
|
|
$state = 'i'; |
|
1725
|
|
|
} elseif ( $state === 'ib' ) { |
|
1726
|
|
|
$output .= '</b>'; |
|
1727
|
|
|
$state = 'i'; |
|
1728
|
|
|
} elseif ( $state === 'both' ) { |
|
1729
|
|
|
$output .= '<i><b>' . $buffer . '</b>'; |
|
1730
|
|
|
$state = 'i'; |
|
1731
|
|
|
} else { // $state can be 'i' or '' |
|
1732
|
|
|
$output .= '<b>'; |
|
1733
|
|
|
$state .= 'b'; |
|
1734
|
|
|
} |
|
1735
|
|
|
} elseif ( $thislen == 5 ) { |
|
1736
|
|
|
if ( $state === 'b' ) { |
|
1737
|
|
|
$output .= '</b><i>'; |
|
1738
|
|
|
$state = 'i'; |
|
1739
|
|
|
} elseif ( $state === 'i' ) { |
|
1740
|
|
|
$output .= '</i><b>'; |
|
1741
|
|
|
$state = 'b'; |
|
1742
|
|
|
} elseif ( $state === 'bi' ) { |
|
1743
|
|
|
$output .= '</i></b>'; |
|
1744
|
|
|
$state = ''; |
|
1745
|
|
|
} elseif ( $state === 'ib' ) { |
|
1746
|
|
|
$output .= '</b></i>'; |
|
1747
|
|
|
$state = ''; |
|
1748
|
|
|
} elseif ( $state === 'both' ) { |
|
1749
|
|
|
$output .= '<i><b>' . $buffer . '</b></i>'; |
|
1750
|
|
|
$state = ''; |
|
1751
|
|
|
} else { // ($state == '') |
|
1752
|
|
|
$buffer = ''; |
|
1753
|
|
|
$state = 'both'; |
|
1754
|
|
|
} |
|
1755
|
|
|
} |
|
1756
|
|
|
} |
|
1757
|
|
|
$i++; |
|
1758
|
|
|
} |
|
1759
|
|
|
// Now close all remaining tags. Notice that the order is important. |
|
1760
|
|
|
if ( $state === 'b' || $state === 'ib' ) { |
|
1761
|
|
|
$output .= '</b>'; |
|
1762
|
|
|
} |
|
1763
|
|
|
if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) { |
|
1764
|
|
|
$output .= '</i>'; |
|
1765
|
|
|
} |
|
1766
|
|
|
if ( $state === 'bi' ) { |
|
1767
|
|
|
$output .= '</b>'; |
|
1768
|
|
|
} |
|
1769
|
|
|
// There might be lonely ''''', so make sure we have a buffer |
|
1770
|
|
|
if ( $state === 'both' && $buffer ) { |
|
1771
|
|
|
$output .= '<b><i>' . $buffer . '</i></b>'; |
|
1772
|
|
|
} |
|
1773
|
|
|
return $output; |
|
1774
|
|
|
} |
|
1775
|
|
|
|
|
1776
|
|
|
/** |
|
1777
|
|
|
* Replace external links (REL) |
|
1778
|
|
|
* |
|
1779
|
|
|
* Note: this is all very hackish and the order of execution matters a lot. |
|
1780
|
|
|
* Make sure to run tests/parserTests.php if you change this code. |
|
1781
|
|
|
* |
|
1782
|
|
|
* @private |
|
1783
|
|
|
* |
|
1784
|
|
|
* @param string $text |
|
1785
|
|
|
* |
|
1786
|
|
|
* @throws MWException |
|
1787
|
|
|
* @return string |
|
1788
|
|
|
*/ |
|
1789
|
|
|
public function replaceExternalLinks( $text ) { |
|
1790
|
|
|
|
|
1791
|
|
|
$bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE ); |
|
1792
|
|
|
if ( $bits === false ) { |
|
1793
|
|
|
throw new MWException( "PCRE needs to be compiled with " |
|
1794
|
|
|
. "--enable-unicode-properties in order for MediaWiki to function" ); |
|
1795
|
|
|
} |
|
1796
|
|
|
$s = array_shift( $bits ); |
|
1797
|
|
|
|
|
1798
|
|
|
$i = 0; |
|
1799
|
|
|
while ( $i < count( $bits ) ) { |
|
1800
|
|
|
$url = $bits[$i++]; |
|
1801
|
|
|
$i++; // protocol |
|
1802
|
|
|
$text = $bits[$i++]; |
|
1803
|
|
|
$trail = $bits[$i++]; |
|
1804
|
|
|
|
|
1805
|
|
|
# The characters '<' and '>' (which were escaped by |
|
1806
|
|
|
# removeHTMLtags()) should not be included in |
|
1807
|
|
|
# URLs, per RFC 2396. |
|
1808
|
|
|
$m2 = []; |
|
1809
|
|
View Code Duplication |
if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) { |
|
1810
|
|
|
$text = substr( $url, $m2[0][1] ) . ' ' . $text; |
|
1811
|
|
|
$url = substr( $url, 0, $m2[0][1] ); |
|
1812
|
|
|
} |
|
1813
|
|
|
|
|
1814
|
|
|
# If the link text is an image URL, replace it with an <img> tag |
|
1815
|
|
|
# This happened by accident in the original parser, but some people used it extensively |
|
1816
|
|
|
$img = $this->maybeMakeExternalImage( $text ); |
|
1817
|
|
|
if ( $img !== false ) { |
|
1818
|
|
|
$text = $img; |
|
1819
|
|
|
} |
|
1820
|
|
|
|
|
1821
|
|
|
$dtrail = ''; |
|
1822
|
|
|
|
|
1823
|
|
|
# Set linktype for CSS - if URL==text, link is essentially free |
|
1824
|
|
|
$linktype = ( $text === $url ) ? 'free' : 'text'; |
|
1825
|
|
|
|
|
1826
|
|
|
# No link text, e.g. [http://domain.tld/some.link] |
|
1827
|
|
|
if ( $text == '' ) { |
|
1828
|
|
|
# Autonumber |
|
1829
|
|
|
$langObj = $this->getTargetLanguage(); |
|
1830
|
|
|
$text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']'; |
|
1831
|
|
|
$linktype = 'autonumber'; |
|
1832
|
|
|
} else { |
|
1833
|
|
|
# Have link text, e.g. [http://domain.tld/some.link text]s |
|
1834
|
|
|
# Check for trail |
|
1835
|
|
|
list( $dtrail, $trail ) = Linker::splitTrail( $trail ); |
|
1836
|
|
|
} |
|
1837
|
|
|
|
|
1838
|
|
|
$text = $this->getConverterLanguage()->markNoConversion( $text ); |
|
1839
|
|
|
|
|
1840
|
|
|
$url = Sanitizer::cleanUrl( $url ); |
|
1841
|
|
|
|
|
1842
|
|
|
# Use the encoded URL |
|
1843
|
|
|
# This means that users can paste URLs directly into the text |
|
1844
|
|
|
# Funny characters like ö aren't valid in URLs anyway |
|
1845
|
|
|
# This was changed in August 2004 |
|
1846
|
|
|
$s .= Linker::makeExternalLink( $url, $text, false, $linktype, |
|
1847
|
|
|
$this->getExternalLinkAttribs( $url ), $this->mTitle ) . $dtrail . $trail; |
|
1848
|
|
|
|
|
1849
|
|
|
# Register link in the output object. |
|
1850
|
|
|
# Replace unnecessary URL escape codes with the referenced character |
|
1851
|
|
|
# This prevents spammers from hiding links from the filters |
|
1852
|
|
|
$pasteurized = self::normalizeLinkUrl( $url ); |
|
1853
|
|
|
$this->mOutput->addExternalLink( $pasteurized ); |
|
1854
|
|
|
} |
|
1855
|
|
|
|
|
1856
|
|
|
return $s; |
|
1857
|
|
|
} |
|
1858
|
|
|
|
|
1859
|
|
|
/** |
|
1860
|
|
|
* Get the rel attribute for a particular external link. |
|
1861
|
|
|
* |
|
1862
|
|
|
* @since 1.21 |
|
1863
|
|
|
* @param string|bool $url Optional URL, to extract the domain from for rel => |
|
1864
|
|
|
* nofollow if appropriate |
|
1865
|
|
|
* @param Title $title Optional Title, for wgNoFollowNsExceptions lookups |
|
1866
|
|
|
* @return string|null Rel attribute for $url |
|
1867
|
|
|
*/ |
|
1868
|
|
|
public static function getExternalLinkRel( $url = false, $title = null ) { |
|
1869
|
|
|
global $wgNoFollowLinks, $wgNoFollowNsExceptions, $wgNoFollowDomainExceptions; |
|
1870
|
|
|
$ns = $title ? $title->getNamespace() : false; |
|
1871
|
|
|
if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions ) |
|
1872
|
|
|
&& !wfMatchesDomainList( $url, $wgNoFollowDomainExceptions ) |
|
|
|
|
|
|
1873
|
|
|
) { |
|
1874
|
|
|
return 'nofollow'; |
|
1875
|
|
|
} |
|
1876
|
|
|
return null; |
|
1877
|
|
|
} |
|
1878
|
|
|
|
|
1879
|
|
|
/** |
|
1880
|
|
|
* Get an associative array of additional HTML attributes appropriate for a |
|
1881
|
|
|
* particular external link. This currently may include rel => nofollow |
|
1882
|
|
|
* (depending on configuration, namespace, and the URL's domain) and/or a |
|
1883
|
|
|
* target attribute (depending on configuration). |
|
1884
|
|
|
* |
|
1885
|
|
|
* @param string $url URL to extract the domain from for rel => |
|
1886
|
|
|
* nofollow if appropriate |
|
1887
|
|
|
* @return array Associative array of HTML attributes |
|
1888
|
|
|
*/ |
|
1889
|
|
|
public function getExternalLinkAttribs( $url ) { |
|
1890
|
|
|
$attribs = []; |
|
1891
|
|
|
$rel = self::getExternalLinkRel( $url, $this->mTitle ); |
|
1892
|
|
|
|
|
1893
|
|
|
$target = $this->mOptions->getExternalLinkTarget(); |
|
1894
|
|
|
if ( $target ) { |
|
1895
|
|
|
$attribs['target'] = $target; |
|
1896
|
|
|
if ( !in_array( $target, [ '_self', '_parent', '_top' ] ) ) { |
|
1897
|
|
|
// T133507. New windows can navigate parent cross-origin. |
|
1898
|
|
|
// Including noreferrer due to lacking browser |
|
1899
|
|
|
// support of noopener. Eventually noreferrer should be removed. |
|
1900
|
|
|
if ( $rel !== '' ) { |
|
1901
|
|
|
$rel .= ' '; |
|
1902
|
|
|
} |
|
1903
|
|
|
$rel .= 'noreferrer noopener'; |
|
1904
|
|
|
} |
|
1905
|
|
|
} |
|
1906
|
|
|
$attribs['rel'] = $rel; |
|
1907
|
|
|
return $attribs; |
|
1908
|
|
|
} |
|
1909
|
|
|
|
|
1910
|
|
|
/** |
|
1911
|
|
|
* Replace unusual escape codes in a URL with their equivalent characters |
|
1912
|
|
|
* |
|
1913
|
|
|
* @deprecated since 1.24, use normalizeLinkUrl |
|
1914
|
|
|
* @param string $url |
|
1915
|
|
|
* @return string |
|
1916
|
|
|
*/ |
|
1917
|
|
|
public static function replaceUnusualEscapes( $url ) { |
|
1918
|
|
|
wfDeprecated( __METHOD__, '1.24' ); |
|
1919
|
|
|
return self::normalizeLinkUrl( $url ); |
|
1920
|
|
|
} |
|
1921
|
|
|
|
|
1922
|
|
|
/** |
|
1923
|
|
|
* Replace unusual escape codes in a URL with their equivalent characters |
|
1924
|
|
|
* |
|
1925
|
|
|
* This generally follows the syntax defined in RFC 3986, with special |
|
1926
|
|
|
* consideration for HTTP query strings. |
|
1927
|
|
|
* |
|
1928
|
|
|
* @param string $url |
|
1929
|
|
|
* @return string |
|
1930
|
|
|
*/ |
|
1931
|
|
|
public static function normalizeLinkUrl( $url ) { |
|
1932
|
|
|
# First, make sure unsafe characters are encoded |
|
1933
|
|
|
$url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/', |
|
1934
|
|
|
function ( $m ) { |
|
1935
|
|
|
return rawurlencode( $m[0] ); |
|
1936
|
|
|
}, |
|
1937
|
|
|
$url |
|
1938
|
|
|
); |
|
1939
|
|
|
|
|
1940
|
|
|
$ret = ''; |
|
1941
|
|
|
$end = strlen( $url ); |
|
1942
|
|
|
|
|
1943
|
|
|
# Fragment part - 'fragment' |
|
1944
|
|
|
$start = strpos( $url, '#' ); |
|
1945
|
|
View Code Duplication |
if ( $start !== false && $start < $end ) { |
|
1946
|
|
|
$ret = self::normalizeUrlComponent( |
|
1947
|
|
|
substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret; |
|
1948
|
|
|
$end = $start; |
|
1949
|
|
|
} |
|
1950
|
|
|
|
|
1951
|
|
|
# Query part - 'query' minus &=+; |
|
1952
|
|
|
$start = strpos( $url, '?' ); |
|
1953
|
|
View Code Duplication |
if ( $start !== false && $start < $end ) { |
|
1954
|
|
|
$ret = self::normalizeUrlComponent( |
|
1955
|
|
|
substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret; |
|
1956
|
|
|
$end = $start; |
|
1957
|
|
|
} |
|
1958
|
|
|
|
|
1959
|
|
|
# Scheme and path part - 'pchar' |
|
1960
|
|
|
# (we assume no userinfo or encoded colons in the host) |
|
1961
|
|
|
$ret = self::normalizeUrlComponent( |
|
1962
|
|
|
substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret; |
|
1963
|
|
|
|
|
1964
|
|
|
return $ret; |
|
1965
|
|
|
} |
|
1966
|
|
|
|
|
1967
|
|
|
private static function normalizeUrlComponent( $component, $unsafe ) { |
|
1968
|
|
|
$callback = function ( $matches ) use ( $unsafe ) { |
|
1969
|
|
|
$char = urldecode( $matches[0] ); |
|
1970
|
|
|
$ord = ord( $char ); |
|
1971
|
|
|
if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) { |
|
1972
|
|
|
# Unescape it |
|
1973
|
|
|
return $char; |
|
1974
|
|
|
} else { |
|
1975
|
|
|
# Leave it escaped, but use uppercase for a-f |
|
1976
|
|
|
return strtoupper( $matches[0] ); |
|
1977
|
|
|
} |
|
1978
|
|
|
}; |
|
1979
|
|
|
return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component ); |
|
1980
|
|
|
} |
|
1981
|
|
|
|
|
1982
|
|
|
/** |
|
1983
|
|
|
* make an image if it's allowed, either through the global |
|
1984
|
|
|
* option, through the exception, or through the on-wiki whitelist |
|
1985
|
|
|
* |
|
1986
|
|
|
* @param string $url |
|
1987
|
|
|
* |
|
1988
|
|
|
* @return string |
|
1989
|
|
|
*/ |
|
1990
|
|
|
private function maybeMakeExternalImage( $url ) { |
|
1991
|
|
|
$imagesfrom = $this->mOptions->getAllowExternalImagesFrom(); |
|
1992
|
|
|
$imagesexception = !empty( $imagesfrom ); |
|
1993
|
|
|
$text = false; |
|
1994
|
|
|
# $imagesfrom could be either a single string or an array of strings, parse out the latter |
|
1995
|
|
|
if ( $imagesexception && is_array( $imagesfrom ) ) { |
|
1996
|
|
|
$imagematch = false; |
|
1997
|
|
|
foreach ( $imagesfrom as $match ) { |
|
1998
|
|
|
if ( strpos( $url, $match ) === 0 ) { |
|
1999
|
|
|
$imagematch = true; |
|
2000
|
|
|
break; |
|
2001
|
|
|
} |
|
2002
|
|
|
} |
|
2003
|
|
|
} elseif ( $imagesexception ) { |
|
2004
|
|
|
$imagematch = ( strpos( $url, $imagesfrom ) === 0 ); |
|
2005
|
|
|
} else { |
|
2006
|
|
|
$imagematch = false; |
|
2007
|
|
|
} |
|
2008
|
|
|
|
|
2009
|
|
|
if ( $this->mOptions->getAllowExternalImages() |
|
2010
|
|
|
|| ( $imagesexception && $imagematch ) |
|
2011
|
|
|
) { |
|
2012
|
|
|
if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) { |
|
2013
|
|
|
# Image found |
|
2014
|
|
|
$text = Linker::makeExternalImage( $url ); |
|
2015
|
|
|
} |
|
2016
|
|
|
} |
|
2017
|
|
|
if ( !$text && $this->mOptions->getEnableImageWhitelist() |
|
|
|
|
|
|
2018
|
|
|
&& preg_match( self::EXT_IMAGE_REGEX, $url ) |
|
2019
|
|
|
) { |
|
2020
|
|
|
$whitelist = explode( |
|
2021
|
|
|
"\n", |
|
2022
|
|
|
wfMessage( 'external_image_whitelist' )->inContentLanguage()->text() |
|
2023
|
|
|
); |
|
2024
|
|
|
|
|
2025
|
|
|
foreach ( $whitelist as $entry ) { |
|
2026
|
|
|
# Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments |
|
2027
|
|
|
if ( strpos( $entry, '#' ) === 0 || $entry === '' ) { |
|
2028
|
|
|
continue; |
|
2029
|
|
|
} |
|
2030
|
|
|
if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) { |
|
2031
|
|
|
# Image matches a whitelist entry |
|
2032
|
|
|
$text = Linker::makeExternalImage( $url ); |
|
2033
|
|
|
break; |
|
2034
|
|
|
} |
|
2035
|
|
|
} |
|
2036
|
|
|
} |
|
2037
|
|
|
return $text; |
|
2038
|
|
|
} |
|
2039
|
|
|
|
|
2040
|
|
|
/** |
|
2041
|
|
|
* Process [[ ]] wikilinks |
|
2042
|
|
|
* |
|
2043
|
|
|
* @param string $s |
|
2044
|
|
|
* |
|
2045
|
|
|
* @return string Processed text |
|
2046
|
|
|
* |
|
2047
|
|
|
* @private |
|
2048
|
|
|
*/ |
|
2049
|
|
|
public function replaceInternalLinks( $s ) { |
|
2050
|
|
|
$this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) ); |
|
2051
|
|
|
return $s; |
|
2052
|
|
|
} |
|
2053
|
|
|
|
|
2054
|
|
|
/** |
|
2055
|
|
|
* Process [[ ]] wikilinks (RIL) |
|
2056
|
|
|
* @param string $s |
|
2057
|
|
|
* @throws MWException |
|
2058
|
|
|
* @return LinkHolderArray |
|
2059
|
|
|
* |
|
2060
|
|
|
* @private |
|
2061
|
|
|
*/ |
|
2062
|
|
|
public function replaceInternalLinks2( &$s ) { |
|
2063
|
|
|
global $wgExtraInterlanguageLinkPrefixes; |
|
2064
|
|
|
|
|
2065
|
|
|
static $tc = false, $e1, $e1_img; |
|
2066
|
|
|
# the % is needed to support urlencoded titles as well |
|
2067
|
|
|
if ( !$tc ) { |
|
2068
|
|
|
$tc = Title::legalChars() . '#%'; |
|
2069
|
|
|
# Match a link having the form [[namespace:link|alternate]]trail |
|
2070
|
|
|
$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; |
|
2071
|
|
|
# Match cases where there is no "]]", which might still be images |
|
2072
|
|
|
$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; |
|
2073
|
|
|
} |
|
2074
|
|
|
|
|
2075
|
|
|
$holders = new LinkHolderArray( $this ); |
|
2076
|
|
|
|
|
2077
|
|
|
# split the entire text string on occurrences of [[ |
|
2078
|
|
|
$a = StringUtils::explode( '[[', ' ' . $s ); |
|
2079
|
|
|
# get the first element (all text up to first [[), and remove the space we added |
|
2080
|
|
|
$s = $a->current(); |
|
2081
|
|
|
$a->next(); |
|
2082
|
|
|
$line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void" |
|
2083
|
|
|
$s = substr( $s, 1 ); |
|
2084
|
|
|
|
|
2085
|
|
|
$useLinkPrefixExtension = $this->getTargetLanguage()->linkPrefixExtension(); |
|
2086
|
|
|
$e2 = null; |
|
2087
|
|
|
if ( $useLinkPrefixExtension ) { |
|
2088
|
|
|
# Match the end of a line for a word that's not followed by whitespace, |
|
2089
|
|
|
# e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched |
|
2090
|
|
|
global $wgContLang; |
|
2091
|
|
|
$charset = $wgContLang->linkPrefixCharset(); |
|
2092
|
|
|
$e2 = "/^((?>.*[^$charset]|))(.+)$/sDu"; |
|
2093
|
|
|
} |
|
2094
|
|
|
|
|
2095
|
|
|
if ( is_null( $this->mTitle ) ) { |
|
2096
|
|
|
throw new MWException( __METHOD__ . ": \$this->mTitle is null\n" ); |
|
2097
|
|
|
} |
|
2098
|
|
|
$nottalk = !$this->mTitle->isTalkPage(); |
|
2099
|
|
|
|
|
2100
|
|
View Code Duplication |
if ( $useLinkPrefixExtension ) { |
|
2101
|
|
|
$m = []; |
|
2102
|
|
|
if ( preg_match( $e2, $s, $m ) ) { |
|
2103
|
|
|
$first_prefix = $m[2]; |
|
2104
|
|
|
} else { |
|
2105
|
|
|
$first_prefix = false; |
|
2106
|
|
|
} |
|
2107
|
|
|
} else { |
|
2108
|
|
|
$prefix = ''; |
|
2109
|
|
|
} |
|
2110
|
|
|
|
|
2111
|
|
|
$useSubpages = $this->areSubpagesAllowed(); |
|
2112
|
|
|
|
|
2113
|
|
|
// @codingStandardsIgnoreStart Squiz.WhiteSpace.SemicolonSpacing.Incorrect |
|
2114
|
|
|
# Loop for each link |
|
2115
|
|
|
for ( ; $line !== false && $line !== null; $a->next(), $line = $a->current() ) { |
|
2116
|
|
|
// @codingStandardsIgnoreEnd |
|
2117
|
|
|
|
|
2118
|
|
|
# Check for excessive memory usage |
|
2119
|
|
|
if ( $holders->isBig() ) { |
|
2120
|
|
|
# Too big |
|
2121
|
|
|
# Do the existence check, replace the link holders and clear the array |
|
2122
|
|
|
$holders->replace( $s ); |
|
2123
|
|
|
$holders->clear(); |
|
2124
|
|
|
} |
|
2125
|
|
|
|
|
2126
|
|
|
if ( $useLinkPrefixExtension ) { |
|
2127
|
|
View Code Duplication |
if ( preg_match( $e2, $s, $m ) ) { |
|
2128
|
|
|
$prefix = $m[2]; |
|
2129
|
|
|
$s = $m[1]; |
|
2130
|
|
|
} else { |
|
2131
|
|
|
$prefix = ''; |
|
2132
|
|
|
} |
|
2133
|
|
|
# first link |
|
2134
|
|
|
if ( $first_prefix ) { |
|
|
|
|
|
|
2135
|
|
|
$prefix = $first_prefix; |
|
|
|
|
|
|
2136
|
|
|
$first_prefix = false; |
|
2137
|
|
|
} |
|
2138
|
|
|
} |
|
2139
|
|
|
|
|
2140
|
|
|
$might_be_img = false; |
|
2141
|
|
|
|
|
2142
|
|
|
if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt |
|
2143
|
|
|
$text = $m[2]; |
|
2144
|
|
|
# If we get a ] at the beginning of $m[3] that means we have a link that's something like: |
|
2145
|
|
|
# [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up, |
|
2146
|
|
|
# the real problem is with the $e1 regex |
|
2147
|
|
|
# See bug 1300. |
|
2148
|
|
|
# Still some problems for cases where the ] is meant to be outside punctuation, |
|
2149
|
|
|
# and no image is in sight. See bug 2095. |
|
2150
|
|
|
if ( $text !== '' |
|
2151
|
|
|
&& substr( $m[3], 0, 1 ) === ']' |
|
2152
|
|
|
&& strpos( $text, '[' ) !== false |
|
2153
|
|
|
) { |
|
2154
|
|
|
$text .= ']'; # so that replaceExternalLinks($text) works later |
|
2155
|
|
|
$m[3] = substr( $m[3], 1 ); |
|
2156
|
|
|
} |
|
2157
|
|
|
# fix up urlencoded title texts |
|
2158
|
|
|
if ( strpos( $m[1], '%' ) !== false ) { |
|
2159
|
|
|
# Should anchors '#' also be rejected? |
|
2160
|
|
|
$m[1] = str_replace( [ '<', '>' ], [ '<', '>' ], rawurldecode( $m[1] ) ); |
|
2161
|
|
|
} |
|
2162
|
|
|
$trail = $m[3]; |
|
2163
|
|
|
} elseif ( preg_match( $e1_img, $line, $m ) ) { |
|
2164
|
|
|
# Invalid, but might be an image with a link in its caption |
|
2165
|
|
|
$might_be_img = true; |
|
2166
|
|
|
$text = $m[2]; |
|
2167
|
|
|
if ( strpos( $m[1], '%' ) !== false ) { |
|
2168
|
|
|
$m[1] = rawurldecode( $m[1] ); |
|
2169
|
|
|
} |
|
2170
|
|
|
$trail = ""; |
|
2171
|
|
|
} else { # Invalid form; output directly |
|
2172
|
|
|
$s .= $prefix . '[[' . $line; |
|
|
|
|
|
|
2173
|
|
|
continue; |
|
2174
|
|
|
} |
|
2175
|
|
|
|
|
2176
|
|
|
$origLink = $m[1]; |
|
2177
|
|
|
|
|
2178
|
|
|
# Don't allow internal links to pages containing |
|
2179
|
|
|
# PROTO: where PROTO is a valid URL protocol; these |
|
2180
|
|
|
# should be external links. |
|
2181
|
|
|
if ( preg_match( '/^(?i:' . $this->mUrlProtocols . ')/', $origLink ) ) { |
|
2182
|
|
|
$s .= $prefix . '[[' . $line; |
|
2183
|
|
|
continue; |
|
2184
|
|
|
} |
|
2185
|
|
|
|
|
2186
|
|
|
# Make subpage if necessary |
|
2187
|
|
|
if ( $useSubpages ) { |
|
2188
|
|
|
$link = $this->maybeDoSubpageLink( $origLink, $text ); |
|
2189
|
|
|
} else { |
|
2190
|
|
|
$link = $origLink; |
|
2191
|
|
|
} |
|
2192
|
|
|
|
|
2193
|
|
|
$noforce = ( substr( $origLink, 0, 1 ) !== ':' ); |
|
2194
|
|
|
if ( !$noforce ) { |
|
2195
|
|
|
# Strip off leading ':' |
|
2196
|
|
|
$link = substr( $link, 1 ); |
|
2197
|
|
|
} |
|
2198
|
|
|
|
|
2199
|
|
|
$unstrip = $this->mStripState->unstripNoWiki( $link ); |
|
2200
|
|
|
$nt = is_string( $unstrip ) ? Title::newFromText( $unstrip ) : null; |
|
2201
|
|
|
if ( $nt === null ) { |
|
2202
|
|
|
$s .= $prefix . '[[' . $line; |
|
2203
|
|
|
continue; |
|
2204
|
|
|
} |
|
2205
|
|
|
|
|
2206
|
|
|
$ns = $nt->getNamespace(); |
|
2207
|
|
|
$iw = $nt->getInterwiki(); |
|
2208
|
|
|
|
|
2209
|
|
|
if ( $might_be_img ) { # if this is actually an invalid link |
|
2210
|
|
|
if ( $ns == NS_FILE && $noforce ) { # but might be an image |
|
2211
|
|
|
$found = false; |
|
2212
|
|
|
while ( true ) { |
|
2213
|
|
|
# look at the next 'line' to see if we can close it there |
|
2214
|
|
|
$a->next(); |
|
2215
|
|
|
$next_line = $a->current(); |
|
2216
|
|
|
if ( $next_line === false || $next_line === null ) { |
|
2217
|
|
|
break; |
|
2218
|
|
|
} |
|
2219
|
|
|
$m = explode( ']]', $next_line, 3 ); |
|
2220
|
|
|
if ( count( $m ) == 3 ) { |
|
2221
|
|
|
# the first ]] closes the inner link, the second the image |
|
2222
|
|
|
$found = true; |
|
2223
|
|
|
$text .= "[[{$m[0]}]]{$m[1]}"; |
|
2224
|
|
|
$trail = $m[2]; |
|
2225
|
|
|
break; |
|
2226
|
|
|
} elseif ( count( $m ) == 2 ) { |
|
2227
|
|
|
# if there's exactly one ]] that's fine, we'll keep looking |
|
2228
|
|
|
$text .= "[[{$m[0]}]]{$m[1]}"; |
|
2229
|
|
|
} else { |
|
2230
|
|
|
# if $next_line is invalid too, we need look no further |
|
2231
|
|
|
$text .= '[[' . $next_line; |
|
2232
|
|
|
break; |
|
2233
|
|
|
} |
|
2234
|
|
|
} |
|
2235
|
|
|
if ( !$found ) { |
|
2236
|
|
|
# we couldn't find the end of this imageLink, so output it raw |
|
2237
|
|
|
# but don't ignore what might be perfectly normal links in the text we've examined |
|
2238
|
|
|
$holders->merge( $this->replaceInternalLinks2( $text ) ); |
|
2239
|
|
|
$s .= "{$prefix}[[$link|$text"; |
|
2240
|
|
|
# note: no $trail, because without an end, there *is* no trail |
|
2241
|
|
|
continue; |
|
2242
|
|
|
} |
|
2243
|
|
|
} else { # it's not an image, so output it raw |
|
2244
|
|
|
$s .= "{$prefix}[[$link|$text"; |
|
2245
|
|
|
# note: no $trail, because without an end, there *is* no trail |
|
2246
|
|
|
continue; |
|
2247
|
|
|
} |
|
2248
|
|
|
} |
|
2249
|
|
|
|
|
2250
|
|
|
$wasblank = ( $text == '' ); |
|
2251
|
|
|
if ( $wasblank ) { |
|
2252
|
|
|
$text = $link; |
|
2253
|
|
|
} else { |
|
2254
|
|
|
# Bug 4598 madness. Handle the quotes only if they come from the alternate part |
|
2255
|
|
|
# [[Lista d''e paise d''o munno]] -> <a href="...">Lista d''e paise d''o munno</a> |
|
2256
|
|
|
# [[Criticism of Harry Potter|Criticism of ''Harry Potter'']] |
|
2257
|
|
|
# -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a> |
|
2258
|
|
|
$text = $this->doQuotes( $text ); |
|
2259
|
|
|
} |
|
2260
|
|
|
|
|
2261
|
|
|
# Link not escaped by : , create the various objects |
|
2262
|
|
|
if ( $noforce && !$nt->wasLocalInterwiki() ) { |
|
2263
|
|
|
# Interwikis |
|
2264
|
|
|
if ( |
|
2265
|
|
|
$iw && $this->mOptions->getInterwikiMagic() && $nottalk && ( |
|
2266
|
|
|
Language::fetchLanguageName( $iw, null, 'mw' ) || |
|
2267
|
|
|
in_array( $iw, $wgExtraInterlanguageLinkPrefixes ) |
|
2268
|
|
|
) |
|
2269
|
|
|
) { |
|
2270
|
|
|
# Bug 24502: filter duplicates |
|
2271
|
|
|
if ( !isset( $this->mLangLinkLanguages[$iw] ) ) { |
|
2272
|
|
|
$this->mLangLinkLanguages[$iw] = true; |
|
2273
|
|
|
$this->mOutput->addLanguageLink( $nt->getFullText() ); |
|
2274
|
|
|
} |
|
2275
|
|
|
|
|
2276
|
|
|
$s = rtrim( $s . $prefix ); |
|
2277
|
|
|
$s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail; |
|
2278
|
|
|
continue; |
|
2279
|
|
|
} |
|
2280
|
|
|
|
|
2281
|
|
|
if ( $ns == NS_FILE ) { |
|
2282
|
|
|
if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) { |
|
2283
|
|
|
if ( $wasblank ) { |
|
2284
|
|
|
# if no parameters were passed, $text |
|
2285
|
|
|
# becomes something like "File:Foo.png", |
|
2286
|
|
|
# which we don't want to pass on to the |
|
2287
|
|
|
# image generator |
|
2288
|
|
|
$text = ''; |
|
2289
|
|
|
} else { |
|
2290
|
|
|
# recursively parse links inside the image caption |
|
2291
|
|
|
# actually, this will parse them in any other parameters, too, |
|
2292
|
|
|
# but it might be hard to fix that, and it doesn't matter ATM |
|
2293
|
|
|
$text = $this->replaceExternalLinks( $text ); |
|
2294
|
|
|
$holders->merge( $this->replaceInternalLinks2( $text ) ); |
|
2295
|
|
|
} |
|
2296
|
|
|
# cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them |
|
2297
|
|
|
$s .= $prefix . $this->armorLinks( |
|
2298
|
|
|
$this->makeImage( $nt, $text, $holders ) ) . $trail; |
|
2299
|
|
|
} else { |
|
2300
|
|
|
$s .= $prefix . $trail; |
|
2301
|
|
|
} |
|
2302
|
|
|
continue; |
|
2303
|
|
|
} |
|
2304
|
|
|
|
|
2305
|
|
|
if ( $ns == NS_CATEGORY ) { |
|
2306
|
|
|
$s = rtrim( $s . "\n" ); # bug 87 |
|
2307
|
|
|
|
|
2308
|
|
|
if ( $wasblank ) { |
|
2309
|
|
|
$sortkey = $this->getDefaultSort(); |
|
2310
|
|
|
} else { |
|
2311
|
|
|
$sortkey = $text; |
|
2312
|
|
|
} |
|
2313
|
|
|
$sortkey = Sanitizer::decodeCharReferences( $sortkey ); |
|
2314
|
|
|
$sortkey = str_replace( "\n", '', $sortkey ); |
|
2315
|
|
|
$sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey ); |
|
2316
|
|
|
$this->mOutput->addCategory( $nt->getDBkey(), $sortkey ); |
|
2317
|
|
|
|
|
2318
|
|
|
/** |
|
2319
|
|
|
* Strip the whitespace Category links produce, see bug 87 |
|
2320
|
|
|
*/ |
|
2321
|
|
|
$s .= trim( $prefix . $trail, "\n" ) == '' ? '' : $prefix . $trail; |
|
2322
|
|
|
|
|
2323
|
|
|
continue; |
|
2324
|
|
|
} |
|
2325
|
|
|
} |
|
2326
|
|
|
|
|
2327
|
|
|
# Self-link checking. For some languages, variants of the title are checked in |
|
2328
|
|
|
# LinkHolderArray::doVariants() to allow batching the existence checks necessary |
|
2329
|
|
|
# for linking to a different variant. |
|
2330
|
|
|
if ( $ns != NS_SPECIAL && $nt->equals( $this->mTitle ) && !$nt->hasFragment() ) { |
|
2331
|
|
|
$s .= $prefix . Linker::makeSelfLinkObj( $nt, $text, '', $trail ); |
|
2332
|
|
|
continue; |
|
2333
|
|
|
} |
|
2334
|
|
|
|
|
2335
|
|
|
# NS_MEDIA is a pseudo-namespace for linking directly to a file |
|
2336
|
|
|
# @todo FIXME: Should do batch file existence checks, see comment below |
|
2337
|
|
|
if ( $ns == NS_MEDIA ) { |
|
2338
|
|
|
# Give extensions a chance to select the file revision for us |
|
2339
|
|
|
$options = []; |
|
2340
|
|
|
$descQuery = false; |
|
2341
|
|
|
Hooks::run( 'BeforeParserFetchFileAndTitle', |
|
2342
|
|
|
[ $this, $nt, &$options, &$descQuery ] ); |
|
2343
|
|
|
# Fetch and register the file (file title may be different via hooks) |
|
2344
|
|
|
list( $file, $nt ) = $this->fetchFileAndTitle( $nt, $options ); |
|
2345
|
|
|
# Cloak with NOPARSE to avoid replacement in replaceExternalLinks |
|
2346
|
|
|
$s .= $prefix . $this->armorLinks( |
|
2347
|
|
|
Linker::makeMediaLinkFile( $nt, $file, $text ) ) . $trail; |
|
2348
|
|
|
continue; |
|
2349
|
|
|
} |
|
2350
|
|
|
|
|
2351
|
|
|
# Some titles, such as valid special pages or files in foreign repos, should |
|
2352
|
|
|
# be shown as bluelinks even though they're not included in the page table |
|
2353
|
|
|
# @todo FIXME: isAlwaysKnown() can be expensive for file links; we should really do |
|
2354
|
|
|
# batch file existence checks for NS_FILE and NS_MEDIA |
|
2355
|
|
|
if ( $iw == '' && $nt->isAlwaysKnown() ) { |
|
2356
|
|
|
$this->mOutput->addLink( $nt ); |
|
2357
|
|
|
$s .= $this->makeKnownLinkHolder( $nt, $text, $trail, $prefix ); |
|
2358
|
|
|
} else { |
|
2359
|
|
|
# Links will be added to the output link list after checking |
|
2360
|
|
|
$s .= $holders->makeHolder( $nt, $text, [], $trail, $prefix ); |
|
2361
|
|
|
} |
|
2362
|
|
|
} |
|
2363
|
|
|
return $holders; |
|
2364
|
|
|
} |
|
2365
|
|
|
|
|
2366
|
|
|
/** |
|
2367
|
|
|
* Render a forced-blue link inline; protect against double expansion of |
|
2368
|
|
|
* URLs if we're in a mode that prepends full URL prefixes to internal links. |
|
2369
|
|
|
* Since this little disaster has to split off the trail text to avoid |
|
2370
|
|
|
* breaking URLs in the following text without breaking trails on the |
|
2371
|
|
|
* wiki links, it's been made into a horrible function. |
|
2372
|
|
|
* |
|
2373
|
|
|
* @param Title $nt |
|
2374
|
|
|
* @param string $text |
|
2375
|
|
|
* @param string $trail |
|
2376
|
|
|
* @param string $prefix |
|
2377
|
|
|
* @return string HTML-wikitext mix oh yuck |
|
2378
|
|
|
*/ |
|
2379
|
|
|
protected function makeKnownLinkHolder( $nt, $text = '', $trail = '', $prefix = '' ) { |
|
2380
|
|
|
list( $inside, $trail ) = Linker::splitTrail( $trail ); |
|
2381
|
|
|
|
|
2382
|
|
|
if ( $text == '' ) { |
|
2383
|
|
|
$text = htmlspecialchars( $nt->getPrefixedText() ); |
|
2384
|
|
|
} |
|
2385
|
|
|
|
|
2386
|
|
|
$link = $this->getLinkRenderer()->makeKnownLink( |
|
2387
|
|
|
$nt, new HtmlArmor( "$prefix$text$inside" ) |
|
2388
|
|
|
); |
|
2389
|
|
|
|
|
2390
|
|
|
return $this->armorLinks( $link ) . $trail; |
|
2391
|
|
|
} |
|
2392
|
|
|
|
|
2393
|
|
|
/** |
|
2394
|
|
|
* Insert a NOPARSE hacky thing into any inline links in a chunk that's |
|
2395
|
|
|
* going to go through further parsing steps before inline URL expansion. |
|
2396
|
|
|
* |
|
2397
|
|
|
* Not needed quite as much as it used to be since free links are a bit |
|
2398
|
|
|
* more sensible these days. But bracketed links are still an issue. |
|
2399
|
|
|
* |
|
2400
|
|
|
* @param string $text More-or-less HTML |
|
2401
|
|
|
* @return string Less-or-more HTML with NOPARSE bits |
|
2402
|
|
|
*/ |
|
2403
|
|
|
public function armorLinks( $text ) { |
|
2404
|
|
|
return preg_replace( '/\b((?i)' . $this->mUrlProtocols . ')/', |
|
2405
|
|
|
self::MARKER_PREFIX . "NOPARSE$1", $text ); |
|
2406
|
|
|
} |
|
2407
|
|
|
|
|
2408
|
|
|
/** |
|
2409
|
|
|
* Return true if subpage links should be expanded on this page. |
|
2410
|
|
|
* @return bool |
|
2411
|
|
|
*/ |
|
2412
|
|
|
public function areSubpagesAllowed() { |
|
2413
|
|
|
# Some namespaces don't allow subpages |
|
2414
|
|
|
return MWNamespace::hasSubpages( $this->mTitle->getNamespace() ); |
|
2415
|
|
|
} |
|
2416
|
|
|
|
|
2417
|
|
|
/** |
|
2418
|
|
|
* Handle link to subpage if necessary |
|
2419
|
|
|
* |
|
2420
|
|
|
* @param string $target The source of the link |
|
2421
|
|
|
* @param string &$text The link text, modified as necessary |
|
2422
|
|
|
* @return string The full name of the link |
|
2423
|
|
|
* @private |
|
2424
|
|
|
*/ |
|
2425
|
|
|
public function maybeDoSubpageLink( $target, &$text ) { |
|
2426
|
|
|
return Linker::normalizeSubpageLink( $this->mTitle, $target, $text ); |
|
2427
|
|
|
} |
|
2428
|
|
|
|
|
2429
|
|
|
/** |
|
2430
|
|
|
* Make lists from lines starting with ':', '*', '#', etc. (DBL) |
|
2431
|
|
|
* |
|
2432
|
|
|
* @param string $text |
|
2433
|
|
|
* @param bool $linestart Whether or not this is at the start of a line. |
|
2434
|
|
|
* @private |
|
2435
|
|
|
* @return string The lists rendered as HTML |
|
2436
|
|
|
*/ |
|
2437
|
|
|
public function doBlockLevels( $text, $linestart ) { |
|
2438
|
|
|
return BlockLevelPass::doBlockLevels( $text, $linestart ); |
|
2439
|
|
|
} |
|
2440
|
|
|
|
|
2441
|
|
|
/** |
|
2442
|
|
|
* Return value of a magic variable (like PAGENAME) |
|
2443
|
|
|
* |
|
2444
|
|
|
* @private |
|
2445
|
|
|
* |
|
2446
|
|
|
* @param int $index |
|
2447
|
|
|
* @param bool|PPFrame $frame |
|
2448
|
|
|
* |
|
2449
|
|
|
* @throws MWException |
|
2450
|
|
|
* @return string |
|
2451
|
|
|
*/ |
|
2452
|
|
|
public function getVariableValue( $index, $frame = false ) { |
|
2453
|
|
|
global $wgContLang, $wgSitename, $wgServer, $wgServerName; |
|
2454
|
|
|
global $wgArticlePath, $wgScriptPath, $wgStylePath; |
|
2455
|
|
|
|
|
2456
|
|
|
if ( is_null( $this->mTitle ) ) { |
|
2457
|
|
|
// If no title set, bad things are going to happen |
|
2458
|
|
|
// later. Title should always be set since this |
|
2459
|
|
|
// should only be called in the middle of a parse |
|
2460
|
|
|
// operation (but the unit-tests do funky stuff) |
|
2461
|
|
|
throw new MWException( __METHOD__ . ' Should only be ' |
|
2462
|
|
|
. ' called while parsing (no title set)' ); |
|
2463
|
|
|
} |
|
2464
|
|
|
|
|
2465
|
|
|
/** |
|
2466
|
|
|
* Some of these require message or data lookups and can be |
|
2467
|
|
|
* expensive to check many times. |
|
2468
|
|
|
*/ |
|
2469
|
|
|
if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$this, &$this->mVarCache ] ) ) { |
|
2470
|
|
|
if ( isset( $this->mVarCache[$index] ) ) { |
|
2471
|
|
|
return $this->mVarCache[$index]; |
|
2472
|
|
|
} |
|
2473
|
|
|
} |
|
2474
|
|
|
|
|
2475
|
|
|
$ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() ); |
|
2476
|
|
|
Hooks::run( 'ParserGetVariableValueTs', [ &$this, &$ts ] ); |
|
2477
|
|
|
|
|
2478
|
|
|
$pageLang = $this->getFunctionLang(); |
|
2479
|
|
|
|
|
2480
|
|
|
switch ( $index ) { |
|
2481
|
|
|
case '!': |
|
2482
|
|
|
$value = '|'; |
|
2483
|
|
|
break; |
|
2484
|
|
|
case 'currentmonth': |
|
2485
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) ); |
|
2486
|
|
|
break; |
|
2487
|
|
|
case 'currentmonth1': |
|
2488
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) ); |
|
2489
|
|
|
break; |
|
2490
|
|
|
case 'currentmonthname': |
|
2491
|
|
|
$value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) ); |
|
2492
|
|
|
break; |
|
2493
|
|
|
case 'currentmonthnamegen': |
|
2494
|
|
|
$value = $pageLang->getMonthNameGen( MWTimestamp::getInstance( $ts )->format( 'n' ) ); |
|
2495
|
|
|
break; |
|
2496
|
|
|
case 'currentmonthabbrev': |
|
2497
|
|
|
$value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) ); |
|
2498
|
|
|
break; |
|
2499
|
|
|
case 'currentday': |
|
2500
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) ); |
|
2501
|
|
|
break; |
|
2502
|
|
|
case 'currentday2': |
|
2503
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) ); |
|
2504
|
|
|
break; |
|
2505
|
|
|
case 'localmonth': |
|
2506
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) ); |
|
2507
|
|
|
break; |
|
2508
|
|
|
case 'localmonth1': |
|
2509
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) ); |
|
2510
|
|
|
break; |
|
2511
|
|
|
case 'localmonthname': |
|
2512
|
|
|
$value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) ); |
|
2513
|
|
|
break; |
|
2514
|
|
|
case 'localmonthnamegen': |
|
2515
|
|
|
$value = $pageLang->getMonthNameGen( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) ); |
|
2516
|
|
|
break; |
|
2517
|
|
|
case 'localmonthabbrev': |
|
2518
|
|
|
$value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) ); |
|
2519
|
|
|
break; |
|
2520
|
|
|
case 'localday': |
|
2521
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) ); |
|
2522
|
|
|
break; |
|
2523
|
|
|
case 'localday2': |
|
2524
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) ); |
|
2525
|
|
|
break; |
|
2526
|
|
|
case 'pagename': |
|
2527
|
|
|
$value = wfEscapeWikiText( $this->mTitle->getText() ); |
|
2528
|
|
|
break; |
|
2529
|
|
|
case 'pagenamee': |
|
2530
|
|
|
$value = wfEscapeWikiText( $this->mTitle->getPartialURL() ); |
|
2531
|
|
|
break; |
|
2532
|
|
|
case 'fullpagename': |
|
2533
|
|
|
$value = wfEscapeWikiText( $this->mTitle->getPrefixedText() ); |
|
2534
|
|
|
break; |
|
2535
|
|
|
case 'fullpagenamee': |
|
2536
|
|
|
$value = wfEscapeWikiText( $this->mTitle->getPrefixedURL() ); |
|
2537
|
|
|
break; |
|
2538
|
|
|
case 'subpagename': |
|
2539
|
|
|
$value = wfEscapeWikiText( $this->mTitle->getSubpageText() ); |
|
2540
|
|
|
break; |
|
2541
|
|
|
case 'subpagenamee': |
|
2542
|
|
|
$value = wfEscapeWikiText( $this->mTitle->getSubpageUrlForm() ); |
|
2543
|
|
|
break; |
|
2544
|
|
|
case 'rootpagename': |
|
2545
|
|
|
$value = wfEscapeWikiText( $this->mTitle->getRootText() ); |
|
2546
|
|
|
break; |
|
2547
|
|
View Code Duplication |
case 'rootpagenamee': |
|
2548
|
|
|
$value = wfEscapeWikiText( wfUrlencode( str_replace( |
|
2549
|
|
|
' ', |
|
2550
|
|
|
'_', |
|
2551
|
|
|
$this->mTitle->getRootText() |
|
2552
|
|
|
) ) ); |
|
2553
|
|
|
break; |
|
2554
|
|
|
case 'basepagename': |
|
2555
|
|
|
$value = wfEscapeWikiText( $this->mTitle->getBaseText() ); |
|
2556
|
|
|
break; |
|
2557
|
|
View Code Duplication |
case 'basepagenamee': |
|
2558
|
|
|
$value = wfEscapeWikiText( wfUrlencode( str_replace( |
|
2559
|
|
|
' ', |
|
2560
|
|
|
'_', |
|
2561
|
|
|
$this->mTitle->getBaseText() |
|
2562
|
|
|
) ) ); |
|
2563
|
|
|
break; |
|
2564
|
|
View Code Duplication |
case 'talkpagename': |
|
2565
|
|
|
if ( $this->mTitle->canTalk() ) { |
|
2566
|
|
|
$talkPage = $this->mTitle->getTalkPage(); |
|
2567
|
|
|
$value = wfEscapeWikiText( $talkPage->getPrefixedText() ); |
|
2568
|
|
|
} else { |
|
2569
|
|
|
$value = ''; |
|
2570
|
|
|
} |
|
2571
|
|
|
break; |
|
2572
|
|
View Code Duplication |
case 'talkpagenamee': |
|
2573
|
|
|
if ( $this->mTitle->canTalk() ) { |
|
2574
|
|
|
$talkPage = $this->mTitle->getTalkPage(); |
|
2575
|
|
|
$value = wfEscapeWikiText( $talkPage->getPrefixedURL() ); |
|
2576
|
|
|
} else { |
|
2577
|
|
|
$value = ''; |
|
2578
|
|
|
} |
|
2579
|
|
|
break; |
|
2580
|
|
|
case 'subjectpagename': |
|
2581
|
|
|
$subjPage = $this->mTitle->getSubjectPage(); |
|
2582
|
|
|
$value = wfEscapeWikiText( $subjPage->getPrefixedText() ); |
|
2583
|
|
|
break; |
|
2584
|
|
|
case 'subjectpagenamee': |
|
2585
|
|
|
$subjPage = $this->mTitle->getSubjectPage(); |
|
2586
|
|
|
$value = wfEscapeWikiText( $subjPage->getPrefixedURL() ); |
|
2587
|
|
|
break; |
|
2588
|
|
|
case 'pageid': // requested in bug 23427 |
|
2589
|
|
|
$pageid = $this->getTitle()->getArticleID(); |
|
2590
|
|
|
if ( $pageid == 0 ) { |
|
2591
|
|
|
# 0 means the page doesn't exist in the database, |
|
2592
|
|
|
# which means the user is previewing a new page. |
|
2593
|
|
|
# The vary-revision flag must be set, because the magic word |
|
2594
|
|
|
# will have a different value once the page is saved. |
|
2595
|
|
|
$this->mOutput->setFlag( 'vary-revision' ); |
|
2596
|
|
|
wfDebug( __METHOD__ . ": {{PAGEID}} used in a new page, setting vary-revision...\n" ); |
|
2597
|
|
|
} |
|
2598
|
|
|
$value = $pageid ? $pageid : null; |
|
2599
|
|
|
break; |
|
2600
|
|
|
case 'revisionid': |
|
2601
|
|
|
# Let the edit saving system know we should parse the page |
|
2602
|
|
|
# *after* a revision ID has been assigned. |
|
2603
|
|
|
$this->mOutput->setFlag( 'vary-revision' ); |
|
2604
|
|
|
wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" ); |
|
2605
|
|
|
$value = $this->mRevisionId; |
|
2606
|
|
|
break; |
|
2607
|
|
View Code Duplication |
case 'revisionday': |
|
2608
|
|
|
# Let the edit saving system know we should parse the page |
|
2609
|
|
|
# *after* a revision ID has been assigned. This is for null edits. |
|
2610
|
|
|
$this->mOutput->setFlag( 'vary-revision' ); |
|
2611
|
|
|
wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" ); |
|
2612
|
|
|
$value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) ); |
|
2613
|
|
|
break; |
|
2614
|
|
View Code Duplication |
case 'revisionday2': |
|
2615
|
|
|
# Let the edit saving system know we should parse the page |
|
2616
|
|
|
# *after* a revision ID has been assigned. This is for null edits. |
|
2617
|
|
|
$this->mOutput->setFlag( 'vary-revision' ); |
|
2618
|
|
|
wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" ); |
|
2619
|
|
|
$value = substr( $this->getRevisionTimestamp(), 6, 2 ); |
|
2620
|
|
|
break; |
|
2621
|
|
View Code Duplication |
case 'revisionmonth': |
|
2622
|
|
|
# Let the edit saving system know we should parse the page |
|
2623
|
|
|
# *after* a revision ID has been assigned. This is for null edits. |
|
2624
|
|
|
$this->mOutput->setFlag( 'vary-revision' ); |
|
2625
|
|
|
wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" ); |
|
2626
|
|
|
$value = substr( $this->getRevisionTimestamp(), 4, 2 ); |
|
2627
|
|
|
break; |
|
2628
|
|
View Code Duplication |
case 'revisionmonth1': |
|
2629
|
|
|
# Let the edit saving system know we should parse the page |
|
2630
|
|
|
# *after* a revision ID has been assigned. This is for null edits. |
|
2631
|
|
|
$this->mOutput->setFlag( 'vary-revision' ); |
|
2632
|
|
|
wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" ); |
|
2633
|
|
|
$value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) ); |
|
2634
|
|
|
break; |
|
2635
|
|
View Code Duplication |
case 'revisionyear': |
|
2636
|
|
|
# Let the edit saving system know we should parse the page |
|
2637
|
|
|
# *after* a revision ID has been assigned. This is for null edits. |
|
2638
|
|
|
$this->mOutput->setFlag( 'vary-revision' ); |
|
2639
|
|
|
wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" ); |
|
2640
|
|
|
$value = substr( $this->getRevisionTimestamp(), 0, 4 ); |
|
2641
|
|
|
break; |
|
2642
|
|
|
case 'revisiontimestamp': |
|
2643
|
|
|
# Let the edit saving system know we should parse the page |
|
2644
|
|
|
# *after* a revision ID has been assigned. This is for null edits. |
|
2645
|
|
|
$this->mOutput->setFlag( 'vary-revision' ); |
|
2646
|
|
|
wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" ); |
|
2647
|
|
|
$value = $this->getRevisionTimestamp(); |
|
2648
|
|
|
break; |
|
2649
|
|
|
case 'revisionuser': |
|
2650
|
|
|
# Let the edit saving system know we should parse the page |
|
2651
|
|
|
# *after* a revision ID has been assigned for null edits. |
|
2652
|
|
|
$this->mOutput->setFlag( 'vary-user' ); |
|
2653
|
|
|
wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-user...\n" ); |
|
2654
|
|
|
$value = $this->getRevisionUser(); |
|
2655
|
|
|
break; |
|
2656
|
|
|
case 'revisionsize': |
|
2657
|
|
|
$value = $this->getRevisionSize(); |
|
2658
|
|
|
break; |
|
2659
|
|
|
case 'namespace': |
|
2660
|
|
|
$value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) ); |
|
2661
|
|
|
break; |
|
2662
|
|
|
case 'namespacee': |
|
2663
|
|
|
$value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) ); |
|
2664
|
|
|
break; |
|
2665
|
|
|
case 'namespacenumber': |
|
2666
|
|
|
$value = $this->mTitle->getNamespace(); |
|
2667
|
|
|
break; |
|
2668
|
|
|
case 'talkspace': |
|
2669
|
|
|
$value = $this->mTitle->canTalk() |
|
2670
|
|
|
? str_replace( '_', ' ', $this->mTitle->getTalkNsText() ) |
|
2671
|
|
|
: ''; |
|
2672
|
|
|
break; |
|
2673
|
|
|
case 'talkspacee': |
|
2674
|
|
|
$value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : ''; |
|
2675
|
|
|
break; |
|
2676
|
|
|
case 'subjectspace': |
|
2677
|
|
|
$value = str_replace( '_', ' ', $this->mTitle->getSubjectNsText() ); |
|
2678
|
|
|
break; |
|
2679
|
|
|
case 'subjectspacee': |
|
2680
|
|
|
$value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) ); |
|
2681
|
|
|
break; |
|
2682
|
|
|
case 'currentdayname': |
|
2683
|
|
|
$value = $pageLang->getWeekdayName( (int)MWTimestamp::getInstance( $ts )->format( 'w' ) + 1 ); |
|
2684
|
|
|
break; |
|
2685
|
|
|
case 'currentyear': |
|
2686
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'Y' ), true ); |
|
2687
|
|
|
break; |
|
2688
|
|
|
case 'currenttime': |
|
2689
|
|
|
$value = $pageLang->time( wfTimestamp( TS_MW, $ts ), false, false ); |
|
|
|
|
|
|
2690
|
|
|
break; |
|
2691
|
|
|
case 'currenthour': |
|
2692
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'H' ), true ); |
|
2693
|
|
|
break; |
|
2694
|
|
|
case 'currentweek': |
|
2695
|
|
|
# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to |
|
2696
|
|
|
# int to remove the padding |
|
2697
|
|
|
$value = $pageLang->formatNum( (int)MWTimestamp::getInstance( $ts )->format( 'W' ) ); |
|
2698
|
|
|
break; |
|
2699
|
|
|
case 'currentdow': |
|
2700
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'w' ) ); |
|
2701
|
|
|
break; |
|
2702
|
|
|
case 'localdayname': |
|
2703
|
|
|
$value = $pageLang->getWeekdayName( |
|
2704
|
|
|
(int)MWTimestamp::getLocalInstance( $ts )->format( 'w' ) + 1 |
|
2705
|
|
|
); |
|
2706
|
|
|
break; |
|
2707
|
|
|
case 'localyear': |
|
2708
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'Y' ), true ); |
|
2709
|
|
|
break; |
|
2710
|
|
|
case 'localtime': |
|
2711
|
|
|
$value = $pageLang->time( |
|
2712
|
|
|
MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ), |
|
2713
|
|
|
false, |
|
2714
|
|
|
false |
|
2715
|
|
|
); |
|
2716
|
|
|
break; |
|
2717
|
|
|
case 'localhour': |
|
2718
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'H' ), true ); |
|
2719
|
|
|
break; |
|
2720
|
|
|
case 'localweek': |
|
2721
|
|
|
# @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to |
|
2722
|
|
|
# int to remove the padding |
|
2723
|
|
|
$value = $pageLang->formatNum( (int)MWTimestamp::getLocalInstance( $ts )->format( 'W' ) ); |
|
2724
|
|
|
break; |
|
2725
|
|
|
case 'localdow': |
|
2726
|
|
|
$value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'w' ) ); |
|
2727
|
|
|
break; |
|
2728
|
|
|
case 'numberofarticles': |
|
2729
|
|
|
$value = $pageLang->formatNum( SiteStats::articles() ); |
|
2730
|
|
|
break; |
|
2731
|
|
|
case 'numberoffiles': |
|
2732
|
|
|
$value = $pageLang->formatNum( SiteStats::images() ); |
|
2733
|
|
|
break; |
|
2734
|
|
|
case 'numberofusers': |
|
2735
|
|
|
$value = $pageLang->formatNum( SiteStats::users() ); |
|
2736
|
|
|
break; |
|
2737
|
|
|
case 'numberofactiveusers': |
|
2738
|
|
|
$value = $pageLang->formatNum( SiteStats::activeUsers() ); |
|
2739
|
|
|
break; |
|
2740
|
|
|
case 'numberofpages': |
|
2741
|
|
|
$value = $pageLang->formatNum( SiteStats::pages() ); |
|
2742
|
|
|
break; |
|
2743
|
|
|
case 'numberofadmins': |
|
2744
|
|
|
$value = $pageLang->formatNum( SiteStats::numberingroup( 'sysop' ) ); |
|
2745
|
|
|
break; |
|
2746
|
|
|
case 'numberofedits': |
|
2747
|
|
|
$value = $pageLang->formatNum( SiteStats::edits() ); |
|
2748
|
|
|
break; |
|
2749
|
|
|
case 'currenttimestamp': |
|
2750
|
|
|
$value = wfTimestamp( TS_MW, $ts ); |
|
2751
|
|
|
break; |
|
2752
|
|
|
case 'localtimestamp': |
|
2753
|
|
|
$value = MWTimestamp::getLocalInstance( $ts )->format( 'YmdHis' ); |
|
2754
|
|
|
break; |
|
2755
|
|
|
case 'currentversion': |
|
2756
|
|
|
$value = SpecialVersion::getVersion(); |
|
2757
|
|
|
break; |
|
2758
|
|
|
case 'articlepath': |
|
2759
|
|
|
return $wgArticlePath; |
|
2760
|
|
|
case 'sitename': |
|
2761
|
|
|
return $wgSitename; |
|
2762
|
|
|
case 'server': |
|
2763
|
|
|
return $wgServer; |
|
2764
|
|
|
case 'servername': |
|
2765
|
|
|
return $wgServerName; |
|
2766
|
|
|
case 'scriptpath': |
|
2767
|
|
|
return $wgScriptPath; |
|
2768
|
|
|
case 'stylepath': |
|
2769
|
|
|
return $wgStylePath; |
|
2770
|
|
|
case 'directionmark': |
|
2771
|
|
|
return $pageLang->getDirMark(); |
|
2772
|
|
|
case 'contentlanguage': |
|
2773
|
|
|
global $wgLanguageCode; |
|
2774
|
|
|
return $wgLanguageCode; |
|
2775
|
|
|
case 'cascadingsources': |
|
2776
|
|
|
$value = CoreParserFunctions::cascadingsources( $this ); |
|
2777
|
|
|
break; |
|
2778
|
|
|
default: |
|
2779
|
|
|
$ret = null; |
|
2780
|
|
|
Hooks::run( |
|
2781
|
|
|
'ParserGetVariableValueSwitch', |
|
2782
|
|
|
[ &$this, &$this->mVarCache, &$index, &$ret, &$frame ] |
|
2783
|
|
|
); |
|
2784
|
|
|
|
|
2785
|
|
|
return $ret; |
|
2786
|
|
|
} |
|
2787
|
|
|
|
|
2788
|
|
|
if ( $index ) { |
|
2789
|
|
|
$this->mVarCache[$index] = $value; |
|
2790
|
|
|
} |
|
2791
|
|
|
|
|
2792
|
|
|
return $value; |
|
2793
|
|
|
} |
|
2794
|
|
|
|
|
2795
|
|
|
/** |
|
2796
|
|
|
* initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers |
|
2797
|
|
|
* |
|
2798
|
|
|
* @private |
|
2799
|
|
|
*/ |
|
2800
|
|
|
public function initialiseVariables() { |
|
2801
|
|
|
$variableIDs = MagicWord::getVariableIDs(); |
|
2802
|
|
|
$substIDs = MagicWord::getSubstIDs(); |
|
2803
|
|
|
|
|
2804
|
|
|
$this->mVariables = new MagicWordArray( $variableIDs ); |
|
2805
|
|
|
$this->mSubstWords = new MagicWordArray( $substIDs ); |
|
2806
|
|
|
} |
|
2807
|
|
|
|
|
2808
|
|
|
/** |
|
2809
|
|
|
* Preprocess some wikitext and return the document tree. |
|
2810
|
|
|
* This is the ghost of replace_variables(). |
|
2811
|
|
|
* |
|
2812
|
|
|
* @param string $text The text to parse |
|
2813
|
|
|
* @param int $flags Bitwise combination of: |
|
2814
|
|
|
* - self::PTD_FOR_INCLUSION: Handle "<noinclude>" and "<includeonly>" as if the text is being |
|
2815
|
|
|
* included. Default is to assume a direct page view. |
|
2816
|
|
|
* |
|
2817
|
|
|
* The generated DOM tree must depend only on the input text and the flags. |
|
2818
|
|
|
* The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899. |
|
2819
|
|
|
* |
|
2820
|
|
|
* Any flag added to the $flags parameter here, or any other parameter liable to cause a |
|
2821
|
|
|
* change in the DOM tree for a given text, must be passed through the section identifier |
|
2822
|
|
|
* in the section edit link and thus back to extractSections(). |
|
2823
|
|
|
* |
|
2824
|
|
|
* The output of this function is currently only cached in process memory, but a persistent |
|
2825
|
|
|
* cache may be implemented at a later date which takes further advantage of these strict |
|
2826
|
|
|
* dependency requirements. |
|
2827
|
|
|
* |
|
2828
|
|
|
* @return PPNode |
|
2829
|
|
|
*/ |
|
2830
|
|
|
public function preprocessToDom( $text, $flags = 0 ) { |
|
2831
|
|
|
$dom = $this->getPreprocessor()->preprocessToObj( $text, $flags ); |
|
2832
|
|
|
return $dom; |
|
2833
|
|
|
} |
|
2834
|
|
|
|
|
2835
|
|
|
/** |
|
2836
|
|
|
* Return a three-element array: leading whitespace, string contents, trailing whitespace |
|
2837
|
|
|
* |
|
2838
|
|
|
* @param string $s |
|
2839
|
|
|
* |
|
2840
|
|
|
* @return array |
|
2841
|
|
|
*/ |
|
2842
|
|
|
public static function splitWhitespace( $s ) { |
|
2843
|
|
|
$ltrimmed = ltrim( $s ); |
|
2844
|
|
|
$w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) ); |
|
2845
|
|
|
$trimmed = rtrim( $ltrimmed ); |
|
2846
|
|
|
$diff = strlen( $ltrimmed ) - strlen( $trimmed ); |
|
2847
|
|
|
if ( $diff > 0 ) { |
|
2848
|
|
|
$w2 = substr( $ltrimmed, -$diff ); |
|
2849
|
|
|
} else { |
|
2850
|
|
|
$w2 = ''; |
|
2851
|
|
|
} |
|
2852
|
|
|
return [ $w1, $trimmed, $w2 ]; |
|
2853
|
|
|
} |
|
2854
|
|
|
|
|
2855
|
|
|
/** |
|
2856
|
|
|
* Replace magic variables, templates, and template arguments |
|
2857
|
|
|
* with the appropriate text. Templates are substituted recursively, |
|
2858
|
|
|
* taking care to avoid infinite loops. |
|
2859
|
|
|
* |
|
2860
|
|
|
* Note that the substitution depends on value of $mOutputType: |
|
2861
|
|
|
* self::OT_WIKI: only {{subst:}} templates |
|
2862
|
|
|
* self::OT_PREPROCESS: templates but not extension tags |
|
2863
|
|
|
* self::OT_HTML: all templates and extension tags |
|
2864
|
|
|
* |
|
2865
|
|
|
* @param string $text The text to transform |
|
2866
|
|
|
* @param bool|PPFrame $frame Object describing the arguments passed to the |
|
2867
|
|
|
* template. Arguments may also be provided as an associative array, as |
|
2868
|
|
|
* was the usual case before MW1.12. Providing arguments this way may be |
|
2869
|
|
|
* useful for extensions wishing to perform variable replacement |
|
2870
|
|
|
* explicitly. |
|
2871
|
|
|
* @param bool $argsOnly Only do argument (triple-brace) expansion, not |
|
2872
|
|
|
* double-brace expansion. |
|
2873
|
|
|
* @return string |
|
2874
|
|
|
*/ |
|
2875
|
|
|
public function replaceVariables( $text, $frame = false, $argsOnly = false ) { |
|
2876
|
|
|
# Is there any text? Also, Prevent too big inclusions! |
|
2877
|
|
|
$textSize = strlen( $text ); |
|
2878
|
|
|
if ( $textSize < 1 || $textSize > $this->mOptions->getMaxIncludeSize() ) { |
|
2879
|
|
|
return $text; |
|
2880
|
|
|
} |
|
2881
|
|
|
|
|
2882
|
|
|
if ( $frame === false ) { |
|
2883
|
|
|
$frame = $this->getPreprocessor()->newFrame(); |
|
2884
|
|
|
} elseif ( !( $frame instanceof PPFrame ) ) { |
|
2885
|
|
|
wfDebug( __METHOD__ . " called using plain parameters instead of " |
|
2886
|
|
|
. "a PPFrame instance. Creating custom frame.\n" ); |
|
2887
|
|
|
$frame = $this->getPreprocessor()->newCustomFrame( $frame ); |
|
2888
|
|
|
} |
|
2889
|
|
|
|
|
2890
|
|
|
$dom = $this->preprocessToDom( $text ); |
|
2891
|
|
|
$flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0; |
|
2892
|
|
|
$text = $frame->expand( $dom, $flags ); |
|
2893
|
|
|
|
|
2894
|
|
|
return $text; |
|
2895
|
|
|
} |
|
2896
|
|
|
|
|
2897
|
|
|
/** |
|
2898
|
|
|
* Clean up argument array - refactored in 1.9 so parserfunctions can use it, too. |
|
2899
|
|
|
* |
|
2900
|
|
|
* @param array $args |
|
2901
|
|
|
* |
|
2902
|
|
|
* @return array |
|
2903
|
|
|
*/ |
|
2904
|
|
|
public static function createAssocArgs( $args ) { |
|
2905
|
|
|
$assocArgs = []; |
|
2906
|
|
|
$index = 1; |
|
2907
|
|
|
foreach ( $args as $arg ) { |
|
2908
|
|
|
$eqpos = strpos( $arg, '=' ); |
|
2909
|
|
|
if ( $eqpos === false ) { |
|
2910
|
|
|
$assocArgs[$index++] = $arg; |
|
2911
|
|
|
} else { |
|
2912
|
|
|
$name = trim( substr( $arg, 0, $eqpos ) ); |
|
2913
|
|
|
$value = trim( substr( $arg, $eqpos + 1 ) ); |
|
2914
|
|
|
if ( $value === false ) { |
|
2915
|
|
|
$value = ''; |
|
2916
|
|
|
} |
|
2917
|
|
|
if ( $name !== false ) { |
|
2918
|
|
|
$assocArgs[$name] = $value; |
|
2919
|
|
|
} |
|
2920
|
|
|
} |
|
2921
|
|
|
} |
|
2922
|
|
|
|
|
2923
|
|
|
return $assocArgs; |
|
2924
|
|
|
} |
|
2925
|
|
|
|
|
2926
|
|
|
/** |
|
2927
|
|
|
* Warn the user when a parser limitation is reached |
|
2928
|
|
|
* Will warn at most once the user per limitation type |
|
2929
|
|
|
* |
|
2930
|
|
|
* The results are shown during preview and run through the Parser (See EditPage.php) |
|
2931
|
|
|
* |
|
2932
|
|
|
* @param string $limitationType Should be one of: |
|
2933
|
|
|
* 'expensive-parserfunction' (corresponding messages: |
|
2934
|
|
|
* 'expensive-parserfunction-warning', |
|
2935
|
|
|
* 'expensive-parserfunction-category') |
|
2936
|
|
|
* 'post-expand-template-argument' (corresponding messages: |
|
2937
|
|
|
* 'post-expand-template-argument-warning', |
|
2938
|
|
|
* 'post-expand-template-argument-category') |
|
2939
|
|
|
* 'post-expand-template-inclusion' (corresponding messages: |
|
2940
|
|
|
* 'post-expand-template-inclusion-warning', |
|
2941
|
|
|
* 'post-expand-template-inclusion-category') |
|
2942
|
|
|
* 'node-count-exceeded' (corresponding messages: |
|
2943
|
|
|
* 'node-count-exceeded-warning', |
|
2944
|
|
|
* 'node-count-exceeded-category') |
|
2945
|
|
|
* 'expansion-depth-exceeded' (corresponding messages: |
|
2946
|
|
|
* 'expansion-depth-exceeded-warning', |
|
2947
|
|
|
* 'expansion-depth-exceeded-category') |
|
2948
|
|
|
* @param string|int|null $current Current value |
|
2949
|
|
|
* @param string|int|null $max Maximum allowed, when an explicit limit has been |
|
2950
|
|
|
* exceeded, provide the values (optional) |
|
2951
|
|
|
*/ |
|
2952
|
|
|
public function limitationWarn( $limitationType, $current = '', $max = '' ) { |
|
2953
|
|
|
# does no harm if $current and $max are present but are unnecessary for the message |
|
2954
|
|
|
# Not doing ->inLanguage( $this->mOptions->getUserLangObj() ), since this is shown |
|
2955
|
|
|
# only during preview, and that would split the parser cache unnecessarily. |
|
2956
|
|
|
$warning = wfMessage( "$limitationType-warning" )->numParams( $current, $max ) |
|
2957
|
|
|
->text(); |
|
2958
|
|
|
$this->mOutput->addWarning( $warning ); |
|
2959
|
|
|
$this->addTrackingCategory( "$limitationType-category" ); |
|
2960
|
|
|
} |
|
2961
|
|
|
|
|
2962
|
|
|
/** |
|
2963
|
|
|
* Return the text of a template, after recursively |
|
2964
|
|
|
* replacing any variables or templates within the template. |
|
2965
|
|
|
* |
|
2966
|
|
|
* @param array $piece The parts of the template |
|
2967
|
|
|
* $piece['title']: the title, i.e. the part before the | |
|
2968
|
|
|
* $piece['parts']: the parameter array |
|
2969
|
|
|
* $piece['lineStart']: whether the brace was at the start of a line |
|
2970
|
|
|
* @param PPFrame $frame The current frame, contains template arguments |
|
2971
|
|
|
* @throws Exception |
|
2972
|
|
|
* @return string The text of the template |
|
2973
|
|
|
*/ |
|
2974
|
|
|
public function braceSubstitution( $piece, $frame ) { |
|
2975
|
|
|
|
|
2976
|
|
|
// Flags |
|
2977
|
|
|
|
|
2978
|
|
|
// $text has been filled |
|
2979
|
|
|
$found = false; |
|
2980
|
|
|
// wiki markup in $text should be escaped |
|
2981
|
|
|
$nowiki = false; |
|
2982
|
|
|
// $text is HTML, armour it against wikitext transformation |
|
2983
|
|
|
$isHTML = false; |
|
2984
|
|
|
// Force interwiki transclusion to be done in raw mode not rendered |
|
2985
|
|
|
$forceRawInterwiki = false; |
|
2986
|
|
|
// $text is a DOM node needing expansion in a child frame |
|
2987
|
|
|
$isChildObj = false; |
|
2988
|
|
|
// $text is a DOM node needing expansion in the current frame |
|
2989
|
|
|
$isLocalObj = false; |
|
2990
|
|
|
|
|
2991
|
|
|
# Title object, where $text came from |
|
2992
|
|
|
$title = false; |
|
2993
|
|
|
|
|
2994
|
|
|
# $part1 is the bit before the first |, and must contain only title characters. |
|
2995
|
|
|
# Various prefixes will be stripped from it later. |
|
2996
|
|
|
$titleWithSpaces = $frame->expand( $piece['title'] ); |
|
2997
|
|
|
$part1 = trim( $titleWithSpaces ); |
|
2998
|
|
|
$titleText = false; |
|
2999
|
|
|
|
|
3000
|
|
|
# Original title text preserved for various purposes |
|
3001
|
|
|
$originalTitle = $part1; |
|
3002
|
|
|
|
|
3003
|
|
|
# $args is a list of argument nodes, starting from index 0, not including $part1 |
|
3004
|
|
|
# @todo FIXME: If piece['parts'] is null then the call to getLength() |
|
3005
|
|
|
# below won't work b/c this $args isn't an object |
|
3006
|
|
|
$args = ( null == $piece['parts'] ) ? [] : $piece['parts']; |
|
3007
|
|
|
|
|
3008
|
|
|
$profileSection = null; // profile templates |
|
3009
|
|
|
|
|
3010
|
|
|
# SUBST |
|
3011
|
|
|
if ( !$found ) { |
|
3012
|
|
|
$substMatch = $this->mSubstWords->matchStartAndRemove( $part1 ); |
|
3013
|
|
|
|
|
3014
|
|
|
# Possibilities for substMatch: "subst", "safesubst" or FALSE |
|
3015
|
|
|
# Decide whether to expand template or keep wikitext as-is. |
|
3016
|
|
|
if ( $this->ot['wiki'] ) { |
|
3017
|
|
|
if ( $substMatch === false ) { |
|
3018
|
|
|
$literal = true; # literal when in PST with no prefix |
|
3019
|
|
|
} else { |
|
3020
|
|
|
$literal = false; # expand when in PST with subst: or safesubst: |
|
3021
|
|
|
} |
|
3022
|
|
|
} else { |
|
3023
|
|
|
if ( $substMatch == 'subst' ) { |
|
3024
|
|
|
$literal = true; # literal when not in PST with plain subst: |
|
3025
|
|
|
} else { |
|
3026
|
|
|
$literal = false; # expand when not in PST with safesubst: or no prefix |
|
3027
|
|
|
} |
|
3028
|
|
|
} |
|
3029
|
|
|
if ( $literal ) { |
|
3030
|
|
|
$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args ); |
|
3031
|
|
|
$isLocalObj = true; |
|
3032
|
|
|
$found = true; |
|
3033
|
|
|
} |
|
3034
|
|
|
} |
|
3035
|
|
|
|
|
3036
|
|
|
# Variables |
|
3037
|
|
|
if ( !$found && $args->getLength() == 0 ) { |
|
3038
|
|
|
$id = $this->mVariables->matchStartToEnd( $part1 ); |
|
3039
|
|
|
if ( $id !== false ) { |
|
3040
|
|
|
$text = $this->getVariableValue( $id, $frame ); |
|
3041
|
|
|
if ( MagicWord::getCacheTTL( $id ) > -1 ) { |
|
3042
|
|
|
$this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) ); |
|
3043
|
|
|
} |
|
3044
|
|
|
$found = true; |
|
3045
|
|
|
} |
|
3046
|
|
|
} |
|
3047
|
|
|
|
|
3048
|
|
|
# MSG, MSGNW and RAW |
|
3049
|
|
|
if ( !$found ) { |
|
3050
|
|
|
# Check for MSGNW: |
|
3051
|
|
|
$mwMsgnw = MagicWord::get( 'msgnw' ); |
|
3052
|
|
|
if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) { |
|
3053
|
|
|
$nowiki = true; |
|
3054
|
|
|
} else { |
|
3055
|
|
|
# Remove obsolete MSG: |
|
3056
|
|
|
$mwMsg = MagicWord::get( 'msg' ); |
|
3057
|
|
|
$mwMsg->matchStartAndRemove( $part1 ); |
|
3058
|
|
|
} |
|
3059
|
|
|
|
|
3060
|
|
|
# Check for RAW: |
|
3061
|
|
|
$mwRaw = MagicWord::get( 'raw' ); |
|
3062
|
|
|
if ( $mwRaw->matchStartAndRemove( $part1 ) ) { |
|
3063
|
|
|
$forceRawInterwiki = true; |
|
3064
|
|
|
} |
|
3065
|
|
|
} |
|
3066
|
|
|
|
|
3067
|
|
|
# Parser functions |
|
3068
|
|
|
if ( !$found ) { |
|
3069
|
|
|
$colonPos = strpos( $part1, ':' ); |
|
3070
|
|
|
if ( $colonPos !== false ) { |
|
3071
|
|
|
$func = substr( $part1, 0, $colonPos ); |
|
3072
|
|
|
$funcArgs = [ trim( substr( $part1, $colonPos + 1 ) ) ]; |
|
3073
|
|
|
$argsLength = $args->getLength(); |
|
3074
|
|
|
for ( $i = 0; $i < $argsLength; $i++ ) { |
|
3075
|
|
|
$funcArgs[] = $args->item( $i ); |
|
3076
|
|
|
} |
|
3077
|
|
|
try { |
|
3078
|
|
|
$result = $this->callParserFunction( $frame, $func, $funcArgs ); |
|
3079
|
|
|
} catch ( Exception $ex ) { |
|
3080
|
|
|
throw $ex; |
|
3081
|
|
|
} |
|
3082
|
|
|
|
|
3083
|
|
|
# The interface for parser functions allows for extracting |
|
3084
|
|
|
# flags into the local scope. Extract any forwarded flags |
|
3085
|
|
|
# here. |
|
3086
|
|
|
extract( $result ); |
|
3087
|
|
|
} |
|
3088
|
|
|
} |
|
3089
|
|
|
|
|
3090
|
|
|
# Finish mangling title and then check for loops. |
|
3091
|
|
|
# Set $title to a Title object and $titleText to the PDBK |
|
3092
|
|
|
if ( !$found ) { |
|
3093
|
|
|
$ns = NS_TEMPLATE; |
|
3094
|
|
|
# Split the title into page and subpage |
|
3095
|
|
|
$subpage = ''; |
|
3096
|
|
|
$relative = $this->maybeDoSubpageLink( $part1, $subpage ); |
|
3097
|
|
|
if ( $part1 !== $relative ) { |
|
3098
|
|
|
$part1 = $relative; |
|
3099
|
|
|
$ns = $this->mTitle->getNamespace(); |
|
3100
|
|
|
} |
|
3101
|
|
|
$title = Title::newFromText( $part1, $ns ); |
|
3102
|
|
|
if ( $title ) { |
|
3103
|
|
|
$titleText = $title->getPrefixedText(); |
|
3104
|
|
|
# Check for language variants if the template is not found |
|
3105
|
|
|
if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) { |
|
3106
|
|
|
$this->getConverterLanguage()->findVariantLink( $part1, $title, true ); |
|
3107
|
|
|
} |
|
3108
|
|
|
# Do recursion depth check |
|
3109
|
|
|
$limit = $this->mOptions->getMaxTemplateDepth(); |
|
3110
|
|
View Code Duplication |
if ( $frame->depth >= $limit ) { |
|
|
|
|
|
|
3111
|
|
|
$found = true; |
|
3112
|
|
|
$text = '<span class="error">' |
|
3113
|
|
|
. wfMessage( 'parser-template-recursion-depth-warning' ) |
|
3114
|
|
|
->numParams( $limit )->inContentLanguage()->text() |
|
3115
|
|
|
. '</span>'; |
|
3116
|
|
|
} |
|
3117
|
|
|
} |
|
3118
|
|
|
} |
|
3119
|
|
|
|
|
3120
|
|
|
# Load from database |
|
3121
|
|
|
if ( !$found && $title ) { |
|
3122
|
|
|
$profileSection = $this->mProfiler->scopedProfileIn( $title->getPrefixedDBkey() ); |
|
3123
|
|
|
if ( !$title->isExternal() ) { |
|
3124
|
|
|
if ( $title->isSpecialPage() |
|
3125
|
|
|
&& $this->mOptions->getAllowSpecialInclusion() |
|
3126
|
|
|
&& $this->ot['html'] |
|
3127
|
|
|
) { |
|
3128
|
|
|
$specialPage = SpecialPageFactory::getPage( $title->getDBkey() ); |
|
3129
|
|
|
// Pass the template arguments as URL parameters. |
|
3130
|
|
|
// "uselang" will have no effect since the Language object |
|
3131
|
|
|
// is forced to the one defined in ParserOptions. |
|
3132
|
|
|
$pageArgs = []; |
|
3133
|
|
|
$argsLength = $args->getLength(); |
|
3134
|
|
|
for ( $i = 0; $i < $argsLength; $i++ ) { |
|
3135
|
|
|
$bits = $args->item( $i )->splitArg(); |
|
3136
|
|
|
if ( strval( $bits['index'] ) === '' ) { |
|
3137
|
|
|
$name = trim( $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) ); |
|
3138
|
|
|
$value = trim( $frame->expand( $bits['value'] ) ); |
|
3139
|
|
|
$pageArgs[$name] = $value; |
|
3140
|
|
|
} |
|
3141
|
|
|
} |
|
3142
|
|
|
|
|
3143
|
|
|
// Create a new context to execute the special page |
|
3144
|
|
|
$context = new RequestContext; |
|
3145
|
|
|
$context->setTitle( $title ); |
|
3146
|
|
|
$context->setRequest( new FauxRequest( $pageArgs ) ); |
|
3147
|
|
|
if ( $specialPage && $specialPage->maxIncludeCacheTime() === 0 ) { |
|
3148
|
|
|
$context->setUser( $this->getUser() ); |
|
3149
|
|
|
} else { |
|
3150
|
|
|
// If this page is cached, then we better not be per user. |
|
3151
|
|
|
$context->setUser( User::newFromName( '127.0.0.1', false ) ); |
|
|
|
|
|
|
3152
|
|
|
} |
|
3153
|
|
|
$context->setLanguage( $this->mOptions->getUserLangObj() ); |
|
3154
|
|
|
$ret = SpecialPageFactory::capturePath( $title, $context ); |
|
3155
|
|
|
if ( $ret ) { |
|
3156
|
|
|
$text = $context->getOutput()->getHTML(); |
|
3157
|
|
|
$this->mOutput->addOutputPageMetadata( $context->getOutput() ); |
|
3158
|
|
|
$found = true; |
|
3159
|
|
|
$isHTML = true; |
|
3160
|
|
|
if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) { |
|
3161
|
|
|
$this->mOutput->updateCacheExpiry( $specialPage->maxIncludeCacheTime() ); |
|
3162
|
|
|
} |
|
3163
|
|
|
} |
|
3164
|
|
|
} elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) { |
|
3165
|
|
|
$found = false; # access denied |
|
3166
|
|
|
wfDebug( __METHOD__ . ": template inclusion denied for " . |
|
3167
|
|
|
$title->getPrefixedDBkey() . "\n" ); |
|
3168
|
|
|
} else { |
|
3169
|
|
|
list( $text, $title ) = $this->getTemplateDom( $title ); |
|
3170
|
|
|
if ( $text !== false ) { |
|
3171
|
|
|
$found = true; |
|
3172
|
|
|
$isChildObj = true; |
|
3173
|
|
|
} |
|
3174
|
|
|
} |
|
3175
|
|
|
|
|
3176
|
|
|
# If the title is valid but undisplayable, make a link to it |
|
3177
|
|
|
if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) { |
|
3178
|
|
|
$text = "[[:$titleText]]"; |
|
3179
|
|
|
$found = true; |
|
3180
|
|
|
} |
|
3181
|
|
|
} elseif ( $title->isTrans() ) { |
|
3182
|
|
|
# Interwiki transclusion |
|
3183
|
|
|
if ( $this->ot['html'] && !$forceRawInterwiki ) { |
|
3184
|
|
|
$text = $this->interwikiTransclude( $title, 'render' ); |
|
3185
|
|
|
$isHTML = true; |
|
3186
|
|
|
} else { |
|
3187
|
|
|
$text = $this->interwikiTransclude( $title, 'raw' ); |
|
3188
|
|
|
# Preprocess it like a template |
|
3189
|
|
|
$text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION ); |
|
3190
|
|
|
$isChildObj = true; |
|
3191
|
|
|
} |
|
3192
|
|
|
$found = true; |
|
3193
|
|
|
} |
|
3194
|
|
|
|
|
3195
|
|
|
# Do infinite loop check |
|
3196
|
|
|
# This has to be done after redirect resolution to avoid infinite loops via redirects |
|
3197
|
|
|
if ( !$frame->loopCheck( $title ) ) { |
|
3198
|
|
|
$found = true; |
|
3199
|
|
|
$text = '<span class="error">' |
|
3200
|
|
|
. wfMessage( 'parser-template-loop-warning', $titleText )->inContentLanguage()->text() |
|
3201
|
|
|
. '</span>'; |
|
3202
|
|
|
wfDebug( __METHOD__ . ": template loop broken at '$titleText'\n" ); |
|
3203
|
|
|
} |
|
3204
|
|
|
} |
|
3205
|
|
|
|
|
3206
|
|
|
# If we haven't found text to substitute by now, we're done |
|
3207
|
|
|
# Recover the source wikitext and return it |
|
3208
|
|
|
if ( !$found ) { |
|
3209
|
|
|
$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args ); |
|
3210
|
|
|
if ( $profileSection ) { |
|
3211
|
|
|
$this->mProfiler->scopedProfileOut( $profileSection ); |
|
3212
|
|
|
} |
|
3213
|
|
|
return [ 'object' => $text ]; |
|
3214
|
|
|
} |
|
3215
|
|
|
|
|
3216
|
|
|
# Expand DOM-style return values in a child frame |
|
3217
|
|
|
if ( $isChildObj ) { |
|
3218
|
|
|
# Clean up argument array |
|
3219
|
|
|
$newFrame = $frame->newChild( $args, $title ); |
|
3220
|
|
|
|
|
3221
|
|
|
if ( $nowiki ) { |
|
3222
|
|
|
$text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG ); |
|
3223
|
|
|
} elseif ( $titleText !== false && $newFrame->isEmpty() ) { |
|
3224
|
|
|
# Expansion is eligible for the empty-frame cache |
|
3225
|
|
|
$text = $newFrame->cachedExpand( $titleText, $text ); |
|
3226
|
|
|
} else { |
|
3227
|
|
|
# Uncached expansion |
|
3228
|
|
|
$text = $newFrame->expand( $text ); |
|
3229
|
|
|
} |
|
3230
|
|
|
} |
|
3231
|
|
|
if ( $isLocalObj && $nowiki ) { |
|
3232
|
|
|
$text = $frame->expand( $text, PPFrame::RECOVER_ORIG ); |
|
3233
|
|
|
$isLocalObj = false; |
|
3234
|
|
|
} |
|
3235
|
|
|
|
|
3236
|
|
|
if ( $profileSection ) { |
|
3237
|
|
|
$this->mProfiler->scopedProfileOut( $profileSection ); |
|
3238
|
|
|
} |
|
3239
|
|
|
|
|
3240
|
|
|
# Replace raw HTML by a placeholder |
|
3241
|
|
|
if ( $isHTML ) { |
|
3242
|
|
|
$text = $this->insertStripItem( $text ); |
|
3243
|
|
|
} elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) { |
|
3244
|
|
|
# Escape nowiki-style return values |
|
3245
|
|
|
$text = wfEscapeWikiText( $text ); |
|
3246
|
|
|
} elseif ( is_string( $text ) |
|
3247
|
|
|
&& !$piece['lineStart'] |
|
3248
|
|
|
&& preg_match( '/^(?:{\\||:|;|#|\*)/', $text ) |
|
3249
|
|
|
) { |
|
3250
|
|
|
# Bug 529: if the template begins with a table or block-level |
|
3251
|
|
|
# element, it should be treated as beginning a new line. |
|
3252
|
|
|
# This behavior is somewhat controversial. |
|
3253
|
|
|
$text = "\n" . $text; |
|
3254
|
|
|
} |
|
3255
|
|
|
|
|
3256
|
|
|
if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) { |
|
3257
|
|
|
# Error, oversize inclusion |
|
3258
|
|
|
if ( $titleText !== false ) { |
|
3259
|
|
|
# Make a working, properly escaped link if possible (bug 23588) |
|
3260
|
|
|
$text = "[[:$titleText]]"; |
|
3261
|
|
|
} else { |
|
3262
|
|
|
# This will probably not be a working link, but at least it may |
|
3263
|
|
|
# provide some hint of where the problem is |
|
3264
|
|
|
preg_replace( '/^:/', '', $originalTitle ); |
|
3265
|
|
|
$text = "[[:$originalTitle]]"; |
|
3266
|
|
|
} |
|
3267
|
|
|
$text .= $this->insertStripItem( '<!-- WARNING: template omitted, ' |
|
3268
|
|
|
. 'post-expand include size too large -->' ); |
|
3269
|
|
|
$this->limitationWarn( 'post-expand-template-inclusion' ); |
|
3270
|
|
|
} |
|
3271
|
|
|
|
|
3272
|
|
|
if ( $isLocalObj ) { |
|
3273
|
|
|
$ret = [ 'object' => $text ]; |
|
3274
|
|
|
} else { |
|
3275
|
|
|
$ret = [ 'text' => $text ]; |
|
3276
|
|
|
} |
|
3277
|
|
|
|
|
3278
|
|
|
return $ret; |
|
3279
|
|
|
} |
|
3280
|
|
|
|
|
3281
|
|
|
/** |
|
3282
|
|
|
* Call a parser function and return an array with text and flags. |
|
3283
|
|
|
* |
|
3284
|
|
|
* The returned array will always contain a boolean 'found', indicating |
|
3285
|
|
|
* whether the parser function was found or not. It may also contain the |
|
3286
|
|
|
* following: |
|
3287
|
|
|
* text: string|object, resulting wikitext or PP DOM object |
|
3288
|
|
|
* isHTML: bool, $text is HTML, armour it against wikitext transformation |
|
3289
|
|
|
* isChildObj: bool, $text is a DOM node needing expansion in a child frame |
|
3290
|
|
|
* isLocalObj: bool, $text is a DOM node needing expansion in the current frame |
|
3291
|
|
|
* nowiki: bool, wiki markup in $text should be escaped |
|
3292
|
|
|
* |
|
3293
|
|
|
* @since 1.21 |
|
3294
|
|
|
* @param PPFrame $frame The current frame, contains template arguments |
|
3295
|
|
|
* @param string $function Function name |
|
3296
|
|
|
* @param array $args Arguments to the function |
|
3297
|
|
|
* @throws MWException |
|
3298
|
|
|
* @return array |
|
3299
|
|
|
*/ |
|
3300
|
|
|
public function callParserFunction( $frame, $function, array $args = [] ) { |
|
3301
|
|
|
global $wgContLang; |
|
3302
|
|
|
|
|
3303
|
|
|
# Case sensitive functions |
|
3304
|
|
|
if ( isset( $this->mFunctionSynonyms[1][$function] ) ) { |
|
3305
|
|
|
$function = $this->mFunctionSynonyms[1][$function]; |
|
3306
|
|
|
} else { |
|
3307
|
|
|
# Case insensitive functions |
|
3308
|
|
|
$function = $wgContLang->lc( $function ); |
|
3309
|
|
|
if ( isset( $this->mFunctionSynonyms[0][$function] ) ) { |
|
3310
|
|
|
$function = $this->mFunctionSynonyms[0][$function]; |
|
3311
|
|
|
} else { |
|
3312
|
|
|
return [ 'found' => false ]; |
|
3313
|
|
|
} |
|
3314
|
|
|
} |
|
3315
|
|
|
|
|
3316
|
|
|
list( $callback, $flags ) = $this->mFunctionHooks[$function]; |
|
3317
|
|
|
|
|
3318
|
|
|
# Workaround for PHP bug 35229 and similar |
|
3319
|
|
|
if ( !is_callable( $callback ) ) { |
|
3320
|
|
|
throw new MWException( "Tag hook for $function is not callable\n" ); |
|
3321
|
|
|
} |
|
3322
|
|
|
|
|
3323
|
|
|
$allArgs = [ &$this ]; |
|
3324
|
|
|
if ( $flags & self::SFH_OBJECT_ARGS ) { |
|
3325
|
|
|
# Convert arguments to PPNodes and collect for appending to $allArgs |
|
3326
|
|
|
$funcArgs = []; |
|
3327
|
|
|
foreach ( $args as $k => $v ) { |
|
3328
|
|
|
if ( $v instanceof PPNode || $k === 0 ) { |
|
3329
|
|
|
$funcArgs[] = $v; |
|
3330
|
|
|
} else { |
|
3331
|
|
|
$funcArgs[] = $this->mPreprocessor->newPartNodeArray( [ $k => $v ] )->item( 0 ); |
|
3332
|
|
|
} |
|
3333
|
|
|
} |
|
3334
|
|
|
|
|
3335
|
|
|
# Add a frame parameter, and pass the arguments as an array |
|
3336
|
|
|
$allArgs[] = $frame; |
|
3337
|
|
|
$allArgs[] = $funcArgs; |
|
3338
|
|
|
} else { |
|
3339
|
|
|
# Convert arguments to plain text and append to $allArgs |
|
3340
|
|
|
foreach ( $args as $k => $v ) { |
|
3341
|
|
|
if ( $v instanceof PPNode ) { |
|
3342
|
|
|
$allArgs[] = trim( $frame->expand( $v ) ); |
|
3343
|
|
|
} elseif ( is_int( $k ) && $k >= 0 ) { |
|
3344
|
|
|
$allArgs[] = trim( $v ); |
|
3345
|
|
|
} else { |
|
3346
|
|
|
$allArgs[] = trim( "$k=$v" ); |
|
3347
|
|
|
} |
|
3348
|
|
|
} |
|
3349
|
|
|
} |
|
3350
|
|
|
|
|
3351
|
|
|
$result = call_user_func_array( $callback, $allArgs ); |
|
3352
|
|
|
|
|
3353
|
|
|
# The interface for function hooks allows them to return a wikitext |
|
3354
|
|
|
# string or an array containing the string and any flags. This mungs |
|
3355
|
|
|
# things around to match what this method should return. |
|
3356
|
|
|
if ( !is_array( $result ) ) { |
|
3357
|
|
|
$result =[ |
|
3358
|
|
|
'found' => true, |
|
3359
|
|
|
'text' => $result, |
|
3360
|
|
|
]; |
|
3361
|
|
|
} else { |
|
3362
|
|
|
if ( isset( $result[0] ) && !isset( $result['text'] ) ) { |
|
3363
|
|
|
$result['text'] = $result[0]; |
|
3364
|
|
|
} |
|
3365
|
|
|
unset( $result[0] ); |
|
3366
|
|
|
$result += [ |
|
3367
|
|
|
'found' => true, |
|
3368
|
|
|
]; |
|
3369
|
|
|
} |
|
3370
|
|
|
|
|
3371
|
|
|
$noparse = true; |
|
3372
|
|
|
$preprocessFlags = 0; |
|
3373
|
|
|
if ( isset( $result['noparse'] ) ) { |
|
3374
|
|
|
$noparse = $result['noparse']; |
|
3375
|
|
|
} |
|
3376
|
|
|
if ( isset( $result['preprocessFlags'] ) ) { |
|
3377
|
|
|
$preprocessFlags = $result['preprocessFlags']; |
|
3378
|
|
|
} |
|
3379
|
|
|
|
|
3380
|
|
|
if ( !$noparse ) { |
|
3381
|
|
|
$result['text'] = $this->preprocessToDom( $result['text'], $preprocessFlags ); |
|
3382
|
|
|
$result['isChildObj'] = true; |
|
3383
|
|
|
} |
|
3384
|
|
|
|
|
3385
|
|
|
return $result; |
|
3386
|
|
|
} |
|
3387
|
|
|
|
|
3388
|
|
|
/** |
|
3389
|
|
|
* Get the semi-parsed DOM representation of a template with a given title, |
|
3390
|
|
|
* and its redirect destination title. Cached. |
|
3391
|
|
|
* |
|
3392
|
|
|
* @param Title $title |
|
3393
|
|
|
* |
|
3394
|
|
|
* @return array |
|
3395
|
|
|
*/ |
|
3396
|
|
|
public function getTemplateDom( $title ) { |
|
3397
|
|
|
$cacheTitle = $title; |
|
3398
|
|
|
$titleText = $title->getPrefixedDBkey(); |
|
3399
|
|
|
|
|
3400
|
|
|
if ( isset( $this->mTplRedirCache[$titleText] ) ) { |
|
3401
|
|
|
list( $ns, $dbk ) = $this->mTplRedirCache[$titleText]; |
|
3402
|
|
|
$title = Title::makeTitle( $ns, $dbk ); |
|
3403
|
|
|
$titleText = $title->getPrefixedDBkey(); |
|
3404
|
|
|
} |
|
3405
|
|
|
if ( isset( $this->mTplDomCache[$titleText] ) ) { |
|
3406
|
|
|
return [ $this->mTplDomCache[$titleText], $title ]; |
|
3407
|
|
|
} |
|
3408
|
|
|
|
|
3409
|
|
|
# Cache miss, go to the database |
|
3410
|
|
|
list( $text, $title ) = $this->fetchTemplateAndTitle( $title ); |
|
3411
|
|
|
|
|
3412
|
|
|
if ( $text === false ) { |
|
3413
|
|
|
$this->mTplDomCache[$titleText] = false; |
|
3414
|
|
|
return [ false, $title ]; |
|
3415
|
|
|
} |
|
3416
|
|
|
|
|
3417
|
|
|
$dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION ); |
|
3418
|
|
|
$this->mTplDomCache[$titleText] = $dom; |
|
3419
|
|
|
|
|
3420
|
|
|
if ( !$title->equals( $cacheTitle ) ) { |
|
3421
|
|
|
$this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] = |
|
3422
|
|
|
[ $title->getNamespace(), $cdb = $title->getDBkey() ]; |
|
3423
|
|
|
} |
|
3424
|
|
|
|
|
3425
|
|
|
return [ $dom, $title ]; |
|
3426
|
|
|
} |
|
3427
|
|
|
|
|
3428
|
|
|
/** |
|
3429
|
|
|
* Fetch the current revision of a given title. Note that the revision |
|
3430
|
|
|
* (and even the title) may not exist in the database, so everything |
|
3431
|
|
|
* contributing to the output of the parser should use this method |
|
3432
|
|
|
* where possible, rather than getting the revisions themselves. This |
|
3433
|
|
|
* method also caches its results, so using it benefits performance. |
|
3434
|
|
|
* |
|
3435
|
|
|
* @since 1.24 |
|
3436
|
|
|
* @param Title $title |
|
3437
|
|
|
* @return Revision |
|
3438
|
|
|
*/ |
|
3439
|
|
|
public function fetchCurrentRevisionOfTitle( $title ) { |
|
3440
|
|
|
$cacheKey = $title->getPrefixedDBkey(); |
|
3441
|
|
|
if ( !$this->currentRevisionCache ) { |
|
3442
|
|
|
$this->currentRevisionCache = new MapCacheLRU( 100 ); |
|
3443
|
|
|
} |
|
3444
|
|
|
if ( !$this->currentRevisionCache->has( $cacheKey ) ) { |
|
3445
|
|
|
$this->currentRevisionCache->set( $cacheKey, |
|
3446
|
|
|
// Defaults to Parser::statelessFetchRevision() |
|
3447
|
|
|
call_user_func( $this->mOptions->getCurrentRevisionCallback(), $title, $this ) |
|
3448
|
|
|
); |
|
3449
|
|
|
} |
|
3450
|
|
|
return $this->currentRevisionCache->get( $cacheKey ); |
|
3451
|
|
|
} |
|
3452
|
|
|
|
|
3453
|
|
|
/** |
|
3454
|
|
|
* Wrapper around Revision::newFromTitle to allow passing additional parameters |
|
3455
|
|
|
* without passing them on to it. |
|
3456
|
|
|
* |
|
3457
|
|
|
* @since 1.24 |
|
3458
|
|
|
* @param Title $title |
|
3459
|
|
|
* @param Parser|bool $parser |
|
3460
|
|
|
* @return Revision |
|
3461
|
|
|
*/ |
|
3462
|
|
|
public static function statelessFetchRevision( $title, $parser = false ) { |
|
3463
|
|
|
return Revision::newFromTitle( $title ); |
|
3464
|
|
|
} |
|
3465
|
|
|
|
|
3466
|
|
|
/** |
|
3467
|
|
|
* Fetch the unparsed text of a template and register a reference to it. |
|
3468
|
|
|
* @param Title $title |
|
3469
|
|
|
* @return array ( string or false, Title ) |
|
3470
|
|
|
*/ |
|
3471
|
|
|
public function fetchTemplateAndTitle( $title ) { |
|
3472
|
|
|
// Defaults to Parser::statelessFetchTemplate() |
|
3473
|
|
|
$templateCb = $this->mOptions->getTemplateCallback(); |
|
3474
|
|
|
$stuff = call_user_func( $templateCb, $title, $this ); |
|
3475
|
|
|
// We use U+007F DELETE to distinguish strip markers from regular text. |
|
3476
|
|
|
$text = $stuff['text']; |
|
3477
|
|
|
if ( is_string( $stuff['text'] ) ) { |
|
3478
|
|
|
$text = strtr( $text, "\x7f", "?" ); |
|
3479
|
|
|
} |
|
3480
|
|
|
$finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title; |
|
3481
|
|
|
if ( isset( $stuff['deps'] ) ) { |
|
3482
|
|
|
foreach ( $stuff['deps'] as $dep ) { |
|
3483
|
|
|
$this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] ); |
|
3484
|
|
|
if ( $dep['title']->equals( $this->getTitle() ) ) { |
|
3485
|
|
|
// If we transclude ourselves, the final result |
|
3486
|
|
|
// will change based on the new version of the page |
|
3487
|
|
|
$this->mOutput->setFlag( 'vary-revision' ); |
|
3488
|
|
|
} |
|
3489
|
|
|
} |
|
3490
|
|
|
} |
|
3491
|
|
|
return [ $text, $finalTitle ]; |
|
3492
|
|
|
} |
|
3493
|
|
|
|
|
3494
|
|
|
/** |
|
3495
|
|
|
* Fetch the unparsed text of a template and register a reference to it. |
|
3496
|
|
|
* @param Title $title |
|
3497
|
|
|
* @return string|bool |
|
3498
|
|
|
*/ |
|
3499
|
|
|
public function fetchTemplate( $title ) { |
|
3500
|
|
|
return $this->fetchTemplateAndTitle( $title )[0]; |
|
3501
|
|
|
} |
|
3502
|
|
|
|
|
3503
|
|
|
/** |
|
3504
|
|
|
* Static function to get a template |
|
3505
|
|
|
* Can be overridden via ParserOptions::setTemplateCallback(). |
|
3506
|
|
|
* |
|
3507
|
|
|
* @param Title $title |
|
3508
|
|
|
* @param bool|Parser $parser |
|
3509
|
|
|
* |
|
3510
|
|
|
* @return array |
|
3511
|
|
|
*/ |
|
3512
|
|
|
public static function statelessFetchTemplate( $title, $parser = false ) { |
|
3513
|
|
|
$text = $skip = false; |
|
3514
|
|
|
$finalTitle = $title; |
|
3515
|
|
|
$deps = []; |
|
3516
|
|
|
|
|
3517
|
|
|
# Loop to fetch the article, with up to 1 redirect |
|
3518
|
|
|
// @codingStandardsIgnoreStart Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed |
|
3519
|
|
|
for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) { |
|
3520
|
|
|
// @codingStandardsIgnoreEnd |
|
3521
|
|
|
# Give extensions a chance to select the revision instead |
|
3522
|
|
|
$id = false; # Assume current |
|
3523
|
|
|
Hooks::run( 'BeforeParserFetchTemplateAndtitle', |
|
3524
|
|
|
[ $parser, $title, &$skip, &$id ] ); |
|
3525
|
|
|
|
|
3526
|
|
|
if ( $skip ) { |
|
3527
|
|
|
$text = false; |
|
3528
|
|
|
$deps[] = [ |
|
3529
|
|
|
'title' => $title, |
|
3530
|
|
|
'page_id' => $title->getArticleID(), |
|
3531
|
|
|
'rev_id' => null |
|
3532
|
|
|
]; |
|
3533
|
|
|
break; |
|
3534
|
|
|
} |
|
3535
|
|
|
# Get the revision |
|
3536
|
|
|
if ( $id ) { |
|
3537
|
|
|
$rev = Revision::newFromId( $id ); |
|
3538
|
|
|
} elseif ( $parser ) { |
|
3539
|
|
|
$rev = $parser->fetchCurrentRevisionOfTitle( $title ); |
|
|
|
|
|
|
3540
|
|
|
} else { |
|
3541
|
|
|
$rev = Revision::newFromTitle( $title ); |
|
3542
|
|
|
} |
|
3543
|
|
|
$rev_id = $rev ? $rev->getId() : 0; |
|
3544
|
|
|
# If there is no current revision, there is no page |
|
3545
|
|
|
if ( $id === false && !$rev ) { |
|
3546
|
|
|
$linkCache = LinkCache::singleton(); |
|
|
|
|
|
|
3547
|
|
|
$linkCache->addBadLinkObj( $title ); |
|
3548
|
|
|
} |
|
3549
|
|
|
|
|
3550
|
|
|
$deps[] = [ |
|
3551
|
|
|
'title' => $title, |
|
3552
|
|
|
'page_id' => $title->getArticleID(), |
|
3553
|
|
|
'rev_id' => $rev_id ]; |
|
3554
|
|
|
if ( $rev && !$title->equals( $rev->getTitle() ) ) { |
|
3555
|
|
|
# We fetched a rev from a different title; register it too... |
|
3556
|
|
|
$deps[] = [ |
|
3557
|
|
|
'title' => $rev->getTitle(), |
|
3558
|
|
|
'page_id' => $rev->getPage(), |
|
3559
|
|
|
'rev_id' => $rev_id ]; |
|
3560
|
|
|
} |
|
3561
|
|
|
|
|
3562
|
|
|
if ( $rev ) { |
|
3563
|
|
|
$content = $rev->getContent(); |
|
3564
|
|
|
$text = $content ? $content->getWikitextForTransclusion() : null; |
|
3565
|
|
|
|
|
3566
|
|
|
if ( $text === false || $text === null ) { |
|
3567
|
|
|
$text = false; |
|
3568
|
|
|
break; |
|
3569
|
|
|
} |
|
3570
|
|
|
} elseif ( $title->getNamespace() == NS_MEDIAWIKI ) { |
|
3571
|
|
|
global $wgContLang; |
|
3572
|
|
|
$message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage(); |
|
3573
|
|
|
if ( !$message->exists() ) { |
|
3574
|
|
|
$text = false; |
|
3575
|
|
|
break; |
|
3576
|
|
|
} |
|
3577
|
|
|
$content = $message->content(); |
|
3578
|
|
|
$text = $message->plain(); |
|
3579
|
|
|
} else { |
|
3580
|
|
|
break; |
|
3581
|
|
|
} |
|
3582
|
|
|
if ( !$content ) { |
|
3583
|
|
|
break; |
|
3584
|
|
|
} |
|
3585
|
|
|
# Redirect? |
|
3586
|
|
|
$finalTitle = $title; |
|
3587
|
|
|
$title = $content->getRedirectTarget(); |
|
3588
|
|
|
} |
|
3589
|
|
|
return [ |
|
3590
|
|
|
'text' => $text, |
|
3591
|
|
|
'finalTitle' => $finalTitle, |
|
3592
|
|
|
'deps' => $deps ]; |
|
3593
|
|
|
} |
|
3594
|
|
|
|
|
3595
|
|
|
/** |
|
3596
|
|
|
* Fetch a file and its title and register a reference to it. |
|
3597
|
|
|
* If 'broken' is a key in $options then the file will appear as a broken thumbnail. |
|
3598
|
|
|
* @param Title $title |
|
3599
|
|
|
* @param array $options Array of options to RepoGroup::findFile |
|
3600
|
|
|
* @return File|bool |
|
3601
|
|
|
*/ |
|
3602
|
|
|
public function fetchFile( $title, $options = [] ) { |
|
3603
|
|
|
return $this->fetchFileAndTitle( $title, $options )[0]; |
|
3604
|
|
|
} |
|
3605
|
|
|
|
|
3606
|
|
|
/** |
|
3607
|
|
|
* Fetch a file and its title and register a reference to it. |
|
3608
|
|
|
* If 'broken' is a key in $options then the file will appear as a broken thumbnail. |
|
3609
|
|
|
* @param Title $title |
|
3610
|
|
|
* @param array $options Array of options to RepoGroup::findFile |
|
3611
|
|
|
* @return array ( File or false, Title of file ) |
|
3612
|
|
|
*/ |
|
3613
|
|
|
public function fetchFileAndTitle( $title, $options = [] ) { |
|
3614
|
|
|
$file = $this->fetchFileNoRegister( $title, $options ); |
|
3615
|
|
|
|
|
3616
|
|
|
$time = $file ? $file->getTimestamp() : false; |
|
3617
|
|
|
$sha1 = $file ? $file->getSha1() : false; |
|
3618
|
|
|
# Register the file as a dependency... |
|
3619
|
|
|
$this->mOutput->addImage( $title->getDBkey(), $time, $sha1 ); |
|
3620
|
|
|
if ( $file && !$title->equals( $file->getTitle() ) ) { |
|
3621
|
|
|
# Update fetched file title |
|
3622
|
|
|
$title = $file->getTitle(); |
|
3623
|
|
|
$this->mOutput->addImage( $title->getDBkey(), $time, $sha1 ); |
|
3624
|
|
|
} |
|
3625
|
|
|
return [ $file, $title ]; |
|
3626
|
|
|
} |
|
3627
|
|
|
|
|
3628
|
|
|
/** |
|
3629
|
|
|
* Helper function for fetchFileAndTitle. |
|
3630
|
|
|
* |
|
3631
|
|
|
* Also useful if you need to fetch a file but not use it yet, |
|
3632
|
|
|
* for example to get the file's handler. |
|
3633
|
|
|
* |
|
3634
|
|
|
* @param Title $title |
|
3635
|
|
|
* @param array $options Array of options to RepoGroup::findFile |
|
3636
|
|
|
* @return File|bool |
|
3637
|
|
|
*/ |
|
3638
|
|
|
protected function fetchFileNoRegister( $title, $options = [] ) { |
|
3639
|
|
|
if ( isset( $options['broken'] ) ) { |
|
3640
|
|
|
$file = false; // broken thumbnail forced by hook |
|
3641
|
|
|
} elseif ( isset( $options['sha1'] ) ) { // get by (sha1,timestamp) |
|
3642
|
|
|
$file = RepoGroup::singleton()->findFileFromKey( $options['sha1'], $options ); |
|
3643
|
|
|
} else { // get by (name,timestamp) |
|
3644
|
|
|
$file = wfFindFile( $title, $options ); |
|
3645
|
|
|
} |
|
3646
|
|
|
return $file; |
|
3647
|
|
|
} |
|
3648
|
|
|
|
|
3649
|
|
|
/** |
|
3650
|
|
|
* Transclude an interwiki link. |
|
3651
|
|
|
* |
|
3652
|
|
|
* @param Title $title |
|
3653
|
|
|
* @param string $action |
|
3654
|
|
|
* |
|
3655
|
|
|
* @return string |
|
3656
|
|
|
*/ |
|
3657
|
|
|
public function interwikiTransclude( $title, $action ) { |
|
3658
|
|
|
global $wgEnableScaryTranscluding; |
|
3659
|
|
|
|
|
3660
|
|
|
if ( !$wgEnableScaryTranscluding ) { |
|
3661
|
|
|
return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text(); |
|
3662
|
|
|
} |
|
3663
|
|
|
|
|
3664
|
|
|
$url = $title->getFullURL( [ 'action' => $action ] ); |
|
3665
|
|
|
|
|
3666
|
|
|
if ( strlen( $url ) > 255 ) { |
|
3667
|
|
|
return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text(); |
|
3668
|
|
|
} |
|
3669
|
|
|
return $this->fetchScaryTemplateMaybeFromCache( $url ); |
|
3670
|
|
|
} |
|
3671
|
|
|
|
|
3672
|
|
|
/** |
|
3673
|
|
|
* @param string $url |
|
3674
|
|
|
* @return mixed|string |
|
3675
|
|
|
*/ |
|
3676
|
|
|
public function fetchScaryTemplateMaybeFromCache( $url ) { |
|
3677
|
|
|
global $wgTranscludeCacheExpiry; |
|
3678
|
|
|
$dbr = wfGetDB( DB_SLAVE ); |
|
3679
|
|
|
$tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry ); |
|
3680
|
|
|
$obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ], |
|
3681
|
|
|
[ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] ); |
|
|
|
|
|
|
3682
|
|
|
if ( $obj ) { |
|
3683
|
|
|
return $obj->tc_contents; |
|
3684
|
|
|
} |
|
3685
|
|
|
|
|
3686
|
|
|
$req = MWHttpRequest::factory( $url, [], __METHOD__ ); |
|
3687
|
|
|
$status = $req->execute(); // Status object |
|
3688
|
|
|
if ( $status->isOK() ) { |
|
3689
|
|
|
$text = $req->getContent(); |
|
3690
|
|
|
} elseif ( $req->getStatus() != 200 ) { |
|
3691
|
|
|
// Though we failed to fetch the content, this status is useless. |
|
3692
|
|
|
return wfMessage( 'scarytranscludefailed-httpstatus' ) |
|
3693
|
|
|
->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text(); |
|
3694
|
|
|
} else { |
|
3695
|
|
|
return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text(); |
|
3696
|
|
|
} |
|
3697
|
|
|
|
|
3698
|
|
|
$dbw = wfGetDB( DB_MASTER ); |
|
3699
|
|
|
$dbw->replace( 'transcache', [ 'tc_url' ], [ |
|
3700
|
|
|
'tc_url' => $url, |
|
3701
|
|
|
'tc_time' => $dbw->timestamp( time() ), |
|
3702
|
|
|
'tc_contents' => $text |
|
3703
|
|
|
] ); |
|
3704
|
|
|
return $text; |
|
3705
|
|
|
} |
|
3706
|
|
|
|
|
3707
|
|
|
/** |
|
3708
|
|
|
* Triple brace replacement -- used for template arguments |
|
3709
|
|
|
* @private |
|
3710
|
|
|
* |
|
3711
|
|
|
* @param array $piece |
|
3712
|
|
|
* @param PPFrame $frame |
|
3713
|
|
|
* |
|
3714
|
|
|
* @return array |
|
3715
|
|
|
*/ |
|
3716
|
|
|
public function argSubstitution( $piece, $frame ) { |
|
3717
|
|
|
|
|
3718
|
|
|
$error = false; |
|
3719
|
|
|
$parts = $piece['parts']; |
|
3720
|
|
|
$nameWithSpaces = $frame->expand( $piece['title'] ); |
|
3721
|
|
|
$argName = trim( $nameWithSpaces ); |
|
3722
|
|
|
$object = false; |
|
3723
|
|
|
$text = $frame->getArgument( $argName ); |
|
3724
|
|
|
if ( $text === false && $parts->getLength() > 0 |
|
3725
|
|
|
&& ( $this->ot['html'] |
|
3726
|
|
|
|| $this->ot['pre'] |
|
3727
|
|
|
|| ( $this->ot['wiki'] && $frame->isTemplate() ) |
|
3728
|
|
|
) |
|
3729
|
|
|
) { |
|
3730
|
|
|
# No match in frame, use the supplied default |
|
3731
|
|
|
$object = $parts->item( 0 )->getChildren(); |
|
3732
|
|
|
} |
|
3733
|
|
|
if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) { |
|
3734
|
|
|
$error = '<!-- WARNING: argument omitted, expansion size too large -->'; |
|
3735
|
|
|
$this->limitationWarn( 'post-expand-template-argument' ); |
|
3736
|
|
|
} |
|
3737
|
|
|
|
|
3738
|
|
|
if ( $text === false && $object === false ) { |
|
3739
|
|
|
# No match anywhere |
|
3740
|
|
|
$object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts ); |
|
3741
|
|
|
} |
|
3742
|
|
|
if ( $error !== false ) { |
|
3743
|
|
|
$text .= $error; |
|
3744
|
|
|
} |
|
3745
|
|
|
if ( $object !== false ) { |
|
3746
|
|
|
$ret = [ 'object' => $object ]; |
|
3747
|
|
|
} else { |
|
3748
|
|
|
$ret = [ 'text' => $text ]; |
|
3749
|
|
|
} |
|
3750
|
|
|
|
|
3751
|
|
|
return $ret; |
|
3752
|
|
|
} |
|
3753
|
|
|
|
|
3754
|
|
|
/** |
|
3755
|
|
|
* Return the text to be used for a given extension tag. |
|
3756
|
|
|
* This is the ghost of strip(). |
|
3757
|
|
|
* |
|
3758
|
|
|
* @param array $params Associative array of parameters: |
|
3759
|
|
|
* name PPNode for the tag name |
|
3760
|
|
|
* attr PPNode for unparsed text where tag attributes are thought to be |
|
3761
|
|
|
* attributes Optional associative array of parsed attributes |
|
3762
|
|
|
* inner Contents of extension element |
|
3763
|
|
|
* noClose Original text did not have a close tag |
|
3764
|
|
|
* @param PPFrame $frame |
|
3765
|
|
|
* |
|
3766
|
|
|
* @throws MWException |
|
3767
|
|
|
* @return string |
|
3768
|
|
|
*/ |
|
3769
|
|
|
public function extensionSubstitution( $params, $frame ) { |
|
3770
|
|
|
$name = $frame->expand( $params['name'] ); |
|
3771
|
|
|
$attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] ); |
|
3772
|
|
|
$content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] ); |
|
3773
|
|
|
$marker = self::MARKER_PREFIX . "-$name-" |
|
3774
|
|
|
. sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX; |
|
3775
|
|
|
|
|
3776
|
|
|
$isFunctionTag = isset( $this->mFunctionTagHooks[strtolower( $name )] ) && |
|
3777
|
|
|
( $this->ot['html'] || $this->ot['pre'] ); |
|
3778
|
|
|
if ( $isFunctionTag ) { |
|
3779
|
|
|
$markerType = 'none'; |
|
3780
|
|
|
} else { |
|
3781
|
|
|
$markerType = 'general'; |
|
3782
|
|
|
} |
|
3783
|
|
|
if ( $this->ot['html'] || $isFunctionTag ) { |
|
3784
|
|
|
$name = strtolower( $name ); |
|
3785
|
|
|
$attributes = Sanitizer::decodeTagAttributes( $attrText ); |
|
3786
|
|
|
if ( isset( $params['attributes'] ) ) { |
|
3787
|
|
|
$attributes = $attributes + $params['attributes']; |
|
3788
|
|
|
} |
|
3789
|
|
|
|
|
3790
|
|
|
if ( isset( $this->mTagHooks[$name] ) ) { |
|
3791
|
|
|
# Workaround for PHP bug 35229 and similar |
|
3792
|
|
|
if ( !is_callable( $this->mTagHooks[$name] ) ) { |
|
3793
|
|
|
throw new MWException( "Tag hook for $name is not callable\n" ); |
|
3794
|
|
|
} |
|
3795
|
|
|
$output = call_user_func_array( $this->mTagHooks[$name], |
|
3796
|
|
|
[ $content, $attributes, $this, $frame ] ); |
|
3797
|
|
|
} elseif ( isset( $this->mFunctionTagHooks[$name] ) ) { |
|
3798
|
|
|
list( $callback, ) = $this->mFunctionTagHooks[$name]; |
|
3799
|
|
|
if ( !is_callable( $callback ) ) { |
|
3800
|
|
|
throw new MWException( "Tag hook for $name is not callable\n" ); |
|
3801
|
|
|
} |
|
3802
|
|
|
|
|
3803
|
|
|
$output = call_user_func_array( $callback, [ &$this, $frame, $content, $attributes ] ); |
|
3804
|
|
|
} else { |
|
3805
|
|
|
$output = '<span class="error">Invalid tag extension name: ' . |
|
3806
|
|
|
htmlspecialchars( $name ) . '</span>'; |
|
3807
|
|
|
} |
|
3808
|
|
|
|
|
3809
|
|
|
if ( is_array( $output ) ) { |
|
3810
|
|
|
# Extract flags to local scope (to override $markerType) |
|
3811
|
|
|
$flags = $output; |
|
3812
|
|
|
$output = $flags[0]; |
|
3813
|
|
|
unset( $flags[0] ); |
|
3814
|
|
|
extract( $flags ); |
|
3815
|
|
|
} |
|
3816
|
|
|
} else { |
|
3817
|
|
|
if ( is_null( $attrText ) ) { |
|
3818
|
|
|
$attrText = ''; |
|
3819
|
|
|
} |
|
3820
|
|
|
if ( isset( $params['attributes'] ) ) { |
|
3821
|
|
|
foreach ( $params['attributes'] as $attrName => $attrValue ) { |
|
3822
|
|
|
$attrText .= ' ' . htmlspecialchars( $attrName ) . '="' . |
|
3823
|
|
|
htmlspecialchars( $attrValue ) . '"'; |
|
3824
|
|
|
} |
|
3825
|
|
|
} |
|
3826
|
|
|
if ( $content === null ) { |
|
3827
|
|
|
$output = "<$name$attrText/>"; |
|
3828
|
|
|
} else { |
|
3829
|
|
|
$close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] ); |
|
3830
|
|
|
$output = "<$name$attrText>$content$close"; |
|
3831
|
|
|
} |
|
3832
|
|
|
} |
|
3833
|
|
|
|
|
3834
|
|
|
if ( $markerType === 'none' ) { |
|
3835
|
|
|
return $output; |
|
3836
|
|
|
} elseif ( $markerType === 'nowiki' ) { |
|
3837
|
|
|
$this->mStripState->addNoWiki( $marker, $output ); |
|
3838
|
|
|
} elseif ( $markerType === 'general' ) { |
|
3839
|
|
|
$this->mStripState->addGeneral( $marker, $output ); |
|
3840
|
|
|
} else { |
|
3841
|
|
|
throw new MWException( __METHOD__ . ': invalid marker type' ); |
|
3842
|
|
|
} |
|
3843
|
|
|
return $marker; |
|
3844
|
|
|
} |
|
3845
|
|
|
|
|
3846
|
|
|
/** |
|
3847
|
|
|
* Increment an include size counter |
|
3848
|
|
|
* |
|
3849
|
|
|
* @param string $type The type of expansion |
|
3850
|
|
|
* @param int $size The size of the text |
|
3851
|
|
|
* @return bool False if this inclusion would take it over the maximum, true otherwise |
|
3852
|
|
|
*/ |
|
3853
|
|
|
public function incrementIncludeSize( $type, $size ) { |
|
3854
|
|
|
if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) { |
|
3855
|
|
|
return false; |
|
3856
|
|
|
} else { |
|
3857
|
|
|
$this->mIncludeSizes[$type] += $size; |
|
3858
|
|
|
return true; |
|
3859
|
|
|
} |
|
3860
|
|
|
} |
|
3861
|
|
|
|
|
3862
|
|
|
/** |
|
3863
|
|
|
* Increment the expensive function count |
|
3864
|
|
|
* |
|
3865
|
|
|
* @return bool False if the limit has been exceeded |
|
3866
|
|
|
*/ |
|
3867
|
|
|
public function incrementExpensiveFunctionCount() { |
|
3868
|
|
|
$this->mExpensiveFunctionCount++; |
|
3869
|
|
|
return $this->mExpensiveFunctionCount <= $this->mOptions->getExpensiveParserFunctionLimit(); |
|
3870
|
|
|
} |
|
3871
|
|
|
|
|
3872
|
|
|
/** |
|
3873
|
|
|
* Strip double-underscore items like __NOGALLERY__ and __NOTOC__ |
|
3874
|
|
|
* Fills $this->mDoubleUnderscores, returns the modified text |
|
3875
|
|
|
* |
|
3876
|
|
|
* @param string $text |
|
3877
|
|
|
* |
|
3878
|
|
|
* @return string |
|
3879
|
|
|
*/ |
|
3880
|
|
|
public function doDoubleUnderscore( $text ) { |
|
3881
|
|
|
|
|
3882
|
|
|
# The position of __TOC__ needs to be recorded |
|
3883
|
|
|
$mw = MagicWord::get( 'toc' ); |
|
3884
|
|
|
if ( $mw->match( $text ) ) { |
|
3885
|
|
|
$this->mShowToc = true; |
|
3886
|
|
|
$this->mForceTocPosition = true; |
|
3887
|
|
|
|
|
3888
|
|
|
# Set a placeholder. At the end we'll fill it in with the TOC. |
|
3889
|
|
|
$text = $mw->replace( '<!--MWTOC-->', $text, 1 ); |
|
3890
|
|
|
|
|
3891
|
|
|
# Only keep the first one. |
|
3892
|
|
|
$text = $mw->replace( '', $text ); |
|
3893
|
|
|
} |
|
3894
|
|
|
|
|
3895
|
|
|
# Now match and remove the rest of them |
|
3896
|
|
|
$mwa = MagicWord::getDoubleUnderscoreArray(); |
|
3897
|
|
|
$this->mDoubleUnderscores = $mwa->matchAndRemove( $text ); |
|
3898
|
|
|
|
|
3899
|
|
|
if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) { |
|
3900
|
|
|
$this->mOutput->mNoGallery = true; |
|
3901
|
|
|
} |
|
3902
|
|
|
if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) { |
|
3903
|
|
|
$this->mShowToc = false; |
|
3904
|
|
|
} |
|
3905
|
|
|
if ( isset( $this->mDoubleUnderscores['hiddencat'] ) |
|
3906
|
|
|
&& $this->mTitle->getNamespace() == NS_CATEGORY |
|
3907
|
|
|
) { |
|
3908
|
|
|
$this->addTrackingCategory( 'hidden-category-category' ); |
|
3909
|
|
|
} |
|
3910
|
|
|
# (bug 8068) Allow control over whether robots index a page. |
|
3911
|
|
|
# @todo FIXME: Bug 14899: __INDEX__ always overrides __NOINDEX__ here! This |
|
3912
|
|
|
# is not desirable, the last one on the page should win. |
|
3913
|
|
View Code Duplication |
if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) { |
|
3914
|
|
|
$this->mOutput->setIndexPolicy( 'noindex' ); |
|
3915
|
|
|
$this->addTrackingCategory( 'noindex-category' ); |
|
3916
|
|
|
} |
|
3917
|
|
View Code Duplication |
if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) { |
|
3918
|
|
|
$this->mOutput->setIndexPolicy( 'index' ); |
|
3919
|
|
|
$this->addTrackingCategory( 'index-category' ); |
|
3920
|
|
|
} |
|
3921
|
|
|
|
|
3922
|
|
|
# Cache all double underscores in the database |
|
3923
|
|
|
foreach ( $this->mDoubleUnderscores as $key => $val ) { |
|
3924
|
|
|
$this->mOutput->setProperty( $key, '' ); |
|
3925
|
|
|
} |
|
3926
|
|
|
|
|
3927
|
|
|
return $text; |
|
3928
|
|
|
} |
|
3929
|
|
|
|
|
3930
|
|
|
/** |
|
3931
|
|
|
* @see ParserOutput::addTrackingCategory() |
|
3932
|
|
|
* @param string $msg Message key |
|
3933
|
|
|
* @return bool Whether the addition was successful |
|
3934
|
|
|
*/ |
|
3935
|
|
|
public function addTrackingCategory( $msg ) { |
|
3936
|
|
|
return $this->mOutput->addTrackingCategory( $msg, $this->mTitle ); |
|
3937
|
|
|
} |
|
3938
|
|
|
|
|
3939
|
|
|
/** |
|
3940
|
|
|
* This function accomplishes several tasks: |
|
3941
|
|
|
* 1) Auto-number headings if that option is enabled |
|
3942
|
|
|
* 2) Add an [edit] link to sections for users who have enabled the option and can edit the page |
|
3943
|
|
|
* 3) Add a Table of contents on the top for users who have enabled the option |
|
3944
|
|
|
* 4) Auto-anchor headings |
|
3945
|
|
|
* |
|
3946
|
|
|
* It loops through all headlines, collects the necessary data, then splits up the |
|
3947
|
|
|
* string and re-inserts the newly formatted headlines. |
|
3948
|
|
|
* |
|
3949
|
|
|
* @param string $text |
|
3950
|
|
|
* @param string $origText Original, untouched wikitext |
|
3951
|
|
|
* @param bool $isMain |
|
3952
|
|
|
* @return mixed|string |
|
3953
|
|
|
* @private |
|
3954
|
|
|
*/ |
|
3955
|
|
|
public function formatHeadings( $text, $origText, $isMain = true ) { |
|
3956
|
|
|
global $wgMaxTocLevel, $wgExperimentalHtmlIds; |
|
3957
|
|
|
|
|
3958
|
|
|
# Inhibit editsection links if requested in the page |
|
3959
|
|
|
if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) { |
|
3960
|
|
|
$maybeShowEditLink = $showEditLink = false; |
|
3961
|
|
|
} else { |
|
3962
|
|
|
$maybeShowEditLink = true; /* Actual presence will depend on ParserOptions option */ |
|
3963
|
|
|
$showEditLink = $this->mOptions->getEditSection(); |
|
3964
|
|
|
} |
|
3965
|
|
|
if ( $showEditLink ) { |
|
3966
|
|
|
$this->mOutput->setEditSectionTokens( true ); |
|
3967
|
|
|
} |
|
3968
|
|
|
|
|
3969
|
|
|
# Get all headlines for numbering them and adding funky stuff like [edit] |
|
3970
|
|
|
# links - this is for later, but we need the number of headlines right now |
|
3971
|
|
|
$matches = []; |
|
3972
|
|
|
$numMatches = preg_match_all( |
|
3973
|
|
|
'/<H(?P<level>[1-6])(?P<attrib>.*?>)\s*(?P<header>[\s\S]*?)\s*<\/H[1-6] *>/i', |
|
3974
|
|
|
$text, |
|
3975
|
|
|
$matches |
|
3976
|
|
|
); |
|
3977
|
|
|
|
|
3978
|
|
|
# if there are fewer than 4 headlines in the article, do not show TOC |
|
3979
|
|
|
# unless it's been explicitly enabled. |
|
3980
|
|
|
$enoughToc = $this->mShowToc && |
|
3981
|
|
|
( ( $numMatches >= 4 ) || $this->mForceTocPosition ); |
|
3982
|
|
|
|
|
3983
|
|
|
# Allow user to stipulate that a page should have a "new section" |
|
3984
|
|
|
# link added via __NEWSECTIONLINK__ |
|
3985
|
|
|
if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) { |
|
3986
|
|
|
$this->mOutput->setNewSection( true ); |
|
3987
|
|
|
} |
|
3988
|
|
|
|
|
3989
|
|
|
# Allow user to remove the "new section" |
|
3990
|
|
|
# link via __NONEWSECTIONLINK__ |
|
3991
|
|
|
if ( isset( $this->mDoubleUnderscores['nonewsectionlink'] ) ) { |
|
3992
|
|
|
$this->mOutput->hideNewSection( true ); |
|
3993
|
|
|
} |
|
3994
|
|
|
|
|
3995
|
|
|
# if the string __FORCETOC__ (not case-sensitive) occurs in the HTML, |
|
3996
|
|
|
# override above conditions and always show TOC above first header |
|
3997
|
|
|
if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) { |
|
3998
|
|
|
$this->mShowToc = true; |
|
3999
|
|
|
$enoughToc = true; |
|
4000
|
|
|
} |
|
4001
|
|
|
|
|
4002
|
|
|
# headline counter |
|
4003
|
|
|
$headlineCount = 0; |
|
4004
|
|
|
$numVisible = 0; |
|
4005
|
|
|
|
|
4006
|
|
|
# Ugh .. the TOC should have neat indentation levels which can be |
|
4007
|
|
|
# passed to the skin functions. These are determined here |
|
4008
|
|
|
$toc = ''; |
|
4009
|
|
|
$full = ''; |
|
4010
|
|
|
$head = []; |
|
4011
|
|
|
$sublevelCount = []; |
|
4012
|
|
|
$levelCount = []; |
|
4013
|
|
|
$level = 0; |
|
4014
|
|
|
$prevlevel = 0; |
|
4015
|
|
|
$toclevel = 0; |
|
4016
|
|
|
$prevtoclevel = 0; |
|
4017
|
|
|
$markerRegex = self::MARKER_PREFIX . "-h-(\d+)-" . self::MARKER_SUFFIX; |
|
4018
|
|
|
$baseTitleText = $this->mTitle->getPrefixedDBkey(); |
|
4019
|
|
|
$oldType = $this->mOutputType; |
|
4020
|
|
|
$this->setOutputType( self::OT_WIKI ); |
|
4021
|
|
|
$frame = $this->getPreprocessor()->newFrame(); |
|
4022
|
|
|
$root = $this->preprocessToDom( $origText ); |
|
4023
|
|
|
$node = $root->getFirstChild(); |
|
4024
|
|
|
$byteOffset = 0; |
|
4025
|
|
|
$tocraw = []; |
|
4026
|
|
|
$refers = []; |
|
4027
|
|
|
|
|
4028
|
|
|
$headlines = $numMatches !== false ? $matches[3] : []; |
|
4029
|
|
|
|
|
4030
|
|
|
foreach ( $headlines as $headline ) { |
|
4031
|
|
|
$isTemplate = false; |
|
4032
|
|
|
$titleText = false; |
|
4033
|
|
|
$sectionIndex = false; |
|
4034
|
|
|
$numbering = ''; |
|
4035
|
|
|
$markerMatches = []; |
|
4036
|
|
|
if ( preg_match( "/^$markerRegex/", $headline, $markerMatches ) ) { |
|
4037
|
|
|
$serial = $markerMatches[1]; |
|
4038
|
|
|
list( $titleText, $sectionIndex ) = $this->mHeadings[$serial]; |
|
4039
|
|
|
$isTemplate = ( $titleText != $baseTitleText ); |
|
4040
|
|
|
$headline = preg_replace( "/^$markerRegex\\s*/", "", $headline ); |
|
4041
|
|
|
} |
|
4042
|
|
|
|
|
4043
|
|
|
if ( $toclevel ) { |
|
4044
|
|
|
$prevlevel = $level; |
|
4045
|
|
|
} |
|
4046
|
|
|
$level = $matches[1][$headlineCount]; |
|
4047
|
|
|
|
|
4048
|
|
|
if ( $level > $prevlevel ) { |
|
4049
|
|
|
# Increase TOC level |
|
4050
|
|
|
$toclevel++; |
|
4051
|
|
|
$sublevelCount[$toclevel] = 0; |
|
4052
|
|
|
if ( $toclevel < $wgMaxTocLevel ) { |
|
4053
|
|
|
$prevtoclevel = $toclevel; |
|
4054
|
|
|
$toc .= Linker::tocIndent(); |
|
4055
|
|
|
$numVisible++; |
|
4056
|
|
|
} |
|
4057
|
|
|
} elseif ( $level < $prevlevel && $toclevel > 1 ) { |
|
4058
|
|
|
# Decrease TOC level, find level to jump to |
|
4059
|
|
|
|
|
4060
|
|
|
for ( $i = $toclevel; $i > 0; $i-- ) { |
|
4061
|
|
|
if ( $levelCount[$i] == $level ) { |
|
4062
|
|
|
# Found last matching level |
|
4063
|
|
|
$toclevel = $i; |
|
4064
|
|
|
break; |
|
4065
|
|
|
} elseif ( $levelCount[$i] < $level ) { |
|
4066
|
|
|
# Found first matching level below current level |
|
4067
|
|
|
$toclevel = $i + 1; |
|
4068
|
|
|
break; |
|
4069
|
|
|
} |
|
4070
|
|
|
} |
|
4071
|
|
|
if ( $i == 0 ) { |
|
4072
|
|
|
$toclevel = 1; |
|
4073
|
|
|
} |
|
4074
|
|
|
if ( $toclevel < $wgMaxTocLevel ) { |
|
4075
|
|
|
if ( $prevtoclevel < $wgMaxTocLevel ) { |
|
4076
|
|
|
# Unindent only if the previous toc level was shown :p |
|
4077
|
|
|
$toc .= Linker::tocUnindent( $prevtoclevel - $toclevel ); |
|
4078
|
|
|
$prevtoclevel = $toclevel; |
|
4079
|
|
|
} else { |
|
4080
|
|
|
$toc .= Linker::tocLineEnd(); |
|
4081
|
|
|
} |
|
4082
|
|
|
} |
|
4083
|
|
|
} else { |
|
4084
|
|
|
# No change in level, end TOC line |
|
4085
|
|
|
if ( $toclevel < $wgMaxTocLevel ) { |
|
4086
|
|
|
$toc .= Linker::tocLineEnd(); |
|
4087
|
|
|
} |
|
4088
|
|
|
} |
|
4089
|
|
|
|
|
4090
|
|
|
$levelCount[$toclevel] = $level; |
|
4091
|
|
|
|
|
4092
|
|
|
# count number of headlines for each level |
|
4093
|
|
|
$sublevelCount[$toclevel]++; |
|
4094
|
|
|
$dot = 0; |
|
4095
|
|
|
for ( $i = 1; $i <= $toclevel; $i++ ) { |
|
4096
|
|
|
if ( !empty( $sublevelCount[$i] ) ) { |
|
4097
|
|
|
if ( $dot ) { |
|
4098
|
|
|
$numbering .= '.'; |
|
4099
|
|
|
} |
|
4100
|
|
|
$numbering .= $this->getTargetLanguage()->formatNum( $sublevelCount[$i] ); |
|
4101
|
|
|
$dot = 1; |
|
4102
|
|
|
} |
|
4103
|
|
|
} |
|
4104
|
|
|
|
|
4105
|
|
|
# The safe header is a version of the header text safe to use for links |
|
4106
|
|
|
|
|
4107
|
|
|
# Remove link placeholders by the link text. |
|
4108
|
|
|
# <!--LINK number--> |
|
4109
|
|
|
# turns into |
|
4110
|
|
|
# link text with suffix |
|
4111
|
|
|
# Do this before unstrip since link text can contain strip markers |
|
4112
|
|
|
$safeHeadline = $this->replaceLinkHoldersText( $headline ); |
|
4113
|
|
|
|
|
4114
|
|
|
# Avoid insertion of weird stuff like <math> by expanding the relevant sections |
|
4115
|
|
|
$safeHeadline = $this->mStripState->unstripBoth( $safeHeadline ); |
|
4116
|
|
|
|
|
4117
|
|
|
# Strip out HTML (first regex removes any tag not allowed) |
|
4118
|
|
|
# Allowed tags are: |
|
4119
|
|
|
# * <sup> and <sub> (bug 8393) |
|
4120
|
|
|
# * <i> (bug 26375) |
|
4121
|
|
|
# * <b> (r105284) |
|
4122
|
|
|
# * <bdi> (bug 72884) |
|
4123
|
|
|
# * <span dir="rtl"> and <span dir="ltr"> (bug 35167) |
|
4124
|
|
|
# We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>, |
|
4125
|
|
|
# to allow setting directionality in toc items. |
|
4126
|
|
|
$tocline = preg_replace( |
|
4127
|
|
|
[ |
|
4128
|
|
|
'#<(?!/?(span|sup|sub|bdi|i|b)(?: [^>]*)?>).*?>#', |
|
4129
|
|
|
'#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|bdi|i|b))(?: .*?)?>#' |
|
4130
|
|
|
], |
|
4131
|
|
|
[ '', '<$1>' ], |
|
4132
|
|
|
$safeHeadline |
|
4133
|
|
|
); |
|
4134
|
|
|
|
|
4135
|
|
|
# Strip '<span></span>', which is the result from the above if |
|
4136
|
|
|
# <span id="foo"></span> is used to produce an additional anchor |
|
4137
|
|
|
# for a section. |
|
4138
|
|
|
$tocline = str_replace( '<span></span>', '', $tocline ); |
|
4139
|
|
|
|
|
4140
|
|
|
$tocline = trim( $tocline ); |
|
4141
|
|
|
|
|
4142
|
|
|
# For the anchor, strip out HTML-y stuff period |
|
4143
|
|
|
$safeHeadline = preg_replace( '/<.*?>/', '', $safeHeadline ); |
|
4144
|
|
|
$safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline ); |
|
4145
|
|
|
|
|
4146
|
|
|
# Save headline for section edit hint before it's escaped |
|
4147
|
|
|
$headlineHint = $safeHeadline; |
|
4148
|
|
|
|
|
4149
|
|
|
if ( $wgExperimentalHtmlIds ) { |
|
4150
|
|
|
# For reverse compatibility, provide an id that's |
|
4151
|
|
|
# HTML4-compatible, like we used to. |
|
4152
|
|
|
# It may be worth noting, academically, that it's possible for |
|
4153
|
|
|
# the legacy anchor to conflict with a non-legacy headline |
|
4154
|
|
|
# anchor on the page. In this case likely the "correct" thing |
|
4155
|
|
|
# would be to either drop the legacy anchors or make sure |
|
4156
|
|
|
# they're numbered first. However, this would require people |
|
4157
|
|
|
# to type in section names like "abc_.D7.93.D7.90.D7.A4" |
|
4158
|
|
|
# manually, so let's not bother worrying about it. |
|
4159
|
|
|
$legacyHeadline = Sanitizer::escapeId( $safeHeadline, |
|
4160
|
|
|
[ 'noninitial', 'legacy' ] ); |
|
4161
|
|
|
$safeHeadline = Sanitizer::escapeId( $safeHeadline ); |
|
4162
|
|
|
|
|
4163
|
|
|
if ( $legacyHeadline == $safeHeadline ) { |
|
4164
|
|
|
# No reason to have both (in fact, we can't) |
|
4165
|
|
|
$legacyHeadline = false; |
|
4166
|
|
|
} |
|
4167
|
|
|
} else { |
|
4168
|
|
|
$legacyHeadline = false; |
|
4169
|
|
|
$safeHeadline = Sanitizer::escapeId( $safeHeadline, |
|
4170
|
|
|
'noninitial' ); |
|
4171
|
|
|
} |
|
4172
|
|
|
|
|
4173
|
|
|
# HTML names must be case-insensitively unique (bug 10721). |
|
4174
|
|
|
# This does not apply to Unicode characters per |
|
4175
|
|
|
# http://www.w3.org/TR/html5/infrastructure.html#case-sensitivity-and-string-comparison |
|
4176
|
|
|
# @todo FIXME: We may be changing them depending on the current locale. |
|
4177
|
|
|
$arrayKey = strtolower( $safeHeadline ); |
|
4178
|
|
|
if ( $legacyHeadline === false ) { |
|
4179
|
|
|
$legacyArrayKey = false; |
|
4180
|
|
|
} else { |
|
4181
|
|
|
$legacyArrayKey = strtolower( $legacyHeadline ); |
|
4182
|
|
|
} |
|
4183
|
|
|
|
|
4184
|
|
|
# Create the anchor for linking from the TOC to the section |
|
4185
|
|
|
$anchor = $safeHeadline; |
|
4186
|
|
|
$legacyAnchor = $legacyHeadline; |
|
4187
|
|
View Code Duplication |
if ( isset( $refers[$arrayKey] ) ) { |
|
4188
|
|
|
// @codingStandardsIgnoreStart |
|
4189
|
|
|
for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i ); |
|
4190
|
|
|
// @codingStandardsIgnoreEnd |
|
4191
|
|
|
$anchor .= "_$i"; |
|
4192
|
|
|
$refers["${arrayKey}_$i"] = true; |
|
4193
|
|
|
} else { |
|
4194
|
|
|
$refers[$arrayKey] = true; |
|
4195
|
|
|
} |
|
4196
|
|
View Code Duplication |
if ( $legacyHeadline !== false && isset( $refers[$legacyArrayKey] ) ) { |
|
4197
|
|
|
// @codingStandardsIgnoreStart |
|
4198
|
|
|
for ( $i = 2; isset( $refers["${legacyArrayKey}_$i"] ); ++$i ); |
|
4199
|
|
|
// @codingStandardsIgnoreEnd |
|
4200
|
|
|
$legacyAnchor .= "_$i"; |
|
4201
|
|
|
$refers["${legacyArrayKey}_$i"] = true; |
|
4202
|
|
|
} else { |
|
4203
|
|
|
$refers[$legacyArrayKey] = true; |
|
4204
|
|
|
} |
|
4205
|
|
|
|
|
4206
|
|
|
# Don't number the heading if it is the only one (looks silly) |
|
4207
|
|
|
if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) { |
|
4208
|
|
|
# the two are different if the line contains a link |
|
4209
|
|
|
$headline = Html::element( |
|
4210
|
|
|
'span', |
|
4211
|
|
|
[ 'class' => 'mw-headline-number' ], |
|
4212
|
|
|
$numbering |
|
4213
|
|
|
) . ' ' . $headline; |
|
4214
|
|
|
} |
|
4215
|
|
|
|
|
4216
|
|
|
if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) { |
|
4217
|
|
|
$toc .= Linker::tocLine( $anchor, $tocline, |
|
4218
|
|
|
$numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) ); |
|
4219
|
|
|
} |
|
4220
|
|
|
|
|
4221
|
|
|
# Add the section to the section tree |
|
4222
|
|
|
# Find the DOM node for this header |
|
4223
|
|
|
$noOffset = ( $isTemplate || $sectionIndex === false ); |
|
4224
|
|
|
while ( $node && !$noOffset ) { |
|
4225
|
|
|
if ( $node->getName() === 'h' ) { |
|
4226
|
|
|
$bits = $node->splitHeading(); |
|
4227
|
|
|
if ( $bits['i'] == $sectionIndex ) { |
|
4228
|
|
|
break; |
|
4229
|
|
|
} |
|
4230
|
|
|
} |
|
4231
|
|
|
$byteOffset += mb_strlen( $this->mStripState->unstripBoth( |
|
4232
|
|
|
$frame->expand( $node, PPFrame::RECOVER_ORIG ) ) ); |
|
4233
|
|
|
$node = $node->getNextSibling(); |
|
4234
|
|
|
} |
|
4235
|
|
|
$tocraw[] = [ |
|
4236
|
|
|
'toclevel' => $toclevel, |
|
4237
|
|
|
'level' => $level, |
|
4238
|
|
|
'line' => $tocline, |
|
4239
|
|
|
'number' => $numbering, |
|
4240
|
|
|
'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex, |
|
4241
|
|
|
'fromtitle' => $titleText, |
|
4242
|
|
|
'byteoffset' => ( $noOffset ? null : $byteOffset ), |
|
4243
|
|
|
'anchor' => $anchor, |
|
4244
|
|
|
]; |
|
4245
|
|
|
|
|
4246
|
|
|
# give headline the correct <h#> tag |
|
4247
|
|
|
if ( $maybeShowEditLink && $sectionIndex !== false ) { |
|
4248
|
|
|
// Output edit section links as markers with styles that can be customized by skins |
|
4249
|
|
|
if ( $isTemplate ) { |
|
4250
|
|
|
# Put a T flag in the section identifier, to indicate to extractSections() |
|
4251
|
|
|
# that sections inside <includeonly> should be counted. |
|
4252
|
|
|
$editsectionPage = $titleText; |
|
4253
|
|
|
$editsectionSection = "T-$sectionIndex"; |
|
4254
|
|
|
$editsectionContent = null; |
|
4255
|
|
|
} else { |
|
4256
|
|
|
$editsectionPage = $this->mTitle->getPrefixedText(); |
|
4257
|
|
|
$editsectionSection = $sectionIndex; |
|
4258
|
|
|
$editsectionContent = $headlineHint; |
|
4259
|
|
|
} |
|
4260
|
|
|
// We use a bit of pesudo-xml for editsection markers. The |
|
4261
|
|
|
// language converter is run later on. Using a UNIQ style marker |
|
4262
|
|
|
// leads to the converter screwing up the tokens when it |
|
4263
|
|
|
// converts stuff. And trying to insert strip tags fails too. At |
|
4264
|
|
|
// this point all real inputted tags have already been escaped, |
|
4265
|
|
|
// so we don't have to worry about a user trying to input one of |
|
4266
|
|
|
// these markers directly. We use a page and section attribute |
|
4267
|
|
|
// to stop the language converter from converting these |
|
4268
|
|
|
// important bits of data, but put the headline hint inside a |
|
4269
|
|
|
// content block because the language converter is supposed to |
|
4270
|
|
|
// be able to convert that piece of data. |
|
4271
|
|
|
// Gets replaced with html in ParserOutput::getText |
|
4272
|
|
|
$editlink = '<mw:editsection page="' . htmlspecialchars( $editsectionPage ); |
|
4273
|
|
|
$editlink .= '" section="' . htmlspecialchars( $editsectionSection ) . '"'; |
|
4274
|
|
|
if ( $editsectionContent !== null ) { |
|
4275
|
|
|
$editlink .= '>' . $editsectionContent . '</mw:editsection>'; |
|
4276
|
|
|
} else { |
|
4277
|
|
|
$editlink .= '/>'; |
|
4278
|
|
|
} |
|
4279
|
|
|
} else { |
|
4280
|
|
|
$editlink = ''; |
|
4281
|
|
|
} |
|
4282
|
|
|
$head[$headlineCount] = Linker::makeHeadline( $level, |
|
4283
|
|
|
$matches['attrib'][$headlineCount], $anchor, $headline, |
|
4284
|
|
|
$editlink, $legacyAnchor ); |
|
4285
|
|
|
|
|
4286
|
|
|
$headlineCount++; |
|
4287
|
|
|
} |
|
4288
|
|
|
|
|
4289
|
|
|
$this->setOutputType( $oldType ); |
|
4290
|
|
|
|
|
4291
|
|
|
# Never ever show TOC if no headers |
|
4292
|
|
|
if ( $numVisible < 1 ) { |
|
4293
|
|
|
$enoughToc = false; |
|
4294
|
|
|
} |
|
4295
|
|
|
|
|
4296
|
|
|
if ( $enoughToc ) { |
|
4297
|
|
|
if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) { |
|
4298
|
|
|
$toc .= Linker::tocUnindent( $prevtoclevel - 1 ); |
|
4299
|
|
|
} |
|
4300
|
|
|
$toc = Linker::tocList( $toc, $this->mOptions->getUserLangObj() ); |
|
4301
|
|
|
$this->mOutput->setTOCHTML( $toc ); |
|
4302
|
|
|
$toc = self::TOC_START . $toc . self::TOC_END; |
|
4303
|
|
|
$this->mOutput->addModules( 'mediawiki.toc' ); |
|
4304
|
|
|
} |
|
4305
|
|
|
|
|
4306
|
|
|
if ( $isMain ) { |
|
4307
|
|
|
$this->mOutput->setSections( $tocraw ); |
|
4308
|
|
|
} |
|
4309
|
|
|
|
|
4310
|
|
|
# split up and insert constructed headlines |
|
4311
|
|
|
$blocks = preg_split( '/<H[1-6].*?>[\s\S]*?<\/H[1-6]>/i', $text ); |
|
4312
|
|
|
$i = 0; |
|
4313
|
|
|
|
|
4314
|
|
|
// build an array of document sections |
|
4315
|
|
|
$sections = []; |
|
4316
|
|
|
foreach ( $blocks as $block ) { |
|
4317
|
|
|
// $head is zero-based, sections aren't. |
|
4318
|
|
|
if ( empty( $head[$i - 1] ) ) { |
|
4319
|
|
|
$sections[$i] = $block; |
|
4320
|
|
|
} else { |
|
4321
|
|
|
$sections[$i] = $head[$i - 1] . $block; |
|
4322
|
|
|
} |
|
4323
|
|
|
|
|
4324
|
|
|
/** |
|
4325
|
|
|
* Send a hook, one per section. |
|
4326
|
|
|
* The idea here is to be able to make section-level DIVs, but to do so in a |
|
4327
|
|
|
* lower-impact, more correct way than r50769 |
|
4328
|
|
|
* |
|
4329
|
|
|
* $this : caller |
|
4330
|
|
|
* $section : the section number |
|
4331
|
|
|
* &$sectionContent : ref to the content of the section |
|
4332
|
|
|
* $showEditLinks : boolean describing whether this section has an edit link |
|
4333
|
|
|
*/ |
|
4334
|
|
|
Hooks::run( 'ParserSectionCreate', [ $this, $i, &$sections[$i], $showEditLink ] ); |
|
4335
|
|
|
|
|
4336
|
|
|
$i++; |
|
4337
|
|
|
} |
|
4338
|
|
|
|
|
4339
|
|
|
if ( $enoughToc && $isMain && !$this->mForceTocPosition ) { |
|
4340
|
|
|
// append the TOC at the beginning |
|
4341
|
|
|
// Top anchor now in skin |
|
4342
|
|
|
$sections[0] = $sections[0] . $toc . "\n"; |
|
4343
|
|
|
} |
|
4344
|
|
|
|
|
4345
|
|
|
$full .= implode( '', $sections ); |
|
4346
|
|
|
|
|
4347
|
|
|
if ( $this->mForceTocPosition ) { |
|
4348
|
|
|
return str_replace( '<!--MWTOC-->', $toc, $full ); |
|
4349
|
|
|
} else { |
|
4350
|
|
|
return $full; |
|
4351
|
|
|
} |
|
4352
|
|
|
} |
|
4353
|
|
|
|
|
4354
|
|
|
/** |
|
4355
|
|
|
* Transform wiki markup when saving a page by doing "\r\n" -> "\n" |
|
4356
|
|
|
* conversion, substituting signatures, {{subst:}} templates, etc. |
|
4357
|
|
|
* |
|
4358
|
|
|
* @param string $text The text to transform |
|
4359
|
|
|
* @param Title $title The Title object for the current article |
|
4360
|
|
|
* @param User $user The User object describing the current user |
|
4361
|
|
|
* @param ParserOptions $options Parsing options |
|
4362
|
|
|
* @param bool $clearState Whether to clear the parser state first |
|
4363
|
|
|
* @return string The altered wiki markup |
|
4364
|
|
|
*/ |
|
4365
|
|
|
public function preSaveTransform( $text, Title $title, User $user, |
|
4366
|
|
|
ParserOptions $options, $clearState = true |
|
4367
|
|
|
) { |
|
4368
|
|
|
if ( $clearState ) { |
|
4369
|
|
|
$magicScopeVariable = $this->lock(); |
|
|
|
|
|
|
4370
|
|
|
} |
|
4371
|
|
|
$this->startParse( $title, $options, self::OT_WIKI, $clearState ); |
|
4372
|
|
|
$this->setUser( $user ); |
|
4373
|
|
|
|
|
4374
|
|
|
$pairs = [ |
|
4375
|
|
|
"\r\n" => "\n", |
|
4376
|
|
|
"\r" => "\n", |
|
4377
|
|
|
]; |
|
4378
|
|
|
$text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text ); |
|
4379
|
|
|
if ( $options->getPreSaveTransform() ) { |
|
4380
|
|
|
$text = $this->pstPass2( $text, $user ); |
|
4381
|
|
|
} |
|
4382
|
|
|
$text = $this->mStripState->unstripBoth( $text ); |
|
4383
|
|
|
|
|
4384
|
|
|
$this->setUser( null ); # Reset |
|
4385
|
|
|
|
|
4386
|
|
|
return $text; |
|
4387
|
|
|
} |
|
4388
|
|
|
|
|
4389
|
|
|
/** |
|
4390
|
|
|
* Pre-save transform helper function |
|
4391
|
|
|
* |
|
4392
|
|
|
* @param string $text |
|
4393
|
|
|
* @param User $user |
|
4394
|
|
|
* |
|
4395
|
|
|
* @return string |
|
4396
|
|
|
*/ |
|
4397
|
|
|
private function pstPass2( $text, $user ) { |
|
4398
|
|
|
global $wgContLang; |
|
4399
|
|
|
|
|
4400
|
|
|
# Note: This is the timestamp saved as hardcoded wikitext to |
|
4401
|
|
|
# the database, we use $wgContLang here in order to give |
|
4402
|
|
|
# everyone the same signature and use the default one rather |
|
4403
|
|
|
# than the one selected in each user's preferences. |
|
4404
|
|
|
# (see also bug 12815) |
|
4405
|
|
|
$ts = $this->mOptions->getTimestamp(); |
|
4406
|
|
|
$timestamp = MWTimestamp::getLocalInstance( $ts ); |
|
4407
|
|
|
$ts = $timestamp->format( 'YmdHis' ); |
|
4408
|
|
|
$tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text(); |
|
4409
|
|
|
|
|
4410
|
|
|
$d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)"; |
|
4411
|
|
|
|
|
4412
|
|
|
# Variable replacement |
|
4413
|
|
|
# Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags |
|
4414
|
|
|
$text = $this->replaceVariables( $text ); |
|
4415
|
|
|
|
|
4416
|
|
|
# This works almost by chance, as the replaceVariables are done before the getUserSig(), |
|
4417
|
|
|
# which may corrupt this parser instance via its wfMessage()->text() call- |
|
4418
|
|
|
|
|
4419
|
|
|
# Signatures |
|
4420
|
|
|
$sigText = $this->getUserSig( $user ); |
|
4421
|
|
|
$text = strtr( $text, [ |
|
4422
|
|
|
'~~~~~' => $d, |
|
4423
|
|
|
'~~~~' => "$sigText $d", |
|
4424
|
|
|
'~~~' => $sigText |
|
4425
|
|
|
] ); |
|
4426
|
|
|
|
|
4427
|
|
|
# Context links ("pipe tricks"): [[|name]] and [[name (context)|]] |
|
4428
|
|
|
$tc = '[' . Title::legalChars() . ']'; |
|
4429
|
|
|
$nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii! |
|
4430
|
|
|
|
|
4431
|
|
|
// [[ns:page (context)|]] |
|
4432
|
|
|
$p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/"; |
|
4433
|
|
|
// [[ns:page(context)|]] (double-width brackets, added in r40257) |
|
4434
|
|
|
$p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/"; |
|
4435
|
|
|
// [[ns:page (context), context|]] (using either single or double-width comma) |
|
4436
|
|
|
$p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; |
|
4437
|
|
|
// [[|page]] (reverse pipe trick: add context from page title) |
|
4438
|
|
|
$p2 = "/\[\[\\|($tc+)]]/"; |
|
4439
|
|
|
|
|
4440
|
|
|
# try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]" |
|
4441
|
|
|
$text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text ); |
|
4442
|
|
|
$text = preg_replace( $p4, '[[\\1\\2\\3|\\2]]', $text ); |
|
4443
|
|
|
$text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text ); |
|
4444
|
|
|
|
|
4445
|
|
|
$t = $this->mTitle->getText(); |
|
4446
|
|
|
$m = []; |
|
4447
|
|
|
if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) { |
|
4448
|
|
|
$text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); |
|
4449
|
|
|
} elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) { |
|
4450
|
|
|
$text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text ); |
|
4451
|
|
|
} else { |
|
4452
|
|
|
# if there's no context, don't bother duplicating the title |
|
4453
|
|
|
$text = preg_replace( $p2, '[[\\1]]', $text ); |
|
4454
|
|
|
} |
|
4455
|
|
|
|
|
4456
|
|
|
# Trim trailing whitespace |
|
4457
|
|
|
$text = rtrim( $text ); |
|
4458
|
|
|
|
|
4459
|
|
|
return $text; |
|
4460
|
|
|
} |
|
4461
|
|
|
|
|
4462
|
|
|
/** |
|
4463
|
|
|
* Fetch the user's signature text, if any, and normalize to |
|
4464
|
|
|
* validated, ready-to-insert wikitext. |
|
4465
|
|
|
* If you have pre-fetched the nickname or the fancySig option, you can |
|
4466
|
|
|
* specify them here to save a database query. |
|
4467
|
|
|
* Do not reuse this parser instance after calling getUserSig(), |
|
4468
|
|
|
* as it may have changed if it's the $wgParser. |
|
4469
|
|
|
* |
|
4470
|
|
|
* @param User $user |
|
4471
|
|
|
* @param string|bool $nickname Nickname to use or false to use user's default nickname |
|
4472
|
|
|
* @param bool|null $fancySig whether the nicknname is the complete signature |
|
4473
|
|
|
* or null to use default value |
|
4474
|
|
|
* @return string |
|
4475
|
|
|
*/ |
|
4476
|
|
|
public function getUserSig( &$user, $nickname = false, $fancySig = null ) { |
|
4477
|
|
|
global $wgMaxSigChars; |
|
4478
|
|
|
|
|
4479
|
|
|
$username = $user->getName(); |
|
4480
|
|
|
|
|
4481
|
|
|
# If not given, retrieve from the user object. |
|
4482
|
|
|
if ( $nickname === false ) { |
|
4483
|
|
|
$nickname = $user->getOption( 'nickname' ); |
|
4484
|
|
|
} |
|
4485
|
|
|
|
|
4486
|
|
|
if ( is_null( $fancySig ) ) { |
|
4487
|
|
|
$fancySig = $user->getBoolOption( 'fancysig' ); |
|
4488
|
|
|
} |
|
4489
|
|
|
|
|
4490
|
|
|
$nickname = $nickname == null ? $username : $nickname; |
|
4491
|
|
|
|
|
4492
|
|
|
if ( mb_strlen( $nickname ) > $wgMaxSigChars ) { |
|
4493
|
|
|
$nickname = $username; |
|
4494
|
|
|
wfDebug( __METHOD__ . ": $username has overlong signature.\n" ); |
|
4495
|
|
|
} elseif ( $fancySig !== false ) { |
|
4496
|
|
|
# Sig. might contain markup; validate this |
|
4497
|
|
|
if ( $this->validateSig( $nickname ) !== false ) { |
|
|
|
|
|
|
4498
|
|
|
# Validated; clean up (if needed) and return it |
|
4499
|
|
|
return $this->cleanSig( $nickname, true ); |
|
|
|
|
|
|
4500
|
|
|
} else { |
|
4501
|
|
|
# Failed to validate; fall back to the default |
|
4502
|
|
|
$nickname = $username; |
|
4503
|
|
|
wfDebug( __METHOD__ . ": $username has bad XML tags in signature.\n" ); |
|
4504
|
|
|
} |
|
4505
|
|
|
} |
|
4506
|
|
|
|
|
4507
|
|
|
# Make sure nickname doesnt get a sig in a sig |
|
4508
|
|
|
$nickname = self::cleanSigInSig( $nickname ); |
|
|
|
|
|
|
4509
|
|
|
|
|
4510
|
|
|
# If we're still here, make it a link to the user page |
|
4511
|
|
|
$userText = wfEscapeWikiText( $username ); |
|
4512
|
|
|
$nickText = wfEscapeWikiText( $nickname ); |
|
4513
|
|
|
$msgName = $user->isAnon() ? 'signature-anon' : 'signature'; |
|
4514
|
|
|
|
|
4515
|
|
|
return wfMessage( $msgName, $userText, $nickText )->inContentLanguage() |
|
4516
|
|
|
->title( $this->getTitle() )->text(); |
|
4517
|
|
|
} |
|
4518
|
|
|
|
|
4519
|
|
|
/** |
|
4520
|
|
|
* Check that the user's signature contains no bad XML |
|
4521
|
|
|
* |
|
4522
|
|
|
* @param string $text |
|
4523
|
|
|
* @return string|bool An expanded string, or false if invalid. |
|
4524
|
|
|
*/ |
|
4525
|
|
|
public function validateSig( $text ) { |
|
4526
|
|
|
return Xml::isWellFormedXmlFragment( $text ) ? $text : false; |
|
4527
|
|
|
} |
|
4528
|
|
|
|
|
4529
|
|
|
/** |
|
4530
|
|
|
* Clean up signature text |
|
4531
|
|
|
* |
|
4532
|
|
|
* 1) Strip 3, 4 or 5 tildes out of signatures @see cleanSigInSig |
|
4533
|
|
|
* 2) Substitute all transclusions |
|
4534
|
|
|
* |
|
4535
|
|
|
* @param string $text |
|
4536
|
|
|
* @param bool $parsing Whether we're cleaning (preferences save) or parsing |
|
4537
|
|
|
* @return string Signature text |
|
4538
|
|
|
*/ |
|
4539
|
|
|
public function cleanSig( $text, $parsing = false ) { |
|
4540
|
|
|
if ( !$parsing ) { |
|
4541
|
|
|
global $wgTitle; |
|
4542
|
|
|
$magicScopeVariable = $this->lock(); |
|
|
|
|
|
|
4543
|
|
|
$this->startParse( $wgTitle, new ParserOptions, self::OT_PREPROCESS, true ); |
|
4544
|
|
|
} |
|
4545
|
|
|
|
|
4546
|
|
|
# Option to disable this feature |
|
4547
|
|
|
if ( !$this->mOptions->getCleanSignatures() ) { |
|
4548
|
|
|
return $text; |
|
4549
|
|
|
} |
|
4550
|
|
|
|
|
4551
|
|
|
# @todo FIXME: Regex doesn't respect extension tags or nowiki |
|
4552
|
|
|
# => Move this logic to braceSubstitution() |
|
4553
|
|
|
$substWord = MagicWord::get( 'subst' ); |
|
4554
|
|
|
$substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase(); |
|
4555
|
|
|
$substText = '{{' . $substWord->getSynonym( 0 ); |
|
4556
|
|
|
|
|
4557
|
|
|
$text = preg_replace( $substRegex, $substText, $text ); |
|
4558
|
|
|
$text = self::cleanSigInSig( $text ); |
|
4559
|
|
|
$dom = $this->preprocessToDom( $text ); |
|
4560
|
|
|
$frame = $this->getPreprocessor()->newFrame(); |
|
4561
|
|
|
$text = $frame->expand( $dom ); |
|
4562
|
|
|
|
|
4563
|
|
|
if ( !$parsing ) { |
|
4564
|
|
|
$text = $this->mStripState->unstripBoth( $text ); |
|
4565
|
|
|
} |
|
4566
|
|
|
|
|
4567
|
|
|
return $text; |
|
4568
|
|
|
} |
|
4569
|
|
|
|
|
4570
|
|
|
/** |
|
4571
|
|
|
* Strip 3, 4 or 5 tildes out of signatures. |
|
4572
|
|
|
* |
|
4573
|
|
|
* @param string $text |
|
4574
|
|
|
* @return string Signature text with /~{3,5}/ removed |
|
4575
|
|
|
*/ |
|
4576
|
|
|
public static function cleanSigInSig( $text ) { |
|
4577
|
|
|
$text = preg_replace( '/~{3,5}/', '', $text ); |
|
4578
|
|
|
return $text; |
|
4579
|
|
|
} |
|
4580
|
|
|
|
|
4581
|
|
|
/** |
|
4582
|
|
|
* Set up some variables which are usually set up in parse() |
|
4583
|
|
|
* so that an external function can call some class members with confidence |
|
4584
|
|
|
* |
|
4585
|
|
|
* @param Title|null $title |
|
4586
|
|
|
* @param ParserOptions $options |
|
4587
|
|
|
* @param int $outputType |
|
4588
|
|
|
* @param bool $clearState |
|
4589
|
|
|
*/ |
|
4590
|
|
|
public function startExternalParse( Title $title = null, ParserOptions $options, |
|
4591
|
|
|
$outputType, $clearState = true |
|
4592
|
|
|
) { |
|
4593
|
|
|
$this->startParse( $title, $options, $outputType, $clearState ); |
|
4594
|
|
|
} |
|
4595
|
|
|
|
|
4596
|
|
|
/** |
|
4597
|
|
|
* @param Title|null $title |
|
4598
|
|
|
* @param ParserOptions $options |
|
4599
|
|
|
* @param int $outputType |
|
4600
|
|
|
* @param bool $clearState |
|
4601
|
|
|
*/ |
|
4602
|
|
|
private function startParse( Title $title = null, ParserOptions $options, |
|
4603
|
|
|
$outputType, $clearState = true |
|
4604
|
|
|
) { |
|
4605
|
|
|
$this->setTitle( $title ); |
|
|
|
|
|
|
4606
|
|
|
$this->mOptions = $options; |
|
4607
|
|
|
$this->setOutputType( $outputType ); |
|
4608
|
|
|
if ( $clearState ) { |
|
4609
|
|
|
$this->clearState(); |
|
4610
|
|
|
} |
|
4611
|
|
|
} |
|
4612
|
|
|
|
|
4613
|
|
|
/** |
|
4614
|
|
|
* Wrapper for preprocess() |
|
4615
|
|
|
* |
|
4616
|
|
|
* @param string $text The text to preprocess |
|
4617
|
|
|
* @param ParserOptions $options Options |
|
4618
|
|
|
* @param Title|null $title Title object or null to use $wgTitle |
|
4619
|
|
|
* @return string |
|
4620
|
|
|
*/ |
|
4621
|
|
|
public function transformMsg( $text, $options, $title = null ) { |
|
4622
|
|
|
static $executing = false; |
|
4623
|
|
|
|
|
4624
|
|
|
# Guard against infinite recursion |
|
4625
|
|
|
if ( $executing ) { |
|
4626
|
|
|
return $text; |
|
4627
|
|
|
} |
|
4628
|
|
|
$executing = true; |
|
4629
|
|
|
|
|
4630
|
|
|
if ( !$title ) { |
|
4631
|
|
|
global $wgTitle; |
|
4632
|
|
|
$title = $wgTitle; |
|
4633
|
|
|
} |
|
4634
|
|
|
|
|
4635
|
|
|
$text = $this->preprocess( $text, $title, $options ); |
|
4636
|
|
|
|
|
4637
|
|
|
$executing = false; |
|
4638
|
|
|
return $text; |
|
4639
|
|
|
} |
|
4640
|
|
|
|
|
4641
|
|
|
/** |
|
4642
|
|
|
* Create an HTML-style tag, e.g. "<yourtag>special text</yourtag>" |
|
4643
|
|
|
* The callback should have the following form: |
|
4644
|
|
|
* function myParserHook( $text, $params, $parser, $frame ) { ... } |
|
4645
|
|
|
* |
|
4646
|
|
|
* Transform and return $text. Use $parser for any required context, e.g. use |
|
4647
|
|
|
* $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions |
|
4648
|
|
|
* |
|
4649
|
|
|
* Hooks may return extended information by returning an array, of which the |
|
4650
|
|
|
* first numbered element (index 0) must be the return string, and all other |
|
4651
|
|
|
* entries are extracted into local variables within an internal function |
|
4652
|
|
|
* in the Parser class. |
|
4653
|
|
|
* |
|
4654
|
|
|
* This interface (introduced r61913) appears to be undocumented, but |
|
4655
|
|
|
* 'markerType' is used by some core tag hooks to override which strip |
|
4656
|
|
|
* array their results are placed in. **Use great caution if attempting |
|
4657
|
|
|
* this interface, as it is not documented and injudicious use could smash |
|
4658
|
|
|
* private variables.** |
|
4659
|
|
|
* |
|
4660
|
|
|
* @param string $tag The tag to use, e.g. 'hook' for "<hook>" |
|
4661
|
|
|
* @param callable $callback The callback function (and object) to use for the tag |
|
4662
|
|
|
* @throws MWException |
|
4663
|
|
|
* @return callable|null The old value of the mTagHooks array associated with the hook |
|
4664
|
|
|
*/ |
|
4665
|
|
View Code Duplication |
public function setHook( $tag, $callback ) { |
|
4666
|
|
|
$tag = strtolower( $tag ); |
|
4667
|
|
|
if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) { |
|
4668
|
|
|
throw new MWException( "Invalid character {$m[0]} in setHook('$tag', ...) call" ); |
|
4669
|
|
|
} |
|
4670
|
|
|
$oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null; |
|
4671
|
|
|
$this->mTagHooks[$tag] = $callback; |
|
4672
|
|
|
if ( !in_array( $tag, $this->mStripList ) ) { |
|
4673
|
|
|
$this->mStripList[] = $tag; |
|
4674
|
|
|
} |
|
4675
|
|
|
|
|
4676
|
|
|
return $oldVal; |
|
4677
|
|
|
} |
|
4678
|
|
|
|
|
4679
|
|
|
/** |
|
4680
|
|
|
* As setHook(), but letting the contents be parsed. |
|
4681
|
|
|
* |
|
4682
|
|
|
* Transparent tag hooks are like regular XML-style tag hooks, except they |
|
4683
|
|
|
* operate late in the transformation sequence, on HTML instead of wikitext. |
|
4684
|
|
|
* |
|
4685
|
|
|
* This is probably obsoleted by things dealing with parser frames? |
|
4686
|
|
|
* The only extension currently using it is geoserver. |
|
4687
|
|
|
* |
|
4688
|
|
|
* @since 1.10 |
|
4689
|
|
|
* @todo better document or deprecate this |
|
4690
|
|
|
* |
|
4691
|
|
|
* @param string $tag The tag to use, e.g. 'hook' for "<hook>" |
|
4692
|
|
|
* @param callable $callback The callback function (and object) to use for the tag |
|
4693
|
|
|
* @throws MWException |
|
4694
|
|
|
* @return callable|null The old value of the mTagHooks array associated with the hook |
|
4695
|
|
|
*/ |
|
4696
|
|
|
public function setTransparentTagHook( $tag, $callback ) { |
|
4697
|
|
|
$tag = strtolower( $tag ); |
|
4698
|
|
|
if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) { |
|
4699
|
|
|
throw new MWException( "Invalid character {$m[0]} in setTransparentHook('$tag', ...) call" ); |
|
4700
|
|
|
} |
|
4701
|
|
|
$oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null; |
|
4702
|
|
|
$this->mTransparentTagHooks[$tag] = $callback; |
|
4703
|
|
|
|
|
4704
|
|
|
return $oldVal; |
|
4705
|
|
|
} |
|
4706
|
|
|
|
|
4707
|
|
|
/** |
|
4708
|
|
|
* Remove all tag hooks |
|
4709
|
|
|
*/ |
|
4710
|
|
|
public function clearTagHooks() { |
|
4711
|
|
|
$this->mTagHooks = []; |
|
4712
|
|
|
$this->mFunctionTagHooks = []; |
|
4713
|
|
|
$this->mStripList = $this->mDefaultStripList; |
|
4714
|
|
|
} |
|
4715
|
|
|
|
|
4716
|
|
|
/** |
|
4717
|
|
|
* Create a function, e.g. {{sum:1|2|3}} |
|
4718
|
|
|
* The callback function should have the form: |
|
4719
|
|
|
* function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... } |
|
4720
|
|
|
* |
|
4721
|
|
|
* Or with Parser::SFH_OBJECT_ARGS: |
|
4722
|
|
|
* function myParserFunction( $parser, $frame, $args ) { ... } |
|
4723
|
|
|
* |
|
4724
|
|
|
* The callback may either return the text result of the function, or an array with the text |
|
4725
|
|
|
* in element 0, and a number of flags in the other elements. The names of the flags are |
|
4726
|
|
|
* specified in the keys. Valid flags are: |
|
4727
|
|
|
* found The text returned is valid, stop processing the template. This |
|
4728
|
|
|
* is on by default. |
|
4729
|
|
|
* nowiki Wiki markup in the return value should be escaped |
|
4730
|
|
|
* isHTML The returned text is HTML, armour it against wikitext transformation |
|
4731
|
|
|
* |
|
4732
|
|
|
* @param string $id The magic word ID |
|
4733
|
|
|
* @param callable $callback The callback function (and object) to use |
|
4734
|
|
|
* @param int $flags A combination of the following flags: |
|
4735
|
|
|
* Parser::SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}} |
|
4736
|
|
|
* |
|
4737
|
|
|
* Parser::SFH_OBJECT_ARGS Pass the template arguments as PPNode objects instead of text. |
|
4738
|
|
|
* This allows for conditional expansion of the parse tree, allowing you to eliminate dead |
|
4739
|
|
|
* branches and thus speed up parsing. It is also possible to analyse the parse tree of |
|
4740
|
|
|
* the arguments, and to control the way they are expanded. |
|
4741
|
|
|
* |
|
4742
|
|
|
* The $frame parameter is a PPFrame. This can be used to produce expanded text from the |
|
4743
|
|
|
* arguments, for instance: |
|
4744
|
|
|
* $text = isset( $args[0] ) ? $frame->expand( $args[0] ) : ''; |
|
4745
|
|
|
* |
|
4746
|
|
|
* For technical reasons, $args[0] is pre-expanded and will be a string. This may change in |
|
4747
|
|
|
* future versions. Please call $frame->expand() on it anyway so that your code keeps |
|
4748
|
|
|
* working if/when this is changed. |
|
4749
|
|
|
* |
|
4750
|
|
|
* If you want whitespace to be trimmed from $args, you need to do it yourself, post- |
|
4751
|
|
|
* expansion. |
|
4752
|
|
|
* |
|
4753
|
|
|
* Please read the documentation in includes/parser/Preprocessor.php for more information |
|
4754
|
|
|
* about the methods available in PPFrame and PPNode. |
|
4755
|
|
|
* |
|
4756
|
|
|
* @throws MWException |
|
4757
|
|
|
* @return string|callable The old callback function for this name, if any |
|
4758
|
|
|
*/ |
|
4759
|
|
|
public function setFunctionHook( $id, $callback, $flags = 0 ) { |
|
4760
|
|
|
global $wgContLang; |
|
4761
|
|
|
|
|
4762
|
|
|
$oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null; |
|
4763
|
|
|
$this->mFunctionHooks[$id] = [ $callback, $flags ]; |
|
4764
|
|
|
|
|
4765
|
|
|
# Add to function cache |
|
4766
|
|
|
$mw = MagicWord::get( $id ); |
|
4767
|
|
|
if ( !$mw ) { |
|
4768
|
|
|
throw new MWException( __METHOD__ . '() expecting a magic word identifier.' ); |
|
4769
|
|
|
} |
|
4770
|
|
|
|
|
4771
|
|
|
$synonyms = $mw->getSynonyms(); |
|
4772
|
|
|
$sensitive = intval( $mw->isCaseSensitive() ); |
|
4773
|
|
|
|
|
4774
|
|
|
foreach ( $synonyms as $syn ) { |
|
4775
|
|
|
# Case |
|
4776
|
|
|
if ( !$sensitive ) { |
|
4777
|
|
|
$syn = $wgContLang->lc( $syn ); |
|
4778
|
|
|
} |
|
4779
|
|
|
# Add leading hash |
|
4780
|
|
|
if ( !( $flags & self::SFH_NO_HASH ) ) { |
|
4781
|
|
|
$syn = '#' . $syn; |
|
4782
|
|
|
} |
|
4783
|
|
|
# Remove trailing colon |
|
4784
|
|
|
if ( substr( $syn, -1, 1 ) === ':' ) { |
|
4785
|
|
|
$syn = substr( $syn, 0, -1 ); |
|
4786
|
|
|
} |
|
4787
|
|
|
$this->mFunctionSynonyms[$sensitive][$syn] = $id; |
|
4788
|
|
|
} |
|
4789
|
|
|
return $oldVal; |
|
4790
|
|
|
} |
|
4791
|
|
|
|
|
4792
|
|
|
/** |
|
4793
|
|
|
* Get all registered function hook identifiers |
|
4794
|
|
|
* |
|
4795
|
|
|
* @return array |
|
4796
|
|
|
*/ |
|
4797
|
|
|
public function getFunctionHooks() { |
|
4798
|
|
|
return array_keys( $this->mFunctionHooks ); |
|
4799
|
|
|
} |
|
4800
|
|
|
|
|
4801
|
|
|
/** |
|
4802
|
|
|
* Create a tag function, e.g. "<test>some stuff</test>". |
|
4803
|
|
|
* Unlike tag hooks, tag functions are parsed at preprocessor level. |
|
4804
|
|
|
* Unlike parser functions, their content is not preprocessed. |
|
4805
|
|
|
* @param string $tag |
|
4806
|
|
|
* @param callable $callback |
|
4807
|
|
|
* @param int $flags |
|
4808
|
|
|
* @throws MWException |
|
4809
|
|
|
* @return null |
|
4810
|
|
|
*/ |
|
4811
|
|
View Code Duplication |
public function setFunctionTagHook( $tag, $callback, $flags ) { |
|
4812
|
|
|
$tag = strtolower( $tag ); |
|
4813
|
|
|
if ( preg_match( '/[<>\r\n]/', $tag, $m ) ) { |
|
4814
|
|
|
throw new MWException( "Invalid character {$m[0]} in setFunctionTagHook('$tag', ...) call" ); |
|
4815
|
|
|
} |
|
4816
|
|
|
$old = isset( $this->mFunctionTagHooks[$tag] ) ? |
|
4817
|
|
|
$this->mFunctionTagHooks[$tag] : null; |
|
4818
|
|
|
$this->mFunctionTagHooks[$tag] = [ $callback, $flags ]; |
|
4819
|
|
|
|
|
4820
|
|
|
if ( !in_array( $tag, $this->mStripList ) ) { |
|
4821
|
|
|
$this->mStripList[] = $tag; |
|
4822
|
|
|
} |
|
4823
|
|
|
|
|
4824
|
|
|
return $old; |
|
4825
|
|
|
} |
|
4826
|
|
|
|
|
4827
|
|
|
/** |
|
4828
|
|
|
* Replace "<!--LINK-->" link placeholders with actual links, in the buffer |
|
4829
|
|
|
* Placeholders created in Linker::link() |
|
4830
|
|
|
* |
|
4831
|
|
|
* @param string $text |
|
4832
|
|
|
* @param int $options |
|
4833
|
|
|
*/ |
|
4834
|
|
|
public function replaceLinkHolders( &$text, $options = 0 ) { |
|
4835
|
|
|
$this->mLinkHolders->replace( $text ); |
|
4836
|
|
|
} |
|
4837
|
|
|
|
|
4838
|
|
|
/** |
|
4839
|
|
|
* Replace "<!--LINK-->" link placeholders with plain text of links |
|
4840
|
|
|
* (not HTML-formatted). |
|
4841
|
|
|
* |
|
4842
|
|
|
* @param string $text |
|
4843
|
|
|
* @return string |
|
4844
|
|
|
*/ |
|
4845
|
|
|
public function replaceLinkHoldersText( $text ) { |
|
4846
|
|
|
return $this->mLinkHolders->replaceText( $text ); |
|
4847
|
|
|
} |
|
4848
|
|
|
|
|
4849
|
|
|
/** |
|
4850
|
|
|
* Renders an image gallery from a text with one line per image. |
|
4851
|
|
|
* text labels may be given by using |-style alternative text. E.g. |
|
4852
|
|
|
* Image:one.jpg|The number "1" |
|
4853
|
|
|
* Image:tree.jpg|A tree |
|
4854
|
|
|
* given as text will return the HTML of a gallery with two images, |
|
4855
|
|
|
* labeled 'The number "1"' and |
|
4856
|
|
|
* 'A tree'. |
|
4857
|
|
|
* |
|
4858
|
|
|
* @param string $text |
|
4859
|
|
|
* @param array $params |
|
4860
|
|
|
* @return string HTML |
|
4861
|
|
|
*/ |
|
4862
|
|
|
public function renderImageGallery( $text, $params ) { |
|
4863
|
|
|
|
|
4864
|
|
|
$mode = false; |
|
4865
|
|
|
if ( isset( $params['mode'] ) ) { |
|
4866
|
|
|
$mode = $params['mode']; |
|
4867
|
|
|
} |
|
4868
|
|
|
|
|
4869
|
|
|
try { |
|
4870
|
|
|
$ig = ImageGalleryBase::factory( $mode ); |
|
4871
|
|
|
} catch ( Exception $e ) { |
|
4872
|
|
|
// If invalid type set, fallback to default. |
|
4873
|
|
|
$ig = ImageGalleryBase::factory( false ); |
|
4874
|
|
|
} |
|
4875
|
|
|
|
|
4876
|
|
|
$ig->setContextTitle( $this->mTitle ); |
|
4877
|
|
|
$ig->setShowBytes( false ); |
|
4878
|
|
|
$ig->setShowFilename( false ); |
|
4879
|
|
|
$ig->setParser( $this ); |
|
4880
|
|
|
$ig->setHideBadImages(); |
|
4881
|
|
|
$ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) ); |
|
4882
|
|
|
|
|
4883
|
|
|
if ( isset( $params['showfilename'] ) ) { |
|
4884
|
|
|
$ig->setShowFilename( true ); |
|
4885
|
|
|
} else { |
|
4886
|
|
|
$ig->setShowFilename( false ); |
|
4887
|
|
|
} |
|
4888
|
|
|
if ( isset( $params['caption'] ) ) { |
|
4889
|
|
|
$caption = $params['caption']; |
|
4890
|
|
|
$caption = htmlspecialchars( $caption ); |
|
4891
|
|
|
$caption = $this->replaceInternalLinks( $caption ); |
|
4892
|
|
|
$ig->setCaptionHtml( $caption ); |
|
4893
|
|
|
} |
|
4894
|
|
|
if ( isset( $params['perrow'] ) ) { |
|
4895
|
|
|
$ig->setPerRow( $params['perrow'] ); |
|
4896
|
|
|
} |
|
4897
|
|
|
if ( isset( $params['widths'] ) ) { |
|
4898
|
|
|
$ig->setWidths( $params['widths'] ); |
|
4899
|
|
|
} |
|
4900
|
|
|
if ( isset( $params['heights'] ) ) { |
|
4901
|
|
|
$ig->setHeights( $params['heights'] ); |
|
4902
|
|
|
} |
|
4903
|
|
|
$ig->setAdditionalOptions( $params ); |
|
4904
|
|
|
|
|
4905
|
|
|
Hooks::run( 'BeforeParserrenderImageGallery', [ &$this, &$ig ] ); |
|
4906
|
|
|
|
|
4907
|
|
|
$lines = StringUtils::explode( "\n", $text ); |
|
4908
|
|
|
foreach ( $lines as $line ) { |
|
4909
|
|
|
# match lines like these: |
|
4910
|
|
|
# Image:someimage.jpg|This is some image |
|
4911
|
|
|
$matches = []; |
|
4912
|
|
|
preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches ); |
|
4913
|
|
|
# Skip empty lines |
|
4914
|
|
|
if ( count( $matches ) == 0 ) { |
|
4915
|
|
|
continue; |
|
4916
|
|
|
} |
|
4917
|
|
|
|
|
4918
|
|
|
if ( strpos( $matches[0], '%' ) !== false ) { |
|
4919
|
|
|
$matches[1] = rawurldecode( $matches[1] ); |
|
4920
|
|
|
} |
|
4921
|
|
|
$title = Title::newFromText( $matches[1], NS_FILE ); |
|
4922
|
|
|
if ( is_null( $title ) ) { |
|
4923
|
|
|
# Bogus title. Ignore these so we don't bomb out later. |
|
4924
|
|
|
continue; |
|
4925
|
|
|
} |
|
4926
|
|
|
|
|
4927
|
|
|
# We need to get what handler the file uses, to figure out parameters. |
|
4928
|
|
|
# Note, a hook can overide the file name, and chose an entirely different |
|
4929
|
|
|
# file (which potentially could be of a different type and have different handler). |
|
4930
|
|
|
$options = []; |
|
4931
|
|
|
$descQuery = false; |
|
4932
|
|
|
Hooks::run( 'BeforeParserFetchFileAndTitle', |
|
4933
|
|
|
[ $this, $title, &$options, &$descQuery ] ); |
|
4934
|
|
|
# Don't register it now, as ImageGallery does that later. |
|
4935
|
|
|
$file = $this->fetchFileNoRegister( $title, $options ); |
|
4936
|
|
|
$handler = $file ? $file->getHandler() : false; |
|
4937
|
|
|
|
|
4938
|
|
|
$paramMap = [ |
|
4939
|
|
|
'img_alt' => 'gallery-internal-alt', |
|
4940
|
|
|
'img_link' => 'gallery-internal-link', |
|
4941
|
|
|
]; |
|
4942
|
|
|
if ( $handler ) { |
|
4943
|
|
|
$paramMap = $paramMap + $handler->getParamMap(); |
|
4944
|
|
|
// We don't want people to specify per-image widths. |
|
4945
|
|
|
// Additionally the width parameter would need special casing anyhow. |
|
4946
|
|
|
unset( $paramMap['img_width'] ); |
|
4947
|
|
|
} |
|
4948
|
|
|
|
|
4949
|
|
|
$mwArray = new MagicWordArray( array_keys( $paramMap ) ); |
|
4950
|
|
|
|
|
4951
|
|
|
$label = ''; |
|
4952
|
|
|
$alt = ''; |
|
4953
|
|
|
$link = ''; |
|
4954
|
|
|
$handlerOptions = []; |
|
4955
|
|
|
if ( isset( $matches[3] ) ) { |
|
4956
|
|
|
// look for an |alt= definition while trying not to break existing |
|
4957
|
|
|
// captions with multiple pipes (|) in it, until a more sensible grammar |
|
4958
|
|
|
// is defined for images in galleries |
|
4959
|
|
|
|
|
4960
|
|
|
// FIXME: Doing recursiveTagParse at this stage, and the trim before |
|
4961
|
|
|
// splitting on '|' is a bit odd, and different from makeImage. |
|
4962
|
|
|
$matches[3] = $this->recursiveTagParse( trim( $matches[3] ) ); |
|
4963
|
|
|
$parameterMatches = StringUtils::explode( '|', $matches[3] ); |
|
4964
|
|
|
|
|
4965
|
|
|
foreach ( $parameterMatches as $parameterMatch ) { |
|
4966
|
|
|
list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch ); |
|
4967
|
|
|
if ( $magicName ) { |
|
4968
|
|
|
$paramName = $paramMap[$magicName]; |
|
4969
|
|
|
|
|
4970
|
|
|
switch ( $paramName ) { |
|
4971
|
|
|
case 'gallery-internal-alt': |
|
4972
|
|
|
$alt = $this->stripAltText( $match, false ); |
|
4973
|
|
|
break; |
|
4974
|
|
|
case 'gallery-internal-link': |
|
4975
|
|
|
$linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) ); |
|
4976
|
|
|
$chars = self::EXT_LINK_URL_CLASS; |
|
4977
|
|
|
$addr = self::EXT_LINK_ADDR; |
|
4978
|
|
|
$prots = $this->mUrlProtocols; |
|
4979
|
|
|
// check to see if link matches an absolute url, if not then it must be a wiki link. |
|
4980
|
|
|
if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) { |
|
4981
|
|
|
$link = $linkValue; |
|
4982
|
|
|
} else { |
|
4983
|
|
|
$localLinkTitle = Title::newFromText( $linkValue ); |
|
4984
|
|
|
if ( $localLinkTitle !== null ) { |
|
4985
|
|
|
$link = $localLinkTitle->getLinkURL(); |
|
4986
|
|
|
} |
|
4987
|
|
|
} |
|
4988
|
|
|
break; |
|
4989
|
|
|
default: |
|
4990
|
|
|
// Must be a handler specific parameter. |
|
4991
|
|
|
if ( $handler->validateParam( $paramName, $match ) ) { |
|
4992
|
|
|
$handlerOptions[$paramName] = $match; |
|
4993
|
|
|
} else { |
|
4994
|
|
|
// Guess not, consider it as caption. |
|
4995
|
|
|
wfDebug( "$parameterMatch failed parameter validation\n" ); |
|
4996
|
|
|
$label = '|' . $parameterMatch; |
|
4997
|
|
|
} |
|
4998
|
|
|
} |
|
4999
|
|
|
|
|
5000
|
|
|
} else { |
|
5001
|
|
|
// Last pipe wins. |
|
5002
|
|
|
$label = '|' . $parameterMatch; |
|
5003
|
|
|
} |
|
5004
|
|
|
} |
|
5005
|
|
|
// Remove the pipe. |
|
5006
|
|
|
$label = substr( $label, 1 ); |
|
5007
|
|
|
} |
|
5008
|
|
|
|
|
5009
|
|
|
$ig->add( $title, $label, $alt, $link, $handlerOptions ); |
|
5010
|
|
|
} |
|
5011
|
|
|
$html = $ig->toHTML(); |
|
5012
|
|
|
Hooks::run( 'AfterParserFetchFileAndTitle', [ $this, $ig, &$html ] ); |
|
5013
|
|
|
return $html; |
|
5014
|
|
|
} |
|
5015
|
|
|
|
|
5016
|
|
|
/** |
|
5017
|
|
|
* @param MediaHandler $handler |
|
5018
|
|
|
* @return array |
|
5019
|
|
|
*/ |
|
5020
|
|
|
public function getImageParams( $handler ) { |
|
5021
|
|
|
if ( $handler ) { |
|
5022
|
|
|
$handlerClass = get_class( $handler ); |
|
5023
|
|
|
} else { |
|
5024
|
|
|
$handlerClass = ''; |
|
5025
|
|
|
} |
|
5026
|
|
|
if ( !isset( $this->mImageParams[$handlerClass] ) ) { |
|
5027
|
|
|
# Initialise static lists |
|
5028
|
|
|
static $internalParamNames = [ |
|
5029
|
|
|
'horizAlign' => [ 'left', 'right', 'center', 'none' ], |
|
5030
|
|
|
'vertAlign' => [ 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', |
|
5031
|
|
|
'bottom', 'text-bottom' ], |
|
5032
|
|
|
'frame' => [ 'thumbnail', 'manualthumb', 'framed', 'frameless', |
|
5033
|
|
|
'upright', 'border', 'link', 'alt', 'class' ], |
|
5034
|
|
|
]; |
|
5035
|
|
|
static $internalParamMap; |
|
5036
|
|
|
if ( !$internalParamMap ) { |
|
5037
|
|
|
$internalParamMap = []; |
|
5038
|
|
|
foreach ( $internalParamNames as $type => $names ) { |
|
5039
|
|
|
foreach ( $names as $name ) { |
|
5040
|
|
|
$magicName = str_replace( '-', '_', "img_$name" ); |
|
5041
|
|
|
$internalParamMap[$magicName] = [ $type, $name ]; |
|
5042
|
|
|
} |
|
5043
|
|
|
} |
|
5044
|
|
|
} |
|
5045
|
|
|
|
|
5046
|
|
|
# Add handler params |
|
5047
|
|
|
$paramMap = $internalParamMap; |
|
5048
|
|
|
if ( $handler ) { |
|
5049
|
|
|
$handlerParamMap = $handler->getParamMap(); |
|
5050
|
|
|
foreach ( $handlerParamMap as $magic => $paramName ) { |
|
5051
|
|
|
$paramMap[$magic] = [ 'handler', $paramName ]; |
|
5052
|
|
|
} |
|
5053
|
|
|
} |
|
5054
|
|
|
$this->mImageParams[$handlerClass] = $paramMap; |
|
5055
|
|
|
$this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) ); |
|
5056
|
|
|
} |
|
5057
|
|
|
return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ]; |
|
5058
|
|
|
} |
|
5059
|
|
|
|
|
5060
|
|
|
/** |
|
5061
|
|
|
* Parse image options text and use it to make an image |
|
5062
|
|
|
* |
|
5063
|
|
|
* @param Title $title |
|
5064
|
|
|
* @param string $options |
|
5065
|
|
|
* @param LinkHolderArray|bool $holders |
|
5066
|
|
|
* @return string HTML |
|
5067
|
|
|
*/ |
|
5068
|
|
|
public function makeImage( $title, $options, $holders = false ) { |
|
5069
|
|
|
# Check if the options text is of the form "options|alt text" |
|
5070
|
|
|
# Options are: |
|
5071
|
|
|
# * thumbnail make a thumbnail with enlarge-icon and caption, alignment depends on lang |
|
5072
|
|
|
# * left no resizing, just left align. label is used for alt= only |
|
5073
|
|
|
# * right same, but right aligned |
|
5074
|
|
|
# * none same, but not aligned |
|
5075
|
|
|
# * ___px scale to ___ pixels width, no aligning. e.g. use in taxobox |
|
5076
|
|
|
# * center center the image |
|
5077
|
|
|
# * frame Keep original image size, no magnify-button. |
|
5078
|
|
|
# * framed Same as "frame" |
|
5079
|
|
|
# * frameless like 'thumb' but without a frame. Keeps user preferences for width |
|
5080
|
|
|
# * upright reduce width for upright images, rounded to full __0 px |
|
5081
|
|
|
# * border draw a 1px border around the image |
|
5082
|
|
|
# * alt Text for HTML alt attribute (defaults to empty) |
|
5083
|
|
|
# * class Set a class for img node |
|
5084
|
|
|
# * link Set the target of the image link. Can be external, interwiki, or local |
|
5085
|
|
|
# vertical-align values (no % or length right now): |
|
5086
|
|
|
# * baseline |
|
5087
|
|
|
# * sub |
|
5088
|
|
|
# * super |
|
5089
|
|
|
# * top |
|
5090
|
|
|
# * text-top |
|
5091
|
|
|
# * middle |
|
5092
|
|
|
# * bottom |
|
5093
|
|
|
# * text-bottom |
|
5094
|
|
|
|
|
5095
|
|
|
$parts = StringUtils::explode( "|", $options ); |
|
5096
|
|
|
|
|
5097
|
|
|
# Give extensions a chance to select the file revision for us |
|
5098
|
|
|
$options = []; |
|
5099
|
|
|
$descQuery = false; |
|
5100
|
|
|
Hooks::run( 'BeforeParserFetchFileAndTitle', |
|
5101
|
|
|
[ $this, $title, &$options, &$descQuery ] ); |
|
5102
|
|
|
# Fetch and register the file (file title may be different via hooks) |
|
5103
|
|
|
list( $file, $title ) = $this->fetchFileAndTitle( $title, $options ); |
|
5104
|
|
|
|
|
5105
|
|
|
# Get parameter map |
|
5106
|
|
|
$handler = $file ? $file->getHandler() : false; |
|
5107
|
|
|
|
|
5108
|
|
|
list( $paramMap, $mwArray ) = $this->getImageParams( $handler ); |
|
5109
|
|
|
|
|
5110
|
|
|
if ( !$file ) { |
|
5111
|
|
|
$this->addTrackingCategory( 'broken-file-category' ); |
|
5112
|
|
|
} |
|
5113
|
|
|
|
|
5114
|
|
|
# Process the input parameters |
|
5115
|
|
|
$caption = ''; |
|
5116
|
|
|
$params = [ 'frame' => [], 'handler' => [], |
|
5117
|
|
|
'horizAlign' => [], 'vertAlign' => [] ]; |
|
5118
|
|
|
$seenformat = false; |
|
5119
|
|
|
foreach ( $parts as $part ) { |
|
5120
|
|
|
$part = trim( $part ); |
|
5121
|
|
|
list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part ); |
|
5122
|
|
|
$validated = false; |
|
5123
|
|
|
if ( isset( $paramMap[$magicName] ) ) { |
|
5124
|
|
|
list( $type, $paramName ) = $paramMap[$magicName]; |
|
5125
|
|
|
|
|
5126
|
|
|
# Special case; width and height come in one variable together |
|
5127
|
|
|
if ( $type === 'handler' && $paramName === 'width' ) { |
|
5128
|
|
|
$parsedWidthParam = $this->parseWidthParam( $value ); |
|
5129
|
|
View Code Duplication |
if ( isset( $parsedWidthParam['width'] ) ) { |
|
5130
|
|
|
$width = $parsedWidthParam['width']; |
|
5131
|
|
|
if ( $handler->validateParam( 'width', $width ) ) { |
|
5132
|
|
|
$params[$type]['width'] = $width; |
|
5133
|
|
|
$validated = true; |
|
5134
|
|
|
} |
|
5135
|
|
|
} |
|
5136
|
|
View Code Duplication |
if ( isset( $parsedWidthParam['height'] ) ) { |
|
5137
|
|
|
$height = $parsedWidthParam['height']; |
|
5138
|
|
|
if ( $handler->validateParam( 'height', $height ) ) { |
|
5139
|
|
|
$params[$type]['height'] = $height; |
|
5140
|
|
|
$validated = true; |
|
5141
|
|
|
} |
|
5142
|
|
|
} |
|
5143
|
|
|
# else no validation -- bug 13436 |
|
5144
|
|
|
} else { |
|
5145
|
|
|
if ( $type === 'handler' ) { |
|
5146
|
|
|
# Validate handler parameter |
|
5147
|
|
|
$validated = $handler->validateParam( $paramName, $value ); |
|
5148
|
|
|
} else { |
|
5149
|
|
|
# Validate internal parameters |
|
5150
|
|
|
switch ( $paramName ) { |
|
5151
|
|
|
case 'manualthumb': |
|
5152
|
|
|
case 'alt': |
|
5153
|
|
|
case 'class': |
|
5154
|
|
|
# @todo FIXME: Possibly check validity here for |
|
5155
|
|
|
# manualthumb? downstream behavior seems odd with |
|
5156
|
|
|
# missing manual thumbs. |
|
5157
|
|
|
$validated = true; |
|
5158
|
|
|
$value = $this->stripAltText( $value, $holders ); |
|
5159
|
|
|
break; |
|
5160
|
|
|
case 'link': |
|
5161
|
|
|
$chars = self::EXT_LINK_URL_CLASS; |
|
5162
|
|
|
$addr = self::EXT_LINK_ADDR; |
|
5163
|
|
|
$prots = $this->mUrlProtocols; |
|
5164
|
|
|
if ( $value === '' ) { |
|
5165
|
|
|
$paramName = 'no-link'; |
|
5166
|
|
|
$value = true; |
|
5167
|
|
|
$validated = true; |
|
5168
|
|
|
} elseif ( preg_match( "/^((?i)$prots)/", $value ) ) { |
|
5169
|
|
|
if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) { |
|
5170
|
|
|
$paramName = 'link-url'; |
|
5171
|
|
|
$this->mOutput->addExternalLink( $value ); |
|
5172
|
|
|
if ( $this->mOptions->getExternalLinkTarget() ) { |
|
5173
|
|
|
$params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget(); |
|
5174
|
|
|
} |
|
5175
|
|
|
$validated = true; |
|
5176
|
|
|
} |
|
5177
|
|
|
} else { |
|
5178
|
|
|
$linkTitle = Title::newFromText( $value ); |
|
5179
|
|
|
if ( $linkTitle ) { |
|
5180
|
|
|
$paramName = 'link-title'; |
|
5181
|
|
|
$value = $linkTitle; |
|
5182
|
|
|
$this->mOutput->addLink( $linkTitle ); |
|
5183
|
|
|
$validated = true; |
|
5184
|
|
|
} |
|
5185
|
|
|
} |
|
5186
|
|
|
break; |
|
5187
|
|
|
case 'frameless': |
|
5188
|
|
|
case 'framed': |
|
5189
|
|
|
case 'thumbnail': |
|
5190
|
|
|
// use first appearing option, discard others. |
|
5191
|
|
|
$validated = ! $seenformat; |
|
5192
|
|
|
$seenformat = true; |
|
5193
|
|
|
break; |
|
5194
|
|
|
default: |
|
5195
|
|
|
# Most other things appear to be empty or numeric... |
|
5196
|
|
|
$validated = ( $value === false || is_numeric( trim( $value ) ) ); |
|
5197
|
|
|
} |
|
5198
|
|
|
} |
|
5199
|
|
|
|
|
5200
|
|
|
if ( $validated ) { |
|
5201
|
|
|
$params[$type][$paramName] = $value; |
|
5202
|
|
|
} |
|
5203
|
|
|
} |
|
5204
|
|
|
} |
|
5205
|
|
|
if ( !$validated ) { |
|
5206
|
|
|
$caption = $part; |
|
5207
|
|
|
} |
|
5208
|
|
|
} |
|
5209
|
|
|
|
|
5210
|
|
|
# Process alignment parameters |
|
5211
|
|
|
if ( $params['horizAlign'] ) { |
|
5212
|
|
|
$params['frame']['align'] = key( $params['horizAlign'] ); |
|
5213
|
|
|
} |
|
5214
|
|
|
if ( $params['vertAlign'] ) { |
|
5215
|
|
|
$params['frame']['valign'] = key( $params['vertAlign'] ); |
|
5216
|
|
|
} |
|
5217
|
|
|
|
|
5218
|
|
|
$params['frame']['caption'] = $caption; |
|
5219
|
|
|
|
|
5220
|
|
|
# Will the image be presented in a frame, with the caption below? |
|
5221
|
|
|
$imageIsFramed = isset( $params['frame']['frame'] ) |
|
5222
|
|
|
|| isset( $params['frame']['framed'] ) |
|
5223
|
|
|
|| isset( $params['frame']['thumbnail'] ) |
|
5224
|
|
|
|| isset( $params['frame']['manualthumb'] ); |
|
5225
|
|
|
|
|
5226
|
|
|
# In the old days, [[Image:Foo|text...]] would set alt text. Later it |
|
5227
|
|
|
# came to also set the caption, ordinary text after the image -- which |
|
5228
|
|
|
# makes no sense, because that just repeats the text multiple times in |
|
5229
|
|
|
# screen readers. It *also* came to set the title attribute. |
|
5230
|
|
|
# Now that we have an alt attribute, we should not set the alt text to |
|
5231
|
|
|
# equal the caption: that's worse than useless, it just repeats the |
|
5232
|
|
|
# text. This is the framed/thumbnail case. If there's no caption, we |
|
5233
|
|
|
# use the unnamed parameter for alt text as well, just for the time be- |
|
5234
|
|
|
# ing, if the unnamed param is set and the alt param is not. |
|
5235
|
|
|
# For the future, we need to figure out if we want to tweak this more, |
|
5236
|
|
|
# e.g., introducing a title= parameter for the title; ignoring the un- |
|
5237
|
|
|
# named parameter entirely for images without a caption; adding an ex- |
|
5238
|
|
|
# plicit caption= parameter and preserving the old magic unnamed para- |
|
5239
|
|
|
# meter for BC; ... |
|
5240
|
|
|
if ( $imageIsFramed ) { # Framed image |
|
5241
|
|
|
if ( $caption === '' && !isset( $params['frame']['alt'] ) ) { |
|
5242
|
|
|
# No caption or alt text, add the filename as the alt text so |
|
5243
|
|
|
# that screen readers at least get some description of the image |
|
5244
|
|
|
$params['frame']['alt'] = $title->getText(); |
|
5245
|
|
|
} |
|
5246
|
|
|
# Do not set $params['frame']['title'] because tooltips don't make sense |
|
5247
|
|
|
# for framed images |
|
5248
|
|
|
} else { # Inline image |
|
5249
|
|
|
if ( !isset( $params['frame']['alt'] ) ) { |
|
5250
|
|
|
# No alt text, use the "caption" for the alt text |
|
5251
|
|
|
if ( $caption !== '' ) { |
|
5252
|
|
|
$params['frame']['alt'] = $this->stripAltText( $caption, $holders ); |
|
5253
|
|
|
} else { |
|
5254
|
|
|
# No caption, fall back to using the filename for the |
|
5255
|
|
|
# alt text |
|
5256
|
|
|
$params['frame']['alt'] = $title->getText(); |
|
5257
|
|
|
} |
|
5258
|
|
|
} |
|
5259
|
|
|
# Use the "caption" for the tooltip text |
|
5260
|
|
|
$params['frame']['title'] = $this->stripAltText( $caption, $holders ); |
|
5261
|
|
|
} |
|
5262
|
|
|
|
|
5263
|
|
|
Hooks::run( 'ParserMakeImageParams', [ $title, $file, &$params, $this ] ); |
|
5264
|
|
|
|
|
5265
|
|
|
# Linker does the rest |
|
5266
|
|
|
$time = isset( $options['time'] ) ? $options['time'] : false; |
|
5267
|
|
|
$ret = Linker::makeImageLink( $this, $title, $file, $params['frame'], $params['handler'], |
|
5268
|
|
|
$time, $descQuery, $this->mOptions->getThumbSize() ); |
|
5269
|
|
|
|
|
5270
|
|
|
# Give the handler a chance to modify the parser object |
|
5271
|
|
|
if ( $handler ) { |
|
5272
|
|
|
$handler->parserTransformHook( $this, $file ); |
|
5273
|
|
|
} |
|
5274
|
|
|
|
|
5275
|
|
|
return $ret; |
|
5276
|
|
|
} |
|
5277
|
|
|
|
|
5278
|
|
|
/** |
|
5279
|
|
|
* @param string $caption |
|
5280
|
|
|
* @param LinkHolderArray|bool $holders |
|
5281
|
|
|
* @return mixed|string |
|
5282
|
|
|
*/ |
|
5283
|
|
|
protected function stripAltText( $caption, $holders ) { |
|
5284
|
|
|
# Strip bad stuff out of the title (tooltip). We can't just use |
|
5285
|
|
|
# replaceLinkHoldersText() here, because if this function is called |
|
5286
|
|
|
# from replaceInternalLinks2(), mLinkHolders won't be up-to-date. |
|
5287
|
|
|
if ( $holders ) { |
|
5288
|
|
|
$tooltip = $holders->replaceText( $caption ); |
|
|
|
|
|
|
5289
|
|
|
} else { |
|
5290
|
|
|
$tooltip = $this->replaceLinkHoldersText( $caption ); |
|
5291
|
|
|
} |
|
5292
|
|
|
|
|
5293
|
|
|
# make sure there are no placeholders in thumbnail attributes |
|
5294
|
|
|
# that are later expanded to html- so expand them now and |
|
5295
|
|
|
# remove the tags |
|
5296
|
|
|
$tooltip = $this->mStripState->unstripBoth( $tooltip ); |
|
5297
|
|
|
$tooltip = Sanitizer::stripAllTags( $tooltip ); |
|
5298
|
|
|
|
|
5299
|
|
|
return $tooltip; |
|
5300
|
|
|
} |
|
5301
|
|
|
|
|
5302
|
|
|
/** |
|
5303
|
|
|
* Set a flag in the output object indicating that the content is dynamic and |
|
5304
|
|
|
* shouldn't be cached. |
|
5305
|
|
|
*/ |
|
5306
|
|
|
public function disableCache() { |
|
5307
|
|
|
wfDebug( "Parser output marked as uncacheable.\n" ); |
|
5308
|
|
|
if ( !$this->mOutput ) { |
|
5309
|
|
|
throw new MWException( __METHOD__ . |
|
5310
|
|
|
" can only be called when actually parsing something" ); |
|
5311
|
|
|
} |
|
5312
|
|
|
$this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency |
|
5313
|
|
|
} |
|
5314
|
|
|
|
|
5315
|
|
|
/** |
|
5316
|
|
|
* Callback from the Sanitizer for expanding items found in HTML attribute |
|
5317
|
|
|
* values, so they can be safely tested and escaped. |
|
5318
|
|
|
* |
|
5319
|
|
|
* @param string $text |
|
5320
|
|
|
* @param bool|PPFrame $frame |
|
5321
|
|
|
* @return string |
|
5322
|
|
|
*/ |
|
5323
|
|
|
public function attributeStripCallback( &$text, $frame = false ) { |
|
5324
|
|
|
$text = $this->replaceVariables( $text, $frame ); |
|
5325
|
|
|
$text = $this->mStripState->unstripBoth( $text ); |
|
5326
|
|
|
return $text; |
|
5327
|
|
|
} |
|
5328
|
|
|
|
|
5329
|
|
|
/** |
|
5330
|
|
|
* Accessor |
|
5331
|
|
|
* |
|
5332
|
|
|
* @return array |
|
5333
|
|
|
*/ |
|
5334
|
|
|
public function getTags() { |
|
5335
|
|
|
return array_merge( |
|
5336
|
|
|
array_keys( $this->mTransparentTagHooks ), |
|
5337
|
|
|
array_keys( $this->mTagHooks ), |
|
5338
|
|
|
array_keys( $this->mFunctionTagHooks ) |
|
5339
|
|
|
); |
|
5340
|
|
|
} |
|
5341
|
|
|
|
|
5342
|
|
|
/** |
|
5343
|
|
|
* Replace transparent tags in $text with the values given by the callbacks. |
|
5344
|
|
|
* |
|
5345
|
|
|
* Transparent tag hooks are like regular XML-style tag hooks, except they |
|
5346
|
|
|
* operate late in the transformation sequence, on HTML instead of wikitext. |
|
5347
|
|
|
* |
|
5348
|
|
|
* @param string $text |
|
5349
|
|
|
* |
|
5350
|
|
|
* @return string |
|
5351
|
|
|
*/ |
|
5352
|
|
|
public function replaceTransparentTags( $text ) { |
|
5353
|
|
|
$matches = []; |
|
5354
|
|
|
$elements = array_keys( $this->mTransparentTagHooks ); |
|
5355
|
|
|
$text = self::extractTagsAndParams( $elements, $text, $matches ); |
|
5356
|
|
|
$replacements = []; |
|
5357
|
|
|
|
|
5358
|
|
|
foreach ( $matches as $marker => $data ) { |
|
5359
|
|
|
list( $element, $content, $params, $tag ) = $data; |
|
5360
|
|
|
$tagName = strtolower( $element ); |
|
5361
|
|
|
if ( isset( $this->mTransparentTagHooks[$tagName] ) ) { |
|
5362
|
|
|
$output = call_user_func_array( |
|
5363
|
|
|
$this->mTransparentTagHooks[$tagName], |
|
5364
|
|
|
[ $content, $params, $this ] |
|
5365
|
|
|
); |
|
5366
|
|
|
} else { |
|
5367
|
|
|
$output = $tag; |
|
5368
|
|
|
} |
|
5369
|
|
|
$replacements[$marker] = $output; |
|
5370
|
|
|
} |
|
5371
|
|
|
return strtr( $text, $replacements ); |
|
5372
|
|
|
} |
|
5373
|
|
|
|
|
5374
|
|
|
/** |
|
5375
|
|
|
* Break wikitext input into sections, and either pull or replace |
|
5376
|
|
|
* some particular section's text. |
|
5377
|
|
|
* |
|
5378
|
|
|
* External callers should use the getSection and replaceSection methods. |
|
5379
|
|
|
* |
|
5380
|
|
|
* @param string $text Page wikitext |
|
5381
|
|
|
* @param string|number $sectionId A section identifier string of the form: |
|
5382
|
|
|
* "<flag1> - <flag2> - ... - <section number>" |
|
5383
|
|
|
* |
|
5384
|
|
|
* Currently the only recognised flag is "T", which means the target section number |
|
5385
|
|
|
* was derived during a template inclusion parse, in other words this is a template |
|
5386
|
|
|
* section edit link. If no flags are given, it was an ordinary section edit link. |
|
5387
|
|
|
* This flag is required to avoid a section numbering mismatch when a section is |
|
5388
|
|
|
* enclosed by "<includeonly>" (bug 6563). |
|
5389
|
|
|
* |
|
5390
|
|
|
* The section number 0 pulls the text before the first heading; other numbers will |
|
5391
|
|
|
* pull the given section along with its lower-level subsections. If the section is |
|
5392
|
|
|
* not found, $mode=get will return $newtext, and $mode=replace will return $text. |
|
5393
|
|
|
* |
|
5394
|
|
|
* Section 0 is always considered to exist, even if it only contains the empty |
|
5395
|
|
|
* string. If $text is the empty string and section 0 is replaced, $newText is |
|
5396
|
|
|
* returned. |
|
5397
|
|
|
* |
|
5398
|
|
|
* @param string $mode One of "get" or "replace" |
|
5399
|
|
|
* @param string $newText Replacement text for section data. |
|
5400
|
|
|
* @return string For "get", the extracted section text. |
|
5401
|
|
|
* for "replace", the whole page with the section replaced. |
|
5402
|
|
|
*/ |
|
5403
|
|
|
private function extractSections( $text, $sectionId, $mode, $newText = '' ) { |
|
5404
|
|
|
global $wgTitle; # not generally used but removes an ugly failure mode |
|
5405
|
|
|
|
|
5406
|
|
|
$magicScopeVariable = $this->lock(); |
|
|
|
|
|
|
5407
|
|
|
$this->startParse( $wgTitle, new ParserOptions, self::OT_PLAIN, true ); |
|
5408
|
|
|
$outText = ''; |
|
5409
|
|
|
$frame = $this->getPreprocessor()->newFrame(); |
|
5410
|
|
|
|
|
5411
|
|
|
# Process section extraction flags |
|
5412
|
|
|
$flags = 0; |
|
5413
|
|
|
$sectionParts = explode( '-', $sectionId ); |
|
5414
|
|
|
$sectionIndex = array_pop( $sectionParts ); |
|
5415
|
|
|
foreach ( $sectionParts as $part ) { |
|
5416
|
|
|
if ( $part === 'T' ) { |
|
5417
|
|
|
$flags |= self::PTD_FOR_INCLUSION; |
|
5418
|
|
|
} |
|
5419
|
|
|
} |
|
5420
|
|
|
|
|
5421
|
|
|
# Check for empty input |
|
5422
|
|
|
if ( strval( $text ) === '' ) { |
|
5423
|
|
|
# Only sections 0 and T-0 exist in an empty document |
|
5424
|
|
|
if ( $sectionIndex == 0 ) { |
|
5425
|
|
|
if ( $mode === 'get' ) { |
|
5426
|
|
|
return ''; |
|
5427
|
|
|
} else { |
|
5428
|
|
|
return $newText; |
|
5429
|
|
|
} |
|
5430
|
|
|
} else { |
|
5431
|
|
|
if ( $mode === 'get' ) { |
|
5432
|
|
|
return $newText; |
|
5433
|
|
|
} else { |
|
5434
|
|
|
return $text; |
|
5435
|
|
|
} |
|
5436
|
|
|
} |
|
5437
|
|
|
} |
|
5438
|
|
|
|
|
5439
|
|
|
# Preprocess the text |
|
5440
|
|
|
$root = $this->preprocessToDom( $text, $flags ); |
|
5441
|
|
|
|
|
5442
|
|
|
# <h> nodes indicate section breaks |
|
5443
|
|
|
# They can only occur at the top level, so we can find them by iterating the root's children |
|
5444
|
|
|
$node = $root->getFirstChild(); |
|
5445
|
|
|
|
|
5446
|
|
|
# Find the target section |
|
5447
|
|
|
if ( $sectionIndex == 0 ) { |
|
5448
|
|
|
# Section zero doesn't nest, level=big |
|
5449
|
|
|
$targetLevel = 1000; |
|
5450
|
|
|
} else { |
|
5451
|
|
|
while ( $node ) { |
|
5452
|
|
View Code Duplication |
if ( $node->getName() === 'h' ) { |
|
5453
|
|
|
$bits = $node->splitHeading(); |
|
5454
|
|
|
if ( $bits['i'] == $sectionIndex ) { |
|
5455
|
|
|
$targetLevel = $bits['level']; |
|
5456
|
|
|
break; |
|
5457
|
|
|
} |
|
5458
|
|
|
} |
|
5459
|
|
|
if ( $mode === 'replace' ) { |
|
5460
|
|
|
$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG ); |
|
5461
|
|
|
} |
|
5462
|
|
|
$node = $node->getNextSibling(); |
|
5463
|
|
|
} |
|
5464
|
|
|
} |
|
5465
|
|
|
|
|
5466
|
|
|
if ( !$node ) { |
|
5467
|
|
|
# Not found |
|
5468
|
|
|
if ( $mode === 'get' ) { |
|
5469
|
|
|
return $newText; |
|
5470
|
|
|
} else { |
|
5471
|
|
|
return $text; |
|
5472
|
|
|
} |
|
5473
|
|
|
} |
|
5474
|
|
|
|
|
5475
|
|
|
# Find the end of the section, including nested sections |
|
5476
|
|
|
do { |
|
5477
|
|
View Code Duplication |
if ( $node->getName() === 'h' ) { |
|
5478
|
|
|
$bits = $node->splitHeading(); |
|
5479
|
|
|
$curLevel = $bits['level']; |
|
5480
|
|
|
if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) { |
|
|
|
|
|
|
5481
|
|
|
break; |
|
5482
|
|
|
} |
|
5483
|
|
|
} |
|
5484
|
|
|
if ( $mode === 'get' ) { |
|
5485
|
|
|
$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG ); |
|
5486
|
|
|
} |
|
5487
|
|
|
$node = $node->getNextSibling(); |
|
5488
|
|
|
} while ( $node ); |
|
5489
|
|
|
|
|
5490
|
|
|
# Write out the remainder (in replace mode only) |
|
5491
|
|
|
if ( $mode === 'replace' ) { |
|
5492
|
|
|
# Output the replacement text |
|
5493
|
|
|
# Add two newlines on -- trailing whitespace in $newText is conventionally |
|
5494
|
|
|
# stripped by the editor, so we need both newlines to restore the paragraph gap |
|
5495
|
|
|
# Only add trailing whitespace if there is newText |
|
5496
|
|
|
if ( $newText != "" ) { |
|
5497
|
|
|
$outText .= $newText . "\n\n"; |
|
5498
|
|
|
} |
|
5499
|
|
|
|
|
5500
|
|
|
while ( $node ) { |
|
5501
|
|
|
$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG ); |
|
5502
|
|
|
$node = $node->getNextSibling(); |
|
5503
|
|
|
} |
|
5504
|
|
|
} |
|
5505
|
|
|
|
|
5506
|
|
|
if ( is_string( $outText ) ) { |
|
5507
|
|
|
# Re-insert stripped tags |
|
5508
|
|
|
$outText = rtrim( $this->mStripState->unstripBoth( $outText ) ); |
|
5509
|
|
|
} |
|
5510
|
|
|
|
|
5511
|
|
|
return $outText; |
|
5512
|
|
|
} |
|
5513
|
|
|
|
|
5514
|
|
|
/** |
|
5515
|
|
|
* This function returns the text of a section, specified by a number ($section). |
|
5516
|
|
|
* A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or |
|
5517
|
|
|
* the first section before any such heading (section 0). |
|
5518
|
|
|
* |
|
5519
|
|
|
* If a section contains subsections, these are also returned. |
|
5520
|
|
|
* |
|
5521
|
|
|
* @param string $text Text to look in |
|
5522
|
|
|
* @param string|number $sectionId Section identifier as a number or string |
|
5523
|
|
|
* (e.g. 0, 1 or 'T-1'). |
|
5524
|
|
|
* @param string $defaultText Default to return if section is not found |
|
5525
|
|
|
* |
|
5526
|
|
|
* @return string Text of the requested section |
|
5527
|
|
|
*/ |
|
5528
|
|
|
public function getSection( $text, $sectionId, $defaultText = '' ) { |
|
5529
|
|
|
return $this->extractSections( $text, $sectionId, 'get', $defaultText ); |
|
5530
|
|
|
} |
|
5531
|
|
|
|
|
5532
|
|
|
/** |
|
5533
|
|
|
* This function returns $oldtext after the content of the section |
|
5534
|
|
|
* specified by $section has been replaced with $text. If the target |
|
5535
|
|
|
* section does not exist, $oldtext is returned unchanged. |
|
5536
|
|
|
* |
|
5537
|
|
|
* @param string $oldText Former text of the article |
|
5538
|
|
|
* @param string|number $sectionId Section identifier as a number or string |
|
5539
|
|
|
* (e.g. 0, 1 or 'T-1'). |
|
5540
|
|
|
* @param string $newText Replacing text |
|
5541
|
|
|
* |
|
5542
|
|
|
* @return string Modified text |
|
5543
|
|
|
*/ |
|
5544
|
|
|
public function replaceSection( $oldText, $sectionId, $newText ) { |
|
5545
|
|
|
return $this->extractSections( $oldText, $sectionId, 'replace', $newText ); |
|
5546
|
|
|
} |
|
5547
|
|
|
|
|
5548
|
|
|
/** |
|
5549
|
|
|
* Get the ID of the revision we are parsing |
|
5550
|
|
|
* |
|
5551
|
|
|
* @return int|null |
|
5552
|
|
|
*/ |
|
5553
|
|
|
public function getRevisionId() { |
|
5554
|
|
|
return $this->mRevisionId; |
|
5555
|
|
|
} |
|
5556
|
|
|
|
|
5557
|
|
|
/** |
|
5558
|
|
|
* Get the revision object for $this->mRevisionId |
|
5559
|
|
|
* |
|
5560
|
|
|
* @return Revision|null Either a Revision object or null |
|
5561
|
|
|
* @since 1.23 (public since 1.23) |
|
5562
|
|
|
*/ |
|
5563
|
|
|
public function getRevisionObject() { |
|
5564
|
|
|
if ( !is_null( $this->mRevisionObject ) ) { |
|
5565
|
|
|
return $this->mRevisionObject; |
|
5566
|
|
|
} |
|
5567
|
|
|
if ( is_null( $this->mRevisionId ) ) { |
|
5568
|
|
|
return null; |
|
5569
|
|
|
} |
|
5570
|
|
|
|
|
5571
|
|
|
$rev = call_user_func( |
|
5572
|
|
|
$this->mOptions->getCurrentRevisionCallback(), $this->getTitle(), $this |
|
5573
|
|
|
); |
|
5574
|
|
|
|
|
5575
|
|
|
# If the parse is for a new revision, then the callback should have |
|
5576
|
|
|
# already been set to force the object and should match mRevisionId. |
|
5577
|
|
|
# If not, try to fetch by mRevisionId for sanity. |
|
5578
|
|
|
if ( $rev && $rev->getId() != $this->mRevisionId ) { |
|
5579
|
|
|
$rev = Revision::newFromId( $this->mRevisionId ); |
|
5580
|
|
|
} |
|
5581
|
|
|
|
|
5582
|
|
|
$this->mRevisionObject = $rev; |
|
5583
|
|
|
|
|
5584
|
|
|
return $this->mRevisionObject; |
|
5585
|
|
|
} |
|
5586
|
|
|
|
|
5587
|
|
|
/** |
|
5588
|
|
|
* Get the timestamp associated with the current revision, adjusted for |
|
5589
|
|
|
* the default server-local timestamp |
|
5590
|
|
|
* @return string |
|
5591
|
|
|
*/ |
|
5592
|
|
|
public function getRevisionTimestamp() { |
|
5593
|
|
|
if ( is_null( $this->mRevisionTimestamp ) ) { |
|
5594
|
|
|
global $wgContLang; |
|
5595
|
|
|
|
|
5596
|
|
|
$revObject = $this->getRevisionObject(); |
|
5597
|
|
|
$timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow(); |
|
5598
|
|
|
|
|
5599
|
|
|
# The cryptic '' timezone parameter tells to use the site-default |
|
5600
|
|
|
# timezone offset instead of the user settings. |
|
5601
|
|
|
# Since this value will be saved into the parser cache, served |
|
5602
|
|
|
# to other users, and potentially even used inside links and such, |
|
5603
|
|
|
# it needs to be consistent for all visitors. |
|
5604
|
|
|
$this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' ); |
|
5605
|
|
|
|
|
5606
|
|
|
} |
|
5607
|
|
|
return $this->mRevisionTimestamp; |
|
5608
|
|
|
} |
|
5609
|
|
|
|
|
5610
|
|
|
/** |
|
5611
|
|
|
* Get the name of the user that edited the last revision |
|
5612
|
|
|
* |
|
5613
|
|
|
* @return string User name |
|
5614
|
|
|
*/ |
|
5615
|
|
|
public function getRevisionUser() { |
|
5616
|
|
|
if ( is_null( $this->mRevisionUser ) ) { |
|
5617
|
|
|
$revObject = $this->getRevisionObject(); |
|
5618
|
|
|
|
|
5619
|
|
|
# if this template is subst: the revision id will be blank, |
|
5620
|
|
|
# so just use the current user's name |
|
5621
|
|
|
if ( $revObject ) { |
|
5622
|
|
|
$this->mRevisionUser = $revObject->getUserText(); |
|
5623
|
|
|
} elseif ( $this->ot['wiki'] || $this->mOptions->getIsPreview() ) { |
|
5624
|
|
|
$this->mRevisionUser = $this->getUser()->getName(); |
|
5625
|
|
|
} |
|
5626
|
|
|
} |
|
5627
|
|
|
return $this->mRevisionUser; |
|
5628
|
|
|
} |
|
5629
|
|
|
|
|
5630
|
|
|
/** |
|
5631
|
|
|
* Get the size of the revision |
|
5632
|
|
|
* |
|
5633
|
|
|
* @return int|null Revision size |
|
5634
|
|
|
*/ |
|
5635
|
|
|
public function getRevisionSize() { |
|
5636
|
|
|
if ( is_null( $this->mRevisionSize ) ) { |
|
5637
|
|
|
$revObject = $this->getRevisionObject(); |
|
5638
|
|
|
|
|
5639
|
|
|
# if this variable is subst: the revision id will be blank, |
|
5640
|
|
|
# so just use the parser input size, because the own substituation |
|
5641
|
|
|
# will change the size. |
|
5642
|
|
|
if ( $revObject ) { |
|
5643
|
|
|
$this->mRevisionSize = $revObject->getSize(); |
|
5644
|
|
|
} else { |
|
5645
|
|
|
$this->mRevisionSize = $this->mInputSize; |
|
5646
|
|
|
} |
|
5647
|
|
|
} |
|
5648
|
|
|
return $this->mRevisionSize; |
|
5649
|
|
|
} |
|
5650
|
|
|
|
|
5651
|
|
|
/** |
|
5652
|
|
|
* Mutator for $mDefaultSort |
|
5653
|
|
|
* |
|
5654
|
|
|
* @param string $sort New value |
|
5655
|
|
|
*/ |
|
5656
|
|
|
public function setDefaultSort( $sort ) { |
|
5657
|
|
|
$this->mDefaultSort = $sort; |
|
5658
|
|
|
$this->mOutput->setProperty( 'defaultsort', $sort ); |
|
5659
|
|
|
} |
|
5660
|
|
|
|
|
5661
|
|
|
/** |
|
5662
|
|
|
* Accessor for $mDefaultSort |
|
5663
|
|
|
* Will use the empty string if none is set. |
|
5664
|
|
|
* |
|
5665
|
|
|
* This value is treated as a prefix, so the |
|
5666
|
|
|
* empty string is equivalent to sorting by |
|
5667
|
|
|
* page name. |
|
5668
|
|
|
* |
|
5669
|
|
|
* @return string |
|
5670
|
|
|
*/ |
|
5671
|
|
|
public function getDefaultSort() { |
|
5672
|
|
|
if ( $this->mDefaultSort !== false ) { |
|
5673
|
|
|
return $this->mDefaultSort; |
|
5674
|
|
|
} else { |
|
5675
|
|
|
return ''; |
|
5676
|
|
|
} |
|
5677
|
|
|
} |
|
5678
|
|
|
|
|
5679
|
|
|
/** |
|
5680
|
|
|
* Accessor for $mDefaultSort |
|
5681
|
|
|
* Unlike getDefaultSort(), will return false if none is set |
|
5682
|
|
|
* |
|
5683
|
|
|
* @return string|bool |
|
5684
|
|
|
*/ |
|
5685
|
|
|
public function getCustomDefaultSort() { |
|
5686
|
|
|
return $this->mDefaultSort; |
|
5687
|
|
|
} |
|
5688
|
|
|
|
|
5689
|
|
|
/** |
|
5690
|
|
|
* Try to guess the section anchor name based on a wikitext fragment |
|
5691
|
|
|
* presumably extracted from a heading, for example "Header" from |
|
5692
|
|
|
* "== Header ==". |
|
5693
|
|
|
* |
|
5694
|
|
|
* @param string $text |
|
5695
|
|
|
* |
|
5696
|
|
|
* @return string |
|
5697
|
|
|
*/ |
|
5698
|
|
|
public function guessSectionNameFromWikiText( $text ) { |
|
5699
|
|
|
# Strip out wikitext links(they break the anchor) |
|
5700
|
|
|
$text = $this->stripSectionName( $text ); |
|
5701
|
|
|
$text = Sanitizer::normalizeSectionNameWhitespace( $text ); |
|
5702
|
|
|
return '#' . Sanitizer::escapeId( $text, 'noninitial' ); |
|
5703
|
|
|
} |
|
5704
|
|
|
|
|
5705
|
|
|
/** |
|
5706
|
|
|
* Same as guessSectionNameFromWikiText(), but produces legacy anchors |
|
5707
|
|
|
* instead. For use in redirects, since IE6 interprets Redirect: headers |
|
5708
|
|
|
* as something other than UTF-8 (apparently?), resulting in breakage. |
|
5709
|
|
|
* |
|
5710
|
|
|
* @param string $text The section name |
|
5711
|
|
|
* @return string An anchor |
|
5712
|
|
|
*/ |
|
5713
|
|
|
public function guessLegacySectionNameFromWikiText( $text ) { |
|
5714
|
|
|
# Strip out wikitext links(they break the anchor) |
|
5715
|
|
|
$text = $this->stripSectionName( $text ); |
|
5716
|
|
|
$text = Sanitizer::normalizeSectionNameWhitespace( $text ); |
|
5717
|
|
|
return '#' . Sanitizer::escapeId( $text, [ 'noninitial', 'legacy' ] ); |
|
5718
|
|
|
} |
|
5719
|
|
|
|
|
5720
|
|
|
/** |
|
5721
|
|
|
* Strips a text string of wikitext for use in a section anchor |
|
5722
|
|
|
* |
|
5723
|
|
|
* Accepts a text string and then removes all wikitext from the |
|
5724
|
|
|
* string and leaves only the resultant text (i.e. the result of |
|
5725
|
|
|
* [[User:WikiSysop|Sysop]] would be "Sysop" and the result of |
|
5726
|
|
|
* [[User:WikiSysop]] would be "User:WikiSysop") - this is intended |
|
5727
|
|
|
* to create valid section anchors by mimicing the output of the |
|
5728
|
|
|
* parser when headings are parsed. |
|
5729
|
|
|
* |
|
5730
|
|
|
* @param string $text Text string to be stripped of wikitext |
|
5731
|
|
|
* for use in a Section anchor |
|
5732
|
|
|
* @return string Filtered text string |
|
5733
|
|
|
*/ |
|
5734
|
|
|
public function stripSectionName( $text ) { |
|
5735
|
|
|
# Strip internal link markup |
|
5736
|
|
|
$text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text ); |
|
5737
|
|
|
$text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text ); |
|
5738
|
|
|
|
|
5739
|
|
|
# Strip external link markup |
|
5740
|
|
|
# @todo FIXME: Not tolerant to blank link text |
|
5741
|
|
|
# I.E. [https://www.mediawiki.org] will render as [1] or something depending |
|
5742
|
|
|
# on how many empty links there are on the page - need to figure that out. |
|
5743
|
|
|
$text = preg_replace( '/\[(?i:' . $this->mUrlProtocols . ')([^ ]+?) ([^[]+)\]/', '$2', $text ); |
|
5744
|
|
|
|
|
5745
|
|
|
# Parse wikitext quotes (italics & bold) |
|
5746
|
|
|
$text = $this->doQuotes( $text ); |
|
5747
|
|
|
|
|
5748
|
|
|
# Strip HTML tags |
|
5749
|
|
|
$text = StringUtils::delimiterReplace( '<', '>', '', $text ); |
|
5750
|
|
|
return $text; |
|
5751
|
|
|
} |
|
5752
|
|
|
|
|
5753
|
|
|
/** |
|
5754
|
|
|
* strip/replaceVariables/unstrip for preprocessor regression testing |
|
5755
|
|
|
* |
|
5756
|
|
|
* @param string $text |
|
5757
|
|
|
* @param Title $title |
|
5758
|
|
|
* @param ParserOptions $options |
|
5759
|
|
|
* @param int $outputType |
|
5760
|
|
|
* |
|
5761
|
|
|
* @return string |
|
5762
|
|
|
*/ |
|
5763
|
|
|
public function testSrvus( $text, Title $title, ParserOptions $options, |
|
5764
|
|
|
$outputType = self::OT_HTML |
|
5765
|
|
|
) { |
|
5766
|
|
|
$magicScopeVariable = $this->lock(); |
|
|
|
|
|
|
5767
|
|
|
$this->startParse( $title, $options, $outputType, true ); |
|
5768
|
|
|
|
|
5769
|
|
|
$text = $this->replaceVariables( $text ); |
|
5770
|
|
|
$text = $this->mStripState->unstripBoth( $text ); |
|
5771
|
|
|
$text = Sanitizer::removeHTMLtags( $text ); |
|
5772
|
|
|
return $text; |
|
5773
|
|
|
} |
|
5774
|
|
|
|
|
5775
|
|
|
/** |
|
5776
|
|
|
* @param string $text |
|
5777
|
|
|
* @param Title $title |
|
5778
|
|
|
* @param ParserOptions $options |
|
5779
|
|
|
* @return string |
|
5780
|
|
|
*/ |
|
5781
|
|
|
public function testPst( $text, Title $title, ParserOptions $options ) { |
|
5782
|
|
|
return $this->preSaveTransform( $text, $title, $options->getUser(), $options ); |
|
5783
|
|
|
} |
|
5784
|
|
|
|
|
5785
|
|
|
/** |
|
5786
|
|
|
* @param string $text |
|
5787
|
|
|
* @param Title $title |
|
5788
|
|
|
* @param ParserOptions $options |
|
5789
|
|
|
* @return string |
|
5790
|
|
|
*/ |
|
5791
|
|
|
public function testPreprocess( $text, Title $title, ParserOptions $options ) { |
|
5792
|
|
|
return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS ); |
|
5793
|
|
|
} |
|
5794
|
|
|
|
|
5795
|
|
|
/** |
|
5796
|
|
|
* Call a callback function on all regions of the given text that are not |
|
5797
|
|
|
* inside strip markers, and replace those regions with the return value |
|
5798
|
|
|
* of the callback. For example, with input: |
|
5799
|
|
|
* |
|
5800
|
|
|
* aaa<MARKER>bbb |
|
5801
|
|
|
* |
|
5802
|
|
|
* This will call the callback function twice, with 'aaa' and 'bbb'. Those |
|
5803
|
|
|
* two strings will be replaced with the value returned by the callback in |
|
5804
|
|
|
* each case. |
|
5805
|
|
|
* |
|
5806
|
|
|
* @param string $s |
|
5807
|
|
|
* @param callable $callback |
|
5808
|
|
|
* |
|
5809
|
|
|
* @return string |
|
5810
|
|
|
*/ |
|
5811
|
|
|
public function markerSkipCallback( $s, $callback ) { |
|
5812
|
|
|
$i = 0; |
|
5813
|
|
|
$out = ''; |
|
5814
|
|
|
while ( $i < strlen( $s ) ) { |
|
5815
|
|
|
$markerStart = strpos( $s, self::MARKER_PREFIX, $i ); |
|
5816
|
|
|
if ( $markerStart === false ) { |
|
5817
|
|
|
$out .= call_user_func( $callback, substr( $s, $i ) ); |
|
5818
|
|
|
break; |
|
5819
|
|
|
} else { |
|
5820
|
|
|
$out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) ); |
|
5821
|
|
|
$markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart ); |
|
5822
|
|
|
if ( $markerEnd === false ) { |
|
5823
|
|
|
$out .= substr( $s, $markerStart ); |
|
5824
|
|
|
break; |
|
5825
|
|
|
} else { |
|
5826
|
|
|
$markerEnd += strlen( self::MARKER_SUFFIX ); |
|
5827
|
|
|
$out .= substr( $s, $markerStart, $markerEnd - $markerStart ); |
|
5828
|
|
|
$i = $markerEnd; |
|
5829
|
|
|
} |
|
5830
|
|
|
} |
|
5831
|
|
|
} |
|
5832
|
|
|
return $out; |
|
5833
|
|
|
} |
|
5834
|
|
|
|
|
5835
|
|
|
/** |
|
5836
|
|
|
* Remove any strip markers found in the given text. |
|
5837
|
|
|
* |
|
5838
|
|
|
* @param string $text Input string |
|
5839
|
|
|
* @return string |
|
5840
|
|
|
*/ |
|
5841
|
|
|
public function killMarkers( $text ) { |
|
5842
|
|
|
return $this->mStripState->killMarkers( $text ); |
|
5843
|
|
|
} |
|
5844
|
|
|
|
|
5845
|
|
|
/** |
|
5846
|
|
|
* Save the parser state required to convert the given half-parsed text to |
|
5847
|
|
|
* HTML. "Half-parsed" in this context means the output of |
|
5848
|
|
|
* recursiveTagParse() or internalParse(). This output has strip markers |
|
5849
|
|
|
* from replaceVariables (extensionSubstitution() etc.), and link |
|
5850
|
|
|
* placeholders from replaceLinkHolders(). |
|
5851
|
|
|
* |
|
5852
|
|
|
* Returns an array which can be serialized and stored persistently. This |
|
5853
|
|
|
* array can later be loaded into another parser instance with |
|
5854
|
|
|
* unserializeHalfParsedText(). The text can then be safely incorporated into |
|
5855
|
|
|
* the return value of a parser hook. |
|
5856
|
|
|
* |
|
5857
|
|
|
* @param string $text |
|
5858
|
|
|
* |
|
5859
|
|
|
* @return array |
|
5860
|
|
|
*/ |
|
5861
|
|
|
public function serializeHalfParsedText( $text ) { |
|
5862
|
|
|
$data = [ |
|
5863
|
|
|
'text' => $text, |
|
5864
|
|
|
'version' => self::HALF_PARSED_VERSION, |
|
5865
|
|
|
'stripState' => $this->mStripState->getSubState( $text ), |
|
5866
|
|
|
'linkHolders' => $this->mLinkHolders->getSubArray( $text ) |
|
5867
|
|
|
]; |
|
5868
|
|
|
return $data; |
|
5869
|
|
|
} |
|
5870
|
|
|
|
|
5871
|
|
|
/** |
|
5872
|
|
|
* Load the parser state given in the $data array, which is assumed to |
|
5873
|
|
|
* have been generated by serializeHalfParsedText(). The text contents is |
|
5874
|
|
|
* extracted from the array, and its markers are transformed into markers |
|
5875
|
|
|
* appropriate for the current Parser instance. This transformed text is |
|
5876
|
|
|
* returned, and can be safely included in the return value of a parser |
|
5877
|
|
|
* hook. |
|
5878
|
|
|
* |
|
5879
|
|
|
* If the $data array has been stored persistently, the caller should first |
|
5880
|
|
|
* check whether it is still valid, by calling isValidHalfParsedText(). |
|
5881
|
|
|
* |
|
5882
|
|
|
* @param array $data Serialized data |
|
5883
|
|
|
* @throws MWException |
|
5884
|
|
|
* @return string |
|
5885
|
|
|
*/ |
|
5886
|
|
|
public function unserializeHalfParsedText( $data ) { |
|
5887
|
|
View Code Duplication |
if ( !isset( $data['version'] ) || $data['version'] != self::HALF_PARSED_VERSION ) { |
|
5888
|
|
|
throw new MWException( __METHOD__ . ': invalid version' ); |
|
5889
|
|
|
} |
|
5890
|
|
|
|
|
5891
|
|
|
# First, extract the strip state. |
|
5892
|
|
|
$texts = [ $data['text'] ]; |
|
5893
|
|
|
$texts = $this->mStripState->merge( $data['stripState'], $texts ); |
|
5894
|
|
|
|
|
5895
|
|
|
# Now renumber links |
|
5896
|
|
|
$texts = $this->mLinkHolders->mergeForeign( $data['linkHolders'], $texts ); |
|
5897
|
|
|
|
|
5898
|
|
|
# Should be good to go. |
|
5899
|
|
|
return $texts[0]; |
|
5900
|
|
|
} |
|
5901
|
|
|
|
|
5902
|
|
|
/** |
|
5903
|
|
|
* Returns true if the given array, presumed to be generated by |
|
5904
|
|
|
* serializeHalfParsedText(), is compatible with the current version of the |
|
5905
|
|
|
* parser. |
|
5906
|
|
|
* |
|
5907
|
|
|
* @param array $data |
|
5908
|
|
|
* |
|
5909
|
|
|
* @return bool |
|
5910
|
|
|
*/ |
|
5911
|
|
|
public function isValidHalfParsedText( $data ) { |
|
5912
|
|
|
return isset( $data['version'] ) && $data['version'] == self::HALF_PARSED_VERSION; |
|
5913
|
|
|
} |
|
5914
|
|
|
|
|
5915
|
|
|
/** |
|
5916
|
|
|
* Parsed a width param of imagelink like 300px or 200x300px |
|
5917
|
|
|
* |
|
5918
|
|
|
* @param string $value |
|
5919
|
|
|
* |
|
5920
|
|
|
* @return array |
|
5921
|
|
|
* @since 1.20 |
|
5922
|
|
|
*/ |
|
5923
|
|
|
public function parseWidthParam( $value ) { |
|
5924
|
|
|
$parsedWidthParam = []; |
|
5925
|
|
|
if ( $value === '' ) { |
|
5926
|
|
|
return $parsedWidthParam; |
|
5927
|
|
|
} |
|
5928
|
|
|
$m = []; |
|
5929
|
|
|
# (bug 13500) In both cases (width/height and width only), |
|
5930
|
|
|
# permit trailing "px" for backward compatibility. |
|
5931
|
|
|
if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) { |
|
5932
|
|
|
$width = intval( $m[1] ); |
|
5933
|
|
|
$height = intval( $m[2] ); |
|
5934
|
|
|
$parsedWidthParam['width'] = $width; |
|
5935
|
|
|
$parsedWidthParam['height'] = $height; |
|
5936
|
|
|
} elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) { |
|
5937
|
|
|
$width = intval( $value ); |
|
5938
|
|
|
$parsedWidthParam['width'] = $width; |
|
5939
|
|
|
} |
|
5940
|
|
|
return $parsedWidthParam; |
|
5941
|
|
|
} |
|
5942
|
|
|
|
|
5943
|
|
|
/** |
|
5944
|
|
|
* Lock the current instance of the parser. |
|
5945
|
|
|
* |
|
5946
|
|
|
* This is meant to stop someone from calling the parser |
|
5947
|
|
|
* recursively and messing up all the strip state. |
|
5948
|
|
|
* |
|
5949
|
|
|
* @throws MWException If parser is in a parse |
|
5950
|
|
|
* @return ScopedCallback The lock will be released once the return value goes out of scope. |
|
5951
|
|
|
*/ |
|
5952
|
|
|
protected function lock() { |
|
5953
|
|
|
if ( $this->mInParse ) { |
|
5954
|
|
|
throw new MWException( "Parser state cleared while parsing. " |
|
5955
|
|
|
. "Did you call Parser::parse recursively?" ); |
|
5956
|
|
|
} |
|
5957
|
|
|
$this->mInParse = true; |
|
5958
|
|
|
|
|
5959
|
|
|
$recursiveCheck = new ScopedCallback( function() { |
|
5960
|
|
|
$this->mInParse = false; |
|
5961
|
|
|
} ); |
|
5962
|
|
|
|
|
5963
|
|
|
return $recursiveCheck; |
|
5964
|
|
|
} |
|
5965
|
|
|
|
|
5966
|
|
|
/** |
|
5967
|
|
|
* Strip outer <p></p> tag from the HTML source of a single paragraph. |
|
5968
|
|
|
* |
|
5969
|
|
|
* Returns original HTML if the <p/> tag has any attributes, if there's no wrapping <p/> tag, |
|
5970
|
|
|
* or if there is more than one <p/> tag in the input HTML. |
|
5971
|
|
|
* |
|
5972
|
|
|
* @param string $html |
|
5973
|
|
|
* @return string |
|
5974
|
|
|
* @since 1.24 |
|
5975
|
|
|
*/ |
|
5976
|
|
|
public static function stripOuterParagraph( $html ) { |
|
5977
|
|
|
$m = []; |
|
5978
|
|
|
if ( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $html, $m ) ) { |
|
5979
|
|
|
if ( strpos( $m[1], '</p>' ) === false ) { |
|
5980
|
|
|
$html = $m[1]; |
|
5981
|
|
|
} |
|
5982
|
|
|
} |
|
5983
|
|
|
|
|
5984
|
|
|
return $html; |
|
5985
|
|
|
} |
|
5986
|
|
|
|
|
5987
|
|
|
/** |
|
5988
|
|
|
* Return this parser if it is not doing anything, otherwise |
|
5989
|
|
|
* get a fresh parser. You can use this method by doing |
|
5990
|
|
|
* $myParser = $wgParser->getFreshParser(), or more simply |
|
5991
|
|
|
* $wgParser->getFreshParser()->parse( ... ); |
|
5992
|
|
|
* if you're unsure if $wgParser is safe to use. |
|
5993
|
|
|
* |
|
5994
|
|
|
* @since 1.24 |
|
5995
|
|
|
* @return Parser A parser object that is not parsing anything |
|
5996
|
|
|
*/ |
|
5997
|
|
|
public function getFreshParser() { |
|
5998
|
|
|
global $wgParserConf; |
|
5999
|
|
|
if ( $this->mInParse ) { |
|
6000
|
|
|
return new $wgParserConf['class']( $wgParserConf ); |
|
6001
|
|
|
} else { |
|
6002
|
|
|
return $this; |
|
6003
|
|
|
} |
|
6004
|
|
|
} |
|
6005
|
|
|
|
|
6006
|
|
|
/** |
|
6007
|
|
|
* Set's up the PHP implementation of OOUI for use in this request |
|
6008
|
|
|
* and instructs OutputPage to enable OOUI for itself. |
|
6009
|
|
|
* |
|
6010
|
|
|
* @since 1.26 |
|
6011
|
|
|
*/ |
|
6012
|
|
|
public function enableOOUI() { |
|
6013
|
|
|
OutputPage::setupOOUI(); |
|
6014
|
|
|
$this->mOutput->setEnableOOUI( true ); |
|
6015
|
|
|
} |
|
6016
|
|
|
} |
|
6017
|
|
|
|
Only declaring a single property per statement allows you to later on add doc comments more easily.
It is also recommended by PSR2, so it is a common style that many people expect.