* (bug 12938) Fix template expansion and 404 returns for action=raw with section
[lhc/web/wiklou.git] / includes / cbt / CBTProcessor.php
1 <?php
2
3 /**
4 * PHP version of the callback template processor
5 * This is currently used as a test rig and is likely to be used for
6 * compatibility purposes later, where the C++ extension is not available.
7 */
8
9 define( 'CBT_WHITE', " \t\r\n" );
10 define( 'CBT_BRACE', '{}' );
11 define( 'CBT_DELIM', CBT_WHITE . CBT_BRACE );
12 define( 'CBT_DEBUG', 0 );
13
14 $GLOBALS['cbtExecutingGenerated'] = 0;
15
16 /**
17 * Attempting to be a MediaWiki-independent module
18 */
19 if ( !function_exists( 'wfProfileIn' ) ) {
20 function wfProfileIn() {}
21 }
22 if ( !function_exists( 'wfProfileOut' ) ) {
23 function wfProfileOut() {}
24 }
25
26 /**
27 * Escape text for inclusion in template
28 */
29 function cbt_escape( $text ) {
30 return strtr( $text, array( '{' => '{[}', '}' => '{]}' ) );
31 }
32
33 /**
34 * Create a CBTValue
35 */
36 function cbt_value( $text = '', $deps = array(), $isTemplate = false ) {
37 global $cbtExecutingGenerated;
38 if ( $cbtExecutingGenerated ) {
39 return $text;
40 } else {
41 return new CBTValue( $text, $deps, $isTemplate );
42 }
43 }
44
45 /**
46 * A dependency-tracking value class
47 * Callback functions should return one of these, unless they have
48 * no dependencies in which case they can return a string.
49 */
50 class CBTValue {
51 var $mText, $mDeps, $mIsTemplate;
52
53 /**
54 * Create a new value
55 * @param $text String: , default ''.
56 * @param $deps Array: what this value depends on
57 * @param $isTemplate Bool: whether the result needs compilation/execution, default 'false'.
58 */
59 function CBTValue( $text = '', $deps = array(), $isTemplate = false ) {
60 $this->mText = $text;
61 if ( !is_array( $deps ) ) {
62 $this->mDeps = array( $deps ) ;
63 } else {
64 $this->mDeps = $deps;
65 }
66 $this->mIsTemplate = $isTemplate;
67 }
68
69 /** Concatenate two values, merging their dependencies */
70 function cat( $val ) {
71 if ( is_object( $val ) ) {
72 $this->addDeps( $val );
73 $this->mText .= $val->mText;
74 } else {
75 $this->mText .= $val;
76 }
77 }
78
79 /** Add the dependencies of another value to this one */
80 function addDeps( $values ) {
81 if ( !is_array( $values ) ) {
82 $this->mDeps = array_merge( $this->mDeps, $values->mDeps );
83 } else {
84 foreach ( $values as $val ) {
85 if ( !is_object( $val ) ) {
86 var_dump( debug_backtrace() );
87 exit;
88 }
89 $this->mDeps = array_merge( $this->mDeps, $val->mDeps );
90 }
91 }
92 }
93
94 /** Remove a list of dependencies */
95 function removeDeps( $deps ) {
96 $this->mDeps = array_diff( $this->mDeps, $deps );
97 }
98
99 function setText( $text ) {
100 $this->mText = $text;
101 }
102
103 function getText() {
104 return $this->mText;
105 }
106
107 function getDeps() {
108 return $this->mDeps;
109 }
110
111 /** If the value is a template, execute it */
112 function execute( &$processor ) {
113 if ( $this->mIsTemplate ) {
114 $myProcessor = new CBTProcessor( $this->mText, $processor->mFunctionObj, $processor->mIgnorableDeps );
115 $myProcessor->mCompiling = $processor->mCompiling;
116 $val = $myProcessor->doText( 0, strlen( $this->mText ) );
117 if ( $myProcessor->getLastError() ) {
118 $processor->error( $myProcessor->getLastError() );
119 $this->mText = '';
120 } else {
121 $this->mText = $val->mText;
122 $this->addDeps( $val );
123 }
124 if ( !$processor->mCompiling ) {
125 $this->mIsTemplate = false;
126 }
127 }
128 }
129
130 /** If the value is plain text, escape it for inclusion in a template */
131 function templateEscape() {
132 if ( !$this->mIsTemplate ) {
133 $this->mText = cbt_escape( $this->mText );
134 }
135 }
136
137 /** Return true if the value has no dependencies */
138 function isStatic() {
139 return count( $this->mDeps ) == 0;
140 }
141 }
142
143 /**
144 * Template processor, for compilation and execution
145 */
146 class CBTProcessor {
147 var $mText, # The text being processed
148 $mFunctionObj, # The object containing callback functions
149 $mCompiling = false, # True if compiling to a template, false if executing to text
150 $mIgnorableDeps = array(), # Dependency names which should be treated as static
151 $mFunctionCache = array(), # A cache of function results keyed by argument hash
152 $mLastError = false, # Last error message or false for no error
153 $mErrorPos = 0, # Last error position
154
155 /** Built-in functions */
156 $mBuiltins = array(
157 'if' => 'bi_if',
158 'true' => 'bi_true',
159 '[' => 'bi_lbrace',
160 'lbrace' => 'bi_lbrace',
161 ']' => 'bi_rbrace',
162 'rbrace' => 'bi_rbrace',
163 'escape' => 'bi_escape',
164 '~' => 'bi_escape',
165 );
166
167 /**
168 * Create a template processor for a given text, callback object and static dependency list
169 */
170 function CBTProcessor( $text, $functionObj, $ignorableDeps = array() ) {
171 $this->mText = $text;
172 $this->mFunctionObj = $functionObj;
173 $this->mIgnorableDeps = $ignorableDeps;
174 }
175
176 /**
177 * Execute the template.
178 * If $compile is true, produces an optimised template where functions with static
179 * dependencies have been replaced by their return values.
180 */
181 function execute( $compile = false ) {
182 $fname = 'CBTProcessor::execute';
183 wfProfileIn( $fname );
184 $this->mCompiling = $compile;
185 $this->mLastError = false;
186 $val = $this->doText( 0, strlen( $this->mText ) );
187 $text = $val->getText();
188 if ( $this->mLastError !== false ) {
189 $pos = $this->mErrorPos;
190
191 // Find the line number at which the error occurred
192 $startLine = 0;
193 $endLine = 0;
194 $line = 0;
195 do {
196 if ( $endLine ) {
197 $startLine = $endLine + 1;
198 }
199 $endLine = strpos( $this->mText, "\n", $startLine );
200 ++$line;
201 } while ( $endLine !== false && $endLine < $pos );
202
203 $text = "Template error at line $line: $this->mLastError\n<pre>\n";
204
205 $context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
206 $text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n</pre>\n";
207 }
208 wfProfileOut( $fname );
209 return $text;
210 }
211
212 /** Shortcut for execute(true) */
213 function compile() {
214 $fname = 'CBTProcessor::compile';
215 wfProfileIn( $fname );
216 $s = $this->execute( true );
217 wfProfileOut( $fname );
218 return $s;
219 }
220
221 /** Shortcut for doOpenText( $start, $end, false */
222 function doText( $start, $end ) {
223 return $this->doOpenText( $start, $end, false );
224 }
225
226 /**
227 * Escape text for a template if we are producing a template. Do nothing
228 * if we are producing plain text.
229 */
230 function templateEscape( $text ) {
231 if ( $this->mCompiling ) {
232 return cbt_escape( $text );
233 } else {
234 return $text;
235 }
236 }
237
238 /**
239 * Recursive workhorse for text mode.
240 *
241 * Processes text mode starting from offset $p, until either $end is
242 * reached or a closing brace is found. If $needClosing is false, a
243 * closing brace will flag an error, if $needClosing is true, the lack
244 * of a closing brace will flag an error.
245 *
246 * The parameter $p is advanced to the position after the closing brace,
247 * or after the end. A CBTValue is returned.
248 *
249 * @private
250 */
251 function doOpenText( &$p, $end, $needClosing = true ) {
252 $fname = 'CBTProcessor::doOpenText';
253 wfProfileIn( $fname );
254 $in =& $this->mText;
255 $start = $p;
256 $ret = new CBTValue( '', array(), $this->mCompiling );
257
258 $foundClosing = false;
259 while ( $p < $end ) {
260 $matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
261 $pToken = $p + $matchLength;
262
263 if ( $pToken >= $end ) {
264 // No more braces, output remainder
265 $ret->cat( substr( $in, $p ) );
266 $p = $end;
267 break;
268 }
269
270 // Output the text before the brace
271 $ret->cat( substr( $in, $p, $matchLength ) );
272
273 // Advance the pointer
274 $p = $pToken + 1;
275
276 // Check for closing brace
277 if ( $in[$pToken] == '}' ) {
278 $foundClosing = true;
279 break;
280 }
281
282 // Handle the "{fn}" special case
283 if ( $pToken > 0 && $in[$pToken-1] == '"' ) {
284 wfProfileOut( $fname );
285 $val = $this->doOpenFunction( $p, $end );
286 wfProfileIn( $fname );
287 if ( $p < $end && $in[$p] == '"' ) {
288 $val->setText( htmlspecialchars( $val->getText() ) );
289 }
290 $ret->cat( $val );
291 } else {
292 // Process the function mode component
293 wfProfileOut( $fname );
294 $ret->cat( $this->doOpenFunction( $p, $end ) );
295 wfProfileIn( $fname );
296 }
297 }
298 if ( $foundClosing && !$needClosing ) {
299 $this->error( 'Errant closing brace', $p );
300 } elseif ( !$foundClosing && $needClosing ) {
301 $this->error( 'Unclosed text section', $start );
302 }
303 wfProfileOut( $fname );
304 return $ret;
305 }
306
307 /**
308 * Recursive workhorse for function mode.
309 *
310 * Processes function mode starting from offset $p, until either $end is
311 * reached or a closing brace is found. If $needClosing is false, a
312 * closing brace will flag an error, if $needClosing is true, the lack
313 * of a closing brace will flag an error.
314 *
315 * The parameter $p is advanced to the position after the closing brace,
316 * or after the end. A CBTValue is returned.
317 *
318 * @private
319 */
320 function doOpenFunction( &$p, $end, $needClosing = true ) {
321 $in =& $this->mText;
322 $start = $p;
323 $tokens = array();
324 $unexecutedTokens = array();
325
326 $foundClosing = false;
327 while ( $p < $end ) {
328 $char = $in[$p];
329 if ( $char == '{' ) {
330 // Switch to text mode
331 ++$p;
332 $tokenStart = $p;
333 $token = $this->doOpenText( $p, $end );
334 $tokens[] = $token;
335 $unexecutedTokens[] = '{' . substr( $in, $tokenStart, $p - $tokenStart - 1 ) . '}';
336 } elseif ( $char == '}' ) {
337 // Block end
338 ++$p;
339 $foundClosing = true;
340 break;
341 } elseif ( false !== strpos( CBT_WHITE, $char ) ) {
342 // Whitespace
343 // Consume the rest of the whitespace
344 $p += strspn( $in, CBT_WHITE, $p, $end - $p );
345 } else {
346 // Token, find the end of it
347 $tokenLength = strcspn( $in, CBT_DELIM, $p, $end - $p );
348 $token = new CBTValue( substr( $in, $p, $tokenLength ) );
349 // Execute the token as a function if it's not the function name
350 if ( count( $tokens ) ) {
351 $tokens[] = $this->doFunction( array( $token ), $p );
352 } else {
353 $tokens[] = $token;
354 }
355 $unexecutedTokens[] = $token->getText();
356
357 $p += $tokenLength;
358 }
359 }
360 if ( !$foundClosing && $needClosing ) {
361 $this->error( 'Unclosed function', $start );
362 return '';
363 }
364
365 $val = $this->doFunction( $tokens, $start );
366 if ( $this->mCompiling && !$val->isStatic() ) {
367 $compiled = '';
368 $first = true;
369 foreach( $tokens as $i => $token ) {
370 if ( $first ) {
371 $first = false;
372 } else {
373 $compiled .= ' ';
374 }
375 if ( $token->isStatic() ) {
376 if ( $i !== 0 ) {
377 $compiled .= '{' . $token->getText() . '}';
378 } else {
379 $compiled .= $token->getText();
380 }
381 } else {
382 $compiled .= $unexecutedTokens[$i];
383 }
384 }
385
386 // The dynamic parts of the string are still represented as functions, and
387 // function invocations have no dependencies. Thus the compiled result has
388 // no dependencies.
389 $val = new CBTValue( "{{$compiled}}", array(), true );
390 }
391 return $val;
392 }
393
394 /**
395 * Execute a function, caching and returning the result value.
396 * $tokens is an array of CBTValue objects. $tokens[0] is the function
397 * name, the others are arguments. $p is the string position, and is used
398 * for error messages only.
399 */
400 function doFunction( $tokens, $p ) {
401 if ( count( $tokens ) == 0 ) {
402 return new CBTValue;
403 }
404 $fname = 'CBTProcessor::doFunction';
405 wfProfileIn( $fname );
406
407 $ret = new CBTValue;
408
409 // All functions implicitly depend on their arguments, and the function name
410 // While this is not strictly necessary for all functions, it's true almost
411 // all the time and so convenient to do automatically.
412 $ret->addDeps( $tokens );
413
414 $this->mCurrentPos = $p;
415 $func = array_shift( $tokens );
416 $func = $func->getText();
417
418 // Extract the text component from all the tokens
419 // And convert any templates to plain text
420 $textArgs = array();
421 foreach ( $tokens as $token ) {
422 $token->execute( $this );
423 $textArgs[] = $token->getText();
424 }
425
426 // Try the local cache
427 $cacheKey = $func . "\n" . implode( "\n", $textArgs );
428 if ( isset( $this->mFunctionCache[$cacheKey] ) ) {
429 $val = $this->mFunctionCache[$cacheKey];
430 } elseif ( isset( $this->mBuiltins[$func] ) ) {
431 $func = $this->mBuiltins[$func];
432 $val = call_user_func_array( array( &$this, $func ), $tokens );
433 $this->mFunctionCache[$cacheKey] = $val;
434 } elseif ( method_exists( $this->mFunctionObj, $func ) ) {
435 $profName = get_class( $this->mFunctionObj ) . '::' . $func;
436 wfProfileIn( "$fname-callback" );
437 wfProfileIn( $profName );
438 $val = call_user_func_array( array( &$this->mFunctionObj, $func ), $textArgs );
439 wfProfileOut( $profName );
440 wfProfileOut( "$fname-callback" );
441 $this->mFunctionCache[$cacheKey] = $val;
442 } else {
443 $this->error( "Call of undefined function \"$func\"", $p );
444 $val = new CBTValue;
445 }
446 if ( !is_object( $val ) ) {
447 $val = new CBTValue((string)$val);
448 }
449
450 if ( CBT_DEBUG ) {
451 $unexpanded = $val;
452 }
453
454 // If the output was a template, execute it
455 $val->execute( $this );
456
457 if ( $this->mCompiling ) {
458 // Escape any braces so that the output will be a valid template
459 $val->templateEscape();
460 }
461 $val->removeDeps( $this->mIgnorableDeps );
462 $ret->addDeps( $val );
463 $ret->setText( $val->getText() );
464
465 if ( CBT_DEBUG ) {
466 wfDebug( "doFunction $func args = "
467 . var_export( $tokens, true )
468 . "unexpanded return = "
469 . var_export( $unexpanded, true )
470 . "expanded return = "
471 . var_export( $ret, true )
472 );
473 }
474
475 wfProfileOut( $fname );
476 return $ret;
477 }
478
479 /**
480 * Set a flag indicating that an error has been found.
481 */
482 function error( $text, $pos = false ) {
483 $this->mLastError = $text;
484 if ( $pos === false ) {
485 $this->mErrorPos = $this->mCurrentPos;
486 } else {
487 $this->mErrorPos = $pos;
488 }
489 }
490
491 function getLastError() {
492 return $this->mLastError;
493 }
494
495 /** 'if' built-in function */
496 function bi_if( $condition, $trueBlock, $falseBlock = null ) {
497 if ( is_null( $condition ) ) {
498 $this->error( "Missing condition in if" );
499 return '';
500 }
501
502 if ( $condition->getText() != '' ) {
503 return new CBTValue( $trueBlock->getText(),
504 array_merge( $condition->getDeps(), $trueBlock->getDeps() ),
505 $trueBlock->mIsTemplate );
506 } else {
507 if ( !is_null( $falseBlock ) ) {
508 return new CBTValue( $falseBlock->getText(),
509 array_merge( $condition->getDeps(), $falseBlock->getDeps() ),
510 $falseBlock->mIsTemplate );
511 } else {
512 return new CBTValue( '', $condition->getDeps() );
513 }
514 }
515 }
516
517 /** 'true' built-in function */
518 function bi_true() {
519 return "true";
520 }
521
522 /** left brace built-in */
523 function bi_lbrace() {
524 return '{';
525 }
526
527 /** right brace built-in */
528 function bi_rbrace() {
529 return '}';
530 }
531
532 /**
533 * escape built-in.
534 * Escape text for inclusion in an HTML attribute
535 */
536 function bi_escape( $val ) {
537 return new CBTValue( htmlspecialchars( $val->getText() ), $val->getDeps() );
538 }
539 }
540