]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/MagicWord.php
MediaWiki 1.16.5-scripts
[autoinstalls/mediawiki.git] / includes / MagicWord.php
1 <?php
2 /**
3  * File for magic words
4  * See docs/magicword.txt
5  *
6  * @file
7  * @ingroup Parser
8  */
9
10 /**
11  * This class encapsulates "magic words" such as #redirect, __NOTOC__, etc.
12  * Usage:
13  *     if (MagicWord::get( 'redirect' )->match( $text ) )
14  *
15  * Possible future improvements:
16  *   * Simultaneous searching for a number of magic words
17  *   * MagicWord::$mObjects in shared memory
18  *
19  * Please avoid reading the data out of one of these objects and then writing
20  * special case code. If possible, add another match()-like function here.
21  *
22  * To add magic words in an extension, use the LanguageGetMagic hook. For
23  * magic words which are also Parser variables, add a MagicWordwgVariableIDs
24  * hook. Use string keys.
25  *
26  * @ingroup Parser
27  */
28 class MagicWord {
29         /**#@+
30          * @private
31          */
32         var $mId, $mSynonyms, $mCaseSensitive, $mRegex;
33         var $mRegexStart, $mBaseRegex, $mVariableRegex;
34         var $mModified, $mFound;
35
36         static public $mVariableIDsInitialised = false;
37         static public $mVariableIDs = array(
38                 'currentmonth',
39                 'currentmonth1',
40                 'currentmonthname',
41                 'currentmonthnamegen',
42                 'currentmonthabbrev',
43                 'currentday',
44                 'currentday2',
45                 'currentdayname',
46                 'currentyear',
47                 'currenttime',
48                 'currenthour',
49                 'localmonth',
50                 'localmonth1',
51                 'localmonthname',
52                 'localmonthnamegen',
53                 'localmonthabbrev',
54                 'localday',
55                 'localday2',
56                 'localdayname',
57                 'localyear',
58                 'localtime',
59                 'localhour',
60                 'numberofarticles',
61                 'numberoffiles',
62                 'numberofedits',
63                 'sitename',
64                 'server',
65                 'servername',
66                 'scriptpath',
67                 'stylepath',
68                 'pagename',
69                 'pagenamee',
70                 'fullpagename',
71                 'fullpagenamee',
72                 'namespace',
73                 'namespacee',
74                 'currentweek',
75                 'currentdow',
76                 'localweek',
77                 'localdow',
78                 'revisionid',
79                 'revisionday',
80                 'revisionday2',
81                 'revisionmonth',
82                 'revisionyear',
83                 'revisiontimestamp',
84                 'revisionuser',
85                 'subpagename',
86                 'subpagenamee',
87                 'talkspace',
88                 'talkspacee',
89                 'subjectspace',
90                 'subjectspacee',
91                 'talkpagename',
92                 'talkpagenamee',
93                 'subjectpagename',
94                 'subjectpagenamee',
95                 'numberofusers',
96                 'numberofactiveusers',
97                 'numberofpages',
98                 'currentversion',
99                 'basepagename',
100                 'basepagenamee',
101                 'currenttimestamp',
102                 'localtimestamp',
103                 'directionmark',
104                 'contentlanguage',
105                 'numberofadmins',
106                 'numberofviews',
107         );
108
109         /* Array of caching hints for ParserCache */
110         static public $mCacheTTLs = array (
111                 'currentmonth' => 86400,
112                 'currentmonth1' => 86400,
113                 'currentmonthname' => 86400,
114                 'currentmonthnamegen' => 86400,
115                 'currentmonthabbrev' => 86400,
116                 'currentday' => 3600,
117                 'currentday2' => 3600,
118                 'currentdayname' => 3600,
119                 'currentyear' => 86400,
120                 'currenttime' => 3600,
121                 'currenthour' => 3600,
122                 'localmonth' => 86400,
123                 'localmonth1' => 86400,
124                 'localmonthname' => 86400,
125                 'localmonthnamegen' => 86400,
126                 'localmonthabbrev' => 86400,
127                 'localday' => 3600,
128                 'localday2' => 3600,
129                 'localdayname' => 3600,
130                 'localyear' => 86400,
131                 'localtime' => 3600,
132                 'localhour' => 3600,
133                 'numberofarticles' => 3600,
134                 'numberoffiles' => 3600,
135                 'numberofedits' => 3600,
136                 'currentweek' => 3600,
137                 'currentdow' => 3600,
138                 'localweek' => 3600,
139                 'localdow' => 3600,
140                 'numberofusers' => 3600,
141                 'numberofactiveusers' => 3600,
142                 'numberofpages' => 3600,
143                 'currentversion' => 86400,
144                 'currenttimestamp' => 3600,
145                 'localtimestamp' => 3600,
146                 'pagesinnamespace' => 3600,
147                 'numberofadmins' => 3600,
148                 'numberofviews' => 3600,
149                 'numberingroup' => 3600,
150                 );
151
152         static public $mDoubleUnderscoreIDs = array(
153                 'notoc',
154                 'nogallery',
155                 'forcetoc',
156                 'toc',
157                 'noeditsection',
158                 'newsectionlink',
159                 'nonewsectionlink',
160                 'hiddencat',
161                 'index',
162                 'noindex',
163                 'staticredirect',
164                 'notitleconvert',
165                 'nocontentconvert',
166         );
167
168         static public $mSubstIDs = array(
169                 'subst',
170                 'safesubst',
171         );
172
173         static public $mObjects = array();
174         static public $mDoubleUnderscoreArray = null;
175
176         /**#@-*/
177
178         function __construct($id = 0, $syn = '', $cs = false) {
179                 $this->mId = $id;
180                 $this->mSynonyms = (array)$syn;
181                 $this->mCaseSensitive = $cs;
182                 $this->mRegex = '';
183                 $this->mRegexStart = '';
184                 $this->mVariableRegex = '';
185                 $this->mVariableStartToEndRegex = '';
186                 $this->mModified = false;
187         }
188
189         /**
190          * Factory: creates an object representing an ID
191          * @static
192          */
193         static function &get( $id ) {
194                 wfProfileIn( __METHOD__ );
195                 if ( !isset( self::$mObjects[$id] ) ) {
196                         $mw = new MagicWord();
197                         $mw->load( $id );
198                         self::$mObjects[$id] = $mw;
199                 }
200                 wfProfileOut( __METHOD__ );
201                 return self::$mObjects[$id];
202         }
203
204         /**
205          * Get an array of parser variable IDs
206          */
207         static function getVariableIDs() {
208                 if ( !self::$mVariableIDsInitialised ) {
209                         # Deprecated constant definition hook, available for extensions that need it
210                         $magicWords = array();
211                         wfRunHooks( 'MagicWordMagicWords', array( &$magicWords ) );
212                         foreach ( $magicWords as $word ) {
213                                 define( $word, $word );
214                         }
215
216                         # Get variable IDs
217                         wfRunHooks( 'MagicWordwgVariableIDs', array( &self::$mVariableIDs ) );
218                         self::$mVariableIDsInitialised = true;
219                 }
220                 return self::$mVariableIDs;
221         }
222
223         /**
224          * Get an array of parser substitution modifier IDs
225          */
226         static function getSubstIDs() {
227                 return self::$mSubstIDs; 
228         }
229
230         /* Allow external reads of TTL array */
231         static function getCacheTTL($id) {
232                 if (array_key_exists($id,self::$mCacheTTLs)) {
233                         return self::$mCacheTTLs[$id];
234                 } else {
235                         return -1;
236                 }
237         }
238
239         /** Get a MagicWordArray of double-underscore entities */
240         static function getDoubleUnderscoreArray() {
241                 if ( is_null( self::$mDoubleUnderscoreArray ) ) {
242                         self::$mDoubleUnderscoreArray = new MagicWordArray( self::$mDoubleUnderscoreIDs );
243                 }
244                 return self::$mDoubleUnderscoreArray;
245         }
246
247         /**
248          * Clear the self::$mObjects variable
249          * For use in parser tests
250          */
251         public static function clearCache() {
252                 self::$mObjects = array();
253         }
254
255         # Initialises this object with an ID
256         function load( $id ) {
257                 global $wgContLang;
258                 $this->mId = $id;
259                 $wgContLang->getMagic( $this );
260                 if ( !$this->mSynonyms ) {
261                         $this->mSynonyms = array( 'dkjsagfjsgashfajsh' );
262                         #throw new MWException( "Error: invalid magic word '$id'" );
263                         wfDebugLog( 'exception', "Error: invalid magic word '$id'\n" );
264                 }
265         }
266
267         /**
268          * Preliminary initialisation
269          * @private
270          */
271         function initRegex() {
272                 #$variableClass = Title::legalChars();
273                 # This was used for matching "$1" variables, but different uses of the feature will have
274                 # different restrictions, which should be checked *after* the MagicWord has been matched,
275                 # not here. - IMSoP
276
277                 $escSyn = array();
278                 foreach ( $this->mSynonyms as $synonym )
279                         // In case a magic word contains /, like that's going to happen;)
280                         $escSyn[] = preg_quote( $synonym, '/' );
281                 $this->mBaseRegex = implode( '|', $escSyn );
282
283                 $case = $this->mCaseSensitive ? '' : 'iu';
284                 $this->mRegex = "/{$this->mBaseRegex}/{$case}";
285                 $this->mRegexStart = "/^(?:{$this->mBaseRegex})/{$case}";
286                 $this->mVariableRegex = str_replace( "\\$1", "(.*?)", $this->mRegex );
287                 $this->mVariableStartToEndRegex = str_replace( "\\$1", "(.*?)",
288                         "/^(?:{$this->mBaseRegex})$/{$case}" );
289         }
290
291         /**
292          * Gets a regex representing matching the word
293          */
294         function getRegex() {
295                 if ($this->mRegex == '' ) {
296                         $this->initRegex();
297                 }
298                 return $this->mRegex;
299         }
300
301         /**
302          * Gets the regexp case modifier to use, i.e. i or nothing, to be used if
303          * one is using MagicWord::getBaseRegex(), otherwise it'll be included in
304          * the complete expression
305          */
306         function getRegexCase() {
307                 if ( $this->mRegex === '' )
308                         $this->initRegex();
309
310                 return $this->mCaseSensitive ? '' : 'iu';
311         }
312
313         /**
314          * Gets a regex matching the word, if it is at the string start
315          */
316         function getRegexStart() {
317                 if ($this->mRegex == '' ) {
318                         $this->initRegex();
319                 }
320                 return $this->mRegexStart;
321         }
322
323         /**
324          * regex without the slashes and what not
325          */
326         function getBaseRegex() {
327                 if ($this->mRegex == '') {
328                         $this->initRegex();
329                 }
330                 return $this->mBaseRegex;
331         }
332
333         /**
334          * Returns true if the text contains the word
335          * @return bool
336          */
337         function match( $text ) {
338                 return (bool)preg_match( $this->getRegex(), $text );
339         }
340
341         /**
342          * Returns true if the text starts with the word
343          * @return bool
344          */
345         function matchStart( $text ) {
346                 return (bool)preg_match( $this->getRegexStart(), $text );
347         }
348
349         /**
350          * Returns NULL if there's no match, the value of $1 otherwise
351          * The return code is the matched string, if there's no variable
352          * part in the regex and the matched variable part ($1) if there
353          * is one.
354          */
355         function matchVariableStartToEnd( $text ) {
356                 $matches = array();
357                 $matchcount = preg_match( $this->getVariableStartToEndRegex(), $text, $matches );
358                 if ( $matchcount == 0 ) {
359                         return null;
360                 } else {
361                         # multiple matched parts (variable match); some will be empty because of
362                         # synonyms. The variable will be the second non-empty one so remove any
363                         # blank elements and re-sort the indices.
364                         # See also bug 6526
365
366                         $matches = array_values(array_filter($matches));
367
368                         if ( count($matches) == 1 ) { return $matches[0]; }
369                         else { return $matches[1]; }
370                 }
371         }
372
373
374         /**
375          * Returns true if the text matches the word, and alters the
376          * input string, removing all instances of the word
377          */
378         function matchAndRemove( &$text ) {
379                 $this->mFound = false;
380                 $text = preg_replace_callback( $this->getRegex(), array( &$this, 'pregRemoveAndRecord' ), $text );
381                 return $this->mFound;
382         }
383
384         function matchStartAndRemove( &$text ) {
385                 $this->mFound = false;
386                 $text = preg_replace_callback( $this->getRegexStart(), array( &$this, 'pregRemoveAndRecord' ), $text );
387                 return $this->mFound;
388         }
389
390         /**
391          * Used in matchAndRemove()
392          * @private
393          **/
394         function pregRemoveAndRecord( ) {
395                 $this->mFound = true;
396                 return '';
397         }
398
399         /**
400          * Replaces the word with something else
401          */
402         function replace( $replacement, $subject, $limit=-1 ) {
403                 $res = preg_replace( $this->getRegex(), StringUtils::escapeRegexReplacement( $replacement ), $subject, $limit );
404                 $this->mModified = !($res === $subject);
405                 return $res;
406         }
407
408         /**
409          * Variable handling: {{SUBST:xxx}} style words
410          * Calls back a function to determine what to replace xxx with
411          * Input word must contain $1
412          */
413         function substituteCallback( $text, $callback ) {
414                 $res = preg_replace_callback( $this->getVariableRegex(), $callback, $text );
415                 $this->mModified = !($res === $text);
416                 return $res;
417         }
418
419         /**
420          * Matches the word, where $1 is a wildcard
421          */
422         function getVariableRegex()     {
423                 if ( $this->mVariableRegex == '' ) {
424                         $this->initRegex();
425                 }
426                 return $this->mVariableRegex;
427         }
428
429         /**
430          * Matches the entire string, where $1 is a wildcard
431          */
432         function getVariableStartToEndRegex() {
433                 if ( $this->mVariableStartToEndRegex == '' ) {
434                         $this->initRegex();
435                 }
436                 return $this->mVariableStartToEndRegex;
437         }
438
439         /**
440          * Accesses the synonym list directly
441          */
442         function getSynonym( $i ) {
443                 return $this->mSynonyms[$i];
444         }
445
446         function getSynonyms() {
447                 return $this->mSynonyms;
448         }
449
450         /**
451          * Returns true if the last call to replace() or substituteCallback()
452          * returned a modified text, otherwise false.
453          */
454         function getWasModified(){
455                 return $this->mModified;
456         }
457
458         /**
459          * $magicarr is an associative array of (magic word ID => replacement)
460          * This method uses the php feature to do several replacements at the same time,
461          * thereby gaining some efficiency. The result is placed in the out variable
462          * $result. The return value is true if something was replaced.
463          * @static
464          **/
465         function replaceMultiple( $magicarr, $subject, &$result ){
466                 $search = array();
467                 $replace = array();
468                 foreach( $magicarr as $id => $replacement ){
469                         $mw = MagicWord::get( $id );
470                         $search[] = $mw->getRegex();
471                         $replace[] = $replacement;
472                 }
473
474                 $result = preg_replace( $search, $replace, $subject );
475                 return !($result === $subject);
476         }
477
478         /**
479          * Adds all the synonyms of this MagicWord to an array, to allow quick
480          * lookup in a list of magic words
481          */
482         function addToArray( &$array, $value ) {
483                 global $wgContLang;
484                 foreach ( $this->mSynonyms as $syn ) {
485                         $array[$wgContLang->lc($syn)] = $value;
486                 }
487         }
488
489         function isCaseSensitive() {
490                 return $this->mCaseSensitive;
491         }
492
493         function getId() {
494                 return $this->mId;
495         }
496 }
497
498 /**
499  * Class for handling an array of magic words
500  * @ingroup Parser
501  */
502 class MagicWordArray {
503         var $names = array();
504         var $hash;
505         var $baseRegex, $regex;
506         var $matches;
507
508         function __construct( $names = array() ) {
509                 $this->names = $names;
510         }
511
512         /**
513          * Add a magic word by name
514          */
515         public function add( $name ) {
516                 global $wgContLang;
517                 $this->names[] = $name;
518                 $this->hash = $this->baseRegex = $this->regex = null;
519         }
520
521         /**
522          * Add a number of magic words by name
523          */
524         public function addArray( $names ) {
525                 $this->names = array_merge( $this->names, array_values( $names ) );
526                 $this->hash = $this->baseRegex = $this->regex = null;
527         }
528
529         /**
530          * Get a 2-d hashtable for this array
531          */
532         function getHash() {
533                 if ( is_null( $this->hash ) ) {
534                         global $wgContLang;
535                         $this->hash = array( 0 => array(), 1 => array() );
536                         foreach ( $this->names as $name ) {
537                                 $magic = MagicWord::get( $name );
538                                 $case = intval( $magic->isCaseSensitive() );
539                                 foreach ( $magic->getSynonyms() as $syn ) {
540                                         if ( !$case ) {
541                                                 $syn = $wgContLang->lc( $syn );
542                                         }
543                                         $this->hash[$case][$syn] = $name;
544                                 }
545                         }
546                 }
547                 return $this->hash;
548         }
549
550         /**
551          * Get the base regex
552          */
553         function getBaseRegex() {
554                 if ( is_null( $this->baseRegex ) ) {
555                         $this->baseRegex = array( 0 => '', 1 => '' );
556                         foreach ( $this->names as $name ) {
557                                 $magic = MagicWord::get( $name );
558                                 $case = intval( $magic->isCaseSensitive() );
559                                 foreach ( $magic->getSynonyms() as $i => $syn ) {
560                                         $group = "(?P<{$i}_{$name}>" . preg_quote( $syn, '/' ) . ')';
561                                         if ( $this->baseRegex[$case] === '' ) {
562                                                 $this->baseRegex[$case] = $group;
563                                         } else {
564                                                 $this->baseRegex[$case] .= '|' . $group;
565                                         }
566                                 }
567                         }
568                 }
569                 return $this->baseRegex;
570         }
571
572         /**
573          * Get an unanchored regex that does not match parameters
574          */
575         function getRegex() {
576                 if ( is_null( $this->regex ) ) {
577                         $base = $this->getBaseRegex();
578                         $this->regex = array( '', '' );
579                         if ( $this->baseRegex[0] !== '' ) {
580                                 $this->regex[0] = "/{$base[0]}/iuS";
581                         }
582                         if ( $this->baseRegex[1] !== '' ) {
583                                 $this->regex[1] = "/{$base[1]}/S";
584                         }
585                 }
586                 return $this->regex;
587         }
588
589         /**
590          * Get a regex for matching variables with parameters
591          */
592         function getVariableRegex() {
593                 return str_replace( "\\$1", "(.*?)", $this->getRegex() );
594         }
595
596         /**
597          * Get a regex anchored to the start of the string that does not match parameters
598          */
599         function getRegexStart() {
600                 $base = $this->getBaseRegex();
601                 $newRegex = array( '', '' );
602                 if ( $base[0] !== '' ) {
603                         $newRegex[0] = "/^(?:{$base[0]})/iuS";
604                 }
605                 if ( $base[1] !== '' ) {
606                         $newRegex[1] = "/^(?:{$base[1]})/S"; 
607                 }
608                 return $newRegex;
609         }
610
611         /**
612          * Get an anchored regex for matching variables with parameters
613          */
614         function getVariableStartToEndRegex() {
615                 $base = $this->getBaseRegex();
616                 $newRegex = array( '', '' );
617                 if ( $base[0] !== '' ) {
618                         $newRegex[0] = str_replace( "\\$1", "(.*?)", "/^(?:{$base[0]})$/iuS" );
619                 }
620                 if ( $base[1] !== '' ) {
621                         $newRegex[1] = str_replace( "\\$1", "(.*?)", "/^(?:{$base[1]})$/S" );
622                 }
623                 return $newRegex;
624         }
625
626         /**
627          * Parse a match array from preg_match
628          * Returns array(magic word ID, parameter value)
629          * If there is no parameter value, that element will be false.
630          */
631         function parseMatch( $m ) {
632                 reset( $m );
633                 while ( list( $key, $value ) = each( $m ) ) {
634                         if ( $key === 0 || $value === '' ) {
635                                 continue;
636                         }
637                         $parts = explode( '_', $key, 2 );
638                         if ( count( $parts ) != 2 ) {
639                                 // This shouldn't happen
640                                 // continue;
641                                 throw new MWException( __METHOD__ . ': bad parameter name' );
642                         }
643                         list( /* $synIndex */, $magicName ) = $parts;
644                         $paramValue = next( $m );
645                         return array( $magicName, $paramValue );
646                 }
647                 // This shouldn't happen either
648                 throw new MWException( __METHOD__.': parameter not found' );
649                 return array( false, false );
650         }
651
652         /**
653          * Match some text, with parameter capture
654          * Returns an array with the magic word name in the first element and the
655          * parameter in the second element.
656          * Both elements are false if there was no match.
657          */
658         public function matchVariableStartToEnd( $text ) {
659                 global $wgContLang;
660                 $regexes = $this->getVariableStartToEndRegex();
661                 foreach ( $regexes as $regex ) {
662                         if ( $regex !== '' ) {
663                                 $m = false;
664                                 if ( preg_match( $regex, $text, $m ) ) {
665                                         return $this->parseMatch( $m );
666                                 }
667                         }
668                 }
669                 return array( false, false );
670         }
671
672         /**
673          * Match some text, without parameter capture
674          * Returns the magic word name, or false if there was no capture
675          */
676         public function matchStartToEnd( $text ) {
677                 $hash = $this->getHash();
678                 if ( isset( $hash[1][$text] ) ) {
679                         return $hash[1][$text];
680                 }
681                 global $wgContLang;
682                 $lc = $wgContLang->lc( $text );
683                 if ( isset( $hash[0][$lc] ) ) {
684                         return $hash[0][$lc];
685                 }
686                 return false;
687         }
688
689         /**
690          * Returns an associative array, ID => param value, for all items that match
691          * Removes the matched items from the input string (passed by reference)
692          */
693         public function matchAndRemove( &$text ) {
694                 $found = array();
695                 $regexes = $this->getRegex();
696                 foreach ( $regexes as $regex ) {
697                         if ( $regex === '' ) {
698                                 continue;
699                         }
700                         preg_match_all( $regex, $text, $matches, PREG_SET_ORDER );
701                         foreach ( $matches as $m ) {
702                                 list( $name, $param ) = $this->parseMatch( $m );
703                                 $found[$name] = $param;
704                         }
705                         $text = preg_replace( $regex, '', $text );
706                 }
707                 return $found;
708         }
709
710         /**
711          * Return the ID of the magic word at the start of $text, and remove
712          * the prefix from $text.
713          * Return false if no match found and $text is not modified.
714          * Does not match parameters.
715          */
716         public function matchStartAndRemove( &$text ) {
717                 $regexes = $this->getRegexStart();
718                 foreach ( $regexes as $regex ) {
719                         if ( $regex === '' ) {
720                                 continue;
721                         }
722                         if ( preg_match( $regex, $text, $m ) ) {
723                                 list( $id, $param ) = $this->parseMatch( $m );
724                                 if ( strlen( $m[0] ) >= strlen( $text ) ) {
725                                         $text = '';
726                                 } else {
727                                         $text = substr( $text, strlen( $m[0] ) );
728                                 }
729                                 return $id;
730                         }
731                 }
732                 return false;
733         }
734 }