Merge "Revert "Log the reason why revision->getContent() returns null""
[lhc/web/wiklou.git] / includes / parser / StripState.php
1 <?php
2 /**
3 * Holder for stripped items when parsing wiki markup.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Parser
22 */
23
24 /**
25 * @todo document, briefly.
26 * @ingroup Parser
27 */
28 class StripState {
29 protected $data;
30 protected $regex;
31
32 protected $parser;
33
34 protected $circularRefGuard;
35 protected $depth = 0;
36 protected $highestDepth = 0;
37 protected $expandSize = 0;
38
39 protected $depthLimit = 20;
40 protected $sizeLimit = 5000000;
41
42 /**
43 * @param Parser|null $parser
44 * @param array $options
45 */
46 public function __construct( Parser $parser = null, $options = [] ) {
47 $this->data = [
48 'nowiki' => [],
49 'general' => []
50 ];
51 $this->regex = '/' . Parser::MARKER_PREFIX . "([^\x7f<>&'\"]+)" . Parser::MARKER_SUFFIX . '/';
52 $this->circularRefGuard = [];
53 $this->parser = $parser;
54
55 if ( isset( $options['depthLimit'] ) ) {
56 $this->depthLimit = $options['depthLimit'];
57 }
58 if ( isset( $options['sizeLimit'] ) ) {
59 $this->sizeLimit = $options['sizeLimit'];
60 }
61 }
62
63 /**
64 * Add a nowiki strip item
65 * @param string $marker
66 * @param string $value
67 */
68 public function addNoWiki( $marker, $value ) {
69 $this->addItem( 'nowiki', $marker, $value );
70 }
71
72 /**
73 * @param string $marker
74 * @param string $value
75 */
76 public function addGeneral( $marker, $value ) {
77 $this->addItem( 'general', $marker, $value );
78 }
79
80 /**
81 * @throws MWException
82 * @param string $type
83 * @param string $marker
84 * @param string $value
85 */
86 protected function addItem( $type, $marker, $value ) {
87 if ( !preg_match( $this->regex, $marker, $m ) ) {
88 throw new MWException( "Invalid marker: $marker" );
89 }
90
91 $this->data[$type][$m[1]] = $value;
92 }
93
94 /**
95 * @param string $text
96 * @return mixed
97 */
98 public function unstripGeneral( $text ) {
99 return $this->unstripType( 'general', $text );
100 }
101
102 /**
103 * @param string $text
104 * @return mixed
105 */
106 public function unstripNoWiki( $text ) {
107 return $this->unstripType( 'nowiki', $text );
108 }
109
110 /**
111 * @param string $text
112 * @return mixed
113 */
114 public function unstripBoth( $text ) {
115 $text = $this->unstripType( 'general', $text );
116 $text = $this->unstripType( 'nowiki', $text );
117 return $text;
118 }
119
120 /**
121 * @param string $type
122 * @param string $text
123 * @return mixed
124 */
125 protected function unstripType( $type, $text ) {
126 // Shortcut
127 if ( !count( $this->data[$type] ) ) {
128 return $text;
129 }
130
131 $callback = function ( $m ) use ( $type ) {
132 $marker = $m[1];
133 if ( isset( $this->data[$type][$marker] ) ) {
134 if ( isset( $this->circularRefGuard[$marker] ) ) {
135 return $this->getWarning( 'parser-unstrip-loop-warning' );
136 }
137
138 if ( $this->depth > $this->highestDepth ) {
139 $this->highestDepth = $this->depth;
140 }
141 if ( $this->depth >= $this->depthLimit ) {
142 return $this->getLimitationWarning( 'unstrip-depth', $this->depthLimit );
143 }
144
145 $value = $this->data[$type][$marker];
146 if ( $value instanceof Closure ) {
147 $value = $value();
148 }
149
150 $this->expandSize += strlen( $value );
151 if ( $this->expandSize > $this->sizeLimit ) {
152 return $this->getLimitationWarning( 'unstrip-size', $this->sizeLimit );
153 }
154
155 $this->circularRefGuard[$marker] = true;
156 $this->depth++;
157 $ret = $this->unstripType( $type, $value );
158 $this->depth--;
159 unset( $this->circularRefGuard[$marker] );
160
161 return $ret;
162 } else {
163 return $m[0];
164 }
165 };
166
167 $text = preg_replace_callback( $this->regex, $callback, $text );
168 return $text;
169 }
170
171 /**
172 * Get warning HTML and register a limitation warning with the parser
173 *
174 * @param string $type
175 * @param int $max
176 * @return string
177 */
178 private function getLimitationWarning( $type, $max = '' ) {
179 if ( $this->parser ) {
180 $this->parser->limitationWarn( $type, $max );
181 }
182 return $this->getWarning( "$type-warning", $max );
183 }
184
185 /**
186 * Get warning HTML
187 *
188 * @param string $message
189 * @param int $max
190 * @return string
191 */
192 private function getWarning( $message, $max = '' ) {
193 return '<span class="error">' .
194 wfMessage( $message )
195 ->numParams( $max )->inContentLanguage()->text() .
196 '</span>';
197 }
198
199 /**
200 * Get an array of parameters to pass to ParserOutput::setLimitReportData()
201 *
202 * @unstable Should only be called by Parser
203 * @return array
204 */
205 public function getLimitReport() {
206 return [
207 [ 'limitreport-unstrip-depth',
208 [
209 $this->highestDepth,
210 $this->depthLimit
211 ],
212 ],
213 [ 'limitreport-unstrip-size',
214 [
215 $this->expandSize,
216 $this->sizeLimit
217 ],
218 ]
219 ];
220 }
221
222 /**
223 * Get a StripState object which is sufficient to unstrip the given text.
224 * It will contain the minimum subset of strip items necessary.
225 *
226 * @deprecated since 1.31
227 * @param string $text
228 * @return StripState
229 */
230 public function getSubState( $text ) {
231 wfDeprecated( __METHOD__, '1.31' );
232
233 $subState = new StripState;
234 $pos = 0;
235 while ( true ) {
236 $startPos = strpos( $text, Parser::MARKER_PREFIX, $pos );
237 $endPos = strpos( $text, Parser::MARKER_SUFFIX, $pos );
238 if ( $startPos === false || $endPos === false ) {
239 break;
240 }
241
242 $endPos += strlen( Parser::MARKER_SUFFIX );
243 $marker = substr( $text, $startPos, $endPos - $startPos );
244 if ( !preg_match( $this->regex, $marker, $m ) ) {
245 continue;
246 }
247
248 $key = $m[1];
249 if ( isset( $this->data['nowiki'][$key] ) ) {
250 $subState->data['nowiki'][$key] = $this->data['nowiki'][$key];
251 } elseif ( isset( $this->data['general'][$key] ) ) {
252 $subState->data['general'][$key] = $this->data['general'][$key];
253 }
254 $pos = $endPos;
255 }
256 return $subState;
257 }
258
259 /**
260 * Merge another StripState object into this one. The strip marker keys
261 * will not be preserved. The strings in the $texts array will have their
262 * strip markers rewritten, the resulting array of strings will be returned.
263 *
264 * @deprecated since 1.31
265 * @param StripState $otherState
266 * @param array $texts
267 * @return array
268 */
269 public function merge( $otherState, $texts ) {
270 wfDeprecated( __METHOD__, '1.31' );
271
272 $mergePrefix = wfRandomString( 16 );
273
274 foreach ( $otherState->data as $type => $items ) {
275 foreach ( $items as $key => $value ) {
276 $this->data[$type]["$mergePrefix-$key"] = $value;
277 }
278 }
279
280 $callback = function ( $m ) use ( $mergePrefix ) {
281 $key = $m[1];
282 return Parser::MARKER_PREFIX . $mergePrefix . '-' . $key . Parser::MARKER_SUFFIX;
283 };
284 $texts = preg_replace_callback( $otherState->regex, $callback, $texts );
285 return $texts;
286 }
287
288 /**
289 * Remove any strip markers found in the given text.
290 *
291 * @param string $text
292 * @return string
293 */
294 public function killMarkers( $text ) {
295 return preg_replace( $this->regex, '', $text );
296 }
297 }