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