Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.59% covered (success)
94.59%
35 / 37
93.75% covered (success)
93.75%
15 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
MagicWord
94.59% covered (success)
94.59%
35 / 37
93.75% covered (success)
93.75%
15 / 16
20.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 load
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getRegex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRegexCase
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getRegexStart
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRegexStartToEnd
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBaseRegex
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 match
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 matchStartToEnd
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 matchAndRemove
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 matchStartAndRemove
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 replace
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getSynonym
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSynonyms
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCaseSensitive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Parser;
8
9use MediaWiki\Language\Language;
10use MediaWiki\MediaWikiServices;
11use UnexpectedValueException;
12use Wikimedia\StringUtils\StringUtils;
13
14/**
15 * This class encapsulates "magic words" such as "#redirect", __NOTOC__, etc.
16 *
17 * See docs/magicword.md.
18 *
19 * @par Usage:
20 * @code
21 *     if ( $magicWordFactory->get( 'redirect' )->match( $text ) ) {
22 *       // some code
23 *     }
24 * @endcode
25 *
26 * Please avoid reading the data out of one of these objects and then writing
27 * special case code. If possible, add another match()-like function here.
28 *
29 * To add magic words in an extension, use $magicWords in a file listed in
30 * $wgExtensionMessagesFiles[].
31 *
32 * @par Example:
33 * @code
34 * $magicWords = [];
35 *
36 * $magicWords['en'] = [
37 *   'magicwordkey' => [ 0, 'case_insensitive_magic_word' ],
38 *   'magicwordkey2' => [ 1, 'CASE_sensitive_magic_word2' ],
39 * ];
40 * @endcode
41 *
42 * For magic words which name Parser double underscore names, add a
43 * GetDoubleUnderscoreIDs hook. Use string keys.
44 *
45 * For magic words which name Parser magic variables, add a GetMagicVariableIDs
46 * hook. Use string keys.
47 *
48 * @since 1.1
49 * @ingroup Parser
50 */
51class MagicWord {
52
53    /** @var string|null Potentially null for a short time before {@see load} is called */
54    public $mId;
55
56    /** @var string[] */
57    public array $mSynonyms;
58
59    /** @var bool */
60    public $mCaseSensitive;
61
62    private ?string $mBaseRegex = null;
63
64    private Language $contLang;
65
66    /**
67     * @internal Use {@see MagicWordFactory::get} instead
68     * @param string|null $id Preload internal name of the magic word
69     * @param string[]|string $syn Preload synonyms for the magic word
70     * @param bool $cs If magic word is case sensitive
71     * @param Language|null $contentLanguage
72     */
73    public function __construct( $id = null, $syn = [], $cs = false, ?Language $contentLanguage = null ) {
74        $this->mId = $id;
75        $this->mSynonyms = (array)$syn;
76        $this->mCaseSensitive = $cs;
77        $this->contLang = $contentLanguage ?? MediaWikiServices::getInstance()->getContentLanguage();
78    }
79
80    /**
81     * Load synonym data from {@see LocalisationCache}.
82     *
83     * @internal For use by {@see MagicWordFactory::get} only
84     * @since 1.1
85     * @param string $id
86     */
87    public function load( $id ): void {
88        $this->mId = $id;
89        $this->contLang->getMagic( $this );
90        if ( !$this->mSynonyms ) {
91            throw new UnexpectedValueException( "Error: invalid magic word '$id'" );
92        }
93    }
94
95    /**
96     * Create a regex to match the magic word in wikitext
97     *
98     * @since 1.1
99     * @return string
100     */
101    public function getRegex(): string {
102        return '/' . $this->getBaseRegex() . '/' . $this->getRegexCase();
103    }
104
105    /**
106     * Get the regexp case modifier ("iu" or empty string).
107     *
108     * This is for building custom regexes that include {@see getBaseRegex}.
109     * The other getter methods return complete expressions that include this already.
110     *
111     * @internal Exposed for {@see Parser::cleanSig} only
112     * @return string
113     */
114    public function getRegexCase(): string {
115        return $this->mCaseSensitive ? '' : 'iu';
116    }
117
118    /**
119     * Create a regex to match the word at the start of a line in wikitext
120     *
121     * @since 1.1
122     * @return string
123     */
124    public function getRegexStart(): string {
125        return '/^(?:' . $this->getBaseRegex() . ')/' . $this->getRegexCase();
126    }
127
128    /**
129     * Create a regex to match the word as the only thing on a line of wikitext
130     *
131     * @since 1.23
132     * @return string
133     */
134    public function getRegexStartToEnd(): string {
135        return '/^(?:' . $this->getBaseRegex() . ')$/' . $this->getRegexCase();
136    }
137
138    /**
139     * Get the middle of {@see getRegex}, without the surrounding slashes or modifiers
140     *
141     * @internal Exposed for {@see Parser::cleanSig} only
142     * @since 1.1
143     * @return string
144     */
145    public function getBaseRegex(): string {
146        if ( $this->mBaseRegex === null ) {
147            // Sort the synonyms by length, descending, so that the longest synonym
148            // matches in precedence to the shortest
149            $synonyms = $this->mSynonyms;
150            usort( $synonyms, static fn ( $a, $b ) => strlen( $b ) <=> strlen( $a ) );
151            foreach ( $synonyms as &$synonym ) {
152                $synonym = preg_quote( $synonym, '/' );
153            }
154            $this->mBaseRegex = implode( '|', $synonyms );
155        }
156        return $this->mBaseRegex;
157    }
158
159    /**
160     * Check if given wikitext contains the magic word
161     *
162     * @since 1.1
163     * @param string $text
164     * @return bool
165     */
166    public function match( $text ): bool {
167        return (bool)preg_match( $this->getRegex(), $text ?? '' );
168    }
169
170    /**
171     * Check if given wikitext contains the word as the only thing on a line
172     *
173     * @param string $text
174     * @return bool
175     * @since 1.23
176     */
177    public function matchStartToEnd( $text ): bool {
178        return (bool)preg_match( $this->getRegexStartToEnd(), $text ?? '' );
179    }
180
181    /**
182     * Remove any matches of this magic word from a given text
183     *
184     * Returns true if the text contains one or more matches, and alters the
185     * input string to remove all instances of the magic word.
186     *
187     * @since 1.1
188     * @param string &$text
189     * @return bool
190     */
191    public function matchAndRemove( &$text ): bool {
192        $text = preg_replace( $this->getRegex(), '', $text ?? '', -1, $count );
193        return (bool)$count;
194    }
195
196    /**
197     * @param string &$text
198     * @return bool
199     */
200    public function matchStartAndRemove( &$text ): bool {
201        $text = preg_replace( $this->getRegexStart(), '', $text ?? '', -1, $count );
202        return (bool)$count;
203    }
204
205    /**
206     * Replace any matches of this word with something else
207     *
208     * @since 1.1
209     * @param string $replacement
210     * @param string $subject
211     * @param int $limit
212     * @return string
213     */
214    public function replace( $replacement, $subject, $limit = -1 ) {
215        $res = preg_replace(
216            $this->getRegex(),
217            StringUtils::escapeRegexReplacement( $replacement ),
218            $subject ?? '',
219            $limit
220        );
221        return $res;
222    }
223
224    /**
225     * Get one of the synonyms
226     *
227     * This exists primarily for calling `getSynonym( 0 )`, which is how
228     * you can obtain the preferred name of a magic word according to the
229     * current wiki's content language. For example, when demonstrating or
230     * semi-automatically creating content that uses a given magic word.
231     *
232     * This works because {@see LocalisationCache} merges magic word data by
233     * appending fallback languages (i.e. "en") after to the language's
234     * own data, and each language's `Messages*.php` file lists the
235     * preferred/canonical form as the first value.
236     *
237     * Calling this with a number other than 0 is unsupported and may
238     * fail.
239     *
240     * @since 1.1
241     * @param int $i
242     * @return string
243     */
244    public function getSynonym( $i ) {
245        return $this->mSynonyms[$i];
246    }
247
248    /**
249     * Get full list of synonyms
250     *
251     * @since 1.7
252     * @return string[]
253     */
254    public function getSynonyms(): array {
255        return $this->mSynonyms;
256    }
257
258    /**
259     * @since 1.7
260     * @return bool
261     */
262    public function isCaseSensitive() {
263        return $this->mCaseSensitive;
264    }
265
266    /**
267     * @return string
268     * @deprecated since 1.42 Internal method should not be used
269     */
270    public function getId() {
271        wfDeprecated( __METHOD__, '1.42' );
272        return $this->mId;
273    }
274}