(bug 366) Time-Variables like CURRENTTIME and CURRENTDAY should use time zones
[lhc/web/wiklou.git] / includes / MagicWord.php
1 <?php
2 /**
3 * File for magic words
4 * @package MediaWiki
5 * @subpackage Parser
6 */
7
8 /**
9 * This class encapsulates "magic words" such as #redirect, __NOTOC__, etc.
10 * Usage:
11 * if (MagicWord::get( 'redirect' )->match( $text ) )
12 *
13 * Possible future improvements:
14 * * Simultaneous searching for a number of magic words
15 * * MagicWord::$mObjects in shared memory
16 *
17 * Please avoid reading the data out of one of these objects and then writing
18 * special case code. If possible, add another match()-like function here.
19 *
20 * To add magic words in an extension, use the LanguageGetMagic hook. For
21 * magic words which are also Parser variables, add a MagicWordwgVariableIDs
22 * hook. Use string keys.
23 *
24 * @package MediaWiki
25 */
26 class MagicWord {
27 /**#@+
28 * @private
29 */
30 var $mId, $mSynonyms, $mCaseSensitive, $mRegex;
31 var $mRegexStart, $mBaseRegex, $mVariableRegex;
32 var $mModified, $mFound;
33
34 static public $mVariableIDsInitialised = false;
35 static public $mVariableIDs = array(
36 'currentmonth',
37 'currentmonthname',
38 'currentmonthnamegen',
39 'currentmonthabbrev',
40 'currentday',
41 'currentday2',
42 'currentdayname',
43 'currentyear',
44 'currenttime',
45 'currenthour',
46 'localmonth',
47 'localmonthname',
48 'localmonthnamegen',
49 'localmonthabbrev',
50 'localday',
51 'localday2',
52 'localdayname',
53 'localyear',
54 'localtime',
55 'localhour',
56 'numberofarticles',
57 'numberoffiles',
58 'sitename',
59 'server',
60 'servername',
61 'scriptpath',
62 'pagename',
63 'pagenamee',
64 'fullpagename',
65 'fullpagenamee',
66 'namespace',
67 'namespacee',
68 'currentweek',
69 'currentdow',
70 'localweek',
71 'localdow',
72 'revisionid',
73 'subpagename',
74 'subpagenamee',
75 'displaytitle',
76 'talkspace',
77 'talkspacee',
78 'subjectspace',
79 'subjectspacee',
80 'talkpagename',
81 'talkpagenamee',
82 'subjectpagename',
83 'subjectpagenamee',
84 'numberofusers',
85 'rawsuffix',
86 'newsectionlink',
87 'numberofpages',
88 'currentversion',
89 'basepagename',
90 'basepagenamee',
91 'urlencode',
92 'currenttimestamp',
93 'localtimestamp',
94 'directionmark',
95 'language',
96 'contentlanguage',
97 'pagesinnamespace',
98 'numberofadmins',
99 );
100
101 static public $mObjects = array();
102
103 /**#@-*/
104
105 function MagicWord($id = 0, $syn = '', $cs = false) {
106 $this->mId = $id;
107 $this->mSynonyms = (array)$syn;
108 $this->mCaseSensitive = $cs;
109 $this->mRegex = '';
110 $this->mRegexStart = '';
111 $this->mVariableRegex = '';
112 $this->mVariableStartToEndRegex = '';
113 $this->mModified = false;
114 }
115
116 /**
117 * Factory: creates an object representing an ID
118 * @static
119 */
120 static function &get( $id ) {
121 if (!array_key_exists( $id, self::$mObjects ) ) {
122 $mw = new MagicWord();
123 $mw->load( $id );
124 self::$mObjects[$id] = $mw;
125 }
126 return self::$mObjects[$id];
127 }
128
129 /**
130 * Get an array of parser variable IDs
131 */
132 static function getVariableIDs() {
133 if ( !self::$mVariableIDsInitialised ) {
134 # Deprecated constant definition hook, available for extensions that need it
135 $magicWords = array();
136 wfRunHooks( 'MagicWordMagicWords', array( &$magicWords ) );
137 foreach ( $magicWords as $word ) {
138 define( $word, $word );
139 }
140
141 # Get variable IDs
142 wfRunHooks( 'MagicWordwgVariableIDs', array( &self::$mVariableIDs ) );
143 self::$mVariableIDsInitialised = true;
144 }
145 return self::$mVariableIDs;
146 }
147
148 # Initialises this object with an ID
149 function load( $id ) {
150 global $wgContLang;
151 $this->mId = $id;
152 $wgContLang->getMagic( $this );
153 }
154
155 /**
156 * Preliminary initialisation
157 * @private
158 */
159 function initRegex() {
160 #$variableClass = Title::legalChars();
161 # This was used for matching "$1" variables, but different uses of the feature will have
162 # different restrictions, which should be checked *after* the MagicWord has been matched,
163 # not here. - IMSoP
164
165 $escSyn = array();
166 foreach ( $this->mSynonyms as $synonym )
167 // In case a magic word contains /, like that's going to happen;)
168 $escSyn[] = preg_quote( $synonym, '/' );
169 $this->mBaseRegex = implode( '|', $escSyn );
170
171 $case = $this->mCaseSensitive ? '' : 'i';
172 $this->mRegex = "/{$this->mBaseRegex}/{$case}";
173 $this->mRegexStart = "/^(?:{$this->mBaseRegex})/{$case}";
174 $this->mVariableRegex = str_replace( "\\$1", "(.*?)", $this->mRegex );
175 $this->mVariableStartToEndRegex = str_replace( "\\$1", "(.*?)",
176 "/^(?:{$this->mBaseRegex})$/{$case}" );
177 }
178
179 /**
180 * Gets a regex representing matching the word
181 */
182 function getRegex() {
183 if ($this->mRegex == '' ) {
184 $this->initRegex();
185 }
186 return $this->mRegex;
187 }
188
189 /**
190 * Gets the regexp case modifier to use, i.e. i or nothing, to be used if
191 * one is using MagicWord::getBaseRegex(), otherwise it'll be included in
192 * the complete expression
193 */
194 function getRegexCase() {
195 if ( $this->mRegex === '' )
196 $this->initRegex();
197
198 return $this->mCaseSensitive ? '' : 'i';
199 }
200
201 /**
202 * Gets a regex matching the word, if it is at the string start
203 */
204 function getRegexStart() {
205 if ($this->mRegex == '' ) {
206 $this->initRegex();
207 }
208 return $this->mRegexStart;
209 }
210
211 /**
212 * regex without the slashes and what not
213 */
214 function getBaseRegex() {
215 if ($this->mRegex == '') {
216 $this->initRegex();
217 }
218 return $this->mBaseRegex;
219 }
220
221 /**
222 * Returns true if the text contains the word
223 * @return bool
224 */
225 function match( $text ) {
226 return preg_match( $this->getRegex(), $text );
227 }
228
229 /**
230 * Returns true if the text starts with the word
231 * @return bool
232 */
233 function matchStart( $text ) {
234 return preg_match( $this->getRegexStart(), $text );
235 }
236
237 /**
238 * Returns NULL if there's no match, the value of $1 otherwise
239 * The return code is the matched string, if there's no variable
240 * part in the regex and the matched variable part ($1) if there
241 * is one.
242 */
243 function matchVariableStartToEnd( $text ) {
244 $matches = array();
245 $matchcount = preg_match( $this->getVariableStartToEndRegex(), $text, $matches );
246 if ( $matchcount == 0 ) {
247 return NULL;
248 } else {
249 # multiple matched parts (variable match); some will be empty because of
250 # synonyms. The variable will be the second non-empty one so remove any
251 # blank elements and re-sort the indices.
252 # See also bug 6526
253
254 $matches = array_values(array_filter($matches));
255
256 if ( count($matches) == 1 ) { return $matches[0]; }
257 else { return $matches[1]; }
258 }
259 }
260
261
262 /**
263 * Returns true if the text matches the word, and alters the
264 * input string, removing all instances of the word
265 */
266 function matchAndRemove( &$text ) {
267 $this->mFound = false;
268 $text = preg_replace_callback( $this->getRegex(), array( &$this, 'pregRemoveAndRecord' ), $text );
269 return $this->mFound;
270 }
271
272 function matchStartAndRemove( &$text ) {
273 $this->mFound = false;
274 $text = preg_replace_callback( $this->getRegexStart(), array( &$this, 'pregRemoveAndRecord' ), $text );
275 return $this->mFound;
276 }
277
278 /**
279 * Used in matchAndRemove()
280 * @private
281 **/
282 function pregRemoveAndRecord( $match ) {
283 $this->mFound = true;
284 return '';
285 }
286
287 /**
288 * Replaces the word with something else
289 */
290 function replace( $replacement, $subject, $limit=-1 ) {
291 $res = preg_replace( $this->getRegex(), wfRegexReplacement( $replacement ), $subject, $limit );
292 $this->mModified = !($res === $subject);
293 return $res;
294 }
295
296 /**
297 * Variable handling: {{SUBST:xxx}} style words
298 * Calls back a function to determine what to replace xxx with
299 * Input word must contain $1
300 */
301 function substituteCallback( $text, $callback ) {
302 $res = preg_replace_callback( $this->getVariableRegex(), $callback, $text );
303 $this->mModified = !($res === $text);
304 return $res;
305 }
306
307 /**
308 * Matches the word, where $1 is a wildcard
309 */
310 function getVariableRegex() {
311 if ( $this->mVariableRegex == '' ) {
312 $this->initRegex();
313 }
314 return $this->mVariableRegex;
315 }
316
317 /**
318 * Matches the entire string, where $1 is a wildcard
319 */
320 function getVariableStartToEndRegex() {
321 if ( $this->mVariableStartToEndRegex == '' ) {
322 $this->initRegex();
323 }
324 return $this->mVariableStartToEndRegex;
325 }
326
327 /**
328 * Accesses the synonym list directly
329 */
330 function getSynonym( $i ) {
331 return $this->mSynonyms[$i];
332 }
333
334 function getSynonyms() {
335 return $this->mSynonyms;
336 }
337
338 /**
339 * Returns true if the last call to replace() or substituteCallback()
340 * returned a modified text, otherwise false.
341 */
342 function getWasModified(){
343 return $this->mModified;
344 }
345
346 /**
347 * $magicarr is an associative array of (magic word ID => replacement)
348 * This method uses the php feature to do several replacements at the same time,
349 * thereby gaining some efficiency. The result is placed in the out variable
350 * $result. The return value is true if something was replaced.
351 * @static
352 **/
353 function replaceMultiple( $magicarr, $subject, &$result ){
354 $search = array();
355 $replace = array();
356 foreach( $magicarr as $id => $replacement ){
357 $mw = MagicWord::get( $id );
358 $search[] = $mw->getRegex();
359 $replace[] = $replacement;
360 }
361
362 $result = preg_replace( $search, $replace, $subject );
363 return !($result === $subject);
364 }
365
366 /**
367 * Adds all the synonyms of this MagicWord to an array, to allow quick
368 * lookup in a list of magic words
369 */
370 function addToArray( &$array, $value ) {
371 foreach ( $this->mSynonyms as $syn ) {
372 $array[$syn] = $value;
373 }
374 }
375
376 function isCaseSensitive() {
377 return $this->mCaseSensitive;
378 }
379 }
380
381 ?>