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